本教材要求开发者已经阅读Apollo文档、“从零开始网络游戏”。教材通过一个完整网络游戏的开发过程介绍进阶内容。网络游戏功能是点击NPC匹配到游戏服,具体代码请参考“AwesomeGame”。

进入和退出游戏

主要包含登录、定时存档、登出、切服等功能。

登录

要求开发者已经了解Apollo框架,然后再阅读下面内容。

登录过程介绍

Apollo引擎在登录过程中会处理顶号问题,开发者开发过程不用考虑顶号。玩家登录过程如下图示:

img

开发者通过上图可以理解登录涉及的几个角色。下面介绍登录涉及的核心事件(具体事件说明请参考服务器MOD SDK和客户端SDK):

  • 第3步,master接受请求后,会触发PlayerLoginServerEvent事件,该事件可以区分玩家登录还是切服。
  • 第5步,玩家登录到lobby,lobby处理登录逻辑时会触发ServerGetPlayerLockEvent事件。
  • 第6步,lobby下传行为包后会触发QueryScriptSaveEvent事件,此时脚本可以从db中读取玩家数据。接着,lobby在地图中创建玩家实例时会触发ServerPlayerBornPosEvent事件,然后立马触发AddServerPlayerEvent事件,建议在mod中监听AddServerPlayerEvent事件处理玩家登录逻辑,AddServerPlayerEvent事件可以区分玩家登录还是切服。
  • 在登录过程中,客户端会触发OnUIInitFinished事件,开发者可以监听该事件,在该事件的回调函数中开始请求获取玩家数据,然后处理客户端逻辑。

服务端登录开发

下面开发AwesomeGame服务端登录功能。思路是:

  • 监听AddServerPlayerEvent事件,从db中读取玩家数据,然后记录到内存,接着把玩家数据同步给客户端。
  • 监听ServerPlayerBornPosEvent事件,设置玩家出生点。

部分核心代码如下:

客户端登录开发

监听服务端发过来的SyncUserDataEvent事件,然后记录玩家数据;监听OnUIInitFinished事件,开始显示ui或处理客户端游戏逻辑。代码如下:

验证功能

用MCStudio进入游戏,查看相关信息:

  • 登录到linux开发机,切到lobby的logs目录,可以看到OnAddServerPlayer方法打印的登录日志:

    img

  • 进入mysql,可以查看到玩家数据:

    img

  • MCStudio中查看玩家登录日志,也即OnSyncUserData方法打印的日志:

    img

公共代码管理

登录相关代码目录结构如下:

如上图示,behavior_packs和developer_mods都是用到modCommon,该目录存放公共逻辑。这里为了方便import公共代码,把behavior_packs和developer_mods中mod根目录命名为相同的名字awesomeScripts。

若开发者使用svn管理代码,推荐使用外链(externals)方式管理公共目录。下面介绍developer_mods下面创建外链方法:

(1)awesomeScripts目录空白处右键,选择属性

img

(2)选中Subversion->Properties

img

(3)点击New->Externals

img

(4)点击 New

img

(5)弹框中,URL中输入modCommon svn地址;Local Path输入modCommon,表示外链在本地的名字。然后点击OK即可。

img

总结

  • 监听服务端的AddServerPlayerEvent事件处理玩家登录逻辑。
  • 监听客户端的OnUIInitFinished事件开始处理客户端逻辑。
  • 玩家在服务端登录过程中,在获取玩家数据后,通过自定义事件把玩家数据同步给客户端。
  • 使用svn 外链,可以方便管理behavior_packs和developer_mods的公共代码。

定时存档

通过set_use_database_save函数打开定时存档功能,引擎会定时触发savePlayerDataEvent/savePlayerDataOnShutDownEvent事件。mod监听这两个事件,然后执行存档逻辑。定时存档把存档从游戏逻辑中解耦出来,让开发者集中于游戏逻辑的开发。

服务端mod开发

设置定时存档,然后监听savePlayerDataEvent/savePlayerDataOnShutDownEvent事件。核心代码如下:

验证功能

  • 用MCStudio进入游戏,在游戏停留2min,然后在db中查看玩家数据,发现login_time发生了变化。
  • 开发者也可以在OnSavePlayerData函数中添加额外日志,接着用MCStudio进入游戏不退出,然后查看 lobby服日志,可以发现lobby会定时打印对应日志。

总结

  • 通过set_use_database_save 函数开启定时存档。
  • 监听savePlayerDataEvent/savePlayerDataOnShutDownEvent事件,处理存档逻辑。

登出

登出过程介绍

玩家从lobby中退出游戏过程如下所示:

img

如上图所示,登出涉及到lobby和master。下面介绍登出涉及到的事件:

(1)第2步,lobby处理玩家登出逻辑会触发savePlayerDataEvent / savePlayerDataOnShutDownEvent事件,此时脚本层可以把玩家数据存档。

(2)第4步,lobby引擎在清理玩家数据后触发DelServerPlayerEvent事件,此时脚本层可以清理脚本层玩家内存数据,该事件可以区分玩家退出还是切服。

(3)第5步,master处理玩家登出逻辑时会触发PlayerLogoutServerEvent事件,该事件可以区分玩家退出还是切服。

下面介绍AwesomeGame登出逻辑的开发。

服务端登出逻辑。

savePlayerDataEvent/savePlayerDataOnShutDownEvent事件相关逻辑已经实现,下面还需要监听DelServerPlayerEvent事件,清除玩家数据。代码如下:

功能验证

用MCStudio进入游戏后立马退出。打开lobby日志,可以查看到登出日志:

img

总结

  • 玩家退出也会触发lobby的savePlayerDataEvent/savePlayerDataOnShutDownEvent事件。
  • 监听OnDelServerPlayer事件清除脚本层中玩家内存数据。

切服

玩家切服是从一个服务器退出,然后再登录到指定服务器过程,实质是退出游戏然后再进入游戏过程。目前,登入事件是可以区分登录和切服,登出事件也可以区分切服过程中登出还是退出游戏。下图是玩家由lobby切到game过程图:

img

如上图所示,第1步到第3步是玩家登出逻辑,第4步到第7步是玩家登录逻辑。下面介绍涉及到的事件和api:

  • 第1步,lobby中调用TransferToOtherServer函数触发切服逻辑。
  • 第2步,master接受切服请求后会触发PlayerTransferServerEvent事件。
  • 第3步触发事件请参考“登出过程介绍”。
  • 第4~8步是玩家登录过程,触发事件请参考“登录过程介绍”

服务器关服

服务器关服前会触发ServerWillShutDownEvent事件,开发者可以在该事件中处理存档和清理现场的逻辑。

AwesomeGame服务端在退出时,需要保存所有在线玩家数据。核心代码如下:

总结:

  • 监听OnServerWillShutDown事件,清理服务端现场。

游戏玩法

这里介绍常见游戏玩法设计,比如NPC、匹配、滚动更新等。示例中实现了三个NPC,玩家点击NPC可以实现切服功能。点击NPCA显示GameA在线人数,可以跳转到包含AwesomeGameMod的GameA。点击NPC B可以显示GameB在线人数,可以跳转到包含TutorialGameMod的GameB。点击NPC C可以实现简单匹配,当匹配中玩家≥2人时,将这些玩家传入gameC服

NPC

NPC实现

NPC通常用于切服和引导功能。创建NPC的要点有:

  • NPC是在服务端mod(developer_mods)里面创建的。
  • NPC是在chunk被初次加载后创建的,用CheckChunkState函数检查chunk可用后再创建NPC。
  • 由于chunk被初次加载时间不确定,因此建议使用定时器定时检查chunk然后创建NPC。

服务端NPC逻辑

新增npcManager.py文件,用于管理所有NPC。核心逻辑是:

  • NpcData记录npc数据,NpcManager管理所有NPC。
  • NpcManager实例初始化时注册定时器,定时创建NPC。

核心代码如下:

功能验证

用MCStudio进入游戏,可以看到玩家前方有三个NPC:

img

总结:

  • 创建NPC前,用CheckChunkState函数检查chunk状态。
  • 推荐用定时器创建NPC。

匹配

匹配的设计

点击NPC后,需要把多个玩家匹配分配到GameC,设计到匹配逻辑。匹配是把多个玩家分配到另外一个单独服务器的过程。它是个全服单点逻辑,建议在service实现匹配功能。

通常匹配功能设计思路如下:

  • lobby向service请求匹配。
  • service包含一个待匹配玩家队列。玩家中途退出时,需要将该玩家从队列中剔除。
  • service每帧遍历所有待匹配玩家,根据一定算法,将多个玩家分配到指定game服务器。
  • service告知game,玩家即将进入,并告知玩家信息。
  • service告知所有玩家切服到指定game。
  • 玩家进入game,完成匹配过程。

下面介绍AwesomeGame匹配功能开发。功能是点击npc,然后玩家匹配到某个game中。匹配非常简单,只是平均把玩家分配到game中。

AwesomeGame匹配功能开发

匹配过程如下所示:

img

  • lobby服务端开发

服务端监听EntityBeKnockEvent事件,处理点击NPC行为,根据NPC的种类,处理不同的请求。点击NCPA和NPCB需要向master查询GameA和GameB的在线人数,点击NPC需要处理匹配逻辑。核心代码如下:

  • service开发

service监听UpdateServerStatusEvent事件,可以获取所有game的状态,这些可用game构成了可用资源池。当有玩家请求匹配时,则从可用资源池中分配资源(也就是匹配算法),然后告知玩家。核心代码如下:

  • Master开发

Master查询对应游戏玩家人数用。核心代码如下:

用MCStudio进入游戏,点击不同的NPC发现切服到对应game。

备注:需要将"awesome_match"添加到deploy.json里servicelist内module_names配置中

总结

  • service监听UpdateServerStatusEvent事件,记录可用服务器列表。
  • 匹配过程主要包括:请求匹配、匹配算法、玩家迁移。

