魔兽3.3.5版本背包模块代码拆分解析(从之前的文章中拆分出来)


不想写在 魔兽服务器学习-笔记之中了,之前的文章篇幅太长,代码在最后的模块

一、涉及到的文件

Object.h
Item.h
Bag.h

这里的背包继承了物品类,而物品类又继承了对象Object类

  • 注意点
    因为这里避免看的越多,所以这里从Bag.h看起,因为从Object.h看起来的话,根本看不完

二、先看成员变量(单个背包的存储数据)

#define MAX_BAG_SIZE 36 
        // Bag Storage space
        Item* m_bagslot[MAX_BAG_SIZE];

可以看到这里是存了指针数组,上限是36(因为c with class的关系,而且对于游戏来说内存很重要)

三、再看玩家背包和DB的交互

  • 背包类和DB交互的代码
        // DB operations
        // overwrite virtual Item::SaveToDB
        void SaveToDB(CharacterDatabaseTransaction trans) override;
        // overwrite virtual Item::LoadFromDB
        bool LoadFromDB(ObjectGuid::LowType guid, ObjectGuid owner_guid, Field* fields, uint32 entry) override;
        // overwrite virtual Item::DeleteFromDB
        void DeleteFromDB(CharacterDatabaseTransaction trans) override;
  • 玩家背包数据的db存储sql语句
DROP TABLE IF EXISTS `character_inventory`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `character_inventory` (
  `guid` int unsigned NOT NULL DEFAULT '0' COMMENT 'Global Unique Identifier',
  `bag` int unsigned NOT NULL DEFAULT '0',
  `slot` tinyint unsigned NOT NULL DEFAULT '0',
  `item` int unsigned NOT NULL DEFAULT '0' COMMENT 'Item Global Unique Identifier',
  PRIMARY KEY (`item`),
  UNIQUE KEY `guid` (`guid`,`bag`,`slot`),
  KEY `idx_guid` (`guid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Player System';
/*!40101 SET character_set_client = @saved_cs_client */;

①guid 人物id
②slot 槽位id
③item 物品id(表中唯一的id,可能是做全服唯一的道具用的)
④bag 背包索引(魔兽可携带多个背包)
表格使用的存储引擎为 InnoDB,字符集为 utf8mb4,排序规则为 utf8mb4_unicode_ci。整个表格用于存储玩家系统中与角色背包相关的数据。

1)逐行解析db表

①PRIMARY KEY (item):将 item 列定义为主键,确保每个物品在表格中是唯一的。
②UNIQUE KEY guid (guid,bag,slot):创建一个唯一键,由 guid、bag 和 slot 三列组成,确保每个背包槽位中的物品在表格中是唯一的。
③KEY idx_guid (guid):创建一个索引,以 guid 列作为索引,用于快速检索与特定角色相关的背包信息。

2)sql语句变更记录(记录一些sql语句对数据存储的影响)

①改变数据表的存储格式

ALTER TABLE `character_inventory` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
  • ALTER TABLE:用于修改数据库表的命令。
  • character_inventory:表名,指定要修改的表为 character_inventory。
  • CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci:用于指定要将表的字符集转换为 utf8mb4,并使用 utf8mb4_unicode_ci 校对规则。

解释:
这个 ALTER TABLE 语句的目的是将 character_inventory 表的字符集从现有的字符集转换为 utf8mb4。 utf8mb4 字符集支持存储更广泛的字符,包括一些特殊的 Unicode 字符,如表情符号。校对规则 utf8mb4_unicode_ci 则用于指定字符的排序和比较规则。

②删除对应列

ALTER TABLE `character_inventory` DROP COLUMN `item_template`;
  • ALTER TABLE:用于修改数据库表的命令。
  • character_inventory:表名,指定要修改的表为 character_inventory。
  • DROP COLUMN:指定要删除列的操作。
  • item_template:要删除的列名,即 character_inventory 表中的 item_template 列。

解释:
这个 ALTER TABLE 语句的目的是从 character_inventory 表中删除名为 item_template 的列。执行该语句后,表中的数据将不再包含 item_template 列的值,并且该列将从表结构中删除。

3)额外提一嘴物品在mysql的存储

CREATE TABLE `item_instance` (
  `guid` int unsigned NOT NULL DEFAULT '0',
  `itemEntry` mediumint unsigned NOT NULL DEFAULT '0',
  `owner_guid` int unsigned NOT NULL DEFAULT '0',
  `creatorGuid` int unsigned NOT NULL DEFAULT '0',
  `giftCreatorGuid` int unsigned NOT NULL DEFAULT '0',
  `count` int unsigned NOT NULL DEFAULT '1',
  `duration` int NOT NULL DEFAULT '0',
  `charges` tinytext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
  `flags` mediumint unsigned NOT NULL DEFAULT '0',
  `enchantments` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  `randomPropertyId` smallint NOT NULL DEFAULT '0',
  `durability` smallint unsigned NOT NULL DEFAULT '0',
  `playedTime` int unsigned NOT NULL DEFAULT '0',
  `text` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
  PRIMARY KEY (`guid`),
  KEY `idx_owner_guid` (`owner_guid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Item System';
/*!40101 SET character_set_client = @saved_cs_client */;

目的:这个SQL语句的目的是创建一个名为 item_instance 的表,用于存储物品实例的相关信息。
guid:物品唯一id
owner_guid:物品归属obj的id

4)预编译背包的物品和db交互的sql语句

①删除背包物品(全服唯一的物品id)CHAR_DEL_CHAR_INVENTORY_BY_ITEM

    PrepareStatement(CHAR_DEL_CHAR_INVENTORY_BY_ITEM, 
    "DELETE FROM character_inventory WHERE item = ?", CONNECTION_ASYNC);

②删除数据内对应背包id、对应格子、对应人物的数据信息

    PrepareStatement(CHAR_DEL_CHAR_INVENTORY_BY_BAG_SLOT, 
    "DELETE FROM character_inventory WHERE bag = ? AND slot = ? AND guid = ?", CONNECTION_ASYNC);

因为之前可以看到玩家背包表有做唯一索引

UNIQUE KEY `guid` (`guid`,`bag`,`slot`)

③查询背包里面单个物品id对应的持续时间、槽位、背包id、槽位id、标志位flags等信息

    PrepareStatement(CHAR_SEL_CHARACTER_INVENTORY, 
    "SELECT creatorGuid, giftCreatorGuid, count, duration, charges, flags, enchantments, randomPropertyId, durability, playedTime, text, bag, slot,item, itemEntry 
    FROM character_inventory ci   #给character_inventory起别名ci
    JOIN item_instance ii         #联表查询item_instance,给item_instance起别名ii
    ON ci.item = ii.guid WHERE ci.guid = ? #查询单个物品在
    ORDER BY bag, slot", CONNECTION_ASYNC);

④给character_inventory 执行插入操作

PrepareStatement(CHAR_REP_INVENTORY_ITEM,
 "REPLACE INTO character_inventory (guid, bag, slot, item) VALUES (?, ?, ?, ?)", CONNECTION_ASYNC);

四、解析成员函数

1)构造函数

Bag::Bag(): Item()
{
    m_objectType |= TYPEMASK_CONTAINER;       //标志是物品是背包类型
    m_objectTypeId = TYPEID_CONTAINER;        //标志这个obj是背包

    m_valuesCount = CONTAINER_END;

    memset(m_bagslot, 0, sizeof(Item*) * MAX_BAG_SIZE);
}

2)析构函数

Bag::~Bag()
{
    for (uint8 i = 0; i < MAX_BAG_SIZE; ++i)
        if (Item* item = m_bagslot[i])
        {
            if (item->IsInWorld())  //背包在地图上,那就需要从地图实例里面去除
            {
                TC_LOG_FATAL("entities.player.items", "Item {} (slot {}, bag slot {}) in bag {} (slot {}, bag slot {}, m_bagslot {}) is to be deleted but is still in world.",
                    item->GetEntry(), (uint32)item->GetSlot(), (uint32)item->GetBagSlot(),
                    GetEntry(), (uint32)GetSlot(), (uint32)GetBagSlot(), (uint32)i);
                item->RemoveFromWorld();
            }
            delete m_bagslot[i];
        }
}

3)实体添加到世界

//把实例添加到世界
void Bag::AddToWorld()
{
    Item::AddToWorld();

    for (uint32 i = 0; i < GetBagSize(); ++i)
        if (m_bagslot[i])
            m_bagslot[i]->AddToWorld();
}

//从世界里面去除实例
void Bag::RemoveFromWorld()
{
    for (uint32 i = 0; i < GetBagSize(); ++i)
        if (m_bagslot[i])
            m_bagslot[i]->RemoveFromWorld();

    Item::RemoveFromWorld();
}

4)背包添加物品

