So, heute machen wir uns an ein dynamisches Haussystem mit den folgenden Eigenschaften:
- Unter Verwendung von gestreamten Checkpoints, das heißt, dass jeweils nur ein Checkpoint zu sehen ist und ebenfalls dieser nur in unmittelbarer Nähe. Daher wäre diese Art von Eingang vorallem für Roleplay-bedachte Server geeignet.
- Ebenfalls werden gestreamte 3DText-Label verwendet, welche in die Mitte des Checkpoints gesetzt werden. Sie sind ebenfalls nur in unmittelbarer Nähe zu sehen, sodass in Gegenden mit vielen Häusern das Haussystem weiterhin gut zu betrachten ist.
- Um die Daten der Häuser zu speichern verwenden wir BlueG's MySQL R7 Plugin, welches mit Cache-Funktionen eine gute Performance aufweisen kann und ebenfalls sehr gut zu bedienen ist. Vorallem bei einem Haussystem kann MySQL sehr viele Vorteile bringen, sobald man sich ein größeres Haussystem mit mehreren kleinen Systemen erstellen möchte.
- Da wir MySQL verwenden werden ist natürlich eine Datenbank benötigt. Um lokal zu programmieren verwende ich selbst meistens XAMPP, wodurch mir direkt nach der Installation alle nötigen Funktionen zur Verfügung stehen, um Datenbanken reichlich zu erstellen. Darauf werde ich jedoch nicht weiter eingehen, da XAMPP durchaus des öfteren schon erwähnt und erklärt wurde. Das einzigst hilfreiche zu erwähnen wäre, dass man durch einen Klick auf 'Admin' unter 'MySQL' im XAMPP Control-Panel direkt zu phpMyAdmin gelangt.
- Wie bereits angesprochen werden wir 3DText-Label und Checkpoints streamen. Wir werden demnach den Streamer von Incognito benutzen, da dieser einfach zu bedienen ist und sehr etabliert ist.
- Um Befehle für das Haussystem zu erstellen werde ich im Tutorial ZCMD verwenden, da ZCMD für mich als eine hilfreiche Erweiterung erscheint und sehr einfach für euch ist, um es auf andere Scriptingarten umzuschreiben.
- Zur Handhabung des Splitten von Parametern bei Befehlseingabe werde ich schlussendlich sscanf von Y_Less zur Hand nehmen, da es ebenfalls bereits sehr etabliert ist.
Unser Scriptkopf sieht demnach also nun wie folgt aus:
#include <a_samp> // SA:MP Include
#include <a_mysql> // MySQL R7
#include <streamer> // Streamer
#include <sscanf2> // sscanf
#include <zcmd> // ZCMD
main() {}
Links zu den benötigten Plugins & Includes:
- BlueG's MySQL R7: http://forum.sa-mp.com/showthread.php?t=56564
- Incognito's Streamer: http://forum.sa-mp.com/showthread.php?t=102865
- Y_Less' sscanf: http://forum.sa-mp.com/showthread.php?t=120356
- ZCMD: http://forum.sa-mp.com/showthread.php?t=91354
Inhalt des Tutorials:
- Planung & Umsetzung der MySQL Datenbank
- Auslesen der MySQL Datenbank
- Wichtigste Grundbefehle
- Abschließendes
Planung & Umsetzung der MySQL Datenbank
Zu Beginn des Tutorials möchte ich anmerken, dass dieses Tutorial lediglich den Grundbaustein für ein ausgereiftes Haussystem legt. Es soll dazu dienen, eine Art der Realisierung eines Haussystems mit den genannten Hilfsmitteln darzulegen. Dies ist daher weder eine perfekte noch eine ausgereifte Version, sondern ein Grundscript, welches in ca. 1h Arbeit entstand. Anzumerken ist ebenfalls, dass jedes größere System etwas an Planung benötigt. Die grundlegende Planung unseres Systems haben wir durch das Festlegen der Plugins & Includes bereits getan. Gehen wir also genauer auf die Planung der MySQL Datenbank ein - es sollte folgende Eigenschaft erfüllt sein:
- Die Datenbank sollte über einen eindeutigen Wert verfügen, wodurch ein Haus eindeutig identifiziertbar ist. Dieser Wert sollte jedoch nicht vom Script aus kontrolliert werden, sondern von der Datenbank selbst, da sonst die Gefahr eines Fehlers zu hoch läge. Abhilfe kann uns MySQL also durch die Spalteneigenschaften 'PRIMARY_KEY' und 'AUTO_INCREMENT' leisten. 'PRIMARY_KEY' wird wie ein 'UNIQUE_KEY' angesehen, da dieser eindeutig ist und als Index der Tabelle fungiert. 'AUTO_INCREMENT' nimmt uns die Arbeit des Zählens ab, da es automatisch neue Datensätze mit einem neuen eindeutigen Index verseht.
Es muss also der Index auf 'PRIMARY' gesetzt werden und 'AUTO_INCREMENT' aktiviert werden - Wir verwenden eine ganze Zahl, also einen Integer (INT) (Bild: http://www.abload.de/img/database13qw5.jpg).
Um unsere Daten in der Datenbank zu speichern, müssen wir uns zuerst überlegen, welche Spalten wir in der Tabelle, in welcher wir unsere Häuser speicher werden, benötigen. Wir arbeiten also an unserem Script weiter, indem wir einen Enumerator erstellen und uns überlegen, was zu speichern sei:
enum hausEnumerator {
hID, // Eindeutige ID des Hauses (Typ: INT)
hPreis, // Preis zum Erwerb des Hauses (Typ: INT)
hBesitzer[MAX_PLAYER_NAME], // Name des Besitzers des Hauses (Typ: VARCHAR ; Größe: MAX_PLAYER_NAME = 24)
hInterior, // Interior des Hauses (nicht die InteriorID - mehr dazu später!) (Typ: INT)
Float:hX, // X-Koordinate des Hauseingangs (Typ: FLOAT)
Float:hY, // Y-Koordinate des Hauseingangs (Typ: FLOAT)
Float:hZ // Z-Koordinate des Hauseingangs (Typ: FLOAT)
};
Die Tabelle sollte also nach folgendem Schema erstellt werden (Dies ist ein Beispiel!):
CREATE TABLE IF NOT EXISTS `breadfish_houses` (
`hID` int(11) NOT NULL AUTO_INCREMENT,
`hPreis` int(11) NOT NULL,
`hBesitzer` varchar(24) NOT NULL,
`hInterior` int(11) NOT NULL,
`hX` float NOT NULL,
`hY` float NOT NULL,
`hZ` float NOT NULL,
PRIMARY KEY (`hID`)
);
Auslesen der MySQL Datenbank
Also, kommen wir als nächstes ans eigentliche Scripting, um was es in diesem Tutorial ja eigentlich gehen soll. Fangen wir damit an, uns eine Verbindung zu MySQL aufzubauen, damit wir auf die Werte per Queries zugreifen können:
#define SQL_DATABASE "database" // Datenbankname
#define SQL_HOST "localhost" // Hostname
#define SQL_USER "root" // Username
#define SQL_PASSWORD "" // Passwort
new sqlHandle; // Variable zum Zwischenspeichern des MySQL-Handles (Teilweise für Funktionen als Parameter benötigt!).
public OnGameModeInit() { // Wir bauen eine Verbindung auf, sobald der Gamemode geladen wird.
// Verbindung mit den oben definierten Parametern aufbauen und Handle übergeben.
sqlHandle = mysql_connect(SQL_HOST, SQL_USER, SQL_DATABASE, SQL_PASSWORD);
// Testen, ob eine Verbindung besteht - Falls nein, Fehlermeldung + Exit!
if(mysql_ping(sqlHandle) != 1) {
print("MySQL Error: Es konnte keine Verbindung zur Datenbank hergestellt werden.");
SendRconCommand("exit");
}
return 1;
}
Eine Verbindung zur Datenbank sollte nun also stehen - ihr könnte ja bereits den Gamemode einmal starten, um zu testen, ob die Console sich wieder schließt und eine Fehlermeldung ausgibt, oder ob die Verbindung besteht. Gehen wir weiter zum Laden den Häuser. Dies werden wir ebenfalls im OnGameModeInit() Callback durchführen, sodass alle Häuser geladen sind, sobald der erste Spieler den Server betritt. Im folgenden werden cache-Funktionen, die in BlueG's MySQL R7 zum ersten mal zur Verfügung stehen, verwendet. Bei Problemen könnt ihr euch auch Tutorials über das Caching anschauen (die gibt es auf english als auch auf deutsch). Wir erweitern den Callback also um eine Funktion, welche unseren ersten Query ausführen soll:
public OnGameModeInit() { // Wir bauen eine Verbindung auf, sobald der Gamemode geladen wird.
// Verbindung mit den oben definierten Parametern aufbauen und Handle übergeben.
sqlHandle = mysql_connect(SQL_HOST, SQL_USER, SQL_DATABASE, SQL_PASSWORD);
// Testen, ob eine Verbindung besteht - Falls nein, Fehlermeldung + Exit!
if(mysql_ping(sqlHandle) != 1) {
print("MySQL Error: Es konnte keine Verbindung zur Datenbank hergestellt werden.");
SendRconCommand("exit");
}
/* Query ausführen, welcher unsere Werte aus der Tabelle holt.
Parameter: connectionHandle, query[], bool:cache, callback[], format[], {Float, ... }
Caching aktiviert, da wir Werte auslesen (SELECT) und später übergeben müssen. All dies werden wir in OnGameModeLoadHouses tun.
*/
mysql_function_query(sqlHandle, "SELECT `hID`, `hPreis`, `hBesitzer`, `hInterior`, `hX`, `hY`, `hZ` FROM `breadfish_houses`", true, "OnGameModeLoadHouses", "", "");
return 1;
}
Zu beachten beim Formulieren von Queries ist:
- Tabellennamen und Feldnamen mit Backticks (´) schreiben, sodass diese von MySQL-Begriffen unterschieden werden können.
- Falls -Strings- (keine Zahlen!) in den Query eingebaut werden, diese immer zuvor escapen, um Fehler des Querys zu verhinden (Hilfeleistung durch mysql_format()).
- * zu verwenden ist bei kleineren Queries akzeptabel, bei größeren jedoch sollte man jedoch die zusätzliche Arbeit MySQL ersparen.
- Standartmäßig werden MySQL-Anweisungen komplett groß geschrieben.
Da MySQL R7 alle Queries, die ausgeführt werden, threaded (das heißt auf einem anderen Thread weiterlaufen lässt, um den Programmfluss nicht zu stoppen, bis der Query ein Ergebnis liefert) müssen wir nun den Umweg über diesen Callback gehen, der auf dem 2. Thread laufen wird (Extra Tutorial mit MySQL R6 zum gleichen Sachverhalt: Click here!). Der Grund liegt darin, dass MySQL R7 die Queries in eine Art Warteschlange einreiht und Schritt für Schritt abarbeitet. Dabei läuft der eigentliche Quellcode weiter und wartet nicht auf eine Antwort. Da es so ohne Warten auf Beendigung des Queries dazu kommen kann, dass wir NULL-Werte einlesen, lassen wir den Callback nach Erreichen eines Ergebnisses aufrufen. Daher gehen wir nun wie folgt vor:
#define MAX_HOUSES (200) // Definieren der Häuserslots - Kann jederzeit erhöht werden, jedoch desto mehr, desto mehr Arbeit für das Script.
new sqlHandle,
hausInfo[MAX_HOUSES][hausEnumerator]; // Array, welcher in Verbindung mit dem Enumerator die Daten der Häuser hält.
forward OnGameModeLoadHouses(); // Wir verfügen über einen public-Callback - dieser muss geforwarded werden -vor- Gebrauch der Funktion.
public OnGameModeLoadHouses() {
new rows, fields, content[MAX_PLAYER_NAME]; // Deklaration der benötigten Variablen (content muss max. so groß wie ein Username sein).
cache_get_data(rows, fields); // Die Anzahl der Reihen und Spalten der Ergebnismenge herauslesen und abspeichern.
for(new i = 0; i != rows; i++) { // Schleife die Anzahl der Reihen (= Anzahl der Häuser) durchlaufen lassen.
cache_get_row(i, 0, content); // Daten einer Zeile im String 'content' speichern.
hausInfo[i][hID] = strval(content); // HausArray mit Enumerator mit dem Integer (strval()) füllen.
cache_get_row(i, 1, content); // Wiederholung für andere Werte ...
hausInfo[i][hPreis] = strval(content);
cache_get_row(i, 2, content); // Feld steigt nach und nach wie auch im Query von links nach rechts.
format(hausInfo[i][hBesitzer], MAX_PLAYER_NAME, "%s", content);
cache_get_row(i, 3, content);
hausInfo[i][hInterior] = strval(content);
cache_get_row(i, 4, content);
hausInfo[i][hX] = floatstr(content);
cache_get_row(i, 5, content);
hausInfo[i][hY] = floatstr(content);
cache_get_row(i, 6, content);
hausInfo[i][hZ] = floatstr(content);
}
printf("Haussystem: Es wurden %i Häuser geladen.", rows); // Ausgabe der Anzahl der geladenen Häuser.
return 1;
}
Um den Vorgang des Ladens der Häuser zu vollenden fehlt noch der letzte Schritt, nähmlich der eigentlichen Generierung der Häuser auf der Karte. Wir erweitern unseren Callback also um eine Funktion, welche uns die Daten in Praktisches umwandeln soll und erweiteren unseren Enumerator um zwei Halter, welche jedoch nicht in der Datenbank abzuspeichern sind. Ebenfalls definieren wir am Kopf des Scriptes, wann ein String NULL ist:
#define HAUS_TEXT_COLOR (0xE2A31DFF) // Farbe des 3D-Text-Labels
#if !defined isnull
#define isnull(%1) \
((!(%1[0])) || (((%1[0]) == '\1') && (!(%1[1]))))
#endif
enum hausEnumerator {
hID, // Eindeutige ID des Hauses (Typ: INT)
hPreis, // Preis zum Erwerb des Hauses (Typ: INT)
hBesitzer[MAX_PLAYER_NAME], // Name des Besitzers des Hauses (Typ: VARCHAR ; Größe: MAX_PLAYER_NAME = 24)
hInterior, // Interior des Hauses (nicht die InteriorID - mehr dazu später!) (Typ: INT)
Float:hX, // X-Koordinate des Hauseingangs (Typ: FLOAT)
Float:hY, // Y-Koordinate des Hauseingangs (Typ: FLOAT)
Float:hZ, // Z-Koordinate des Hauseingangs (Typ: FLOAT)
hCpID, // CheckpointID (Nicht in Datenbank abzuspeichern!)
Text3D:h3DText // ID des 3DText-Labels (Nicht in Datenbank abzuspeichern!)
};
public OnGameModeLoadHouses() {
new rows, fields, content[MAX_PLAYER_NAME]; // Deklaration der benötigten Variablen (content muss max. so groß wie ein Username sein).
cache_get_data(rows, fields); // Die Anzahl der Reihen und Spalten der Ergebnismenge herauslesen und abspeichern.
for(new i = 0; i != rows; i++) { // Schleife die Anzahl der Reihen (= Anzahl der Häuser) durchlaufen lassen.
cache_get_row(i, 0, content); // Daten einer Zeile im String 'content' speichern.
hausInfo[i][hID] = strval(content); // HausArray mit Enumerator mit dem Integer (strval()) füllen.
cache_get_row(i, 1, content); // Wiederholung für andere Werte ...
hausInfo[i][hPreis] = strval(content);
cache_get_row(i, 2, content); // Feld steigt nach und nach wie auch im Query von links nach rechts.
format(hausInfo[i][hBesitzer], MAX_PLAYER_NAME, "%s", content);
cache_get_row(i, 3, content);
hausInfo[i][hInterior] = strval(content);
cache_get_row(i, 4, content);
hausInfo[i][hX] = floatstr(content);
cache_get_row(i, 5, content);
hausInfo[i][hY] = floatstr(content);
cache_get_row(i, 6, content);
hausInfo[i][hZ] = floatstr(content);
CreateHouseOnMap(i); // Übergabe des Indexes im Array zur einfacheren Handhabung.
}
printf("Haussystem: Es wurden %i Häuser geladen.", rows); // Ausgabe der Anzahl der geladenen Häuser.
return 1;
}
stock CreateHouseOnMap(hausID) {
// Erstellen eines neuen Checkpoints am Eingang des Hauses. Die CheckpointID wird im Array gespeichert.
hausInfo[hausID][hCpID] = CreateDynamicCP(hausInfo[hausID][hX], hausInfo[hausID][hY], hausInfo[hausID][hZ], 1.5);
new labelText[70]; // Erstellen eines neuen Strings zur Formatierung eines 3D-Text-Labels.
if(isnull(hausInfo[hausID][hBesitzer])) { // Falls das Haus noch keinen Besitzer hat (dann ist der String NULL) ...
format(labelText, sizeof(labelText), "- Dieses Haus ist zu kaufen! -\nPreis: $%i", hausInfo[hausID][hPreis]); // ... setze Text.
} else { // Falls doch ...
format(labelText, sizeof(labelText), "- Dieses Haus ist in Besitz! -\nBesitzer: %s", hausInfo[hausID][hBesitzer]); // ... setze Text.
}
// Erstellen des 3D-Text-Labels mithilfe des Streamers. Ebenfalls wird der Text in die Mitte des Checkpoints gesetzt und die Streamdistance auf 5 gesetzt,
// sodass der Text nur in unmittelbarer Nähe zu sehen ist.
hausInfo[hausID][h3DText] = CreateDynamic3DTextLabel(labelText, HAUS_TEXT_COLOR, hausInfo[hausID][hX], hausInfo[hausID][hY], hausInfo[hausID][hZ] - 0.3, 5);
return 1;
}