MIDGARD · Multi User Dungeon Alpha 0.1

Kapitel 5: Funktionen & Rückgabewerte

Funktionen sind das Herz von LPC. Ohne Funktionen gäbe es keine Logik, kein Verhalten und keine Interaktion. In diesem Kapitel lernst du von Grund auf, was Funktionen sind, wie sie aufgebaut sind, wie Parameter funktionieren, wie Rückgabewerte genutzt werden und wie man typische Anfängerfehler vermeidet. Wir gehen bewusst langsam vor – dieses Kapitel ist lang, aber essenziell.

Weiter zu Kapitel 6

Danach: Kontrollstrukturen – Schleifen, switch, break.

5.1 Was ist eine Funktion?

Eine Funktion ist ein benannter Codeblock, der eine bestimmte Aufgabe erfüllt. Du kannst dir eine Funktion wie eine Maschine vorstellen: Du gibst ihr eventuell etwas hinein (Parameter), sie arbeitet damit – und gibt vielleicht etwas zurück (Rückgabewert).

Beispiele aus dem echten Leben:

  • „addiere zwei Zahlen“
  • „prüfe, ob ein Spieler lebt“
  • „gib eine Beschreibung zurück“

In LPC ist fast alles eine Funktion: Ausgaben, Abfragen, Aktionen. Je besser du Funktionen verstehst, desto sauberer wird dein Code.

5.2 Der grundlegende Aufbau einer Funktion

Jede Funktion besteht aus vier Teilen:

  • Rückgabewert-Typ
  • Name der Funktion
  • Parameterliste (optional)
  • Funktionskörper

int add(int a, int b)
{
  return a + b;
}
      

Das liest sich so: „Die Funktion add bekommt zwei Zahlen und gibt eine Zahl zurück.“

5.3 Rückgabewerte – was kommt aus einer Funktion heraus?

Der Rückgabewert ist das Ergebnis einer Funktion. Sein Typ steht ganz vorne.


int get_level()
{
  return 10;
}
      

Die Funktion gibt immer einen int zurück. Das Schlüsselwort return beendet die Funktion sofort.

Du kannst Rückgabewerte direkt benutzen:


int lvl;

lvl = get_level();
      

5.4 void-Funktionen – wenn nichts zurückkommt

Manche Funktionen sollen nichts zurückgeben, sondern nur etwas tun (z.B. Text ausgeben oder einen Zustand ändern).


void say_hello()
{
  write("Hallo!\n");
}
      

void bedeutet: kein Rückgabewert. Du darfst das Ergebnis dieser Funktion nicht speichern.


// falsch!
int x = say_hello();
      

5.5 Parameter – Werte an Funktionen übergeben

Parameter sind Eingabewerte für Funktionen. Sie stehen in den runden Klammern.


int square(int x)
{
  return x * x;
}
      

Aufruf:


int result;

result = square(5);  // result = 25
      

Parameter sind lokale Variablen. Sie existieren nur während der Funktionsausführung.

5.6 Mehrere Parameter

Funktionen können mehrere Parameter haben. Jeder Parameter hat einen eigenen Datentyp.


string full_name(string first, string last)
{
  return first + " " + last;
}
      

string name;

name = full_name("Odin", "Allvater");
      

5.7 Funktionen auf Objekten aufrufen

LPC ist objektorientiert. Das heißt: Du rufst Funktionen oft auf anderen Objekten auf.


object pl;

pl = this_player();
write(pl->query_name());
      

Der Operator -> bedeutet: „Rufe diese Funktion auf diesem Objekt auf.“

Sehr wichtig: Prüfe immer, ob das Objekt existiert!


if (pl)
  write(pl->query_name());
      

5.8 Frühzeitiges Verlassen einer Funktion

Mit return kannst du eine Funktion jederzeit verlassen. Das wird oft für Prüfungen genutzt.


int can_enter(object pl)
{
  if (!pl)
    return 0;

  if (pl->query_level() < 10)
    return 0;

  return 1;
}
      

Diese Struktur ist sehr typisch im Midgard MUD.

5.9 Sichtbarkeit von Funktionen

Wie bei Variablen gibt es auch bei Funktionen Sichtbarkeiten.


public int query_hp();
protected void heal();
private void internal_reset();
      
  • public – jeder darf die Funktion aufrufen
  • protected – nur dieses Objekt und Erben
  • private – nur dieses Objekt selbst

Anfänger-Tipp: Mache nur das public, was wirklich von außen gebraucht wird.

5.10 Häufige Anfängerfehler bei Funktionen

  • Falscher Rückgabewert-Typ
  • return vergessen
  • Parameter in falscher Reihenfolge
  • Objekte nicht auf 0 prüfen
  • Zu große Funktionen schreiben

// falsch: kein return
int broken()
{
  int x = 5;
}
      

5.11 Gute Funktionsnamen

Funktionsnamen sind extrem wichtig für Lesbarkeit.

  • query_hp()
  • set_name(string n)
  • doit()
  • f1()

Gute Namen sparen Erklärungen.

5.12 Mini-Übungen

  1. Schreibe eine Funktion int is_even(int x).
  2. Schreibe eine Funktion, die einen Namen zurückgibt.
  3. Schreibe eine Funktion, die prüft, ob ein Spieler existiert.

int is_even(int x)
{
  if (x % 2 == 0)
    return 1;
  return 0;
}
      

5.13 Zusammenfassung

  • Funktionen kapseln Logik
  • Parameter sind Eingaben
  • Rückgabewerte sind Ergebnisse
  • return beendet Funktionen
  • Sichtbarkeit schützt vor Fehlern

Wenn du dieses Kapitel verstanden hast, kannst du aktiv Verhalten programmieren.

5.14 Für Fortgeschrittene: Closures in LPC (Funktionen als Werte)

In vielen LPC-Beispielen siehst du Funktionsnamen als Strings: Man schreibt irgendwo "cmd_suchen", und die Mudlib ruft diese Funktion später auf. Das funktioniert – aber es hat Grenzen: Strings sind nur Text. Ein Tippfehler wird oft erst zur Laufzeit bemerkt, und du kannst schwer „Kontext“ mitgeben (also: welche Daten sollen beim Aufruf schon fest eingebaut sein?). Genau hier kommen Closures ins Spiel.

Eine Closure ist vereinfacht gesagt ein „Funktionszeiger“ oder „Funktionsobjekt“: Du speicherst nicht den Namen als Text, sondern eine echte Referenz auf ausführbaren Code. Das bedeutet: Du kannst Closures in Variablen speichern, in Arrays oder Mappings ablegen, als Argument an Funktionen übergeben und später ausführen. Damit wird LPC deutlich flexibler – vor allem für Callbacks, Filter, Sortierungen, Ereignisse und generische Helferfunktionen.

1) Warum Closures? Ein Anfängerbild

Stell dir vor, du willst eine Liste von Objekten durchsuchen und nur die behalten, die eine bestimmte Eigenschaft erfüllen. Du könntest hart eine Schleife schreiben, überall wiederholen, und jedes Mal die Bedingung anpassen. Mit Closures kannst du stattdessen sagen: „Hier ist die Bedingung als Funktion – wende sie auf jedes Objekt an.“ Das ist sauberer, kürzer und wiederverwendbar.

2) Closure-Grundformen (das Wichtigste, ohne Overkill)

Je nach Driver/Mudlib gibt es unterschiedliche Schreibweisen. Im Midgard MUD sieht man häufig diese Form: #'funktionsname. Das bedeutet: „Nimm eine Referenz auf diese Funktion.“ Viele Beispiele nutzen Closures auch indirekt, ohne dass es groß erklärt wird – zum Beispiel bei filter(), map() oder bei Properties, die eine Closure akzeptieren.


// Beispiel: Closure auf eine Funktion in diesem Objekt
closure c = #'is_wizard;

// Später ausführen (konkret: über funcall)
if (funcall(c, this_player()))
  write("Du bist ein Magier.\n");

int is_wizard(object who)
{
  return who && IS_WIZARD(who);
}
  

Das neue Wort hier ist funcall(): Damit rufst du eine Closure auf, als würdest du eine Funktion aufrufen. Du übergibst die Argumente genauso wie bei normalen Funktionen.

3) Closures mit filter() und map() (super praktisch)

Zwei sehr typische Helfer sind filter() und map(). Sie arbeiten auf Arrays. filter() behält nur Elemente, für die die Bedingung wahr ist. map() wandelt jedes Element um (z.B. Objekt → Name).


// Angenommen, du hast ein Array von Objekten:
object *inv = all_inventory(this_player());

// 1) Filter: nur lebende Wesen (living)
object *livings = filter(inv, #'living);

// 2) Map: aus Objekten Strings machen (z.B. object_name)
string *names = map(livings, #'object_name);

// Ausgabe
printf("Lebende Objekte: %O\n", names);
  

Wichtig: Hier siehst du ein schönes Prinzip: Du gibst nicht „den Namen einer Funktion als String“ an, sondern die Funktion selbst als Closure. Dadurch ist vieles robuster. Wenn es die Funktion nicht gibt, fällt es typischerweise früher auf. Und du kannst leichter zwischen Funktionen wechseln, ohne überall Strings auszutauschen.

4) Closures mit Kontext (warum das so mächtig ist)

Eine häufige Anfängerfrage: „Wie filtere ich nach etwas, das ich zur Laufzeit kenne – z.B. nach einer ID, einem Grenzwert oder einem Namen?“ Genau dafür sind Closures ideal, weil du den Kontext als Parameter mitschicken kannst – entweder direkt beim Aufruf oder indem du eine Hilfsfunktion baust, die zusätzliche Argumente akzeptiert.


// Wir filtern Inventar nach einer bestimmten ID (z.B. "flea").
// Idee: Hilfsfunktion, die (object ob, string id) prüft.
int has_id(object ob, string id)
{
  return ob && ({int})ob->id(id);
}

void demo_filter_by_id(string id)
{
  object *inv = all_inventory(this_player());

  // filter() ruft has_id(obj, id) auf, wenn der Driver/Mudlib extra Argumente unterstützt
  // (Im Midgard-Kontext ist das oft möglich. Wenn nicht, nimm eine andere Strategie, siehe Hinweis unten.)
  object *hits = filter(inv, #'has_id, id);

  printf("Gefunden: %O\n", map(hits, #'object_name));
}
  

Hinweis für Anfänger: Nicht jede Kombination ist in jeder Umgebung gleich. Manche Treiber/Mudlibs unterstützen Zusatzargumente bei filter/map, andere erwarten nur (Array, Closure). Wenn Zusatzargumente nicht gehen, löst du es klassisch mit einer Schleife – oder du baust die Logik anders (z.B. erst map → Daten, dann manuell filtern). Die Grundidee bleibt: Closures helfen dir, Logik als „Baustein“ zu behandeln.

5) Objektbindung: Closure auf Funktion eines anderen Objekts

Closures können auch auf Funktionen in einem anderen Objekt zeigen. Das ist typisch bei Callbacks: Du gibst einer Funktion „was sie später aufrufen soll“. Dadurch muss der Empfänger nicht wissen, welche Funktion genau ausgeführt wird – er ruft einfach die Closure auf.


// Beispiel: Du willst, dass Objekt A später eine Funktion in Objekt B aufruft.
// (Pseudo-Beispiel, weil konkrete Syntax variieren kann.)

// In B:
void notify(string msg) { tell_object(this_player(), msg); }

// In A:
void do_callback(closure cb)
{
  if (cb) funcall(cb, "Callback wurde ausgelöst.\n");
}

// Aufrufidee:
object b = this_object(); // nur als Beispiel
closure cb = #'notify;
do_callback(cb);
  

Warum ist das gut? Weil du „do_callback“ generisch hältst: Es kann jeden Callback ausführen, solange er dieselbe Art von Argumenten akzeptiert. So baut man flexible Systeme (Events, Hooks, Verzögerungen, Timer, UI-Reaktionen, Auslöser wie dein Hebel im Kapitel-13-Beispiel).

6) Typische Anfängerfehler

  • Strings mit Closures verwechseln: "is_wizard" ist Text, #'is_wizard ist Code-Referenz. Text kann Tippfehler verstecken – Closures sind meist robuster.
  • Falsche Argumente bei funcall: Wenn die Funktion zwei Parameter erwartet, musst du auch zwei übergeben. Sonst knallt es (oder liefert falsche Ergebnisse).
  • Zu früh optimieren: Wenn du gerade erst Schleifen lernst, ist es okay, erst die Schleife zu schreiben. Closures sind ein Werkzeug für Eleganz und Wiederverwendung – kein Muss für jede Kleinigkeit.

7) Wo du Closures im MUD sofort wiedertriffst

Du wirst Closures häufig sehen bei: filter, map, Sortierfunktionen, Event/Hooks-Systemen, manchmal bei Properties (wenn eine Property statt Text eine Funktion zur Berechnung akzeptiert), und bei Tools/Debugging (einige Mechaniken im MGtool arbeiten intern stark callback-orientiert). Wenn du Closures verstanden hast, erkennst du viele „magische“ Patterns plötzlich als ganz normal: „Aha, da wird Code als Wert weitergereicht.“

Empfehlung: Baue Closures zuerst in kleinen, ungefährlichen Stellen ein – zum Beispiel beim Filtern und Mappen von Arrays. Sobald das sitzt, kannst du damit größere Systeme entkoppeln (z.B. Hebel löst Callback aus, Raum entscheidet, was passiert).

Ausblick

In Kapitel 6 lernst du Kontrollstrukturen: Schleifen (for, while), switch, break und typische Muster im Midgard MUD.

Weiter zu Kapitel 6

Kontrolle über den Programmfluss.