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

万字长文,以Hero Academy为例谈游戏中的异步互动设计

发布时间:2015-03-26 12:18:41 Tags:,

作者:Ross Przybylski

本文只是其中的一部分,未完待续。

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

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

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

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

向大师学习

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

Hero-Academy-Gameplay(from indieflashblog)

Hero-Academy-Gameplay(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_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服务器拓展在异步多人模式和即时在线模式之间无缝地转换。

异步游戏真的非常棒,因为玩家可以无需长期待在游戏中便能享受到有趣的游戏体验。是在线服务器成就了这种便捷的游戏风格,而本篇文章将着重解释如何通过服务器加载之前 所储存的数据,并将其用于游戏客户端的用户界面上。

本篇文章将解释:

1.如何查询储存于MySQL数据库中的一列游戏记录,并将结果传送给游戏客户端

2.如何在客户端上说明查询结果,并设计一个有意义的游戏列表用户界面而帮助我们更有效地游戏

3.如何通过一款异步游戏而再次创造出生动且同步的多人体验

4.如何重播动画以呈现出玩家对手的移动

要求

预备知识

基于游戏的开发体验

熟悉ActionScript 3.0

阅读过本系列文章的第二部分

产品要求

Flash Professional(试用版)

Smart Fox Server Pro(试用版)

MySQL数据库

用户级别

高级

生成玩家的游戏列表

大受欢迎的异步游戏,如《英雄法师》便使用了“游戏列表”用户界面,即让玩家能够进入并继续之前的异步游戏过程。

Hero-Academy-Game-List(from indieflashblog)
Hero-Academy-Game-List(from indieflashblog)

在创造《英雄法师》的游戏列表时,我最先创造了名为“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;
}

而以下便是我们的结果:

gameListLayoutBasic2(from indieflashblog)

gameListLayoutBasic2(from indieflashblog)

高级游戏列表查询

尽管具有功能性,但是上述所创造的基本游戏列表缺少了稳定用户体验所需要的关键信息。玩家需要知道游戏的创造时间,上一个回合是什么时候,是谁的回合,最重要的是游戏 将加载哪个对手。

整合游戏和游戏结果列表

最理想的查询需要使用最少的资源和带宽将所有相关信息传回客户端。由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

玩家的游戏列表是异步多人游戏体验的核心。该列表是用于导航,检查状态,并反映游戏的发展。除此之外,游戏列表也是一个非常棒的排行榜/记录工具,能够用于回顾过去的战 斗,敌人等等内容!

gameListUI(from indieflashblog)

gameListUI(from indieflashblog)

相关游戏记录信息

一个优秀的游戏列表是始于一个优秀的游戏记录。每一个游戏记录都应该包含如下信息:

最后一个回合或者完成游戏所需要的时间

游戏创造的时间

状态(不管是轮到玩家攻击,等待回合,防御,或获得胜利)

参与其中的玩家名字

这些属性能够有效地帮助玩家选择想要加载的游戏。除此之外我们也可以添加更多细节以及其它可能性:

独特的游戏记录ID

是否进行排名

地图的名称

游戏对象

最理想的情况便是设计能够匹配列表大小的游戏记录,从而让它们能够更有效地呈现在任何规格的手机设备上。《英雄法师》便是利用玩家形象去呈现角色肖像:

character portraits(from indieflashblog)

character portraits(from indieflashblog)

填充列表

在设计好游戏的记录单元格布局后,我们可以将游戏记录类别添加到列表组件中,从而确保玩家可以使用该内容去访问游戏过程。往列表中添加记录的过程与在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天内未回到之前的游戏回合中,那么对手玩家就可以选择与之解除关系。

使用过滤器只呈现出活跃的游戏,基于特定对手寻找游戏,以及查阅对手的状态等也是非常有帮助的功能。

总结

本篇文章主要解释了异步多人游戏用户界面的创建,已储存的游戏环节的加载,以及对手最后一次移动的动画重放。而在接下来的文章中我们将专注于异步多人游戏匹配系统的相 关理念,从而让玩家可以无需在线连接而开始玩一款新游戏或加入现有的游戏中。

多人游戏成功的关键在于拥有大量一起玩游戏的用户群。但如果一开始你并没有什么用户基础,又该如何集结大量玩家呢?移除“必须在线玩游戏”这个条件。异步匹配功能有助 于促进玩家参与多人游戏,即使他们并不在线!随机匹配方式则可让玩家遇到新人并结识新友,邀请系统则有助于玩家发展这些友情,并创造传播游戏的热情。本文将说明如何使 用游戏服务器和数据库来管理创造和加入多人游戏的过程。

本文将说明:

1.如何创造一个直观的多人模式界面

2.如何设置随机玩家的异步多人模式匹配

3.如何设计异步游戏邀请系统

4.如何设计好友管理系统,以便玩家邀请对方参与游戏(即使对方并不在线)

要求

*必备知识

开发回合制游戏的经验

熟悉ActionScript 3.0

*必需产品

Flash Professional

Smart Fox Server Pro

MySQL Database

*用户级别

高级

创造直观的多人体验

用户体验是创造异步多人游戏最重要的部分。无论你的游戏有多棒,如果它不能立即清晰地让玩家知道如何设置游戏,那就一定会流失用户。

在《Hero Mages》最初的即时多人模式部署中,用户可以登录和立即进入聊天大厅,并同其他玩家交流、创造游戏,访问系统中的其他模块(例如积分排行榜和地图编辑器)。该 系统模拟了类似《暗黑破坏神》、《星际争霸》等游戏的原版Battle.net的多人门户设计。硬核游戏玩家通常很熟悉这种设置,但许多休闲玩家却会对此不知所措。

学习案例

确定游戏界面如何运行,以及设计有效而直观的布局确实是个大挑战。最有效的方法之一就是探索其他游戏如何处理这个问题,然后在原先的部署基础上再进行优化和提升。

《Hero Academy》是一款拥有直观用户界面的成功异步多人游戏。多人模式会直接启动玩家的游戏列表,以便他们开始新的配对或者继续玩他们已经开始的游戏。为了联系其他玩 家,你的游戏屏幕上要有三个选项:一个是通过搜索邀请玩家,一个是找到随机对手,一个是通过Facebook邀请好友。《Hero Academy》的社交媒体绑定是一个更便于邀请好友的 重要功能。最后,《Hero Academy》的随机匹配屏幕也允许玩家去设置他们的团队并找到对手。注意要妥善设置“购买”按钮,以便何促销IAP内容。

注:本文并非旨在探讨如何绑定Facebook功能,但我会说明你设计自己的游戏内好友列表(针对那些并不使用Facebook的玩家)的过程。

《Hero Academy》的多人模式始于游戏列表(from indieflashblog)

《Hero Academy》的多人模式始于游戏列表(from indieflashblog)

《Hero Mages》功能的多人模式界面具有一个直观的通道屏幕,允许玩家访问该应用所有可用的功能,其中包括创造新游戏、加载和继续玩当前游戏,查看积分排行榜,设计定制 地图等功能。这其中还包含经典的在线聊天大厅界面,玩家可以从右上方的图标进入该界面。

《Hero Mages》支持即时多人和异步多人模式,它与设计一个支持玩家不同选项的新游戏创造屏幕密切相关。我在《Hero Academy》中添加了直接从英雄选择画购买扩展内容的功 能。但这里存在一些微妙的不同之处。“购买”标签并不出现于各个角色之上,而是出现一个将你引 向具有不同产品的新扩展画面的“购买”按钮。这种设置的首要原因是角色需 要成套购买。第二个原因是将英雄直接“锁定”在列表中,许多原来的玩家就会觉得自 己购买应用时没有“获得完整的游戏”。这种分离法能够更好地将新角色区分出来,让玩家 看到新角色是扩展内容(而不是核心套件)。

《Hero Mages》中的多人模式门关(from indieflashblog)

《Hero Mages》中的多人模式门关(from indieflashblog)

最后,《Hero Mages》的邀请系统与《Hero Academy》不同,它需要独立的画面。玩家可以邀请多达7名的好友加入游戏。《Hero Academy》“搜索好友”功能的一个弱点在于,如 果你的好友不在Facebook上,那你每次想邀请他们玩游戏时都需要输入对方的用户名。而《Hero Mages》则在你邀请好友加入游戏时创建了游戏内好友列表,从而改进了这一系统 。我稍后会说明这个系统的运行方式。

 

HeroMages_Invite2(from indieflashblog)

HeroMages_Invite2(from indieflashblog)

(在邀请好友时,你可以从一个下拉菜单中选择好友,该菜单还提供了添加还不是好友的玩家选项)

如何加入游戏?

注意以上所示的两个界面风格都不包包含“加入游戏”按钮。随着游戏创造的推进,人们该如何联系上对方?答案就是由屏幕背后的服务器来处理人们加入游戏的问题。当玩家“ 创造”一个配对时,他们实际上是向服务器发送了与一名玩家联系的请求。服务器会根据其他玩家是否在等待的情况,将这一请求存储为新游戏记录或者以已在排队的现成游戏记 录连接他们的游戏参数选择。系统不应令玩家为加入游戏的任务而操心,因为这会增加游戏体验的复杂性。除非你的游戏还支持实时多人模式,并且你也允许用户在游戏中途过程 中加入。《Hero Mages》的玩家可以通过主聊天大厅,点击右侧列表的即时游戏即可加入。

值得注意的是,《Hero Mages》还会在轮到玩家的回合时,将异步玩家引进即时游戏回合中。如果此时玩家正在异步情况下通过游戏列表玩对手载入的游戏,那么他们就会自动加 入即时游戏回合中。

总结有效的多人界面特点

正如上图所示,有效的多人游戏界面包含以下关键元素:

1.访问多人模式功能的门关屏幕

2.创造新游戏的明显而直观的方法

3.邀请好友的有效系统

设置一个随机的1v1配对

正如上文所述,有效的异步多人界面可以通过配对消除加入戏的问题。创造两名玩家之间随机的1v1匹配模式的最基本方法就是在首次发出配对请求时创造一个新游戏纪录。之后 ,当玩家再次请求配对时,就以先存的会话将他们连接起来。

当玩家开始一个随机对手配对时,会有以下两者中的一种情况:

1.现成的配对不可行,所以服务器要创造一个新配对,并将其存储在数据库中。用户会进入一个“寻找玩家”的屏幕。他们可能会继续等待一位对手加入,或者取消等待。即使他 们取消了等待,其请求仍然会保存在服务器中。这样,当有对手加入时,系统就会向其发送通知,他们就可以准备玩游戏了。

2.现成的配对是可行的,所以服务器完成了一个带有用户名参数的待定配对,并向他们发送游戏记录。服务器应分配“加入的”玩家开始第一回合。这样他们在游戏加载完成时就 可以立即开始玩游戏。

HeroMages_FindingPlayer(from indieflashblog)

HeroMages_FindingPlayer(from indieflashblog)

(当玩家进入这一界面时,系统就创造了一个新的游戏记录。玩家可以等待与一名对手建立联系或者选择取消!)

HeroMages_SearchingForOpponentPrompt(from indieflashblog)

HeroMages_SearchingForOpponentPrompt(from indieflashblog)

(该系统会在数据库中建立配对请求,并在找到一名对手时通知玩家,这样他们就无需继续等待。)

HeroMages_SearchingForOpponent(from indieflashblog)

HeroMages_SearchingForOpponent(from indieflashblog)

(新记录出现在玩家游戏列表中,代表该系统正在搜索一名对手。)

如何运行

本文并不讨论《Hero Mages》编程随机配对模式的代码执行方法,但会完整地概括该流程的要点。使用本文所列要点、编写客户端的技术,以及前两篇文章中所提到的服务器端代 码,将有助于你获得编写任何异步多人游戏的切入点。

1.当用户选择“随机匹配”按钮时,就会触发“sendMatchMakeCommand”,它会在客户端指导游戏创造类去创建一个新的游戏数据结构,并向服务器发送一个请求。

2.在服务器端,查看是否还有尚未配对的游戏正在排队等待。

A 如果当前没有尚未配对的游戏,在服务器创造一个拥有-1状态的新游戏记录,显示游戏正在等待一个对手。向客户端发回一个请求,显示未发现配对。

B 如果当前存在未配对游戏,载入该游戏记录的数据,并在回复中将其发回客户端。

3.在客户端

A 如果没找到配对,显示“寻找玩家”屏幕。如果玩家取消了,显示提示“配对就绪时我们会通知您”。

B 如果找到配对了,创造一个新游戏数据,整合玩家游戏参数(例如英雄选择)与配对玩家的当前游戏设置数据,并将响应发回到服务器。

4.在服务器端

A 为配对玩家更新具有新记录的游戏成绩表

B 更新具有新游戏设置数据的游戏记录

C 返回响应命令以载入游戏(就像第3篇文章中的载入游戏方式,只是这里会有一个额外的已包含配对游戏的标识)

5.在客户端

A 载入游戏数据并开始游戏。配对玩家可以立即开始玩游戏。

B 向服务器发送响应提示游戏已经配对

6.在服务器端,查看原来的配对创造者是否在线。如果在线,向其发送应用内部通知,告知配对已就绪。如果不在线,就发送推送通知或电子邮件。如果玩家仍然处于“寻找玩家 ”屏幕,我们就可以将他们与对手连接到即时游戏会话中。

随机异步游戏很适合1发配对,但如果你想同2人以上的玩家一起游戏又该如何呢?也许你可以设置一个更大的随机游戏,但这却存在可能对异步游戏产生危害的问题。例如,假如 你玩随机的PvP游戏,而你的队友却打算退出游戏了该怎么办?此外还要考虑到越多玩家参与一款游戏,轮到每位玩家的时间就越长。如果是随机玩家,大家就无法联系对方,告诉 他“嘿,轮到你了,让我们完成这个游戏吧!”这正是创造与好友绑定的游戏邀请系统的好处。这样你就知道和你一起玩的人可以看到游戏设置,以且能够一起完成游戏会话。

设计异步游戏邀请系统

1.游戏组织者启动一个新游戏创造屏幕,并且能够分配所有的游戏选项。这包括选择地图、游戏目标、组队以及增加电脑对手。游戏屏幕应该包括针对每位玩家(人类或者电脑) 的可增加单元。

2.游戏主持人可以使用玩家选择下拉菜单并选择“邀请好友”来邀请玩家。这里的邀请仅局限于玩家好友,因为玩家不可以向那些无意玩多人游戏的随机玩家发送干扰性的邀请。

3.当所有的选项都设置和分配好时,游戏组织者可以点击“发送邀请”按钮。这可以向服务器发送包含游戏设置数据,以及所有参与玩家的ID这一信息。

4.在服务器端,使用本系列第2篇中介绍的相同功能创造一个新游戏记录,但这里有两个不同之处:

A 游戏状态存储为-2以指示游戏邀请接受情况仍然待定

B hm_gameresults表格应该进行调整以便纳入存储玩家邀请接受状态的新参数以及定制游戏参数。

5.当游戏数据中的服务器创造好游戏记录时,它就会像其他游戏一样出现在邀请玩家的游戏列表。但当玩家选择它时,它不会启动游戏,而是载入游戏当前参数返回游戏创造屏幕 。此时玩家可以选择他们的英雄,并选择接受或拒绝邀请。

6.如果玩家拒绝邀请,游戏状态就会设置为-3,或者邀请被拒绝。游戏就会向所有被邀请玩家显示为完成状态,并且不再向其开放。

7.如果玩家接受邀请,系统就会呼叫服务器将该名玩家的接受状态设置为true。

8.在客户端,游戏记录界面应该破译被接受的响应,并在游戏记录绘制恰当的标记。例如,将那些已被邀请但并未回应的玩家显示为信封图标。而那些接受邀请者则显示为绿色的 打勾符号,拒绝进显示为红色的打叉符号。

9.当最后一名玩家接受游戏邀请时,该服务器就会分配游戏数据以便合并所有的新玩家参数。如果接受邀请的玩家积极应战,它就会发送一个启动游戏的信号,让他们各自开始游 戏。否则,它就会发送通知,指出哪位玩家先开局。

游戏成绩表的附件

回忆本系列第2篇中提到的hm_gamersulfts表格。这个表格用于存储与玩家相关的数据,其中包括玩家输赢结果,以及他们见证的最后一个游戏命令指数(用于查看游戏动画重播) 。对于邀请系统来说,我们需要额外的数值来追踪邀请响应,以及任何用于定制玩家团队或角色选择的参数。

table 1(from indieflashblog)

table 1(from indieflashblog)

记住,游戏邀请只是简单地创造一个会出现在玩家游戏列表的新游戏记录。唯一的区别在于除非所有玩家都接受邀请,该游戏状态为-2,或者所有玩家的接受情况仍然待定。我们 不想在游戏数据本身中存储玩家准备状态,因为两名玩家在同一时间接受邀请,就会覆盖另一者的接受状态。使用每位玩家各自的游戏成绩记录,就可以让每位玩家修改自己的状 态。只有当最后一名玩家接受邀请时,我们才能重写游戏数据以纳入该玩家的所有游戏设置信息(类似于以上部分中提到的合并配对的随机玩家)。

更深的设计考虑

游戏邀请发送出去后,如果玩家不回应怎么办?

游戏邀请的响应方式与游戏记录一样。它们会与玩家正在玩的游戏或已经完成的游戏一样出现在游戏列表。其区别就在于,游戏邀请的状态标识是-2,在游戏列表中的标注是“邀 请待定”。

HeroMages_PendingInvite(from indieflashblog)

HeroMages_PendingInvite(from indieflashblog)

(邀请待定的游戏记录使用符号来指示哪位玩家已经接受邀请)