bool Bag::Create(ObjectGuid::LowType guidlow, uint32 itemid, Player const* owner)
{
//读取物品的配置信息,例如堆叠数量、过期时间、耐久度
    ItemTemplate const* itemProto = sObjectMgr->GetItemTemplate(itemid);

    if (!itemProto || itemProto->ContainerSlots > MAX_BAG_SIZE)
        return false;

    Object::_Create(guidlow, 0, HighGuid::Container);

	//标志位字段设置物品id
    SetEntry(itemid);
    //OBJECT_FIELD_SCALE_X 是用来控制游戏中物体的缩放比例的字段
    SetObjectScale(1.0f);

    if (owner)
    {
    //如果这个背包添加道具是添加到玩家身上,在对应标志位填玩家id
        SetGuidValue(ITEM_FIELD_OWNER, owner->GetGUID());
        SetGuidValue(ITEM_FIELD_CONTAINED, owner->GetGUID());
    }

//ITEM_FIELD_DURABILITY:物品的当前耐久度。
//ITEM_FIELD_MAXDURABILITY:物品的最大耐久度
    SetUInt32Value(ITEM_FIELD_MAXDURABILITY, itemProto->MaxDurability);
    SetUInt32Value(ITEM_FIELD_DURABILITY, itemProto->MaxDurability);
    //ITEM_FIELD_STACK_COUNT:物品的堆叠数量
    SetUInt32Value(ITEM_FIELD_STACK_COUNT, 1);

    // Setting the number of Slots the Container has
    //根据配置设置背包槽位数
    SetUInt32Value(CONTAINER_FIELD_NUM_SLOTS, itemProto->ContainerSlots);

    // Cleaning 20 slots
    for (uint8 i = 0; i < MAX_BAG_SIZE; ++i)
    {
    //将背包对象的每个槽位的GUID值设置为空,以确保背包对象创建时的初始状态是空的,没有任何物品。
        SetGuidValue(CONTAINER_FIELD_SLOT_1 + (i*2), ObjectGuid::Empty);
        m_bagslot[i] = nullptr;
    }

    return true;
}

5)获得对应id的物品数量

  • 备注
    eItem->GetTemplate()->GemProperties用于计算背包宝石的数量
  • 代码
uint32 Bag::GetItemCount(uint32 item, Item* eItem) const
{
    Item* pItem;
    uint32 count = 0;
    for (uint32 i=0; i < GetBagSize(); ++i)
    {
        pItem = m_bagslot[i];
        if (pItem && pItem != eItem && pItem->GetEntry() == item)
            count += pItem->GetCount();
    }

    if (eItem && eItem->GetTemplate()->GemProperties)
    {
        for (uint32 i=0; i < GetBagSize(); ++i)
        {
            pItem = m_bagslot[i];
            if (pItem && pItem != eItem && pItem->GetTemplate()->Socket[0].Color)
                count += pItem->GetGemCountWithID(item);
        }
    }

    return count;
}

6)根据物品标签获得对应物品数量

  • 备注
    uint32 limitCategory表示物品的标签

  • 代码

uint32 Bag::GetItemCountWithLimitCategory(uint32 limitCategory, Item* skipItem) const
{
    uint32 count = 0;
    for (uint32 i = 0; i < GetBagSize(); ++i)
        if (Item* pItem = m_bagslot[i])
            if (pItem != skipItem)
                if (ItemTemplate const* pProto = pItem->GetTemplate())
                    if (pProto->ItemLimitCategory == limitCategory)
                        count += m_bagslot[i]->GetCount();

    return count;
}

7)根据物品id获取对应背包内物品的槽位

uint8 Bag::GetSlotByItemGUID(ObjectGuid guid) const
{
    for (uint32 i = 0; i < GetBagSize(); ++i)
        if (m_bagslot[i] != 0)
            if (m_bagslot[i]->GetGUID() == guid)
                return i;

    return NULL_SLOT;
}

五、背包总结

在这里插入图片描述
玩家身上有150个物品格子,而在这个格子里面,有的物品是背包,那么这个背包就可以存储最多36个物品(这也是为什么Bag继承Item的原因,因为可以根据Item找到对应的Bag)

enum PlayerSlots
{
    // first slot for item stored (in any way in player m_items data)
    PLAYER_SLOT_START           = 0,
    // last+1 slot for item stored (in any way in player m_items data)
    PLAYER_SLOT_END             = 150,
    PLAYER_SLOTS_COUNT          = (PLAYER_SLOT_END - PLAYER_SLOT_START)
};

class player{
.....
        Item* m_items[PLAYER_SLOTS_COUNT];
....
}

七、已上阵的装备代码

1)建立人物碰撞体代码讲解开始

  • 代码讲解
    遍历身上装备道具,通知客户端创建对应地图实体
  • 对应数据
    也是存在之前说的道具数组里面
        Item* m_items[PLAYER_SLOTS_COUNT];
  • 代码
void Player::BuildCreateUpdateBlockForPlayer(UpdateData* data, Player* target) const
{
    if (target == this)
    {
    	//遍历身上装备的装备枚举,到EQUIPMENT_SLOT_END
        for (uint8 i = 0; i < EQUIPMENT_SLOT_END; ++i)
        {
            if (m_items[i] == nullptr)
                continue;
			//通知客户端创建对应实体
            m_items[i]->BuildCreateUpdateBlockForPlayer(data, target);
        }

        for (uint8 i = INVENTORY_SLOT_BAG_START; i < BANK_SLOT_BAG_END; ++i)
        {
            if (m_items[i] == nullptr)
                continue;

            m_items[i]->BuildCreateUpdateBlockForPlayer(data, target);
        }
        for (uint8 i = KEYRING_SLOT_START; i < CURRENCYTOKEN_SLOT_END; ++i)
        {
            if (m_items[i] == nullptr)
                continue;

            m_items[i]->BuildCreateUpdateBlockForPlayer(data, target);
        }
    }

    Unit::BuildCreateUpdateBlockForPlayer(data, target);
}

2)源码枚举

enum EquipmentSlots : uint8                                 // 19 slots
{
    EQUIPMENT_SLOT_START        = 0,
    EQUIPMENT_SLOT_HEAD         = 0,
    EQUIPMENT_SLOT_NECK         = 1,
    EQUIPMENT_SLOT_SHOULDERS    = 2,
    EQUIPMENT_SLOT_BODY         = 3,
    EQUIPMENT_SLOT_CHEST        = 4,
    EQUIPMENT_SLOT_WAIST        = 5,
    EQUIPMENT_SLOT_LEGS         = 6,
    EQUIPMENT_SLOT_FEET         = 7,
    EQUIPMENT_SLOT_WRISTS       = 8,
    EQUIPMENT_SLOT_HANDS        = 9,
    EQUIPMENT_SLOT_FINGER1      = 10,
    EQUIPMENT_SLOT_FINGER2      = 11,
    EQUIPMENT_SLOT_TRINKET1     = 12,
    EQUIPMENT_SLOT_TRINKET2     = 13,
    EQUIPMENT_SLOT_BACK         = 14,
    EQUIPMENT_SLOT_MAINHAND     = 15,
    EQUIPMENT_SLOT_OFFHAND      = 16,
    EQUIPMENT_SLOT_RANGED       = 17,
    EQUIPMENT_SLOT_TABARD       = 18,
    EQUIPMENT_SLOT_END          = 19
};
--------------------------------------
EQUIPMENT_SLOT_HEAD:头部
EQUIPMENT_SLOT_NECK:颈部
EQUIPMENT_SLOT_SHOULDERS:肩部
EQUIPMENT_SLOT_BODY:身体
EQUIPMENT_SLOT_CHEST:胸部
EQUIPMENT_SLOT_WAIST:腰部
EQUIPMENT_SLOT_LEGS:腿部
EQUIPMENT_SLOT_FEET:脚部
EQUIPMENT_SLOT_WRISTS:手腕
EQUIPMENT_SLOT_HANDS:手部
EQUIPMENT_SLOT_FINGER1:手指1
EQUIPMENT_SLOT_FINGER2:手指2
EQUIPMENT_SLOT_TRINKET1:饰品1
EQUIPMENT_SLOT_TRINKET2:饰品2
EQUIPMENT_SLOT_BACK:背部
EQUIPMENT_SLOT_MAINHAND:主手
EQUIPMENT_SLOT_OFFHAND:副手
EQUIPMENT_SLOT_RANGED:远程
EQUIPMENT_SLOT_TABARD:战袍

六、代码

class TC_GAME_API Bag : public Item
{
    public:
        Bag();
        ~Bag();

        void AddToWorld() override;
        void RemoveFromWorld() override;

        bool Create(ObjectGuid::LowType guidlow, uint32 itemid, Player const* owner) override;

        void StoreItem(uint8 slot, Item* pItem, bool update);
        void RemoveItem(uint8 slot, bool update);

        Item* GetItemByPos(uint8 slot) const;
        uint32 GetItemCount(uint32 item, Item* eItem = nullptr) const;
        uint32 GetItemCountWithLimitCategory(uint32 limitCategory, Item* skipItem = nullptr) const;

        uint8 GetSlotByItemGUID(ObjectGuid guid) const;
        bool IsEmpty() const;
        uint32 GetFreeSlots() const;
        uint32 GetBagSize() const { return GetUInt32Value(CONTAINER_FIELD_NUM_SLOTS); }

        // DB operations
        // overwrite virtual Item::SaveToDB
        void SaveToDB(CharacterDatabaseTransaction trans) override;
        // overwrite virtual Item::LoadFromDB
        bool LoadFromDB(ObjectGuid::LowType guid, ObjectGuid owner_guid, Field* fields, uint32 entry) override;
        // overwrite virtual Item::DeleteFromDB
        void DeleteFromDB(CharacterDatabaseTransaction trans) override;

        void BuildCreateUpdateBlockForPlayer(UpdateData* data, Player* target) const override;

        std::string GetDebugInfo() const override;

    protected:

        // Bag Storage space
        Item* m_bagslot[MAX_BAG_SIZE];
};