Tizeng's blog Ordinary Gamer

Cocos2dx开发笔记3——搬砖

2020-01-10
Tizeng

此笔记以Mac系统为平台。

服务器通信

如何接收服务器发来的消息?

以背包数据为例,obj_PackData方法接收背包的初始化消息,收到消息的过程为,Callback.lua脚本中处理服务器发来的消息,先分发给DataManeger,然后在分发给UIManager,调用的是各自的dispatchMsg方法,传入消息名称(obj_PackData)和数据包。DataManager在初始化时会包含所有处理消息的脚本名以及它们的实例,然后以传入的消息名索引对应的方法,缓存在本地,然后调用。UIManager的实现有所不同,每创建一个panel,UIManager都会按该panel的名字将其储存进一个表,分发时遍历所有已储存的panel并以传进的函数名索引查找该函数,如果找到则调用。

之前的bbl项目中若要接收服务器消息需要在脚本开头先注册消息名称,是因为在该项目中DataManager在分发消息前按接收消息的名称储存了该脚本的实例,然后分发时用消息名称索引需要接收该消息的实例,调用相应的方法,这样做的好处是不用每次收到消息时都去遍历有哪些数据脚本需要该消息,提升性能。UIManager也是类似,为了不在每次收到消息时都去遍历所有ui,也按照消息的名称来储存所有需要该消息的实例。在关闭ui时,释放空间的操作是在UIManager中的update方法中,目的是在下一帧才执行我们的释放操作,这样可以防止在同一帧内发生多次创建和删除操作。

proto消息

protobuf是由谷歌开发的一种语言中立、平台无关,可扩展的序列化数据的格式,相比于xml更小巧高效,通过官方的编译器根据定义好的.proto文件生成目标语言的代码,就可以序列化成二进制文件进行传输,或是反序列化来读取了。 proto消息长度不能超过32个字符,否则会被截断。

在zt项目中,向服务器发送proto消息,使用了protoc-gen-lua,它是一个为lua实现的protobuf,提供了将消息序列化的接口

c++消息

c++消息注册在LuaCmdDef

配置读取

g_DataManager中的getXmlDatagetExeclData负责读取xml和excel配置。

上漂提示和确认弹窗

大部分上漂和提示在ChatData中的showGameMsgTips方法实现,根据传入不同的提示类型调用不同的方法去显示,确认弹窗在showMessageBoxMedium方法实现。

注册回调函数

首先看handlerregisterClickHandler方法的实现:

function handler(obj, method)
    return function (...)
        return method(obj, ...)
    end
end

-- 类A中实现的注册方法
function M:registerClickHandler(obj, func, ...)
    if type(func) == "function" then 
        self.m_clickCallbackArgs = {...}
        self.m_clickCallbackFunc = handler(obj, func)
    elseif type(obj) == "function" then 
        self.m_clickCallbackArgs = {...}
        self.m_clickCallbackFunc = obj
    else
        self.m_clickCallbackArgs = nil
        self.m_clickCallbackFunc = nil
    end
end

-- 类A中使用回调
function M:onClick(touch, event)
    if self.m_clickCallbackFunc then 
        self.m_clickCallbackFunc(self, unpack(self.m_clickCallbackArg))
    end
end

-- 在另一个类B中生成这个类A的实例instanceA,然后注册类B中的方法为类A实例的回调
-- instanceA:registerClickHandler(selfB, selfB.someFunction)

外部生成实例后若想注册点击回调函数,则直接向上面一样调用registerClickHandler方法,也就是说这里想把类B的一个方法注册为类A实例的回调函数,至于该回调在何时调用我们不关心,注册时传入的self是类B的self,进入registerClickHandlerhanlder方法会将其作为obj参数传入,并作为第一个参数传入self.someFunction,注意这里handler返回的并不是self.someFunction这个方法本身,而是一个匿名函数:

function(...) 
    return instanceB.someFunction(selfB, ...)
end

因此最后调用类A的方法onCLick时,其中作为参数传入的self是类A的实例,就算后面的参数列表为nil,它也会作为唯一的参数传入self.someFunction(selfB, selfA)

创建UI

