客户端#
服务端看得差不多了,我们再来研究一下客户端。
联机大厅#
连上服务端的第一步就是根据服务器返回的(roomlist, events, clientlist, wsid)来执行客户端的roomlist函数,其位于noname/library/index.js
的message.client
变量下。
首先客户端会执行服务端的key函数,把本地的onlineKey发送给服务器,以更新到ws对象中;
接着就是更新本地的最近联机ip,并执行服务端的changeAvatar函数,把本地的名称(nickname)和头像(avatar)更新到ws对象中;
然后就是手动调用本地的updaterooms和updateevents来更新房间列表和约战;
然后就是重连检测,如果对比上了缓存房间id,则执行服务端的enter函数来进入这个房间;
最后是房间退出后的重开和重连。
在上述内容中,我们主要关心两个东西,一个房间列表的点击事件,二是创建房间的点击事件。对于前者,其在updaterooms中将事件赋给了ui.click.connectroom
,其位于noname/ui/click/index.js
中,其大部分判定没啥好说的,最后无非就是执行了服务器的enter函数;对于后者,其事件为ui.click.connectMenu
,其位于noname/ui/create/menu/index.js
中,刚点击还只是一个设置UI,其和本地共用一个menu,均定义在noname/ui/create/menu/pages/startMenu.js
中,只不过会根据是否处于联机而改变UI样式,在联机模式下其会将配置保存在lib.configOL
,最后通过执行服务端的create完成房间的创建。
开房机制#
至此我们已经搞清楚了基本的UI交互,创建房间的起点就是执行服务端的create函数,而之前我们知道服务端所做的只是保存一些必要信息后,又将执行权返还给了用户,因此对于开房机制的探究,应该从函数lib.message.client.createroom()
开始。
我们可以看到其实际就是执行了本地的模式切换函数game.switchMode(lib.configOL.mode)
,即从connect模式转化为联机设置中的模式。其只有两个核心步骤,第一执行对应模式在mode
的js脚本文件,第二模仿proceed2来执行模式加载完后的回调。我们可以看到过程基本一致,最后还是得进行模式中的start函数,因此联机房间实际还是在mode对应的文件中。
在模式的start中,进行一些初始化操作后,其基本都会根据是否为联机状态来调用game.waitForPlayer
函数,这就是联机的等待房间,当然game只负责创建事件,具体的执行在content.js
中。
首先调用
game.createServer
在本地创建服务器,其就是在本地8080端口创建了一个ws服务器,并使用lib.init.connection
监听连接事件,当然这玩意处于内网也只有本地可以连接到,先不管它;先看连接事件,首先将连接到的ws封装到Client对象中,并保存到
lib.node.clients
数组中,然后监听message和关闭事件,并向它发送开始的信息client.send("opened")
;Client定义于
library/element/client.js
中,总共只有两个方法,close没啥好说的,send主要做的是让连接到ws服务器的对象发送指令数据,特殊地其会让函数参数改为指令exec
;关闭事件没啥好说,看message事件,其接收一个json数组,第一个参数为
lib.message.server
下函数,后面为执行这个函数的参数,例如收到[“tempResult”, result],就会执行lib.message.server.tempResult(result)
。
接着就是给房主加个主的标记,并执行服务端的config来将本地的联机设置
lib.configOL
更新上;然后创建等待界面UI,把房主放进去,将服务器状态改成等待中,并通过
game.pause()
暂停游戏;我们把视线转化到开始游戏的点击事件,其位于
noname/ui/create/index.js
中,首先根据game.online
(创建房间后会变成false)判断按钮为“开始游戏”还是“退出联机”,退出的就是执行game.reload()
来重新加载游戏;而房主则会判断
game.connectPlayers
中空位个数,如果大等于需要人数减一(即没有两个人),则报弹窗结束点击事件,达标的话则执行game.resume()
恢复游戏。
最后就是,先修改各种状态变量(非等待状态、游戏中等),然后移除各种无用UI,并向全体玩家通过
game.broadcast("gameStart")
广播游戏开始。
实际上,房主以外的玩家自己也会执行mode下对应模式的文件,只不过要配合房主运行,具体让我们来看下部分内容。
进房机制#
当我们点击选项房间列表的某个房间时,由之前可知,实际就是房主执行了lib.message.client.onconnection()
并把我们的wsid传入。让我们仔细探究一下其干了什么吧。
其只有一句话,但包含两个步骤,首先创建一个NodeWS对象,并将其保存到
lib.wsOL
,索引为wsid,然后执行lib.init.connection
函数;NodeWS对象定义于
noname/library/element/nodeWS.js
中,用于房主保存连接用户数据以跟本地房主的ws服务器进行相互响应,其相当于一个代理服务器;其核心方法为send,但实际就是房主自己通过与服务器连接的ws将数据发送到服务器。
而后来执行的连接函数,就是房主ws服务器监听的事件,此时在房主那边又得到了一个Client的身份,其保存最开始连接创建的NodeWS对象,因此执行
client.send("opened")
,就相当于房主通过服务器告诉连接用户执行本地的lib.message.client.opened()
函数;在这个函数中,后半部分为重连,不用管,其核心就是向服务器发送init指令,但我们知道此时客户端已经属于某个房间有了owner,因此服务器只是转发,并附上相应的wsid。此时房主通过
lib.message.client.onmessage()
响应此事件,若不在连接的lib.wsOL
中则不管,否则执行对应NodeWS对象的onmessage进行响应,其作用如之前所说就是分exec和非exec,来执行房主lib.message.server
下的函数;所以最后就是执行了房主的
lib.message.server.init
,其包含一系列的判断过程。首先判断是否为被禁止进入的玩家;接着判断是否为连接上的断线玩家,是的话则以加入游戏的方式进行reinit;然后再进行版本号比较;接着判断是否为等待状态,非等待时给房主加上诸如移除旁观之类的按钮,并让玩家以旁观身份reinit(在客户端的reinit中会根据第5个参数来给自己加上observe的标签);接着看房间是否已满员;最后玩家终于以等待的方式进来了,房主会在game.connectPlayers
补上这个玩家,并让客户端执行自己的init;在客户端的init中,先进行了一系列的配置和UI生成,不用管它,关键是后面,房主以
game.switchMode
切换模式,而客户端以game.loadModeAsync()
来切换模式,两者都要载入mode对应的js脚本,差别是不会进行模式、卡牌和角色之类的导入工作,而将其放到了回调函数中;在回调函数中,进行了数据库的初始化,以保证卡牌、角色之类的信息存在于本地,没有进行禁将之类的操作。而后续的事件中,其创建game事件但内容不是模式的start而是函数
lib.init.startOnline
,最后进入游戏循环,并向房主发送inited以更新初始化结束状态;客户端startOnline的内容十分简洁,就是等待服务端发送事件,我们在本地执行事件以后,将结果发送给房主的result函数进行处理,并再次进入等待状态。
通过前面我们知道,房主在最后会广播一个gameStart表示游戏开始,因此此时客户端将执行
lib.message.client.gameStart()
函数,其就是一堆清除等待界面UI并创建游戏界面UI的过程。回到房主,其继续执行着模式的start,根据模式的不同广播的内容有所不同,我们以身份场为例;在身份确定以后,房主会发送一次广播,参数为一个函数和身份分配结果,由于接收到函数,客户端因此执行
lib.message.client.exec
把输入的函数执行,把身份信息存储在lib.config.mode_config.identity.identity
和房主同步。然后房主执行randomMapOL
,其同样广播了一个函数回调,总之就是一个根据信息把玩家画到客户端界面的函数,最后房主执行chooseCharacterOL
进入选将界面;选将过程老长了,懒得解说了,总之就是需要全体玩家操作的时候就通过
game.broadcast()
进行广播,而只有部分玩家操作的时候,选出相应玩家的Client对象,通过send
来发送指令。选将结束后,房主通过game.syncState()
同步大量状态到客户端并触发gameStart事件,具体哪些状态可以参考noname/library/element/player.js
的getState()函数;接下来是摸牌事件gameDraw,其通过
directgain()
函数获得牌,其同样是通过broadcast将房主获得牌的信息传给客户端,紧接着的是联机置换手牌从game.replaceHandcards
到replaceHandcardsOL
,也是一通麻烦的broadcast操作,懒得解说了;接下来进入回合循环gameloop,一通操作全在房主那里进行,直到通过trigger触发回合开始事件,在trigger中除了gameStart、gameDrawEnd这些特殊事件外,均在一个统一的框架下进行,中间有着一大堆的层层转包,但大部分事件其实客户端只需要观看即可。
在联机模式下,游戏主要在房主那里进行,在没有事件响应的时候,客户端虽然也持有一个game对象,但大部分的状态更新,全程依靠房主运行时发出的game.broadcast()
广播,其遍布于游戏的每一个角落,只能说这是游戏联机劣根的根源所在。
交互机制#
作为客户端,游戏的交互主要有两个方面,一个是进入回合的出牌操作阶段,另一个则是响应事件的时候(包括触发技能、响应其它玩家等)。我们先来看前者,其核心调用了玩家的chooseToUse()
函数,通过转交以后,其位于library/element/content.js
中执行;对于后者也是一样地在最后回到content中进行响应。
在各种响应的操作中,有一个核心的三连判断,一判断是否为自己,二判断是否为联机,三则视其为ai,作为客户端主要响应的是二,此时房主将为此事件执行event.send()
,而这就是交互的起点。
首先其会调出节点存储的玩家对象,发送一个函数回调让客户端执行,然后执行
player.wait()
即给这名玩家思考进度条,并暂停房主的game;回调的过程在客户端执行,步骤比较简单就是获得房主的各种信息以后,将事件注入,并通过
game.resume()
恢复客服端的游戏进程;此时客户端的startOnline恢复运行,并执行房主给予的事件,得到结果以后通过
game.send
执行房主的result来处理结果,并再次进入等待状态;此时在房主的
lib.message.server.result
中,让这名玩家执行unwait
表示玩家响应结束,并传入结果result;房主的处理过程虽然一大堆,但总共也就几步,先清除玩家等待之类的UI信息,接着将事件处理的结果放到自己的event中,最后恢复自己的游戏。
可以看到联机的交互机制似乎比想象的简单很多,但代价是联机相关的代码十分的分散,导致维护和编写代码时会产生诸多的困难,因此我们看到联机下删除了许多的模式和内容,这是我们应该理解的,谁让无名杀的联机架构不太行呢!