How-To 'Moorhuhn'-Klon in Godot Part 3

Fertiges Moorschwein und erstes Level

Erschieß das Schwein!

Im letzten Part haben wir unser Moorschwein Skript angefangen und es in Bewegung gesetzt. Heute wollen wir dafür sorgen, dass der Spieler auch damit interagieren kann.

In Godot gibt es mehrere Möglichkeiten und Funktionen, um den Spielerinput auszuwerten. In den Funktionen _process und _physics_process zum Beispiel kann über die Input Klasse überprüft werden, ob aktuell eine Eingabe stattfindet.

1
2
3
func _physics_process(delta : float) -> void:
if Input.is_mouse_button_pressed(BUTTON_LEFT):
//do stuff

Eine andere Möglichkeit ist die Funktion _Input, welche bei einer Eingabe direkt aufgerufen wird und als Parameter das InputEvent liefert.

1
2
3
func _Input(evt : InputEvent) -> void:
if evt is InputEventMouseButton and evt.is_pressed() and evt.button_index == BUTTON_LEFT:
//do stuff

In beiden Fällen ist es ein globales Event. Also egal, wo unser Spieler auf den Bildschirm tippt, es bekommen alle Objekte mit, die die _Input Methode implementieren oder in ihrem Process Inputs auswerten. Ein Möglichkeit wäre nun für jedes Schwein die Position des Mauszeigers auszuwerten und damit zu überprüfen, ob es getroffen wurde. Dazu müssten wir bestimmen, an welchen Koordinaten sich unser Schwein befindet, wie groß die Fläche ist, die es einnimmt und wie man diese berechnet. Solange wir das Icon als Bild verwenden, wäre das noch relativ leicht, da wir einen rechteckigen Bereich haben, den wir prüfen. Wenn die Form komplexer wird, müssen wir uns dann aber langsam etwas ausdenken.

Zum Glück bietet uns Godot eine andere Alternative. Öffnet unsere Moorschwein-Szene und fügt eine neue Node zu unserer Wurzelnode hinzu. Die Node, die wir brauchen, heißt Area2D. Wenn man prüfen möchte, ob irgendetwas einen bestimmmten Bereich betritt, dann ist die Area2D genau das, was man braucht. Sie bietet Signale, die ausgelöst werden, wenn ein Körper einen Bereich betritt, aber auch welche zur Erkennung von Mausevents in einem Bereich. Also genau das, was wir suchen. Jetzt muss man sich nur noch fragen, wie wir denn überhaupt den Bereich festlegen, den die Area2D überwacht.
Aufmerksamen Lesern dürfte aufgefallen sein, dass neben der Area2D im Szenentab ein Warnschild angezeigt wird. Dieses weist darauf hin, dass keine untergeordneten Formen vorhanden sind und deswegen keine Kollision möglich ist. Was hier fehlt, ist eine CollisonShape2D. Wie der Name schon sagt, ist diese Node ein Container für unsere Kollisionsform. Wählt die Area2D aus und fügt eine CollisionShape2D hinzu. Bevor wir sie verwenden können, müssen wir eine Form festlegen. Das tun wir im Inspektor. Klickt auf Shape und ihr bekommt eine Liste von Auswahlmöglichkeiten. Da wir aktuell unser Icon als Sprite verwenden, brauchen wir ein simples Rechteck. Also wählt “Neues RectangleShape2D” aus. Im 2D Bereich solltet ihr jetzt ein blaues Rechteck sehen, dessen Größe ihr mit zwei orangenen Punkten verändern könnt.

Legt die Shape über euren Sprit und passt die Größe dementsprechend an. Wer es genauer haben will, kann noch einmal auf die erstellte Form in der CollisionShape2D klicken. Dadurch sollten sich die Optionen der Form ausklappen und ihr könnt über den Punkt “Extents” die genaue Größe der Form in Pixeln angeben.

Sobald unsere Form passt, können wir uns endlich darum kümmern, dass unser Schwein erschossen werden kann. Wählt die Area2D wieder aus und klickt im Inspektor auf den Tab Node. Hier seht ihr alle Signale, die von der Area2D verbunden werden können. Das Signal, das wir brauchen, heißt input_event. Doppelklickt auf dieses. Es öffnet sich ein Dialog zur Auswahl der Methode, die aufgerufen werden soll, wenn dieses Signal abgefeuert wird. Achtet darauf, dass Funktion erstellen aktiviert ist. Wählt im Szenenbaum unsere Wurzel, benennt die zu erstellende Methode um in “_on_Moorschwein_input_event” und klickt auf verbinden. Die Ansicht sollte auf unser Skript wechseln, in dem am Ende die Methode hinzugefügt wurde. Fügen wir nun folgendes hinzu:

1
2
3
4
5
6
func kill() -> void:
queue_free()

func _on_MoorSchwein_input_event(viewport, event, shape_idx) -> void:
if event is InputEventMouseButton and event.is_pressed() and event.button_index == BUTTON_LEFT:
kill()

In unserer Input Methode prüfen wir, ob das ankommende Event durch einen Mousebutton ausgelöst wurde und ob es sich hierbei um die linke Maustaste handelt. Die Funktion is_pressed() sagt uns, ob die Taste gedrückt wurde. Das ist deswegen wichtig, da auch das Loslassen der Taste das Signal auslöst.
Unser Schwein wird also nur getötet, indem die linke Maustaste gedrückt wird. Das Töten passiert durch den Aufruf der Funktion queue_free(). Diese gibt den Speicher für das Moorschwein frei, sobald es möglich ist.

Probiert es gerne mal aus, indem ihr die Szene startet und versucht das Moorhuhn anzuklicken. Solltet ihr es treffen, verschwindet es.
Da wir wollen, dass unsere Schweine verschwinden, wenn sie den Bildschirm verlassen, anstatt auf der anderen Seite wieder aufzutauchen, passen wir schnell noch die _physics_process Methode an:

1
2
3
4
5
6
7
8
func _physics_process(delta:float) -> void:
m_start += delta
position.x += delta * HORIZONTAL_SPEED
position.y = sin((m_start)*VERTICAL_SPEED)*AMPLITUDE + m_start_position.y
if global_position.x > get_viewport_rect().size.x:
kill()
if global_position.x < 0:
kill()

Damit haben wir erstmal alles, was wir für ein brauchbares Ziel brauchen. Jetzt können wir uns daran machen unser erstes Level anzulegen.

Unser erstes Level

Legt eine neue Szene an und startet als Wurzelnode wieder mit einer Node2D. Nennt diese Level. Jetzt müssen wir uns überlegen, was in unserem Level alles benötigt wird.
Zuallererst brauchen wir unsere Moorschweinchen. Diese können wir aber nicht einfach so unserem Level hinzufügen, da sie ja nach und nach auftauchen sollen. Wir brauchen also eine Möglichkeit unsere Schweinchen periodisch zu instanziieren. Fügen wir also eine Node hinzu, die uns genau das erlaubt: Ein Timer. Benennt ihn in NormalPigTimer um. Bevor wir damit etwas anfangen können, müssen wir unser Level noch mit einem Skript verbinden, also tun wir das schnell auf die gleiche Weise wie bei unserem Schwein. Sobald unser Skript existiert, wählt ihr wieder den Timer aus und wählt im Inspektor wieder den Tab Node. Verbindet das Signal timeout mit der Methode “_on_NormalPig_timeout”. Achtet wieder darauf, dass “Funktion erstellen” auf on steht.
Dann legen wir noch eine Node2D an und nennen diese NormalPig. Diese Node nutzen wir als Sammelplatz für unsere Moorschweininstanzen.
Danach passen wir das Level Skript wie folgt an:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

const PIG = preload("res://gameobjects/MoorSchwein.tscn")

func _build_pig(points : int, horizontal_speed : float, vertical_speed :float, amplitude : float, scale : Vector2) -> Area2D:
var pig = PIG.instance()
var screen_size = int(get_viewport_rect().size.y)
pig.HORIZONTAL_SPEED = horizontal_speed
pig.POINTS = points
pig.VERTICAL_SPEED = vertical_speed
pig.AMPLITUDE = amplitude
pig.scale = scale
var x = 0
var dir = randi()%2
if dir == 0:
pig.HORIZONTAL_SPEED *= -1
x = get_viewport_rect().size.x
pig.position = Vector2(x, randi()%screen_size)
return pig


func _on_NormalPig_timeout() -> void:
var pig = _build_pig(25,100,2,150, Vector2(1,1))
$NormalPig.add_child(pig)

Zuerst laden wir unsere Moorschwein Szene, damit wir sie im Folgenden instanziieren können.

Dann haben wir die Funktion _build_pig. Diese erzeugt eine neue Instanz unserer Moorschweinszene. Wir speichern uns die Höhe des Bildschirms in der Variable screen_size und setzen unsere Moorhuhneigenschaften über die Parameter. Außerdem setze ich hier die Scale Eigenschaft des Schweins. Darauf gehen wir gleich noch einmal ein. Die Richtung, in die unser Schwein fliegt, wird zufällig entschieden. Dazu nutzen wir die Funktion randi(), die uns eine zufällige ganze Zahl zurückgibt. Danach folgt ein wenig Mathe: %2 ist der Modulo und liefert uns immer den Rest der Division von 2 ergo 0 oder 1. Wie bereits erwähnt, bewegt sich unser Schwein nach links, wenn die horizontale Geschwindigkeit negativ ist. Daher multiplizieren wir diese mit -1 und setzen die X-Position an die rechte Seite des Bildschirms, wenn “dir” 0 ist. Also bei dir == 0 fliegt unser Schwein vom rechten Bildschirmrand nach links. Bei dir == 1 vom linken Rand des Bildschirms nach rechts. Die Y-Koordinate ist eine zufällige Zahl zwischen 0 und der Höhe des Bildschirms.

Jedes Mal wenn unser Timer abläuft, wird also eine neue Instanz unseres Moorschweins erzeugt, welche wir unserem NormalPig Node als Child hinzufügen. Dadurch wird es in unserem Level angezeigt. Die Werte könnt ihr hier nach Belieben setzen.

Jetzt sollten wir noch schauen, dass unser Timer auch auslöst. Wählt den Timer aus und schaut in den Inspektor. Hier solltet ihr die Eigenschaften Wait Time, Process Mode, One Shot und Autostart einstellen.
Process Mode bestimmt, wann der Timer aktualisiert wird. Dies kann entweder im Physics Step eines Frames passieren (also wieder in festen Frameraten) oder im Idle Step jedes Frames. Belasst es hier ruhig bei Idle.
Wait Time ist die Dauer in Sekunden, die vergehen muss, bevor das time_out Signal gesendet wird. Diese stellen wir auf 5 Sekunden. One Shot bedeutet, dass das Signal nur einmal ausgelöst wird. Da wir immer wieder neue Schweinchen brauchen, stellen wir diese Eigenschaft auf false. Autostart hingegen können wir auf true setzen, da wir wollen, dass der Timer mit Spielbeginn starten soll.
Wenn ihr jetzt das Level startet, solltet ihr alle 5 Sekunden ein neues Schwein spawnen. Probiert es ruhig mal aus.

Wir brauchen mehr Schweine!

Das Ganze ist ein wenig langweilig, da wir jetzt konstant immer ein Schwein mit derselben Geschwindigkeit und Größe haben. Daher sollten wir es noch ein wenig interessanter machen. Fügt nochmal zwei Timer und zwei Ordner hinzu. Nennt diese BigPig/-Timer und SmallPig/-Timer. Die Timeout Methoden sehen wie folgt aus. Wie ihr seht, setze ich hier andere Werte für den Scale. Vector2(0.75,0.75) sorgt dafür, dass die Schweine nur 75% so groß sind wie unsere Ausgangsgröße. Vector2(1.5,1.5) bedeutet dementsprechend, dass sie 50% größer sind. Die Timer habe ich auf 3 Sekunden für die großen und 10 Sekunden für die kleinen Schweine eingestellt.

1
2
3
4
5
6
7
8
func _on_SmallPig_timeout():
var pig = _build_pig(50,150,4,200, Vector2(0.75,0.75))
$SmallPig.add_child(pig)


func _on_BigPig_timeout():
var pig = _build_pig(5,50,2,100, Vector2(1.5,1.5))
$BigPig.add_child(pig)

Wenn ihr das ganze übernommen habt und startet, solltet ihr Schweine in verschiedenen Größen über den Bildschirm fliegen sehen. Jetzt kann es aber noch sein, dass perspektivisch etwas im Argen liegt. Wir wollen, dass die großen Schweine ganz vorne fliegen und unsere kleinen Schweine ganz hinten damit es so aussieht, als ob diese weiter entfernt fliegen. Da wir für jeden Typ einen Node2D Ordner angelegt haben, ist das ganz einfach. Jedes Objekt in Godot hat einen Z-Index, der bestimmt, ob eine Objekt weiter vorne oder weiter hinten gezeichnet wird. Haben Objekte den gleichen Z-Index werden diese in der Reihenfolge gezeichnet, in der sie im Szenenbaum auftauchen. Wenn also der Szenenbaum aussieht wie in diesem Bild, werden die kleinen Schweine zuerst gezeichnet, gefolgt von den normalgroßen und den größeren.

Das soll es dann auch fürs Erste gewesen sein. In Part 4 kümmern wir uns dann darum, dass wir auch mal Punkte für das Treffen von Schweinen erhalten und diese auch angezeigt werden.