游戏物品模块
GameItems模块主要负责游戏内所有物品的管理,包括但不限于:
核心功能特点:
该模块为游戏内经济系统和玩家进度提供基础支持,是连接游戏多个子系统的核心模块。
游戏物品模块采用关系型数据库设计,通过多个相互关联的数据表实现物品管理、用户物品关联、属性存储和宝箱配置等功能。数据结构设计遵循以下原则:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 分类ID,主键 |
| name | varchar | 分类名称 |
| code | varchar | 分类编码(唯一) |
| icon | varchar | 分类图标 |
| sort | int | 排序权重 |
| parent_id | int | 父分类ID(可为空,用于实现分类层级) |
| created_at | timestamp | 创建时间 |
| updated_at | timestamp | 更新时间 |
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 物品ID,主键 |
| name | varchar | 物品名称 |
| description | text | 物品描述 |
| category_id | int | 物品分类ID,外键关联item_categories表 |
| type | tinyint | 物品类型(用于区分可用操作,1:可使用, 2:可装备, 3:可合成, 4:可交任务, 5:可开启...) |
| is_unique | tinyint | 是否是单独属性物品(0:否,默认, 1:是) |
| rarity | tinyint | 稀有度(1:普通, 2:稀有, 3:史诗, 4:传说...) |
| icon | varchar | 物品图标路径 |
| max_stack | int | 最大堆叠数量 |
| sell_price | int | 出售价格 |
| default_expire_seconds | int | 玩家获取物品后的默认有效秒数(0表示永久有效) |
| display_attributes | json | 展示属性,以JSON格式存储键值对,用于界面展示和描述的属性 |
| numeric_attributes | json | 数值属性,以JSON格式存储键值对,用于计算和游戏逻辑的属性(宝箱物品可存储min_drop_count和max_drop_count) |
| global_expire_at | timestamp | 物品全局过期时间(可为空) |
| created_at | timestamp | 创建时间 |
| updated_at | timestamp | 更新时间 |
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 唯一物品ID,主键 |
| item_id | int | 关联的基础物品ID,外键关联items表 |
| name | varchar | 物品名称(可以与基础物品不同,如“锐利的钢刀”) |
| display_attributes | json | 展示属性,以JSON格式存储键值对,用于界面展示和描述的属性 |
| numeric_attributes | json | 数值属性,以JSON格式存储键值对,用于计算和游戏逻辑的属性 |
| expire_at | timestamp | 物品过期时间(可为空) |
| created_at | timestamp | 创建时间 |
| updated_at | timestamp | 更新时间 |
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 记录ID,主键 |
| user_id | int | 用户ID,外键 |
| item_id | int | 统一属性物品ID,外键关联items表(始终有值) |
| instance_id | int | 单独属性物品ID,外键关联item_instances表(可为空) |
| quantity | int | 数量(对于单独属性物品,该值始终为1) |
| expire_at | timestamp | 用户物品过期时间(可为空) |
| created_at | timestamp | 获取时间 |
| updated_at | timestamp | 更新时间 |
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 物品组ID,主键 |
| name | varchar | 物品组名称 |
| code | varchar | 物品组编码(唯一) |
| description | text | 物品组描述 |
| created_at | timestamp | 创建时间 |
| updated_at | timestamp | 更新时间 |
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 记录ID,主键 |
| group_id | int | 物品组ID,外键关联item_groups表 |
| item_id | int | 物品ID,外键关联items表 |
| weight | int | 权重,决定从物品组中选择该物品的概率 |
| created_at | timestamp | 创建时间 |
| updated_at | timestamp | 更新时间 |
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 记录ID,主键 |
| chest_id | int | 宝箱物品ID,外键关联items表 |
| item_id | int | 可能获得的物品ID(与group_id二选一) |
| group_id | int | 物品组ID,外键关联item_groups表(与item_id二选一) |
| is_unique | tinyint | 是否生成单独属性物品(0:否, 1:是) |
| min_quantity | int | 最小数量 |
| max_quantity | int | 最大数量 |
| weight | int | 权重,决定获取概率 |
| allow_duplicate | tinyint | 是否允许在同一宝箱中重复掉落(0:不允许, 1:允许) |
| pity_count | int | 保底次数,当玩家连续未获得该内容达到次数后必定获得(0表示不启用保底) |
| pity_weight_factor | float | 保底权重因子,用于递增概率计算(默认2.0) |
| created_at | timestamp | 创建时间 |
| updated_at | timestamp | 更新时间 |
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 记录ID,主键 |
| user_id | int | 用户ID |
| chest_id | int | 宝箱ID,外键关联items表 |
| chest_content_id | int | 宝箱内容ID,外键关联item_chest_contents表 |
| current_count | int | 当前计数,每开启一次宝箱增加1 |
| last_reset_time | timestamp | 上次重置时间(可用于周期性重置) |
| created_at | timestamp | 创建时间 |
| updated_at | timestamp | 更新时间 |
为了确保系统的高性能运行,我们对关键字段进行了索引设计:
| 表名 | 索引字段 | 索引类型 | 说明 |
|---|---|---|---|
| items | id | 主键 | 物品ID主键索引 |
| items | category_id | 普通索引 | 加速按物品分类查询 |
| item_categories | id | 主键 | 分类ID主键索引 |
| item_categories | parent_id | 普通索引 | 加速查询子分类 |
| item_categories | code | 唯一索引 | 确保分类编码唯一性 |
| items | type | 普通索引 | 加速按物品类型查询 |
| items | is_unique | 普通索引 | 加速查询单独属性物品 |
| items | global_expire_at | 普通索引 | 加速过期物品查询 |
| item_instances | id | 主键 | 唯一物品ID主键索引 |
| item_instances | item_id | 普通索引 | 加速根据基础物品查询唯一物品 |
| item_instances | expire_at | 普通索引 | 加速过期物品查询 |
| item_users | user_id, item_id | 复合索引 | 加速用户统一属性物品查询 |
| item_users | user_id, instance_id | 复合索引 | 加速用户单独属性物品查询 |
| item_users | expire_at | 普通索引 | 加速过期物品查询 |
| item_groups | id | 主键 | 物品组ID主键索引 |
| item_groups | code | 唯一索引 | 确保物品组编码唯一性 |
| item_group_items | group_id | 普通索引 | 加速查询物品组内容 |
| item_group_items | item_id | 普通索引 | 加速查询物品所属物品组 |
| item_chest_contents | chest_id | 普通索引 | 加速宝箱内容查询 |
| item_chest_contents | item_id | 普通索引 | 加速物品在宝箱中的查询 |
| item_chest_contents | group_id | 普通索引 | 加速查询物品组在宝箱中的配置 |
| item_chest_contents | is_unique | 普通索引 | 加速查询生成唯一物品的宝箱配置 |
| item_chest_contents | pity_count | 普通索引 | 加速查询启用保底的宝箱内容 |
| item_pity_times | user_id, chest_id, chest_content_id | 复合索引 | 加速查询用户对特定宝箱内容的保底计数 |
以下是数据模型之间的关联关系图:
graph TD
User["User"] --- UserItems{"item_users<br>M:N"}
UserItems --- Item["Item<br>统一属性物品"]
UserItems --- InstanceItem["item_instances<br>单独属性物品"]
InstanceItem -->|"N:1"| Item
Category["item_categories<br>物品分类"] -->|"1:N"| Item
Category -->|"1:N"| Category
ItemGroup["item_groups<br>物品组"] --- GroupItems{"item_group_items<br>物品组内容"}
GroupItems -->|"N:1"| Item
subgraph "宝箱系统"
ChestItem["Item<br>(宝箱分类)"] -->|"1:N"| ChestConfig["item_chest_contents<br>宝箱配置"]
ChestConfig -->|"item_id"| ItemDrop["Item<br>掉落物品"]
ChestConfig -->|"group_id"| ItemGroup
ChestConfig -->|"is_unique=1"| InstanceItem
ChestConfig -->|"pity_count>0"| HasPity["(启用保底)"]
User --- PityCounter{"item_pity_times<br>用户宝箱内容保底计数"}
PityCounter -->|"N:1"| ChestConfig
end
classDef entity fill:#f9f,stroke:#333,stroke-width:2px;
classDef junction fill:#bbf,stroke:#33f,stroke-width:2px;
classDef virtual fill:#dfd,stroke:#383,stroke-width:1px;
class User,Item,InstanceItem,ChestItem,ItemDrop,Category,ItemGroup entity;
class UserItems,ChestConfig,PityCounter,GroupItems junction;
class HasPity virtual;
图表说明:
为了支持“每个用户拥有的同类物品可以有不同属性”的需求(如不同的刀有不同的属性),我们采用了两种物品类型的设计:
该设计允许我们实现以下功能:
物品类型分离
item_categories表实现物品分类的层级结构,支持无限层级的分类items表存储统一属性物品,属性以JSON格式存储,并关联到分类items表中的is_unique字段标记该物品是否是单独属性物品item_instances表存储单独属性物品,属性以JSON格式存储用户物品关联的灵活性
item_users表中的item_id始终有值,而instance_id字段可以为空quantity始终为1宝箱生成机制
item_chest_contents表中的is_unique字段控制生成类型items表中is_unique为1的物品才能生成单独属性物品实例单独属性物品创建流程
is_unique是否为1用户单独属性物品查询
统一属性物品添加逻辑
在涉及多表操作的关键业务逻辑中,使用数据库事务确保数据一致性:
获取用户物品
使用物品
添加物品到用户背包
检查物品有效性
打开宝箱
带保底机制的宝箱开启
创建单独属性物品
获取用户单独属性物品
物品模块支持两种过期时间机制:
全局过期时间定义在items表的global_expire_at字段中,表示该物品在所有用户中的绝对过期时间。当超过这个时间点后,所有用户的该物品均失效。
应用场景:
用户物品过期时间定义在item_users表的expire_at字段中,表示特定用户的特定物品的过期时间。这允许每个用户的同一物品有不同的过期时间。
应用场景:
系统应定期运行任务检查并处理过期物品:
过期物品处理流程
全局过期物品处理
用户物品过期处理
宝箱可以根据稀有度和内容分为不同类型:
每个宝箱可以同时掉落多个物品:
numeric_attributes字段中定义了min_drop_count和max_drop_count属性,而不是使用独立字段item_chest_contents表中的allow_duplicate字段控制物品是否可以在同一宝箱中重复掉落宝箱内容的概率由item_chest_contents表中的weight字段决定:
为了确保玩家在多次开启宝箱后能获得稀有物品,系统实现了保底机制:
保底机制直接集成在宝箱内容配置中,具有以下优势:
内容级别保底:每个宝箱内容可以设置自己的保底次数,当玩家连续未获得该内容达到指定次数后,必定获得该内容
灵活的概率调整:通过 pity_weight_factor 字段控制递增概率的幅度,可以为不同内容设置不同的递增策略
简化的数据结构:不需要额外的保底机制表和关联表,直接在宝箱内容表中定义保底机制
保底机制的实现方式有:
确定保底:当玩家开启指定数量宝箱后,必定获得特定宝箱内容
递增概率:每次未获得目标内容时,增加下次获得的概率
多内容保底:为宝箱中的不同内容设置不同的保底计数
以下是保底机制的实现代码示例:
保底机制实现流程
获取宝箱内容配置
获取用户保底计数
检查是否触发保底
选择保底内容
选择其他物品
除了确定保底外,还可以实现递增概率的保底机制:
递增概率计算方式
在 item_chest_contents 表中可以定义递增概率的计算公式参数:
这种方式的工作原理:
这种设计可以确保玩家在一定次数内获得特定内容,同时保持一定的随机性和惊喜感。
每次宝箱开启都应记录详细日志,包括:
这些日志可用于数据分析和问题排查。
当玩家多次获得同一物品时,系统需要根据物品类型和过期时间进行不同的处理:
对于统一属性物品(如消耗品、材料等),当过期时间相同时,采用数量累加策略:
// 为用户添加统一属性物品
public function addItemToUser($userId, $itemId, $quantity = 1, $expireAt = null)
{
$item = Item::find($itemId);
// 如果未指定过期时间且物品有默认过期秒数,则计算过期时间
if ($expireAt === null && $item->default_expire_seconds > 0) {
$expireAt = now()->addSeconds($item->default_expire_seconds);
}
// 检查用户是否已有该物品且过期时间相同
$userItem = UserItem::where('user_id', $userId)
->where('item_id', $itemId)
->whereNull('unique_item_id') // 确保是统一属性物品
->where(function($query) use ($expireAt) {
if ($expireAt === null) {
$query->whereNull('expire_at');
} else {
$query->where('expire_at', $expireAt);
}
})
->first();
if ($userItem) {
// 已有物品,数量累加
$userItem->quantity += $quantity;
// 检查是否超过最大堆叠数量
if ($item->max_stack > 0 && $userItem->quantity > $item->max_stack) {
// 如果超过最大堆叠数量,创建新的堆叠
$excessQuantity = $userItem->quantity - $item->max_stack;
$userItem->quantity = $item->max_stack;
$userItem->save();
// 递归调用自身添加超出的数量
$this->addItemToUser($userId, $itemId, $excessQuantity, $expireAt);
} else {
$userItem->save();
}
} else {
// 创建新记录
$userItem = new UserItem();
$userItem->user_id = $userId;
$userItem->item_id = $itemId;
$userItem->quantity = min($quantity, $item->max_stack > 0 ? $item->max_stack : $quantity);
$userItem->expire_at = $expireAt;
$userItem->save();
// 如果数量超过最大堆叠数量,递归创建新堆叠
if ($item->max_stack > 0 && $quantity > $item->max_stack) {
$this->addItemToUser($userId, $itemId, $quantity - $item->max_stack, $expireAt);
}
}
return $userItem;
}
对于有过期时间的统一属性物品,按照过期时间分组存储:
这样可以确保物品按照正确的过期顺序被使用或过期。在使用物品时,优先使用即将过期的物品。
// 使用物品时,优先使用即将过期的物品
public function useItem($userId, $itemId, $quantity = 1)
{
// 获取用户的该物品,按过期时间升序排序(先使用即将过期的)
$userItems = UserItem::where('user_id', $userId)
->where('item_id', $itemId)
->whereNull('unique_item_id')
->where(function($query) {
$query->whereNull('expire_at')
->orWhere('expire_at', '>', now());
})
->orderBy('expire_at', 'asc')
->orderBy('created_at', 'asc')
->get();
// 检查总数量是否足够
$totalQuantity = $userItems->sum('quantity');
if ($totalQuantity < $quantity) {
return [
'success' => false,
'message' => '物品数量不足'
];
}
// 开始事务
DB::beginTransaction();
try {
$remainingToUse = $quantity;
foreach ($userItems as $userItem) {
if ($remainingToUse <= 0) break;
if ($userItem->quantity <= $remainingToUse) {
// 如果当前记录的数量小于等于需要使用的数量,全部使用并删除记录
$remainingToUse -= $userItem->quantity;
$userItem->delete();
} else {
// 如果当前记录的数量大于需要使用的数量,减少数量
$userItem->quantity -= $remainingToUse;
$userItem->save();
$remainingToUse = 0;
}
}
DB::commit();
return [
'success' => true,
'message' => '物品使用成功',
'data' => [
'used_quantity' => $quantity,
'remaining_quantity' => $totalQuantity - $quantity
]
];
} catch (\Exception $e) {
DB::rollBack();
return [
'success' => false,
'message' => '物品使用失败: ' . $e->getMessage()
];
}
}
对于单独属性物品(如装备、宠物等),每次获取都创建新记录,即使是相同物品:
item_instances表中有一条记录item_users表中的quantity始终为1这种设计允许玩家拥有多个相同类型但属性不同的物品,比如多把强化等级不同的武器。