yc筆記

一、static變量初始化問題

  • 1、lb收到外網有玩家擊殺了幫會boss後,沒有發放獎勵的BUG。隨後進一步查詢,發現計算怪物掉落的接口RETURN_FALSE了。僞代碼如下:
   void ApiTranslateDropBag(UINT bagId,std::vector<DropItem>& dropItems)
   {
        CCopyDropSystem* pdrop = ApiGetCopySysytem();
        if(!drop){RETURN_FALSE;}
        pdrop->translateDropBag(bagId,dropItems);
   }
  • 發現每次計算掉落的時候,都在第4行報錯了。很是奇怪,如果game正常初始化的話,那麼 ApiGetCopySysytem()一定會返回掉落系統的指針的。後面繼續追蹤,發現ApiGetCopySysytem()的代碼如下:
static CCopyDropSystem* ApiGetCopySysytem()
{
   static CCopyDropSystem* psys = (CCopyDropSystem*)WorldFindSystem("copydrop");
   if(!psys){
       RETURN_NULL;
   }
   return psys;
}
  • 第5行,只在game初始化的時候,報錯過1次。原因是static CCopyDropSystem* psys這個指針被初始化了一次以後,再調用到ApiGetCopySysytem()接口,就不會再執行(CCopyDropSystem*)WorldFindSystem(“copydrop”)這個函數了。而是直接返回已經初始化好的psys。
  • 後面繼續追蹤,第一次初始化失敗是因爲在定時器裏,manager會向game同步【保底出金】信息。
    而此時game的系統還沒被初始化好,WorldFindSystem(“copydrop”)返回的是nullptr,並且賦值給了static變量:psys,
    導致隨後game啓動完畢,殺怪走到掉落接口,都不能正常取到掉落系統指針。

二、一個測試時,引起的反思。在九州密藏活動完成後,提交獎勵的接口中。

extern bool ApiKyushuPutItem(CPlayer& player,bool isloss)
{
	...
    CItemBag* itembag = PlayerGetItemBag(player);
    if(!itembag){RETURN_FALSE;}
    BagSubItem(itembag,tid);
    if(!CallExpr(_kyushu_reward,&player,total_score)){RETURN_FALSE;}    /// 調用表達式;
	...
    return true;
 }
  • 這裏我原本漏寫了一行代碼: SynItemGemDiff(player),同步揹包中最新的信息到客戶端。
    但是第一遍測試的時候,沒有問題。因爲策劃在獎勵表達式_kyushu_reward中配置了道具獎勵。
    通過表達式PlayerAddItem()來發放獎勵,這個表達式中有同步信息,所以測試沒問題。
extern bool PlayerAddItem(CPlayer* player,UINT itemId,UINT count)
{
    if(!player){RETURN_FALSE;}
    CPlayerAddItem(*player,itemId,count);
    SynItemGemDiff(*player);
}
  • 第二次測試的時候,策劃關閉了道具獎勵的投放,只獎勵了經驗和靈力。所以 ApiKyushuPutItem接口就有點問題了,因爲沒有同步服務器最新的揹包數據到客戶端。那麼問題來了。
  • 如果,我在第9行,加上了SynItemGemDiff(*player),那麼自然沒問題了。
    但是如果策劃在表格裏又配置了道具獎勵,那麼道具獎勵表達式PlayerAddItem會同步1次,
    我的提交獎勵接口ApiKyushuPutItem會同步1次。那麼這個接口會同步兩次數據到客戶端,這樣就顯得有點浪費效率了。如果表格獎勵多份道具,配置了多個表達式,那麼同步次數線性增長;相當於O(n)的效率;我看了一下項目中的實現,居然是用的及時同步。也就是確確實實同步了2次。
    emm,,,,— —!
    忽然想起來,以前的實現是先打個【需要同步】的標記,然後在定時器中定時遍歷標記,如果是【需要同步】那麼發包同步給客戶端,然後將標記置爲【已同步】。那麼如果同步間隔是1s,剛剛的兩次SynItemGemDiff(*player),就可能只發了一次消息同步包到客戶端。但是不記得是不是遇到了什麼坑,才改到了這個樣子。(showlog看了下,貌似還是jinhai讓liyong修改的)後來有一次,youjun也問我,這裏的同步是否不需要及時同步,可以改爲在tick中。嗯,我是贊同不需要及時同步的,但是考慮到項目已經上線好久,以穩爲主,我不敢隨意去填坑(說不定填着填着就踩坑裏去了),撰此文記之。

