游戏邦在:
杂志专栏:
gamerboom.com订阅到鲜果订阅到抓虾google reader订阅到有道订阅到QQ邮箱订阅到帮看

分享制作异步多人游戏的方法和经验(3)

发布时间:2013-12-26 17:13:25 Tags:,,,,

作者:Ross Przybylski

因为玩家喜欢无需消耗较长时间的社交游戏体验,所以异步多人游戏非常受欢迎。这种便利的游戏风格是由在线服务器所创造的,即让玩家可以直接在自己的设备上体验游戏的更新内容。本篇文章将解释服务器如何加载之前保存到数据库的游戏,并让游戏玩家可以在游戏客户端的用户界面上进行访问。(请点击此处阅读本文第12、4篇

本文将解释:

1.如何查询MySQL数据库中的一列游戏记录,并将结果发送到游戏客户端。

2.如何在客户端上解释查询结果,并设计玩家可以继续的有意义的游戏列表用户界面。

3.如何重新创造一个声明并从一款异步游戏中同步多人体验。

4.如何在玩家对手离开时重播动画去呈现他们的行动。

要求:

先备知识

经历过回合游戏的开发

熟悉ActionScript 3.0

阅读过第二部分内容

所需工具

Flash Professional

Smart Fox Server Pro

MySQL Database

用户级

高级

生成玩家游戏列表

像《英雄学院》等受欢迎的异步游戏便使用了一个“游戏列表”用户界面,允许玩家访问并继续他们的异步游戏环节。

Multiplayer game list in Hero Academy(from indieflashblog)

Multiplayer game list in Hero Academy(from indieflashblog)

当为我的《英雄法师》创建游戏列表时,我一开始创造了一个名为“HM_GamesList”的全新用户界面屏幕类。我想要先专注于数据和代码组件,所以最初的设计是局限于一个数据头和一个可卷动的列表组件,将用于填充从服务器上检索到的信息:

A basic game list UI(from indieflashblog)

A basic game list UI(from indieflashblog)

该界面中有一个数据库查询,包含了一列活跃玩家的游戏记录。所有的MySQL查询生成都是发生在服务器端,与在线服务器的交流过程如下:

1.游戏客户端:从服务器中请求数据

2.服务器:处理请求,发送客户端回应

3.游戏客户端:接收服务器回应

4.游戏客户端:基于数据执行预想的任务

第一步:请求游戏列表

游戏客户端从服务器中请求游戏列表:

//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”);
}

第二步:处理游戏列表请求

服务器端代码处理请求并发送给客户端一个回应。在第二部分中:保存Game State到Online Database,我解释了如何使用两个表格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….
}
}

第三步:接收游戏列表回应

游戏客户端接收服务器回应。

//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….
}
}

第四步:填充游戏列表

游戏客户端执行填充列表的预想任务:

//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;
}

以下是我们的结果:

Basic Game List Populated with Game Id's(from indieflashblog)

Basic Game List Populated with Game Id’s(from indieflashblog)

高级游戏类别查询

尽管具有功能,但上述创造的基本游戏列表缺少可靠的用户体验所需要的关键信息。玩家需要知道游戏是何时创造出来的,何时会出现最后回合,当下是谁的回合,以及最重要的,游戏将加载哪个敌人。

整合游戏和游戏结果表

理想的查询需要将使用资源和带宽把所有的相关信息送回客户端。Marco Rousonelos(游戏邦注:Reflection Software的程序员)所设计的这一查询以及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

基于这一查询,我们可以生成如下结果集:

result set(from indieflashblog)

result set(from indieflashblog)

添加额外的玩家

对于那些支持2名以上玩家的游戏,我们将添加额外的内容到查询中:

#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

限制结果

我们必须注意的是这一查询将为所提供的会员ID送回所有游戏记录结果。当游戏变得更受欢迎且玩家开始获得游戏时,这将创造出非常大的数据集。为了保证服务器,网络以及用户设备不会超载,我们最后包含一个LIMIT声明,如此便只会送回结果的目标部分:

#CODE EXAMPLE 8: Limit Statement
Limit 0, 30

定制查询