注意这些图标是用于代表游戏邀请状态。那些已被邀请但并未回应的玩家显示为信封图标。而那些接受邀请者则显示为绿色的打勾符号,拒绝进显示为红色的打叉符号。

要注意保持游戏列表的整洁,以确保玩家返回应用时很看到清晰的“任务”集合。

之前文章的游戏列表分类算法运行方法如下:

1.玩家需要回应邀请的游戏处于最高优先权,因为游戏只有在所有玩家都接受的状态下才能开始。它们要在游戏列表中居首。

2.轮到玩家出击的游戏处于第二优先权,因为如果玩家不完成自己的回合,这些游戏就无法取得进展。这些记录要按照日期第二分类条件显示在列表中。

3.排在第三位的是正在进行中的游戏或者等待其他玩家接受邀请的游戏,它们同样依照记录中最后一次活动的时期/时间进行分类。

4.最后是已经完成的游戏和拒绝的邀请,同样以时间分类。

那些尚未得到响应的邀请仍会显示在玩家的游戏列表中,等待其他玩家的操作(类似于游戏等待对手的回合)。玩家可以选择“放弃”3天后仍未执行自己这一回合的对手。在游戏 开始之前的任何时间,任何玩家都可以更改对游戏邀请的接受状态,例如将接受改为拒绝。这样会让游戏显示“邀请被拒绝”的状态。游戏就会显示为完成,并且不会再影响玩家 状态。

当游戏准备开始时,谁先下手?

基于邀请的游戏参与玩家可能多达8人。这里不建议由最后进来的玩家先开始,而应该遵循最初游戏设置所计算出来找随机回合顺序。系统应该在游戏开始时向首个玩家发送执行操 作的通知。当游戏开始时,游戏状态就会更改为“游戏正在进行中”,玩家就会看到游戏列表中的游戏记录更新。

什么机制可以阻止玩家发送不受欢迎的邀请?

玩家可能使用积分排行榜作为查看最高排名的玩家是谁,并向对方提交挑战请求,以期赢得更高的点数。但这有可能让热门玩家受到干扰,《Hero Mages》就要求玩家只能邀请好 友,而不是任何玩家。这有助于杜绝不请自来的游戏邀请,保持游戏列表的整洁性。

与此同时,系统也不能限制玩家在游戏中发展友谊。为此,《Hero Mages》允许你通过游戏邀请来请求新好友。当添加一名新好友时,玩家就可以向其发送游戏邀请。如果该玩家 接受游戏邀请,他们就会自动成为游戏组织者的好友。如果他们拒绝游戏邀请,这种友情状态仍会处于待定状态,直到他们接受或者取消邀请为者。我们将在下一个环节中介绍创 造游戏内好友系统的过程。

创造游戏内好友系统

像Facebook这类社交网站是将玩家与现成的好友连接起来的优秀解决方法,但仅仅依赖这些网站却可能疏离那些并不使用社交媒体或者不喜欢让你的游戏获取其社交媒体帐号的用 户。

推荐使用游戏内好友系统的原因如下:

1.你可以完全控制如何建立游戏内的友谊关系,它具有更高的灵活性,更便于在游戏内部社区中进行病毒扩散。

2.玩家的友谊独立于社交网络,所以无论你的用户是否在Facebook或其他社交网站上,你都可以创建玩家社区。

3.但仍然可以增加绑定社交媒体的选项,以便强化你的游戏社区。

这一部分描述的是设计游戏内好友系统,以及将其同游戏界面和邀请系统绑定,以便促进游戏玩家社区发展的过程。

好友表格

创造好友系统的第一步是创建一个新的数据库表格。以下就是《Hero Mages》的标准惯例,我使用的标识符是 “hm_friends”。该表格应该包括以下属性:

table 2(from indieflashblog)

table 2(from indieflashblog)

客户端好友列表UI

为了创造新的好友关系,客户端将需要一些额外的用户界面组件设置,以便向服务器发送一个新好友请求。一个重要的问题就是:“玩家如何在游戏中设置友情?”你的好友系统 越直观,玩家就越有可能在游戏中建立友情,强化游戏的玩家社区和长期可行性。

《Hero Mages》中的玩家可以通过3种方式建立友情:

1.打开另一玩家的资料,选择“添加好友”按钮。玩玩可以通过在线聊天室获取对方资料、积分排行榜,并在游戏中的玩家列表中选择该玩家名称。

2.使用“好友管理”屏幕(通过多人模式门关进入)并寻找玩家的用户名。

3.当设置新游戏邀请时选择“添加好友”。可通过好友管理选项2执行这一操作,之后返回游戏邀请屏幕,允许玩家快速邀请新好友加入游戏。

还可以在设置定制游戏邀请时添加好友(from indieflashblog)

还可以在设置定制游戏邀请时添加好友(from indieflashblog)

添加好友

客户端将以既定的合适UI,向服务器发送一个好友请求,其中包含发送请求的成员ID,以及他们想添加的好友ID。

在服务器端会发生以下情况:

1.首先查看是否已存在好友关系。

sql = “SELECT * FROM hm_friends WHERE ID_MEMBER = “+params.hmId+” AND ID_FRIEND = “+params.fId;

2.根据查询是否回馈结果,创造一个新的友情记录或更新当前的友情记录状态。

A 如果查询没有结果,说明不存在好友关系,就要创造新记录:

sql = “INSERT into hm_friends (ID_MEMBER, ID_FRIEND, accepted) VALUES (“+params.hmId+”, “+params.fId+”, 1) , (“+params.fId+”, “+params.hmId+”, -1)”
;

B 否则就是存在好友关系,更新好友关系的状态:

sql = “UPDATE hm_friends set accepted = 1 WHERE ID_MEMBER = “+params.hmId+” and ID_FRIEND = “+params.fId;

3.向客户端发送一个包含玩家更新好友列表的回复。该列表包括名称、ID、虚拟角色,以及玩家好友的友情状态。

值得注意的是,每个好友关系是由hm_friends表格中的两个记录所定义。在各个好友关系中,玩家将具有一个由ID_MEMBER存储他们自己ID的记录,以及另一个在ID_FRIEND中存储 好友ID的记录。这两条记录都很重要,这样玩家就可以在无需创建/删除额外记录的情况下添加和移除好友。

遵循这一做法的原因有三:

1.保持追踪已存在的好友关系,确保玩家无法向不明确的对象发送好友请求。当对方拒绝好友请求时,“接受”属性就会设置为0,这样该用户就不会再出现额外的好友请求。只有 在该玩家添加了最初发送请求者为好友时,两者才能建立好友关系。

2.保持追踪已存在的好友关系,以便程序查看“sentGameInvite”属性。这一属性存储了游戏是否向这名玩家发送了邀请的情况。如果这一数值标识为true,那么游戏就不允许玩家 再向未接受邀请的用户发送干扰性的邀请信息。

3.最重要的是,确认好友关系生效需要这两条记录。可以将该系统想象成双方握手。发送邀请的玩家会自动将其“接受”标记为true,而获得邀请的玩家则将其“接受”标记为-1 (未决定),直到他们决定接受或拒绝为止。

移除好友

以这一设置移除好友很容易:简单地将将玩家的“接受”状态更新为0以移除好友。这要使用到以下陈述:

sql = “UPDATE hm_friends set accepted = 0 WHERE ID_MEMBER = “+params.hmId+” and ID_FRIEND = “+params.fId;

获取好友列表

好友系统会集合在一起,通过这种服务器端的MySAL陈述将好友列表返回客户端的玩家。以下是应部署于服务器端代码的 “getFriendsList”函数例子:

//CODE EXAMPLE FRIEND LIST
function getFriendsList(hmId){//PASS THE ID OF THE PLAYER WHOSE FRIEND LIST WE ARE RETURNING
//STEP 1: Generate friends list query results by joining tables with pertinent data together
sql = “SELECT * FROM (\r”;
sql += “SELECT s.memberName as friendName, avatar, m.ID_FRIEND as fId, f.accepted as friendAccept, m.accepted as myAccept, m.sentGameInvite as g FROM
hm_friends m\r”
sql += “JOIN hm_friends f ON f.ID_MEMBER = m.ID_FRIEND AND m.ID_MEMBER = f.ID_FRIEND\r”
sql += “JOIN smf_members s on m.ID_FRIEND = s.ID_MEMBER\r”
sql += “WHERE m.ID_MEMBER = “+hmId+” and f.accepted != 0 and m.accepted != 0″;
sql += “) as x\r”;
sql += “ORDER BY myAccept, friendAccept, friendName”;

//STEP 2: Populate the results into an array of objects that can be populated into a list component by client
var queryRes = dbase.executeQuery(sql);
var friendsList = [];
for(var i = 0; i < queryRes.size(); i++){
var tempRow = queryRes.get(i)
var row = new Object()
row.n = tempRow.getItem(“friendName”)
row.a = tempRow.getItem(“avatar”);
row.fId = tempRow.getItem(“fId”);
row.g = tempRow.getItem(“g”);

//STEP 3: Set the status of the player based on query results
if(tempRow.getItem(“myAccept”) == -1){
row.s = -2; //This player has requested me as a friend
}
else if(tempRow.getItem(“friendAccept”) == -1){
row.s = -1; //I’ve requested this player as a friend and am waiting response
}
else{
row.s = 1; //We’re friends
}
friendsList.push(row);
}
return friendsList;
}

处理客户端的服务器响应

同之前的例子一样,最后一步就是处理客户端的服务器响应。最佳做法是在一个普遍易用的位置存储好友列表数据。这样,数据就可以填充多种视图,例如位于好友管理系统的好 友列表UI,或者游戏邀请系统中的下拉菜单。

总结

本文说明了如何设计一个高效异步多人界面,如何设置随机配对,如何设计游戏邀请系统,以及如何创造简单的好友列表等方法。本系列下篇文章将说明将完整的异步多人体验绑 定在一起的最终必要元素。

相关拓展阅读:篇目1篇目2篇目3(本文由游戏邦编译,转载请注明来源,或咨询微信zhengjintiao)

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 ndieFlashBlog.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, creatio 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.

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.

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.

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:

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!

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:

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.

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.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 awayRequirements

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.

The key to a successful multiplayer game is having a critical mass of people to play with each other. But how do you amass players without an initial player base in the first place? Remove the requirement of “must-be-online-to-play”. Asynchronous matchmaking enables players to engage in multiplayer games, even if they’re not online! Random matchups allow players to meet new people and make new friends, and an invitation system enables them to grow these
friendships and create a viral spread of enthusiasm for your game. This article will explain how to use your game server and database to manage the process of creating and joining multiplayer games.

This article will explain:

How to create an intuitive multiplayer interface

How to setup an asynchronous multiplayer match with a random player

How to design an asynchronous game invitation system

How to design a friends management system so players can invite each other to game even when their friends aren’t online

Requirements

Prerequisite knowledge

Experience developing turn based games

Familiarity with ActionScript 3.0

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

Has Read “Part 3: Loading Games from the Database”

Required products

Flash Professional (Download trial)

Smart Fox Server Pro (Download trial)

MySQL Database

User level

Advanced

Creating an Intuitive Multiplayer Experience

User experience is the most important part of creating an asynchronous multiplayer game. Regardless of how awesome your game may be, you will lose users if it’s not immediately clear to them how to setup multiplayer games.

In the original, real-time multiplayer implementation for Hero Mages, users would login and immediately enter a chat lobby where they can converse with other players, create games, and access other modules of the system such as the leaderboards and map editor. The system was modeled to mimic the design of popular multiplayer gateways like the original Battle.net for Diablo and Starcraft.

Hardcore gamers are generally familiar with this setup, but many casual gamers are completely lost and uncertain about what to do next.

Learn by Example

Defining, in clear terms, how the game interface should function and designing layouts that are effective and intuitive to users is a challenging process. One of the most effective ways to overcome this challenge is to explore how other games have handled the problem and then seek to improve upon existing implementations.

Hero Academy is a very successful asynchronous multiplayer game that has an intuitive user interface. Multiplayer launches directly to the player’s game list where they can start new matches or continue playing games they’ve already started. Creating a new game takes you to a screen with three options for connecting with friends: inviting a player via search, finding a random opponent, and inviting a friend via Facebook. Hero Academy’s social media integration is an important feature for making it easier to invite friends. Finally, Hero Academy’s random match screen allows player to setup their team and find an opponent. Note the well-placed “Buy” button here that helps promote In-App purchasing.

Note: I won’t be discussing how to integrate with Facebook in this article (perhaps in the future) but I will explain the process for designing your own in-game friends list for those players who don’t use Facebook.

Hero Academy’s multiplayer begins from the game list.

Creating a new multiplayer game in Hero Academy

The ability to buy character expansions has been added to the party builder screen.

The new multiplayer interface for Hero Mages features an intuitive gateway screen that allows players to access all available functions of the application, including the ability to create new games, load and continue playing existing games, view the leaderboard, design custom maps, and so forth. The classic live-chat lobby interface is included as well and can be accessed via the icon in the upper left.

As Hero Mages supports both real-time multiplayer and asynchronous multiplayer, it was pertinent to design a new game creation screen that highlights the different options available to the player.

As with Hero Academy, I’ve added the ability to buy expansions directly from the hero selection screen. There’s a subtle difference here, however: “Buy” labels are not placed over each character.

Rather, the “Buy” button takes you to a new expansions screen for different products. The first reason for this is that characters are bought in sets. The second is that by having “locked” heroes directly in the list, many existing players express feelings of “not getting the full game” when they purchase the app. This separation helps to better distinguish the fact that new characters are expansions (not part of the core set).

The new multiplayer gateway for Hero Mages

Creating a new multiplayer game in Hero Mages

The ability to buy character expansions has been added to the party builder

Finally, the invitation system for Hero Mages demands a separate screen since, unlike Hero Academy, players can invite up to seven of their friends to a
game. One of the weaknesses of the Hero Academy “Friend Search” is that, if your friend is NOT on Facebook, it’s necessary to type in your friends’
username each time you want to play a game. Hero Mages improves on this system by building an in-game friends list as you invite players to join your games. I’ll explain how this system works later in the article.

You can invite up to seven of your friends to play a game of Hero Mages

To invite friends, you select them from a drop down menu which also provides the option to add players who you aren’t yet friends with.

What about joining games?

Notice how neither interface style shown above includes a “Join Game” button. With all this game creation going on, how are people actually connecting with one and other? The answer is that the joining of games is handled for the user behind the scenes by the server. When a player “creates” a new match, they’ re actually sending a request to the server to connect with a player. Depending on whether another player is waiting or not, the server will either store their request as a new game record or connect their game preferences with an existing game record waiting on queue. The player shouldn’t be bothered with the task of joining because this diminishes the simplicity of the experience. The exception to this principle is if your game also supports live multiplayer games and you want to allow users to join sessions in progress. This is accomplished in Hero Mages via the main chat lobby. Users can click on live games in the list to the left to join them.

It’s also worthy to note that Hero Mages will also connect asynchronous players to a live game session while they’re taking their turn. If, at the same time a person is playing asynchronously their opponent loads that session from their games list, they’ll be automatically joined together in a live game session.

Summary of an Effective Multiplayer Interface

As demonstrated in the above images, an effective multiplayer game interface includes the following key elements:

Gateway screen for accessing all of the multiplayer features

Obvious and intuitive means to create new games

Effective system for inviting friends

Setting up a Random 1v1 Match

As illustrated above, effective asynchronous multiplayer interfaces eliminate the concept of joining games in favor of matchmaking. The most basic way to create a random 1v1 match between two players is to create a new game record the first time a match is requested. Then, the next time a player requests a match, connect them with the preexisting session.

When a player starts a random opponent match, one of two things happens:

An existing match is not available, so the server creates a new match and stores it in the database. The user is taken to a “Finding Player” screen. They may continue waiting for an opponent to join, or cancel. Even if they cancel, their request remains on the server. This way, they can be notified when an opponent has joined and they are ready to take their turn.

An existing match is available, so the server completes a pending match with the user’s game preferences and sends them the game record to load. The server should always configure the “joining” player to get the first turn. This way, they can begin playing immediately once the game is loaded.

By the time the player reaches this screen, a new game record has been created. The player can wait to connect with an opponent or choose to cancel.

The system will store the match request within the database and notify the player when an opponent has been found so they don’t have to keep waiting.

A new record appears in the player’s game list, representing that the system is searching for opponent.

How it Works

The specific code implementation used to program the random matchmaking for Hero Mages is beyond the scope of this article, but a complete outline of the process is explained below. Using this outline and the techniques for coding client and server side code learned in the last two articles should provide a helpful starting point for programming any asynchronous multiplayer game.

In order to create the seamless matchmaking experience illustrated above, several communications between the client and server take place to appropriately configure the game:

When the user selects the “Random Match” button, a “sendMatchMakeCommand” is triggered which instructs the game creation class on the client side to build a new game data structure and send a request to the server.

On server side, check to see if there are any unmatched games waiting in queue

If no unmatched games available, create a new game record on the server with a status of -1 to indicate that the game is waiting for opponent. Send response back to client indicating that a match was not found.

If an unmatched game is available, load this game record’s data and send it back to the client in the response

On client side,

If no match was found, show “Finding Player” screen. If player cancels, show prompt saying “We’ll notify you when your match is ready.”

If match was found, create a new game data that merges player’s game preferences such as hero selections with the matched player’s existing game setup data and send response back to server

On server side,

Update the game results table with new record entry for the matched player

Update the game record itself with new game setup data

Return response command to load game (works just like loading the game in article 3 except an additional flag for matched game is included)n client side,

Load the game data and start the game. Matched player may begin playing immediately.

Send a response to server indicating game has been matched

On server side, check if original match creator is online. If so, send them in-app notification that their match is ready. If not online, send a push notification or email. If the player is still on the “Finding Player” screen, we can connect them to a live game session with their opponent.

Random asynchronous games are well suited for 1v1 matches, but what if you want to play a game with more than 2 people? While it’s possible to setup a larger random game, this allows the potential for issues that may be disastrous for an asynchronous game. For example, what if you play a random 2v2 game and your teammate decides to quit playing? Also consider that the more players involved in a large game, the longer it will take before each player’s turn
cycles back. With random players, there’s no way to contact each other and say “Hey, you, take your turn so we can finish up this game!” This is why it’s beneficial to create a game invitation system that’s restricted to friends. This way, you know the people you’re playing with have had a chance to see the game settings and are committed to finishing the session.

Designing an Asynchronous Game Invitation System

A game invitation system is essential for players to engage their friends in asynchronous multiplayer battles. The first requirement of building an invitation is the game creation screen- a place where the game host can configure options like which map to play on, whether or not there will be teams or computer opponents, and what the game objective will be. The second requirement is a mean to communicate between all invited players so that each player can submit their character choices and invitation acceptance and the game can begin when everyone is ready.

How it Works

The game organizer launches a new game creation screen and can configure all game options. This includes choosing the map, game objective, organizing teams, and adding computer opponents. The game screen should include cells for each player (human or computer) that can be added.

The game host can invite players using the player options drop down and selecting “Invite Friend”. It’s important to limit invitations to friends because you don’t players being allowed to spam invites to random players who may not want to play a multiplayer game.

When all options are setup and configured, the game organizer can click the “Send Invitation” button. This sends a message to the server with the game setup data and the ID’s of all the players involved.

On the server side, a new game record is created using the same functionality discussed in Tutorial 2 with two differences:

The game status is stored as -2 to indicate the game is pending invitation acceptance

The hm_gameresults table should be modified to include new parameters that store the players’ invitation acceptance status and custom game preferences.

Once the game record has been created by the server in the games data, it will appear in the invited players’ game lists just like other games. Instead of launching the game when a player selects it, however, the application will load the game’s current parameters back into the game creation screen. Here the player can select their heroes and choose to accept or decline the invitation.

If the player declines invitation, the game status is set to -3, or Invitation Declined. The game will be flagged as completed for all players that were invited and will no longer be accessible.

If the player accepts invitation, a call to the server is made to set the acceptance status for that player to true.

On the client side, the game record interface should interpret accepted responses and draw appropriate markers on the game record. For example, players who were invited but haven’t yet responded show an envelope. Players who have accepted show a green check mark. Players who decline show a red x.

When the final player has accepted the game invitation, the server will assemble the game data to incorporate all of the new player preferences. If the player who accepted has the active turn, it will send back a signal to launch the game so they can take their turn. Otherwise, it will send a notification to whichever player has the first turn.

Additions to the Game Results Table

Recall from part 2 of this series the creation of a table called hm_gameresults. This table is responsible for storing player-specific data related to the game, including the player’s win/loss result and the index of the last game command they witnessed (used for viewing game animation replays). For the invitation system, additional values are needed to track invitation response and any parameters for customizing the player’s team or character preferences.

hm_gameresults

Field Type Notes

accept tinyint(4) Stores value of -1, 1 or 0 to indicate whether player has not yet responded, accepted, or declined respectively

ID_MAGE int(10) Stores the unique id of the player’s mage they have chosen to bring to battle

ID_GUARD1 int(10) Stores the unique id of the player’s guardian they have chosen to bring to battle

ID_GUARD2 int(10) Stores the unique id of the player’s guardian they have chosen to bring to battle

Remember, a game invitation is simply creating a new game record that can show up in the player’s game list. The only difference is that until all players have accepted, the status of that game is -2, or pending invitation acceptance from all players. We don’t want to store players’ ready status in the game data itself because two players accepting an invitation at the same time could overwrite the others’ acceptance. By using each player’s own game result record, each player can individually modify their status. Only when the final player has accepted the invitation do we rewrite the game data to include all of the player’s game setup information (similar to merging the matched random player in the section above).

Further Design Considerations

Important questions regarding how to handle unique situations will arise during the creation of the game invitation system. Below are a few challenges that came up during the development of Hero Mages invitation system and how I handled them.

What happens to a game invitation once it’s been sent and what if players don’t respond to it?

Game invitations are designed to act in the same fashion as game records. They appear in the player’s game list with the games they are currently playing or have completed. The difference is invitations are marked with a different status id, in this case, -2, and are denoted on the game list as “Pending invitation”.

Game records that are pending invitation use symbols to indicate which players have accepted.

Note that iconic symbols are used to represent the status of the game invitation. Players who were invited but haven’t yet responded show an envelope. Players who have accepted show a green check mark. Players who decline shw a red x.

It’s important to keep the game list as tidy as possible to ensure players have a clear set of “tasks” to complete when they return to the application.

The game list sort algorithm is adjusted from the previous articles to work as follows:

Games whose invitation the player needs to respond to are the highest priority because a game can’t start until all players have accepts. These will be listed first in the game list.

Games in progress where it’s the player’s turn to attack are the next highest priority because these games cannot progress until the player has completed their turn. These records are shown next in the list with a secondary sort condition by date.

Games in progress or waiting for other players to accept invitation are listed next, also sorted by the date/time of the last activity on that record.Finally, completed games and declined invitations, also sorted by date, are listed last.

To address the original design concern, invitations that haven’t been responded to remain listed in the player’s game list, pending action from other players (similar to games waiting on opponent’s turn). Players have the option to “drop” opponents who haven’t taken their turn after (3) days. And, at any time before a game starts, any of the players may change their acceptance to a game invitation to “Decline”. Doing so will set the status of the game to “Invitation Declined”. The game will be considered complete and will not affect player stats.

When a game is ready to play, who takes the first turn?

Since invitation based games can potentially involve up to 8 players, it’s not advisable to simply have the last player who accepts go first. Instead, the game should follow the random turn order pattern originally calculated in the initial game setup. The system should be configured to send a notification to the first player to take their turn once the game has started. Once a game has started, the status changes to “Game in progress”, and players will see the game record updated as such on their game list.

What mechanism prevents a player from spamming undesired game invitation requests?

Players may use the leaderboard as a means to look up the highest ranked player and submit challenge request for the chance to earn higher rating points. Because this could potentially overwhelm popular players, Hero Mages requires that players invite friends rather than just any player. This mitigates unsolicited game invitation requests and keeps players’ games lists manageable.

At the same time, it’s important not to limit the development of friendships within the game. For this reason, Hero Mages allows you to request new friends via the game invitation itself. When a new friend is added, players may send them (1) game invitation request before they’ve accepted the friendship. If the player accepts the game invite, they’ll automatically become friends with the game organizer. If they decline the game request, the friendship status will
remain pending until they accept it or the inviting player cancels their invitation request. The process for creating an in-game friends system is explained in the next section.

Creating an In-Game Friends System

While social networks like Facebook are a great solution for connecting players with their existing friends, relying solely on these sites can isolate players that don’t use social media or simply prefer not to give your game access to their social media accounts.

Having an in-game friends system is advisable for the following reasons:

You have full control over how in-game friendships are formed allowing for greater flexibility and viral spread of your game’s internal communityFriendships are independent of social networks, so your game can build community regardless of whether your users are on Facebook or other social networks

The option to integrate with social media to enhance your game’s community is still available

This section describes the process of designing an in-game friends system and integrating it with your game’s interface and invitation system to foster the development of your game’s player community.

The Friends Table

The first step in creating a friends system is to create a new database table. Following the standard Hero Mages convention, I used the identifier “hm_friends”. The table should include the following properties:

hm_friends

Field Type Notes

ID_LINK int(10) The unique ID of the friend relationship

ID_MEMBER int(10) The ID of the member who has a friend

ID_FRIEND int(10) The ID of the member who is the member’s friend

accepted tinyint(4) ( 0, 1, -1) Indicates whether friend invite has been declined, accepted, or not responded to yet respectively

sentGameInvite tinyint(4) (0 or 1) Indicates whether or not game invite has been sent on this friendship

Client Side Friend List UI

In order to create new friend relationships, the client side will need some additional user interface components setup in order to send a new friend request to the server. The important question to ask is: “How will players setup a friendship within the game?” The more intuitive your friends system is, the greater the likelihood that players will establish friendships with the game, strengthening the game’s player community and long-term viability.

Friendships in Hero Mages can be established in three ways:

By opening another player’s stats profile and selecting the “Add Friend” button. Player stat profiles can be accessed in the live chat room, the leaderboard, and within games by selecting the player’s name in the player list.

By using the “Friends Management” screen (accessed via the multiplayer gateway) and searching for the player’s username.

By selecting “Add Friend” when setting up a new game invitation. Using this action will leverage the Friends Management option #2 and then return to the game invitation screen, allowing the player the ability to quickly invite new friends to their game.

Players can add friends via players’ stat cards.

Friends can be added and removed via the Friends Management screen.

Friends can also be added when setting up a custom game invitation

Adding Friends

With the proper UI established, the client side will send the server a friend request that includes the ID of the member who’s requesting the friendship and the ID of the player they want to become friends with.

On the server side, the following happens:

First check to see if an existing friendship exists

sql = “SELECT * FROM hm_friends WHERE ID_MEMBER = “+params.hmId+” AND ID_FRIEND = “+params.fId;

Depending on whether or not query returns results, create new friendship record or update the status of the existing friendship record

If query returns no results, relationship does not exist yet, create new record:

sql = “INSERT into hm_friends (ID_MEMBER, ID_FRIEND, accepted) VALUES (“+params.hmId+”, “+params.fId+”, 1) , (“+params.fId+”, “+params.hmId+”, -1)”
;

Otherwise, relationship exists, update the status of the relationship:

sql = “UPDATE hm_friends set accepted = 1 WHERE ID_MEMBER = “+params.hmId+” and ID_FRIEND = “+params.fId;

Send the client side a response that includes the player’s updated friendship list. This list includes the names, ids, avatars, and relationship status of the player’s friends.

It is important to note that each friendship is defined by two records in the hm_friends table. For each friendship, the player will have a record where their ID_MEMBER stores their own ID and another record where for their friend stores their ID in ID_FRIEND. The two records are necessary so that friendships can be added and removed without the need to create/delete additional records.

It’s advisable to follow this practice for three reasons:

Keeping track of existing friendship relationships ensures that player’s cannot indefinitely spam players to be their friends. Once a friend request is declined, the “accepted” property is set to 0 so that additional friend requests will not appear from this user. The friendship can only be established by this player adding the original requester as a friend.

Keeping track of existing friendships allows the program to check the “sentGameInvite” property. This property stores whether or not a game invite was sent on this friendship. The game should not allow players to spam invites to players who have not accepted their friendship if this value has been flagged as true.

Most importantly, two records are needed to validate friendship acceptance. Think of the system as a two-way handshake. The player who initiates the friendship automatically sets their “acceptance” flag to true. The player who is requested as a friend will have their “acceptance” set to -1 (undecided) until they decide to accept the friendship or decline it.

Removing Friends

Removing a friendship with this setup is easy: simply update the “accepted” status of the player opting to remove the friend to ’0′. The following statement is used:

sql = “UPDATE hm_friends set accepted = 0 WHERE ID_MEMBER = “+params.hmId+” and ID_FRIEND = “+params.fId;

Getting the Friends List

The friend system comes together via the MySQL statement on the server side that returns the friend list back to the player on the client side. Below is an example of a “getFriendsList” function that should be implemented in the server side code:

Handle Server Response on Client Side

As in previous examples, the last step is handling the server side response on the client. The best practice is to store the friend list data in a universally accessible location. This way, the data can populate multiple style views like a friend list UI in the friend management system or a drop down menu in the game invitation system.


上一篇:

下一篇: