インベントリはRobloxにおけるRPGやサバイバルゲームの基盤です。設計が甘いとアイテム増殖バグや同期ズレ、モバイルでのドラッグ遅延が発生し、経済が破綻します。
サーバー権限ステートマシン、複合データ構造、ProfileServiceによるセッションロックを導入することで、数千人の同時接続でも安全で軽快なシステムを構築できます。
1. インベントリ設計モデル: 固定スロット vs グリッド vs 重量制限
ゲームデザインに応じた最適な構造の選定:
- 固定スロット型: 配列で管理するホットバー形式(例: 20枠)。O(1)アクセスと直感的なシリアライズが可能。
- 空間グリッド型(テトリス式): 複数マスを占有するDiablo式配置。2Dビットマスクでの衝突判定が必要。
- 重量・過積載制限: 各アイテムの重量を合算し、上限超過時に移動速度低下ペナルティを付与。
2. サーバー権限検証と楽観的クライアントUI
セキュリティを担保しながら即時レスポンスを実現する手法:
- 楽観的UI予測: ドラッグ時にサーバー応答を待たず即座にアイコンを移動させて描画。
- サーバーでの検証: 所有権、スロット範囲、重量予算、クールダウンをチェックして確定。
- 自動ロールバック: 不正な移動や拒否された操作は元の位置へ瞬時に巻き戻し。
3. 複合データ構造とスタック直列化の最適化
Luauにおける静的データと動的データの分離:
- ベース辞書と個別インスタンス: 名前や重量はReplicatedStorageに置き、個別UUIDや耐久度はプレイヤー側に保持。
- スタック統合計算:
math.min(maxStack - target.Count, source.Count)で安全に数量を合算。 - DataStore保存圧縮: 空きスロットを除外して4MB制限を余裕を持って遵守。
ServerScriptService.Inventory.InventoryManager
-- サーバー権限インベントリマネージャー
local InventoryManager = {}
InventoryManager.__index = InventoryManager
local ItemDatabase = require(game.ReplicatedStorage.ItemDatabase)
function InventoryManager.new(player, maxSlots, maxWeight)
local self = setmetatable({}, InventoryManager)
self.Player = player
self.MaxSlots = maxSlots or 30
self.MaxWeight = maxWeight or 100
self.Slots = table.create(self.MaxSlots, false)
self.CurrentWeight = 0
self.IsLocked = false
return self
end
function InventoryManager:AddItem(itemId, amount)
if self.IsLocked then return false, "INVENTORY_LOCKED" end
local itemData = ItemDatabase[itemId]
if not itemData then return false, "INVALID_ITEM" end
local addedWeight = itemData.Weight * amount
if self.CurrentWeight + addedWeight > self.MaxWeight then
return false, "OVER_ENCUMBERED"
end
for slotIndex = 1, self.MaxSlots do
local slot = self.Slots[slotIndex]
if slot and slot.Id == itemId and slot.Count < itemData.MaxStack then
local space = itemData.MaxStack - slot.Count
local toAdd = math.min(space, amount)
slot.Count = slot.Count + toAdd
amount = amount - toAdd
self.CurrentWeight = self.CurrentWeight + (itemData.Weight * toAdd)
if amount <= 0 then return true end
end
end
for slotIndex = 1, self.MaxSlots do
if not self.Slots[slotIndex] then
self.Slots[slotIndex] = {
UUID = game:GetService("HttpService"):GenerateGUID(false),
Id = itemId,
Count = amount,
Durability = itemData.MaxDurability
}
self.CurrentWeight = self.CurrentWeight + (itemData.Weight * amount)
return true
end
end
return false, "INVENTORY_FULL"
end
return InventoryManager
4. 増殖バグ(Dupe)対策とトランザクションロック
切断バグやトレード詐欺を根絶する仕組み:
- MemoryStoreセッションロック: アイテム移動中に一時キーを取得して二重消費を防止。
- 原子的トレード実行: 単一のサーバーフレーム内で両者のインベントリを同時に更新。
- リクエスト頻度制御: 100msのクールダウンを設けてパケット送信攻撃を遮断。
5. モバイル対応ドラッグ&ドロップとタッチUX
スマートフォンやタブレットでの快適な操作性:
- 最低44pxのタッチ領域: UIScaleを用いて高解像度端末でも誤タップを防止。
- 長押しコンテキストメニュー: 右クリックのない環境向けに長押しでラジアルメニューを表示。
- 触覚(ハプティクス)連携: HapticServiceを利用して装着時に小気味よい振動を伝達。
Frequently Asked Questions
なぜインベントリ処理はサーバー上で行わなければならないのですか?
クライアント側に計算を任せると偽アイテムの生成や増殖チートが発生します。サーバー検証が必須です。
楽観的UIはどのように操作遅延を解消しますか?
操作と同時にローカルで先行描画し、サーバーから拒否された場合のみ巻き戻すことで遅延を感じさせません。
セッションロックの役割は何ですか?
複数サーバーから同一プレイヤーのデータが同時に書き換えられるのを防ぎ、切断を利用した増殖を防止します。
モバイルでのドラッグ誤動作を防ぐには?
移動中は軽量な仮アイコンのみを追従させ、指を離した瞬間にのみ配置判定を行います。