Chapter 12. Multiplayer Games

Table of Contents

Overview
Core Networking Concepts
The Network Interfaces
Multiplayer Logic The Bluetooth Way
The Two-player XFuBluetoothNetwork
The XFuBluetoothMultiNetwork
More On Bluetooth
The XFuInetNetwork

Overview

The networking API in X-Forge allows packets to be sent from one "client" (device) to a "receiver" on another client. Receivers are like mail boxes, with an ID number as the address. Receivers allow packets to be sent, for example, directly to a specific game object on another client. Packets can be sent with various levels of priority, depending on the need.

Multiplayer games are implemented by making game objects synchronize their state over the network by sending packets to other clients participating in the same game. Typically, each game object will have its own receiver, allowing packets to be sent directly from one game object to the same game object on another client.

Core Networking Concepts

Note

Remember that you need to call xfcUseNetwork() in order to be able to use the network components in X-Forge. The method xfcUseNetwork() only defines that network is used with this application. To enable or disable network xfcNetworkStartup() or xfcNetworkCleanup() must be called. The network will not be usable until xfcNetworkStartup() has been called.

While networking is usually handled through the xfutil network interface, which simplifies things a lot by hiding most of the complexity of the X-Forge core networking API, it is still a good idea to learn the lower level concepts to understand what actually goes on behind the scenes.

The X-Forge core network API has "communication handlers" (that inherit from XFcCommunicationHandler) each of which support a specific type of network, for example Bluetooth or Internet. The communication handlers are handed to the "communication scheduler" (XFcCommunicationScheduler), which takes care of scheduling and controlling the packet traffic. This is the basic framework that manages incoming and outgoing packets.

Packets are sent to "clients", which represent a device on the network. The client object is an instance of a subclass of XFcClientCommWin. Exactly which class, depends on the type of network used to communicate with the particular device. For WLAN (or any type of Internet traffic), the class is XFcInetClientWin. For Bluetooth, it is XFcBtClientWin.

Clients are added to the communication scheduler, which returns a numerical ID that is used from that point on when referring to this client. For example, whenever a packet is sent to the client, the numerical client ID is used to specify the client.

Packets are always sent to a specific "receiver" on a client. A receiver is like a mailbox on the client, with a numeric ID as its address. Receivers are registered with the communication scheduler. Any packet sent to a non- existent receiver will be delivered to the default data receiver, which is also registered with the communication scheduler. Packets arrive to the receiver in form of a call to addData(). It is then up to the receiver (a subclass of XFcDataReceiver) to parse and handle the packet.

Packets are sent by requesting a packet frame from the communication scheduler. When requesting the packet frame, one specifies the client ID of the receiver and the desired message slot. The message slot determines how the packet is sent. There are four slots:

XFCNET_NONGUARANTEED       non-guaranteed, quick delivery of the packet
XFCNET_GUARANTEED          guaranteed delivery (slower)
XFCNET_QUICKGUARANTEED     prioritized guaranteed delivery
XFCNET_RECENTSTATE         recent state (explained below)

The packet frame is like an envelope for the data to be sent in the packet. Data is written to the packet frame by locking it, which returns a pointer to the data buffer in the packet frame. In addition to the actual data in the packet, one must also specify the receiver ID and the size of the data to the packet frame. After unlocking the packet frame, the packet is placed in the queue of the communication scheduler and is ready for delivery using a suitable communication handler (depending on the client to which the packet is to be sent).

Recent state packet frames work a little differently. Recent state packets are modifiable all the way until they are actually delivered. Recent state packet frames have an ID that can be used to specify a particular recent state packet frame. No matter how many times a recent state frame packet has been modified before it is actually delivered, only one copy of that packet will be sent at the next delivery. Recent state packet frames exist to allow up-to-date data to be sent without needing to flood the network with packets that do nothing more than override the previous packet.

The X-Forge core networking API does not assume that the underlying network and protocol is connection oriented. For this reason, communication is not established by explicitly connecting to another client but rather simply by sending data to it. Also, since there might not be any actual connection, it is impossible to disconnect from a client. Instead, the concept of losing contact with a client depends on the underlying network. For UDP/IP, it simply means that there has been no traffic with the client for a specific time.