查询将根据个性化的游戏进行调整与定制,并受在服务器请求中包装额外的参数所控制。例如,你可以储存一个“最低限制”属性和一个“limitSpan”属性去控制查询的限制。

因为可靠的查询能够送回必要的结果集,所以我们准备生成更有效的用户体验去呈现结果。

设计一个异步多人游戏UI

玩家的游戏列表是异步多人游戏体验的核心。列表是用于导航,检查状态,并在进程中加入游戏。此外,游戏列表还是一个很棒的排行/记录工具,能够用于回顾之前的战斗,敌人等更多内容!

Hero Mages Game List User Interface(from indieflashblog)

Hero Mages Game List User Interface(from indieflashblog)

相关的游戏记录信息

一个优秀的游戏列表将从优秀的游戏记录单元开始。每个游戏记录都应该包含如下信息:

何时是最后的回合或者游戏是何时结束

游戏诞生的时间

状态(玩家是否转向攻击,等待回合,防卫或获胜)

参与玩家的名字

这些属性将帮助玩家选择理想的游戏进行加载,我们也能够添加更多细节以及其它可能性:

独特的游戏记录ID

比赛是否进行了排名

地图的名称

游戏目标

游戏记录单元设计应该去适应列表的大小,从而让它们能够基于任何大小的手机设备而有效地呈现出来。《英雄法师》利用玩家形象以更有趣的视觉效果去呈现角色特征。

One versus one matches are displayed with larger avatars for a more dramatic effect(from indieflashblog)

One versus one matches are displayed with larger avatars for a more dramatic effect(from indieflashblog)

Team games use colored bars to separate the players grouped on the same team.(from indieflashblog)

Team games use colored bars to separate the players grouped on the same team.(from indieflashblog)

填充列表

一旦设计出游戏记录单元布局,游戏记录类的实例便能够被添加到一个列表组件中,即玩家可以用于访问他们的游戏回合。添加记录到列表的过程与添加道具到我们的基本UI列表非常相似,除了不是添加简单的单元外—-我们添加的是自己特别设计的单元。

《英雄法师》利用了AURA的多屏幕组件UI。AURA可以替代ActionScript 3.0的动画,使用程序和资源。这是我为管理监听器,资源和设计UI等任务的速度所编写的一个类和组件程序库。截图中的列表是适应用户设备的屏幕大小与输入控制的高级组件。如果你是在触屏手机上玩游戏,该列表将通过碰触进行操作。同样的列表如果运行于台式机上则能通过标准的滚动条进行导航。列表也能够根据手机GPU进行优化,并在像第一代iPad等设备上基于60帧/秒渲染单元。

加载游戏

游戏列表的主要功能是让玩家通过在列表上选择一个条款而加载已保存的游戏回合。与保存游戏的过程类似,加载游戏也要求使用客户端和服务端代码去创造一个加载游戏请求,从数据库中检索游戏状态,并打开游戏引擎去保存预想的游戏。再一次,我们要遵循4个交流步骤:

1.游戏客户端:从服务器中请求数据

2.服务器:处理请求,发送客户端回应

3.游戏客户端:接收服务器回应

4.游戏客户端:基于数据执行预想的任务

注:尽管在游戏列表查询中收集游戏状态数据是可能的,我还是建议你们针对个性化的游戏记录使用一个单独的服务器请求,如此便能够节省带宽。

第一步:请求加载游戏

游戏列表中的每个单元将使用如下代码为预想的游戏记录请求服务器:

//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”);
}
}

第二步:处理加载游戏请求

在服务器端,我们为“加载游戏”请求添加一个条件。当《英雄法师》能够通过使用Smart Fox进行同步游戏时,为什么不将异步游戏转变成实时在线比赛?房间循环在服务器上的房间列表上迭代着,并检查任何房间的游戏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;
}
}

第三步:收到加载游戏回应

回到客户端,我们的Smart Fox Server扩展回应监听器需要一个新条件去听取来自服务器的“加载游戏”回应。基于回应类型,我们将加入当先的实时游戏或基于返回参数创造新游戏:

//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….
}

第四步:加载游戏状态

不同游戏引擎的游戏加载函数都不同,而以下是函数需要处理的一些任务:

1.将返回的游戏状态数据串带回一个目标上

2.设置两个不同的标志——“isRunningCommandList”=true而“useAnimations”=false

3.通过引擎运行命令列表而在背景中有效地“玩游戏”直至最后的命令出现。你的引擎代码将检查isRunningCommandList标志以确保任何对于命令的自动回应都不会被忽视—-前提是它们已经包含在命令列表中。

重新播放动画

遵循上述步骤将让你能够从游戏列表中加载任何游戏并恢复游戏状态,让其能够匹配最后的记录。需要考虑的是,当其他玩家离开时,异步多人游戏敌人将会改变游戏状态。简单地加载当前游戏状态将会让玩家感到困惑,因为他们将不知道敌人执行了怎样的命令。对于有效第一步多人游戏体验,我们需要对命令记录过程和游戏加载代码做出一些改变。

记录最后的行动见证

最后的行动见证必须面对不同玩家进行单独记录,所以我们需要添加一个额外的属性到hm_gameresults表格中。这一属性是用于储存最后命令的索引。

当发送新的游戏命令时,只要跟随命令传递命令记录索引便可。之后,在我们的代码块中为了处理游戏状态更新,我们将在错误回应下方添加如下代码:

//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;
}
}

保存了最后的命令索引,播放适当的动画的窍门是在到达lastCmd索引时恢复useAnimations标志。

其它注意事项

在我所解释的步骤外还有许多方法能够定制异步游戏体验并添加额外的功能与函数。举个例子来说吧,如果玩家拥有无限的时间去回应一款异步游戏的话会怎样?缺少运动员精神的失败玩家将会无限期地延缓下一个回合,从而阻止获胜的玩家宣布自己的胜利。解决这一问题的一种方法便是创造“maxWait”时期,让玩家可以在敌人太久未继续下一个回合时略过他们。在《英雄法师》中,我让玩家可以在敌人3天内未继续下一个回合时将其舍弃。

接下来

本篇文章解释了创造一个异步多人游戏列表用户界面,加载已储存的游戏回合,播放敌人最后的行动动画等过程。而下一篇文章将专注于异步多人游戏的匹配系统理念,从而让玩家可以无需实时网络连接而开始新的游戏或加入现有的游戏。

本文为游戏邦/gamerboom.com编译,拒绝任何不保留版权的转载,如需转载请联系:游戏邦

How to Create an Asynchronous Multiplayer Game Part 3: Loading Games from the Database

by Ross Przybylski

Asynchronous multiplayer games are awesome because players can enjoy social gaming experiences without having to commit to long sit-down sessions. This convenient style of play is made possible by online servers that deliver updates on games being played directly to a player’s device. This article will explain how games previously saved to a database are loaded by the server and made accessible within the game client’s user interface.

Hero Mages Asynchronous Multiplayer Preview

This article will explain:

1.How to query a list of game records stored in a MySQL database and send the results set to the game client
2.How to interpret the query results on the client side and design a meaningful games list user interface the player can use to resume playing
3.How to recreate a live and synchronous multiplayer experience from an async game
4.How to replay animations to represent the moves made by the player’s opponent while they were away
Requirements

Prerequisite knowledge

Experience developing turn based games

Familiarity with ActionScript 3.0

Has Read “Part 2: Saving the Game State to Online Database”

Required products

Flash Professional (Download trial)

Smart Fox Server Pro (Download trial)

MySQL Database

User level

Advanced

Generating the Player’s Games List

Popular async games like Hero Academy utilize a “games list” user interface that allow players to access and resume playing their asynchronous game sessions.

Multiplayer game list in Hero Academy

When building my games list for Hero Mages, I started by creating a new user interface screen class called “HM_GamesList”. I wanted to focus on the data and code components first, so the initial design is limited to a header and a scrollable list component that will be used to populate information retrieved from the server:

A basic game list UI

This interface is populated by a database query that gets a list of the active player’s game records. All of the MySQL query generation takes place on the server side, and the process for communicating with our online server looks like this:

1.Game Client: Request data from server

2.Server: Handle request, send client response

3.Game Client: Receive server response

4.Game Client: Perform desired task with data

Step 1: Request Game List

The game client requests the game list from the server:

Step 2: Handle Game List Request

The server side code handles the request and sends the client back a response. In Part 2: Saving the Game State to Online Database, I explain how games are saved to a MySQL database using two tables, hm_games and hm_gameresults. The function loadGameList will build a MySQL query that returns the relevant data we need to populate the game list.

Step 3: Receive Game List Response

The game client receives the server response.

Step 4: Populate Game List

The game client performs the desired task of populating the list:

And here’s our result:

Advanced Game List Query

While functional, the basic game list created above lacks key information needed for a solid user experience. Players need to know when the game was created, when the last turn was, whose turn it is, and most importantly who their opponents are for the game to be loaded.

Merging the Games and Games Results Tables

The ideal query needs to return all of the relevant information back to the client using the least resources and bandwidth as possible. This query, designed by Marco Rousonelos, programmer at Reflection Software, and laptop alias of the MySQL help forums uses ranking and derived tables to generate the desired results set:

With this query we can generate results sets that look like this:

Adding Additional Players

For games that support more than two players, we add additional lines to the query as follows

Ordering the Results

Ideally, we’d like to return the results set in the following order:

1.Game Status (display games still in progress first)

2.Turn (display games where it’s player’s turn first)

3.Time Last Update (display games last updated first)

We can achieve this by adding some sort order by statements to the query:

Limiting the Results

It’s important to note that this query returns ALL game record results for the provided member id. As the game becomes popular and player starts to accrue games, this can result in a very large set of data. To ensure the server, network, and the user’s device are not overloaded, it is best to include a LIMIT statement so that only a targeted portion of the results are returned:

Customizing the Query

Queries can be tweaked and customized as needed for inpidual games and controlled by packaging additional parameters in the server request. For example, you could store a “lowerLimit” property and a “limitSpan” property to control the limits of the query.

With a solid query capable of returning the necessary results set, we’re ready to generate a more effective user experience to display the results.

Designing an Asynchronous Multiplayer UI

The player’s game list is the core of the async multiplayer experience. The list is used to navigate, check the status of, and join games in progress. Additionally, the game list is a great leaderboard/logging tool that can be used as a means to review past battles, opponents, and more!

Hero Mages Game List User Interface

Relevant Game Record Information

A good game list starts with a good game record cell. Each game record should contain the following information for the player:

Time last turn was taken or that game was completed

Time game was created

Status (whether it’s players turn to attack, waiting for turn, defeat, or victor)

Names of players participating

These properties are helpful to the player in choosing the desired game to load. It’s always possible to add more details, and some other possibilities might include:

Unique game record id

Whether or not match is ranked

Name of the map

Game objective

Game record cells should ideally be designed to adapt to the size of the list so they can display appropriately on any size mobile device. Hero Mages makes use of player avatars to display character portraits for a more interesting visual display:

One versus one matches are displayed with larger avatars for a more dramatic effect

Team games use colored bars to separate the players grouped on the same team.

Populating the List

Once the game record cell layout has been designed, instances of the game record class can be added to a list component that the player can use to access their game sessions. The process of adding records to the list is similar to adding items to our basic UI list above, except instead of adding simple cells, we’re adding our own custom designed cells.

Hero Mages leverages AURA multiscreen component UIs. AURA stands for Animations, Utilities, and Resources for ActionScript 3.0. It’s a library of classes and components I wrote to speed of tasks like managing listeners, resources, and designing UI. The list seen in the screenshot below is advanced component that adapts to the screen size and input controls of the user’s device. For example, if you’re playing on a touch-based input mobile device, the list is operated using swipes. The same list running on a desktop is navigated using a standard scroll bar. The list is also optimized for mobile GPU’s and can render cells at 60fps on devices like first generation iPads.

The implementation of the component list is beyond the scope of this article. I will be releasing an article that explains the mechanisms of the component that will include downloadable examples in the near future.

Loading a Game

The primary function of the games list is to allow the player to load saved game sessions by selecting an item in the list. Similar to the process of saving games, loading games will require both client-side and server-side code to make a load game request, retrieve game state from the database, and fire up the game engine to restore the desired game. Once again, we follow our 4 communication steps:

1.Game Client: Request data from server

2.Server: Handle request, send client response

3.Game Client: Receive server response

4.Game Client: Perform desired task with data

Note: While it would be possible to gather the game state data in the game list query, I recommend using a separate server request for the inpidual game record as outlined below in order to conserve bandwidth.

Step 1: Request Load Game

Each cell in our game list will use the following code to make the request to the server for the desired game record:

Step 2: Handle Load Game Request

On the server side, we add a condition for “Load Game” request. Since Hero Mages games can also be played synchronously using Smart Fox, why not transform an async game to a live online match if the players are both online? The room loop iterates through the list of rooms on servers and checks to see if any room’s game id matches the record the player is attempting to load asynchronously. If a match is found, the server returns the room id so they can connect immediately with live player. For live matches, players load game data directly from the game room host. If a live match is not found, the server loads the game state from the database.

NOTE: In order for this loop to work, it’s necessary to store the game record id as a room variable when creating a live game room. You do not need to do this if you’re only interested in asynchronous gameplay.

The load game function will handle the retrieval of the necessary information for loading the game.

Step 3: Receive Load Game Response

Back on the client side, our Smart Fox Server extension response listener needs a new condition to listen for “Load Game” response from the server. Depending on the response type, we’ll either join the existing live game or create a new game with the returned parameters:

Step 4: Load Game State

The load game function will be different for each game engine, but here are some tasks the function will need to handle:

1.Convert the returned game state data string back into an object (See Part 2)

2.Set two distinct flags “isRunningCommandList” = true and “useAnimations” = false

3.Run the list of commands through the engine to effectively “play the game” in the background up to the last command. Your engine code should check the isRunningCommandList flag to ensure any automatic responses to commands (such as counter attack actions) are not fired if they are already included in the command list.

Replaying Animations

Following the above steps will allow you to load any game from your games list and restore the game state so that it matches the last recorded move. Consider, however, that an asynchronous multiplayer opponent will make changes to the game state while the other player is away. Simply loading the current game state will be confusing to the player because they won’t know what commands were carried out by their opponent. For an effective asynchronous multiplayer experience, we need to make some modifications to the command recording process and the game loading code.

Recording the Last Move Witnessed

The last move witnessed must be recorded separately for each player, so we need to add an additional property to the table hm_gameresults first created in Part 2 called “lastCmd”. This property is an integer value designed to store the index of the last command this player witnessed for the game.

When sending new game commands, simply pass the index of the command log along with the command. Then, in our code block for handling updates to the game state (created in Part 2) we’re going to add the following code just below the error response:

Show Animations for Unseen Moves

In our load game handler on the server, we’ll add an additional item to the response to store the player’s lastCmd witnessed as follows:

Here’s what the complete load game response looks like (combining Step 2: Handle Game Load Request with our new code)

With the last command index stored, the trick to playing the appropriate animations is to reactivate the useAnimations flag once the lastCmd index is reached.

Other Considerations

Going beyond the steps I’ve explained, there are many ways to customize the asynchronous experience and add additional features and functionality. For instance, what might happen if a player is given unlimited time to respond to an asynchronous game? A losing player with bad sportsmanship might decide to suspend taking their turn indefinitely, preventing the winning player from claiming victory. One way to solve this problem is to enable a “maxWait” period that allows players to drop opponents if they haven’t taken their turn in so much time. For Hero Mages, I allow players to drop their opponents if they haven’t taken their turn in 3 days.

Other features that might be helpful would be search filters for displaying only active games, finding games against particular opponents, the ability to look up the stats of your opponents, etc.

Coming Next

This article explained the process of building an asynchronous multiplayer game list user interface, loading stored game sessions, and replaying the opponent’s last move animations. The next article will focus on the aspect of the concept of asynchronous multiplayer match-making so that players can start new games or join existing ones without requiring a live online connection.(source:indieflashblog)


上一篇:

下一篇: