作者:Ross Przybylski
异步游戏真的非常棒,因为玩家可以无需长期待在游戏中便能享受到有趣的游戏体验。是在线服务器成就了这种便捷的游戏风格,而本篇文章将着重解释如何通过服务器加载之前所储存的数据,并将其用于游戏客户端的用户界面上。(请点击此处阅读第1部分)
本篇文章将解释:1.如何查询储存于MySQL数据库中的一列游戏记录,并将结果传送给游戏客户端
2.如何在客户端上说明查询结果,并设计一个有意义的游戏列表用户界面而帮助我们更有效地游戏
3.如何通过一款异步游戏而再次创造出生动且同步的多人体验
4.如何重播动画以呈现出玩家对手的移动
要求预备知识
基于游戏的开发体验
熟悉ActionScript 3.0
阅读过本系列文章的第二部分
产品要求Flash Professional(试用版)
Smart Fox Server Pro(试用版)
MySQL数据库
用户级别
高级
生成玩家的游戏列表大受欢迎的异步游戏,如《英雄法师》便使用了“游戏列表”用户界面,即让玩家能够进入并继续之前的异步游戏过程。
在创造《英雄法师》的游戏列表时,我最先创造了名为“HM_GamesList”的全新用户界面屏幕类。我想要先专注于数据和代码组件,所以最初的设计便局限于数据头和滚动列表组件,即能够用于在服务器上填充信息:
这一界面中有一个数据查询库,即带有一列活跃玩家的游戏数据。所有MySQL查询生成都是发生在服务器一端,而与我们的在线服务器的交流如下:
1.游戏客户端:从服务器上请求数据
2.服务器:处理请求,向客户端发送回应
3.游戏客户端:收到服务器回应
4.游戏客户端:基于数据执行预期任务
步骤1:请求游戏列表游戏客户端从服务器上请求游戏列表:
//CODE EXAMPLE 1: Request Game List from Server
private function getGameList(lowerLimit:int){
//Create a new object to send command parameters
var params = new Object();
//Pass the player’s unique member id
params.pId = pId;
//Show user prompt while waiting for response
showPrompt(“ProcessingRequestPrompt”);
//Send Smart Fox Server an extension message
/*
sendXtMessage(xtName:String, cmd:String, paramObj:*, type:String = “xml”)
xtName = Name of your server side extension
cmd = Unique identifier name for this command
paramObj = Object contain parameters for command
type = Indicates whether we’re sending as XML or raw string
*/
smartFox.sendXtMessage(“HMServer”, “Game List”, params, “xml”);
}
步骤2:处理游戏列表请求服务器端将处理请求并向客户端发送响应。在本系列文章的第二部分中,我曾经解释过游戏是如何使用两个附录(hm_games和hm_gameresults)保存到MySQL数据库中。功能loadGameList将创建MySQL查询并发回我们所需要的数据去生成游戏列表。
//CODE EXAMPLE 2: Handle Game List Request on Server
function handleRequest(cmd, params, user, fromRoom, protocol){
if(protocol == “xml”){
//….CODE FOR OTHER EXTENSION COMMANDS OMITTED….
else if(cmd == “Game List”){
if(params.hmId != null){
//THE FOLLOWING MYSQL STATEMENT GATHERS A LIST OF GAMES PLAYER HAS PLAYED BY JOINING
//THE GAME AND GAME RESULTS TABLES CREATED IN PART 2
var sql = “SELECT ID_GAME from hm_games JOIN hm_gameresults using (ID_GAME) WHERE ID_MEMBER =1″+params.hmId;
//WE CREATE AN ARRAY TO STORE THE GAME LIST
var gameList = [];
//WE EXECUTE THE QUERY
var queryRes = dbase.executeQuery(sql);
//IF THE QUERY RETURNS RESULTS, POPULATE TO ARRAY
if(queryRes != null && queryRes.size() > 0){
for(var i = 0; i < queryRes.size(); i++){
//GET THE ACTIVE ROW
var dataRow = queryRes.get(i);
//CREATE GAME RECORD OBJECT
var gameRecord = {};
//STORE THE GAME ID IN THE RECORD
gameRecord.ID_GAME = dataRow.getItem(“ID_GAME”);
//ADD RECORD TO ARRAY
gameList.push(gameRecord);
}
}
//STORE THE GAME LIST IN THE SERVER RESPONSE
response.gameList = getGameList(params.hmId);
}
}
//….CODE FOR OTHER EXTENSION COMMANDS OMITTED….
}
}
步骤3:收到游戏列表回应游戏客户端收到服务器回应。
//CODE EXAMPLE 3: Receive Game List from Server
private function onExtensionResponse(evt:SFSEvent):void{
//EXTRACT RESPONSE TYPE AND RESPONSE DATA FROM SFSEvent
var type:String = evt.params.type;
var dataObj:Object = evt.params.dataObj;
//….CODE OMITTED….
//EXTRA COMMAND FROM RETURNED DATA OBJECT
cmd= dataObj.cmd;
var error:String = dataObj.error;
//….CODE OMITTED….
if(error != “”){
//IF RESPONSE RETURNS AN ERROR, SHOW USER A MESSAGE PROMPT
showPrompt(“HM_MessagePrompt”, cmd + ” Error”, error);
}
else{
//….CODE OMITTED….
//ADD CONDITION FOR SERVER RESPONSE CREATE GAME RECORD
else if(cmd == “Game List”){
//HIDE OUR PROCESSING REQUEST PROMPT
hidePrompt();
//INSRUCT OUR GAME LIST CLASS TO RECEIVE THE LIST
gameList.receiveGameList(dataObj);
}
//….CODE OMITTED….
}
}
步骤4:填充游戏列表游戏客户端执行预期任务而填充列表:
//CODE EXAMPLE 4: Populate Game List
private function receiveGameList(gameList:Object):void{
//The game list is returned from server as array
var gameList:Array = dataObj.gameList;
//Create a new data provider to store the list
var dp:DataProvider = new DataProvider();
//Iterate through the list to add new items to data provider
for(var i:int = 0; i < gameList.length; i++){
var gameRecord:Object = gameList[i];
//Add a label property to object so it shows up in list cell
gameRecord.label = gameRecord.ID_GAME;
//Add item to data provider
dp.addItem(gameRecord);
}
//Set our UI list’s data provider
list.dataProvider = dp;
}
而以下便是我们的结果:
高级游戏列表查询尽管具有功能性,但是上述所创造的基本游戏列表缺少了稳定用户体验所需要的关键信息。玩家需要知道游戏的创造时间,上一个回合是什么时候,是谁的回合,最重要的是游戏将加载哪个对手。
整合游戏和游戏结果列表最理想的查询需要使用最少的资源和带宽将所有相关信息传回客户端。由Reflection Software的程序员Marco Rousonelos所设计的这一查询结合MySQL能够帮助各大论坛使用排名和派生表去生成预期结果集:
#CODE EXAMPLE 5: ADVANCED GAME LIST QUERY
SELECT IF(whoseTurn = 2 and status != 2, 1, 0) as myTurn, ID_GAME, ID_GAMETYPE, version, timeLastTurn, timeCreated, timeRecorded, status, isAsync, whoseTurn,
MAX(CASE WHEN PN = 1 THEN ID_MEMBER ELSE NULL END) AS ‘P1ID’,
MAX(CASE WHEN PN = 1 THEN memberName ELSE NULL END) AS ‘P1N’,
MAX(CASE WHEN PN = 1 THEN result ELSE NULL END) AS ‘P1R’,
MAX(CASE WHEN PN = 2 THEN ID_MEMBER ELSE NULL END) AS ‘P2ID’,
MAX(CASE WHEN PN = 2 THEN memberName ELSE NULL END) AS ‘P2N’,
MAX(CASE WHEN PN = 2 THEN result ELSE NULL END) AS ‘P2R’
FROM
(SELECT g.ID_GAME, g.ID_GAMETYPE, g.version, timeLastTurn, timeCreated, timeRecorded, status, isAsync, whoseTurn, r.ID_MEMBER, r.result,
( CASE g.ID_GAME
WHEN @curGame
THEN @curRow := @curRow + 1
ELSE @curRow := 1 AND @curGame := g.ID_GAME END
) AS PN
FROM hm_games g
JOIN hm_gameresults r USING(ID_GAME)
JOIN hm_gameresults pg ON g.ID_GAME = pg.ID_GAME AND pg.ID_MEMBER =2
,(SELECT @curRow := 0, @curGame := -1) n
) data
JOIN smf_members m USING(ID_MEMBER)
GROUP BY ID_GAME
基于这一查询我们能够生成如下结果集:
ID_GAME 1010
P1ID 1
P1N Ross
P1R 0
P2ID 2
PD2 Kelly
P2R 0
Status 1
whose Turn 1
……
添加额外的玩家
对于支持两个以上玩家的游戏,我们需要在查询中添加如下附加内容:
#CODE EXAMPLE 6: Additional Player Support
MAX(CASE WHEN PN = 3 THEN ID_MEMBER ELSE NULL END) AS ‘P3ID’,
MAX(CASE WHEN PN = 3 THEN memberName ELSE NULL END) AS ‘P3N’,
MAX(CASE WHEN PN = 3 THEN result ELSE NULL END) AS ‘P3R’,
MAX(CASE WHEN PN = 4 THEN ID_MEMBER ELSE NULL END) AS ‘P4ID’,
MAX(CASE WHEN PN = 4 THEN memberName ELSE NULL END) AS ‘P4N’,
MAX(CASE WHEN PN = 4 THEN result ELSE NULL END) AS ‘P4R’
结果排序我们应该按照如下顺序设置结果集:
1.游戏状态(首先呈现出进行中的游戏)
2.回合(首先呈现出玩家所处回合)
3.最新更新(首先呈现出游戏的最新更新)
我们可以通过添加一些排序次序而做到这一点,即对于查询的声明:
#CODE EXAMPLE 7: Order Statement
Order by status asc, myTurn desc, timeRecorded desc
限制结果我们必须清楚这一查询结果将为所有发出请求的会员账号发送所有游戏记录结果。但是随着游戏变得更加受欢迎,即越来越多玩家开始进入游戏,我们将面对越来越庞大的数据集。所以为了确保服务器,网络和用户设备不会负荷过大,我们必须包含限制声明,如此用户便只能接收到特定的结果:
#CODE EXAMPLE 8: Limit Statement
Limit 0, 30
定制查询我们可以根据不同个性化的游戏调整并定制查询,同时也能够通过包装服务器请求中的额外参数对此进行控制。例如你可以储存一个“lowerLimit”属性和一个“limitSpan”属性去控制查询的限制。
基于稳定的查询,即能够传送必要的结果集,我们将准备生成更有效的用户体验而呈现出结果。
设计异步多人UI玩家的游戏列表是异步多人游戏体验的核心。该列表是用于导航,检查状态,并反映游戏的发展。除此之外,游戏列表也是一个非常棒的排行榜/记录工具,能够用于回顾过去的战斗,敌人等等内容!
相关游戏记录信息一个优秀的游戏列表是始于一个优秀的游戏记录。每一个游戏记录都应该包含如下信息:
最后一个回合或者完成游戏所需要的时间
游戏创造的时间
状态(不管是轮到玩家攻击,等待回合,防御,或获得胜利)
参与其中的玩家名字
这些属性能够有效地帮助玩家选择想要加载的游戏。除此之外我们也可以添加更多细节以及其它可能性:
独特的游戏记录ID
是否进行排名
地图的名称
游戏对象
最理想的情况便是设计能够匹配列表大小的游戏记录,从而让它们能够更有效地呈现在任何规格的手机设备上。《英雄法师》便是利用玩家形象去呈现角色肖像:
填充列表在设计好游戏的记录单元格布局后,我们可以将游戏记录类别添加到列表组件中,从而确保玩家可以使用该内容去访问游戏过程。往列表中添加记录的过程与在UI列表上添加内容一样,只不过这是在添加一些简单的单元格,而我们所添加的则是自己定制设计的单元格。
《英雄法师》使用了AURA多屏幕组件UI。AURA代表面线ActionScript 3.0动画,实用工具和资源。这是我所编写的一个类别和组件库,即用于提升像监听器与资源管理和UI设计等任务的速度。在屏幕截图下方的列表是符合屏幕规格以及用户设备输入控制的高级组件。举个例子来说吧,如果你正在一个触屏输入手机设备上玩游戏,我们便可以通过滑动去操作该列表。而如果面对的是台式机,你则需要使用标准滚动条进行导航。我们也可以面向手机GPU去优化该列表,并在像第一代iPad等设备商基于60帧/秒去渲染单元格。
游戏加载游戏列表的主要功能是让玩家能够通过在列表上选择一个项目去加载之前保存的游戏环节。与游戏保存的过程类似,游戏加载也要求客户端和服务器代码去创造游戏加载请求,并从数据库中检索游戏状态,并启动游戏引擎去恢复玩家想要玩的游戏内容。这一次我们也需要遵循4个步骤:
1.游戏客户端:向服务器请求数据
2.服务器:处理请求,向客户端发送回应
3.游戏客户端:收到服务器回应
4.游戏客户端:基于数据执行预期任务
注:尽管我们能在游戏列表查询中收集游戏状态数据,但是我仍建议使用另一个服务器请求去获得新游戏记录,如此才能有效节省带宽。
步骤1:请求游戏加载游戏列表中的每个单元格将使用如下代码向服务器发送请求:
//CODE EXAMPLE 9: Client Side Load Game Request
//In the Game List constructor, add an event listener to our list for when a cell is clicked
public function HM_GameList(){
//…CODE OMITTED
list.addEventListener(ListEvent.ITEM_CLICK, gameSelected, false, 0, true);
}
//The game selected function handles our server request
private function gameSelected(evt:Event):void{
//First ensure a valid cell is selected
if(list.selectedIndex != -1){
/*
It’s possible that older games may not be compatible with newer versions of the engine.
So, It’s a good idea to store the required game version in the game record data.
You can write an compatability check function to ensure the version is compatible.
*/
if(HM_App.isCompatibleVersion(list.selectedItem.v) == false){
HM_Main.HMMain.showPrompt(“HM_MessagePrompt”, “Version Mismatch”, “Your game version ‘”+ HM_App.appVersion +”‘ is not compatible with this recorded game’s version ‘” + list.selectedItem.v+”‘.”);
return;
}
//Once again, create a new params object to store request parameters
var params = new Object();
params.gId = list.selectedItem.ID_GAME;
//Show our request prompt to the user
showPrompt(“ProcessingRequestPrompt”);
//And send the message to server
smartFox.sendXtMessage(“HMServer”, “Load Game”, params, “xml”);
}
}
步骤2:处理游戏请求在服务器这端,我们将为“游戏加载”请求添加一个状态。可能有人会问,既然《英雄法师》也能够使用Smart Fox进行同步游戏,为什么我们不在玩家同时在线时将异步游戏变成在线实时对抗呢?游戏空间将在服务器上循环访问空间列表,并检查是否有任何空间的游戏ID符合玩家想要异步加载的记录。如果能够找到合适的对抗,那么服务器将传回空间ID让它能够与在线玩家进行连接。而对于实时对抗,玩家可以直接从游戏空间中加载游戏数据。但是如果找不到实时对抗,那么服务器便只能从数据库加载游戏状态。
注:为了实现这一循环,我们必须在创造一个实时游戏空间时将游戏记录ID当成空间变量储存起来。但是如果你只对异步游戏玩法感兴趣的话,你便无需这么做。
//CODE EXAMPLE 10: Server Side Handle Load Game Request
function handleRequest(cmd, params, user, fromRoom, protocol){
if(protocol == “xml”){
//….CODE FOR OTHER EXTENSION COMMANDS OMITTED….
else if(cmd == “Load Game”){
//First, we package our response to include the game record id and a room id
response.gId = params.gId;
response.rId = -1;
//HERE WE WANT TO GO THROUGH LIST OF ACTIVE GAMES AND SEE IF ANY MATCH TARGET GAME ID, IF SO, JOIN THAT ROOM, OTHERWISE, FIRE UP GAME
var rooms = _server.getCurrentZone().getRooms();
for(var i = 0; i < rooms.length; i++){
var room = rooms[i];
if(room.getName().indexOf(“#”+params.gId) != -1 || (room.getVariable(“gId”) != null && room.getVariable(“gId”).getValue() == params.gId)){
response.rId = room.getId();
break;
}
}
//A live room matching the game id was not found, so we need to load the game
if(response.rId == -1){
var gameRecord = loadGame(params.gId, response);
response.cL = gameRecord.cL;
var memberId = user.getVariable(“hmId”).getValue();
sql = “SELECT lastCmd from hm_gameResults WHERE ID_MEMBER = “+memberId+” and ID_GAME =”+params.gId;
queryRes = dbase.executeQuery(sql);
dataRow = queryRes.get(0);
response.lC = dataRow.getItem(“lastCmd”);
}
}
//….CODE FOR OTHER EXTENSION COMMANDS OMITTED….
}
}
游戏加载功能将为游戏加载检索必要的信息。
//CODE EXAMPLE 11: Server Side Load Game Function
function loadGame(ID_GAME, response){
//Generate a MySQL statement to load the record for the provided game record id
sql = “SELECT cmdLog, timeCreated, timeRecorded, timeLastTurn, status from hm_games WHERE ID_GAME = ” + ID_GAME;
queryRes = dbase.executeQuery(sql);
//If the query was unsuccessful, add an error message to the prompt to inform user
if(queryRes == null || queryRes.size() <= 0){
response.error = “Unable to load game”;
return “”;
}
else{
//Hero Mages can be played synchronously and asynchronously
//Whenever an unfinished game is loaded as an async game, we change isAsync property to reflect
if(queryRes.get(0).getItem(“status”) != 2){
dbase.executeCommand(“Update hm_games set isAsync = 1 WHERE ID_GAME = ” + ID_GAME);
}
//Package our response with the cmdLog, which is where we store game state in PART 2
var gameRecord = {};
gameRecord.cL =queryRes.get(0).getItem(“cmdLog”)
return gameRecord;
}
}
步骤3:接收游戏加载回应回到客户端,我们的Smart Fox服务器扩展响应监听器需要一个新的状态去接收来自服务器的“游戏加载”回应。基于回应类型,我们可以选择加入现有的实时游戏或使用收到的参数创造一款新游戏:
//CODE EXAMPLE 12: Client Side Load Game Response
private function onExtensionResponse(evt:SFSEvent):void{
//….CODE OMITTED….
//ADD CONDITION FOR SERVER RESPONSE LOAD GAME
else if(cmd == “Load Game”){
//Hide the waiting for response prompt
hidePrompt();
//Check to see if server provided a room id
if(dataObj.rId == -1){
//Room id not provided, so we load the game state directly from response object
loadGame(dataObj);
}
else{
/*
The server provided a room id. This means there is a
live game for this session already created by another player
so all we have to do is join the game
*/
joinRoom(dataObj.rId, “”, false);
}
}
//….CODE OMITTED….
}
步骤4:游戏状态加载基于不同游戏引擎,游戏功能的加载也会不同,不过游戏功能都需要处理以下一些任务:
1.将接收到的游戏状态数据串转化回对象中
2.设置两个不同的标记“isRunningCommandList”=正确以及“useAnimations”=错误
3.在引擎中运行命令列表从而在幕后有效地“播放游戏”。你的引擎代码应该检查isRunningCommandList标记以确保当命令的自动回应(游戏邦注:如反攻行动)已包含于命令列表时,我们不会再次启动它。
重放动画遵循上述步骤你将能从游戏列表中加载任何游戏,并恢复与最后一次移动记录相符合的游戏状态。然而我们也需要考虑到异步游戏组件也会在其他玩家离开时改变游戏状态。而简单地加载当前的游戏状态会让玩家感到困惑,因为他们并不知道自己的组件正在执行怎样的命令。为了创造有效的异步多人游戏体验,我们便需要对命令记录过程和游戏加载代码做出一定的修改。
记录最后一次移动为了分别为每个玩家记录最后一次移动,我们需要在hm_gameresults(是在第二部分文章所创造的名为“lastCmd”的列表)上添加额外的属性。这一属性是一个整数值,即用于储存玩家最后一次移动的相关索引。
当我们需要发送新的游戏命令时,只要将命令记录索引沿着命令进行传达便可。随后,我们将在代码块中(也就是用于处理游戏状态更新)添加如下代码:
//CODE EXAMPLE 13: Storing lastCmd
function handleRequest(cmd, params, user, fromRoom, protocol){
if(protocol == “str”){
//GENERATE LIST OF RECIPIENTS THE SERVER WILL SEND THIS COMMAND TO
//….CODE OMITTED….
//params[2] stores game record id
//If this game record id is included, we need to write this command to stored game log
if(params[2] != undefined){
if(params[1].indexOf(“cT=eT”) != -1){//If this is an end turn command
//Convert notated command into object
var cmdObj = stringToObject(params[1]+”", “|”, “=”);
//Get the id of player whose turn is next
var nextTurnId = cmdObj.nId;
//Write update to game record in database
sql = “UPDATE hm_games set cmdLog = CONCAT(cmdLog, ‘<c>” + params[1] + “</c>’), timeRecorded = ” + Math.floor(getTimer() / 1000) + “, timeLastTurn = ” + Math.floor(getTimer() / 1000) +”, whoseTurn = “+nextTurnId+” WHERE ID_GAME = ” + params[2];
}
else{
//Write update to game record in database
sql = “UPDATE hm_games set cmdLog = CONCAT(cmdLog, ‘<c>” + params[1] + “</c>’), timeRecorded = ” + Math.floor(getTimer() / 1000) +” WHERE ID_GAME = ” + params[2];
}
success = dbase.executeCommand(sql);
if(success == false){
//THE DATABASE DID NOT RECORD THE MOVE CORRECTLY
//CREATE A NEW RESPONSE TO NOTIFY GAME CLIENT OF THE ERROR
}
//***NEW CODE BEGIN***
//Get list of all users in this room and update the lastCmd property in game results for everyone who witnessed this move live
var lastCmd = cmdObj.lC; //Store the lastCmd to record last witnessed move in live players’ gameresult records
var allUsers = room.getAllUsers();
for(i = 0; i < allUsers.length; i++){
var memberId = allUsers[i].getVariable(“hmId”).getValue();
sql = “UPDATE hm_gameResults set lastCmd =”+lastCmd+” WHERE ID_MEMBER = “+memberId+” and ID_GAME =”+gId;
trace(“GAME RECORD UPDATE: ” + sql);
dbase.executeCommand(sql);
}
//***NEW CODE END***
}
_server.sendResponse([params[1]], -1, null, recipients, “str”);
return;
}
为那些看不见的移动呈现动画我们将在服务器上的游戏加载处理程序上添加一个额外内容去储存玩家的lastCmd,如下:
//CODE EXAMPLE 14: Getting lastCmd
var memberId = user.getVariable(“hmId”).getValue();
sql = “SELECT lastCmd from hm_gameResults WHERE ID_MEMBER = “+memberId+” and ID_GAME =”+params.gId;
queryRes = dbase.executeQuery(sql);
dataRow = queryRes.get(0);
response.lC = dataRow.getItem(“lastCmd”);
以下便是完整的游戏加载回应:
//CODE EXAMPLE 15: Revised Game Load Handler
function loadGame(ID_GAME, response){
//Generate a MySQL statement to load the record for the provided game record id
sql = “SELECT cmdLog, timeCreated, timeRecorded, timeLastTurn, status from hm_games WHERE ID_GAME = ” + ID_GAME;
queryRes = dbase.executeQuery(sql);
//If the query was unsuccessful, add an error message to the prompt to inform user
if(queryRes == null || queryRes.size() <= 0){
response.error = “Unable to load game”;
return “”;
}
else{
//Hero Mages can be played synchronously and asynchronously
//Whenever an unfinished game is loaded as an async game, we change isAsync property to reflect
if(queryRes.get(0).getItem(“status”) != 2){
dbase.executeCommand(“Update hm_games set isAsync = 1 WHERE ID_GAME = ” + ID_GAME);
}
//Package our response with the cmdLog, which is where we store game state in PART 2
var gameRecord = {};
gameRecord.cL =queryRes.get(0).getItem(“cmdLog”)
//***NEW CODE BEGIN***
//CODE EXAMPLE 14: Getting lastCmd
var memberId = user.getVariable(“hmId”).getValue();
sql = “SELECT lastCmd from hm_gameResults WHERE ID_MEMBER = “+memberId+” and ID_GAME =”+params.gId;
queryRes = dbase.executeQuery(sql);
dataRow = queryRes.get(0);
response.lC = dataRow.getItem(“lastCmd”);
//***NEW CODE END***
return gameRecord;
}
}
在储存了最后的命令索引后,我们可以通过再次激活useAnimations标记而有效地播放适当的动画。
其它注意事项除了我所解释的这些步骤,我们还可以通过许多方法去定制异步体验并添加额外的功能。举个例子来说吧,如果玩家能够拥有无限的世界去回应一款异步游戏的话会怎样?一个缺乏运动精神的玩家会无限期延缓他的回合,并阻止获胜玩家宣布胜利。而解决这一问题的一大方法便是设置“最大等待”期限,让玩家可以与长时间未回到游戏中的对手解除关系。就像在《英雄法师》中,我便规定如果玩家在3天内未回到之前的游戏回合中,那么对手玩家就可以选择与之解除关系。
使用过滤器只呈现出活跃的游戏,基于特定对手寻找游戏,以及查阅对手的状态等也是非常有帮助的功能。
总结本篇文章主要解释了异步多人游戏用户界面的创建,已储存的游戏环节的加载,以及对手最后一次移动的动画重放。而在接下来的文章中我们将专注于异步多人游戏匹配系统的相关理念,从而让玩家可以无需在线连接而开始玩一款新游戏或加入现有的游戏中。
原文发表于2012年5月30日,所涉事件和数据均以当时为准。