创建ui在UIManager中有两个接口,一个是getOrCreatePanel,它会先调用getPanel去取,如果取不到则调用用createPanel方法创建ui并返回,UIManager会储存每个创建的实例(创建是调用的Node节点中的create方法)。而如果是用createPanelOnly创建的ui,就不能用getOrCreatePanel方法来获得实例,因为它没有在UIManager中保存已创建ui的实例,直接调用会导致后者再以自己的方式创建并储存该ui,一般来说调用了createPanelOnly的类中会自己储存ui的实例,然后实现相应的get方法去取,我们应该去调用这些get方法。

有一类ui的创建较为特殊,道具点击后弹出的小弹窗,根据道具种类不同,弹窗上的按钮种类可能有区别,也可能需要不同的回调函数,创建背包界面时面对茫茫多的道具,显然应该封装一个方法来处理不同道具的callback,背包中首先为每个道具创建空的UIItemIconBg实例,然后根据道具data中的grid格子坐标获取itemData,然后用itemData去初始化创建的格子,其中便包括给其注册正确的callback函数,通常情况下(没有需要对比的其他道具),这个callback先创建UIBaseTips,它是tips的基本背景图(后面也会根据道具的data切换),然后根据该道具的baseid得到需要显示的按钮btnList,tips中按钮也需要回调,所有这些回调函数统一定义在UITipsConfig中,按类型进行索引,然后给当前物品弹窗tips设置正确的回调。

衣橱系统新功能(2020.2.19)

在衣橱ui中加入一个“进阶”页签,让衣品达到十段的外套可以继续升钻,点击提升按钮后弹出可选择提升材料的窗口,选择后点击确定消耗材料,提升外套经验,达到当前等级要求的经验后升一颗钻,按白蓝黄绿紫的顺序,每种颜色有五级,升满后变为下一个颜色,直到紫5。

这本身是一个很基础的功能,但却比预期的时间晚了24个小时才完成,主要的原因有二,一是上面总结的get和创建ui的方法使用不当,导致get到了错误的ui实例,界面收到服务器消息后没有实时刷新;二是没有很快理解UIWardrobChoose这个ui的创建逻辑,把自己绕进去了,浪费了很多时间。

总结来说这个功能无非就是调用之前已经写好的各种ui的api,很多东西已经是实现好的,只有UIWardrobChoose这个类,由于之前只有一个地方使用,里面做了一些特殊处理,现在把它优化成可以在衣橱系统下通用的接口,触发条件和回调函数由外部传入。

道具通用点击tips

有很多地方需要显示道具icon,而这些icon通常是可以点击的,点击后会弹出一个tips显示道具的各种信息,名称、品类、简介等,这就需要用到PackageData中的显示通用tips的方法,创建icon实例后,调用之前描述的registerClickHandler方法,将显示tips的方法注册给icon,然后通过道具的baseid去初始化tips的界面。

愚人节活动(2020.3.20)

活动页面显示三个道具,只有一个是真道具,根据用户的选择获得标注数量的真道具,如果猜错获得一半数量。根据配置显示服务器发过来的道具,发过来的消息会包含配置中的index,用这些index读baseid,然后正确的初始化itemicon。

用户上次的选择:要求是界面在用户选择道具后显示猜中和猜错两种文字,直到下一次活动开启前,关闭ui或重新登录后再打开活动界面都可以看到上次选择的结果。首先是可以参与活动的情况,选择之前服务器会告知客户端哪个是真道具,在玩家选择后将其与玩家的选择比对,根据结果刷新ui;然后是在下一次活动开始前打开界面,这时ui的初始化就要用到上次选择的结果,这个信息问data要,我们在每次选择后都会将正确与否储存在data中,而如果是重新登录,服务器发来的消息也包含上一次的选择结果。

刷新ui:分三种情况,当不需要倒计时且剩余次数大于零时,刷新真假道具icon和描述;当次数为0时显示无次数文本,描述为上次选择的结果;最后一种则是需要倒计时,显示倒计时文本和描述(上次选择的结果),同时刷新道具icon为真道具。

