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

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

发布时间:2014-01-21 15:44:38 Tags:,,,

作者:Paul Firth

我已经有一段时间没发博文了,因为我正投入制作一款名为《mmoAsteroids》的多人游戏。本文主要讲述我在制作这款游戏过程中所学到的最关键要点,以及我其他的多人模式原型。(请点击此处阅读本文下篇

引言

首先要说明的是我对多人模式的定义及其含义。这里我要说的是互联网上的非本地多人模式,而非处于同一台电脑上的游戏模式。

当然,这意味着游戏中要有一些允许玩家相互沟通的渠道。我选择了服务器端模式而非点对点模式,因为这是我的目标平台客户端Adobe Flash所支持的。

客户端

为何选择Flash?因为:

*安装基础很庞大,即使是与HTML5相比较

*这是一个可靠的平台,你并不需要担心不同浏览器或不同硬件的问题

*Flash端口是一个发布游戏的出色资源

*这里有极为成熟的开发环境,支持通过Visual Studio进行完整的调试

*它支持创建于内部的TCP套接口

服务器端

因为我使用了客户端-服务器模式,我就必须选择一个支持服务端代码的平台。

我选择了node.js,只是因为它是获得正常表现服务器设置,以最少的代码运行的最快方法。此外,你用自己的服务器就不存在可同时支持的客户端上限的问题,这一点不同于Player.io或SmartFoxServer等现成的程序包,它们将你的用户数量限制在一定范围内,除非你注册其付费服务。

当然,它们的付费服务也提供了更多功能,所以要妥当衡量成本和你所选择平台的好处。

client Server(from wildbuny)

client Server(from wildbuny)

基于服务器-客户端的多人模式游戏如何运行?

简单地说,客户端和服务器通过彼此发送信息进行交流。客户端可能会向服务器发送一个声明它想前进的信息。服务器将收到信息并作出相关回应。服务器可能会发送信息志明玩家已经收集到了一个能量提升道具,或者有其他人加入了游戏。

客户端和服务器都保存了游戏世界的备份,服务器拥有主要备份,而客户端则(可能部分)拥有本地模拟和展示用途的备份。服务器掌握了所有游戏重要决策的权限,例如玩家被子弹击中,或者被枪杀,或者升级了。客户端则掌管人们所摁压的键,以及其他用户输入数据。

实行这种权力分配是为了避免作弊现象:虽然只有当你的游戏大到足以令黑客花时间寻找作弊方法的程度时,这种权力分配才会派得上用场,但还是有必要提前做点简单的准备,避免被入侵的客户端将黑客杀死游戏世界中的所有人等类型的信息传递给服务器。

warcraft Fail(from wildbunny)

warcraft Fail(from wildbunny)

让服务器掌握权限可以避免这种情况发生。

MMO?MO?

在我们继续深入探讨之前,很有必要再区分一下不同的多人游戏类型,因为这种游戏类型对你所需要编写的代码量有极大的影响。

先从其中最简单的模式开始,我将把MO或称多人在线游戏定义为在一台服务器上(连接互联网的一台实体电脑)尽量支持更多玩家的多人游戏。

MMO则是大型多人在线游戏,它们可能采用多种形式,但几乎所有的MMO游戏都不止一台实体服务器来处理庞大的负荷。

它们如何共同运行将决定游戏的设计,例如,你可能希望游戏世界支持所有玩家同时共享(如《Eve Online》),这要求多个服务器一起形成一个大型碎片来分担负荷。你可能还会想以你的游戏世界为实例,以便每个服务器都能真正把握游戏世界的一个备份,同一个世界的玩家也相互看不到彼此,例如《Realm of the Mad God》。但你可能还会想将两者结合在一起,将游戏世界划分为多个区域,这样玩家就看不到另一个区域的人,但他们却可以在这些区域间穿梭,比如《魔兽世界》的做法。

realm of the mad god(from wildbunny)

realm of the mad god(from wildbunny)

制作一款拥有非实例化世界,真正的MMO游戏是一项庞大的任务。即使是那种拥有多个区域并在其中穿行的游戏也相当复杂。

你实例化的世界越多,就越容易创造其结构,因为你可以在游戏扩展时简单地将实例分配给新服务器。如果玩家无法在这些实例之间沟交流,情况仍然会更简单。这正是《mmoAsteroids》目前所用的框架。每个世界都可以容纳20名玩家,每个服务器一次可支持10个实例,前提是每个服务器的在线玩家容量是200人。

我将在本文描述的游戏类型是MO,因为这是最容易编码的类型。你完全有可能将这种游戏转变为高度实例化的MMO。

TCP vs UDP

由于Flash仅支持TCP,因此我们就不难在TCP或UDP这两种通信协议中做出选择了。这对于速度非常快的多人游戏来说看似一个大问题,因为TCP拥有可靠、按次序交付的政策,尤其是拥有Nagles算法(它可以在发送之前将不同的数据包整合到一个更大的有效载荷,这可能引进延迟的问题)UDP在这一点上与之截然不同,也没有提供类此功能,但也不会遇到这类问题。但是,例课堂TCP还是可以禁用Nagle,从布消除这种延迟限制。

最重要的是,任何开始使用UDP的游戏都将植入自己版本的TCP的可靠、按次序交付政策,如果一旦禁用了Nagle,在粗速度方面TCP和UDP这间就没有那么大区别了。

tcp-UDP(from wildbunny)

tcp-UDP(from wildbunny)

要求

如果你想跟随本文的步骤制作自己的服务器端代码,那你就必须下载和安装node.js。在原型设计阶段你不需要真正的服务器,你可以在本地运行一切内容——这是一种更快更简单的开发方法。

当你想真正配置的时候,你可能需要自己的VPS,或者注册node.js托管供应商的服务(游戏邦注:现在可供选择的服务供应商非常多)。

游戏

为便于本文描述,我选择重塑《mmoAsteroids》的原型版本。对我来说,这款游戏很有趣,因为它是一款节奏非常快、极具响应性的街机游戏。它并不是那种典型的多人游戏所采用的题材(但《Continuum》和《Realm of theMad God》是例外),也许是因为这类游戏在互联网上很难发挥良好的表现。

原型设计极其重要,也是你想到要做什么游戏时应该首先做好的事情。其目标是设置好游戏的核心环节,并尽快投入测试,因为会阻碍游戏走向成功的重要因素就是无法完工!

以下就是这款游戏的原型设计:

prototype(from wildbunny)

prototype(from wildbunny)

何处入手?

多人游戏的基本元素是服务器。在这个例子中,它就是套接字-服务器,因为客户端会通过套接字进行连接。一个套接字只是同个网络上的电脑之间进行持续通信的接入渠道。这一点不同于HTTP,它是一个无持续连接的请求/回应协议。

对于这种游戏来说,持续连接至关重要,因为回应时间如此重要——我们无法等待数秒来自服务器键盘输入的响应。

套接字服务器

针对套接字-服务器,我将使用创建于node.js内部的net程序包,因为它提供了简洁的界面以及一个良好的起点。有经验的读者可能会困惑为何我不选择使用可支持WebSocket的socket.io程序包。原因是我想更多地控制信息从服务器传送的方式,但很难找到Flash执行完整的网络套接字协议的合理方法。

如果你为自己的客户端选择使用javascript,你就应该使用socket.io,因为javascript自己没有本地TCP套接字支持,所以最适合WebSockets。

最简单的套接字服务器

以下就是node.js最简单的套接字服务器例子:

var port = 8000;
var host = “localhost”;
var net = require(‘net’);

var server = net.createServer( function(socket)
{
socket.on(‘data’, function(data)
{
console.log(“data received: ” + data);
});

socket.on(‘close’, function(socket)
{
console.log(“close received”);
});

socket.on(‘timeout’, function(data)
{
console.log(“timeout received”);
});

socket.on(‘error’, function(data)
{
console.log(“error received”);
});
});

server.listen(port, host);

这确实足以证明你可以用这样几行代码创造一个套接字服务器。

实现createServer()的第一个参数就是闭合差,它在连接上客户端时就会被调用。在这个闭合差内,我们会附上不同的监听器来处理node.js向我们发送的事件,例如结束(当客户端不连接时),或者数据(当客户端已经传送一些数据)。在最后一行我们开启服务器监听连接情况,使用我们想要的端口和我们想监听的主机地址。

要选择哪个端口完全取决于你,尽管你可能需要在市场调查上投入一点精力,确认大多数玩家是否能够通过你所选择的端口连接网络。许多玩家可能在上班,或者被防火墙所禁止连接到非标准端口数据。我为《mmoAsteroids》选择的是443,这通常用于HTTPS协议,我发现这个端口能够让我接触最大的玩家数量。

数据事件

以上服务器最有趣的地方在于数据事件,它会显示数据已抵达处理——这是客户端和服务器的主要通信试。注意这并不意味着信息已经到达,而是指有些数据已经处于可读状态。这可能是来自客户端的部分信息,也可以是汇集到一起的信息。原因是TCP是一个流导向的协议,而不是信息导向的协议。我们应该做点工作在这个基本上创造我们自己的信息协议。

信息协议

我们希望从客户端收到每条信息时可以获得服务器的通知。为了实现这一点,我们必须设计一个简单的信息协议。在此教程中我选择的是基于UTF8,用一个终结字符分开每个信息的简单协议。

图1(from wildbunny)

图1(from wildbunny)

图1显示了简单的信息例子和终结字符(游戏邦注:这是指/n,或者ASCII代码10这个新行字符)。当然,这意味着信息无法将/n纳入信息的一部分,因为这会混淆协议。

图2(from wildbunny)

图2(from wildbunny)

图2显示了在message\n和message\n这两条分隔信息中可能遇到的1-5列独立数据事件。这可以作为我们必须解决的一个处理类型示例。

所幸这类分隔信息在带有字符串时特别简单,因为javascript和actionscript在字符串中都有split()函数,这会根据一个分隔字符将输入字符串划分成块,将我们的信息有效分隔出来。如图2显示,这并没有那么简单,因为并非所有数据事件都会产生一个带有终结符的字符串。我们在前进时应该缓冲字符。

var server = net.createServer( function(socket)
{
//
// this gets called once for each connection we have from a client…
//

// utf8 encoding
socket.setEncoding(‘utf8′);

// no data received yet
var socketData = “”;

//
// attach data handler
//

socket.on(‘data’, function(data)
{
socketData += data;

var substrings = socketData.split( “\n” );
var lastMsg = substrings.length-1;

if ( substrings[lastMsg].length!=0 )
{
// partial data read, store for later
socketData = substrings[lastMsg];
}
else
{
// full read, clear buffer
socketData = “”;
}

// process all messages
for ( var i = 0; i<lastMsg; i++ )
{
var message = substrings[i];

// process message!
}
}
});

以上代码就是我们实际所需的内容。在数据处理器中,其接收的数据最先缓冲:

socketData += data;

然后它划分成子字串:

var substrings = socketData.split( “\n” );

这会返回最少为1个条目的阵列。然后我们可以查看此时是否有部分可读的数据:

var lastMsg = substrings.length-1;
if ( substrings[lastMsg].length!=0 )
{
// partial data read, store for later
socketData = substrings[lastMsg];
}

我们可以用最后一个子字串来代替缓冲数据。如果这不代表我们可以完整读取数据,我们就可以完全清除缓冲数据:

else
{
// full read, clear buffer
socketData = “”;
}

然后我们就可以继续处理任何以及目前所有接收到的数据:

// process all messages
for ( var i = 0; i<lastMsg; i++ )
{
var message = substrings[i];

// process message!
}

我们在客户端编写相同的代码,现在我们就有一个可行的信息协议了!

连载

现在我们很高兴地在客户端收发字符串信息,但这有什么用处呢?我们应该能够发送数据,而不仅仅是字符串!

对于javascript和现在的actionscript,Flash Player 11.0已经含有针对行业标准数据连载/反连载格式JSON的植入支持。行业中还有其他可行的连载格式,但这个是最快加入运行的。你应该花些时间调查,确定哪个是最符合你本人需求的格式。

在javascript中将一个对象连载到JSON就像下面那么简单:

var messageString = JSON.stringify(data) + Message.Constants.kMessageTermintor;

要注意组成我们部分协议的附加信息的终结器。在actionscript中它几乎是相同的:

var message:String = JSON.stringify(data) + kMessageTermintor;

反连载也很简单:

var data = JSON.parse(messageString);

var data:Object = JSON.parse(messageString);

唯一的小问题就在于反连载数据一直是令人讨厌的对象,不是具体类型。这在javascript中并不是什么大问题,因为这正是语言的运行方式,但actionscript却是一种静态型的语言,我们可以利用这一点在编辑过程中排除漏洞,以免它们在运行时间中突然冒泡。

为了在actionscript中反连载为具体类型,我们必须做些额外数据。

package Code.System
{
import flash.utils.*;
import flash.geom.*;
import flash.system.*;
import flash.display.*;
import flash.net.*;

public class Helpers
{
/**
*
* @param obj
* @return Class
*
*/
static public function GetClass( obj:Object ):Class
{
return Class( getDefinitionByName( getQualifiedClassName( obj ) ) );
}

/**
*
* @param obj
* @return String
*
*/
static public function GetClassName( obj:Object ):String
{
return getQualifiedClassName( obj );
}

/**
* Take a annoymous type and try to copy the properies over into the given concrete type
*
* @param source Annoymous type
* @param targetType Concrete type
*
*/
static public function CloneIntoR( source:Object, targetType:Class ):*
{
Assert( source!=null, “CloneIntoR(): source is null! Type=” + GetClassName(targetType) );
var data:* = new targetType( );

for ( var prop:Object in source )
{
Assert( data.hasOwnProperty( prop ), “Helpers.CloneInto(): supplied type didn’t have required property ” + prop );

try
{
data[prop] = source[prop];
}
catch ( e:Error )
{
data[prop] = CloneIntoR( source[prop], GetClass( data[prop] ));
}
}

return data;
}

/**
* Import an annoymous type from the server into a concrete type of the client
*
* @param source Annoymous type
* @param targetType Concrete type
*
*/
static public function ImportServerDef( source:Object, targetType:Class ):*
{
var data:* = CloneIntoR( source, targetType );
data.PostFixUp( );
return data;
}
}
}

上述函数Helpers.ImportServerDef() 的作用就在于此。它甚至可用于递归存储复杂类型。你可以像这样使用它:

var psd:PlayerSpawnDef = Helpers.ImportServerDef(data, PlayerSpawnDef);

在这种情况下,PlayerSpawnDef可以代表当游戏中出现新玩家时,客户端希望从服务器接收到的数据:

来自服务器的信息:

{“m_pos”:{“m_x”:44.179,”m_y”:-426.812},”m_uid”:10003,”m_name”:”Guest10003″}

具体类型:

public class PlayerSpawnDef extends BaseDef
{
public var m_pos:Vector2;
public var m_uid:uint;
public var m_name:String;

public function PlayerSpawnDef()
{
m_pos = new Vector2();
}
}

这可以形成针对每个信息独立名称的严格类型检查,从而避免打字而造成的漏洞。这对于任何比这个简单的原型更大的游戏来说都是极为重要的。

构造器创造一个空的Vector2()原因在于Helpers.ImportServerDef()会知道它应该导入的类型——如果某个变量是无交的,那就没有必要在actionscript运行时间中去确定该变量类型。

信息

现在我们已经能够通过我们的信息协议发送对象和数据,让它们在客户端反连载成具体类型。那么客户端和服务器如何真正了解自己收到了什么类型的信息?这可能是创造了一名新玩家的信息,或者回应玩家输入的信息,也可能是其他类型的信息。我们应该预先将信息包装到一个特定的容器类(它描述了信息类型以及信息数据)中。

package Code.Messages
{
public class MessageContainer
{
/** Message name */
public var n:String;

/** Message string */
public var s:String;

/** Message data – to be filled in by client */
public var m_data:*;
}
}

以上就是我的处理方式。所有信息都要编码到一个容器中,而后者则包含信息名称和信息字符串本身。例如,为了节省带宽,信息名称可能定义为以下这种单个字母:

/**
* Must be kept in sync with server!
*/
public class MessageNames
{
static public const kTime:String = “a”;
static public const kCreatePlayer:String = “b”;
static public const kReady:String = “c”;

}

含有容器完全编码的信息如下所示:

“{“n”:”c”,”s”:”{\”m_pos\”:{\”m_x\”:362.597,\”m_y\”:9.912},\”m_uid\”:10005,\”m_name\”:\”Guest10005\”}”}”

所以,信息名称是“c”而信息本身则是:

“{\”m_pos\”:{\”m_x\”:362.597,\”m_y\”:9.912},\”m_uid\”:10005,\”m_name\”:\”Guest10005\”}”

而解码容器字符串时:

package Code.Messages
{
import flash.net.*;
import flash.utils.*;

import Code.System.*;
import Code.Maths.*;

/**
* Static class members to handle message sending and reading
*/
public class Message
{

/**
* Deserialise a message from a string
*
* @param messageString
* @return MessageContainer
*
*/
static public function Get(messageString:String):MessageContainer
{
var containerA:Object = JSON.parse( messageString );

if ( containerA.s!=undefined )
{
containerA.m_data = JSON.parse( containerA.s );
}

// clone into concrete type
var container:MessageContainer = Helpers.CloneIntoR( containerA, MessageContainer );

return container;
}
}
}

我可以调用:

var messageContainer:MessageContainer = Message.Get(messageString);

这样可以得到信息容器,这样可以得到解码信息以及像之前描述一样,被挑选并被信息协议分隔成一个信息的名称。之后我可以简单地询问信息类型,并合理地反连载内部数据:

switch (messageContainer.n)
{
case MessageNames.kReady:
{
// unpack and create the world

}
break;

case MessageNames.kCreatePlayer:
{
// deserialise the message data into a concrete type
var psd:PlayerSpawnDef = Helpers.ImportServerDef(messageContainer.m_data, PlayerSpawnDef);

}
break;
}

注意:内部信息数据无法反连载成正确的具体类型,除非信息类型已经通过信息名称而被游戏代码所理解,正如上述创造一名玩家的例子所示。

现在,我们已经有了一个套接字服务器,可连载和反连载的信息协议,确定两端所收到信息类型的方法。现在我们所需要的就是游戏本身了!

但这是我们下篇文章的内容,敬请期待!

原文发表于2012年10月9日,所涉时间及数据以当时为准。(本文为游戏邦/gamerboom.com编译,拒绝任何不保留版权的转载,如需转请联系:游戏邦

How to make a multi-player game – part 1

Posted on October 9, 2012 by Paul Firth

Its been a while since my last post, this is because I’ve been working on a multi-player game, called mmoAsteroids which you can play by clicking on the icon on the side-bar. This post is my attempt to crystallise the most important points I’ve learned during the making of this game and my other multi-player prototypes.

Introduction

Firstly, it’s important to identify what I mean by multi-player and what implications that has. I’m talking about non-local multi-player, over the internet rather than at the same computer.

Of course this means there needs to be some kind of way for the players to communicate with each other over the internet. I’ve chosen the server-client model rather than peer-to-peer, because that’s what my target platform client, Adobe Flash supports.

Client side

Why choose Flash? Because:

The install base is massive, even compared to HTML5

It’s a fixed platform, you don’t need to worry about different browsers, or different hardware

The Flash portals are an amazing resource for distributing games

There are very mature development environments available which support full debugging via the world class Visual Studio

It has support for TCP sockets built right in

Server side

Because I’m using the client-server model, I need to choose a platform for the server-side code.

I’ve chosen node.js for this article, simply because it’s the fastest way to get a decently performing server set up and running with the least amount of code. Also, with your own server there are no limits on the number of clients you can support simultaneously, unlike ready made packages like player.io, or SmartFoxServer which limit you to a certain number of users until you sign up to their paid plan.

Of course, they provide a lot more features for that money as well; it’s important to weigh up the costs and benefits of whatever platform you choose.

Client-server model

How do server-client based multi-player games work?

Briefly, the client and server communicate by sending messages to one another. The client might send a message to the server saying he wants to move forwards. The server will receive the message and react accordingly. The server might send back a message saying that the player collected a power-up, or that someone else has joined the game.

The client and server both maintain a copy of the game universe; the server has the master copy and then client has a (possibly partial) copy for local simulation and display purposes. The server has authority over all the important decisions in game, like players getting hit by bullets, or being killed, or levelling up. The client has authority about what keys are being pressed and other pieces of user input data.

The reason for this separation of authority is to prevent cheating; although this is only really something you need to worry about once your game is big enough for hackers to invest time in finding cheats, it’s worth taking simple steps ahead of time to prevent the possibility of a hacked client from, for example, transmitting that he killed everyone in the world to the server, or something of that nature.

Having the server be in authority simply prevents this from being possible.

WoW recently suffered from hackers exploiting the system

MMO? MO?

It’s important to distinguish between the various types of multi-player game before we get into this too deeply, because the type can have a massive effect on the amount of code you need to write.

Starting at its simplest form, I’m going to define an MO or Multi-player Online game as a multi-player game supporting as many players as possible running on one server (a physical computer located on the internet somewhere).

An MMO is a Massively Multi-player Online game. There are many different forms this can take, but nearly all of them will involve more than one physical server working together to handle the huge load that ‘massively’ implies.

How they work together will define how the game is to be designed; for example you might want your game universe to be shared by all players at the same time, as in Eve Online; this requires that several servers work together to form a large shard to share the load. Or you might want to instance your universe so that each individual server actually holds a unique copy of the universe and players in one universe cannot see players in another, like Realm of the Mad God. Or yet again, maybe you want to have some kind of combination of the two, where the universe is split up into realms and players cannot see players in other realms but they can travel between these realms, like in World of Warcraft.

Realm of the Mad God

Making a genuine MMO with a non-instanced universe is a massive undertaking, not to be attempted lightly. Even one with realms and travel between is fantastically complicated.

The more you instance your universe, the easier it gets to create the architecture because you can simply hand off instances to new servers as your game expands. If the players can’t communicate across these instances things are easier still. This is the architecture that mmoAsteroids uses currently. Each universe can hold 20 players (since the level is only a certain physical pixel size) and each server can hold 10 instances at once, giving an on-line capacity of 200 players per server.

What I’m going to describe in this article is an MO, since that is the easiest concept to code. Adapting this to a highly instanced MMO is entirely possible.

TCP vs UDP

The choice of whether to use TCP or UDP communication protocols has been made for us by the fact that Flash only supports TCP. This would seem like a big problem for a very fast paced multi-player game because TCP has a reliable, in-order delivery policy and in particular has Nagles algorithm which tries to group separate packets of data together into larger payloads before actually sending them, which can introduce lag. UDP is very different in this regard and offers no such features, but also suffers from no such issues. However, using TCP it is possible to disable Nagle and thereby remove this lag inducing restriction, so all is not lost.

TCP and UDP

At the end of the day, anyone set on using UDP for their game will have to implement their own version of TCP’s reliable, in-order delivery policy anyway, so once Nagle has been disabled there really isn’t that much separating TCP and UDP in terms of raw speed. Here is a related discussion on GameDev.net.

Requirements

If you would like to follow this article through and make your own server-side code, you will need download and install node.js. At the prototyping stage you don’t need a real server, you can just run everything locally – this is a far simpler and quicker way to develop anyway.

When you want to deploy for real, you’ll either need your own VPS, or you can subscribe to one of the growing number of node.js hosting providers. Here is a large list of providers.

The game

For this article, I’ve chosen to recreate the prototype version of mmoAsteroids. For me this game is interesting because it’s a very fast paced, bullet-hell like, twitch reaction arcade game. Not a genre that is typically seen as a multi-player game (although there are exceptions such as Continuum and Realm of the Mad God) perhaps because it’s difficult to get such games performing well over the internet.

Prototyping is extremely important and is absolutely the first thing you should do once you’ve figured out what game you’d like to make. The goal is to get the core part of the game up and into testing as fast as possible because the number one thing which will stop your game from being a success is it not being finished! Read more about how to approach the process of making a game here.

Here is the prototype in all its glory which you can buy the full source code to at the bottom of this article:

Where to start?

The fundamental component of a multi-player game is the server. In this case it’s a socket-server since clients will be connecting via sockets. A socket is just an access channel for communication over a persistent connection between computers on a network. This is different to HTTP (which is how you are reading this article right now), which is a request/response protocol with no persistent connection.

Having a persistent connection is very important for a game like this, because response times are so important – we can’t be waiting around for seconds for a response to keyboard input from the server!

The socket server

For the socket-server, I’m going to be using the net package which is built into node.js, because it provides a nice clean interface and a good starting point. The experienced reader might be wondering why I’ve not chosen to use the socket.io package which enables WebSocket support for node.js. The reason is that I want more control over the way messages get broadcast from the server and that finding a decent Flash implementation of the full web-socket protocol is difficult.

If you have chosen to use javascript for your client, you should use socket.io because javascript is best suited to WebSockets since it has no native TCP socket support of its own.

The simplest socket server

Here is what the most simple socket server possible looks like in node.js:

var port = 8000;
var host = “localhost”;
var net = require(‘net’);

var server = net.createServer( function(socket)
{
socket.on(‘data’, function(data)
{
console.log(“data received: ” + data);
});

socket.on(‘close’, function(socket)
{
console.log(“close received”);
});

socket.on(‘timeout’, function(data)
{
console.log(“timeout received”);
});

socket.on(‘error’, function(data)
{
console.log(“error received”);
});
});

server.listen(port, host);

It really is a testament to the quality of node.js that you can create a socket server in such few lines of code. You can browse the documentation for the net package here.

The first parameter to createServer() is the closure which will be called once a connection is made from a client. During this closure we attach various listeners to handle events that node.js sends us, such as close (when a client disconnects), or data (when a client has transmitted some data). Then on the final line we start the server listening for connections, using the port we want and the host address that we want to listen on.

The mighty node.js

The choice of which port to use is entirely up to you, although it probably pays to do some research to find out whether the majority of your players are able to connect to the internet via the port you’ve chosen; many players might be at work, or behind a firewall in a library which prohibits connections on non-standard port numbers. For mmoAsteroids, I chose 443, which is usually used by the HTTPS protocol; I found this to be the port which afforded me the greatest amount of external visibility to players.

The data event

The most interesting aspect of the above server is the data event, which indicates that data has arrived for processing – this is how the client and server will communicate primarily. Note that it does not mean a message has arrived, just that some amount of data is present to be read. It could be part of a message sent from a client, or it could be several messages together. The reason for this is that TCP is a stream oriented protocol, not a message oriented one. We need to do a little bit of work to build our own message protocol on top of this.

The message protocol

We would like the server to notify us when individual messages get received from the client. In order to do this we must design a simple message protocol. The one I’ve chosen for this tutorial is a simple UTF8 string based protocol with a terminating character separating each message.

Figure 1

Figure 1 shows a simple example of a message and the terminating character (which in this case is \n, or ASCII code 10, the newline character). Of course this means that messages cannot contain \n as part of the message, as this would confuse the protocol.

Figure 2

Figure 2 shows a possible set of individual data events numbered 1-5 encountered during the transmission of two individual messages: message\n and message\n. This gives you an example of the type of processing that we need to handle.

Fortunately, actually separating messages like this is quite easy when dealing with strings because both javascript and actionscript have the split() function on a string, which divides the input string up into chunks based on a separating character; effectively separating our messages from each other. It’s not quite as simple as that, though as you can see from Figure 2, because not all data events will yield a string with a terminating character; we need to buffer the data as we go along.

var server = net.createServer( function(socket)
{
//
// this gets called once for each connection we have from a client…
//

// utf8 encoding
socket.setEncoding(‘utf8′);

// no data received yet
var socketData = “”;

//
// attach data handler
//

socket.on(‘data’, function(data)
{
socketData += data;

var substrings = socketData.split( “\n” );
var lastMsg = substrings.length-1;

if ( substrings[lastMsg].length!=0 )
{
// partial data read, store for later
socketData = substrings[lastMsg];
}
else
{
// full read, clear buffer
socketData = “”;
}

// process all messages
for ( var i = 0; i<lastMsg; i++ )
{
var message = substrings[i];

// process message!
}
}
});

The above code is all we need in reality. Inside the data handler, the data received is first buffered:

socketData += data;

Then it is split into sub-strings:

var substrings = socketData.split( “\n” );

This returns an array with minimum of 1 entry. Then we check to see if we have a partial read:

var lastMsg = substrings.length-1;
if ( substrings[lastMsg].length!=0 )
{
// partial data read, store for later
socketData = substrings[lastMsg];
}

In which case we replace the buffered data with the last substring. If not it means we have a full read and we clear the buffered data completely:

else
{
// full read, clear buffer
socketData = “”;
}

Then we go on to process any and all messages received so far:

// process all messages
for ( var i = 0; i<lastMsg; i++ )
{
var message = substrings[i];

// process message!
}

We write the same code on the client and now we have a working message protocol!

Serialisation

Now we can happily send string based messages to and from our client. But what use is that? We need to be able to send data, not just strings!

Both javascript and now actionscript, Flash Player 11.0 have built-in support for the industry standard data serialisation/deserialisation format, JSON which is what I’ve chosen to use for this tutorial. There are many other possible serialisation formats, but this is the one which is fastest to get up and running. It pays to do some research and find the format which is best suited for your individual needs.

Mmm, serialisation…

Serialising an object to JSON in javascript is as simple as:

var messageString = JSON.stringify(data) + Message.Constants.kMessageTermintor;

Notice the appended message terminator which forms part of our protocol. In actionscript it’s nearly identical:

var message:String = JSON.stringify(data) + kMessageTermintor;

Deserialisation is just as simple:

var data = JSON.parse(messageString);

var data:Object = JSON.parse(messageString);

The only slight issue is that the deserialised data is always just annoymous objects, not concrete types. This doesn’t matter so much in javascript because that’s how the language was designed to work, but actionscript is a statically typed language and we can take advantage of that to root out bugs at compile time before they occur unexpectedly in the runtime.

In order to deserialise into a concrete type in actionscript we must do some extra wrangling of the data.

package Code.System
{
import flash.utils.*;
import flash.geom.*;
import flash.system.*;
import flash.display.*;
import flash.net.*;

public class Helpers
{
/**
*
* @param obj
* @return Class
*
*/
static public function GetClass( obj:Object ):Class
{
return Class( getDefinitionByName( getQualifiedClassName( obj ) ) );
}

/**
*
* @param obj
* @return String
*
*/
static public function GetClassName( obj:Object ):String
{
return getQualifiedClassName( obj );
}

/**
* Take a annoymous type and try to copy the properies over into the given concrete type
*
* @param source Annoymous type
* @param targetType Concrete type
*
*/
static public function CloneIntoR( source:Object, targetType:Class ):*
{
Assert( source!=null, “CloneIntoR(): source is null! Type=” + GetClassName(targetType) );
var data:* = new targetType( );

for ( var prop:Object in source )
{
Assert( data.hasOwnProperty( prop ), “Helpers.CloneInto(): supplied type didn’t have required property ” + prop );

try
{
data[prop] = source[prop];
}
catch ( e:Error )
{
data[prop] = CloneIntoR( source[prop], GetClass( data[prop] ));
}
}

return data;
}

/**
* Import an annoymous type from the server into a concrete type of the client
*
* @param source Annoymous type
* @param targetType Concrete type
*
*/
static public function ImportServerDef( source:Object, targetType:Class ):*
{
var data:* = CloneIntoR( source, targetType );
data.PostFixUp( );
return data;
}
}
}

The above function Helpers.ImportServerDef() does just such a job. It will even work for recursively stored complex types. You can use it like this, for example:

var psd:PlayerSpawnDef = Helpers.ImportServerDef(data, PlayerSpawnDef);

Where PlayerSpawnDef in this case represents the data the client expects to receive from the server when a new player spawns in the game:

Message from server:

{“m_pos”:{“m_x”:44.179,”m_y”:-426.812},”m_uid”:10003,”m_name”:”Guest10003″}

Concrete type:

public class PlayerSpawnDef extends BaseDef
{
public var m_pos:Vector2;
public var m_uid:uint;
public var m_name:String;

public function PlayerSpawnDef()
{
m_pos = new Vector2();
}
}

This enables strict type checking on the individual names of each part of the message, which prevents a typo from resulting in a bug. Absolutely invaluable in anything larger than this simple prototype.

The reason the constructor creates an empty Vector2() is so that Helpers.ImportServerDef() can know the type it’s supposed to be importing – there is no way in the actionscript runtime to determine the type of variable if that variable is null.

Messages

Ok, now we’re able to send objects and data over our message protocol and have them deserialised into concrete types on the client. So how do the client and server actually know what type of message they are receiving? It might be a message to create a new player, or one to respond to player input, or any other kind. We need to pre-package our messages inside a special container class which describes the message type as well as the message data.

package Code.Messages
{
public class MessageContainer
{
/** Message name */
public var n:String;

/** Message string */
public var s:String;

/** Message data – to be filled in by client */
public var m_data:*;
}
}

Above is how I handle this. All messages are encoded into a container, which contains the name of the message and the message string itself. So for example message names might be defined as single letters like this, for the sake of bandwidth saving:

/**
* Must be kept in sync with server!
*/
public class MessageNames
{
static public const kTime:String = “a”;
static public const kCreatePlayer:String = “b”;
static public const kReady:String = “c”;

}

And a fully encoded message with a container might look like this:

“{“n”:”c”,”s”:”{\”m_pos\”:{\”m_x\”:362.597,\”m_y\”:9.912},\”m_uid\”:10005,\”m_name\”:\”Guest10005\”}”}”

So, the message name is “c” and the message itself is:

“{\”m_pos\”:{\”m_x\”:362.597,\”m_y\”:9.912},\”m_uid\”:10005,\”m_name\”:\”Guest10005\”}”

And when it comes to decoding the container string:

package Code.Messages
{
import flash.net.*;
import flash.utils.*;

import Code.System.*;
import Code.Maths.*;

/**
* Static class members to handle message sending and reading
*/
public class Message
{

/**
* Deserialise a message from a string
*
* @param messageString
* @return MessageContainer
*
*/
static public function Get(messageString:String):MessageContainer
{
var containerA:Object = JSON.parse( messageString );

if ( containerA.s!=undefined )
{
containerA.m_data = JSON.parse( containerA.s );
}

// clone into concrete type
var container:MessageContainer = Helpers.CloneIntoR( containerA, MessageContainer );

return container;
}
}
}

I can just call:

var messageContainer:MessageContainer = Message.Get(messageString);

Which gets the message container, containing the decoded message and its name which was picked up and separated into a message by the message protocol as previously described. I can then simply

interrogate the type of message and deserialise the inner data appropriately:

switch (messageContainer.n)
{
case MessageNames.kReady:
{
// unpack and create the world

}
break;

case MessageNames.kCreatePlayer:
{
// deserialise the message data into a concrete type
var psd:PlayerSpawnDef = Helpers.ImportServerDef(messageContainer.m_data, PlayerSpawnDef);

}
break;
}

Note that the inner message data cannot be deserialised into the correct concrete type until the type of message has been understood by the game code via the message name, as shown above when creating a player.

Ok, now we have a socket server, a message protocol with serialisation and deserialisation, a way to determine the type of message received on either end. Now all we need is the actual game itself!

That’s the subject of the next article in this series!(source:wildbunny


上一篇:

下一篇: