gai-godot-games/state/CA1.md

7.6 KiB

GAI/CA1 - Yan Wittmann

Die Implementierung kann auch auf Gitty gefunden werden.

1.1 Part 1: State Machines

Einleitung

Dieses Projekt stellt eine State Machine dar, die in der Lage ist, zwischen unterschiedlichen Zuständen aufgrund von komplexen Bedingungen zu wechseln. Hier wird diese für einen Character Controller eingesetzt, sie ist aber generisch genug, um auch für andere Anwendungen verwendet zu werden. Die Konfiguration der States und Transitions erfolgt über eine JSON-Datei character_state_machine.json, die zur Laufzeit eingelesen wird.

Konkret wird hier ein Reinigungsroboter simuliert, der Müllsäcke einsammelt und zu einem Müllcontainer bringt. Regelmäßig muss er auch seine Batterie aufladen und dafür zu einer Ladestation fahren.

Eine Video-Demo ist hier verfügbar: state-machine-demo.mp4

Alle bewertungsrelevanten Punkte sind erfüllt:

  • Das Projekt implementiert eine State Machine, die einen nicht-Spieler-Charakter steuert.
  • Der aktuelle Zustand ist mit "b" einblendbar.
  • Weitere Informationen über den aktuellen Zustand und die Transitionen sind zur Laufzeit mit "b" einblendbar. Weitere Contrubutors können im Code einfach hinzugefügt werden.
  • Ein Node-Graph zeigt die Transitionen und deren Bedingungen, sowie den aktuellen Zustand an. Mit "a" einblendbar.
  • Die Visualisierung der Zusatzinformationen kann über "a" und "b" aktiviert und deaktiviert werden.

Szenen und States-Implementierung

Die Haupt-Szene ist StateMachineWorld.tscn. Diese verwaltet unter anderem diese Insatanzen:

States implementieren die State.gd-Klassenschnittstelle, die die folgenden Methoden definiert:

func state_enter() -> void
func state_process(delta: float) -> void
func state_exit(new_state: State) -> void

Da die Transitionen ausschließlich über die JSON-Datei konfiguriert werden, sind die States selbst nur für die Implementierung der Logik zuständig. Ein Beispiel von state_PickupTrash.gd, der sich auf den Müllsack zubewegt:

extends State

var pickup_trash_target: Vector2 = Vector2.ZERO


func state_enter():
    var waste = state_machine.state_transfer_variables["waste"]
    pickup_trash_target = waste.position
    print(waste, " ", pickup_trash_target)


func state_process(delta: float) -> void:
    character.move_towards(pickup_trash_target, delta)
    character.move_and_slide()

Es wird hier bereits viel auf den Charakter ausgelagert, um die States möglichst einfach zu halten. Das viel interresantere ist jedoch die Konfiguration der Transitionen, was im folgenden Kapitel beschrieben wird.

Konfiguration der Transitionen

In der JSON-Datei character_state_machine.json werden die States und Transitions konfiguriert. Hierbeit ist das Format möglichst offen gehalten, um auch komplexere Bedingungen zu ermöglichen.

Auf den obersten Ebenen werden allgemein die folgenden Keys verlangt:

{
  "start": {
    "state": "<state_name>"
  },
  "template_transitions": {
    "<transition_name>": {}
  },
  "states": {
    "<state_name>": {
      "transitions": []
    }
  }
}
  • start: Bestimmt initiale Eigenschaften der State Machine. Eine Property state gibt den Startzustand an.
  • template_transitions: Definieren Transitionen, die in states referenziert werden können, falls sie mehrfach verwendet werden.
  • states: Definieren die States und über transitions deren Transitionen.

Transitionen sind das Kernstück der Konfiguration und haben folgende Struktur:

{
    "target": "<state_name>" | { "type": "<selector_type>" },
    "signal": "<signal_name>",
    "conditions": [],
    "transfer": {}
}
  • target: Der Name des Zielzustands, zu dem die State Machine wechseln soll. Kann auch ein Objekt sein, das dynamisch einen Zielzustand bestimmt.
  • signal: (Optional) Ein Signalname, der von der State Machine seit dem letzten Tick empfangen werden muss, damit die Transition berücksichtigt wird.
  • conditions: Ein Array von Bedingungen, die alle erfüllt sein müssen, damit die Transition durchgeführt wird.
  • transfer: Ein Objekt, das definiert, welche Daten beim Übergang in den neuen Zustand übergeben werden sollen.
{
    "template": "<template_transition_name>",
}
  • Oder einfach eine Referenz auf eine template_transition.

Bedingungen (conditions)

Bedingungen sind ein Array von Objekten, die alle erfüllt sein müssen, damit eine Transition ausgeführt wird. Jede Bedingung hat folgende Struktur:

{
    "type": "<condition_type>",
    "left": {},
    "right": {}
}
  • type: Der Typ der Bedingung ("=", "!=", ">", "<", ">=", "<=").
  • left: Der linke Operand der Bedingung.
  • right: Der rechte Operand der Bedingung.

Die Operanden können entweder eine value Property (direkter Wert), einen accessor (Pfad zu einer Variable) oder eine function (Aufruf einer Funktion) verwenden.

  • value: Ein direkter Wert (z.B. {"value": 100}).
  • accessor: Ein Array von Strings, das einen Pfad zu einer Variable in der Szene oder der State Machine angibt (z.B. {"accessor": ["character", "battery_charge"]}).
  • function: Ein Objekt, das eine Funktion mit Argumenten aufruft (z.B. {"function": "distance", "args": [...]}).

Diese können beliebig verschachtelt werden.

{
  "type": "<=",
  "left": {
    "value": 100
  },
  "right": {
    "accessor": [
      "character",
      "battery_charge"
    ]
  }
}

Transfer (transfer)

Das transfer-Objekt ermöglicht es, Daten zwischen Zuständen zu übertragen. Es ist ein Objekt, bei dem der Key der Name der Variable ist, und der Wert ein Accessor, der die Datenquelle angibt.

"transfer": {
    "waste": {
        "accessor": [
            "signals",
            "waste_detected",
            "args",
            "waste"
        ]
    }
}

In diesem Beispiel wird die Variable waste des Zielzustands mit dem Wert des Arguments waste des Signals waste_detected befüllt.

Dynamische Zielzustände

Der target einer Transition kann auch ein Objekt sein, das dynamisch einen Zielzustand bestimmt. Das folgende Beispiel zeigt, wie man in der Historie der Zustände zu einem vorherigen Zustand zurückkehrt, aber gewisse Zustände dabei ignoriert:

{
    "target": {
        "type": "history_first",
        "ignore": [
            "RechargeBattery",
            "GoToBattery"
        ],
        "restore_transfer_variables": true
    }
}
  • type: Der Typ des dynamischen Ziels (hier: history_first).
  • ignore: Eine Liste von Zuständen, die bei der Suche nach dem vorherigen Zustand ignoriert werden sollen.
  • restore_transfer_variables: Ein Boolean, der angibt, ob die Transfervariablen dieses vorherigen Zustands ebenfalls wiederhergestellt werden sollen.