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:
- SMCharacter.gd: Der Character, auf den die State Machine angewendet wird.
- StateMachine.gd: Als child node des Characters hat die state machine die States als ihre child nodes.
- Weitere Instanzen wie ein TrashBin.tscn oder Battery.tscn.
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 Propertystate
gibt den Startzustand an.template_transitions
: Definieren Transitionen, die instates
referenziert werden können, falls sie mehrfach verwendet werden.states
: Definieren die States und übertransitions
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.