倒计时:下一次活动开启前打开界面会有格式为“x分x秒”的倒计时,首先想到用update方法每帧去查询下次活动开始时间与服务器时间的差值,如果大于零则说明需要倒计时,然后利用帧间隔做减法,取到整数部分更新到屏幕上。由于显示的时间只精确到秒,所以没有必要每帧都去刷新ui,可以存一个累计变量,当它达到一秒后,再对时间做更新。要注意的是,判断是否需要倒计时是通过查询下次活动开始时间与服务器时间的差值,ui界面刷新时会受到这个结果的影响,目前设置的阈值是1,也就是说只有当差值大于1时才说明需要计时,不设成0是为了防止服务器在时间相等后发送了更新消息,在更新消息中刷新ui时如果判断为真则会去初始化倒计时而不刷新道具icon了。

序列帧动画播放:如果要用sequence创建序列事件,运行runAction的不能是ui自己,要单独创建一个sprite,然后用这个spite去run,要特别注意,就算是播放序列帧动画,期间发生的变换也是未知的,因此这个sprite上最好不能有任何别的需要显示的东西,否则播放动画时会出现不可预测的问题。位置就是这个sprite的位置,我们希望动画在屏幕正中间播放,那么可以将这个sprite移到ui的中间,创建时我们将其加入了ui背景的child,默认会在背景的左下角,只需要将sprite的坐标设为背景长宽的一半就到了正中间(注意是背景size,不是ui的size)。

禁止用户操作:要求在播放动画时禁止用户输入,只需要维护一个变量记录是否正在播放动画,在执行动画播放时置为true,在动画播放完调用回调时置为false,然后在每个按钮点击时加入这个变量的判断,只有在未播放动画时才响应点击。

动画背景遮罩:在播放动画时为了突出动画效果,需要在背景生成一个黑色半透明遮罩,实现的方式参考显示ui时背景的遮罩,创建一个LayerColor,大小为cc.Director:getInstance():getVisibleSize(),即窗口大小,然后将其加入ui的child,并在适当的时候显示。

道具icon环绕特效:根据道具的品质在icon周围显示相应颜色的环绕特效,其实也是一个序列帧动画,只是会不停播放,然后是颜色,品质环绕动画的颜色是纯白,我们根据道具的baseid拿到quality之后,根据配置中的颜色来对icon中的effect2成员设置颜色,这样环绕动画的颜色也会变成相应的颜色,因为序列帧是通过这个成员run的。

场景载入完成后上漂:这里的关键是如何判断当前场景已经载入完毕,载入界面在关闭时会调用GameData中的方法告知其场景已经完成载入,然后GameData会依次告知所有data类型的脚本,再在data脚本中做相应的处理,因此要先将处理函数注册给GameData

Bug1:本地计算时间时,每次累计超过一秒就将时间减一,这样其实每次都会累计一点误差,当倒计时比较长的时候会累积到比较大的误差,导致超过与服务器同步时间的阈值,解决办法是每秒直接同步服务器时间,不本地去减了,其实也可以改成减去累计的实际时间,即比一秒钟多一点的时间消除误差,但是为了保险还是直接同步吧,反正也每帧去对比了,如果某个地方出现误差并且累计了,阈值设多高都没有意义。

Bug2:最后一轮活动参与的倒计时到了之后界面不刷新,原因是ui刷新的时刻是收到服务器消息的时候,刷新的方法会检查时间差,小于差值才刷新,如果提前收到了消息就不会刷新了,解决方法是本地再弄一个倒计时,到点了就刷,这样不管是否提早收到了消息,都能保证屏幕一定被刷新。

Bug3:之前参加了活动,活动结束之后,切换地图显示有活动上漂,原因是显示上漂时没有判断是否在活动时间内,想当然的以为服务器会告知。

几个bug问题都是出在边界条件的处理上,还有服务器跨零点或者活动结束时的情况,以后一定要记得考虑到,多测试。

UI注意事项

(1)有时ui的回调函数需要用到闭包,即在回调函数外创建的panel实例,在回调函数中使用,直接使用该实例是不安全的,如果这个ui在外部已经被析构,那么触发回调函数时这个panel就会变成野指针,正确的做法是通过UIManager去get,而且一定要记得get了之后每次使用自己ui实例的时候都要进行判空,否则就没有意义了。

