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

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

发布时间:2013-04-16 14:40:46 Tags:,,,

作者:Ross Przybylski

毫无疑问,异步多人游戏玩法(也就是允许玩家几天登录一次游戏)是手机游戏开发的新趋势。许多热门的多人游戏都是异步的,比如《填字游戏》(一种每次移步一步的拼字游戏)和《你猜我画》(Zynga所收购的游戏)。(请点击此处阅读本文第23篇4篇

当我的跨平台多人游戏《英雄法师》在iOS上发布时,我以为人们会为它的玩法——允许使用PC或Android设备与朋友一起在线战斗,感到兴奋。让我惊讶的是,我收到的反馈中,绝大多数的意思是“这款游戏如果能够多名玩家不同时在线也能玩就太好了!”

正如大多数《英雄法师》的玩家所知道的,我并没有感到沮丧——我立即将下一个包括异步多人玩法的更新当作优先工作。我以前从来没有编写过“异步多人”的代码,所以我想我得从我一惯的做法入手:谷歌搜索。“如何编写异步多人游戏玩法的代码”的搜索结果并不实用:我发现有不少关于“异步玩法多么了不起”和“了解大量支持异步玩法的游戏”的文章,但它们并没有讲到“如何制作”的点子上。因此我想到“这对游戏开发者来说应该是很重要的资源”,所以我决定记录我制作《英雄法师》的异步多人玩法的过程,并发表出来,这样我们都能从我所希望写成的“如何制作”系列文章中获益。

《英雄法师》是用Adobe Flash制作的,所以我的程序代码案例是ActionScript 3格式的。但是,异步多人的设计和机制适用于任何开发语言。

向大师学习

学习如何编程的最好办法就是,研究成功地实现你想要的结果的应用。我的目标是在我的幻想风格、回合制、策略游戏中实现异步多人玩法。苹果应用商店里正好有一款类似的游戏,并且它的异步玩法做得非常棒。它就是Robot Entertainment的《英雄学院》。所以我花了一些时间玩这款游戏(这是最好的研究方法)。我发现,这款游戏将社交媒体如Facebook和Twitter,与有效的异步多人UI相结合,很好地解决了“孤立社区”的难题——这也是我自己的“在线即时”多人游戏遇到的困境。

Hero-Academy-Gameplay(from indieflashblog)

Hero-Academy-Gameplay(from indieflashblog)

Hero-Academy-Game-List(from indieflashblog)

Hero-Academy-Game-List(from indieflashblog)

以下是《英雄学院》的异步多人玩法的概述:

1、启动游戏时自动登录服务器

2、通过Facebook和Twitter邀请/挑战玩家

3、创建新游戏的选项或寻找随机对手的选项

A、如果玩家创建新游戏,则新游戏将被添加到内部游戏列表中,等待其他玩家加入。

B、如果玩家选择加入游戏,则玩家将加入在内部游戏列表中显示的随机游戏。加入的玩家得到第一回合。

4、玩家在自己的回合中,在提交命令以前,可以执行全部的5次移动或取消所有移动。

5、一旦回合提交,游戏的数据库将更新游戏状态,并“推送”一个提示给对手,告诉该玩家轮到他的回合。

6、对手有24个小时可完成他的回合,否则玩家可以宣布该回合失效。

7、玩家可以选择在这个游戏中轮流,或返回个人“游戏列表”中加载任何已经玩过的游戏。游戏以对手、创建数据、最后一次移动时间和状态(即胜利、失败、等待回合或就绪)为标签。

《英雄法师》的特殊要求

对《英雄学院》的研究使我深入了解了异步多人玩法,但《英雄法师》因为其特殊的游戏机制,还必须考虑到其他问题:

1、《英雄学院》具有取消功能,对异步玩法来说是非常棒的,因为玩家在提交最终选择以前,可以实验不同的移动组合。《英雄学院》能够这么设定,是因为游戏中的所有伤害量是固定的。而《英雄法师》要根据骰子数计算伤害,所以撤消的选项就不可行了,因为它会影响游戏的关键机制:运气。

2、《英雄学院》不支持即时多人玩法。所有移动和甚至玩家聊天都是通过数据库更新来记录游戏状态的。虽然有可能和其他人在同一个房间内玩《英雄学院》,但这种体验并不理想,因为你必须等待推送提示你对手完成他的命令。对于《英雄法师》,这个系统的改进办法是,当双方玩家均在线时,能够保存了“即时”游戏链接——这样你就能实时看到你的对手的移动和交流信息。

如何制作异步多人玩法

了解了《英雄学院》的UI,以及认真考虑《英雄法师》的特殊要求后,我想到以下执行异步多人玩法的必要步骤:

1、想办法将游戏状态保存到在线数据库中。

2、编写一个数据库查询,用来加载玩家的游戏列表,同时通过点击列表上的项目,使玩家载入和恢复游戏。

3、在载入游戏时,点击可查看该游戏是否可以“即时”玩。如果可以,则加入该游戏,并与目前在线的玩家关联。如果不可以,则创建一个“即时”游戏房间,并载入该游戏。

4、想办法回放任何自玩家上一次登录后没有“看到”的动画。

5、在实际游戏中,使数据库中已保存的游戏状态更新玩家的命令(这与《英雄学院》是不同的,因为《英雄学院》要求你等到对方按下回合结束键,数据库才更新)。通过编写持续的、更小的更新,可以节省带宽,而且可以很自然地从异步过渡到即时玩法。

6、想办法将即时玩法元素(回合计时器、掉落计时器、游戏持续时间表、AI变化控制器)过渡到异步玩法(游戏邦注:这是《英雄法师》的特殊要求,不适用于其他异步游戏)。

7、设计一个UI,用于浏览和加入异步多人游戏。

8、制作一个匹配系统,允许玩家选择军队参数、对手类型等,还可以将玩家与数据库中的可用对手相匹配。

9、当游戏回合结束时,通过邮件或设备推送提示发送回合开始的信息给下一个对手。

game List Layout(from indieflashblog)

game List Layout(from indieflashblog)

异步多人游戏允许两个或以上的玩家参与游戏,不需要同时登录。支持异步玩法的关键是,将游戏状态保存到在线数据库中,这样你和你的对手才能在自己的回合时重新取回游戏。本文将解释我如何实现游戏状态储存和重新载入,并且提供实用的代码案例,希望在你为游戏设计相同的玩法时能派上用场。

本文将介绍:

1、如何通过简单的2D网格表现法和基于该表现法的命令记录表现游戏状态。

2、如何使用Smart Fox Server Pro的服务器端扩展将游戏数据写入在线数据库。

要求

前提:

开发回合制游戏的经验

熟悉ActionScript 3.0

知道如何设置MySQL数据库

知道如何编写Smart Fox服务器扩展

必需产品:

Flash Professional

Smart Fox Server Pro

MySQL Database

用户水平:

中级到高级

将游戏状态表现为数据

游戏状态是由所有定义游戏面板当前状态的元素组成的:游戏面板的布局、游戏部件的位置、游戏中的所有角色的当前属性和作用、各玩家手中的卡片,以及(如果对游戏很重要)产生游戏当前状态的一系列移动。如何根据这些因素的复杂度良好地展示游戏数据。在《英雄法师》中,我使用了两种办法——简单的2D网格表现法和基于表现法的命令记录。

简单的2D网格表现法

可以使用用二维数组表现游戏部件在游戏面板网格上的位置。例如,一个简单三连棋游戏可以表现如下:

//CODE EXAMPLE 1: TIC TAC TOE REPRESENTED AS 2D ARRAY
var ticTacToeGameState:Array = [];
ticTacToeGameState[0] = [X, O, X];
ticTacToeGameState[1] = [X, X, O];
ticTacToeGameState[2] = [O, X, O];

这个基于表现法的数据在程序代码的情况下是足够的,但对于异步游戏,必须使用平面数据结构将这个表现法保存到在线数据库中。当表现游戏状态时,为了节约带宽和服务器空间,最好使用尽可能少的信息。

假设我们知道三连棋总是3×3,那么这个游戏可以使用平面字符串表示如下:

//CODE EXAMPLE 2: TIC TAC TOE REPRESENTED AS FLAT STRING
var ticTacToeGameState:String = “XOXXXOOXO”;

在取回游戏状态数据时,我们可以再次建立如上所示的二维数组:

//CODE EXAMPLE 3: CONVERT 2D GAME STRING TO 2D ARRAY
var ticTacToeGameGrid:Array = [];
for(var i:int = 0; i < 3; i++){
var gridRow:Array = [];
for(var j:int = 0; j < 3; j++){
gridRow.push(ticTacToeGameState.charAt(i+j));
}
ticTacToeGameGrid.push(gridRow);
}

《英雄法师》使用这个简单的2D网格表现法将地图布局保存成一系列X和O。X表示墙,O表示开放空间,起始位置是一系列表示玩家和特殊单位类型放置区域的数值组合。

基于表现法的命令日志

如上所示,拉成一条单行文本串的二组数组可以用来表现许多基于网格的游戏。那种根据特定游戏活动发生时间的游戏很适合用基于现表法的命令日志来表示。

基于表现法的命令日志的好处

命令日志表现法的作用是,使游戏引擎通过提供产生当前状态的游戏命令列表,重制保存好的游戏状态。命令被储存成简化符号,以节省文件空间和带宽。当接收命令日志时,动画将不可播放,这样游戏就可以立即重制了。

使用命令日志重制游戏确保所有必要的游戏细节:游戏卡片、面板部件和这些部件的状态(游戏邦注:准确地表现为完全相同的样式,这个样式产生最初的游戏状态)。另外,命令日志显示了完整的游戏历史。对于异步游戏来说,这是极其有益的,因为玩家可以回顾活动列表,然后想起他们的当前状态是如何产生的,从而制定相应的策略。玩家还可以恢复自己没有机会看到的活动。

缩略的游戏符号

编写有效的基于表现法的命令日志,困难的地方在于设计一种既简单又准确的符号形式。为命令格式定义一系列预期标准也是很重要的。以下是我为《英雄法师》制作的句法:

1、各个独立命令放在“<c>command</c>”内

2、命令内容由“|”分开,内容分配由“=”表示

3、所有命令都包含定义命令类型(“cT”)的内容。不同的命令类型来自相应函数名称的缩写符号。例如,指示单位执行某活动的命令可以表示为“cT=uA”。

4、复杂的数据结构如单位和活动由特殊的数值id表示。

A、根据单位被添加到游戏面板的顺序分配id。

B、根据咒语在面板数组的索引来分配id。

C、根据能力在单位能力数组的索引来分配id。

5、命令的目标用逗号隔开;网格坐标(X和Y)用冒号隔开。

通过遵守严格的符号和用数值id引用复杂的实例对象,游戏命令可以用来表示简单的字符串,如下所示:

//CODE EXAMPLE 4: OBJECT TO STRING FUNCTION
function objectToString(object:Object, separator:String, valAssignment:String):String{
var string:String = “”;
for (var prop:* in object){
string += prop + valAssignment + object[prop] + separator;
}
string = string.substr(0, string.length – separator.length);
return string;
}

如果你的对象包含嵌套的数组,你就必须首先使用特殊分离器和分配字符将那些数组编码成字符串:

//CODE EXAMPLE 5: CONVERT ARRAY TO STRING
var myArray:Array = [4, 5, 6, 7];
var myStringArray:String = myArray.toString();

以下是《英雄法师》中的完整游戏命令:

//CODE EXAMPLE 6: GENERATE GAME COMMAND
function generateUseActionCommandString():String{
//Create a new object to store command properties
var HM_UseAction:Object = new Object();

//Store the command type: “uA” represents “useAction”
HM_UseAction.cT = “uA”;

//The abilityUser is a complex, custom datatype
//So, we store the id of the unit using ability
HM_UseAction.uId = abilityUser.unitId;

//The unit’s ability is also a complex, custom datatype
//So, we store the id that represents its index in the abilities array
HM_UseAction.i = abilityIndex;

//pT represents the primary targets
//In actual game, a function discerns between target types (units, spaces)
//Here, we simply convert the array of choices to a comma deliniated string
HM_UseAction.pT = primaryTargetsToActOn.toString();

//Encapsulate the command within c-tags
var strCmd:String = “<c>”+objectToString(HM_UseAction, “|”, “=”)+”</c>”;

//A preview of the assembled command
trace(strCmd) //<c>cT=uA|uId=1|i=1|pT=4,5,6″</c>

//Return the command
return strCmd;
}

作为参考,下列函数可以用来将字符串转换回对象:

//CODE EXAMPLE 7: STRING TO OBJECT FUNCTION
function stringToObject(string:String, separator:String, valAssigment:String):Object{
var object:Object = new Object();
var props:Array = string.split(separator);
for(var i:int = 0; i < props.length; i++){
var vals:Array = props[i].split(valAssigment);
object[vals[0]] = vals[1];
}
return object;
}

将游戏写入数据库

如果游戏应用可以用简单的文本文件表现它的保存状态,那么下一步就是将游戏的保存数据写入在线数据库,这样其他玩家就可以随时恢复游戏状态。为此,你需要一个带MySQL、SQL或其他形式的数据库的网上服务器,以及一个网上服务或服务器来与数据库交流、运行必要的查询,和发送/接收来自应用的数据。

《英雄法师》使用Smart Fox服务器完成实时多人连接(在线聊天和异步玩法),所以我使用服务器端代码来处理与数据库的交流活动。《英雄法师》的数据储存在MySQL数据库中,我已经通过我的主机供应商GoDaddy.com提前做好这个数据库了。我喜欢使用SmartFox服务器,是因为我可以通过ActionScript 1.0直接使用MySQL,而不必担心不懂PHP或其他服务器语言的问题。

定义储存游戏数据的表格

定义将用来把游戏信息保存在数据库中的表格也非常重要。为此,《英雄法师》使用两套表格:

表格“hm_games”用来保存所有相关的游戏数据。“cmdLog”栏保存游戏引擎将用于重建游戏状态的符号命令的实际列表。

hm_games
Field Type Notes
ID_GAME int(10) Stores the unique game record id
ranked tinyint(4) Indicates whether or not game is counted for rank
timeRecorded int(10) The epoch time in seconds game was last updated
version varchar(16) Indicates client version game was created with
cmdLog TEXT The game state represented via command log
status tinyint(4) Indicates whether the game is in progress or complete
timeCreated int(10) The epoch time in seconds game was created
whoseTurn mediumint(8) The user id of the player whose turn is active
timeLastTurn int(10) The epoch time in seconds last turn was completed
isAsync tinyint(4) Indicates whether or not game is async or synced

表格“hm_gameresults”用来保存与游戏相关的玩家特定信息。某一游戏的所有玩家都通过ID_GAME与hm_games表格关联起来。这个表格保存结果(无论玩家是胜利还是失败)、排名变化(如果游戏有排名的话),同时还要进一步更新,以帮助决定再次加入游戏的玩家必须看到的动画。

hm_gameresults
Field Type Notes
ID_MEMBER mediumint(8) The unique id of player participating in this game
ID_GAME int(10) The unique id of the game record for this result
result tinyint(4) The outcome of the game for this player (win/loss)
ratingChange tinyint(4) The change in players rating for ranked games


创建新游戏记录

《英雄法师》具有异步多人玩法,但我还没有给异步匹配系统做过UI。然而,令人兴奋的游戏创建屏幕非常适合用来解释如何将新游戏记录保存到数据库。

游戏客户端和在线服务之间的基本通信运作如下:

1、如果游戏主机已配置所有游戏选项,并且玩家觉得满意,则他们会按下“Start Game”。

2、游戏客户端将游戏背景格式化为符号游戏命令,发送命令给服务器,并等待回应。

3、服务器端脚本接收命令并创建游戏记录,执行一个MySQL声明,以便在hm_gameresults表格中创建新入口,和在hm_gameresults表格中为各名玩家创建新记录。

4、如果服务器的数据库运行完全顺利,则服务器对客户端作出回应,反馈新创建的游戏记录的ID_GAME。如果服务器运行失败,客户端接收到新游戏无法创建的反馈。

5、如果客户端接收ID_GAME,游戏主机就用这个内容重新装配命令符号,并发送开始游戏命令给所有玩家。如果收到“操作失败”,则游戏客户端将显示错误信息。

注:如果你需要学习如何给Smart Fox Server Pro编写服务器端扩展,请参考其他网上教程。

当符号化游戏命令装配完毕,就使用以下函数发送命令给服务器端扩展:

//CODE EXAMPLE 8: SEND CREATE GAME COMMAND
private function sendStartGameCommand(HM_GameVars:Object):void{
//Store a reference to the created game settings object for use later
gameVarObj = HM_GameVars;

//Check to see that smartFox is connected and that there are at least 2 players
if(smartFox.isConnected == true && playerSettingsList.length > 1){
//Send the command to create new game record to Smart Fox Server extension
//Commands can be sent as string or xml; normally, I use string for speed
//In this case, I use xml to save the work of encoding to string
smartFox.sendXtMessage(“HMServer”, “CreateGameRecord”, HM_GameVars, “xml”);
}
else{
//If this is a practice game with only 1 player, no need to store to database
//Fire game up immediately
fireUpGameWithRecordID(-1);
}
}

在服务器端,我给“Create Game”命令添加了新条件,用来在数据库中插入新游戏记录:

//CODE EXAMPLE 9: CREATE GAME RECORD IN DATABASE
function handleRequest(cmd, params, user, fromRoom, protocol){
if(protocol == “xml”){
//….CODE FOR OTHER EXTENSION COMMANDS OMITTED….

else if(cmd == “CreateGameRecord”){
//EXTRACT THE PLAYER INFORMATION AND TURN ORDER FROM THE RECEIVED COMMAND
var players = String(params.pS).split(“,”);
var randomTurnOrders = params.rTO.split(“,”);

//GENERATE THE MYSQL STATEMENT TO ADD NEW GAME RECORD BASED ON GAME SETTINGS
var gameRecordSQL = “INSERT into hm_games (ranked, timeCreated, version, status, timeRecorded, whoseTurn, cmdLog) VALUES (”
gameRecordSQL += “‘” + params.r + “‘, “                               //ranked
gameRecordSQL += “‘” + Math.floor(getTimer() / 1000) + “‘, “     //timeCreated
gameRecordSQL += “‘” + params.v + “‘, “                               //version
gameRecordSQL += “‘” + 1 + “‘, “                                      //status
gameRecordSQL += “‘” + Math.floor(getTimer() / 1000) + “‘, “     //timeRecorded
gameRecordSQL += “‘” + stringToObject(players[randomTurnOrders[0]], “;”, “:”).hmId + “‘, “          //whoseTurn
gameRecordSQL += “‘” + “<c>” + objectToString(params, “|”, “=”) + “</c>” + “‘”  //cmdLog
gameRecordSQL += “)”;

//EXECUTE MYSQL COMMAND AND CHECK IF IT WAS SUCCESSFUL
success = dbase.executeCommand(gameRecordSQL);
if(success == false){
//IF THIS FAILS, WE NEED TO REPORT BACK AN ERROR TO CLIENT
trace(“UNABLE TO CREATE GAME RECORD”);
response.error = “Unable to create new game record in database”;
}
else{
//ONCE GAME RECORD IS ADDED, GRAB ITS ID (WE KNOW ITS THE LAST INSERTED RECORD)
sql = “SELECT LAST_INSERT_ID()”
var queryRes = dbase.executeQuery(sql);
var dataRow = queryRes.get(0);

//STORE THE GAME RECORD ID IN OUR RESPONSE OBJECT
response.id = dataRow.getItem(“LAST_INSERT_ID()”);

//CREATE A STATEMENT TO INSERT A NEW RECORD IN GAME RESULTS TABLE FOR EACH PLAYER
var gameResultsSQL = “INSERT into hm_gameresults (ID_GAME, ID_MEMBER, result, ratingChange) VALUES “;
for(var i = 0; i < players.length; i++){
//CONVERT THE PLAYER OPTIONS FROM STRING TO OBJECT
//THIS IS SO WE CAN EXTRACT PROPERTIES LIKE PLAYER ID
var playerOptions = stringToObject(players[i], “;”, “:”)

gameResultsSQL += “(LAST_INSERT_ID(), ‘” + playerOptions.hmId + “‘, ‘” + “-2″ + “‘, ‘” + “0″ + “‘)”
if(i < players.length – 1){
gameResultsSQL += “, “;
}
}
success = dbase.executeCommand(gameResultsSQL);
if(success == false){
//IF THIS FAILS, WE NEED TO REPORT BACK AN ERROR TO CLIENT
trace(“UNABLE TO CREATE GAME RESULTS RECORD IN DATABASE”);
response.error = “Unable to create game results records in database”;
}
}
}

//….CODE FOR OTHER EXTENSION COMMANDS OMITTED….
}
}

返回客户端,我给来自服务器的“Create Game”命令的响应添加了新条件:

//CODE EXAMPLE 10: RECEIVE SERVER SIDE RESPONSE
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 == “CreateGameRecord”){
//INSTRUCT GAME OPTION SCREEN TO FIRE UP GAME RECORD
gameOptionsScreen.fireUpGameWithRecordID(dataObj.id);
}
//….CODE OMITTED….
}
}

这个调用游戏大厅的最后一个函数来发出开始游戏的命令:

//CODE EXAMPLE 11: FIRE UP GAME
public function fireUpGameWithRecordID(gameRecordId:int):void{
//Recall in previous step we stored gameVarObj for future use
//Here, we add the database id for the new game record
gameVarObj.gId = gameRecordId;

//Assemble the game command into abbreviated string notation
var HM_Command:String = Utils.objectToString(gameVarObj, “|”, “=”);

//Add the command to client side que
addToCommandQue(HM_Command);

//Send the command to start game to any live players
if(smartFox.isConnected == true){
smartFox.sendCmd(HM_Command);
}
}

更新游戏状态

当创建在在线数据库中的游戏记录和游戏客户端可以获得引用记录的ID时,游戏状态的改变就可以由附加的新游戏命令轻松记录到命令日志栏中。

在本文的前半部分,我想到游戏状态的更新应该根据游戏创建的方式来决定。在Robot Entertainment的《英雄学院》一例中,玩家在提交回合以前可以选择取消,更新游戏状态自然要在回合提交后发生。相反地,《英雄法师》允许玩家秘各个可用单位互动、施放咒语和发动攻击(根据骰子数决定伤害程度)。因为结果的随机性,《英雄法师》就不能使用取消功能了。因此,我决定,玩家每发送一次命令,游戏的命令日志就更新一次。

因为《英雄法师》也可以即时玩,所以我决定把我现在的服务器扩展(用于交换即时玩家之间的游戏命令)也更新了,使它也能处理储存在数据库中的游戏状态。这样,只需要让客户端发送一次命令给服务器,我就可以最有效地利用带宽。

以下是处理游戏状态更新的代码:

//CODE EXAMPLE 12: UPDATE GAME RECORD
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
}
}
_server.sendResponse([params[1]], -1, null, recipients, “str”);
return;
}
}

总结

本文介绍了制作一个异步多人游戏的最基本的步骤:将游戏状态表现为数据,并保存到在线数据库中。在下一篇文章中,我将分享如何从数据库中恢复游戏状态,以及如何使用Flash、ActionScript和Smart Fox服务器拓展在异步多人模式和即时在线模式之间无缝地转换。(本文为游戏邦/gamerboom.com编译,拒绝任何不保留版权的转载,如需转载请联系:游戏邦

How to Create an Asynchronous Multiplayer Game

by Ross Przybylski

There’s no doubting the fact that “asynchronous” multiplayer gameplay, or the ability to play games one turn per session over the course of several days, is the new hot trend in mobile gaming. Many of the top multiplayer games are asynchronous, including the popular Words with Friends (a one-move-at-a-time implementation of Scrabble) and Draw Something (a unique take on Pictionary that was recently bought by Zynga for 200 million).

When my cross-platform multiplayer game, Hero Mages, launched on iOS, I thought people would be excited by the ability to play live online battles with their friends playing on PC’s or Android devices. To my surprise, the overwhelming feedback I got was “this game would totally rock if only it had async multiplayer!”

As most Hero Mages players know, I’m not one to disappoint- so I immediately restructured my priorities for the next game update to include asynchronous multiplayer. Having never programmed “async multiplayer” before, I figured I’d start like I always do: with a Google search. The returns for ” how to program async multiplayer” weren’t very helpful: I found articles talking about how async is awesome and learned about a great selection of titles that support it- but the key “how to” part was strangely absent. It then occurred to me “This would be a really great resource for game developers”, so I’ve decided to chronicle my development of async multiplayer for Hero Mages here on IndieFlashBlog.com so that we can all benefit from what I hope will grow into a helpful “how to” series of articles.

Hero Mages is built using Adobe Flash, so my program code examples will be in ActionScript 3. The design and mechanics of async multiplayer, however, are applicable for any language.

Learn from the Masters

One of the best ways to learn how to program something new is to study an application that successfully achieves your desired result. My goal is to achieve asynchronous multiplayer gameplay for my fantasy themed turn-based tactical strategy game, Hero Mages. It just so happens there’s a similar game to Hero Mages on the App Store with a terrific implementation of async gameplay: Hero Academy by Robot Entertainment. So, I’ve spent some time the past couple weeks playing the game (what better way to do research) and I’ve got to give the developers serious props: their integration of social media such as Facebook and Twitter tied in with a very effective async multiplayer user interface does a very effective job at solving the “isolated community” dilemma one such as myself might experience in a “live online” multiplayer game.

Hero Academy Game Play

Multiplayer game list in Hero Academy

Here’s an overview of the async multiplayer in Hero Academy:

1.Automatic login to servers upon launching game

2.Invite/challenge players via Facebook and Twitter

3.Option to create a new game or find a random opponent

A.If player creates a new game, it gets added to an internal game list and waits for opponent to join

B.If player opts to join game, a random game on the internal game list is selected and the player is connected. The joining player gets the first turn.

4.Player can make up to 5 actions during their turn with the option to undo any or all of their choices before submitting their final commands.

5.Once turn is submitted, database record of game is updated with new game state and a “Push” notification is sent to the opponent, indicating to them that it’s now their turn

6.Opponent has 24 hours to complete their turn or the player can declare them as forfeit

7.Player has the option to take turns in their next active game or return to their personal “Game list” which allows them to load any played game. Games are labeled with opponent, creation date, last move time, and status: victory, defeat, waiting for turn, ready to attack

Special Considerations for Hero Mages

Experimenting with Hero Academy provided a lot of insight into user experience of async multiplayer, but Hero Mages will need some specific considerations given its unique play mechanics:

1.Hero Academy leverages an undo system which is great for async because players can experiment different move combinations before submitting their final choice. This works for Hero Academy because all damage is a fixed amount. Damage in Hero Mages, however, is calculated via dice rolls. Allowing an undo option isn’t feasible because it would affect a key play mechanic: chance.

2.Hero Academy does not support live multiplayer. All of the moves and even player chat are communicated via database updates to the recorded game state. While it’s possible to play a game of Hero Academy with someone in the same room, the experience isn’t optimal because you have to wait for push notifications to go through and re-watch the animations your opponent already made when he commanded his units. An improvement to the system that would work well for Hero Mages would be the ability to restore a “live” game connection when both players are online- allowing you to see your opponents moves and communications being made in real-time.

How-to Create Async Multiplayer Development Outline

After  studying the user interface of Hero  Academy and carefully considering the special needs for Hero Mages, I came up with the following  steps that would be necessary to implement asynchronous multiplayer in my game:

1.Create  a way to save the game state to an online database

2.Write  a database query that loads a list of the player’s games and allows player to  load and restore a game session by clicking on an item in the list

3.Upon  loading a game session, check to see if the session is currently available for  “live” play. If so, join the session in progress to connect with  opponents currently online. If not, create a “live” game room and  load the session.

4.Devise  a way to replay any animations player did not “see” since their last  time playing the game session

5.During  actual game play, write updates to the stored game state in the database each  time player issues an order (this differs from Hero Academy where you would wait until the end turn button is pressed  to update the database). By writing continuous, smaller updates to database  less bandwidth is used and the async-to-live play transition can be seamlessly  achieved.

6.Find  a way to transition real-time gameplay elements (turn timer, drop timer, game  duration clock, AI turn controller) to async (unique challenge for Hero Mages, not applicable to all async  games)

7.Design  a user interface for viewing and joining async multiplayer games

8.Create  a match-making system that allows player to pick their army preferences,  preferred opponent type, etc. and matches player to games with available  opponents in database

9.When  a game turn is complete, send a notification to next opponent via email or  device push notification indicating that it is now their turn to play

Asynchronous multiplayer games allow two or more people to play together without the need to be participating in the session at the same time. A key component of supporting asynchronous gameplay is saving the game state to an online database so that it can be retrieved by you and your opponents when it’s time to take the next turn. This article will explain how I achieved game state storing and reloading for my cross-platform game, Hero Mages, and provide insights and code examples helpful for doing the same with your games.

This article will explain:

1.How to represent game state as data using simple 2D grid representation and command log based representation

2.How to write game data to an online database using server side extensions with Smart Fox Server Pro.

Requirements

Prerequisite knowledge

Experience developing turn based games

Familiarity with ActionScript 3.0

Knowledge of how to setup a MySQL database

Knowledge of how to write Smart Fox Server extensions

Required products

Flash Professional (Download trial)

Smart Fox Server Pro (Download trial)

MySQL Database

User level

Intermediate to Advanced

Representing the Game State as Data

The game state consists of everything that defines the current status of the game board: the layout of the game board, the positions of game pieces, the current attributes and effects of any characters in play, cards in each player’s hand, and (if it’s important to your game) the series of moves that led up to present state of the game. How to best represent a game as data varies based on the complexity of these factors. I will explain two methods I used for Hero Mages: simple 2D grid representation and command log based representation.

Simple 2D Grid Representation

A two-dimensional array can be used to represent the position of game pieces on a game board grid. For example, a simple game of tic-tac-toe might be represented as follows:

1//CODE EXAMPLE 1: TIC TAC TOE REPRESENTED AS 2D ARRAY

2var ticTacToeGameState:Array = [];

3ticTacToeGameState[0] = [X, O, X];

4ticTacToeGameState[1] = [X, X, O];

5ticTacToeGameState[2] = [O, X, O];

This array based representation words adequately enough in the context of program code, but for an async game, it’s necessary to store this representation to an online database using a flat data structure. It’s best to use as little information as possible when representing the game state in order to conserve bandwidth and server space.

Assuming we know our tic-tac-toe grid is always 3×3, this same game could be represented using a flat character string:

1//CODE EXAMPLE 2: TIC TAC TOE REPRESENTED AS FLAT STRING

2var ticTacToeGameState:String = “XOXXXOOXO”;

Upon retrieval of the game state data, we can recreate the two dimensional array shown above:

1//CODE EXAMPLE 3: CONVERT 2D GAME STRING TO 2D ARRAY
2var ticTacToeGameGrid:Array = [];
3for(var i:int = 0; i < 3; i++){
4     var gridRow:Array = [];
5     for(var j:int = 0; j < 3; j++){
6          gridRow.push(ticTacToeGameState.charAt(i+j));
7     }
8     ticTacToeGameGrid.push(gridRow);
9}

Hero Mages utilizes this simple 2D grid representation to store map layouts as a series of X’s and O’s. X’s represent walls, O’s represent open spaces, and start location are a series of numerical combinations representing player team and specific unit type placement areas.

Command Log Based Representation

Many grid-based games can be fully represented by a two dimensional array flattened out to a single line text string as explained above. Games that depend on when particular game moves take place will benefit from a command log based representation.

Benefits of Command Log Based Representation

The goal of a command log representation is to leverage the game engine to recreate a saved game state by providing the list of game commands that led up to the current state. Commands are stored as abbreviated notation to conserve file space and bandwidth. Upon receiving the command log, the game engine literally “plays” the game as instructed up to the last command. During this time, animations are disabled so that the game can be recreated instantly.

Using a command log to recreate a game ensures that all essential game details: the cards that were played, the pieces on board, and the status of those pieces are accurately represented in exactly the same fashion that led up to the game play state in the first place. Additionally, the command log shows a complete history of what has happened since the start of the game. This is extremely helpful for asynchronous games because the player can review the list of moves to recall what events led up to their current position and plan their strategy accordingly. It’s also possible to reanimate select moves that a player returning from to an async game did not have the opportunity to see (I’ll discuss this last part in a future article).

Abbreviated Game Notation

The challenge of writing an effective command log based representation of a game is devising a form of notation that is as descriptive as necessary and as concise as possible. It’s important to define a series of expected standards for the command format. Here’s a look at how I built the syntax for Hero Mages:

1.Each individual command is enclosed in bracketed c tags “<c>command</c>“

2.Command properties are separated by pipes “|” and property assignments are delineated by equal sign “=”

3.All commands contain a property that defines the command type called “cT”. The different command types are symbols derived from abbreviating the corresponding function name. For example, a command that instructed a unit to use an action would look like “cT=uA”

4.Complex data structures such as units and actions are represented with unique numerical ids.

A.Units are assigned ids based on the order they are added to the game board.

B.Spells are assigned ids based on their index in the deck array.

C.Abilities are assigned ids based on their index in the units’ abilities array.

5.Targets of a command are separated by commas and grid coordinates (x and y) are separated by semi-colons.

By following a strict notation and referencing complex, instanced objects with numerical ids, game commands can be represented as simple strings using the following function:

1//CODE EXAMPLE 4: OBJECT TO STRING FUNCTION
2function objectToString(object:Object, separator:String, valAssignment:String):String{
3     var string:String = “”;
4     for (var prop:* in object){
5          string += prop + valAssignment + object[prop] + separator;
6     }
7     string = string.substr(0, string.length – separator.length);
8     return string;
9}

If your object contains nested arrays, you will need to first encode those arrays as strings with unique separator and assignment characters:

1//CODE EXAMPLE 5: CONVERT ARRAY TO STRING
2var myArray:Array = [4, 5, 6, 7];
3var myStringArray:String = myArray.toString();

And here’s what the assembly of a complete game command looks like in Hero Mages:

1//CODE EXAMPLE 6: GENERATE GAME COMMAND
2function generateUseActionCommandString():String{
3     //Create a new object to store command properties
4     var HM_UseAction:Object = new Object();
5
6     //Store the command type: “uA” represents “useAction”
7     HM_UseAction.cT = “uA”;
8
9     //The abilityUser is a complex, custom datatype
10     //So, we store the id of the unit using ability
11     HM_UseAction.uId = abilityUser.unitId;
12
13     //The unit’s ability is also a complex, custom datatype
14     //So, we store the id that represents its index in the abilities array
15     HM_UseAction.i = abilityIndex;
16
17     //pT represents the primary targets
18     //In actual game, a function discerns between target types (units, spaces)
19     //Here, we simply convert the array of choices to a comma deliniated string
20     HM_UseAction.pT = primaryTargetsToActOn.toString();
21
22     //Encapsulate the command within c-tags
23     var strCmd:String = “<c>”+objectToString(HM_UseAction, “|”, “=”)+”</c>”;
24
25     //A preview of the assembled command
26     trace(strCmd) //<c>cT=uA|uId=1|i=1|pT=4,5,6″</c>
27
28     //Return the command
29     return strCmd;
30}

For reference, the following function can be used to convert string commands back into objects:

1//CODE EXAMPLE 7: STRING TO OBJECT FUNCTION
2function stringToObject(string:String, separator:String, valAssigment:String):Object{
3     var object:Object = new Object();
4     var props:Array = string.split(separator);
5     for(var i:int = 0; i < props.length; i++){
6          var vals:Array = props[i].split(valAssigment);
7          object[vals[0]] = vals[1];
8     }
9     return object;
10}

Writing the Game to Database

Once the game application is capable of representing its save state as a simple text file, the next step is writing the game’s save data to an online database so it can be retrieved asynchronously by other players. In order to do this, you’ll need a web server with MySQL, SQL, or another form of database as well as a web service or server to communicate with the database, run necessary queries, and send/receive data to/from the application.

Hero Mages uses Smart Fox Server for real-time multiuser connectivity (online chat and synchronous gameplay) so I leverage my existing server-side code to handle communication with the database. Hero Mages data is stored in a MySQL database which I’ve previously setup using my hosting provider, GoDaddy.com. The reason I enjoy working with SmartFoxServer is that I can use MySQL directly with ActionScript 1.0 and not have to worry about knowing how to program in PHP or other server-side languages.

Defining Tables to Store Game Data

It’s important to start by defining tables that will be used to store the game information in the database. Hero Mages uses two sets of tables for this purpose:

The table “hm_games” is used to store all of the relevant game data. The “cmdLog” field will store the actual list of notated commands the game engine will use to rebuild the game state.

hm_games
Field    Type    Notes
ID_GAME    int(10)    Stores the unique game record id
ranked    tinyint(4)    Indicates whether or not game is counted for rank
timeRecorded    int(10)    The epoch time in seconds game was last updated
version    varchar(16)    Indicates client version game was created with
cmdLog    TEXT    The game state represented via command log
status    tinyint(4)    Indicates whether the game is in progress or complete
timeCreated    int(10)    The epoch time in seconds game was created
whoseTurn    mediumint(8)    The user id of the player whose turn is active
timeLastTurn    int(10)    The epoch time in seconds last turn was completed
isAsync    tinyint(4)    Indicates whether or not game is async or synced
The table “hm_gameresults” is used to store player-specific information related to the game. All of the players for a particular game are connected to the hm_games table via ID_GAME. This table stores the result (whether or not the player won or lost), rating change (if game is ranked), and will also be developed further later to help determine which animations the player needs to see when they rejoin the game.

hm_gameresults
Field    Type    Notes
ID_MEMBER    mediumint(8)    The unique id of player participating in this game
ID_GAME    int(10)    The unique id of the game record for this result
result    tinyint(4)    The outcome of the game for this player (win/loss)
ratingChange    tinyint(4)    The change in players rating for ranked games
Creating a New Game Record

Hero Mages was designed for synchronous multiplayer game play, and I haven’t yet developed a user interface design for an asynchronous match-making system. However, the existing game creation screen will work perfectly for the purposes of illustrating how to save a new game record to the database.

Game Creation Screen for Hero Mages

The basic communication flow between the game client and online server works like this:

1.Once game host has configured all game options to their satisfaction, they will press “Start Game”

2.The game client formats the game settings as a notated game command, sends this command to the server, and awaits a response

3.The server side script receives the command to create a new game record, executes a MySQL statement to create a new entry in the hm_gameresults table as well as new records in the hm_gameresults table for each player.

4.If the server’s database operations are completed successfully, the server returns a response to the client indicating the newly created game record’s ID_GAME. If the server operations fail, a response is returned to the client indicating that the new game could not be created.

5.If client receives ID_GAME, the game host repackages the command notation with this property and sends the start game command out to all users. If a “failed operation” is returned, game client shows user an error message.

Note: If you need assistance learning how to write a server-side extension for Smart Fox Server Pro, follow the excellent tutorials available at: http://www.smartfoxserver.com/docs/1x/

Once the notated game command is assembled, the following function is used to send the command to the server side extension:

1//CODE EXAMPLE 8: SEND CREATE GAME COMMAND
2private function sendStartGameCommand(HM_GameVars:Object):void{
3     //Store a reference to the created game settings object for use later
4     gameVarObj = HM_GameVars;
5
6     //Check to see that smartFox is connected and that there are at least 2 players
7     if(smartFox.isConnected == true && playerSettingsList.length > 1){
8          //Send the command to create new game record to Smart Fox Server extension
9          //Commands can be sent as string or xml; normally, I use string for speed
10          //In this case, I use xml to save the work of encoding to string
11          smartFox.sendXtMessage(“HMServer”, “CreateGameRecord”, HM_GameVars, “xml”);
12     }
13     else{
14          //If this is a practice game with only 1 player, no need to store to database
15          //Fire game up immediately
16          fireUpGameWithRecordID(-1);
17     }
8}

On the server side, I add a new condition for the “Create Game” command which handles the insertion of a new game record in the database:

//CODE EXAMPLE 9: CREATE GAME RECORD IN DATABASE
function handleRequest(cmd, params, user, fromRoom, protocol){
if(protocol == “xml”){
//….CODE FOR OTHER EXTENSION COMMANDS OMITTED….

else if(cmd == “CreateGameRecord”){
//EXTRACT THE PLAYER INFORMATION AND TURN ORDER FROM THE RECEIVED COMMAND
var players = String(params.pS).split(“,”);
var randomTurnOrders = params.rTO.split(“,”);

//GENERATE THE MYSQL STATEMENT TO ADD NEW GAME RECORD BASED ON GAME SETTINGS
var gameRecordSQL = “INSERT into hm_games (ranked, timeCreated, version, status, timeRecorded, whoseTurn, cmdLog) VALUES (”
gameRecordSQL += “‘” + params.r + “‘, “                               //ranked
gameRecordSQL += “‘” + Math.floor(getTimer() / 1000) + “‘, “     //timeCreated
gameRecordSQL += “‘” + params.v + “‘, “                               //version
gameRecordSQL += “‘” + 1 + “‘, “                                      //status
gameRecordSQL += “‘” + Math.floor(getTimer() / 1000) + “‘, “     //timeRecorded
gameRecordSQL += “‘” + stringToObject(players[randomTurnOrders[0]], “;”, “:”).hmId + “‘, “          //whoseTurn
gameRecordSQL += “‘” + “<c>” + objectToString(params, “|”, “=”) + “</c>” + “‘”  //cmdLog
gameRecordSQL += “)”;

//EXECUTE MYSQL COMMAND AND CHECK IF IT WAS SUCCESSFUL
success = dbase.executeCommand(gameRecordSQL);
if(success == false){
//IF THIS FAILS, WE NEED TO REPORT BACK AN ERROR TO CLIENT
trace(“UNABLE TO CREATE GAME RECORD”);
response.error = “Unable to create new game record in database”;
}
else{
//ONCE GAME RECORD IS ADDED, GRAB ITS ID (WE KNOW ITS THE LAST INSERTED RECORD)
sql = “SELECT LAST_INSERT_ID()”
var queryRes = dbase.executeQuery(sql);
var dataRow = queryRes.get(0);

//STORE THE GAME RECORD ID IN OUR RESPONSE OBJECT
response.id = dataRow.getItem(“LAST_INSERT_ID()”);

//CREATE A STATEMENT TO INSERT A NEW RECORD IN GAME RESULTS TABLE FOR EACH PLAYER
var gameResultsSQL = “INSERT into hm_gameresults (ID_GAME, ID_MEMBER, result, ratingChange) VALUES “;
for(var i = 0; i < players.length; i++){
//CONVERT THE PLAYER OPTIONS FROM STRING TO OBJECT
//THIS IS SO WE CAN EXTRACT PROPERTIES LIKE PLAYER ID
var playerOptions = stringToObject(players[i], “;”, “:”)

gameResultsSQL += “(LAST_INSERT_ID(), ‘” + playerOptions.hmId + “‘, ‘” + “-2″ + “‘, ‘” + “0″ + “‘)”
if(i < players.length – 1){
gameResultsSQL += “, “;
}
}
success = dbase.executeCommand(gameResultsSQL);
if(success == false){
//IF THIS FAILS, WE NEED TO REPORT BACK AN ERROR TO CLIENT
trace(“UNABLE TO CREATE GAME RESULTS RECORD IN DATABASE”);
response.error = “Unable to create game results records in database”;
}
}
}

//….CODE FOR OTHER EXTENSION COMMANDS OMITTED….
}
}
Back on the client side, I add a new condition for responding to the “Create Game” command sent from server:

//CODE EXAMPLE 10: RECEIVE SERVER SIDE RESPONSE
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 == “CreateGameRecord”){
//INSTRUCT GAME OPTION SCREEN TO FIRE UP GAME RECORD
gameOptionsScreen.fireUpGameWithRecordID(dataObj.id);
}
//….CODE OMITTED….
}
}
This calls the final function of the game lobby to send out the start game command

//CODE EXAMPLE 11: FIRE UP GAME
public function fireUpGameWithRecordID(gameRecordId:int):void{
//Recall in previous step we stored gameVarObj for future use
//Here, we add the database id for the new game record
gameVarObj.gId = gameRecordId;

//Assemble the game command into abbreviated string notation
var HM_Command:String = Utils.objectToString(gameVarObj, “|”, “=”);

//Add the command to client side que
addToCommandQue(HM_Command);

//Send the command to start game to any live players
if(smartFox.isConnected == true){
smartFox.sendCmd(HM_Command);
}
}
Updating the Game State

With the game record created in the online database and an ID referencing the record available to the game client, changes to the game state can be easily recorded by appending new game commands to the command log field.

In the first article of this series, I consider when updates to the game state should take place based on the style of game being created. In the case of Robot Entertainment’s Hero Academy, which grants players a 5-move turn with the ability to undo actions before submitting a turn, updates to the game state naturally take place at the conclusion of a turn. Hero Mages, by contrast, allows players to interact with each of their available units, cast spells, and make attacks that cause damage based on the results of dice rolls. Due to the random nature of outcomes, an undo feature isn’t plausible for Hero Mages. Therefore, I decided to update my game’s command log each time the player sends a command.

Because Hero Mages can also be played in real time, I decided it was best to upgrade my existing server extension (used for communicating game commands to live players) to also handle updates to the game state stored on the database. This way, I’d make the most efficient use of bandwidth by only having to have the client send the command to the server one time.

Here’s the block of code that handles the game state update:

Coming Next

This article has covered the first and most fundamental step of creating an asynchronous multiplayer game: representing your game state as data and storing that data to an online database. Next in the series I’ll share how to restore the game state from the database as well as how to seamlessly transition between async multiplayer mode and real-time online play using Flash, ActionScript, and Smart Fox Server extensions.(source:indieflashblog)


上一篇:

下一篇: