Anmelden

browsergames tutorials

Der Weg zum eigenen Browsergame - Das Eventsystem

Bei einem eigenen Browsergame fängt man, in der Regel, erst einmal klein an und erweitert das Spiel schritt für Schritt um neue Funktionalitäten. Dabei kommt man sehr schnell an die Grenze der Übersichtlichkeit und man muss für jede weitere Änderung oder für jedes weitere Feature Unmengen an Zeit damit verschwenden, sich im eigenen Code zurecht zu finden. Zudem sind unübersichtliche Codes recht fehleranfällig. Es macht also Sinn den Code, schon von Anfang an, möglichst strukturiert und flexibel anzulegen. Das erschwert zwar den Einstieg, macht den Code aber lesbarer und hält ihn klein.

Ein Element einer Gameengine kommt bei der Spielentwicklung des öfteren zum Einsatz und lohnt sich daher besonders zur strukturierten Umsetzung - das Eventsystem.

Events (Ereignisse)

Als Event (Ereignis) bezeichnen wir eine Begebenheit die zu einem bestimmten Zeitpunkt eintritt. Ob die Truppen von einem erfolgreichen Beutezug zurückkehren, die Händler neue Waren bringen, oder ein Bauauftrag abschließt, an diesen Zeitpunkten muss ein Event die nötigen Skripte ausführen.

Reihenfolge

Manche Events sind voneinander abhängig und sollten daher in chronologischer Reihenfolge abgearbeitet werden. So ist die Goldkammer erst nach der Ankunft des Händlers gefüllt und plündernde Truppen nehmen das zuvor angelieferte Gold direkt wieder mit. Würde zuerst die Plünderung abgearbeitet werden, wäre noch kein Gold zum Abtransport vorhanden.

Wir müssen also sicherstellen, dass die zeitliche Reihenfolge gewahrt bleibt und die Events nacheinander ausgelöst werden.

Die Eventklasse "MyEventSystem"

Die Verwaltung der Events wird über eine Klasse mit statischen Methoden realisiert. Diese Klasse soll jeweils eine oder mehrere Skripte an einen Eventnamen binden und das Event aufrufbar machen. Dabei werden die Skripte, je nach vorher festgelegter Priorität, abgearbeitet. Im Fehlerfall bricht der Eventhandler die Ausführung der Skripte ab und liefert einen Fehlerwert.

class MyEventSystem
{
  // Liste der registrierten Events
  private static $events = array();
  // Globaler Zähler der jeden Eventeintrag eindeutig identifiziert
  private static $id_counter = 0;
  // Ergebnisliste
  private static $results = NULL;
 
  // Fügt einem Event eine Funktion/Methode hinzu
  public static function registerEvent($name, $function_name, $class_name='', $priority=3)
  {
    // Bei jeder neuen Registrierung wird der globaler Zähler um eins erhöht.
    self::$id_counter++;
    $name = strtolower($name);
    if (!isset(self::$events[$name]))
    {
      // Wenn noch kein Eintrag für diese Event existiert, wird ein Neuer angelegt
      self::$events[$name] = array();
      self::$events[$name][] = array('id'=>self::$id_counter, 'priority'=>$priority, 'function_name'=>$function_name, 'class_name'=>$class_name);
    }
    else
    {
      // Existiert bereits ein Eintrag für das Event, wird der Neue angehangen
      self::$events[$name][] = array('id'=>self::$id_counter, 'priority'=>$priority, 'function_name'=>$function_name, 'class_name'=>$class_name);
      // Im Anschluss wird nach Priorität (bzw. bei Gleichheit nach Registrierungsreihenfolge) sortiert.
      usort(self::$events[$name], 'self::compareEventPriorities');
    };
  }
  // Vergleicht zwei Events nach Priorität
  private static function compareEventPriorities($a, $b)
  {
    if ($a['priority'] > $b['priority']) return 1;
    if ($a['priority'] < $b['priority']) return -1;
    // Sind die Prioritätswerte identisch, so wird nach der Registrierungsreihenfolge sortiert
    if ($a['id'] < $b['id']) return 1;
    if ($a['id'] > $b['id']) return -1;
    return 0;
  }
  // Vergleicht zwei Zeiten (Alt vor Jung)
  public static function compareTimes($a, $b)
  {
    if ($a['time'] > $b['time']) return 1;
    if ($a['time'] < $b['time']) return -1;
    return 0;
  }
  // Führt ein Event aus
  public static function handleEvent($name, $time, $params)
  {
    // Prüfen ob mind. ein Eintrag für dieses Event existiert
    $name = strtolower($name);
    if (!isset(self::$events[$name]) || empty(self::$events[$name])) return false;
    // Alle registrierten Funktionen/Methoden nacheinander ausführen
    $result = false;
    foreach(self::$events[$name] as $event)
    {
      // Wurde eine Klasse angegeben?
      if (!empty($event['class_name']))
      {
        // .. so handelt es sich um eine Methode
        $index = strtolower($event['class_name'].'::'.$event['function_name']);
        // Aufruf der Methode mit $time und $params als Parameter
        $result = call_user_func(array($event['class_name'], $event['function_name']), $time, $params);
      }
      else
      {
        // .. andernfalls um eine Funktion
        $index = strtolower($event['function_name']);
        // Aufruf der Methode mit $time und $params als Parameter
        $result = call_user_func($event['function_name'], $time, $params);
      };
      // Das Ergebnis der Funktion/Methode wird in der Ergebnismenge gespeichert
      self::$results[$name][$index] = $result;
      if ($result===false) break;
    };
    // Das letzte Ergebnis wird zurück gegeben
    return $result;
  }
  // Liefert alle Ergebnislisten.
  public static function getResults($event_name='')
  {
    if (!empty($event_name))
    {
      // Wird ein Eventname angegeben, wird zunächst geprüft ob dieser existiert ..
      $name = strtolower($name);
      if (!isset(self::$results[$name]) || empty(self::$results[$name])) return false;
      // .. und bei erfolgreichen Prüfung zurückgegeben
      return self::$results[$name];
    }
    else
    {
      // Wird kein Eventname angegeben, wird die gesamte Ergebnisliste zurückgegeben
      return self::$results;
    }
  }
  // Setzt alle Ergebnisse zurück
  public static function resetResults(){ self::$results = array(); }
}

Die zwei Kernmethoden dieser Klasse sind registerEvent() und handleEvent(). Mittels registerEvent() lassen sich zum einen neue Events anlegen, und zum anderen einem bestehenden Event eine oder mehrere Funktionen/Methoden, inklusive Prioritäten, zuordnen. Mit handleEvent() lässt sich nun das Event mit dem genauen Zeitpunkt des Aufrufs, sowie variablen Parametern ausführen. Das Event führt nun die ihm zugeordneten Funktionen/Methoden in der festgelegten Reihenfolge aus. Tritt bei einer Funktion/Methode ein Fehler auf, so wird das gesamte Event gestoppt.

registerEvent()

Um ein neues Event anzulegen, verwenden wir die Methode registerEvent() und füttern sie mit zwei, bzw. drei Parametern. Zwei Parameter benötigen wir wenn wir das Event eine einfache Funktion aufrufen lassen wollen. Möchten wir aber eine Klassenmethode verwenden, muss als dritter Parameter der Name der Klasse übergeben werden.

In diesem Fall arbeiten wir ausschließlich mit Methoden der Klasse EventScripts1.

// Sammlung von Skripten die in den Events aufgerufen werden
class EventScripts1
{
  public function scriptA($calling_time, $params)
  {
    return $calling_time.' | '.$params['soldiers'].' Soldaten erreichen das Heimatdorf.';
  }
  public function scriptB($calling_time, $params)
  {
    return $calling_time.' | '.'Folgende Waren werden ins Lager gebracht: Gold='.intval($params['gold']).', Holz='.intval($params['wood']);
  }
  public function scriptC($calling_time, $params)
  {
    return $calling_time.' | '.$params['soldiers'].' Soldaten greifen das Dorf '.$params['target_name'].' an.';
  }
  public function scriptD($calling_time, $params)
  {
    return $calling_time.' | '.'Bauauftrag abgeschlossen. '.$params['building_name'].' wurde auf Stufe '.$params['new_level'].' ausgebaut.';
  }
}
 

In den oberen Methoden werden lediglich Ausgaben generiert. Von weiteren Logiken und Datenbankzugriffen wurde, der Einfachheit wegen, abgesehen. An dieser Stelle ist es an jedem selbst zu entscheiden was in seinem Spiel passieren soll.

Im nächsten Schritt verbinden wir diese Klassenmethoden mit den Events.

// Zuordnung der Skripte zu Eventgruppen
MyEventSystem::registerEvent('Truppenankunft', 'scriptA', 'EventScripts1', 1);
MyEventSystem::registerEvent('Truppenankunft', 'scriptB', 'EventScripts1', 2);
MyEventSystem::registerEvent('Handelswaren', 'scriptB', 'EventScripts1');
MyEventSystem::registerEvent('AusgehenderAngriff', 'scriptC', 'EventScripts1');
MyEventSystem::registerEvent('Bauauftrag', 'scriptD', 'EventScripts1');

Das Event "Truppenankunft" löst also die Skripte scriptA und scriptB aus. Wenn scriptA den Wert FALSE liefert, wird das Event abgebrochen und scriptB nicht mehr ausgeführt. Die anderen Events werden jeweils mit einem Skript verknüpft.

handleEvent()

Die Methode handleEvent() führt ein vorher registriertes Event aus und füttert die Event-Skripte mit dem Ausführungszeitpunkt und diversen sonstigen Informationen.

Zuerst werden die aktuellen Events geladen. Entweder man läd sie aus einer Datenbank heraus oder aus Dateien. Die Datenbankversion ist auf jeden Fall zu bevorzugen. Das Ergebnis sollte wie folgt aussehen:

// Aktuelle Events (evtl. aus einer Datenbank ausgelesen)
$events = array(
  array('id'=>1, 'time'=>1, 'name'=>'Truppenankunft', 'description'=>'Ankunft der Truppen im Heimatdorf', 'params'=>array('soldiers'=>763, 'gold'=>10)),
  array('id'=>2, 'time'=>4, 'name'=>'Handelswaren', 'description'=>'Ankunft der Handelswaren', 'params'=>array('gold'=>7, 'wood'=>3)),
  array('id'=>3, 'time'=>2, 'name'=>'Bauauftrag', 'description'=>'Bauauftrag abgeschlossen', 'params'=>array('building_name'=>'Haupthaus', 'new_level'=>3)),
  array('id'=>4, 'time'=>3, 'name'=>'AusgehenderAngriff', 'description'=>'Ausgehender Angriff', 'params'=>array('soldiers'=>1000, 'target_name'=>'Barbarendorf 7')),
);
 
// Die Events chronologisch sortieren (die "Älteren" sollen vor den "Jüngeren" abgearbeitet werden)
usort($events, 'MyEventSystem::compareTimes');
 

Die anschließende Sortierung sorgt dafür, dass die Events in chronologischer Reihenfolge vorliegen. Man könnte die Daten auch direkt von der Datenbank sortieren lassen, allerdings macht das den Datenbankzugriff unnötig langsamer. Eine PHP-seitige Sortierung ist in solchen Fällen stets zu bevorzugen.

Jetzt lassen wir mal die Methode handleEvent() über die einzelnen Events laufen und schauen uns das Endergebnis an.

// Alte Ergebnisse löschen
MyEventSystem::resetResults();
 
// Aktuelle Zeit (Events müssen diesen Zeitstempel erreicht haben um abgearbeitet zu werden)
$now = 4;
 
// Die Liste der aktuellen Events durchlaufen
foreach($events as $event)
{
  // Ist der Event-Zeitpunkt bereits verstrichen, wird das Event ausgelöst.
  if ($now >= $event['time'])
  {
    // Event auslösen
    MyEventSystem::handleEvent($event['name'], $event['time'], $event['params']);
 
    // Evtl. dieses abgearbeitete Event aus der Datenbank entfernen...
  };
};
 
// Alle Eventergebnisse holen
$results = MyEventSystem::getResults();
 
// Eventergebnisse ausgeben
echo print_r($results, true);

Schlusswort

Nacheinander werden die Events in der korrekten Reihenfolge angestoßen. Diese füttern die zugeordneten Skripte mit den nötigen Daten und sammeln alle Ergebnisse. Durch diese Art der Event-Behandlung bleibt der Code aufgeräumt und flexibel. Wenn ein Spieler einen Händler losschickt, wird in der Datenbank eine Zeile in die Event-Tabelle eingefügt, in der die benötigten Informationen zum Handel enthalten sind, sowie der Zeitpunkt an dem der Händler sein Ziel erreicht. Bei jedem Seitenaufruf des Spielers werden die mit ihm in Verbindung stehenden Events geladen, und diejenigen, die bereits abgelaufen sind, abgearbeitet. Wenn die Events zweier Spieler zusammenfallen (zum Beispiel Spieler A greift Spieler B an), muss dafür gesorgt werden, dass alle Events beider Spieler bis zu dem Zeitpunkt des Aufeinandertreffens abgearbeitet sind. Praktisch gesehen würde man die Events beider Spieler laden, gemeinsam sortieren und dann abarbeiten. Wurde Spieler B vorher noch von Spieler C angegriffen, müssten seine Events auch noch dazugenommen werden - usw. Das lässt sich mit einer Rekursion gut umsetzen und ist, wegen der erhöhten Komplexität, ein Thema für sich.

Hier noch mal der gesamte Code:

class MyEventSystem
{
  // Liste der registrierten Events
  private static $events = array();
  // Globaler Zähler der jeden Eventeintrag eindeutig identifiziert
  private static $id_counter = 0;
  // Ergebnisliste
  private static $results = NULL;
 
  // Fügt einem Event eine Funktion/Methode hinzu
  public static function registerEvent($name, $function_name, $class_name='', $priority=3)
  {
    // Bei jeder neuen Registrierung wird der globaler Zähler um eins erhöht.
    self::$id_counter++;
    $name = strtolower($name);
    if (!isset(self::$events[$name]))
    {
      // Wenn noch kein Eintrag für diese Event existiert, wird ein Neuer angelegt
      self::$events[$name] = array();
      self::$events[$name][] = array('id'=>self::$id_counter, 'priority'=>$priority, 'function_name'=>$function_name, 'class_name'=>$class_name);
    }
    else
    {
      // Existiert bereits ein Eintrag für das Event, wird der Neue angehangen
      self::$events[$name][] = array('id'=>self::$id_counter, 'priority'=>$priority, 'function_name'=>$function_name, 'class_name'=>$class_name);
      // Im Anschluss wird nach Priorität (bzw. bei Gleichheit nach Registrierungsreihenfolge) sortiert.
      usort(self::$events[$name], 'self::compareEventPriorities');
    };
  }
  // Vergleicht zwei Events nach Priorität
  private static function compareEventPriorities($a, $b)
  {
    if ($a['priority'] > $b['priority']) return 1;
    if ($a['priority'] < $b['priority']) return -1;
    // Sind die Prioritätswerte identisch, so wird nach der Registrierungsreihenfolge sortiert
    if ($a['id'] < $b['id']) return 1;
    if ($a['id'] > $b['id']) return -1;
    return 0;
  }
  // Vergleicht zwei Zeiten (Alt vor Jung)
  public static function compareTimes($a, $b)
  {
    if ($a['time'] > $b['time']) return 1;
    if ($a['time'] < $b['time']) return -1;
    return 0;
  }
  // Führt ein Event aus
  public static function handleEvent($name, $time, $params)
  {
    // Prüfen ob mind. ein Eintrag für dieses Event existiert
    $name = strtolower($name);
    if (!isset(self::$events[$name]) || empty(self::$events[$name])) return false;
    // Alle registrierten Funktionen/Methoden nacheinander ausführen
    $result = false;
    foreach(self::$events[$name] as $event)
    {
      // Wurde eine Klasse angegeben?
      if (!empty($event['class_name']))
      {
        // .. so handelt es sich um eine Methode
        $index = strtolower($event['class_name'].'::'.$event['function_name']);
        // Aufruf der Methode mit $time und $params als Parameter
        $result = call_user_func(array($event['class_name'], $event['function_name']), $time, $params);
      }
      else
      {
        // .. andernfalls um eine Funktion
        $index = strtolower($event['function_name']);
        // Aufruf der Methode mit $time und $params als Parameter
        $result = call_user_func($event['function_name'], $time, $params);
      };
      // Das Ergebnis der Funktion/Methode wird in der Ergebnismenge gespeichert
      self::$results[$name][$index] = $result;
      if ($result===false) break;
    };
    // Das letzte Ergebnis wird zurück gegeben
    return $result;
  }
  // Liefert die letzte Ergebnisliste des gewünschen Events.
  public static function getResult($event_name, $calling_name='')
  {
    $name = strtolower($name);
    if (!isset(self::$results[$name]) || empty(self::$results[$name])) return false;
    if (!empty($calling_name)) 
    {
      $calling_name = strtolower($calling_name);
      if (!isset(self::$results[$name][$calling_name]) || empty(self::$results[$name][$calling_name])) return false;
      return self::$results[$name][$calling_name];
    };
    end(self::$results[$name]);
    $key = key(self::$results[$name]);
    return self::$results[$name][$key];
  }
  // Liefert alle Ergebnislisten.
  public static function getResults($event_name='')
  {
    if (!empty($event_name))
    {
      // Wird ein Eventname angegeben, wird zunächst geprüft ob dieser existiert ..
      $name = strtolower($name);
      if (!isset(self::$results[$name]) || empty(self::$results[$name])) return false;
      // .. und bei erfolgreichen Prüfung zurückgegeben
      return self::$results[$name];
    }
    else
    {
      // Wird kein Eventname angegeben, wird die gesamte Ergebnisliste zurückgegeben
      return self::$results;
    }
  }
  // Setzt alle Ergebnisse zurück
  public static function resetResults(){ self::$results = array(); }
}
 
// Sammlung von Skripten die in den Events aufgerufen werden
class EventScripts1
{
  public function scriptA($calling_time, $params)
  {
    return $calling_time.' | '.$params['soldiers'].' Soldaten erreichen das Heimatdorf.';
  }
  public function scriptB($calling_time, $params)
  {
    return $calling_time.' | '.'Folgende Waren werden ins Lager gebracht: Gold='.intval($params['gold']).', Holz='.intval($params['wood']);
  }
  public function scriptC($calling_time, $params)
  {
    return $calling_time.' | '.$params['soldiers'].' Soldaten greifen das Dorf '.$params['target_name'].' an.';
  }
  public function scriptD($calling_time, $params)
  {
    return $calling_time.' | '.'Bauauftrag abgeschlossen. '.$params['building_name'].' wurde auf Stufe '.$params['new_level'].' ausgebaut.';
  }
}
 
// Zuordnung der Skripte zu Eventgruppen
MyEventSystem::registerEvent('Truppenankunft', 'scriptA', 'EventScripts1', 1);
MyEventSystem::registerEvent('Truppenankunft', 'scriptB', 'EventScripts1', 2);
MyEventSystem::registerEvent('Handelswaren', 'scriptB', 'EventScripts1');
MyEventSystem::registerEvent('AusgehenderAngriff', 'scriptC', 'EventScripts1');
MyEventSystem::registerEvent('Bauauftrag', 'scriptD', 'EventScripts1');
 
// Aktuelle Events (evtl. aus einer Datenbank ausgelesen)
$events = array(
  array('id'=>1, 'time'=>1, 'name'=>'Truppenankunft', 'description'=>'Ankunft der Truppen im Heimatdorf', 'params'=>array('soldiers'=>763, 'gold'=>10)),
  array('id'=>2, 'time'=>4, 'name'=>'Handelswaren', 'description'=>'Ankunft der Handelswaren', 'params'=>array('gold'=>7, 'wood'=>3)),
  array('id'=>3, 'time'=>2, 'name'=>'Bauauftrag', 'description'=>'Bauauftrag abgeschlossen', 'params'=>array('building_name'=>'Haupthaus', 'new_level'=>3)),
  array('id'=>4, 'time'=>3, 'name'=>'AusgehenderAngriff', 'description'=>'Ausgehender Angriff', 'params'=>array('soldiers'=>1000, 'target_name'=>'Barbarendorf 7')),
);
 
// Die Events chronologisch sortieren (die "Älteren" sollen vor den "Jüngeren" abgearbeitet werden)
usort($events, 'MyEventSystem::compareTimes');
 
// Alte Ergebnisse löschen
MyEventSystem::resetResults();
 
// Aktuelle Zeit (Events müssen diesen Zeitstempel erreicht haben um abgearbeitet zu werden)
$now = 4;
 
// Die Liste der aktuellen Events durchlaufen
foreach($events as $event)
{
  // Ist der Event-Zeitpunkt bereits verstrichen, wird das Event ausgelöst.
  if ($now >= $event['time'])
  {
    // Event auslösen
    MyEventSystem::handleEvent($event['name'], $event['time'], $event['params']);
 
    // Evtl. dieses abgearbeitete Event aus der Datenbank entfernen...
  };
};
 
// Alle Eventergebnisse holen
$results = MyEventSystem::getResults();
 
// Eventergebnisse ausgeben
echo print_r($results, true);

Bastards of Hell

Bastards of Hell - Biker Browsergame

Bastards of Hell - Das Biker Browsergame

Harte Fäuste, glühender Asphalt, laute Motoren, grenzenlose Freiheit - das ist die Welt eines Outlaw-Bikers in Bastards of Hell.

Weiterlesen...

Abenisa - Die 4-teilige Browsergame Saga

Abenisa - kostenloses Browsergame

Abenisa - das Strategie-Browsergame

Spiele jetzt die 4-teilige Browsergame-Saga.

Weiterlesen...

War of Titans

War of Titans

War of Titans - das Gladiatoren-Browsergame

Kämpfe online gegen andere Gladiatoren und erlebe die Action in der Arena.

Weiterlesen...

Der Weg zum eigenen Browsergame - Die Rangliste

Der Weg zum eigenen Browsergame

Thema: Die Rangliste

Weiterlesen...

Der Weg zum eigenen Browsergame

browsergames tutorial

Tipps und Tricks zur Erstellung eines Browsergames.

Insider verraten wie es geht.

Weiterlesen...