Spine Tutorial 5 – Multiplayer

Voraussetzungen

Einleitung

Dieses Tutorial zeigt auf, wie mithilfe von Spine ein Multiplayer realisiert werden kann. Die aktuelle Funktionalität ist dabei allerdings nicht auf MMO-Server ausgelegt, sondern auf direkte Multiplayer-Partien zwischen x Spielern, die gleichzeitig ein Spiel suchen.

Die Funktionen

Die verschiedenen Funktionen dieses Moduls sollen hier kurz vorgestellt werden.

Spine_SetHostname

Mit Spine_SetHostname lässt sich der Hostname des genutzten Servers angeben. Standardmäßig steht der Wert intern auf dem Spine-Multiplayer-Server. Allerdings erlaubt dieser nur Mods zu Testzwecken und genehmigte Mods. Wer einen eigenen Server aufsetzen will, muss die entsprechende API dafür implementieren und mit dieser Funktion den Server setzen. Wer nur einen kleinen Multiplayer hat, kann auch anfragen, ob er direkt über Spine gehostet wird.

Spine_SearchMatch

Mit Spine_SearchMatch kann man nach einem Match suchen. Dazu gibt man an, wie viele Spieler das Match am Ende haben soll. Außerdem wird ein Identifier benötigt. Der Identifier kann genutzt werden, um unterschiedliche Level oder Spielmodi zu repräsentieren. Ein Match wird gefunden, wenn für den gleichen Identifier die angegebene Anzahl an Spielern gefunden wurde. Dabei gilt: Wer zuerst ein Match sucht, kriegt auch zuerst eines. Bei eigener Implementierung des Servers kann das Verhalten natürlich beliebig angepasst werden. Das bedeutet, dass man mit

Spine_SearchMatch(2, 0);

ein Match mit zwei Spielern (aktueller Spieler + ein weiterer) für Identifier 0 suchen kann.

Spine_SearchMatchWithFriend

Mit Spine_SearchMatchWithFriend kann man nach einem Match gezielt mit einem Freund suchen. Dazu gibt man an, wie der Freund heißt. Außerdem wird ein Identifier benötigt. Der Identifier kann genutzt werden, um unterschiedliche Level oder Spielmodi zu repräsentieren. Ein Match wird gefunden, wenn für den gleichen Identifier ein Match mit dem angegebenen Freund gefunden wurde. Das bedeutet, dass man mit

Spine_SearchMatchWithFriend(0, "Bonne");

ein Match mit für Identifier 0 suchen mit dem Freund „Bonne“ suchen kann. Erst, wenn dieser Spieler ebenfalls ein Spiel sucht (egal ob er auch nach einem Freund sucht oder nicht), wird ein Match gefunden. Das lässt sich gut mit dem Freunde-Modul in Tutorial 6 kombinieren.

Spine_StopSearchMatch

Mit Spine_StopSearchMatch kann man die Suche nach einem Match wieder beenden.

Spine_IsInMatch

Mit Spine_IsInMatch lässt sich überprüfen, ob bereits ein Match gefunden wurde. Da es keinen Callback-Mechanismus dafür gibt, muss man nach dem Aufruf von Spine_SearchMatch selber überprüfen, wann Spine_IsInMatch TRUE zurückliefert, um dann entsprechend das Spiel zu starten.

Spine_GetPlayerCount

Mit Spine_GetPlayerCount lässt sich abfragen, wie viele Spieler im aktuellen Match vorhanden sind.

Spine_GetPlayerUsername

Mit Spine_GetPlayerUsername lässt sich für jede Spielernummer (beginnend bei 0) der Name des Spielers herausfinden.

Spine_CreateMessage

Mit Spine_CreateMessage kann man eine Nachricht erstellen. Eine Nachricht ist ein Container für Daten, den man anschließend über das Internet versenden kann. Spine_CreateMessage benötigt als Parameter den Nachrichtentyp, der erstellt werden soll. Derzeit verfügbare Nachrichtentypen sind:

  • SPINE_MESSAGETYPE_BASE: Basisnachricht, enthält nur einen Usertyp, kein Platz für Daten
  • SPINE_MESSAGETYPE_INT: Nachricht, die einen Integer übertragen kann
  • SPINE_MESSAGETYPE_STRING: Nachricht, die einen String übertragen kann
  • SPINE_MESSAGETYPE_INT4: Nachricht, die vier Integer übertragen kann

Jede Nachricht hat als beliebig zu setzendes Feld den

var int userType;

Dieser Kann genutzt werden, um den selben Spine-Nachrichtentyp (z.B. SPINE_MESSAGETYPE_INT) für unterschiedliche modspezifische Zwecke zu nutzen, die Nachrichten aber trotzdem unterscheiden zu können. Dazu später mehr in einem Beispiel.

Wichtig ist, dass man die Werte _vtbl, messageType und username NIEMALS verändert. Diese sind intern wichtig und messageType und username können in der Mod abgefragt werden, um die Nachricht zu identifizieren.

Je nach erstellter Nachricht, kann man dann weitere Daten setzen, z.B. Integerwerte oder auch Strings. Der aktuelle Umfang an Nachrichtenklassen entspricht den bisher benötigten für die existierenden Multiplayer-Modifikationen. Wenn neue Daten übermittelt werden sollen, kann jederzeit Kontakt zu uns aufgenommen werden, um weitere Klassen hinzuzufügen.

Die Funktion gibt einen Pointer auf die Klasse zurück und muss daher noch mit MEM_PtrToInst in eine Klasse umgewandelt werden.

Spine_SendMessage

Spine_SendMessage sendet eine Nachricht direkt an alle anderen Spieler. Hier muss ein Nachrichten-Pointer übergeben werden. Wichtig ist, dass die Nachricht nach dem Aufruf dieser Funktion intern gelöscht wird und daher nicht mehr benutzt werden darf.

Spine_ReceiveMessage

Alle Nachrichten, die der aktuelle Spieler von anderen Spielern erhält, werden in Spine der Reihe nach gespeichert und können mit Spine_ReceiveMessage in der Mod abgeholt werden. Die Nachricht wird beim Aufruf der Funktion aus dem Buffer in Spine entfernt und muss daher direkt verarbeitet werden. Auch hier bekommt man wieder einen Pointer zurück.

Spine_DeleteMessage

Mit Spine_DeleteMessage muss jede Nachricht, die über Spine_ReceiveMessage empfangen wurde, wieder gelöscht werden. Andernfalls würde sich unreferenzierter Speicher ansammeln und Gothic irgendwann abstürzen.

Example

Um das ganze Konzept etwas klarer darzustellen, soll anhand der Schach-Mod kurz dargestellt werden, wie man die Funktionen richtig nutzt, um einen Multiplayer zu realisieren.

Begonnen wird beim Klick der Dialogoption „Gegner suchen“ mit einem Aufruf von

Spine_SearchMatch(2, SPINE_SCORE_1);

SPINE_SCORE_1 ist dabei eine Score-Konstante für den Highscore und hat den Wert 0. Nur wird nach einem Match gesucht. In einer sekündlichen Schleife wird überprüft, ob bereits ein Spiel gefunden wurde.

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();

holt den Status der Suche ab. Wenn foundMatch nach diesem Aufruf FALSE ist, wird nur eine Ausgabe gemacht, dass weiter nach einem Spiel gesucht wird und die Suche abgebrochen werden kann. Andernfalls jedoch, wird ermittelt, welcher der beiden Spieler Spieler 1 ist. Das ist wichtig, um zu ermitteln, welcher Spieler für Verwaltungsaufgaben genutzt wird. Beispielsweise wird bei der Schach-Mod die Farbe der Spieler einmalig beim Start ausgewürfelt. Das Auswürfeln übernimmt dabei der Host. Die Ermittlung des Hosts geschieht über

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 ist dabei eine globale String-Variable, die den Namen des aktuellen Spielers enthält. Dieser wurde über Spine_GetCurrentUsername abgefragt. Wenn Spieler 0 (der erste in der Liste) gleich dem aktuellen Spielernamen ist, dann ist der Spieler Host und der Gegnername ist bei Index 1. Ansonsten wird der Gegner Host.

Nach einer Ausgabe des Gegnernames kommt nun das Erstellen und Versenden der ersten Nachricht, wenn der aktuelle Spieler der Host ist.

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); 
};

Zuerst wird in diesem Beispiel die Farbe des aktuellen Spielers ausgewürfelt. Dann wird mit

var int msgPtr; msgPtr = Spine_CreateMessage(SPINE_MESSAGETYPE_INT);

eine Nachricht angelegt, die einen Integer enthält. Daraus wird mit

var Spine_IntMessage sendColorMsg; sendColorMsg = MEM_PtrToInst(msgPtr);

eine Instanz der Nachrichtenklasse gemacht, damit wir daran nun auch Werte setzen können. Da es immer möglich ist, dass wir mehrere Nachrichten vom Typ SPINE_MESSAGETYPE_INT verschicken, gibt es noch die Zeile

sendColorMsg.userType = MSGTYPE_COLOR;

Hier wird der userType auf einen für die Schach-Mod definierten Wert gesetzt. In der Schach-Mod gibt es folgende drei Usertypes:

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

Je nach eigener Farbe wird in der Nachricht dann der Parameter entsprechend gesetzt, entweder auf

sendColorMsg.param = 1;

oder auf

sendColorMsg.param = 2;

Nach dem Befüllen der Nachricht muss sie nur noch abgeschickt werden. Das geschieht über

Spine_SendMessage(msgPtr);

Wie im Beispiel zu sehen, wird hier noch nichts für den Spieler gemacht, der nicht der Host ist. Dieser muss natürlich die vom Host gesendete Nachricht erhalten und auswerten. Daher wurde er mit BinDran = 2; in einen internen Zustand gesetzt, der signalisiert, dass auf die Farbsynchronisation gewartet wird. Dies geschieht wieder in der sekündlichen Schleife und sieht folgendermaßen aus.

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(); holt die aktuell erste Nachricht ab und speichert sie in der lokalen Integer-Variable msg. Wenn noch keine Nachricht angekommen ist, ist msg 0. Ansonsten wird aus dem Pointer eine Instanz der Basisklasse gemacht und in smsg gespeichert. Nun kann ganz einfach der userType überprüft werden. Wenn dieser tatsächlich MSGTYPE_COLOR ist, wird die Nachricht in eine Integer-Instanz der Spine-Nachricht umgewandelt, damit auch ein Zugriff auf den übermittelten Parameter möglich ist. Dann kann ganz einfach mit PLAYER_COLOUR = colorMsg.param; die Farbe des Spielers gesetzt werden. Danach wird noch je nach Farbe entschieden, ob der Spieler nun am Zug ist oder nicht und die Kamera entsprechend umgesetzt.

Wichtig ist noch die letzte Zeile in diesem Zweig, wo mit Spine_DeleteMessage(msg); die verarbeitete Nachricht gelöscht wird, um kein Speicherleck zu verursachen.

Die weiteren Nachrichten funktionieren über das gleiche Prinzip.