进入和退出游戏登录登录过程介绍服务端登录开发客户端登录开发验证功能公共代码管理总结定时存档服务端mod开发验证功能总结登出登出过程介绍服务端登出逻辑。功能验证总结切服服务器关服游戏玩法NPCNPC实现服务端NPC逻辑功能验证总结:匹配匹配的设计AwesomeGame匹配功能开发总结游戏外功能运营指令获取玩家数据运营指令验证官方运营指令总结优化和维护内存检查制造内存泄漏问题检查内存泄漏总结Hunter获取lobby服在线玩家信息清空lobby服玩家信息总结多mod管理控制服务端多个mod加载顺序总结
本教材要求开发者已经阅读Apollo文档、“从零开始网络游戏”。教材通过一个完整网络游戏的开发过程介绍进阶内容。网络游戏功能是点击NPC匹配到游戏服,具体代码请参考“AwesomeGame”。
进入和退出游戏
主要包含登录、定时存档、登出、切服等功能。
登录
要求开发者已经了解Apollo框架,然后再阅读下面内容。
登录过程介绍
Apollo引擎在登录过程中会处理顶号问题,开发者开发过程不用考虑顶号。玩家登录过程如下图示:
开发者通过上图可以理解登录涉及的几个角色。下面介绍登录涉及的核心事件(具体事件说明请参考服务器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事件,设置玩家出生点。
部分核心代码如下:
xclass 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
客户端登录开发
监听服务端发过来的SyncUserDataEvent事件,然后记录玩家数据;监听OnUIInitFinished事件,开始显示ui或处理客户端游戏逻辑。代码如下:
xxxxxxxxxx
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方法打印的登录日志:
- 进入mysql,可以查看到玩家数据:
- MCStudio中查看玩家登录日志,也即OnSyncUserData方法打印的日志:
公共代码管理
登录相关代码目录结构如下:
xxxxxxxxxx
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目录空白处右键,选择属性
(2)选中Subversion->Properties
(3)点击New->Externals
(4)点击 New
(5)弹框中,URL中输入modCommon svn地址;Local Path输入modCommon,表示外链在本地的名字。然后点击OK即可。
总结
- 监听服务端的AddServerPlayerEvent事件处理玩家登录逻辑。
- 监听客户端的OnUIInitFinished事件开始处理客户端逻辑。
- 玩家在服务端登录过程中,在获取玩家数据后,通过自定义事件把玩家数据同步给客户端。
- 使用svn 外链,可以方便管理behavior_packs和developer_mods的公共代码。
定时存档
通过set_use_database_save函数打开定时存档功能,引擎会定时触发savePlayerDataEvent/savePlayerDataOnShutDownEvent事件。mod监听这两个事件,然后执行存档逻辑。定时存档把存档从游戏逻辑中解耦出来,让开发者集中于游戏逻辑的开发。
服务端mod开发
设置定时存档,然后监听savePlayerDataEvent/savePlayerDataOnShutDownEvent事件。核心代码如下:
xxxxxxxxxx
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中退出游戏过程如下所示:
如上图所示,登出涉及到lobby和master。下面介绍登出涉及到的事件:
(1)第2步,lobby处理玩家登出逻辑会触发savePlayerDataEvent / savePlayerDataOnShutDownEvent事件,此时脚本层可以把玩家数据存档。
(2)第4步,lobby引擎在清理玩家数据后触发DelServerPlayerEvent事件,此时脚本层可以清理脚本层玩家内存数据,该事件可以区分玩家退出还是切服。
(3)第5步,master处理玩家登出逻辑时会触发PlayerLogoutServerEvent事件,该事件可以区分玩家退出还是切服。
下面介绍AwesomeGame登出逻辑的开发。
服务端登出逻辑。
savePlayerDataEvent/savePlayerDataOnShutDownEvent事件相关逻辑已经实现,下面还需要监听DelServerPlayerEvent事件,清除玩家数据。代码如下:
xxxxxxxxxx
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日志,可以查看到登出日志:
总结
- 玩家退出也会触发lobby的savePlayerDataEvent/savePlayerDataOnShutDownEvent事件。
- 监听OnDelServerPlayer事件清除脚本层中玩家内存数据。
切服
玩家切服是从一个服务器退出,然后再登录到指定服务器过程,实质是退出游戏然后再进入游戏过程。目前,登入事件是可以区分登录和切服,登出事件也可以区分切服过程中登出还是退出游戏。下图是玩家由lobby切到game过程图:
如上图所示,第1步到第3步是玩家登出逻辑,第4步到第7步是玩家登录逻辑。下面介绍涉及到的事件和api:
- 第1步,lobby中调用TransferToOtherServer函数触发切服逻辑。
- 第2步,master接受切服请求后会触发PlayerTransferServerEvent事件。
- 第3步触发事件请参考“登出过程介绍”。
- 第4~8步是玩家登录过程,触发事件请参考“登录过程介绍”
服务器关服
服务器关服前会触发ServerWillShutDownEvent事件,开发者可以在该事件中处理存档和清理现场的逻辑。
AwesomeGame服务端在退出时,需要保存所有在线玩家数据。核心代码如下:
xxxxxxxxxx
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事件,清理服务端现场。
游戏玩法
这里介绍常见游戏玩法设计,比如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。
核心代码如下:
xxxxxxxxxx
NPC_POS = {
"gameA": (1396.273, 68, 57.163),
"gameB": (1403.273, 68, 57.163),
"gameC": (1410.273, 68, 57.163),
}
class NpcData(object):
'''
记录npc的数据
'''
def __init__(self, pos, name):
super(NpcData, self).__init__()
self.entity_id = None
self.pos = pos
self.inited = False
self.name = name
def init_success(self, id):
'''
npc初始化成功
'''
self.entity_id = id
self.inited = True
class NpcManager(object):
'''
管理所有npc
'''
def __init__(self, server):
super(NpcManager, self).__init__()
global NPC_POS
self.server = weakref.proxy(server)
self.waiting_npc = []#需要注册的npc列表
self.active_npc_dict = {}#entity id => NpcData
for name, pos in NPC_POS.iteritems():
npc_data = NpcData(pos, name)
self.waiting_npc.append(npc_data)
#添加定时器,注册npc。
self.register_npc_timer = timer.TimerManager.addTimer(10, self.TryCreateNpcs)
def TryCreateNpcs(self):
'''
尝试创建npc
'''
self.register_npc_timer = None
if not self.waiting_npc:
return
npc_num = len(self.waiting_npc)
for id in xrange(npc_num):
if self._CreateSingleNpc(id):
npc_data = self.waiting_npc[id]
self.active_npc_dict[npc_data.entity_id] = npc_data
self.waiting_npc[id] = None
self.waiting_npc = [one for one in self.waiting_npc if one]
if self.waiting_npc:
self.register_npc_timer = timer.TimerManager.addTimer(2, self.TryCreateNpcs)
else:
print 'create all npc success'
def _CreateSingleNpc(self, id):
'''
创建一个npc
'''
npc_data = self.waiting_npc[id]
#检查引擎是否已经加载了chunk。只有先加载chunk,然后才创建npc。
chunkComp = self.server.CreateComponent("-1", "Minecraft", "chunkSource")
exist = chunkComp.CheckChunkState(4, npc_data.pos)
if (not exist) or (int(exist) == -1):
print 'create npc failed'
return False
#创建npc,并设置属性。
temp_entity = self.server.CreateTempEntity()
type_comp = self.server.CreateComponent(
temp_entity.mId,
modConfig.Minecraft,
'type'
)
type_comp.type = serverApi.GetMinecraftEnum().EntityConst.TYPE_NPC
engine_type = self.server.CreateComponent(
temp_entity.mId,
modConfig.Minecraft,
'engineType'
)
engine_type.engineType = serverApi.GetMinecraftEnum().EntityType.Husk
pos_comp = self.server.CreateComponent(
temp_entity.mId,
modConfig.Minecraft,
'pos'
)
pos_comp.pos = (npc_data.pos[0], npc_data.pos[1], npc_data.pos[2])
rot_comp = self.server.CreateComponent(
temp_entity.mId,
modConfig.Minecraft,
'rot'
)
rot_comp.rot = (0, 180)
dimesion_comp = self.server.CreateComponent(
temp_entity.mId,
modConfig.Minecraft,
'dimension'
)
dimesion_comp.dimensionId = 4
entity_id = self.server.CreateEntity(temp_entity)
#检查npc是否成功创建。
if (not entity_id) or (int(entity_id) == -1):
return False
npc_data.init_success(entity_id)
name_comp = self.server.CreateComponent(entity_id, 'Minecraft', 'name')
name_comp.SetName(NPC_NAME[npc_data.name])
return True
def GetNpcData(self, entity_id):
return self.active_npc_dict.get(entity_id, None)
功能验证
用MCStudio进入游戏,可以看到玩家前方有三个NPC:
总结:
- 创建NPC前,用CheckChunkState函数检查chunk状态。
- 推荐用定时器创建NPC。
匹配
匹配的设计
点击NPC后,需要把多个玩家匹配分配到GameC,设计到匹配逻辑。匹配是把多个玩家分配到另外一个单独服务器的过程。它是个全服单点逻辑,建议在service实现匹配功能。
通常匹配功能设计思路如下:
- lobby向service请求匹配。
- service包含一个待匹配玩家队列。玩家中途退出时,需要将该玩家从队列中剔除。
- service每帧遍历所有待匹配玩家,根据一定算法,将多个玩家分配到指定game服务器。
- service告知game,玩家即将进入,并告知玩家信息。
- service告知所有玩家切服到指定game。
- 玩家进入game,完成匹配过程。
下面介绍AwesomeGame匹配功能开发。功能是点击npc,然后玩家匹配到某个game中。匹配非常简单,只是平均把玩家分配到game中。
AwesomeGame匹配功能开发
匹配过程如下所示:
- lobby服务端开发
服务端监听EntityBeKnockEvent事件,处理点击NPC行为,根据NPC的种类,处理不同的请求。点击NCPA和NPCB需要向master查询GameA和GameB的在线人数,点击NPC需要处理匹配逻辑。核心代码如下:
xxxxxxxxxx
class AwesomeServer(ServerSystem):
def __init__(self, namespace, systemName):
...
self.ListenForEvent(
serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
'EntityBeKnockEvent',
self, self.OnNpcTouched
)
...
def OnNpcTouched(self, args):
'''
点击npc回调函数。
'''
npc_entity_id = args['entityId']
npc_data = self.npc_mgr.GetNpcData(npc_entity_id)
if not npc_data:
return
player_entity_id = args['srcId']
uid = self.playerid2uid[player_entity_id]
if npc_data.name == 'gameA':
#请求gameA玩家人数
request_data = {
'game': 'gameA',
'player_id': player_entity_id, 'uid': uid,
'client_id':netgameApi.GetServerId()
}
self.NotifyToMaster(
modConfig.GetPlayerNumOfGameEvent,
request_data
)
elif npc_data.name == 'gameB':
#切换至gameB
request_data = {
'game': 'gameB',
'player_id': player_entity_id, 'uid': uid,
'client_id': netgameApi.GetServerId()
}
self.NotifyToMaster(
modConfig.GetPlayerNumOfGameEvent,
request_data
)
elif npc_data.name == 'gameC':
# 请求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.playerid2uid[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.transferPlayerQueue.append(playerId)
CoroutineMgr.StartCoroutine(self.Transfer2Server(playerId, desc_game))
def Transfer2Server(self,playerId,descGame):
'''
把玩家传送至对应的服
'''
yield -30
#判断玩家是否在待传送队列里,若玩家中途下线,则不作处理
if playerId in self.transferPlayerQueue:
netgameApi.TransferToOtherServerById(playerId, descGame)
self.transferPlayerQueue.remove(playerId)
- service开发
service监听UpdateServerStatusEvent事件,可以获取所有game的状态,这些可用game构成了可用资源池。当有玩家请求匹配时,则从可用资源池中分配资源(也就是匹配算法),然后告知玩家。核心代码如下:
xxxxxxxxxx
class AwesomeService(ServiceSystem):
def __init__(self, namespace, systemName):
ServiceSystem.__init__(self, namespace, systemName)
self.mFrameCnt = 0
self.player_server = {}
self.gamec_matching_player = []#gameC的匹配玩家
self.active_game_server_ids = [] #可用game列表
self.search_active_game_idx = 0
self.game_status = {}#serverid => server status.server status:
self.DefineEvent(modConfig.MatchResultEvent)
self.DefineEvent(modConfig.MatchNumEvent)
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.gamec_matching_player:
self.gamec_matching_player.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.gamec_matching_player),
"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.player_server[player_id] = server_id
#如果已经在匹配队列,则不加入匹配队列
if player_id in self.gamec_matching_player:
return
else:
logger.info("%s matching",player_id)
self.gamec_matching_player.append(player_id)
def GameCMatch(self):
'''
检查匹配队列,匹配成功,清空匹配队列
:return:
'''
if not self.gamec_matching_player:
return
desc_game = -1
if len(self.gamec_matching_player) >=2:
desc_game = self.MatchAlgorithm()
if desc_game == -1:
return
for i in range(len(self.gamec_matching_player)):
playerId = self.gamec_matching_player[i]
self.NotifyToServerNode(self.player_server[playerId], modConfig.MatchResultEvent, {'player_id': playerId,'desc_game':desc_game,'game':'gameC'})
self.gamec_matching_player = []#清空匹配队列
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.game_status = {}
self.active_game_server_ids = []
for server_id, status in args.iteritems():
id = int(server_id)
int_status = int(status)
self.game_status[id] = int_status
if int_status == EServerStatus.OK:
self.active_game_server_ids.append(id)
- Master开发
Master查询对应游戏玩家人数用。核心代码如下:
xxxxxxxxxx
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。
备注:需要将"awesome_match"添加到deploy.json里servicelist内module_names配置中
总结
- service监听UpdateServerStatusEvent事件,记录可用服务器列表。
- 匹配过程主要包括:请求匹配、匹配算法、玩家迁移。
游戏外功能
运营指令
运营指令:接收外部http请求,处理游戏相关逻辑,比如给某个玩家发物品,公告等。
master是运营执行入口,开发者可以根据需要,把请求分发到lobby/game/service。下面给AwesomeGame新增一个运营指令,功能是打印指定玩家信息。
获取玩家数据运营指令
由于lobby是异步定时存档的,因此mysql数据可能不是最新的。这里实现方案是:
- 玩家在lobby,从对应lobby的内存中拉取玩家数据。
- 玩家不在lobby,选择任意一个可用lobby,从db中读取玩家数据。
处理过程如下所示:
master主要接受请求然后转发,核心代码如下所示:
xxxxxxxxxx
class AwesomeMaster(MasterSystem):
def __init__(self, namespace, systemName):
MasterSystem.__init__(self, namespace, systemName)
# 注册gm指令
masterHttp.RegisterMasterHttp('/get-user-info', self, self.OnGetUserInfo) self.DefineEvent('GetUserInfoRequestEvent')
self.ListenForEvent(
'Minecraft', 'AwesomeLobby',
'GetUserInfoResponseEvent',
self, self.OnGetUserInfoResponse
)
def OnGetUserInfo(self, client_id, request_body):
'''
获取gm指令
'''
import ujson as json
request = json.loads(request_body)
uid = request['uid']
redis_key_player = "online_user_%d" % uid
#获取玩家在线状态
redisPool.AsyncHgetall(
redis_key_player,
lambda record:self._GetUserInfoCb(client_id, uid, record)
)
def _GetUserInfoCb(self, client_id, uid, record):
'''
回调函数。获取目标lobby,向lobby请求在线人数。
'''
serverid = None
serverlistConf = masterConf.netgameConf['serverlist']
if record:
#若玩家在game中,则随机从一个lobby获取在线人数。
serverid = record.get('serverid', None)
tmpServerConf = masterConf.serverListMap.get(serverid, None)
if not tmpServerConf or tmpServerConf['type'] != 'lobby':
serverid = None
if not serverid:
for serverConf in serverlistConf:
#服务器可用且是lobby
if serverConf['type'] == 'lobby' \
and serverManager.IsValidServer(serverConf['serverid']):
serverid = serverConf['serverid']
break
if not serverid:
response = self.makeFailResponse(master_http.HTTP_CODE_FAIL, 'no valid lobby.')
masterHttp.SendHttpResponse(client_id, response)
return
request_data = {'uid' : uid, 'client_id' : client_id}
self.NotifyToServerNode(serverid, 'GetUserInfoRequestEvent', request_data)
def OnGetUserInfoResponse(self, args):
'''
接受玩家数据,返回http请求。
'''
client_id = args['client_id']
entity = args['user_info']
response = self.makeResponse(master_http.HTTP_CODE_SUCCESS, '', entity)
masterHttp.SendHttpResponse(client_id, response)
Lobby主要获取玩家数据,核心代码如下所示:
xxxxxxxxxx
class AwesomeServer(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
self.ListenForEvent(
modConfig.Minecraft, modConfig.MasterSystemName, modConfig.GetUserInfoRequestEvent,
self, self.OnGetUserInfoRequest)
def OnGetUserInfoRequest(self, args):
'''
获取玩家数据。
'''
uid = args['uid']
client_id = args['client_id']
player_data = self.player_map.get(uid, None)
if not player_data:
if self.dbType == DbType.Mongo:
self.mongoMgr.QueryPlayerData(
uid, uid,
lambda data: self._OnGetUserInfoRequestCb(client_id, data))
elif self.dbType == DbType.Mysql:
self.mysqlMgr.QueryPlayerData(
uid, uid,
lambda data: self._OnGetUserInfoRequestCb(client_id, data))
else:
self._GetUserInfoResponse(client_id, player_data.toSaveDict())
def _OnGetUserInfoRequestCb(self, client_id, record):
'''
回调函数,处理db操作结果,把玩家数据告知master。
'''
if record:
player_data = playerData.PlayerData()
player_data.initPlayer(-1, record)
self._GetUserInfoResponse(client_id, player_data.toSaveDict())
else:
self._GetUserInfoResponse(client_id, {})
def _GetUserInfoResponse(self, client_id, player_info):
'''
玩家数据告知master。
'''
response_data = {'client_id' : client_id, 'user_info' : player_info}
self.NotifyToMaster('GetUserInfoResponseEvent', response_data)
验证
登录到开发机,然后给master发送curl请求,即可获取结果,如下图示:
官方运营指令
查看“服务器MOD SDK”中【运营指令】部分,里面介绍了常用的指令,比如禁言、踢人等。
总结
-
运营指令的实现通常分为两个步骤:
- master接受响应指令,将指令请求转发到其他服务器;
-
lobby/game/serivce实现指令功能。
-
官方实现了常见的运营指令,具体可以查看“服务器MOD SDK”中【运营指令】部分。
优化和维护
优化部分介绍如何查找脚本层内存泄漏。维护部分介绍如何追查线上问题。
内存检查
开发者先阅读“服务器MOD SDK”中“内存检查”部分。下面结合“AwesomeGame”网络游戏介绍如何检查内存泄漏。
制造内存泄漏问题
lobby玩家退出时,不清理内存数据。按照下面方式修改代码:
xxxxxxxxxx
def OnDelServerPlayer(self, args):
'''
清除玩家内存数据。
'''
player_id = args.get('id','-1')
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]
检查内存泄漏
然后给master发送check-memory-run指令,生成一个内存快照,结果如下图示:
查看lobby日志:
用MCStudio进入游戏,然后退出,再给master发送check-memory-run指令,再生成一次内存快照,结果如下图示:
再次查看lobby日志:
分析:
[Top 10 differences]记录占用内存最多的10行代码。简单分析下面日志:
obj_report.py文件43行占用内存最多,占用48k内存,实例个数为1,两次内存快照间增加了一个实例,平均每个实例占用48k。、
[DIFF_MORE]记录了内存变化最多的类型。简单分析下面日志:
两次内存快照间新增了一个PlayerData类型实例。
接着分析AwesomeGame内存泄漏问题。第一次内存快照时,没有玩家登录,接着玩家登录再登出,然后再生成了一次内存快照。第二次内存快照时,没有玩家在游戏中,因此服务端内存是不会有玩家数据,也即不会有PlayerData类型实例的。但是,[DIFF_MORE]中显示内存中还是多了一个PlayerData类型实例,这说明存在内存泄漏。
总结
- check-memory-run指令可以检测两次内存快照间内存变化。
- 内存泄漏检查要点:在游戏平稳时生成内存快照(比如没有玩家登录登出),然后分析内存变化。
Hunter
开发者先阅读“服务器MOD SDK”中“Hunter调试命令”部分。下面结合“AwesomeGame”网络游戏介绍如何在线调试mod。
获取lobby服在线玩家信息
用MCStudio进入游戏,然后给master发送/hunter-debug指令:
实质是在lobby中执行下面代码:
xxxxxxxxxx
import server.extraServerApi as serverApi
#获取AwesomeServer实例
mainSys=serverApi.GetSystem("Minecraft", "AwesomeLobby")
#打印玩家信息
print mainSys.playerid2uid
执行结果需要查看lobby日志:
日志说明,lobby服只有一个玩家,日志打印了该玩家的player id和uid。
清空lobby服玩家信息
然后给master发送/hunter-debug指令:
实质是在lobby中执行下面代码:
xxxxxxxxxx
import server.extraServerApi as serverApi
#获取AwesomeServer实例
mainSys=serverApi.GetSystem("Minecraft", "AwesomeLobby")
#清除玩家信息
mainSys.playerid2uid = {}
mainSys.player_map = {}
mainSys.uid2dimension = {}
print “clear ok”
执行结果需要查看lobby日志:
日志打印”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中配置内容如下所示:
xxxxxxxxxx
{
"modName":"neteaseMonitor",
"modRequire":[ ]
}
modName表示mod的名字,modRequire表示前置mod名字列表。上面内容表示当前mod的名字是neteaseMonitorSample,它在neteaseMonitor后面加载。
- neteaseMonitor根目录下添加netease_require.json文件,配置内容如下:
xxxxxxxxxx
{
"modName":"neteaseMonitor",
"modRequire":[ ]
}
注意,neteaseMonitor一定要配置netease_require.json,设置mod的名字,否则引擎找不到这个mod。
总结
相互依赖的多个mod,都需要配置netease_require.json,设置mod名字和依赖关系。