三年來的寫的代碼真的不少,有必要試着理清一下思路,找到不依賴公司的框架,可以帶走的東西。html
三年來主要的工做是完成手機遊戲的功能需求。公司的遊戲客戶端是使用lua語言。服務器是使用c++。如下舉例的遊戲主要基於ARPG的,SLG的部分另外有一篇 http://www.cnblogs.com/yao2yaoblog/p/6723621.html。mysql
客戶端c++
1.mvc算法
有借鑑意義的一部分是客戶端view的管理。分三層:view,ctrl,data。是一個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.NGUI的DrawCall優化。服務器
NGUI是Unity一個開源的製做UI的插件。DrawCall是NGUI裏面的一個概念。
Unity準備好數據通知GPU繪製的過程叫DrawCall。至於準備哪些數據,沒有深刻過源碼就不深究了。一次DrawCall會消耗大量GPU資源,因此一般製做UI時須要減小DrawCall的數量。
全部的UI組件都有個UIWidget的腳本。每一個UIWidget的顯示渲染順序都是由其面板UIPanel的depth和本身的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.服務端客戶端通訊。