(2)创建ui后要记得马上调用showToScene方法,否则在该帧结束时会被cocos的内存管理回收。

(3)创建新ui时,有两个问题要注意,一个是灰色遮罩,需要手动激活显示,另一个是zorder层级,要在配置文件中设置zorder值。

选国功能移植(2020.4.15)

拒绝堆屎山。拒绝不了的时候只能优雅的根据屎山的结构尽量去优化。 本来这个功能是版本1的,现在要移植到版本2,版本1只有三个国家,实现很蛋疼,三个国旗可以选择,csd里面就拼了三个一模一样的节点,然后手动加载了不同的国旗图,版本2现在有十个国家,就要重复十次相同的操作,由于这是登录创角界面,不能有任何差错,因此我就傻x的还按之前的结构每个需要用到的节点都去注册,只对使用它们的方式做了优化,之前是同一种操作根据不同国家输入写三次,我把需要操作的节点存入若干table,然后用countryid作为索引直接遍历,但是节点还是一个个去注册了,这就导致如果策划改动了csd中的父子结构,设置可见性时就会受影响。最理想的实现方式应该是把每个国旗弄一个单独的csd,然后初始化选国界面的时候依次按国家data去初始化,这样任何改动只需要做一次,也不用复制粘贴节点,最多就是多了一步读配置的操作。但目前策划保证这个界面不会有新的改动(之前也是这么说的,导致我偷懒没有把版本1的代码优化干净),为了节省时间,通过每个国旗的父子关系使用getChildByName方法拿需要的节点,这样就只需要注册十个国旗最上层的节点,前提是父子结构未来不会更改,否则代码又要改。其实稍微想想就知道这样的实现同样不符合ETC(easier to change)原则,完全依赖策划对未来的保证,如果下次再动一定要优化成单独的cell。

itemicon的大小是定死的78x78像素,但会根据所在父节点scale变化。如果有地方整体变大或变小了,可以根据变化的比例去设置icon的scale同步变化。

创角界面的动画

新建角色界面中,选择诸如发色、性别等属性时,各属性ui选择后会从上方飞入,这是CocosStudio中做好的动画,实现方式是在时间轴上的同一段间隔内对需要的所有属性ui节点制作飞入的效果动画,然后点击时根据点击的类型隐藏其他类型的ui,并播放这一时段的动画,玩家就能看到相应的ui飞入。有意思的是对于属性的点击响应,这个功能并没有像其他ui一样为按钮注册响应函数,而是重载了父类UIPanel的通用方法onPanelTouchEnded,它(似乎)会响应该界面所有位置的点击,然后根据点击结束的位置,来判断点击了哪个按钮。

退出选角界面后长时间的转圈表现

Callback脚本中定义了一些全局方法,显示转圈界面的方法就定义在里面,在UILogin中请求服务器列表时,执行了一个sequence,让UICircle开始转,当拉取服务器列表有结果了之后(或达到时间上限)会将其关闭。这里有个问题就是,为何从游戏中退出到登录界面就不会有很久的转圈时间,而从选角界面退出来就要转十几秒。怀疑和拉服务器列表有关

那么服务器列表是如何拉取的呢?首先读取热更配置中的区列表下载地址,然后DownloadManager负责下载,这里涉及到C++方法的调用,下载和判断的过程是在C++层完成的,完成后会响应一个lua的回调,关闭转圈ui。下载的过程还存疑

家族界面进度条优化(2020.5.7)

给定一个进度条,设置了三个百分比$\alpha_1$、$\alpha_2$、$\alpha_3$,现在要求根据配置中的三个值a、b、c来更新进度条的表现,其中c为最大值,比如当前的值t小于b大于a,那么进度条应该在0到$\alpha_1$的区间内,如果大于a小于b,那么应该在$\alpha_1$和$\alpha_2$之间,如果等于c那么进度条应该到底,进度条应该按照超出的不同大小按比例在相应的区间内移动。这里不能直接用$t\over c$来得到百分比,因为abc所对应的比例可能与$\alpha_1$、$\alpha_2$、$\alpha_3$不同,如果直接除比例显示会不正确甚至超出当前区间。要实现这种效果其实很简单,首先判断当前t处于哪个区间内,然后计算超出当前区间的数额在当前区间的比例,最后乘以此区间的百分比占总百分比的比例即可。

增加货币上限(2020.5.8)

需求是将某货币的上限从五位数增加到七位数(单位为一万个基础货币),如果超出则显示99999,看起来是个十分简单的需求,但由于服务器消息是32位的,加到七位数后,所代表的基础货币数额已经超出32位数的上限,造成溢出,导致显示错误。因此需要改用专门的64位消息接收货币,但之前众多功能和接口都是使用的老消息的接口,且之前的实现是把货币信息和玩家信息放在一起,这导致两个问题,如果将货币数据从玩家信息中移出单独存储,要修改的地方太多,出错风险高,而若要使用原有的结构存储,收到老消息时就会覆盖掉原有的数据。解决方法是依然将货币数据分开存储,每次收到老消息都立即将本地的货币信息覆盖到老消息上,这样分发的时候就不会覆盖,然后每次收到新消息时都将货币信息同步到之前的玩家信息中,这样那些通过玩家信息获取货币信息的地方就不用改动,然后如果新的代码直接调用单独的货币接口。

端午节龙舟活动(2020.6.8)

在活动时间内可点击icon弹出主界面,点击前往报名寻路至npc,在npc对话界面中点击报名开始匹配,同时在任务栏生成一个cell显示匹配状态,匹配成功后出现二次弹框,点击确认进入地图,点击取消关闭,再次点击任务栏cell弹出二次弹框。

NPC位置由服务器发过来的九屏信息同步。任务栏本身是会有一系列的分支,根据玩家目前的任务进度点击cell会跳到不同的分支执行不同的逻辑,但是我们现在只需要它响应一个点击,因此用到了之前项目写好的虚拟任务接口,从虚拟任务基类派生一个子类,它就是我们的cell,然后实现该cell需要表现的接口,然后在需要显示cell时创建实例并添加进TaskData中储存虚拟任务的表。剩下的就是在正确时候给服务器发送消息,并在收到消息时做相应的处理了。

Debug日志

(1)碰到创建tableview后模拟器卡死的问题,排查后发现是在tableViewAtIndex和numberOfCellsInTableView方法中打印太多表类型日志导致。

(2)调用一个很常见的接口,但是老是报错,最后发现是由于是util通用接口,应用.操作符而不是冒号。已经不是第一次遇到这个问题了,要留心。

(3)该活动是由以前的老活动改写而成,以前活动的内容没有变,只是从新的入口进入,为了区分将以前的老协议重新定义但实现没有改,有少部分比较通用的活动接口比如hasFinish判断活动是否结束没有改,导致如果参加完老活动不退出游戏也不重新登录,再开一局新活动,上次进去残留的数据会被hasFinish读取,导致错误判断活动结束。解决方法是重新为新活动定义接口,或者想办法在退出场景时清楚残留的数据。

避暑山庄活动(2020.7.6)

和龙舟活动类似,是一个移植的活动,要点还是根据新的活动需求对之前的协议进行复用,玩法逻辑和之前保持不变,只是增加了一个专属的货币积分用作筹码,没有什么特别的东西。bug就不单独列出来了,统一放到合集中。

宝石争霸活动(2020.7.19)

今日如果有活动显示红点,点击一次后消失。为了让客户端在每次启动时知道玩家是不是本日首次登录,可以在本地存一个键值对,key是玩家的thisid加上本日的起始时间,这样只需要查找有没有这个值就知道今天是不是首次登录了,然后条件再加一个是否点击过的flag进行判断,如果未点击则一直返回true,如果已经点击则将flag置为false,并加入键值对,这样当天都不会判过了。注意在设置点击flag时也要取键值对做一下判断,否则点击flag会被后面的点击设回true,下次跨天后就进入了错误的条件。

有一个需求是在进入活动地图后在任务栏显示当前各个玩家的状态。任务栏是一个公用的UITaskTrace,目前的实现是根据进入的不同任务地图显示不同的info界面,每次有新的活动要显示ui时就在一连串if中再加一个判断,看看是不是进入了某某地图,然后创建那个活动的ui,活动ui由UITaskTrace管理,UITaskTrace由UIManager管理,需要刷新的时候先由UIManager拿到UITaskTrace,然后将数据传入活动ui进行刷新。

一个非常简单的问题,要求每次登陆弹一次提示框,之后就不弹了。只需要在onLoadingDone里面加一个标志位判断是否弹出即可。

地图采集矿石,要求点击后移动过去,到达了开始读条采集,读条完后采集完毕,读条可以被打断。为了让客户端表现同步,先请求开始采集,本地读条结束后再请求读条结束完成采集,服务器验证后删掉矿堆给宝石。

挂机按钮自动拾取。以前有自动拾取道具的逻辑,先拿到地图中所有掉落,然后遍历检查格子坐标,如果当前道具正好在主角格子处,则直接请求拾取,否则计算xy坐标距离之和作为阈值,每次遇到比上次小就更新,以此找到最近的道具,然后调用移动接口向其移动。在挂机时有三个状态,拾取、攻击和随机移动,每帧更新到下一个状态,不断循环,宝石争霸的掉落不是普通道具,而是生成的NPC,要实现挂机自动拾取宝石,只需要在之前的拾取方法中加入宝石争霸地图的特殊处理,方法和找道具一样,只是把拿道具换成拿所有NPC,并检查一下是不是宝石,然后按之前的操作跳到下一个挂机状态。

要求头顶刷新当前持有的宝石数量,并可以看到其他玩家的宝石数量。之前为了显示实时排行榜,在地图中会不断刷新玩家积分等信息,我们此时通过发过来的玩家id找到玩家实例,一并设置玩家头顶的宝石icon和数量,并在character脚本中加入设置阵营的函数来选择宝石颜色。有了宝石阵营,就能很容易在AttackManger中进行敌我识别,同阵营队友不可攻击,不同阵营可攻击。阵营同样可以方便设置玩家名字颜色,根据主角阵营区分队友和敌人,返回不同的颜色。

Debug日志

(1)看不见其他人的宝石数量,原因是对主角显示了icon,而没有将所有人都显示

(2)无法采集和读条,采集消息是请求了的,只是被移动打断了,因此不能在移动中请求和显示读条,而应该在移动结束后和访问NPC回调中处理

(3)连续点击采集进度条可能消失,然后无法采集,移动后恢复,原因是请求了移动打断了读条,所以服务器需要在每次访问NPC后重新开始计算读条

(4)准备阶段本来只同步一次时间,然后客户端自己倒计时,现在每秒同步后出现刷出无倒计时文本(%s)和正确倒计时交替出现的bug。原因是之前服务器只同步一次时间信息,由客户端自己通过RepeatForever一个sequence倒计时,后来服务器改为每秒同步信息,就会出现交替刷的情况,把逻辑改为只在服务器刷即可

(5)退出副本按钮是通用的,在需要显示时设置对应的回调函数以发送退出副本的消息,不同数据类型都会在GameData脚本中注册onLoadingDone方法,各自处理场景加载完毕之后的逻辑,然后根据特定的枚举去设定退出副本的回调

(6)在挂机Manager中设置了宝石争霸地图中按钮始终显示,只判断了宝石争霸地图,覆盖了上面的判断,因此不在宝石争霸地图中的话就无论如何不会显示了,简单的bug,完全可以避免

Bug合集

  • 配置中定义ui的callback方法,使用self调用该ui脚本中的方法,发现调用不到,原因是这个callback方法中定义了另一个callback2,来作为二次确认弹框的回调,而如果在callback2中使用self,得到的就是二次弹框ui的实例,而不是原ui实例
  • 排行榜刷新,在创建时请求数据,服务器数据清零后再点开会残留上次的奖励icon,关掉再开就消失,原因是奖励icon只有在服务器发新itemid过来时才会remove掉然后创建新的,导致如果发过来没有id之前的就不会清,而在ui刚创建时会用已有数据也就是上次数据先刷一次,等收到服务器新数据再刷的时候就残留了上次的icon了

Comments

Content