Getting a notification of communication from a previously unknown client comes in form of a callback to handleSender(), defined in XFcUnknownSender, which is meant to be inherited and overridden. The actual object that should receive the callback is registered with the communication handler. Based on the data of that initial packet, it is up to the application to decide if the client that sent the packet should be accepted or rejected. Accepting the client simply means that the client is added to the local communication scheduler. In the future when that client sends a packet, the client will be identified and the packet will go directly to the intended receiver. If the client is rejected, any further data from it will again arrive in the handleSender() method, for re-evaluation.

Notifications about losing contact with a client also work using a callback. This callback is clientLost(), defined in XFcClientLost. The actual object that should receive this callback is registered with the communication handler.

The Network Interfaces

The default implementations of the network interfaces (XFuInetNetwork, XFuBluetoothNetwork and XFuBluetoothMultiNetwork) hide much of the complexity of the X-Forge core networking API. The Bluetooth/Inet network interfaces support Bluetooth and UDP/IP (Internet) as networks (though not both at the same time), but at the same time hide the low-level APIs (communication scheduler, communication handlers and client objects) completely, allowing developers to simply register clients using their addresses and deal with the client IDs.

Deciding if new clients should be accepted or rejected is simplified and automated with a concept called "game token". Establishing communication with a client is done by sending it a "game connect packet" that contains a game token - an unsigned 32 bit integer. The client that receives the game connect packet automatically compares the game token in the packet to its own game token, which it has previously set using the setAcceptGameToken() method. If the tokens match, the client is automatically accepted and added to the communication scheduler. Rejected clients are quietly ignored. Sending the game connect packet is done using the sendGameConnectPacket() method.

The network interface notifies "network event handlers" about newly accepted clients, as well as lost contact with existing clients. Any class that inherits from XFuNetworkEventHandler can register itself as an event handler with the network interface and there can be any number of event handlers.

Manually adding a client, for example in order to contact a server, is done by calling addClient() in the network interface and specifying an address. Clients can either be removed manually or by letting the network interface do it automatically when the contact to a client is lost.

Data receivers, including the default receiver that is used when no existing receiver is found for a packet, are registered with the network interface rather than with the communication scheduler.

Sending packets can be done in two ways. Either by requesting a packet frame (or recent state frame) from the network interface or by using the send() or sendRecentState() methods. Using packet frames directly is a more low level way to send packets and involves more manual work. One must lock the packet frame to obtain a pointer to the data buffer, copy the data manually into the packet frame data buffer, set the receiver ID and length of data and finally unlock the packet frame. The send() methods, on the other hand, use so called "serializable" objects. Any class that inherits from XFuSerializable, and implements the pure virtual serialize() and deserialize() methods, can be used as a serializable object with the send() methods. Packet frame with data size less than 0 is not sent. These packet frames are removed from queue and marked as empty packets. If packet frame is "recentstate" the frame is marked as empty and frame is erased but not sent.

When a serializable object is sent, a pointer to it is passed to the send() method in the network interface, which requests a packet frame and locks it. Next, serialize() in the serializable object is called and a pointer to the packet frame data buffer is passed to it. It is now up to the serializable object to write itself to the data buffer.

In the same way, serializable objects can be created or updated from a packet by calling the deserialize() method, passing it a pointer to the packet data buffer. The serializable object then reads and parses the data buffer and creates or updates itself from that data.

By implementing packets in a network protocol as classes that inherit from XFuSerializable, packet creation and parsing can be greatly streamlined. The data in the packets can be made accessible with getter and setter methods so that the actual game code only needs to deal with the high level primitives, and never the bytes from the packets sent over the network. Only the serialize() and deserialize() methods need to concern themselves with packet parsing to and from the network.

Multiplayer Logic The Bluetooth Way

