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");”我们已经无需理会了。它做的应当是客户端的逻辑,有兴趣的可以自行验证。