12.1 Was bedeutet „guter Code“ im MUD?
„Guter Code“ ist ein Begriff, der schnell beliebig wirkt — was für eine Person
elegant ist, ist für eine andere überkomplex. Aber im Kontext eines MUD gibt es
klare Kriterien, weil ein MUD eine Reihe ungewöhnlicher Eigenschaften hat:
Es läuft jahrelang ohne Neustart, es wird von vielen Magierinnen
und Magiern gemeinsam entwickelt, und es muss persistent mit Hunderten von
Objekten klarkommen, die zur selben Zeit aktiv sind.
Guter LPC-Code ist deshalb nicht der cleverste oder kürzeste Code.
Cleverer Code ist oft schwer zu verstehen — und in einem MUD muss dein Code
auch in zwei Jahren noch debuggbar sein, möglicherweise durch jemand anderen,
der gerade neu in der Mudlib ist. In einem MUD ist guter Code vor allem:
- Verständlich für andere (und für dich selbst in einem Jahr).
Lesbarkeit schlägt Knappheit. Eine Variable, die
schaden_durch_feuer
heißt, ist besser als fdsm, auch wenn sie länger ist.
- Robust gegenüber Fehlern und unerwarteten Situationen.
Spieler tun verrückte Dinge, Objekte verschwinden, das MUD ist nie
„im Standardzustand“. Dein Code muss damit rechnen.
- Leicht erweiterbar. Heute ist es ein Hebel, der eine Tür öffnet —
morgen soll er auch noch einen Alarm auslösen, eine Quest fortschreiben, ein Log
schreiben. Wenn du das von Anfang an einplanst (z.B. über Hooks/Events),
sparst du dir später viele Umbauten.
- Sicher im Umgang mit fremden Objekten. Ein Item kann jederzeit
zerstört werden, ein Spieler kann ausloggen, ein Raum kann neu geladen werden.
Defensive Prüfungen sind Pflicht.
- Konform mit der Mudlib. Wenn die Mudlib eine Standard-Lösung
bietet (
AddItem, SetProp, ReceiveMsg),
benutze sie. Eigene Lösungen für gelöste Probleme bringen Bugs und sind
schwerer zu warten.
Viele Anfänger versuchen, möglichst „mächtig“ zu programmieren — sie wollen ihre
Fähigkeiten zeigen, raffinierte Lösungen finden, die alte Hasen beeindrucken.
Erfahrene Magier dagegen versuchen, möglichst langweiligen Code zu schreiben:
wenig Zeilen, viele Kommentare, alles geradeaus, keine Tricks. Warum?
Langweiliger Code ist stabil. Er hat weniger Stellen, an denen
Bugs sich verstecken können, weniger Wechselwirkungen, weniger Überraschungen.
Wenn du einen Hebel programmierst, der „nur“ einen Hebel macht — und genau das
sauber tut —, hast du gewonnen. Ein Hebel, der gleichzeitig drei verschiedene
Tricks beherrscht und mit fünf Sonderzeichen-Befehlen experimentiert, wird in
sechs Monaten als „zu komplex zum Anfassen“ gemieden.
Merksatz: „Code wird einmal geschrieben, aber zehnmal gelesen.“
Investiere die zusätzlichen Sekunden in Klarheit beim Schreiben — du sparst Stunden
beim späteren Lesen und Debuggen.
12.2 Klarer Aufbau von Objekten
Ein häufiges Anfängerproblem ist chaotischer Code:
Funktionen überall, globale Variablen ohne Struktur,
Logik im falschen Objekt.
Bewährtes Grundschema für viele Objekte:
// 1. Includes & Inherits
#include <properties.h>
inherit "/std/thing";
// 2. Konstanten & Defines
#define MAX_USES 3
// 3. Membervariablen
int uses;
// 4. create()
protected void create() {
::create();
uses = MAX_USES;
}
// 5. Öffentliche Schnittstellen (Spielerbefehle)
int cmd_use(string str) { ... }
// 6. Interne Hilfsfunktionen
protected void do_effect(object who) { ... }
Diese Reihenfolge ist keine Pflicht, aber sie hilft enorm,
sich schnell im Code zurechtzufinden.
12.3 Trennung von Verantwortung
Eine der wichtigsten Regeln guter Architektur lautet:
Ein Objekt sollte genau eine Hauptaufgabe haben.
Schlechte Beispiele:
- Ein Raum, der Quests verwaltet
- Ein Item, das Spielerlevel verändert
- Ein NPC, der globale Spielzustände speichert
Bessere Aufteilung:
- Raum: Umgebung & Beschreibung
- Item: Interaktion & Effekte
- Questobjekt: Questlogik
- Serviceobjekt: Berechnungen, Verwaltung
Wenn du merkst, dass ein Objekt immer größer wird,
ist das meist ein Zeichen, dass du es aufteilen solltest.
12.4 Lesbarkeit vor Cleverness
LPC erlaubt sehr kompakte Konstrukte.
Aber kompakt heißt nicht automatisch gut.
Schwer lesbar
if(o&&living(o)&&interactive(o)&&o->QueryProp(P_HP)>0)
o->SetProp(P_HP,o->QueryProp(P_HP)-10);
Besser lesbar
if (!objectp(o)) return;
if (!living(o)) return;
if (!interactive(o)) return;
int hp = o->QueryProp(P_HP);
if (hp <= 0) return;
o->SetProp(P_HP, hp - 10);
Ja, das ist länger. Aber Fehler lassen sich hier viel leichter finden.
12.5 Defensive Programmierung als Standard
In einem laufenden MUD können jederzeit Dinge passieren:
Spieler loggen aus, Objekte werden zerstört, Räume neu geladen.
Deshalb gilt:
- Jeder Objektzugriff kann fehlschlagen
- Jeder Funktionsaufruf kann einen Fehler werfen
- Jede Referenz kann plötzlich
0 sein
Beispiel mit catch()
mixed res;
if (catch(res = ob->DangerousCall())) {
log_file("errors", "DangerousCall failed in "+object_name(this_object())+"\n");
return;
}
Defensive Programmierung fühlt sich am Anfang „übertrieben“ an,
spart dir aber später extrem viel Zeit.
12.6 Weniger globale Variablen
Globale Variablen sind bequem, aber gefährlich.
Sie machen Abhängigkeiten unsichtbar und Fehler schwer nachvollziehbar.
Problematisch
object last_user;
void use(object who) {
last_user = who;
}
Was passiert, wenn zwei Spieler gleichzeitig interagieren?
Besser
void use(object who) {
// benutze who lokal
}
Speichere nur das, was wirklich dauerhaft zum Objekt gehört.
12.7 Logging statt stilles Scheitern
Ein häufiger Anfängerfehler ist: Fehler passieren — aber niemand merkt es.
Dein Code prüft eine Bedingung, sie schlägt fehl, der Code kehrt einfach um, und
der Spieler steht da und wundert sich, warum nichts passiert ist. Aus deiner Sicht
ist alles in Ordnung („return 0 — geht halt nicht“), aber für den Spieler ist es ein
Bug.
Schlimmer noch: Manchmal stimmt der Fehler gar nicht mit deiner Erwartung überein.
Vielleicht hast du erwartet, dass ob ein bestimmtes Item ist —
in Wahrheit ist es ein anderes oder gar 0. Ohne Logs hast du keine Chance, das
herauszufinden, ohne stundenlang manuell zu testen.
Schlecht: stilles Scheitern
if (!ob) return;
Funktioniert technisch — aber dein zukünftiges Ich (oder ein anderer Magier) hat keine
Ahnung, dass der Code an dieser Stelle abbricht. Wenn der Bug erst Wochen später
auffällt, weißt du nicht, wann ob erstmals fehlte.
Besser: mit Log-Eintrag
if (!ob) {
log_file("debug",
"ob missing in " + object_name(this_object()) + "\n");
return;
}
Jetzt kannst du in /log/debug nachsehen, wann der Fehler aufgetreten ist
und in welchem Objekt. Das ist Gold wert beim Debuggen.
Was wann loggen?
Faustregel: Logge alles, was unerwartet ist. Erwartete Fehlerfälle
(z.B. Spieler tippt ein falsches Kommando) musst du nicht loggen — sonst füllen sich
deine Logs mit Nichts. Aber unerwartete Fehler — fehlende Objekte, ungültige Argumente,
nicht erfüllte Vorbedingungen — solltest du dokumentieren:
// Erwartet: Spieler hat keine Schluessel
if (!present("schluessel", pl))
return notify_fail("Du hast keinen Schluessel.\n"), 0;
// UNerwartet: Tuer-Objekt fehlt im Raum
if (!objectp(tuer_ob)) {
log_file("debug/raum",
sprintf("[%s] Tuer-Objekt fehlt in %O\n",
ctime(time()), this_object()));
return 0;
}
Strukturierte Log-Dateien
Lege deine Logs unter sinnvollen Namen ab — nicht alles in eine einzige Riesendatei.
Übliche Namensschemata: "debug/<modul>",
"error/<modul>", "audit/<quest>":
log_file("debug/quest_drache", text);
log_file("error/movement", text);
log_file("audit/login", text);
Logs sind deine Augen, wenn etwas schiefgeht, während du nicht online bist.
Sie sind oft der Unterschied zwischen „Bug nach 5 Minuten gefunden“ und
„Bug nach 5 Tagen Tests immer noch nicht reproduziert“.
12.8 Zusammenarbeit & Code-Stil
LPC-Code wird selten nur von einer Person gelesen. Im Midgard MUD arbeiten
viele Magierinnen und Magier gemeinsam an einer großen Welt. Selbst wenn du
ein Gebiet komplett alleine baust: Eines Tages wird vielleicht jemand übernehmen,
oder du selbst wirst nach drei Jahren zurückkommen und fragen: „Was hat sich der
Verfasser dabei nur gedacht?“
Code-Stil ist deshalb keine Geschmacksfrage — er ist Kommunikation
zwischen dir und allen, die später deinen Code lesen. Und das schließt dich selbst ein.
Achte daher auf:
- Einheitliche Einrückung (im Midgard üblich: 2 oder 4 Leerzeichen,
niemals Tab-Stops mischen). Konsistenz ist wichtiger als die genaue Wahl.
- Sinnvolle Funktions- und Variablennamen —
cmd_oeffnen statt f1, last_attacker statt
x. Jeder Name sollte beim Lesen sofort den Zweck verraten.
- Kommentare bei nicht offensichtlicher Logik —
besonders wenn du etwas Trickreiches machst, einen Workaround einbaust oder eine
Begründung hast, die im Code nicht sichtbar ist.
- Keine „magischen Zahlen“ ohne Erklärung. Wenn dein Code
if (level > 27) schreibt, ist unklar, warum 27. Besser:
#define MAX_NPC_LEVEL 27 oder zumindest ein Kommentar.
- Konsistente Modifier-Reihenfolge: Im Midgard üblich
private varargs int foo() — modifier vor varargs vor Rückgabetyp.
- Strings in adjazenter Schreibweise statt mit
+:
das ist effizienter und liest sich besser.
Kommentare: Warum, nicht Was
Der Klassiker des schlechten Kommentars:
i++; // erhöhe i um 1
Das sieht jeder. Schlechte Kommentare sind Lärm — sie blähen den Code auf, ohne
Wert zu liefern. Bessere Kommentare erklären warum etwas passiert,
nicht was:
// Wir starten bei 1, weil Index 0 die "leere" Eintragung ist —
// die ersten Spieler bekommen IDs ab 1.
int next_id = 1;
// 5 Sekunden Verzögerung, damit der Spieler die Animation sieht
// bevor das Objekt verschwindet.
call_out("destruct", 5);
// HACK: Diese Zeile umgeht einen Bug in /std/room/exits.c bis
// Issue #234 gefixt ist. Bitte beim nächsten Update entfernen.
SetProp(P_EXITS, ([ ]));
Kommentare sind aber auch nicht der Ersatz für Klarheit. Wenn du fünf Zeilen
Kommentar brauchst, um drei Zeilen Code zu erklären — vielleicht ist der Code
zu kompliziert. Refaktoriere in eine sprechend benannte Funktion.
Code-Reviews
Bevor du etwas Größeres ins MUD bringst, lass es einen erfahrenen Magier
anschauen. Frischer Blick fängt Fehler, die du selbst nicht mehr siehst,
weil du den Code zu lange im Kopf hattest.
12.8a Effizienz-Tipps (verdichtet aus /doc/concepts/effizienz)
Lesbarkeit hat im Midgard MUD Vorrang vor Effizienz – aber es gibt ein paar
Faustregeln, die nichts kosten und einen messbaren Unterschied machen.
Schnelle Wins
foreach ist schneller und lesbarer als for+Index.
- Lokale Arrays mit bekannter Größe vorab anlegen:
int *x = allocate(10); statt x = ({}); x += ({i}); in der Schleife.
this_player(), this_object(), environment(),
previous_object() sind so schnell wie Variablen – nur dann zwischenspeichern,
wenn der Wert sich ändern kann.
present_clone() ist billiger als present()+geschützte IDs, wenn du
ein konkretes Objekt suchst.
- Mengen-Löschen aus Arrays: gleiche Werte auf 0 setzen, dann
array -= ({0}).
x & y oder x | y auf Arrays geht oft schneller als handgeschriebene Schleifen.
Speicher
- Globale Mappings/Arrays/Strings klein halten und nur bei Bedarf erweitern.
- Wiederholten gleichen Code in eine geerbte Klasse ziehen – spart Programmgröße.
- Blueprints sind teurer zu laden als Clones zu erzeugen. Blueprint-Initialisierung
kann oft übersprungen werden:
protected void create() {
if (!clonep(this_object())) {
set_next_reset(-1); // BP nicht resetten
return;
}
::create();
// restliche Initialisierung
}
Lambda vermeiden, Inline-Closures bevorzugen
Lambdas sind schwer zu lesen und meistens langsamer als Inline-Closures.
Im modernen Midgard-Code:
// schlecht
filter(users(),
lambda(({'x}), ({#'call_other,'x,"QueryProp",P_SECOND})));
// besser (Inline-Closure)
filter(users(), function int (object u)
{ return u->QueryProp(P_SECOND); });
// am besten (eigene Lfun + Lfun-Closure)
private int _is_second(object u) { return u->QueryProp(P_SECOND); }
filter(users(), #'_is_second);
call_out & heart_beat sparsam
- Jeder aktive
call_out hält ein Objekt am Auswappen – das kostet Speicher.
- NPCs sollten
set_heart_beat(0) rufen, sobald sie alleine im Raum sind.
- Für regelmäßige Tickjobs lieber
set_next_reset()+reset()
als call_out-Ketten.
call_out(..., 0) niemals in rekursiven Callouts – das läuft sofort wieder.
12.8b Style-Konventionen im Midgard MUD
- Zeilenlänge max. 80 Zeichen – das ist immer noch der Standard.
- Strings über Zeilen brechen mit nebeneinanderstehenden Literalen, nicht mit
+:
SetProp(P_LONG,
"Erste Zeile.\n"
"Zweite Zeile.\n");
- Kein
this_object()->funktion() aus Faulheit – lokale Funktionen ohne
-> sind schneller und klarer.
- NPC-Sprache: nie Anführungszeichen um Gesagtes setzen
(also nicht
"sage \"Hallo\"") – im Midgard-Stil ist das ohne Quotes.
- UTF-8: echte Umlaute in LPC-Strings (
ä, ö, ü, ß), niemals
ae/oe/ue/ss.
- Pfade über zentrale
#define-Header in local.h, nicht hardcodiert.
#pragma strong_types, #pragma save_types und
#pragma rtt_checks sind ausdrücklich erwünscht – sie fangen Fehler früh ab.
- Items, die ein Spieler über Logout behalten soll, brauchen
SetProp(P_AUTOLOADOBJ, 1).
12.9 Typische Anfängerfallen – zusammengefasst
- zu große Objekte
- direktes Manipulieren fremder Objekte
- fehlende Sicherheitsprüfungen
- keine Fehlerbehandlung
- übermäßig cleverer, schwer lesbarer Code
Fast jeder erfahrene Magier ist genau in diese Fallen getappt.
Wichtig ist nicht, sie nie zu machen –
sondern sie zu erkennen und daraus zu lernen.
12.10 Abschluss & Ausblick
Du hast jetzt ein vollständiges Grundlagenverständnis von LPC:
von Datentypen über Objekte, Vererbung, Events, Sicherheit
bis hin zu sauberer Architektur.
Der nächste Schritt ist Praxis:
kleine Räume bauen, Items schreiben, Code lesen,
mit erfahrenen Magiern sprechen.
Guter LPC-Code entsteht nicht über Nacht –
sondern durch viele kleine Verbesserungen.
Willkommen in der Welt der MUD-Entwicklung.
Fast fertig!
Du hast das komplette Anfänger-Manual gelesen.
Nutze es als Nachschlagewerk, kehre zu Kapiteln zurück
und erweitere dein Wissen Schritt für Schritt.
In Kapitel 13 gibt es noch ein großes Beispielprojekt zum Lernen. Das wird toll!
Weiter zu Kapitel 13
Großes Lernbeispiel – Waldlichtung zur Wikingerzeit (Raum, Hebel, Versteck, NPC, Items)