There are two Bluetooth network interfaces in X-Forge. The other is for two players only, called XFuBluetoothNetwork, and the other for two or more players, called XFuBluetoothMultiNetwork. This may sound silly, but there is a reason. In Bluetooth the devices can be either 'master' or 'slave'. These roles effectively dictate the only way a multiplayer framework can be done efficiently for more than two players. The Bluetooth master has a chairman-like role in the communications. It allocates each slave a time-slice in which the slave can transmit information to the master. This is to avoid "filling the air" with Bluetooth packets. The master, however, can send information to the slaves anytime and the slaves will receive it. The Bluetooth slaves also only accept one incoming connection and that is from the particular Bluetooth master terminal, whose Bluetooth band they are in. This is why the only logical place to have a multiplayer game server is on the Bluetooth master. The major differences to the two player method are in initiating the connections. The slave devices can advertise themselves running a certain service the master devices are interested about. The masters can run an advertiser discovery and gather a list of slaves running that service. This means that working with XFuBluetoothMultiNetwork the game host actually finds out about the advertising game clients and connects to them, unlike to what we are used to with the Internet games for example.

The two-player XFuBluetoothNetwork is there to provide with a more traditional way of setting up a multiplayer game. With it the game host works as a Bluetooth slave, listening for incoming connections from a game client, i.e. a Bluetooth master. During the game there's practically no difference in packet exchange between the two Bluetooth network interface variants. With the two-player version the mechanism in initiating a multiplayer game is similar between the Bluetooth and inet versions. With more players - if there are versions of a game for both Bluetooth and inet networking - the logic has to follow that of the XFuBluetoothMultiNetwork or then be made different between the Bluetooth and inet versions.

To make things easier to understand (hopefully), we define a local terminology here, which is used in explaining the network interfaces.

Role in game logic:

  • Game Server = logical server of the game, distributes info among itself and clients
  • Game Client = logical client of the game server

Role in connection:

  • Network Server = listens and accepts connections from Network Clients
  • Network Client = searches and connects to a Network Server

Role in Bluetooth:

  • Bluetooth Master = master on the Bluetooth band, always the Network Client
  • Bluetooth Slave = listener on the Bluetooth band, always the Network Server

Thus with XFuBluetoothNetwork (2 players):

  • Game Server = Bluetooth Slave = Network Server
  • Game Client = Bluetooth Master = Network Client

With XFuBluetoothMultiNetwork (2 or more players):

  • Game Server = Bluetooth Master = Network Client
  • Game Clients = Bluetooth Slaves = Network Servers

With XFuInetNetwork (2 or more players):

  • Game Server = Network Server
  • Game Clients = Network Clients

or

  • Game Server = Network Client
  • Game Clients = Network Servers

The Two-player XFuBluetoothNetwork

Starting the network interface with Bluetooth support is done by calling enableClientService() or enableServerService(). Closing the current service is done by calling closeService(). The enableServerService() method creates a Network Server service for a given port. Valid port numbers are between 1-30 (this is a RFCOMM limitation, which is used for now), but port number 0 can also be provided in which case the system will find a free port to the Network Client - here also the Game Client. Starting a Network Server service with port 0, an advertiser service has to be started as well. This is done by calling startAdvertiser(). The advertiser service will register a new entry to the SDP (Service Discovery Protocol), here a Game Server.

After this a Game Client can request form SDP the running Network Server services, calling startServerDiscovery(). If a Game Server is found, SDP will tell the Game Client the port where the Game Server is running. Only after this can the Game Client connect to the Game Server. If the port number 0 is used, a single port won't be concurrently allocated for several services. This limits the number of concurrent services to 30. When creating a Network Client with enableClientService(), the port number is not needed. The port must be given as parameter to addClient() method. The same limitation of the port range is related to the Bluetooth Network Client as is with Bluetooth Network Server (1-30). The XFuBluetoothNetwork can not handle more than one Network Client.

Using startDeviceDiscovery() one can create a device discovery request over the Bluetooth. The Bluetooth device discovery is not related to the X-Forge Bluetooth server, but is a lower layer Bluetooth functionality. The device discovery can be stopped with stopDeviceDiscovery(). The discovery is asynchronous and deviceDiscovery() is used as a callback.

