三年的總結(技術篇)

三年來的寫的代碼真的不少,有必要試着理清一下思路,找到不依賴公司的框架,可以帶走的東西。html

三年來主要的工做是完成手機遊戲的功能需求。公司的遊戲客戶端是使用lua語言。服務器是使用c++。如下舉例的遊戲主要基於ARPG的,SLG的部分另外有一篇 http://www.cnblogs.com/yao2yaoblog/p/6723621.html。mysql

 

客戶端c++

1.mvc算法

有借鑑意義的一部分是客戶端view的管理。分三層:viewctrldata。是一個mvc的思想。view是加載面板layout,展現遊戲畫面的。data是存儲服務端下發的數據。ctrl是負責調度的。ctrl一般能夠寫成單例,做爲全局可方便調用的。sql

我拿下圖的一個業務來舉例。福利大廳裏面領取獎勵。數據庫

1) 服務器下發數據以後,首先是用data來存放這些數據(可領取,不可領取)。一般會調用ctrl裏面的函數來把數據傳到data,相似這樣編程

LevelRewardCtrl:Instance():SavaRewardData()

 2)頁面上要展現的內容,首先是由layout佈局決定的,view加載這個layout。再取data裏的數據對頁面上的內容進行update。json

local data = LevelRewardCtrl:Instance():GetRewardData()

mvc這種結構,頗有借鑑意義,會使得邏輯清晰不少。數組

 

2.NGUIDrawCall優化。服務器

NGUI是Unity一個開源的製做UI的插件。DrawCall是NGUI裏面的一個概念。

Unity準備好數據通知GPU繪製的過程叫DrawCall。至於準備哪些數據,沒有深刻過源碼就不深究了。一次DrawCall會消耗大量GPU資源,因此一般製做UI時須要減小DrawCall的數量。

全部的UI組件都有個UIWidget的腳本。每一個UIWidget的顯示渲染順序都是由其面板UIPaneldepth和本身的depth共同決定的。

1)面板深度加權重。假設有UIPanel:P1,P2,UIWidget:W1,W2。W1在P1裏,W2在P2裏,P1的depth大於P2的depth,那麼不管W1,W2的depth哪一個大,W1最後算出來的depth都比W2大。

2)儘可能避免圖集交叉。假設有圖集A,B。UIWidget:WA1,WA2使用圖集A。UIWidget:WB1,WB2使用圖集B。深度從小到大若是是WA1,WA2,WB1,WB2。那麼會是2個DrawCall。若是是WA1,WB1,WA2,WB2,那麼會變成4個DrawCall。相鄰的深度若是是同一個圖集,一般會合併成一個DrawCall。

DrawCall合併算法:先把UIPanel中的Widget按depth從小到大排序,若是depth相同那按照material的ID來排序。而後遍歷每一個元素,把material相同的Widget歸類到同一個drawCall。

3)動態元素和靜態元素區分。也不能一味的合併DrawCall。遊戲裏有一些元素是長時間不動的,好比背景的一些元素。有一些元素是常常須要變更的。若是把大量靜態元素和動態元素合併成了一個DrawCall,也會浪費開銷。

4)改變Position代替SetActive。SetActive須要作大量工做,若是隻是改變Position,開銷會小不少。

5)文字置頂。UILabel,UIRichlabel的深度置頂。會使得關於文字的drawcall所有合併。

 

3.lua的閉包

http://www.cnblogs.com/yao2yaoblog/p/6413190.html

 

4.內存池。

在遊戲裏的物品格子使用了內存池的策略。物品格子在不少地方須要使用,並且裏面的組件很多。sprite,texture,label組合都有。在經歷了多個遊戲,多個版本以後最終採用的是內存池的方法。

也就是說使用不須要業務手動建立,而是從內存池取得,銷燬改成放回內存池。

 

服務端

c++是屬於個人編程母語。不過確實是一門深不見底的語言,不管看多少資料,總會新的東西展示在眼前。三年的代碼量只能保證業務需求變化不大的狀況下,儘可能精簡,不要犯低級錯誤。用最穩妥的寫法,不要秀語法,不要秀語法,不要秀語法。能上籃別扣。

由於服務器是c++的,因此空指針,數組越界的問題很容易就讓服務器崩潰了。崩潰了須要重啓,對遊戲運營會形成損失,玩家會流失的。確定要儘量少重啓。

服務器的全部進程如圖:

 

1.我的系統。

指的是玩家本身的系統,只涉及客戶端和GameWorld進程,加上數據庫存儲的操做。

能夠拿坐騎進階系統舉例,簡化而言,服務器有個數據ride_level。客戶端根據服務器下發的這個ride_level,在場景上,UI上展現不一樣的模型。

ride_level是玩家登錄的時候,從數據庫取上來,放到內存裏,組織在userlogic類裏面。userlogic類聲明在Role類裏。這樣經過Role的實例就能夠拿到這個ride_level。因而各我的系統之間均可以經過role實例互相取得數據。

好比坐騎系統的等級是根據翅膀系統的等級而變(瞎編的需求),能夠這樣在翅膀系統寫代碼(僞代碼),翅膀升級的時候致使坐騎升級。

void Wing::Levelup()
{
    wing_level++;
    role->GetRide()->Levelup();
}

void Ride::Levelup()
{
    ride_level++;
}

這些我的系統的數據會在玩家下線的時候存儲到數據庫,或者是服務器進程退出的時候存儲到數據庫,或者每隔一段時間存儲到數據庫。

至於數據庫的存取使用的是json,存到mysql裏的是一個json的字符串。

 

2.全服系統

好比幫派系統,好比結婚系統,好比全服的活動。數據區別與玩家本身的數據,在數據庫中存在全服的數據表裏,全服的功能在一個Global進程裏。

Globa裏一般會有全服的在線玩家list,每當一個玩家登陸時,會從GW同步數據到Global,用一個簡化的用戶信息結構GlobalUser管理。

一般狀況,若是須要獲得其餘玩家的數據,就必須通過Global獲得。既是考慮多人的功能時,須要考慮Global和GW的通訊問題。副本比較特殊,副本能夠在GW管理多人信息。

舉個例子。幫派神樹的澆灌,大意是說整個幫派全部玩家共有一顆樹,你們能夠消耗本身的物品或者幫貢,來對樹升級。

首先可能在客戶端判斷本身的幫貢是否足夠,發消息到GW,判斷本身的幫貢是否足夠,發消息到Global對神樹升級,在發消息回GW對本身的幫貢進行扣除。僞代碼以下:

// client lua
function ProtocalArmy::ReqUpTree(need_data)
{
    Protocal.Begin(1000)//協議號
    Protocal.SetData("i", need_data)    //設置傳輸數據
    Protocal.Send(NetId)    //根據Ip地址發到服務端
}

// GW c++
RoleArmy::ReqUpTree(need_data)
{
    if (m_data < need_data)
    {
        // 幫貢不夠
        return;
    }

    // 發消息到Global 帶着數據或不帶
    TreeUpReqStruct turs;
    turs.need_data = need_data;
    SendToGlbal(net_id, (const char*)&turs, sizeof(TreeUpReqStruct));
}

// Global c++
ArmyManager::ReqUpTree(need_data)
{
    // 神樹升級
    TreeUp();

    // 發消息回GW 帶着數據或不帶
    TreeUpReqSucBackStruct tursbs;
    tursbs.need_data = need_data;
    SendToGW(net_id, (const char*)&tursbs, sizeof(TreeUpReqSucBackStruct));
}

// GW c++
RoleArmy::ReqUpTreeSucBack(need_data)
{
    // 減幫貢
    m_data -= need_data;

    // 發消息回客戶端 給玩家反饋
}

整個流程大概如上,可見涉及Global的功能已經比只涉及GW難一些,由於涉及到GW和Global之間消息互發,若是思路不是很清晰,容易混亂。作功能以前須要弄清楚哪些數據在GW有,哪些數據在Globa有,數據會怎樣改變。

在這個需求裏面,爲何不直接從客戶端發消息到Global呢,而要通過GW。是由於我的的幫貢信息是放在GW的,Global沒有,先要在GW用幫貢數據作個判斷。這個時候須要扣除的幫貢還不能直接扣除,須要等Global的神樹數據改變後,再返回來改變幫貢。

其中有一個異常問題值得思考。假如Global數據變化以後,發消息會GW時,兩個進程斷開了,會怎樣。豈不是會形成數據錯亂。致使神樹升級,可是幫貢沒扣。

這樣講可能感覺不深。借用數據庫」事務「的概念來講。銀行轉帳的例子,銀行A轉帳到銀行B,其實是銀行A減,銀行B加,必須保證二者都完成,纔算一次轉帳操做,不然要回滾。

剛纔講的例子是Global進程的神樹數據和GW進程的幫貢數據也要同時改變,不然理論上應該回滾。可是咱們的服務器好像沒作這方面的考慮。由於GW和Global一般在一臺服務器上,通訊時間能夠忽略不計。幾乎不可能出現上述狀況。

可是在我作過的另外一個SLG服務器架構裏面就有可能出這個問題。是一個懸而未決的問題。記錄在這裏,也許之後會獲得答案。

3.跨服系統

跨服副本。

4.技能系統

技能系統是已有的基礎模塊了,不過我作過一個功能叫魔神系統。簡而言之,是人物能夠變身,變身以後人物的技能列表換掉,等變身時間到技能列表再還原回來。

這就涉及到技能模塊和我的系統模塊。我的系統模塊多是控制變身的條件,狀態,cd等等。真正的大頭在技能模塊。

技能大體能夠分爲,被動和主動。

5.aoi模塊

6.副本管理

副本管理所有在GW進程。副本其實就是涉及到場景管理。全部場景都是副本,有一個logic做爲基類。若是是普通場景,沒有什麼特殊操做,那麼用一個default子類便可。若是須要特殊操做則用子類,重載基類的方法來實現。

要建立一個場景,一般須要3個變量,scend_id,scene_key,logic_type。

bool CreateFb(int scene_id, int scene_key, int logic_type);

scene_id是由配置而來,scene_key用來惟一標誌這個場景,logic_type表明這個副本的玩法。其中單人副本中,scene_key一般採用自增方法獲得,而多人副本中,scene_key一般須要記錄下來,以保證多人進入的是同一副本。

副本玩法比較重要的幾個,須要重載的方法。心跳,人物進入,人物退出,人物死亡,人物被攻擊等等。

virtual void Update(unsigned long interval, time_t now_second){}

首先心跳,重載這個函數,能夠來控制副本的狀態。interval是兩次調用的間隔,now_second是如今的時間戳。例如若是副本有準備,開始,結束3個狀態。能夠在副本初始化的時候,算好這2個關鍵時間點m_begin_time,m_end_time。在update裏分別與now做比較:

switch(m_status)
{
    case FB_READY:
    {
          if (now_second > m_begin_time)
          {
               m_status = FB_BEGIN;
          }
          break;      
    }
    case FB_BEGIN:
    {
           if (now_second > m_end_time)
          {
               m_status = FB_END;
          }
          break;      
    }
    case FB_END:
    {
          // destroyfb(); 
          break;      
    }
} 

從而切換副本的狀態。

人物的進出。副本一般會有一個玩家列表,來管理這個副本里玩家的信息。單人副本比較簡單,多人副本須要根據需求在進出副本的時候寫邏輯。好比玩家出了副本,在副本記錄的信息需不須要清空。一般是會清空的。

清空和不清空的區別,在於一套對象管理的機制。場景裏新建立一我的物,會有一個role對象產生,一般用一個obj_id標誌這個對象。這個obj_id產生的策略,我理解爲搶佔式的。

若是A進入場景,那麼obj_id = 1給A,B再進入,那麼obj_id = 2給B,這時副本管理列表裏obj_id = 1, 2分別是A,B玩家。這時A玩家推出了副本,C玩家進來了,這時obj_id = 1就給了C。同時列表裏的信息覆蓋掉A的信息。

若是需求是清空的,那就沒任何問題。新進來的玩家覆蓋舊玩家的信息。可是若是需求是清空的,那麼A的第一次進入副本的信息怎麼記錄呢,好比他殺了個怪。這就須要另外一個表了。

一個表是正在副本里的玩家信息m_on_fb_user_map,一個表是進來過副本又出去了的玩家信息m_out_fb_user_map。一般這個結構是寫成一個std::map< UserId, UserInfo >的map。

 

 

通訊相關

1.服務器進程間通訊

2.服務端客戶端通訊。

相關文章
相關標籤/搜索