H2Engine游戲服務器設計之屬性管理器


游戲服務器設計之屬性管理器

  游戲中角色擁有的屬性值很多,運營多年的游戲,往往會有很多個成長線,每個屬性都有可能被N個成長線模塊增減數值。舉例當角色戴上武器時候hp+100點,卸下武器時HP-100點,這樣加減邏輯只有一處還比較好控制,如果某天有個特殊功能當被某技能攻擊時,角色武器會被擊落,這樣就會出現減數值的操作不止一處。如果邏輯處理不當,比如擊落的時候沒有恰當的減數值,再次穿戴武器就導致屬性值加了兩邊,也就是玩家經常說的刷屬性。這種bug對游戲平衡性影響很大,反響很惡劣,bug又很難被測試發現。本文將介紹一種管理屬性的思路,最大限度的避免此類bug,如果出現bug,也能夠很好的排查。

設計思路

  刷屬性bug的核心原因是某功能的模塊數值加了N次,所以各個模塊加的屬性要被記錄,加過了必須不能重復加。設計這樣的數據結構。

//!各個屬性對應一個總值
//!各個屬性對應各個模塊的分值
template<typename T>
class PropCommonMgr
{
public:
    typedef T ObjType;
    typedef int64_t (*functorGet)(ObjType);
    typedef void (*functorSet)(ObjType, int64_t);
    struct PropGetterSetter
    {
        PropGetterSetter():fGet(NULL), fSet(NULL){}        
        functorGet fGet;
        functorSet fSet;
        std::map<std::string, int64_t> moduleRecord;
    };
    void regGetterSetter(const std::string& strName, functorGet fGet, functorSet fSet){
        PropGetterSetter info;
        info.fGet = fGet;
        info.fSet = fSet;
        propName2GetterSetter[strName] = info;
    }
  public:
      std::map<std::string, PropGetterSetter>    propName2GetterSetter;
  };
  1. 關於數據結構的get和set,我們為每個屬性命名一個名字,這樣處理數據的時候會非常方便(比如道具配增加屬性等等),角色屬性有很多種,這里不能一一定義,所以屬性管理器只是映射屬性,並不創建屬性值。通過regGetterSetter接口,注冊get和set的操作映射。為什么不需要提供add和sub接口能,因為add和sub可以通過get和set組合實現。get和set的接口實現如下:
int64_t get(ObjType obj, const std::string& strName) {
        typename std::map<std::string, PropGetterSetter>::iterator it = propName2GetterSetter.find(strName);
        if (it != propName2GetterSetter.end() && it->second.fGet){
            return it->second.fGet(obj);
        }
        return 0;
    }
    bool set(ObjType obj, const std::string& strName, int64_t v) {
        typename std::map<std::string, PropGetterSetter>::iterator it = propName2GetterSetter.find(strName);
        if (it != propName2GetterSetter.end() && it->second.fSet){
            it->second.fSet(obj, v);
            return true;
        }
        return false;
    }
  1. 關於add和sub,前面提到要避免刷屬性,就必須避免重復加屬性。所以每個模塊再加屬性前必須檢查一下是否該模塊已經加了屬性,如果加過一定要先減后加。因為每次模塊加屬性都記錄在屬性管理器中,那么減掉的數值一定是正確的。這樣可以避免另外一種常見bug,如加了100,減的時候計算錯誤減了80,也會積少成多造成刷屬性。add和sub的代碼如下:
int64_t addByModule(ObjType obj, const std::string& strName, const std::string& moduleName, int64_t v) {
        typename std::map<std::string, PropGetterSetter>::iterator it = propName2GetterSetter.find(strName);
        if (it != propName2GetterSetter.end() && it->second.fGet && it->second.fSet){
            int64_t ret =it->second.fGet(obj);
            std::map<std::string, int64_t>::iterator itMod = it->second.moduleRecord.find(moduleName);
            if (itMod != it->second.moduleRecord.end()){
                ret -= itMod->second;
                itMod->second = v;
            }
            else{
                it->second.moduleRecord[moduleName] = v;
            }
            ret += v;
            it->second.fSet(obj, ret);
            return ret;
        }
        return 0;
    }
    int64_t subByModule(ObjType obj, const std::string& strName, const std::string& moduleName) {
        typename std::map<std::string, PropGetterSetter>::iterator it = propName2GetterSetter.find(strName);
        if (it != propName2GetterSetter.end() && it->second.fGet && it->second.fSet){
            int64_t ret =it->second.fGet(obj);
            std::map<std::string, int64_t>::iterator itMod = it->second.moduleRecord.find(moduleName);
            if (itMod == it->second.moduleRecord.end()){
                return ret;
            }
            ret -= itMod->second;
            it->second.moduleRecord.erase(itMod);
            it->second.fSet(obj, ret);
            return ret;
        }
        return 0;
    }
    int64_t getByModule(ObjType obj, const std::string& strName, const std::string& moduleName) {
        typename std::map<std::string, PropGetterSetter>::iterator it = propName2GetterSetter.find(strName);
        if (it != propName2GetterSetter.end() && it->second.fGet && it->second.fSet){
            int64_t ret =it->second.fGet(obj);
            std::map<std::string, int64_t>::iterator itMod = it->second.moduleRecord.find(moduleName);
            if (itMod != it->second.moduleRecord.end()){
                return itMod->second;
            }
        }
        return 0;
    }
    std::map<std::string, int64_t> getAllModule(ObjType obj, const std::string& strName) {
        std::map<std::string, int64_t> ret;
        typename std::map<std::string, PropGetterSetter>::iterator it = propName2GetterSetter.find(strName);
        if (it != propName2GetterSetter.end() && it->second.fGet && it->second.fSet){
            ret = it->second.moduleRecord;
        }
        return ret;
    }

  如上代碼所示,addByModule和subByModule必須提供模塊名,比如穿裝備的時候加血量:addByModule('HP', 'Weapon', 100),而卸下武器的時候只要subByModule('HP', 'Weapon'),因為屬性管理器知道減多少。

總結

  1. 屬性提供一個名字映射有很多好處,比如裝備配屬性,buff配屬性的,有名字相關聯會特別方便
  2. 提供一個get和set接口的映射,這樣屬性管理器就和具體的對象的屬性字段解耦了。即使是現有的功能模塊也可以集成這個屬性管理器。
  3. 屬性的add和sub操作,都在屬性管理器中留下記錄,這樣即使出現問題,通過getByModule getAllModule兩個接口亦可以輔助查找問題。
  4. 屬性管理已經集成到H2Engine中,github地址: https://github.com/fanchy/h2engine


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM