Розробка системи інвентарю мобільної гри

TRUETECH займається розробкою, підтримкою та обслуговуванням мобільних додатків iOS, Android, PWA. Маємо великий досвід та експертизу для публікації мобільних додатків до популярних маркетів Google Play, App Store, Amazon, AppGallery та інші.

Розробка та підтримка будь-яких видів мобільних додатків:

Інформаційні та розважальні мобільні програми
Новинки, ігри, довідники, онлайн-каталоги, погодні, фітнес та здоров'я, туристичні, освітні, соціальні мережі та месенджери, квіз, блоги та подкасти, форуми, агрегатори
Мобільні програми електронної комерції
Інтернет-магазини, B2B-додатки, маркетплейси, онлайн-обмінники, кешбек-сервіси, біржі, дропшиппінг-платформи, програми лояльності, доставка їжі та товарів, платіжні системи
Мобільні програми для управління бізнес-процесами
CRM-системи, ERP-системи, управління проектами, інструменти для команди продажів, облік фінансів, управління виробництвом, логістика та доставка, управління персоналом, системи моніторингу даних
Мобільні програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, платформи надання електронних послуг, платформи кешбеку, відеохостинги, тематичні портали, платформи онлайн-бронювання та запису, платформи онлайн-торгівлі

Це лише деякі з типів мобільних додатків, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 1735 послуг
Розробка системи інвентарю мобільної гри
Середній
~3-5 днів
Часті запитання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_mobile-applications_feedme_467_0.webp
    Розробка мобільного додатка для компанії FEEDME
    792
  • image_mobile-applications_xoomer_471_0.webp
    Розробка мобільного додатку для компанії XOOMER
    671
  • image_mobile-applications_rhl_428_0.webp
    Розробка мобільного додатку для компанії RHL
    1097
  • image_mobile-applications_zippy_411_0.webp
    Розробка мобільного додатку для компанії ZIPPY
    969
  • image_mobile-applications_affhome_429_0.webp
    Розробка мобільного додатку для компанії Affhome
    914
  • image_mobile-applications_flavors_409_0.webp
    Розробка мобільного додатку для компанії FLAVORS
    495

Розробка системи інвентаря мобільної гри

Інвентар — це не просто список предметів. Це трансакційна система з бізнес-правилами: неможна витратити більше монет, ніж є; неможна взяти два унікальних предмети; стак повинен корректно об'єднуватися. Помилки тут коштують грошей: дубльовані предмети через race condition, від'ємна валюта через несинхронізовані запити — це реальні баги у production.

Структура даних інвентаря

@Entity(tableName = "inventory_items")
data class InventoryItemEntity(
    @PrimaryKey val instanceId: String,    // унікальний ID кожного предмету
    val playerId: String,
    val itemDefinitionId: String,          // посилання на шаблон предмету
    val quantity: Int,                     // для стакируємих
    val durability: Int? = null,           // для предметів з міцністю
    val enchantments: String = "[]",       // JSON array додаткових властивостей
    val acquiredAt: Long,
    val slotIndex: Int? = null             // для екіпірованих предметів
)

// Визначення предмету (статичні дані — завантажуються з assets)
data class ItemDefinition(
    val id: String,
    val name: String,
    val type: ItemType,                    // WEAPON, ARMOR, CONSUMABLE, CURRENCY
    val isStackable: Boolean,
    val maxStackSize: Int = 1,
    val isUnique: Boolean = false,         // неможна мати більше одного
    val maxQuantity: Int = Int.MAX_VALUE
)

Розділення на InstanceData (те, що унікально для кожного предмету у гравця) та ItemDefinition (шаблон предмету) — класичний паттерн. Завантажуємо шаблони з JSON в assets при старті, не кладемо у БД — вони не змінюються в runtime.

Трансакційні операції

Race condition при поповненні гаманця — класична проблема. Два паралельних запити «додати 100 монет» обидва читають поточне значення 500, обидва пишуть 600. Повинно бути 700.

@Dao
interface InventoryDao {
    // Атомарне додавання до стеку — не читаємо й не пишемо окремо
    @Query("""
        UPDATE inventory_items
        SET quantity = MIN(quantity + :amount, :maxStackSize)
        WHERE instance_id = :instanceId AND player_id = :playerId
    """)
    suspend fun incrementQuantity(instanceId: String, playerId: String,
                                   amount: Int, maxStackSize: Int): Int

    // Атомарне списання з перевіркою — не дає йти у мінус
    @Query("""
        UPDATE inventory_items
        SET quantity = quantity - :amount
        WHERE instance_id = :instanceId AND player_id = :playerId
          AND quantity >= :amount
    """)
    suspend fun decrementQuantity(instanceId: String, playerId: String, amount: Int): Int
    // Повертає кількість оновлених рядків — якщо 0, значит не вистачило

    @Transaction
    suspend fun transferItem(fromPlayerId: String, toPlayerId: String,
                              instanceId: String): Boolean {
        val updated = updateOwner(instanceId, fromPlayerId, toPlayerId)
        return updated > 0
    }
}

decrementQuantity повертає кількість затронутих рядків. Якщо 0 — операція не прошла через нестачу ресурсів. Жодного read-check-write — одна атомарна SQL-операція.

Серверна валідація

Локальний інвентар — для відображення. Все, що касається реальної монетизації (покупки, видатки gems, отримання за реальні гроші), обов'язково проходить валідацію на сервері:

class InventoryRepository(
    private val localDao: InventoryDao,
    private val api: InventoryApi
) {
    suspend fun spendGems(amount: Int, reason: String): Result<Unit> {
        return try {
            // Сервер перевіряє баланс, списує, повертає нов стан
            val serverState = api.spendGems(SpendGemsRequest(amount, reason))

            // Синхронізуємо локальне стан з сервером
            localDao.updateCurrencyBalance(
                playerId = serverState.playerId,
                gems = serverState.newGemsBalance
            )
            Result.success(Unit)
        } catch (e: InsufficientFundsException) {
            Result.failure(e)
        }
    }
}

Оптимістичний update на клієнті з rollback при помилці — тільки для некритичних операцій. Для монетизації — завжди server-first.

Сортування та фільтрація

Інвентар із 500 предметів неможна тримати весь у пам'яті та фільтрувати на клієнті. Room Paging 3:

@Dao
interface InventoryDao {
    @Query("""
        SELECT * FROM inventory_items
        WHERE player_id = :playerId
          AND (:typeFilter IS NULL OR item_type = :typeFilter)
          AND (:searchQuery IS NULL OR item_name LIKE '%' || :searchQuery || '%')
        ORDER BY
          CASE :sortBy WHEN 'rarity' THEN rarity_value ELSE acquired_at END DESC
    """)
    fun pagingSource(playerId: String, typeFilter: String?, searchQuery: String?,
                     sortBy: String): PagingSource<Int, InventoryItemEntity>
}

// ViewModel
val inventoryItems = Pager(PagingConfig(pageSize = 20)) {
    dao.pagingSource(playerId, selectedType, searchQuery, sortBy)
}.flow.cachedIn(viewModelScope)

Paging 3 завантажує по 20 предметів при скролі — жодного лага при великому інвентарі.

Drag-and-drop перестановка слотів

У Jetpack Compose через reorderable бібліотеку (burnoutcrew/reorderable) або через detectDragGesturesAfterLongPress. Ключовий момент — застосовувати новий порядок оптимістично у UI одразу, а у БД — батчем після drop:

fun onItemDropped(fromIndex: Int, toIndex: Int) {
    // Оптимістично оновлюємо список у пам'яті
    val newList = _inventoryState.value.toMutableList().apply {
        add(toIndex, removeAt(fromIndex))
    }
    _inventoryState.value = newList

    // Зберігаємо новий порядок у БД батчем
    viewModelScope.launch(Dispatchers.IO) {
        dao.updateSlotIndices(newList.mapIndexed { index, item ->
            SlotUpdate(item.instanceId, index)
        })
    }
}

Розробка системи інвентаря з трансакційними операціями, серверною валідацією та Paging: 2–4 тижні. Вартість рассчитывается індивідуально.