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