Spine Tutorial 5 – Multiplayer

Requirements

Introduction

This tutorial covers how to create multiplayer using Spine. The current functionality isn’t build for MMO servers, but for direct multiplayer sessions for x players that are looking for a match at the same time.

The Functions

The functions of this module shall be explained here.

Spine_SetHostname

With Spine_SetHostname you can set the hostname for the used server. Per default the value is set internally to the Spine multiplayer server. But this one just allows to be used for testing and for approved mods. Whoever wants to use an own server has to implement the API for that and set the server with this function. Those who just have a small multiplayer can also request to be hosted directly via Spine.

Spine_SearchMatch

With Spine_SearchMatch you can search for a match. Therefor you need to enter the player amount you’re looking for. Furthermore an identifier is required. The identifier can be used to represent different levels or game modes. A match will be found when for the same identifier the given number of players is available. Here counts: Who starts search first also gets a match first. Using an own server implementation allows to vary this behavior of course. That means with

Spine_SearchMatch(2, 0);

you can look for a game with identifier 0 and two players (current player + another one).

Spine_SearchMatchWithFriend

With Spine_SearchMatchWithFriend you can search for a match directly with a given friend. Therefore you need to tell Spine the name of the friend. Additionally the identifier is required. The identifier can be used to represent different levels or game modes. A match is found when the given friend also looks for a match with the same identifier. That means with

Spine_SearchMatchWithFriend(0, "Bonne");

you can search a match with identifier 0 and friend “Bonne”. Only when this player also looks for a match (no matter if per friend or normal search) a match can be found. That can be combined with the Friends tutorial.

Spine_StopSearchMatch

With Spine_StopSearchMatch you can stop looking for a match.

Spine_IsInMatch

With Spine_IsInMatch you can check whether a match has been found already. As there is no callback mechanism you need to check this yourself after calling Spine_SearchMatch whether Spine_IsInMatch returns TRUE. If so you can start the game.

Spine_GetPlayerCount

With Spine_GetPlayerCount you can query how many players are available in the current match.

Spine_GetPlayerUsername

With Spine_GetPlayerUsername you can query the name for player with an index (starting with 0).

Spine_CreateMessage

With Spine_CreateMessage you can create a message. A message is a container for data that can be sent through the internet afterwards. Spine_CreateMessage requires the message type to be created as parameter. The currently available message types are:

  • SPINE_MESSAGETYPE_BASE: Base message, just contains the user type, no space for data
  • SPINE_MESSAGETYPE_INT: message to transmit an integer
  • SPINE_MESSAGETYPE_STRING: message to transmit a string
  • SPINE_MESSAGETYPE_INT4: message to transmit four integers

Every message has the field

var int userType;

that can be used freely.

It can be used to use the same Spine message type (e.g. SPINE_MESSAGETYPE_INT) for different mod specific use cases and still be able to distinguish the messages. We’ll see more on this later in the example.

It’s important to NEVER change the values of _vbtl, messageType and username. These are important for internal use and messageType and username can be read within the mod to identify the messages.

Depending on the created message more data can be set, e.g. integers or strings. The current amount of message classes matches the required ones for the current multiplayer modifications. If you require new ones, just contact us and we can add more with the next Spine update.

The function returns a pointer to the class and has to be converted to an instance with MEM_PtrToInst afterwards to be accessed properly.

Spine_SendMessage

Spine_SendMessage now sends the message to all other players. Here you need a message pointer. It’s important to know that the message will be deleted internally after sending, so after calling this function you mustn’t use it again.

Spine_ReceiveMessage

All messages the current player receives from other players are queued in Spine and can be collected using Spine_ReceiveMessage within the mod. The message is removed frm the buffer in Spine when calling the function and has to be handled directly then. Here we also get back a pointer.

Spine_DeleteMessage

Every message received via Spine_ReceiveMessage has to be deleted manually again using Spine_DeleteMessage. Otherwise unreferenced memory would sum up and Gothic crash in the end.

Example

To better explain how this works I want to show the implementation of the Chess mod as example so you know how to set up a multiplayer.

First you start by clicking the dialog option “Search opponent” with a call to

Spine_SearchMatch(2, SPINE_SCORE_1);

SPINE_SCORE_1 is a score constant for the highscore with value 0. Now Spine is looking for a match. In loop running every second we will check now if a match has been found.

if (Online_Kampf == 1) 
&& (hero.aivar[AIV_Invincible] == FALSE) 
&& (Match_Running == FALSE) 
{ 
        var int foundMatch; foundMatch = Spine_IsInMatch(); 
 
        BinDran = FALSE; 
 
        if (!foundMatch) { 
                PrintScreen    ("Warte auf Gegner!", -1, 90, FONT_SCREEN, 2); 
 
                PrintScreen    ("Zum Abbrechen oder Spiel beenden ’M’ druecken!", -1, 85, FONT_SCREEN, 2); 
        } else { 
                var string name; name = Spine_GetPlayerUsername(0); 
                if (Hlp_StrCmp(name, username)) { 
                        oppname = Spine_GetPlayerUsername(1); 
                        Online_Host = TRUE; 
                } else { 
                        oppname = name; 
                        Online_Host = FALSE; 
                }; 
                PrintScreen    (ConcatStrings("Gegner: ", oppname), -1, 95, FONT_SCREEN, 2); 
 
                Match_Running = TRUE; 
 
                if (Online_Host == TRUE) { 
                        PLAYER_COLOUR = Hlp_Random(2) + 1; 
 
                        var int msgPtr; msgPtr = Spine_CreateMessage(SPINE_MESSAGETYPE_INT); 
                        var Spine_IntMessage sendColorMsg; sendColorMsg = MEM_PtrToInst(msgPtr); 
                        sendColorMsg.userType = MSGTYPE_COLOR; 
 
                        if (PLAYER_COLOUR == COLOUR_WHITE) { 
                                BinDran = TRUE; 
 
                                sendColorMsg.param = 2; 
                        } else { 
                                BinDran = FALSE; 
 
                                Wld_SendUnTrigger      ("CAM_WHITE"); 
                                Wld_SendTrigger("CAM_BLACK"); 
 
                                sendColorMsg.param = 1; 
                        }; 
 
                        Spine_SendMessage(msgPtr); 
                } else { 
                        BinDran = 2; 
                }; 
        }; 
};
var int foundMatch; foundMatch = Spine_IsInMatch();

fetches the state of the search. If foundMatch is FALSE after this call we just print that we’re continue to search a match and the search can be stopped. In the other case we detect which of the players is player 1. That’s important to know which of the players has to be used for some management tasks. E.g. in the Chess mod the color of the players is determined randomly at game start. The random selection is handled by the “host”. The host detection is done via

var string name; name = Spine_GetPlayerUsername(0); 
if (Hlp_StrCmp(name, username)) { 
        oppname = Spine_GetPlayerUsername(1); 
        Online_Host = TRUE; 
} else { 
        oppname = name; 
        Online_Host = FALSE; 
};

username in this case is a global string variable containing the name of the current Spine user. This has been queried via Spine_GetCurrentUsername. If player 0 (the first in the list) is equal to the current player name, then this player is the host and the opponent name is at index 1. Otherwise the opponent is becoming the host.

After outputting the opponent name now we create and send the first message in case the current player is the host.

if (Online_Host == TRUE) { 
        PLAYER_COLOUR = Hlp_Random(2) + 1; 
 
        var int msgPtr; msgPtr = Spine_CreateMessage(SPINE_MESSAGETYPE_INT); 
        var Spine_IntMessage sendColorMsg; sendColorMsg = MEM_PtrToInst(msgPtr); 
        sendColorMsg.userType = MSGTYPE_COLOR; 
 
        if (PLAYER_COLOUR == COLOUR_WHITE) { 
                BinDran = TRUE; 
 
                sendColorMsg.param = 2; 
        } else { 
                BinDran = FALSE; 
 
                Wld_SendUnTrigger      ("CAM_WHITE"); 
                Wld_SendTrigger("CAM_BLACK"); 
 
                sendColorMsg.param = 1; 
        }; 
 
        Spine_SendMessage(msgPtr); 
};

First in this example the color of the current player is selected. Then with

var int msgPtr; msgPtr = Spine_CreateMessage(SPINE_MESSAGETYPE_INT);

a message containing an integer is created. This is converted to an instance using

var Spine_IntMessage sendColorMsg; sendColorMsg = MEM_PtrToInst(msgPtr);

so we can set the value. As it’s always possible to have multiple messages of type SPINE_MESSAGETYPE_INT there is the additional line

sendColorMsg.userType = MSGTYPE_COLOR;

Here the userType is set to a value defined for the Chess mod. There these three user types in the Chess mod:

const int MSGTYPE_UPGRADE = 0; 
const int MSGTYPE_ZUG = 1; 
const int MSGTYPE_COLOR = 2;

Depending on the own color the parameter in the message is either set to

sendColorMsg.param = 1;

or to

sendColorMsg.param = 2;

After filling the message it just needs to be send. This is done via

Spine_SendMessage(msgPtr);

As you see in the example nothing needs to be done for the player who isn’t the host. But this player of course needs to receive the message of the host and react to it. Therefore he was set to an internal state via BinDran = 2; to signalize he’s waiting for the color synchronization. This is also done in a loop running every second and looks like the following.

if (Match_Running == TRUE) { 
        if (BinDran == 2) { 
                msg = Spine_ReceiveMessage(); 
 
                if (msg == 0) { 
                        PrintScreen    ("Warte auf Farbe", -1, 90, FONT_SCREEN, 2); 
                } else { 
                        var Spine_Message smsg; smsg = MEM_PtrToInst(msg); 
                        if (smsg.userType == MSGTYPE_COLOR) { 
                                var Spine_IntMessage colorMsg; colorMsg = MEM_PtrToInst(msg); 
                                PLAYER_COLOUR = colorMsg.param; 
 
                                if (PLAYER_COLOUR == COLOUR_WHITE) { 
                                        BinDran = TRUE; 
                                } else { 
                                        PLAYER_COLOUR = COLOUR_BLACK; 
 
                                        BinDran = FALSE; 
 
                                        Wld_SendUnTrigger      ("CAM_WHITE"); 
                                        Wld_SendTrigger("CAM_BLACK"); 
                                }; 
                        }; 
                        Spine_DeleteMessage(msg); 
                }; 
        }; 
};

msg = Spine_ReceiveMessage(); fetches the first message and stores it in the local integer variable msg. If there is no message yet, msg is 0. Otherwise the pointer will be converted to an instance of the base class and stored in smsg. Now we can easily check the userType. If it really is MSGTYPE_COLOR, the message is converted to an integer instance of the message so we can also access the parameter. Now we can easily set the color of the player using PLAYER_COLOUR = colorMsg.param;. Afterwards depending on the color the decision is made which player’s turn it is and the camera set accordingly.

Important is the last line in this branch where the processed message is deleted using Spine_DeleteMessage so we don’t produce memory leaks.

The other messages work the same way.