00003 不思议迷宫.0006:客户端的操作如何反应到服务器?

来源:互联网 发布:地热模拟软件 编辑:程序博客网 时间:2024/04/28 08:18


00003不思议迷宫.0006:客户端的操作如何反应到服务器?

玩家点击手机屏幕,根据点到内容的不同而执行不同的操作,比如切换画面或者场景、播放动画或声音、发送数据等等。我现在所关心的是点到物品,比如主界面中的海怪触手、漂流瓶、罐子等等,还有地牢中神龙许愿点击99次矿物后才出现的钻石。

我在主界面的创建代码中未能找到海怪触手、漂流瓶、罐子之类的相关代码(可能有,但被我忽略了;也可能是确实没有,它们都是动态创建的),所以研究下地牢中的物品捡取吧。

地牢界面的入口代码为/src/game/ui/form/dungeon/UIDungeonMain.luac。打开瞅瞅,发现代码很长,那如何快速找到我们所关心的物品捡取事件呢?其实办法很简单,只要搜索就行。

所谓物品捡取事件,其实也就是屏幕点击事件,只不过点击到的是物品。在cocos2dx中,点击事件需要通过addTouchEventListeneraddClickEventListener之类的函数进行注册。查找结果让我有点意外:

--注册点击事件

functionUIDungeonMain:registerTouchEvent()

   -- 卷轴按钮

   local btn_Magic =findChildByName(self.node, "CT2/juanzhou");

   local function onMagicOnClick(sender,eventType)

       if eventType == ccui.TouchEventType.endedthen

           AudioM.playFx("button_spell");

 

           if ME.user.forbidToOpenMagicUI andnot DungeonGuideM.isGuideFinished() then

               -- 禁止打开魔法书界面

               return;

           end

 

           -- 打开魔法书界面

           self:showMagicScrollUI();

       end

   end

   AddTouchEventListener(btn_Magic,onMagicOnClick);

 

   -- 宝物按钮

   local btnTreasure =findChildByName(self.node, "CT2/baowu");

   ……

   AddTouchEventListener(btnTreasure,onTreasureOnClick);

 

   -- 英雄格子

   localbgHero = findChildByName(self.node, "CT2/bg3");

   ……

   AddTouchEventListener(bgHero,onBgHeroOnClick);

 

   -- 施法选择目标背景

   local screen_bg =self.node:getChildByName("select_target_bg")

   ……

   screen_bg:addTouchEventListener(onOnClick);

 

   -- 称号冒泡点击事件

   local careerBubble =findChildByName(self.node, "CT2/career_bubble/bg");

   ……

   careerBubble:addTouchEventListener(onBubbleClick);

end

和捡取物品一点关系都没有!

好吧,那就换个思路。想想,既然是捡取物品,那相关的函数的函数名或者函数代码中总应当出现itemequipment这些字样吧?这次确实找到了:

   -- 注册捡取物品的处理函数

   EventMgr.register("UIDungeonMain", event.PICK_UP_ITEM,function(params)

       self.grids[params.pos]:onPickUp(params.bonus, params.newBonus,

           params.isItemVisible, params.noAlert, nil, params.borrowGrid);

       localpos = params.pos;

       localtype = params.type;

 

       -- 如果是拾取地图

       if type== GRID_TYPE_MAP then

           -- 判断邻格是否开启

           local adjoinGrids = DungeonM.getAdjoinGrids(pos);

           fori = 1, #adjoinGrids do

               local targetPos = adjoinGrids[i];

               local ok = DungeonM.canOpenGrid(targetPos);

               if ok == GRID_OPEN_OK then

                   self.grids[targetPos]:gotoVisible();

               end

           end

       end

 

       self:whenPickUpItem(params);

       -- 更新界面UI

       --self:updateUI();

   end);

拾取地图这什么鬼?先不管它,还是研究下self.grids[params.pos]:onPickUpself:whenPickUpItem

找到self.grids的赋值处,确定self.grids[params.pos]的类型:

   -- 生成格子

   self.grids ={}

   for i = 1,GRID_ROW_NUM do

       for j =1, GRID_COL_NUM do

           ……

           local grid =UIGrid.create(……);

           ……

           self.grids[index] = grid;

           ……

       end

       ……

   end

UIGrid.create。再看看UIGrid.onPickUp

--道具捡取回调函数

function UIGrid:onPickUp(bonus, newBonus,isItemVisible, noAlert, curNum, borrowGrid)

   ……

   if bonus[1]== 1 then

       -- 物品

       localitemId = bonus[2];

       localnum = bonus[3];

 

       localitemName = ItemM.query(itemId, "name");

       ……

 

       localitemsFlyIn;

       localdelayFly = false

       ……

       itemsFlyIn = function()

           -- 飞入效果

           local fileName = ItemM.query(itemId, "icon");

           local iconPath = getItemIconPath(fileName);

           ifSpellM.isSpell(itemId) then -- 卷轴

               ……

           elseif EquipM.isEquipment(itemId) then -- 宝物

               ……

           elseif DragonWishM.isWishItem(itemId) then -- 龙珠

               ……

           elseif SkyResourceM.query(itemId) then -- 天空物资

               ……

           elseif PropertyM.isProperty(itemId) then  -- 道具

               AudioM.playFx("pick_goods");

               pickupPropertyEffect(self, UIDungeonMgr.getCurLevel():getEquipNode(),iconPath, itemId, num);

           else-- 其他

               if not noAlert then

                   str = itemName;

               end

               local itemBar = uiCurLevel:getFreeItemBar();

               local iconNode = itemBar:getChildByName("icon");

               local textNode = itemBar:getChildByName("num");

               local total = (nil == curNum) and ItemM.getAmount(ME.user, itemId) orcurNum;

               gainSpecialItemEffect(self, iconNode, iconPath, textNode, total, num);

               AudioM.playFx("pick_goods");

           end

       end

 

       -- 不需要延迟,直接播放飞的效果

       if notdelayFly then

           itemsFlyIn();

       end

       ……

   end

 

   ……

end

仅仅是播放相关特效,并不涉及物品数量的处理。那么物品数量的处理在self:whenPickUpItem中?

--拾取物品的回调

function UIDungeonMain:whenPickUpItem(args)

   local dungeonId= DungeonM.getDungeonId();

   local layer= DungeonM.currentLayer();

 

   for _,taskInfo in ipairs(DailyTaskM.getTaskList()) do

       iftaskInfo.dungeon_id == dungeonId and taskInfo.floor == layer then

           ……

       end

   end

end

真是失望,还是没有。只能看看event.PICK_UP_ITEM,是在哪里被触发的了。

src目录中搜索包含“PICK_UP_ITEM”的文件,然后发现了DungeonM.luac中内容:

--拾取物品

function pickUp(pos)

   ……

 

   -- 格子

   local grids= getCurrentDungeon();

   local grid =grids[pos];

   local bonus= grid.bonus;

 

   ……

   -- 奖励清除

   grid.bonus =nil;

   grid.picked= true;

 

   ……

 

   -- 增加行为

   -- 这一句必须放在doBonus之前,以保证命令的先后顺序(doBonus中也会触发事件)

   addAction({ ["cmd"] = "pick_item",["pos"] = pos, })

 

   localresult;

   -- 如果是立即使用的道具,那么使用掉

   -- 应该在ProertyM里面做的,而且已经有现成的功能(配置auto_use便可)。by panyl

   if bonus[1]== 1 and type(bonus[2]) == "number" then

       ……

   else

       -- 奖励

       result = BonusM.doBonus(bonus, "pick_item");

   end

 

   -- !!!!!!!!!!!!!!!!!!!!!!!!

   -- 这里事件必须在奖励之后做。因为拾取第一颗龙珠时,会去抽取许愿选项,

   -- 需要抽取随机数,等等,如果不放在奖励之后就会导致先后顺序问题,而且拾取龙珠/抽取许愿还不在同一个回合中

   -- 一个回合事件

   EventMgr.fire(event.COMBAT_ROUND, { ["pos"] = pos,["isDelay"] = true });

 

   -- 事件

   EventMgr.fire(event.PICK_UP_ITEM, {["bonus"]= bonus, ["pos"] = pos, ["newBonus"] = result,["type"] = grid.type, ["class"] = grid.class, });

 

   -- 尝试完成成就:获得竞技场对手物品

   EventMgr.fire(event.GET_ARENA_ENEMY_ITEM, {["bonus"] = bonus,["pos"] = pos, });

 

   Profiler.funcEnd("pickUp");

   return true,true;

end

EventMgr.fire(event.PICK_UP_ITEM”是我们寻找的内容,但它不重要,因为我们已经知道它只干界面上的事。我们需要弄明白在它之前发生了啥。阅读了代码之后,有两句话引起了我们的注意:

   addAction({ ["cmd"] = "pick_item",["pos"] = pos, })

   result = BonusM.doBonus(bonus, "pick_item");

addAction的参数中,我看到了“cmd”,这个词和网络相关,看起来需要重点关注。

--地牢探索行为

function addAction(action, delay)

   ……

   -- 加入延时action

   if delaythen

       table.insert(delayAction, action);

       return;

   end

   ……

   -- 第一个字节存放id,第二个字节pos,接着四个字节存放操作数据,最后两个字节存放操作累计次数,共8个字节

   localactionId = DungeonActionM.query(action.cmd, "id");

   localpos = action.pos or 0;

   local data =action.data or 0;

 

   local num =#actionCache;

 

   -- 看下是否需要合并,如果前六个字节一样(idposdata)则需要合并次数

   if isSame…… then

       combine(……)

       return;

   end

 

   -- 新插入的一个操作

   -- 8个字节

   local buf =Buffer.create(8);

 

   -- id

   Buffer.set8(buf, 1, actionId);

 

   -- pos

   Buffer.set16(buf, 2, pos);

 

   -- data

   Buffer.set32(buf, 4, data);

 

   -- times

   Buffer.set16(buf, 8, (action["times"] or 1));

 

   table.insert(actionCache, buf);

 

   addDelayAction();

end

addAction函数将参数作了转化,然后保存在actionCache中,最后调用了一次addDelayAction,这是为毛?

function addDelayAction()

   localtoAction = delayAction;

   delayAction= {};

 

   for _,action in pairs(toAction) do

       addAction(action);

   end

end

addAction函数中,如果指定了第二个参数为true,则第一个参数action将被保存到缓存而非actionCache中,缓存的内容直到下次调用addAction(x, false)时才会被保存到actionCache中。

actionCachesync函数中是被使用:

--同步所有操作

function sync()

   ……

   -- 如果有缓存的操作

   Operation.cmd_dungeon_action(dungeonContainer.identify, actionCache);

 

   -- 清空缓存

   actionCache = {};

   ……

end

应当是发送给服务器端的,网络流程和上一章中的数据验证应该是一样的,不过我们还是验证一下。Operation.cmd_dungeon_action

function Operation.cmd_dungeon_action(identify,actions)

   -- 当前层

   local layer= DungeonM.currentLayer();

 

   -- 最后的属性

   DungeonLogM.collectFinalData(layer);

 

   -- 行为队列是空的

   if #actions<= 0 then

       return;

   end

 

   -- 把所有操作都连接成一个buffer

   local buf =Buffer.create(0);

   for _,action in pairs(actions) do

       buf =Buffer.append(buf, action);

   end

 

   -- 获取随机数游标

   local randomCursor= RandomFactoryM.packRandomCursor();

 

   -- 等待应答id(一个唯一的id

   local authId= os.time();

 

   local v = {

       ["identify"]   =identify,

       ["auth_id"]    =authId,

       ["layer"]      = layer,

       ["cursor"]     =randomCursor.value,

       ["args"]       =buf.value,

       ["attrib"]     =SimpleEncryptM.collectAttribCoding(ME.user),

   };

 

   -- 等待队列

   DungeonServiceM.addWaitSync("CMD_DUNGEON_ACTION", v);

end

actions被转换到连续的空间中,然后和其他一些参数一起,传递给DungeonServiceM.addWaitSync。其中有个参数["attrib"]     =SimpleEncryptM.collectAttribCoding(ME.user),显得可疑。

DungeonServiceM.addWaitSync函数:

--缓存同步操作消息,如果没有收到服务器的应答就重发

function addWaitSync(cmd, msg,only_one)

   local id = msg.auth_id;

   msg.only_one = only_one;

   queue[id] = { cmd, msg, };

 

   -- 先发一次

   reSendMsg();

 

   -- 定时重连

   ScheduleM.createScheme("DungeonServiceM", reSendMsg,SYNC_MSG_REPOST_TIME, true)

end

reSendMsg函数:

--重发消息

function reSendMsg()

   local keys =table.keys(queue);

   ……

   -- 如果消息已经空了或者已经离开游戏了

   if notME.isInGame or (#keys <= 0) then

       -- 移除定时器

       clearMsgQue();

 

       return;

   end

 

   -- 如果网络没连接上就不管了

   ……

 

   -- 不能同步

   ……

 

   -- 发送每条同步消息,按照id排序

   table.sort(keys);

   for _, id inpairs(keys) do

       ifqueue[id] then

           local cmd = queue[id][1];

           local v  = queue[id][2];

           SyncM.addMessage(cmd, v);

       end

 

       -- 如果是仅发一次,删除

       ifqueue[id][2].only_one then

           queue[id] = nil;

       end

   end

 

   SyncM.startSync();

end

Operation.cmd_dungeon_action中,传递了authId = os.time();该值在DungeonServiceM.addWaitSync中被用作queuekey。在reSendMsg中又根据keyqueue进行了排序。简单说,queue是按照时间先后的顺序进行排序的。之后,就按照顺序依次传递给SyncM.addMessage

SyncM.addMessage仅将数据存到内部缓存:

--添加一条同步消息

function addMessage(cmd, para)

   table.insert(todoMessages, { cmd, para });

end

reSendMsg函数的最后,调用SyncM.startSync进行真正的同步操作:

--开始进行同步

function startSync()

   -- 判断

   if notcanSync() then

       return;

   end

 

   local socket= require "socket";

   lastTime =socket.gettime();

 

   -- todoMessages的消息内容剪切出来

   messages =table.append(messages, todoMessages);

   todoMessages= {};

   if(#messages == 0) then

       -- 没有任何数据需要同步

       lastTime= -999;

       return;

   end

 

   -- 先锁住

   locked =true;

 

   -- 发送消息给服务器,开始同步

   trace("SyncM", "开始进行同步,版本号(%d),消息数量:%d", sync, #messages);

   Communicate.send("CMD_START_SYNC", { sync =sync });

   for _, v inipairs(messages) do

       Communicate.send(v[1], v[2]);

   end

   Communicate.send("CMD_END_SYNC", { sync =sync });

end

最后是几个Communicate.send调用。它的详情,在前面介绍过,此处不再赘述。

同步完成后还有一个CMD_END_SYNC回调:

--同步完毕

function endSync(success)

   success =iif(success == nil, true, success);

   trace("SyncM", "同步完成,当前同步版本号:%d",ME.user.dbase:query("sync", 0));

   sync =ME.user.dbase:query("sync", 0);

   messages ={};

   if (notsuccess) then

       --同步失败了后续的东西也不需要再同步了,直接丢弃以服务器为准

       todoMessages = {};

   end

   ……

   -- 解锁

   locked =false;

   ……

end

在服务器接收到CMD_START_SYNC之后,就开始处理我们的各种点击事件了(在(x,y)位置捡取物品z)。

DungeonM.pickUp函数中的另一个惹人注意的语句“result =BonusM.doBonus(bonus, "pick_item");”我们已经无需理会了。它做的应当是客户端的逻辑,有兴趣的可以自行验证。

0 0