# 游戏玩法

这里介绍常见游戏玩法设计,比如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: 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需要处理匹配逻辑。核心代码如下:

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事件,记录可用服务器列表。

  • 匹配过程主要包括:请求匹配、匹配算法、玩家迁移。