# 游戏玩法
这里介绍常见游戏玩法设计,比如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。
核心代码如下:
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需要处理匹配逻辑。核心代码如下:
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构成了可用资源池。当有玩家请求匹配时,则从可用资源池中分配资源(也就是匹配算法),然后告知玩家。核心代码如下:
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查询对应游戏玩家人数用。核心代码如下:
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事件,记录可用服务器列表。
匹配过程主要包括:请求匹配、匹配算法、玩家迁移。