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

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

发布时间:2013-04-29 08:42:28 Tags:,,,,

作者:Ross Przybylski

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

本篇文章将解释:

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”的全新用户界面屏幕类。我想要先专注于数据和代码组件,所以最初的设计便局限于数据头和滚动列表组件,即能够用于在服务器上填充信息:

gameListLayoutBasic1(from indieflashblog)

gameListLayoutBasic1(from indieflashblog)

这一界面中有一个数据查询库,即带有一列活跃玩家的游戏数据。所有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天内未回到之前的游戏回合中,那么对手玩家就可以选择与之解除关系。

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

总结

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

原文发表于2012年5月30日,所涉事件和数据均以当时为准。

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

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

May 30th, 2012

by Ross Przybylski

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

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.(source:indieflashblog)


上一篇:

下一篇: