您的位置:首页 > 其它

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

2017-02-20 14:51 363 查看


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

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

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

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

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

--
注册点击事件

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

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

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

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

   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]:onPickUp和self: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;

 

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

   
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中。

actionCache在sync函数中是被使用:

--
同步所有操作

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中被用作queue的key。在reSendMsg中又根据key对queue进行了排序。简单说,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");”我们已经无需理会了。它做的应当是客户端的逻辑,有兴趣的可以自行验证。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