三、副本結算、關閉流程

cxy提了一個在煉魔陣中出現的問題(嚴格上來說算不上bug,因爲一開始設計就未支持)
副本結算的時候,服務器會通知客戶端結算信息,客戶端顯示結算界面,副本如果結束了,就會踢出玩家,玩家loading界面後,在主場景中,繼續觀看結算界面。玩家可以手動關閉結算界面,或者客戶端倒計時10s後,結算界面自動關閉。
煉魔陣是一個日常活動,但是爲了引導新手玩家參加,在任務中添加了一個“任務煉魔陣”,該任務完成後,會獲得大量經驗,並讓玩家升級。
問題就出在這裏,1\服務器通知客戶端顯示結算界面2\副本結束玩家loading界面3\玩家升級特效4\升級解鎖引導圖標5\結算界面自動關閉。
也就是說,玩家第一次參加完煉魔陣,結算的時候,界面彈框是啪啪啪的一頓操作,讓人眼花繚亂。體驗很不友好。
問題介紹到此處,下面開始排查分析;去分析梳理了一下副本結算流程:順道發現了幾個小問題
  • 1、在mgr上CreateMap創建GMap*的時候,只是初始化了_pre_end_time(副本結束時間)=0,真正設置的地方是在game的gs_copy.cpp的OnPlayerEnterPlayer()事件中,設置一個副本的結束時間,居然是要監聽的玩家進入地圖事件。講道理在創建的時候,就應該設置了,如果mgr上沒有fuben.csv的配置信息,那麼mgr就應該讀取一下表格,這樣才合理。
void CCopySystem::OnPlayerEnterPlayer(CPlayer& player)
{
	...
    UINT cfg_close_second = DictToUint32(fuben_res,"close_seconds");
    CCreture* pcre = PlayerGetCre(player);
    if(!pcre){RETURN_FALSE;}
    MapObj* pobj = GetMapObj(*pcre);
    if(!pobj){RETURN_FALSE;}
    Map* pmap = obj->GetMap();
    if(!pmap){RETURN_FALSE;}
    pmap->SetMapCloseTime(cfg_close_second);        /// 在這裏設置的結束時間
    ...
}
void SetMapCloseTime(UINT seconds)
{
    EvMemPacket packet = ;
    packet.Write(type);
    packet.Write(seconds);
    SendToManagerServer(packet);
}
  • 2、gw_map_manager是服務器中所有地圖管理類,在Tick中遍歷所有的地圖是否過了要關閉的時間戳,如果到了要關閉的時候,就開始執行關閉;
void Tick()
{
    for(auto& amap:all_maps)
    {
        if(!amap){continue;}
        if(amap->CanClose()){    /// 能否關閉;
            amap->Close();        /// 執行關閉;
        }
    }
}
在執行關閉的時候,會通知game;
game在收到mgr上的關閉GMap的事件時,就開始踢人了,而game上的副本系統是在踢人的前一刻進行結算的;那麼必然是剛結算完,就退出了副本。
ON_MSG_TO_GAME(M2G_MAP_CLOSE)
{
    evBroadCast().SendEvent(EV_COPY_SETTLE);    /// 1、結算;
    PlayerLeaveMap(player);                        /// 2、踢人;
}
所以根源就在這裏,【結算操作】和【場景踢人】都是在一個消息響應函數裏做的;
那麼我修正這個問題的方案,就是加入一個結算時間,
void Tick()
{
    for(auto& amap:all_maps)
    {
        if(!amap){continue;}
        if(amap->CanSettle()){    /// 能否結算;
            amap->Settle();        /// 執行結算;
        }
        if(amap->CanClose()){    /// 能否關閉;
            amap->Close();        /// 執行關閉;
        }
    }
}

game上監聽兩個消息:
ON_MSG_TO_GAME(M2G_MAP_SETTLE)
{
    evBroadCast().SendEvent(EV_COPY_SETTLE);    /// 1、結算;
}
ON_MSG_TO_GAME(M2G_MAP_CLOSE)
{
    PlayerLeaveMap(player);                     /// 2、踢人;
}

四、屬性監控

屬性系統在遊戲裏是一個比較好玩的系統,對於經驗豐富的程序來說,簡單的就只是對數字的加減乘除。但對初級程序來說,繞進入就出不來了,明明很簡單,就是算不對。對數值策劃來說,那麼就是重中之重。
屬性數據太變態吧,玩家不用充錢就可以玩的很爽。數據太摳門吧,小R玩家留不住,小R走了,大R沒了虐待的對象,自然也流失,項目也就GG了。
今天先不說屬性系統的實現,來說一下【當屬性出問題了,如何快速定位屬性bug出在哪裏】。前兩天,剛解決了一個屬性bug的問題。
事情的經過是,外網有個玩家報了個BUG:玩家進入到亂鬥場以後,脫下所有防具(6件防具+2件首飾),並且關閉寵物。然後退出亂鬥場,再穿上所有防具,並打開寵物。然後戰力就多了3000。
看到這個bug,怎麼辦呢?
  • 第一步,先在內網按步驟復現一遍。發現並沒有復現。
  • 第二步,去外網復現一遍,發現依舊沒有復現。
  • 第三步,導入外網玩家的數據,再按步驟復現一遍,還是沒有復現。
    。。。。。。— —!

開始懷疑玩家是不是虛報bug了(這是一個很嚴重的錯誤思想)。永遠不要憑空懷疑虛報bug,除非你拿出實實在在的證據。
由於實在不能復現,版本改動中又修改了一些代碼。所以懷疑是否內網已盲改了該BUG。正好第二天要更新版本,所以本想擱置到第二天更新後,看看外網是否能夠重現。
好了,接下來是神來一筆,mingke在一次操作中,偶然復現了這個bug,於是拿到該賬號、密碼,在內網環境中開始調試。
事情經過大概是這樣,廢話不多說,直接說解決方案。

  • 原因:因爲影響屬性的系統有很多,法寶、鍛造、天工匣、靈石、寶石,屬性又是很容易出問題的,程序寫錯代碼,策劃配錯表格,所以如果每次出現問題,都要去調查一遍的話,那麼極其浪費時間。
  • 結果:設計了一個屬性監控系統,由於屬性變化太過頻繁,不可能對所有玩家的所有屬性進行監控,折中方案就是有一個監控列表,記錄了需要被監控的玩家oid,該列表會存盤記數據庫。當玩家屬性出現異常以後,通過輸入gm指令,把該玩家的oid加入到監控列表中。隨後玩家的所有變更屬性的行爲,都會被log記錄下來,存儲到日誌服務器中,以供程序分析。系統要注意下列幾點:
    • 1、監控列表,因爲日誌量太大,對所有玩家監控會有效率問題。
    • 2、gm指令可以修改監控列表,添加、移除等操作。
    • 3、遊戲上層中,所有會修改屬性的操作都需要添加屬性變化的log,也就是reason。
    • 4、屬性變更行爲存盤日誌數據庫,格式是:【時間、屬性名、修改原因、修改前值、修改後值】
    • 5、在每個操作打上reason標記,在最底層的CreSetAttr()接口中,取出標記,如果cre是監控列表匯中玩家,則添加該行爲信息到m_attrChgMap中。
    • 6、m_attrChgMap的size()大於500時,存盤一次。或者5分鐘間隔,會存盤一次。另外在玩家下線時,會存盤一次。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章