# 进入和退出游戏

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

# 登录

要求开发者已经了解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事件,设置玩家出生点。

部分核心代码如下:

class AwesomeServer(ServerSystem):   
	def __init__(self, namespace, systemName):      
		ServerSystem.__init__(self, namespace, systemName)      
		self.mongoMgr = MongoOperation()
		self.mysqlMgr = MysqlOperation()
		self.dbType = 0
        #初始化连接池
		if self.mongoMgr.InitMongoDb() == True:
			self.dbType = DbType.Mongo
		elif self.mysqlMgr.InitMysqlDb() == True:
			self.dbType = DbType.Mysql
        #注册事件
		self.ListenForEvent(
					serverApi.GetEngineNamespace(), 
					serverApi.GetEngineSystemName(),
					modConfig.AddServerPlayerEvent,
					self, self.OnAddServerPlayer
					)      
		self.ListenForEvent(
					serverApi.GetEngineNamespace(), 
					serverApi.GetEngineSystemName(), 
					modConfig.ServerPlayerBornPosEvent,
					self, self.OnPlayerBornPosEvent
					)
        self.ListenForEvent(
            modConfig.Minecraft, 
            modConfig.LobbyClientSystemName, 
            modConfig.LoginRequestEvent,
            self, self.OnLoginRequest)
        self.DefineEvent(modConfig.GetUserInfoResponseEvent)
		...
		#【自定义事件,给lobb/game发送同步数据事件】	
		self.DefineEvent(modConfig.SyncUserDataEvent)      
		# 玩家对象管理      
		self.player_map = {}      
		#玩家player id到uid的映射      
		self.playerid2uid = {}  
		#玩家初始在dimension 4,需要创建dimension
		dimensionComp = self.CreateComponent(playerId, "Minecraft", "dimension")
		dimensionComp.CreateDimension(4)
            
	def OnAddServerPlayer(self, args):      
		'''      
	 	添加玩家的监听函数      
		'''      
	 	playerId = args.get('id','-1')      
		uid = netgameApi.GetPlayerUid(playerId)     
		self.playerid2uid[playerId] = uid      
		self.QueryPlayerData(playerId, uid)   
    
	def OnPlayerBornPosEvent(self, data):      
		'''      
		设置player出生点和dimension      
		'''      
		import random      
		#设置dimension      
		uid = data['userId']      
		BORN_POS = (1395.664, 5.2, 51.441)      
		data['posx'] = BORN_POS[0]      
		data['posy'] = BORN_POS[1]      
		data['posz'] = BORN_POS[2]      
		data['dimensionId'] = 4      
	 	data['ret'] = True      
		self.uid2dimension[uid] = data['dimensionId'] 
        
 	def QueryPlayerData(self, player_id, uid):      
		'''      
		db中获取玩家数据      
		'''      
		if self.dbType == DbType.Mongo :
			self.mongoMgr.QueryPlayerData(player_id,uid,
				lambda data:self.QuerySinglePlayerCallback(
					player_id,uid, data))
		elif self.dbType == DbType.Mysql :
			self.mysqlMgr.QueryPlayerData(player_id,uid,
				lambda data: self.QuerySinglePlayerCallback(
				player_id, uid, data))

	def QuerySinglePlayerCallback(self, player_id, uid, data):
		'''
		回调函数。若玩家存在,则注册玩家;否则记录玩家信息
		'''
		# 数据库请求返回时,玩家已经主动退出了
		if not self.playerid2uid.has_key(player_id):
			return
		if not data:	# 找不到玩家数据,注册一个新玩家
			nickname = netgameApi.GetPlayerNickname(player_id)
			data = playerData.PlayerData.getNewPlayerInfo(uid, nickname)
			self.InsertPlayerData(player_id, uid)
		#记录玩家数据
		player = playerData.PlayerData()
		if isinstance(data,tuple):
			data = player.changeMysqlTupleToPlayerDict(data)
		player.initPlayer(player_id, data)
		#刷新玩家登录事件
		player.refreshLoginTime()
		self.player_map[uid] = player
		#把玩家数据同步给客户端
		event_data = player.toSaveDict()
		event_data['player_id'] = player_id
		self.NotifyToClient(player_id, modConfig.SyncUserDataEvent, event_data)
    	
	def InsertPlayerData(self, player_id, uid):
		'''
		把玩家数据插入db
		'''
		nickname = netgameApi.GetPlayerNickname(player_id)
		new_player_data = playerData.PlayerData.getNewPlayerInfo(uid, nickname)
		if self.dbType == DbType.Mongo:
			self.mongoMgr.InsertPlayerData(
				player_id,uid,new_player_data)
		elif self.dbType == DbType.Mysql:
			self.mysqlMgr.InsertPlayerData(player_id, 
			uid, new_player_data)
            
	def OnLoginRequest(self, data):
		'''
		玩家登录逻辑
		'''
		import lobbyGame.netgameApi as lobbyGameApi
		player_id = data['id']
		uid = lobbyGameApi.GetPlayerUid(player_id)
		CoroutineMgr.StartCoroutine(self._DoSendLoginResponseData(player_id, uid))
        
	def _DoSendLoginResponseData(self, player_id, uid):
		'''
		将玩家数据推送给客户端。若还没从db获取玩家数据,则延迟5帧再试
		'''
		if uid in self.player_map:
			player = self.player_map[uid]
			event_data = player.toSaveDict()
			event_data['player_id'] = player_id
			self.NotifyToClient(player_id, modConfig.LoginResponseEvent, event_data)
			return
		yield -5

# 客户端登录开发

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

	class AwesomeClient(ClientSystem):
	def __init__(self,namespace,systemName):
		ClientSystem.__init__(self,namespace,systemName)
		# 注册事件
		self.ListenForEvent(
			modConfig.Minecraft, 
			modConfig.LobbyServerSystemName, 
			modConfig.LoginResponseEvent, 
			self, self.OnLoginResponse
		)
        self.ListenForEvent(
            clientApi.GetEngineNamespace(), 
            clientApi.GetEngineSystemName(),
            modConfig.UiInitFinished, 
            self, self.OnUIInitFinished
        )
		self.my_player_data = None
        
	def OnUIInitFinished(self,args):
		'''
		请求登录到服务端,获取玩家数据
		'''
		logger.info("OnUIInitFinished : %s", args)
		playerId = clientApi.GetLocalPlayerId()
		loginData = {}
		loginData['id'] = playerId
		self.NotifyToServer(modConfig.LoginRequestEvent, loginData)

	def OnLoginResponse(self, args):
		'''
		初始化玩家数据,然后开始客户端逻辑
		'''
		logger.info("OnLoginResponse : %s", args)
		player_info = args
		self.my_player_data = playerData.PlayerData()
		self.my_player_data.initPlayer(player_info['player_id'], player_info)
		self.InitUi()

# 验证功能

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

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

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

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

# 公共代码管理

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

behavior_packs
	AwesomeBehavior
		awesomeScripts
			clientUtils
			modCommon
				__init__.py
				coroutineMgrGas.py
				modConfig.py
				playerData.py
			modUI
			__init__.py
			AwesomeClient.py
			modMain.py
		manifest.json
developer_mods
	AwesomeLobby
		awesomeScripts
			modCommon
				__init__.py
				coroutineMgrGas.py
				modConfig.py
				playerData.py
			__init__.py
			AwesomeServer.py
			modMain.py
			npcManager.py
			mysqlOperation.py
			mongoOperation.py

如上图示,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事件。核心代码如下:

class AwesomeServer(ServerSystem):
	def __init__(self, namespace, systemName):
		... 
		netgameApi.SetUseDatabaseSave(True, "awesome", 120)#定时存档,时间间隔是120s
		netgameApi.SetNonePlayerSaveMode(True)

		self.ListenForEvent(
			serverApi.GetEngineNamespace(), 
			serverApi.GetEngineSystemName(), 
			'savePlayerDataEvent',
			self, self.OnSavePlayerData
		)
		self.ListenForEvent(
			serverApi.GetEngineNamespace(), 
			serverApi.GetEngineSystemName(), 
			'savePlayerDataOnShutDownEvent',
			self, self.OnSavePlayerData
		)
	...
	def OnSavePlayerData(self, args):
		'''
		把玩家数据存档。这个函数一定要调用save_player_data_result函数,把存档状态告知引擎。
		'''
		uid = int(args["playerKey"])
		cpp_callback_idx = int(args["idx"])
		player_data = self.player_map.get(uid, None)
		if not player_data:
			#告知引擎,存档状态。注意传入回调函数id
			netgameApi.SavePlayerDataResult(cpp_callback_idx, True)
		def _SavePlayerCb(args):
			uid, ret = args
			if ret:
				netgameApi.SavePlayerDataResult(cpp_callback_idx, True)
			else:
				netgameApi.SavePlayerDataResult(cpp_callback_idx, False)
		self.SavePlayerByUid(uid, _SavePlayerCb)

	def SavePlayerByUid(self, uid, cb = None):
		'''
		保存玩家数据
		'''
		player = self.player_map.get(uid, None)
		if not player:
			return
		player_dict = player.toSaveDict()
		if self.dbType == DbType.Mongo:
			self.mongoMgr.SavePlayerByUid(uid,player_dict,cb)
		elif self.dbType == DbType.Mysql:
			self.mysqlMgr.SavePlayerByUid(uid,player_dict,cb)

# 验证功能

  • 用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事件,清除玩家数据。代码如下:

class AwesomeServer(ServerSystem):
	def __init__(self, namespace, systemName):
		...
		self.ListenForEvent(
				serverApi.GetEngineNamespace(), 
				serverApi.GetEngineSystemName(), 
				'DelServerPlayerEvent', 
				self, self.OnDelServerPlayer
			)

	def OnDelServerPlayer(self, args):
		'''
		清除玩家内存数据。
		'''
		player_id = args.get('id','-1')
		logout.info("OnDelServerPlayer player id=%s"% player_id)
		uid = self.playerid2uid.get(player_id, None)
		if not uid:
			return
		del self.playerid2uid[player_id]
		if uid in self.player_map:
			del self.player_map[uid]
		if uid in self.uid2dimension:
			del self.uid2dimension[uid]

# 功能验证

用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服务端在退出时,需要保存所有在线玩家数据。核心代码如下:

class AwesomeServer(ServerSystem):
	def __init__(self, namespace, systemName):
		...
		self.ListenForEvent(
			serverApi.GetEngineNamespace(), 
			serverApi.GetEngineSystemName(), 
			'ServerWillShutDownEvent',
			self, self.OnServerWillShutDown
		)
		...
	
	def OnServerWillShutDown(self, args):
		# 即将关机,先给所有还在线玩家挂一个存档任务
		for uid, player in self.player_map.iteritems():
			self.SavePlayerByUid(uid)
		# 同步完成所有还挂着的异步数据库操作
		if self.dbType == DbType.Mongo:
			self.mongoMgr.Destroy()
		elif self.dbType == DbType.Mysql:
			self.mysqlMgr.Destroy()

总结:

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