三年的总结(技术篇)

三年来的写的代码真的不少,有必要试着理清一下思路,找到不依赖公司的框架,可以带走的东西。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.服务端客户端通讯。

相关文章
相关标签/搜索