游戏外功能

运营指令

运营指令:接收外部http请求,处理游戏相关逻辑,比如给某个玩家发物品,公告等。

master是运营执行入口,开发者可以根据需要,把请求分发到lobby/game/service。下面给AwesomeGame新增一个运营指令,功能是打印指定玩家信息。

获取玩家数据运营指令

由于lobby是异步定时存档的,因此mysql数据可能不是最新的。这里实现方案是:

  • 玩家在lobby,从对应lobby的内存中拉取玩家数据。
  • 玩家不在lobby,选择任意一个可用lobby,从db中读取玩家数据。

处理过程如下所示:

img

master主要接受请求然后转发,核心代码如下所示:

Lobby主要获取玩家数据,核心代码如下所示:

验证

登录到开发机,然后给master发送curl请求,即可获取结果,如下图示:

img

官方运营指令

查看“服务器MOD SDK”中【运营指令】部分,里面介绍了常用的指令,比如禁言、踢人等。

总结

  • 运营指令的实现通常分为两个步骤:

    • master接受响应指令,将指令请求转发到其他服务器;
  • lobby/game/serivce实现指令功能。

  • 官方实现了常见的运营指令,具体可以查看“服务器MOD SDK”中【运营指令】部分。

优化和维护

优化部分介绍如何查找脚本层内存泄漏。维护部分介绍如何追查线上问题。

内存检查

开发者先阅读“服务器MOD SDK”中“内存检查”部分。下面结合“AwesomeGame”网络游戏介绍如何检查内存泄漏。

制造内存泄漏问题

lobby玩家退出时,不清理内存数据。按照下面方式修改代码:

检查内存泄漏

然后给master发送check-memory-run指令,生成一个内存快照,结果如下图示:

img

查看lobby日志:

img

用MCStudio进入游戏,然后退出,再给master发送check-memory-run指令,再生成一次内存快照,结果如下图示:

img

再次查看lobby日志:

img

分析:

[Top 10 differences]记录占用内存最多的10行代码。简单分析下面日志:

img

obj_report.py文件43行占用内存最多,占用48k内存,实例个数为1,两次内存快照间增加了一个实例,平均每个实例占用48k。、

[DIFF_MORE]记录了内存变化最多的类型。简单分析下面日志:

img

两次内存快照间新增了一个PlayerData类型实例。

接着分析AwesomeGame内存泄漏问题。第一次内存快照时,没有玩家登录,接着玩家登录再登出,然后再生成了一次内存快照。第二次内存快照时,没有玩家在游戏中,因此服务端内存是不会有玩家数据,也即不会有PlayerData类型实例的。但是,[DIFF_MORE]中显示内存中还是多了一个PlayerData类型实例,这说明存在内存泄漏。

总结

  • check-memory-run指令可以检测两次内存快照间内存变化。
  • 内存泄漏检查要点:在游戏平稳时生成内存快照(比如没有玩家登录登出),然后分析内存变化。

Hunter

开发者先阅读“服务器MOD SDK”中“Hunter调试命令”部分。下面结合“AwesomeGame”网络游戏介绍如何在线调试mod。

获取lobby服在线玩家信息

用MCStudio进入游戏,然后给master发送/hunter-debug指令:

img

实质是在lobby中执行下面代码:

执行结果需要查看lobby日志:

img

日志说明,lobby服只有一个玩家,日志打印了该玩家的player id和uid。

清空lobby服玩家信息

然后给master发送/hunter-debug指令:

img

实质是在lobby中执行下面代码:

执行结果需要查看lobby日志:

img

日志打印”clear ok”,说明清除成功。

总结

hunter-debug指令支持在线执行一段python脚本,使用该指令可以方便查看变量信息,修改变量内容。

多mod管理

控制服务端多个mod加载顺序

多个mod情况下,有时需要控制mod的加载顺序。下面以服务端官方mod neteaseMonitorSample和neteaseMonitor为例,介绍如何设置mod加载顺序。

服务端官方mod是官方提供的mod,都是在developer_mods目录下。neteaseMonitorSample和neteaseMonitor 存放在lobby/game引擎的developer_mods目录下。

neteaseMonitorSample要求在neteaseMonitor 后面加载。可以按照下面步骤配置加载顺序:

  • neteaseMonitorSample根目录下添加netease_require.json文件,neteaseMonitorSample目录结构如下所示:

    neteaseMonitorSample netease_require.json neteaseMonitorSample init.py modMain.py netease_require.json中配置内容如下所示:

modName表示mod的名字,modRequire表示前置mod名字列表。上面内容表示当前mod的名字是neteaseMonitorSample,它在neteaseMonitor后面加载。

  • neteaseMonitor根目录下添加netease_require.json文件,配置内容如下:

注意,neteaseMonitor一定要配置netease_require.json,设置mod的名字,否则引擎找不到这个mod。

总结

相互依赖的多个mod,都需要配置netease_require.json,设置mod名字和依赖关系。