--- front: hard: 进阶 time: 20分钟 --- # 游戏玩法 ​ 这里介绍简易网络服模板的玩法实现,包括NPC以及匹配两块内容。 ​ 简易网络服模板的大厅服中有三个NPC,玩家点击NPC可以实现切服功能。点击NPC-A显示GameA在线人数,可以跳转到包含AwesomeGameMod的GameA,GameA实现的是常规的生存服。点击NPC-B可以显示GameB在线人数,可以跳转到包含TutorialGameMod的GameB,在GameB的聊天栏中输入“钻石剑”,可在背包中获得钻石剑*1。点击NPC-C可以实现简单匹配,当匹配中玩家≥2人时,将这些玩家传入gameC服。在每个Game服中,都有一个返回大厅NPC。 ## NPC ### NPC实现 ​ npc由npc插件实现,只需要配置插件里的mod.json,即可生成对应npc。具体配置如下: ```python { "_comment":"mod的名字", "netgame_mod_name":"neteaseNpcDev", "_comment":"mod的版本号", "netgame_mod_version":"1.0.4", "_comment":"适用最低的引擎版本", "min_app_version":"1.15.0.release20191226", "_comment":"该服务器Mod隶属于“功能NPC”插件", "description":"Npc mod插件", "_comment":"适用的服务器类型", "support_server_type":["lobby","game"], "_comment":"作者名字", "author": "NetEase", "_comment":"NPC类型参数配置", "NPCS_TYPE": { "_comment":"typeId,和NPCS_DISTRIBUTE中的typeId对应", "1001":{ "name": "NPC-A", "identifier": "minecraft:npc", "simpleStyle": false, "modName": "Minecraft", "systemName": "AwesomeLobby", "funcName": "OnNpcTouched", "funcArgs": ["gameA"] }, "1002":{ "name": "NPC-B", "identifier": "minecraft:npc", "simpleStyle": false, "modName": "Minecraft", "systemName": "AwesomeLobby", "funcName": "OnNpcTouched", "funcArgs": ["gameB"] }, "1003":{ "name": "NPC-C", "identifier": "minecraft:npc", "simpleStyle": false, "modName": "Minecraft", "systemName": "AwesomeLobby", "funcName": "OnNpcTouched", "funcArgs": ["gameC"] }, "1004":{ "name": "返回大厅NPC", "identifier": "minecraft:npc", "simpleStyle": false, "modName": "AwesomeGame", "systemName": "FpsServerSystem", "funcName": "OnNpcTouched", "funcArgs": ["lobby"] }, "1005":{ "name": "返回大厅NPC", "identifier": "minecraft:npc", "simpleStyle": false, "modName": "Minecraft", "systemName": "TutorialGame", "funcName": "OnNpcTouched", "funcArgs": ["lobby"] }, "1006":{ "name": "返回大厅NPC", "identifier": "minecraft:npc", "simpleStyle": false, "modName": "Minecraft", "systemName": "gameMod", "funcName": "OnNpcTouched", "funcArgs": ["lobby"] } }, "_comment":"NPC分布列表", "NPCS_DISTRIBUTE": [ { "typeId" : "1001", "server" : "lobby", "pos" : [1396, 4, 57], "orientations" : [0, 180], "dimensionId" : 4 }, { "typeId" : "1002", "server" : "lobby", "pos" : [1403, 4, 57], "orientations" : [0, 180], "dimensionId" : 4 }, { "typeId" : "1003", "server" : "lobby", "pos" : [1410, 4, 57], "orientations" : [0, 180], "dimensionId" : 4 }, { "typeId" : "1004", "server" : "gameA", "pos" : [5, 4, 5], "orientations" : [0, 180], "dimensionId" : 0 }, { "typeId" : "1005", "server" : "gameB", "pos" : [5, 4, 5], "orientations" : [0, 180], "dimensionId" : 0 }, { "typeId" : "1006", "server" : "gameC", "pos" : [5, 4, 5], "orientations" : [0, 180], "dimensionId" : 0 } ] } ``` ### 功能验证 用MCStudio进入游戏,可以看到玩家前方有三个NPC: ![img](./images/wps11.jpg) ### NPC插件总结: - 创建NPC前,用CheckChunkState函数检查chunk状态。 - 推荐用定时器创建NPC。 ## 匹配 ### 匹配的设计 ​ 点击NPC-C后,把多个玩家匹配分配到GameC。匹配是把多个玩家分配到另外一个单独服务器的过程,它是全服单点逻辑,建议在service实现匹配功能。 通常匹配功能设计思路如下: * lobby向service请求匹配。 * service包含一个待匹配玩家队列。玩家中途退出时,需要将该玩家从队列中剔除。 * service每帧遍历所有待匹配玩家,根据一定算法,将多个玩家分配到指定game服务器。 * service告知game,玩家即将进入,并告知玩家信息。 * service告知所有玩家切服到指定game。 * 玩家进入game,完成匹配过程。 ### 简易网络服模板匹配功能开发 匹配过程如下所示: ![img](./images/wps12.jpg) * lobby服务端开发 服务端监听EntityBeKnockEvent事件,处理点击NPC行为,根据NPC的种类,处理不同的请求。点击NCP-A和NPC-B需要向master查询GameA和GameB的在线人数,点击NPC-C需要处理匹配逻辑。核心代码如下: ```python class AwesomeServer(ServerSystem): ... def OnNpcTouched(self, npc_entity_id, player_entity_id, gameType): ''' 点击npc回调函数。 ''' uid = self.playerid2uid[player_entity_id] if gameType == 'gameA': logger.info("%s touch NPC gameA",player_entity_id) #请求gameA玩家人数 request_data = {'game': 'gameA', 'player_id': player_entity_id,'uid': uid,'client_id':netgameApi.GetServerId()} self.NotifyToMaster(modConfig.GetPlayerNumOfGameEvent,request_data) elif gameType == 'gameB': logger.info("%s touch NPC gameB",player_entity_id) #请求gameB玩家人数 request_data = {'game': 'gameB', 'player_id': player_entity_id, 'uid': uid, 'client_id': netgameApi.GetServerId()} self.NotifyToMaster(modConfig.GetPlayerNumOfGameEvent, request_data) elif gameType == 'gameC': logger.info("%s touch NPC gameC",player_entity_id) # 请求gameC匹配队列人数 request_data = {'uid': uid, 'player_id': player_entity_id, 'game': 'gameC'} self.RequestToService(modConfig.awesome_match, modConfig.RequestMatchNum, request_data) def OnSureGame(self,args): ''' 切服逻辑,如果是gameA和gameB则直接传去对应服,如果是gameC则加入匹配队列 ''' logger.info("OnSureGame {}".format(args)) if args['game'] == "gameA": netgameApi.TransferToOtherServer(args['playerId'], "gameA") elif args['game'] == "gameB": netgameApi.TransferToOtherServer(args['playerId'], "gameB") elif args['game'] == "gameC": playerId = args['playerId'] uid = self.mPlayerid2uid[playerId] levelcomp = self.CreateComponent(playerId, modConfig.Minecraft, "lv") playerLevel = levelcomp.GetPlayerLevel() if playerLevel >= 0:#大于0级才能匹配 request_data = {'uid': uid, 'player_id': playerId,'game':args["game"]} self.RequestToService( modConfig.awesome_match, modConfig.RequestMatch, request_data ) tipData = {'tipType' : TipType.matching} #1匹配中 self.NotifyToClient(playerId, modConfig.MatchResultTip, tipData) else: tipData = {'tipType': TipType.levelNotEnough} #0等级不够 self.NotifyToClient(playerId, modConfig.MatchResultTip, tipData) def OnMatchResultEvent(self, args): ''' 处理匹配结果。切到指定服务器。 ''' logger.info("OnMatchResultEvent {}".format(args)) playerId = args['player_id'] desc_game = args['desc_game'] if args['game'] == 'gameC': #如果是gameC则延时1S传送 tipData = {'tipType': TipType.toTransfer} # 2 即将传送 self.NotifyToClient(playerId, modConfig.MatchResultTip, tipData) self.mTransferPlayerQueue.append(playerId) CoroutineMgr.StartCoroutine(self.Transfer2Server(playerId, desc_game)) def Transfer2Server(self,playerId,descGame): ''' 把玩家传送至对应的服 ''' yield -30 #判断玩家是否在待传送队列里,若玩家中途下线,则不作处理 if playerId in self.mTransferPlayerQueue: netgameApi.TransferToOtherServerById(playerId, descGame) self.mTransferPlayerQueue.remove(playerId) ``` - service开发 service监听UpdateServerStatusEvent事件,可以获取所有game的状态,这些可用game构成了可用资源池。当有玩家请求匹配时,则从可用资源池中分配资源(也就是匹配算法),然后告知玩家。核心代码如下: ```python class AwesomeService(ServiceSystem): def __init__(self, namespace, systemName): ServiceSystem.__init__(self, namespace, systemName) self.mFrameCnt = 0 self.mPlayerServer = {} self.mGameCMatchingPlayer = []#gameC的匹配玩家 self.mActiveGameServerIds = [] #可用game列表 self.mGameStatus = {}#serverid => server status.server status: #注册service接口 self.RegisterRpcMethod(modConfig.awesome_match, modConfig.RequestMatch, self.OnRequestMatch) self.RegisterRpcMethod(modConfig.awesome_match, modConfig.RequestMatchCancel, self.OnRequestMatchCancel) self.RegisterRpcMethod(modConfig.awesome_match, modConfig.RequestMatchNum, self.OnRequestMatchNum) def OnRequestMatchCancel(self,server_id, callback_id,args): logger.info("OnRequestMatchCancel {}".format(args)) player_id = args["player_id"] if player_id in self.mGameCMatchingPlayer: self.mGameCMatchingPlayer.remove(player_id) def OnRequestMatchNum(self,server_id, callback_id, args): ''' 返回匹配队列人数 :return: ''' logger.info("OnRequestMatchNum {}".format(args)) result_data = { 'uid':args["uid"],'player_id':args["player_id"], 'playernum':len(self.mGameCMatchingPlayer), "game": args["game"] } self.NotifyToServerNode(server_id, modConfig.MatchNumEvent, result_data) def OnRequestMatch(self, server_id, callback_id, args): ''' 请求匹配进入gameC的游戏 ''' logger.info("OnRequestMatch {}".format(args)) player_id = args['player_id'] self.mPlayerServer[player_id] = server_id #如果已经在匹配队列,则不加入匹配队列 if player_id in self.mGameCMatchingPlayer: return else: logger.info("%s matching",player_id) self.mGameCMatchingPlayer.append(player_id) def GameCMatch(self): ''' 检查匹配队列,匹配成功,清空匹配队列 :return: ''' if not self.mGameCMatchingPlayer: return desc_game = -1 if len(self.mGameCMatchingPlayer) >=2: desc_game = self.MatchAlgorithm() if desc_game == -1: return for i in range(len(self.mGameCMatchingPlayer)): playerId = self.mGameCMatchingPlayer[i] self.NotifyToServerNode(self.mPlayerServer[playerId], modConfig.MatchResultEvent, {'player_id': playerId,'desc_game':desc_game,'game':'gameC'}) self.mGameCMatchingPlayer = []#清空匹配队列 def Update(self): self.mFrameCnt += 1 if self.mFrameCnt % 10 == 0:#10帧匹配一次 self.GameCMatch() def MatchAlgorithm(self): ''' 匹配算法 ''' serverid = -1 serverlistConf = serviceConf.netgameConf['serverlist'] for serverConf in serverlistConf: if serverConf['type'] == "gameC": serverid = serverConf['serverid'] break return serverid def OnUpdateServerStatusEvent(self, args): ''' 记录服务器状态 ''' logger.info("OnUpdateServerStatusEvent {}".format(args)) self.mGameStatus = {} self.mActiveGameServerIds = [] for server_id, status in args.iteritems(): id = int(server_id) int_status = int(status) self.mGameStatus[id] = int_status if int_status == EServerStatus.OK: self.mActiveGameServerIds.append(id) ``` - Master开发 Master查询对应游戏玩家人数用。核心代码如下: ```python class AwesomeMaster(MasterSystem): ... ... def GetPlayerNumOfGame(self,args): serverlistConf = masterConf.netgameConf['serverlist'] print "OnGetPlayerNumOfGameResponse",args checkServeridList = [] for serverConf in serverlistConf: if serverConf['type'] == args["game"]: serverid = serverConf['serverid'] checkServeridList.append(serverid) playernum = 0 for serverid in checkServeridList: playernum += serverManager.GetOnlineNumByServerId(serverid) request_data = { 'game': args["game"], 'playernum': playernum, 'player_id':args["player_id"] } self.NotifyToServerNode( args["client_id"], modConfig.GetPlayerNumOfGameRequestEvent, request_data) ``` 用MCStudio进入游戏,点击不同的NPC发现切服到对应game。 备注:新建一个mod.json于ServiceMod/developer_mods/AwsomeService下面,mod.json的内容是 { "netgame_mod_name": null, "netgame_mod_version": "1.0.0", "min_app_version": null, "author": null, "module_names": ["awsome_match"], "support_server_type": null "unsupport_app_version": null, "CustomPath": null } ### 总结 - service监听UpdateServerStatusEvent事件,记录可用服务器列表。 - 匹配过程主要包括:请求匹配、匹配算法、玩家迁移。