The XFuBluetoothMultiNetwork

Starting the network interface with Bluetooth support for more than two players is similar to that of the two-player XFuBluetoothNetwork, but the roles are practically reversed. Starting the network interface is done by calling enableHostService() or enableClientService(). Closing the current service is done by calling closeService(). Please note that the methods are named with the game logic in mind. Here enableClientService() method actually creates a Network Server (but for a Game Client) service for a given port. Valid port numbers are again between 1-30 (RFCOMM limitation), but port number 0 can also be provided in which case the system will find a free port to the Network Client - here the Game Server. Starting a Network Server service with port 0, an advertiser service again has to be started as well. This is done by calling startAdvertiser(). The advertiser service will register a new entry to the SDP (Service Discovery Protocol), a Game Client.

After this a Game Server can request SDP running Bluetooth Network Server services, like our Game Client, by calling startClientDiscovery(). If a Game Client is found, SDP will tell the Game Server the port where the Game Client is running. Only after this can the Game Server connect to the Game Client. If the port number 0 is used, a single port won't be concurrently allocated for several services. This limits the number of concurrent services to 30. When creating a Network Client with enableHostService() the port number is not needed. The port must be given as parameter to addClient() method. Again the same limitation of the port range applies (1-30).

The device discovery also exists in this interface, and works similarly to that in XFuBluetoothNetwork.

More On Bluetooth

The Bluetooth connection is connection oriented, althougt it behaves like it would not be. The connection procedure is started with addClient() and it is asynchronous. For this reason one is not able to send any packets before the connection is established (X-Forge will not give any packet frames before connection). Basically this means that one has to call sendGameConnetPacket() multiple times and when the connection is established, the packet is sent. If an error occurs, clientLost() is called. The size of the X-Forge Bluetooth packet frame is limited to 128 bytes and the size of MTU (Maximum Transfer Unit) for the X-Forge Bluetooth is limited to 512 bytes by default.

Nothing prevents you from using device discovery in conjunction with the advertiser. This is not recommended, however. In this case you could search for devices with the device discovery, and then search for advertised services on the found devices. This can be very slow, if the advertiser discovery is made to all found devices.

The advertiser discovery can be slow in general. The number of devices in the area directly affect this. When requesting for SDP services a connection with other devices have to be made, and making a connection in general is very slow. Another factor is the fact that at least in the Series 60 there's a very short timeout in the SDP service. This slows up the discovery, as a time-out isn't in fact an error, but results in connection retries.

The XFuInetNetwork

Starting the network interface with inet support is done by calling enableService(). Closing the current service is done by calling closeService(). The XFuInetNetwork uses non-connected methods to send packets over the network. The method used in the inet communication is UDP. This method forces that one must select a game port for the Network Client and Network Server. If 0 is selected the system will use any free port. Unlike with Bluetooth addClient() does not make any connection and one can get packet frames immediately. Still one must call sendGameConnectPacket() until the Network Server sends reply or clientLost() is called. The XFuInetNetwork can handle more than one Network Client. Default frame size for a single inet packet is set to 1024 bytes. The XFuInetNetwork does not send the whole frame, only size of the data inside it. MTU for maximum packet size is 1500 bytes (usual default MTU for inet networking).

Device discovery is not supported with inet communication.

Advertiser service is used for finding X-Forge Game Servers over the network. The advertising service uses UDP broadcast for Network Server queries. The advertiser is started with startAdvertiser() and stopped with stopAdvertiser(). Before calling these methods one must enable inet service with enableService(). After the advertiser is started, the advertiser server can make replies to server queries. Listener discovery service is started by calling startServerDiscovery(). This service is asynchronous and its launched on its own thread (remember to set mutexes as needed). The Network Server list is given through the deviceDiscovery() callback. The Network Server discovery is ended with stopServerDiscovery(). The Network Server query can fail silently if UDP packet is missed. The query is not resent automatically, and is thus the application must resend it if needed.