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

以《mmoAsteroids》为例分享多人游戏制作经验(下)

发布时间:2014-01-23 17:41:05 Tags:,,,,

作者:Paul Firth

这是关于创造一款多人游戏的系列文章中的第二部分。(请点击此处阅读本文上篇

上次我在node.js上创造一个TCP套接字服务器,我们能够发送并接收复杂的类型。

以下是游戏的实况版本。我将在本系列文章中进行详细描述:

live version(from wildbunny)

live version(from wildbunny)

时钟同步

客户端和服务器的时钟是同步的,因为如果存在任何基于时间的插值,你便会希望服务器和客户端在时间上保持一致,如此你的插值对象的位置也将保持一致。

我在2D空间MMO游戏中使用这一技巧以确保轨道小行星在所有客户端与服务器上保持着同样的位置。小行星围绕着中央位置插入一个轨道—-这里不会发送升级信息去纠正它们的位置,唯一保持其同步的元素是在所有客户端和服务器上具有同样的时间值。

在我们尝试着同步时钟前,我们需要确保正在服务器和客户端上使用同样的时间概念。我们想要估算自1970/01/01以来的秒数,这在javascript和actionscript中是一样的:

var time = new Date( ).getTime( )/1000.0;

同步时钟的一种方法是先想出需要花多长时间让信息完成来回循环,即从客户端–>服务器–>客户端,就像ping(游戏邦注:计算机网络管理实用程序)那样。我们是通过发送一个包含客户端当地时间的信息做到这点。然而服务器将回应它所收到的数据以及有关服务器上的时间的额外参数。

当信息回到客户端上时,客户端可以在信息中提取早前的客户端时间,即通过客户端上的“现在”时间,然后到达一个整体的往返时间。基于我们到达时所有重要的延迟值而计算这一往返时间的半值。延迟时间是指我们的信息到达服务器所花费的时间。

使用来自服务器(服务器时间)的回应的第二部分内容,我们可以计算能够说明任何时间区域差值的偏差。

实际上,我们并不是只做一次同步,因为这是从属于基于当前客户端和服务器连接质量的误差—-想象客户端是基于火车上的3G网络,那么其往返时间将与基与最近手机信号台的接近度具有较大的差别。为了解决这一问题,我们将继续同步并计算平均值。

/**
* Send clock synchronisation message
*/
private function SyncClocks( ):void
{
Message.SerialiseAndSend( m_socket, MessageNames.kTime, {m_clientTime:m_LocalTimeSeconds} );
}

/**
* Get the time on this local machine, in seconds
*/
static public function get m_LocalTimeSeconds( ):Number
{
return new Date( ).getTime( )/1000.0;
}

/**
* Syncronise clocks
*
* @param message
*
*/
public function TimeMessage( message:MessageContainer ):void
{
var now:Number = m_LocalTimeSeconds;
var clientTime:Number = message.m_data.m_clientTime;
var serverTime:Number = message.m_data.m_serverTime;

// round trip time in seconds
var roundTripSeconds:Number = now-clientTime;

// latency is how long message took to get to server
var latency:Number = roundTripSeconds/2;

// difference between server time and client time
var serverDeltaSeconds:Number = serverTime-now;

// store averages
if ( m_latency!=Number.MAX_VALUE )
{
m_latency = ( m_latency+latency )*0.5;
}
else
{
m_latency = latency;
}

// this is the current compenstation
var totalDeltaSeconds:Number = serverDeltaSeconds+m_latency;
m_timeCompensation = totalDeltaSeconds;

// check again in 5 seconds
setTimeout( SyncClocks, 5*1000 );
}

在这一代码中,我使用了一个简单的移动平均数。你可能想要做些更高级的事,就像储存更多数据点而不只是最后的值,然后通过与数据集的中值做比较而过滤掉任何异常值。

Flash和套接字策略文件

再进一步分析前,我们需要说说当与一个套接字服务器相连接时Flash是如何运行的。Flash客户端将在第一次连接套接字服务器时为一个策略文件发送请求;这一策略文件将告诉Flash套接字服务器接收Flash连接的是哪个领域和端口。

Flash将如下发送一个请求:

<policy-file-request/>

这将首先于端口843执行,然后在该端口中你可以选择连接服务器。

服务器必须基于一个稳定的策略文件(其末段是一个空字符)做出回应。这是我在node.js中用于行程策略回应的函数:

/**
* Get the socket policy response for the given port number
*/
function GetPolicyResponse(port)
{
var xml = ‘<?xml version=”1.0″?>\n<!DOCTYPE cross-domain-policy SYSTEM’
+ ‘ “http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd”>\n<cross-domain-policy>\n’;

xml += ‘<allow-access-from domain=”*” to-ports=”‘ + port + ‘”/>\n’;
xml += ‘</cross-domain-policy>\n’;

return xml;
}

我发现尽管大多数Flash客户端请求一个策略文件,这里存在一些惯常活动,所以你必须积极地处理这两种情况。

在转换反应游戏中同步状态

这时候,谈论我们如何设计具有足够回应的系统去处理转换反应游戏是非常重要的。在多人游戏中存在两种不同的同步状态方法(关于状态我指的是位置,方向,速度以及其他游戏内部对象的属性):

定期发送有关所有改变了状态的对象的更新

只在一个事件出现时发送更新

第一个方法是基于固定间隔,即关于所有对象都改变状态时发送数据。这引进了等于状态更新期间的时间延迟,但也保证了状态信息的稳定流,即不受限于溢流。

第二个方法只在事件发生时发送状态,如一个按键被按压。这具有最少的延迟,但却是受到溢流的支配;例如当玩家开始敲打按键时。

我选择使用一个基于方法的事件,因为在转换反应游戏中反应时间很重要,同时也因为它允许我去处理子弹问题。

子弹问题

想象如果在游戏中每个子弹发射都会引起信息的传播那会怎样?这很快便会淹没服务器可用带宽,并开始引出各种延迟问题。为了解决这一问题,我从客户端发送了按键到服务器,并提供了武器自动重复,如此玩家是否发射便是取决于当前哪个按键处于客户端下方。

如果我检测到按键敲进游戏(这是这类型游戏的一种自然行为),我便会提供一个信息告诉玩家他们可以抑制发射。

当玩家在发射子弹时,子弹将在客户端和服务器上基于恒速发射出去,这里存在一个同步计时器将确保客户端和服务器同时发射子弹。

客户端预测和服务器控制

客户端是否会发送其状态到服务器,或者客户端是否只会发送输入内容并等待服务器的新状态?

前一种方法是基于延迟自由和反应式,但却出现了一个有关信任的严重问题—-骗子可以改变客户端而在游戏中获取优势。例如客户端在技能方面具有控制力;发生改变的客户端只能通过告诉服务器它撞击每个人一千次而杀死游戏中的所有人,或它可以基于超速度升级玩家等等。

后者则避开了这些问题,但却会创造出武术延迟,这也就等于信息往返时间。

就像我在之前文章中所提到的,尽管你可以在真正拥有一款受欢迎的游戏时再去担心骗子,但我们仍可以事先采取一些预防方法。传统的方法是让服务器去控制游戏中的一切内容,并通过进行客户端预测而处理延迟问题。

以下是有关客户端预测和完整的服务器控制的相关资料:

http://gafferongames.com/networking-for-game-programmers/what-every-programmer-needs-to-know-about-game-networking/

http://fabiensanglard.net/quakeSource/quakeSourcePrediction.php

https://developer.valvesoftware.com/wiki/Latency_Compensating_Methods_in_Client/Server_In-game_Protocol_Design_and_Optimization

而我将在本文中分享不同的方法。这种方法带有一些优势:

非常容易理解且可以快速运行。

玩家永远不会因为延迟而看到自己的角色变得扭曲或结巴。

能够实现与刚体的复杂互动。

能够让游戏运行于3G网络中。

这一方法让服务器对于游戏中的任何内容都具有控制权—-除了玩家的位置和角度,因为这是基于客户端范围。这显然会引起有关骗子的信任问题,所以为了缓解这一问题,服务器将固定每个玩家的最高速度。如果玩家进行瞬间移动,这便等于他们在面对其它客户端和服务器时是基于正常速度移动,因此便否定了欺骗的利益。

为了执行这一技巧,我伴随着按键发送了客户端当前的位置和角度。

//
// handle player input message sending
//

if (keysDown != m_lastKeysDown)
{
m_playerInputDef.Initialise(keysDown, m_gameLogic.m_ThisPlayer.m_Pos, m_gameLogic.m_ThisPlayer.m_Angle);
Message.SerialiseAndSend(m_socket, MessageNames.kPlayerInput, m_playerInputDef.m_Annon);

m_lastKeysDown = keysDown;
}

当这一数据到达服务器(以及其它客户端)时,当前位置和新传播位置间将出现增量计算。一小部分增量将用于位置和角度的每一帧,然后增量将最终被耗尽。

所以修正内容主要用于这一应用的一开始,或至少在最后。不过从计算上来看它从未到达最后。

我们应该清楚在这时候,这只能用于修正目的;客户端和服务器都运行着同样的代码,即将对按键做出回应。所有的对象都是基于同样的方式跨越所有系统进行整合,所以客户端和服务器将无需这样的修改而进行粗略的同步。而这里所存在的问题是,“粗略”代表不是很理想,并且它们将因为缺少修改而出现飘移问题。

飘移的问题

尽管同样的游戏代码继续运行于客户端和服务器上,即基于同样的方法对按键做出反应,并基于同样的代码和同样的方法继续整合对象,而如果你不去修改它的话,你便仍会遇到飘移的问题。

飘移的出现源于多个元素,但最明显的还是客户端和服务器之间的代码差异性,或者因为硬件或特定语言执行细节所引起的数学计算结果,或客户端与服务器间的非同一时间步骤。

还有一个元素是延迟的不可预知性。例如,客户端为30次记录按下向前的按键然后松开。这是源自客户端到服务器的两个不同信息,按压按键和松开。因为延迟,服务器将在未来的某一时间接收到这两个信息,但问题并不在此;问题在于如果延迟从按压按键到松开间发生改变,那么服务器关于向前按压按键的记录数将不同于客户端!

在上述的例子中,我们是为了达到一致的记录数而按压按键,但延迟却会基于导致服务器上不一致的按压持续时间的尝试而发生变化。如果存在一个恒定的延迟时间的话这便不是问题所在,但如果具有较大的变量便会引起问题的出现。

通过每次按键发送客户端当前的位置和角度,这一飘移问题将得到纠正。

简单化

这一技巧的魅力就在于它非常简单。鉴于该系统的属性,玩家的位置/角度总是在服务器之前,所以当计算修正的增量时,它们从未即时呈现逐帧倒放,就像它们将带有完整的服务器控制技巧,所以总是能够顺畅地出现。

当然了,这一技巧也具有自己的缺陷:

如果玩家正在快速移动,那么因为延迟,客户端与服务器的位置差异将会因为过大而引起撞击检测问题。

带有较大延迟连接的玩家将在面向所有其它客户端检测时做出古怪的行为。

限定玩家的最大速度以减轻欺骗行为。

话虽这么说,但从业务角度来看最有可能阻碍你的游戏成功的元素还是未能即时完成。只要妥协方案是可被接受的,那么将你在开发中所节省的时间用于创造更棒的游戏便都是有价值的。

整合和时间步骤

我已经提到了客户端和服务器必须运行同样的代码。但我们还需要讨论下时间步骤,因为尽管我们拥有强大的修正系统去处理飘移问题,但事实上我们希望这些修正内容能够尽可能较小。

面对可变的时间步骤,基于较长的帧系列挪动的距离数每次都会出现变化,甚至是伴随着等速度。这在平台游戏中带有跳跃高度的角色上便特别明显;我还发现一个在关卡的某部分不能跳跃的角色却可以在另外一个关卡轻松地跳出同样的高度。

按键上的行动

所以让我们具体谈谈我是如何传达按键,以及伴随着这些按键服务器该做些什么。

首先,来自客户端键盘的按键代码将被转化为游戏内部的行动,并被整合到位元栏中。

var keysDown:int = 0;

// translate actual keycodes into logical actions
if (m_keyboard.IsKeyDown(eKeyCodes.kLeftArrow) || m_keyboard.IsKeyDown(eKeyCodes.kA))
{
keysDown |= Player.kLeftD;
}
if (m_keyboard.IsKeyDown(eKeyCodes.kRightArrow) || m_keyboard.IsKeyDown(eKeyCodes.kD))
{
keysDown |= Player.kRightD;
}
if (m_keyboard.IsKeyDown(eKeyCodes.kUpArrow) || m_keyboard.IsKeyDown(eKeyCodes.kW))
{
keysDown |= Player.kUpD;
}
if (m_keyboard.IsKeyDown(eKeyCodes.kDownArrow) || m_keyboard.IsKeyDown(eKeyCodes.kS))
{
keysDown |= Player.kDownD;
}
if (m_keyboard.IsKeyDown(eKeyCodes.kSpace) || m_keyboard.IsKeyDown(eKeyCodes.kCtrl))
{
keysDown |= Player.kSpaceD;
}

这一重测图的优势在于你可以在将来轻松地重新定义控制,且不会出现任何问题。

在服务器上,这些按键将基于与在客户端上一样的方式行动。

客户端:

/**
* Move the ship forward in time and accept player inputs
*
* @param dt
*/
public override function Integrate( dt:Number ):void
{

// handle player inputs
if ( IsKeyDown( Player.kLeftD ) )
{
m_AngularVel -= Player.kTurnRate*dt;
}
if ( IsKeyDown( Player.kRightD ) )
{
m_AngularVel += Player.kTurnRate*dt;
}
if ( IsKeyDown( Player.kUpD) )
{
m_Vel.AddTo( m_Direction.MulScalarTo( Player.kAccelerateRate*dt ) );
}
else if ( IsKeyDown( Player.kDownD ) )
{
m_Vel.SubFrom( m_Direction.MulScalarTo( Player.kDecelerateRate*dt ) );
}
}

服务器:

/**
* Move forward in time.
*
* Keep in sync with client!
*/
Integrate:function(dt)
{

// handle player inputs
if ( this.IsKeyDown( Player.Constants.kLeftD ) )
{
this.m_angularVel -= Player.Constants.kTurnRate*dt;
}
if ( this.IsKeyDown( Player.Constants.kRightD ) )
{
this.m_angularVel += Player.Constants.kTurnRate*dt;
}
if ( this.IsKeyDown( Player.Constants.kUpD) )
{
this.m_vel.AddTo( this.GetDirection().MulScalarTo( Player.Constants.kAccelerateRate*dt ) );
}
else if ( this.IsKeyDown( Player.Constants.kDownD ) )
{
this.m_vel.SubFrom( this.GetDirection().MulScalarTo( Player.Constants.kDecelerateRate*dt ) );
}
}

之所以在整合函数中处理这些输入内容是源于我正使用的整合方法,这通过基于调整时间步骤而调用每帧多次的整合去补偿帧率变量。

其它游戏内部事件

就像我之前所提到的,在本篇文章中,除了玩家的位置和方向,所有内容都是基于服务器那一面。同样地,事件将如下:

时间同步ping

玩家手上

玩家死亡

玩家再生

玩家加入

玩家离开

等等,这些都是通过服务器向客户端进行传播。每个信息都有其信息风格,而客户端带有一个信息处理块,能够处理这些信息中的数据和行动。

服务器也具有同样的信息处理块,并能够处理来自客户端的信息,包括:

时间同步ping

新玩家为世界国家做好准备

玩家按压按键

玩家发送聊天信息

玩家输入真名

玩家连接

玩家断开连接

玩家加入的游戏状态

在这一简单的例子中,当新玩家加入游戏时,整个游戏状态将持续下去,发送玩家加入信息,其他玩家便会知道有新玩家出现。

/**
* Process all messages received from all clients!
*/
OnMessage:function(client, message)
{
var thisPlayer = this.m_players[client.m_uid];

switch (message.n)
{

case MessageNames.kReady:
{
if (this.m_numPlayers < GameLogicConstants.kMaxPlayers)
{
//
// client is ready! serialise the world!
//

// get data for world
var world = [];
for (var key in this.m_players)
{
var player = this.m_players[key];
world.push( player.GetData() );
}

var defaultName = “Guest”+utils.PadNumber(client.m_uid, 4);
var newPlayer = new Player(client.m_socket, this.FindRandomSpawnLocation(), client.m_uid, defaultName);

this.AddPlayer(newPlayer);

// new player data for serialisation
var pd = {m_pos:newPlayer.m_pos, m_uid:client.m_uid, m_name:defaultName};

// send this data to the client
Message.SerialiseAndSend(client.m_socket, MessageNames.kReady, {m_world:world, m_you:pd});

// broadcast new player to all clients, except the one who is ready
this.m_server.BroadcastExcept( MessageNames.kCreatePlayer, pd, client.m_uid );
}
}

}
}

这一技巧适用于这一简单的游戏,但如果你拥有更大的世界,或者更多玩家,那么在带宽上它便会开始显得笨拙且昂贵。你可能想要着眼于像Interest Management这样的技巧在更大的游戏中处理这样的问题。就像我便在2D太空MMO游戏中使用了Interest Management。

游戏中的每个对象都有一个名为GetData()的方法,它将返回在客户端重头开始创造对象所需要的完整状态。以下是有关这种方法的代码:

/**
* Serialise this player
*/
GetData:function()
{
return {m_mod:this._super(),
m_uid:this.m_uid,
m_keysDown:this.m_keysDown,
m_lastKeysDown:this.m_lastKeysDown,
m_bulletTimer:this.m_bulletTimer,
m_health:this.m_health,
m_invincibleTimer:this.m_invincibleTimer,
m_kills:this.m_kills,
m_died:this.m_died,
m_name:this.m_name};
},

你可以看到这里有许多有关玩家的重要细节,包括他的UID,即在服务器上的唯一标识符,现在哪个按键是向下,哪个按键是基于最后一帧,哪个子弹计时器同步了客户端和服务器之间的子弹发射,健康,杀掉多少人,死亡数量及其名字。而位置,角度和速度则是储存在m_mod中,即源自描述所有移动对象的基础类。

当加入客户端收到信息并终断这一世界数据数组时,游戏状态将被同步,而状态中的任何更新都将由基于正常事件的信息系统所处理。对于其它客户端来说,呈现一个更简单的数据集将延续下去,并对新玩家进行描述:

// new player data for serialisation
var pd = {m_pos:newPlayer.m_pos, m_uid:client.m_uid, m_name:defaultName

因为所有玩家的加入状态都是同样的,除了UID,名字和位置以外,这些都是延续现有客户端的必要元素。举个例子来说吧,所有新玩家开始不再循环,具有完整的健康值,没杀过任何人也从未死过,这时候我们就不需要延续数据—-因为新玩家带有这些数值的默认值。

聊天

如果缺少让玩家相互交谈的方法,那便不存在任何一款完整的多人玩家游戏。幸运的是执行一个聊天系统非常简单;通过在客户端的一个文本框中阅读聊天信息,发送到服务器上,然后传到到所有客户端中,在此所有信息都被呈现于聊天窗口的适当位置上。

这里唯一需要担心的便是过滤不敬的言语。我们可以在客户端或服务器上做到这点,而我选择在客户端上执行。文本必须在呈现于信息接收前过滤好。原因很简单:想象存在一个已被破解的客户端,在这里核可删除了聊天过滤器;他能够打出未过滤的信息然后将其传播给所有客户端!

以下是一个简单的聊天过滤执行:

/**
* Replace the given character in the string
* @param str
* @param char
* @param index
* @return String
*
*/
private function ReplaceChar(str:String, char:String, index:int):String
{
return str.substr(0,index) + char + str.substr(index + 1);
}

/**
* Censor the given text string
*/
public function Censor(text:String):String
{
for each(var swear:String in m_naughtyWords)
{
var done:Boolean = false;

while (false == done)
{
// find start location of profanity
var lowerString:String = text.toLowerCase();
var startPos:int=lowerString.indexOf( swear );
if (startPos == -1)
{
// no more occurances
break;
}

// length is
var length:int = swear.length;
var end:int = startPos+length;

// replace
for (var i:int=startPos; i<end; i++)
{
text = ReplaceChar(text, “*”, i);
}
}
}

return text;
}

它用星号替换了不敬言语的子串,并返回了过滤后的字符串。

值得指出的是,储存任何未过滤的数据是有意义的(游戏邦注:例如在数据库中),因为在此之后你将能够自由地改变过滤内容并仍拥有最初的数据。

与聊天一样的是,你可以在聊天窗口中呈现游戏事件,就像在这个例子中杀人事件便与聊天内容一起呈现出来。

Chat text with game events(from wildbunny)

Chat text with game events(from wildbunny)

Bot

在在线演示版本中有两个bot,它们都是服务器那一端的bot,所以相当于其他现实玩家,除了他们的技能水平(在大多数情况下是如此)!

这是通过为每个bot(运行于服务器面)创造一个虚假客户端而进行处理。每个bot基于与真正的客户端同样的做法创造一个套接字连接到服务器,但比起完整的客户端,bot所运行的代码却相对较小。

的确,以下是bot的完整代码:

require(‘./Scalar’);
require(‘./Player’);
require(‘./Message’);

// import node.js net library
var net = require(‘net’);

/**
* Class to emulate a client
*/
Bot = Class.extend(
{
Init:function(host, port, uid)
{
this.m_host = host;
this.m_port = port;
this.m_player = null;
this.m_lastKeysDown = 0;

var scope=this;

this.m_socket = net.connect(port, host, function()
{
Message.SerialiseAndSend(scope.m_socket, MessageNames.kReady, {uid:uid});
});
},

Update:function()
{
if (this.m_socket && this.m_player && Scalar.RandInt(50)==0)
{
var keysDown = Scalar.RandInt(15);

// always fire
keysDown |= Player.Constants.kSpaceD;

if (this.m_lastKeysDown != keysDown)
{
Message.SerialiseAndSend(this.m_socket, MessageNames.kPlayerInput,    {m_keysDown:keysDown,
m_pos:this.m_player.m_pos,
m_angle:this.m_player.m_angle});
this.m_lastKeysDown = keysDown;
}
}
}
});

就像你所看到的,AI的完整范围包括基于1/51的概率发送一个完全随机的按键状态改变!因为知道这有多简单,所以我经常会被它们的现实性给惊讶到。这将呈现能够帮助模拟某些现实行为的随机事件。

当第一个玩家进入游戏中时bot便被创造出来,而当没有任何玩家离开时bot便会被摧毁。在PvP游戏中,设有bot很重要,因为很多时候并没有许多真正的玩家在玩游戏,而这可能会导致较糟糕的游戏体验和负面的评论。

玩家个性化

让玩家能在多人游戏中进行个性化选择非常重要。最简单的方法便是让他们输入自己的绰号。在这个例子中,这与处理其它信息的方法是一样的;它将传播到所有客户端上,之后这些客户端将更新改变后的名字副本。服务器也将储存这一新名字,如此之后的玩家将能在加入游戏时延续下去。当过滤了不敬言语后,名字将被呈现在每个玩家面前!

Avatar customisation in 2D Space MMO(from wildbunny)

Avatar customisation in 2D Space MMO(from wildbunny)

简单的数据追踪也能够让游戏变得更有趣,就像玩家拿自己与别人做比较的方法一样。在伴随着本文的演示版本中,玩家名字后面的括号指的是玩家死亡的次数减去杀人的次数。

版本控制

当你创造了自己的多人游戏,并将其推广到各种门户网站上时,你必须对其进行更新并修改所发现的任何漏洞,并且无需在每个门户网站上查找并改变每个游戏副本。你可以通过创造一个boot-loader(能够从你所控制的固定位置上加载游戏的主要部分)做到这点。当然了,关于这一点具有一些专门术语,特别是浏览器缓存。

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

How to make a multi-player game – part 2

Posted on November 20, 2012 by Paul Firth

Hello and welcome back to my blog! This is part 2 in the series where I talk about making a multi-player game.

Last time we built a TCP socket server in node.js and we’re able to send and receive complex types. Read the first article if you’ve not already done so here.

Here is a live version of the game I’m describing in this series:

Clock synchronisation

It’s important that both the client and server’s clocks are synchronised because if there is any time based interpolation, you want both server and client to agree on what time it is and therefore at what position your interpolated object is.

Asteroids on an interpolation orbit

I use this technique in 2D Space MMO to ensure the orbiting asteroids are in the same position across all clients and on the server. The asteroids are actually interpolating on an orbit around a central location – there are no update messages getting sent to correct their positions, the only thing which keeps them synchronised is having the same time value across all clients and server.

Before we can try to synchronise clocks, we need to be sure we’re using the same concept of time on both server and client. We want to calculate the number of seconds since 1970/01/01, which is the same in both javascript and actionscript:

The way to synchronise clocks is to first work out how long it takes a message to do a round-trip from client->server->client, like a ping, essentially. We do this by sending a message which contains the client’s local time. The server will then reply with the exact data it received and also an additional parameter which is the time on the server.

When the message arrives back on the client, the client can subtract the old client time in the message from the time ‘now’ on the client, thereby arriving at a total round-trip time. Taking half of this round-trip time we arrive at an estimate of the all important latency value. Latency is how long it takes for our messages to arrive on the server.

Using the second part of the reply from the server (the server’s time), we can then compute an offset which will account for any time-zone differences.

In reality we don’t just do this synchronisation once, because it is subject to error based on the current quality of the connection between the client and the server – imagine the client is on a 3G network travelling on a train, the round-trip time might vary considerably from attempt to attempt based on the proximity and line of sight to the nearest cell tower. In order to combat this problem, we continuously synchronise and take an average value.

In this code I’m using a simple moving average. You might want to do something more advanced like store more data points instead of just the last value and then filter out any outliers by comparing against the median of the dataset.

Flash and the socket policy file
Before we go any further it’s worth mentioning how Flash works when connecting to a socket server. The Flash client will send a request for a policy file when it first connects to a socket server; this policy file tells Flash which domains and ports the socket server accepts Flash connections on. You can read more about socket policy files here.

Flash will send a request which looks like this:

<policy-file-request/>

It will do this first on port 843 and then on the port you chose to connect with the server.

The server must respond with a valid policy file which must be terminated with a null character. Here is the function I use to form the policy response in node.js:

I’ve found that although the majority of Flash clients do request a policy file, there are some which wont so you have to be able to deal with both cases to be completely robust.

Synchronising state in a twitch reaction game
At this point it’s important to talk about exactly how we can go about designing the system to be responsive enough to handle a twitch reaction game. There are primarily two different ways of synchronising state in a multi-player game (by state I mean position, orientation, velocity and other attributes of in-game objects):

Periodically send updates about all objects which have changed state

Send updates only when an event occurs

The first method sends data at a fixed interval about all objects which have changed state. This introduces a lag which is equal to the time between state updates, but ensures a steady flow of state information which is not subject to flooding.

The second only sends state when an event occurs, such as a key being pressed. This has minimal lag but can be subject to flooding; for example if the player starts hammering keys.

I chose to use an event based method because the response times are important in a twitch reaction game, and also because it allows me to handle the problem with bullets.

The problem with bullets
Imagine if every single bullet fired in game caused a message to be broadcast? This would soon overwhelm the server’s available bandwidth and start leading to nasty lagging issues. To solve this problem I simply send key-presses from client to server and give the weapon auto-repeat so that the player is either firing or not firing based on which keys are currently down on the client.

If I detect key hammering in the game (which is quite a natural behaviour for this type of game), I put up a message telling the player they can hold down fire instead.

When the player is firing bullets, bullets are emitted at a constant rate on both client and server and there is a synchronised timer which ensures that both client and server fire bullets at the same moment in time.

Client side prediction and server authority
Does the client send its state to the server, or does the client send inputs only and simply await the new state from the server?

The former approach is lag free and responsive but presents a serious problem with trust – the client could have been altered by cheaters to gain an advantage in game. Imagine if the client had authority over kills, for example; an altered client could simply kill everyone else in game by telling the server it had hit everyone a thousand times, or it could upgrade the player with super-speed etc.

The latter avoids these problems but introduces an unacceptable amount of lag which is equal to the message round-trip time.

As I mentioned in the last article, although you don’t need to worry about cheaters until you actually have a popular game, it’s worth taking preventative steps ahead of time. The traditional method is to simply have the server be in authority over everything in game and to cope with the lag by doing client side prediction.

Here are some reading materials which cover client-side prediction and complete server authority:

http://gafferongames.com/networking-for-game-programmers/what-every-programmer-needs-to-know-about-game-networking/

http://fabiensanglard.net/quakeSource/quakeSourcePrediction.php

https://developer.valvesoftware.com/wiki/Latency_Compensating_Methods_in_Client/Server_In-game_Protocol_Design_and_Optimization

I’m going to talk about a different method in this article. It has a number of advantages:

Incredibly simple to understand and quick to get running

Player never sees their character getting warped around or stuttering due to lag

Enables complex interactions with rigid bodies

Enables the game to be played acceptably over a 3G network

This method has server-side authority over everything in game, except the player’s position and angle, which are client sided. This obviously raises a trust concern with cheaters teleporting around the level, so to mitigate this the server (and other clients) simply clamp the maximum speed of each player. Then if a player teleports, it’s equivalent to them moving there at normal speed for all other clients and the server, thereby negating the benefit of the cheat.

In order to implement this technique I send the client’s current position and angle along with key-presses.

When this data arrives at the server (and the other clients) a delta is computed between the current position and this new broadcast position. A fraction of this delta is then applied each frame to the position and angle, and then the delta is depleted by that amount.

So the correction is applied most at the beginning of its application and least at the end. In fact it never reaches the end, technically.

It should be noted at this point that this is indeed only used for correction purposes; both the client and the server all run the same game code which responds to key-presses. All objects are integrated in the same way across all systems so client and server will be roughly in sync without this correction. The problem is, ‘roughly’ is not good enough and they will start to drift off without it.

The problem with drift

Although the same game-code runs on client and server, which responds to key-presses in the same way, integrates objects forward in time the same way using the same code, you will still get problems with drift if you do nothing to correct it.

Drift occurs due to a number of factors, the most obvious being accidental differences in the code between client->server, or differing mathematical result of calculations due to hardware or language specific implementation details, or a non identical time-step between client->server.

The other one is due to the unpredictable nature of lag. For example, the client holds down the forward key for 30 ticks and then releases it. That’s two different messages getting sent from client to server, one key down and one key up. Due to lag, the server will receive these two messages at some time in the future, the problem isn’t that; the problem is that if the lag changes from key-down to key-up, the number of ticks the server will have the forward key down for could be different than the client!

In the above example a key is held down for a consistent number of ticks each time, but the lag varies on each attempt resulting in a inconsistent key-down duration on the server. This wouldn’t be a problem if there was a constant amount of lag, but in reality it varies enough to cause problems.

By sending the client’s current position and angle with each key-press this drift can be corrected as described before it gets significant.

Simplicity

The beauty of this technique is that it’s incredibly simple. The player’s position/angle is always slightly ahead of the server because of the nature of this system, so that when the correction deltas are computed they never represent a step backwards in time, like they would with a fully server authoritative technique so always appear smooth and un-jarring.

Of course, this technique does come with its own disadvantages which it is only fair to discuss:

If the player is very fast moving, the difference in position on client->server due to lag can be large enough to cause problems with hit-detection

A player with a very laggy connection will appear to behave oddly for all other clients observing

The maximum speed of the player must be clamped to mitigate cheating

That being said, from a business point-of-view the thing most likely to stop your game being a success is it not being done in time or at all. As long as the compromises are acceptable, any time you can save during development which could be spent on actually making the game better is extremely valuable.

You can read more about my love of simplicity in game development in this article on how to make games.

Integration and the time-step

I’ve already mentioned that the client and server must run the same code. It’s worth talking about the time-step, because although we have a very robust correction system to deal with drift, in reality we want those corrections to be as small as possible.

With a variable time-step, the amount of distance actually moved over a long series of frames will be different each time even with a constant velocity. This is especially noticeable with the jump-height of a character in a platform game; I’ve seen it be pronounced enough that a character won’t be able to make a jump in one part of the level which he has no trouble making in another for the same height.

I’m using the method Glenn Fiedler recommends near the bottom of his article on the subject.

Acting on key-presses

Ok, so lets talk about exactly how I transmit key-presses and what the server does with them.

Firstly, the key-codes from the client’s keyboard are transformed into in-game actions and packed into a bit-field. You can read more about bit-fields in this article I wrote about understanding binary.

The advantage of this remapping is that you can easily redefine the controls at a later date with no problems.

On the server, these key-presses are actioned in the same way as they are on the client.

Client:

Server:

The reason these inputs are processed inside the Integrate function is due to the integration method I’m using, which compensates for frame-rate variation by calling Integrate multiple times per frame depending on the adjusting time-step.

Other in-game events

As I mentioned before, everything except player position and orientation is server-sided in this article. As such, events like:

Time synchronisation ping

Player takes damage

Player died

Player respawned

Player joined

Player left

…are all handled by the server broadcasting to the clients. Each message has its own message type and the client has a message processing section which deals with the data and actions represented in these messages.

The server has a similar message processing section and deals with messages from the client, which include:

Time synchronisation ping

New player ready for world state

Player pressed a key

Player chat message

Player entered his real name

Player connected

Player disconnected

Game state on player join

In this simple example, when a new player joins the game, the entire game state gets serialised and sent to the joining player and all other players get a message that a new player has joined the game.

This technique works fine for this simple game, but if you have a much larger world, or many more players it will start to get bulky and expensive on bandwidth. You might want to look at techniques like Interest Management to handle this problem in larger games. I use Interest Management in 2D Space MMO.

Each object in game has a method called GetData() which returns the complete state required to create the object from scratch on the client. Here is what this looks like for the players:

You can see there are the vital details for the player, including his UID which uniquely identifies this object on the server, what keys are down now and which were down last frame, the bullet timer which synchronises the firing of bullets between client and server, health, number of kills, number of deaths and his name. The position, angle and velocities are actually stored inside the m_mod member which is inherited from the base class which describes all moving objects.

When the joining client receives and has deserialised this array of world data, the game state is synchronised and any further update in state will be handled by the normal event based messaging system. For the other clients present a more simple set of data is serialised which describes the new player:

Because the joining state of all players is identical, bar the UID, the name and the position, these are the only things which are necessary to serialise to the existing clients. For example, all new players start un-rotated, all have full health, zero kills and have died zero times so there is no need to serialise that data – new players have defaults for those values.

Chat

No multi-player game would be complete without some way for players to chat with each other. Luckily implementing a chat system is very simple; chat messages are read from a text-box on the client, sent to the server and then broadcast to all clients, where upon the message is displayed at the appropriate location in the chat window.

The only thing to worry about is filtering the text for profanity. This can be done on either client or server, I’ve chosen to implement this on the client. Text must be filtered before being displayed on reception of the message rather than before transmitting it from the source. The reason is simple: imagine a hacked client exists where the hacker has removed the chat filter; he would be able to type unfiltered messages which then get broadcast to all clients!

Here is a simple chat filter implementation:

It replaces sub-string occurrences of profanity with the asterisk character and returns the filtered string.

It’s worth pointing out that it makes sense to store any data unfiltered (in a database, for example) because you are then free to change the filter and will still have the original data to work with.

As well as chat, you can display game events in the chat window, as in this example whereby kill events are displayed with the chat.

Bots

There are two bots in the live demo which accompanies this article, they are server-side bots so are genuinely equivalent to other real players in all things accept their level of skill… in most cases!

They are handled by creating a fake client for each bot which runs server side. Each bot creates a socketed connection to the server in exactly the same way a genuine client would, but the code the bot runs is comparatively tiny compared to the full client.

Indeed, here is the full code of the bot:

As you can see, the full extent of their AI involves sending a completely random key-state change at a random point at a probability of 1/51! Given how simple this is, I’m constantly surprised by how realistic they can seem. It goes to show that random events can go a long way toward emulating real behaviour in some cases.

Bots are created when the first player joins the game and are destroyed when there are zero real players left. In a PvP only game, it’s important to have bots because a lot of the time there wont be that many real players playing, which will lead to a bad play experience and bad reviews.

Player personalisation

It’s very important to be able to let players personalise themselves in a multi-player game. The simplest way to do this is to allow them to enter their own nickname. In this example it’s handled the same way as any other message; it gets broadcast to all clients who then update their copy of the changed name. The server also stores this new name so that subsequent players will have it serialised to them when they join. The name is displayed on each player after being run through the profanity filter!

Simple stats tracking can also help make the game more fun, like a way for players to compare themselves against others. In the demo accompanying this article the number in brackets after the player’s name is the number of times the player has died subtracted from the kill count.

Version control

Once you’ve made your multi-player game and distributed it onto the portals, it’s very important that you are able to update it and fix any bugs you find without having to manually locate and alter each copy of the game on every portal. You can do this by creating a boot-loader which loads the main part of the game from a fixed location which you control. Of course there are technicalities associated with this, not least of which is browser-caching. You can read more about how to solve this in this article I wrote a while back.

Buy the source code

As ever you can buy the source-code accompanying this article! Your purchases help me to be able to continue writing articles like these.

It will give you the complete prototype as shown in playable form above, with both server and client code. You will need either Flash Develop or Amethyst to build the client-side code and of course you will need node.js installed to run the server-side. You will also need Flex SDK version 4.5.1 or above. If you would like to edit the assets included with this demo, you will need Adobe Flash CS4+. Note that you cannot build the client side code with only the Flash IDE.

It comes in two versions, a personal edition which you are free to use for your own, non commercial purposes and a commercial version which allows you to use the code in any number of different commercial products or games:

Personal use licence – USD 49.99

Commercial use licence – USD 199.99

Subscribers can access the source here

If it seems expensive, bare in mind that it took a couple of solid weeks of programming to produce, which would have been around $3500 if I were contracted… Not to mention the many weeks and days it took to arrive at a solution powerful enough to handle a twitch reaction game over a 3G connection!

That’s all for now! Until next time, have fun!(source:wildbunny)


上一篇:

下一篇: