README.md 26 KB

GameItems模块

游戏物品模块

1. 模块功能和目的

GameItems模块主要负责游戏内所有物品的管理,包括但不限于:

  • 物品基础属性管理(名称、描述、图标、类型等)
  • 物品获取与消耗逻辑
  • 物品库存管理
  • 物品交易系统
  • 特殊物品效果实现
  • 宝箱类物品管理与开启机制
  • 物品过期时间管理(全局过期和用户特定过期)

核心功能特点:

  1. 多物品宝箱系统:支持一个宝箱同时掉落多个物品,并可配置每个物品的数量范围和概率
  2. 灵活的过期机制:支持全局过期时间和用户特定过期时间,适应不同的游戏需求
  3. 完善的属性系统:通过键值对存储物品属性,支持复杂的物品效果实现

该模块为游戏内经济系统和玩家进度提供基础支持,是连接游戏多个子系统的核心模块。

2. 数据结构设计

游戏物品模块采用关系型数据库设计,通过多个相互关联的数据表实现物品管理、用户物品关联、属性存储和宝箱配置等功能。数据结构设计遵循以下原则:

  1. 模块化:各个表结构清晰,职责单一
  2. 扩展性:支持通过属性表扩展物品特性,无需修改数据库结构
  3. 性能优化:合理的索引设计和关联关系

2.1 数据表设计

item_categories 表(物品分类)

字段名 类型 说明
id int 分类ID,主键
name varchar 分类名称
code varchar 分类编码(唯一)
icon varchar 分类图标
sort int 排序权重
parent_id int 父分类ID(可为空,用于实现分类层级)
created_at timestamp 创建时间
updated_at timestamp 更新时间

items 表(统一属性物品)

字段名 类型 说明
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 更新时间

item_instances 表(单独属性物品)

字段名 类型 说明
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 更新时间

item_users 表(用户物品关联)

字段名 类型 说明
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 更新时间

item_groups 表(物品组)

字段名 类型 说明
id int 物品组ID,主键
name varchar 物品组名称
code varchar 物品组编码(唯一)
description text 物品组描述
created_at timestamp 创建时间
updated_at timestamp 更新时间

item_group_items 表(物品组内容)

字段名 类型 说明
id int 记录ID,主键
group_id int 物品组ID,外键关联item_groups表
item_id int 物品ID,外键关联items表
weight int 权重,决定从物品组中选择该物品的概率
created_at timestamp 创建时间
updated_at timestamp 更新时间

item_chest_contents 表(宝箱内容配置)

字段名 类型 说明
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 更新时间

item_pity_times 表(用户宝箱内容保底计数)

字段名 类型 说明
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 更新时间

2.2 数据库索引设计

为了确保系统的高性能运行,我们对关键字段进行了索引设计:

表名 索引字段 索引类型 说明
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 | 复合索引 | 加速查询用户对特定宝箱内容的保底计数 |

2.3 可视图的方式

以下是数据模型之间的关联关系图:

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;

图表说明:

  • User 与 Item/InstanceItem:用户可以拥有统一属性物品和单独属性物品,通过 item_users 表关联
  • InstanceItem 与 Item:多对一关系,每个单独属性物品都关联到一个基础物品模板
  • Category 与 Item:一对多关系,每个物品属于一个分类
  • Category 与 Category:一对多关系,实现分类的层级结构
  • ItemGroup 与 Item:多对多关系,通过 item_group_items 表实现物品组与物品的关联
  • 宝箱物品与掉落物品:属于宝箱分类的Item与 item_chest_contents 表是一对多关系
  • item_chest_contents 与物品/物品组:可以配置单个物品或物品组作为奖励,并可以生成单独属性物品(通过is_unique字段控制)
  • 宝箱内容保底:宝箱内容配置通过 pity_count 字段直接定义保底次数
  • 用户与宝箱内容保底:用户通过 item_pity_times 表记录对每个宝箱内容的尝试次数

2.4 单独属性物品实现

为了支持“每个用户拥有的同类物品可以有不同属性”的需求(如不同的刀有不同的属性),我们采用了两种物品类型的设计:

实现方式说明

该设计允许我们实现以下功能:

  1. 物品类型分离

    • item_categories表实现物品分类的层级结构,支持无限层级的分类
    • items表存储统一属性物品,属性以JSON格式存储,并关联到分类
    • items表中的is_unique字段标记该物品是否是单独属性物品
    • item_instances表存储单独属性物品,属性以JSON格式存储
    • 每个单独属性物品都关联到一个基础物品模板
  2. 用户物品关联的灵活性

    • 用户可以同时拥有统一属性物品和单独属性物品
    • item_users表中的item_id始终有值,而instance_id字段可以为空
    • 对于单独属性物品,quantity始终为1
  3. 宝箱生成机制

    • 宝箱可以配置生成统一属性物品或单独属性物品
    • 通过item_chest_contents表中的is_unique字段控制生成类型
    • 只有items表中is_unique为1的物品才能生成单独属性物品实例

实现要点

  1. 单独属性物品创建流程

    • 获取基础物品信息,并检查is_unique是否为1
    • 生成随机属性(展示属性和数值属性)
    • 创建单独属性物品实例记录
    • 将物品实例关联到用户
  2. 用户单独属性物品查询

    • 支持按基础物品类型过滤
    • 返回包含完整属性信息的物品数据
  3. 统一属性物品添加逻辑

    • 检查用户是否已有该物品
    • 如果已有,增加数量
    • 如果没有,创建新记录

2.5 数据库事务

在涉及多表操作的关键业务逻辑中,使用数据库事务确保数据一致性:

  1. 宝箱开启操作:包含宝箱扣除和物品添加的原子性操作
  2. 物品交易:确保物品转移的原子性
  3. 批量物品操作:如批量删除过期物品
  4. 单独属性物品创建:包含创建 item_instances 记录和关联到用户的原子性操作
  5. 宝箱生成单独属性物品:当宝箱配置为生成单独属性物品时,需要保证创建和关联的原子性

3. 与其他模块的交互

3.1 与用户模块交互

  • 用户模块通过GameItems提供的服务接口获取用户拥有的物品
  • 用户等级提升时,可通过GameItems模块发放奖励物品

3.2 与商店模块交互

  • 商店模块调用GameItems的接口进行物品购买和出售
  • 商店模块从GameItems获取物品基础信息用于展示

3.3 与任务模块交互

  • 任务完成时,通过GameItems模块发放奖励物品
  • 任务进度可能需要检查用户是否拥有特定物品或数量

3.4 与战斗模块交互

  • 战斗中使用消耗品时,调用GameItems模块的物品使用接口
  • 战斗结束后,通过GameItems模块发放掉落物品

4. 主要功能接口

4.1 物品管理接口

  1. 获取用户物品

    • 功能:获取用户拥有的所有物品
    • 参数:用户ID,物品类型(可选)
    • 返回:物品列表,包含数量、属性等信息
  2. 使用物品

    • 功能:消耗指定数量的物品并触发效果
    • 参数:用户ID、物品ID、数量
    • 返回:使用结果、剩余数量、触发效果
  3. 添加物品到用户背包

    • 功能:向用户背包添加指定物品
    • 参数:用户ID、物品ID、数量、过期时间(可选)
    • 返回:添加结果、当前数量
  4. 检查物品有效性

    • 功能:检查用户物品是否过期
    • 参数:用户ID、物品ID
    • 返回:有效性状态、错误信息(如有)

4.2 宝箱系统接口

  1. 打开宝箱

    • 功能:消耗宝箱并获取随机物品
    • 参数:用户ID、宝箱ID、数量
    • 返回:获得的物品列表、剩余宝箱数量
  2. 带保底机制的宝箱开启

    • 功能:打开宝箱并考虑保底机制
    • 参数:用户ID、宝箱ID、数量
    • 返回:获得的物品列表、是否触发保底、剩余宝箱数量

4.3 单独属性物品接口

  1. 创建单独属性物品

    • 功能:为用户创建具有独特属性的物品
    • 参数:用户ID、基础物品ID、属性参数(可选)
    • 返回:创建的单独属性物品信息
  2. 获取用户单独属性物品

    • 功能:获取用户拥有的单独属性物品
    • 参数:用户ID、基础物品ID(可选)
    • 返回:单独属性物品列表及其属性

5. 物品过期时间管理

物品模块支持两种过期时间机制:

5.1 全局过期时间

全局过期时间定义在items表的global_expire_at字段中,表示该物品在所有用户中的绝对过期时间。当超过这个时间点后,所有用户的该物品均失效。

应用场景:

  • 限时活动物品
  • 赠送的体验物品
  • 版本更新后需要清除的物品

5.2 用户物品过期时间

用户物品过期时间定义在item_users表的expire_at字段中,表示特定用户的特定物品的过期时间。这允许每个用户的同一物品有不同的过期时间。

应用场景:

  • 限时使用的装备或道具
  • 会员特权物品
  • 活动奖励物品

5.3 过期处理机制

系统应定期运行任务检查并处理过期物品:

过期物品处理流程

  1. 全局过期物品处理

    • 查询全局过期的物品
    • 删除所有用户的该物品或标记为失效(从 item_users 表中删除)
    • 可选操作:发送通知给用户
  2. 用户物品过期处理

    • 查询用户物品过期记录(从 item_users 表中查询)
    • 删除过期的用户物品
    • 可选操作:发送通知给用户

6. 宝箱系统设计

6.1 宝箱类型

宝箱可以根据稀有度和内容分为不同类型:

  1. 普通宝箱:包含基础消耗品和少量资源
  2. 稀有宝箱:有机会获得稀有装备和较多资源
  3. 限定宝箱:只在特定活动期间可获得,包含限定物品
  4. 主题宝箱:围绕特定主题设计,如节日宝箱、职业宝箱等

6.2 宝箱掉落机制

6.2.1 多物品掉落

每个宝箱可以同时掉落多个物品:

  • 每个宝箱类型物品在numeric_attributes字段中定义了min_drop_countmax_drop_count属性,而不是使用独立字段
  • 宝箱物品属于特定的分类(在item_categories表中定义)
  • 宝箱内容可以配置单个物品或物品组,物品组可以包含多个物品并根据权重随机选择
  • 系统在打开宝箱时会随机决定实际掉落的物品数量
  • 可以通过item_chest_contents表中的allow_duplicate字段控制物品是否可以在同一宝箱中重复掉落

6.2.2 概率机制

宝箱内容的概率由item_chest_contents表中的weight字段决定:

  • 每个物品的概率 = 该物品权重 / 所有物品权重之和

6.2.3 保底机制

为了确保玩家在多次开启宝箱后能获得稀有物品,系统实现了保底机制:

保底机制设计

保底机制直接集成在宝箱内容配置中,具有以下优势:

  1. 内容级别保底:每个宝箱内容可以设置自己的保底次数,当玩家连续未获得该内容达到指定次数后,必定获得该内容

  2. 灵活的概率调整:通过 pity_weight_factor 字段控制递增概率的幅度,可以为不同内容设置不同的递增策略

  3. 简化的数据结构:不需要额外的保底机制表和关联表,直接在宝箱内容表中定义保底机制

保底机制的实现方式有:

  1. 确定保底:当玩家开启指定数量宝箱后,必定获得特定宝箱内容

  2. 递增概率:每次未获得目标内容时,增加下次获得的概率

  3. 多内容保底:为宝箱中的不同内容设置不同的保底计数

以下是保底机制的实现代码示例:

保底机制实现流程

  1. 获取宝箱内容配置

    • 从 item_chest_contents 表查询宝箱的所有内容配置
    • 过滤出 pity_count > 0 的内容,这些是启用了保底的内容
  2. 获取用户保底计数

    • 从 item_pity_times 表查询用户对各宝箱内容的当前计数
    • 如果不存在,创建新记录并初始化计数
  3. 检查是否触发保底

    • 当用户开启宝箱但未获得特定内容时,增加该内容的计数
    • 判断是否达到内容的 pity_count 值
    • 如果触发保底,重置计数
  4. 选择保底内容

    • 当触发保底时,直接选择对应的宝箱内容
    • 将选中的内容标记为保底物品
  5. 选择其他物品

    • 根据权重随机选择其他物品
    • 处理重复掉落限制
递增概率实现

除了确定保底外,还可以实现递增概率的保底机制:

递增概率计算方式

在 item_chest_contents 表中可以定义递增概率的计算公式参数:

  • 调整后概率 = 基础概率 * (1 + (当前计数 / pity_count) * pity_weight_factor)
  • pity_weight_factor 字段存储加权系数,默认为2.0

这种方式的工作原理:

  1. 当玩家开启宝箱但未获得特定内容时,该内容的计数增加
  2. 下次开启宝箱时,该内容的概率会根据计数增加
  3. 当计数达到 pity_count 时,概率变为100%,必定获得该内容

这种设计可以确保玩家在一定次数内获得特定内容,同时保持一定的随机性和惊喜感。

6.3 宝箱日志记录

每次宝箱开启都应记录详细日志,包括:

  • 用户信息
  • 宝箱类型
  • 开启时间
  • 获得的物品详情

这些日志可用于数据分析和问题排查。

7. 同一物品多次获取处理

当玩家多次获得同一物品时,系统需要根据物品类型和过期时间进行不同的处理:

7.1 统一属性物品的处理

对于统一属性物品(如消耗品、材料等),当过期时间相同时,采用数量累加策略:

// 为用户添加统一属性物品
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;
}

7.2 过期时间分组策略

对于有过期时间的统一属性物品,按照过期时间分组存储:

  • 相同物品但过期时间不同,创建不同的记录
  • 相同物品且过期时间相同,数量累加

这样可以确保物品按照正确的过期顺序被使用或过期。在使用物品时,优先使用即将过期的物品。

// 使用物品时,优先使用即将过期的物品
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()
        ];
    }
}

7.3 单独属性物品的处理

对于单独属性物品(如装备、宠物等),每次获取都创建新记录,即使是相同物品:

  • 每个单独属性物品都在item_instances表中有一条记录
  • 每个单独属性物品在item_users表中的quantity始终为1
  • 可以通过额外的属性(如耐久度、强化等级等)区分

这种设计允许玩家拥有多个相同类型但属性不同的物品,比如多把强化等级不同的武器。

8. 注意事项

  1. 物品使用逻辑应根据物品类型进行不同处理
  2. 需要考虑物品数量上限和背包容量限制
  3. 稀有物品获取应有日志记录
  4. 物品交易需考虑并发安全问题,建议使用数据库事务
  5. 敏感操作(如删除物品)需要有权限验证
  6. 在获取用户物品时,始终检查过期时间,不展示已过期物品
  7. 在添加物品到用户背包时,需要检查全局过期时间
  8. 宝箱开启操作应使用事务确保原子性
  9. 宝箱配置应由管理员仔细调整和测试,确保概率合理
  10. 当物品数量超过最大堆叠数量时,应自动创建新的堆叠