Das erste Spiel¶
Übersicht¶
Diese Anleitung wird Ihnen dabei helfen, Ihr erstes Godot-Projekt umzusetzen. Sie werden lernen wie der Godot-Editor funktioniert, wie ein Projekt zu strukturieren ist und man ein 2D-Spiel erstellt.
Bemerkung
Dieses Projekt ist eine Einführung in die Godot Engine. Es wird angenommen, dass Sie bereits etwas Programmiererfahrung haben. Falls Sie vorher noch nie programmiert haben, sollten Sie hier beginnen: Skripten.
Das Spiel heißt "Dodge the Creeps!". Ihr Charakter muss sich bewegen, um den Gegnern so lange wie möglich auszuweichen. Hier eine Vorschau auf das Endergebnis:

Warum 2D? 3D-Spiele sind viel komplexer als 2D-Spiele. Sie sollten sich deshalb an 2D halten, bis Sie ein gutes Verständnis für den Spielentwicklungsprozess haben.
Projektkonfiguration¶
Starten Sie Godot und erstellen ein neues Projekt. Danach, laden Sie dodge_assets.zip
herunter. Die Datei beinhaltet die Bilder und Töne, sogenannte "Assets", die Sie verwenden werden um das Spiel zu erstellen. Entpacken Sie die Dateien in den zuvor gewählten Projektordner.
Bemerkung
In dieser Anleitung gehen wir davon aus, dass Sie mit dem Editor vertraut sind. Falls Sie Szenen und Nodes noch nicht gelesen haben, informiere Sie sich darüber, wie Sie ein Projekt einrichten und den Editor verwenden können.
Dieses Spiel verwendet das Hochformat, daher müssen wir die Größe des Spielfensters anpassen. Klicken Sie auf Projekt -> Projekteinstellungen -> Display (Anzeige) -> Window (Fenster) und setzen "Width" auf 480
und "Height" auf 720
.
Setzen Sie auch in diesem Abschnitt unter den "Stretch" -Optionen Mode
auf "2d" und "Aspect
auf "keep". Dies stellt sicher, dass das Spiel auf unterschiedlich großen Bildschirmen konsistent skaliert wird.
Projektorganisation¶
In diesem Projekt werden 3 unabhängige Szenen erstellt: Player
, Mob
und HUD
, die wir in der Main
-Szene des Spiels kombinieren werden. In einem größeren Projekt kann es nützlich sein, Ordner für die verschiedenen Szenen und deren Skripte zu erstellen. Bei diesem relativ kleinen Spiel können Sie Ihre Szenen und Skripte jedoch im Wurzelverzeichnis des Projekts speichern, der als res://
bezeichnet wird. Sie können Ihren Projektordner im Dateisystem-Dock in der unteren linken Ecke sehen:

Spieler-Szene¶
Die erste erstellte Szene definiert das Player
-Objekt. Einer der Vorteile einer separaten Spieler-Szene ist, dass diese unabhängig vom Rest des Spiels getestet werden kann, bevor andere Teile des Spiels existieren müssen.
Node Struktur¶
Um zu beginnen, müssen wir einen Wurzel-Node für das Spieler-Objekt wählen. Als eine generelle Regel gilt, ein Wurzel-Node einer Szene sollte immer die gewünschte Funktionalität des Objektes reflektieren - was das Objekt ist. Klicken Sie auf den "Andere Node" - Knopf und fügen einen Area2D Node zu der Szene hinzu.

Godot zeigt ein Icon mit einem Warnhinweis im Node des Szenenbaumes. Dieses kann vorerst ignoriert werden, wir werden uns später darum kümmern.
Mit Area2D
können Objekte erkannt werden, die mit dem Spieler überlappen oder diesen berühren. Wir ändern den Namen des Nodes zu Player
, indem wir darauf Doppelklicken. Nun da wir den Wurzel-Node der Szene gesetzt haben, können wir weitere Nodes hinzufügen, um ihm damit mehr Funktionalität zu geben.
Bevor wir weitere untergeordnete Nodes zu Player
hinzufügen, wollen wir sicherstellen, dass wir diese nicht aus Versehen bewegen oder deren Größe ändern, indem wir auf sie klicken. Wählen Sie den Node aus und klicke auf das Symbol rechts neben dem Schloss; sein Tooltip lautet "Verhindert das Auswählen von Unterobjekten dieses Nodes."

Speichern Sie die Szene. Klicken Sie auf Szene -> Speichern oder drücken Ctrl + S unter Windows/Linux oder Cmd + S unter MacOS.
Bemerkung
Für dieses Projekt halten wir uns an die Godot-Namenskonvention.
GDScript: Klassen (Nodes) nutzen
PascalCase
, Variablen und Funktionensnake_case
und KonstantenALL_CAPS
(Siehe GDScript Stil-Richtlinien).C#: Klassen, Exportvariablen und Methoden verwenden die
PascalCase
-Schreibweise, private Felder_camelCase
, lokale Variablen und Parameter verwendencamelCase
(Siehe C# Stil-Richtlinien). Beachten Sie die genaue Schreibweise, wenn Sie Signale einbinden möchten.
Sprite-Animation¶
Klicken Sie auf den Player
Node und fügen ein AnimatedSprite Node als Unterobjekt hinzu. Das AnimatedSprite
übernimmt das Erscheinungsbild und die Animationen für unseren Spieler. Achten Sie auf das Warnsymbol neben dem Node . Ein AnimatedSprite
benötigt eine SpriteFrames Ressource, eine Liste der Animationen, die angezeigt werden können. Um eine zu erstellen suchen Sie die Eigenschaft Frames
im Inspektor und klicken "[leer]" -> "Neues SpriteFrames". Nun sollte sich automatisch das Animationspanel öffnen:

Auf der linken Seite befindet sich eine Liste von Animationen. Klicken Sie auf die "default" Animation und benennen sie in "walk" um. Dann klicken Sie auf die "hinzufügen" Schaltfläche, um eine zweite Animation "up" hinzuzufügen. Finden Sie die zwei Bilder, playerGrey_up[1/2]
und playerGrey_walk[1/2]
, im "Dateisystem"-Reiter und ziehen diese in den "Animationsbilder"-Bereich zu den entsprechenden Animationen:

Die Spielerbilder sind ein bisschen zu groß für das Spielefenster, also müssen wir sie verkleinern. Klicken Sie auf den AnimatedSprite
Node und setzen die Eigenschaft Scale
auf (0.5, 0.5)
. Sie können sie im Inspektor unterhalb der Überschrift Node2D
finden.

Abschließend fügen Sie ein CollisionShape2D als ein Unterobjekt von Player
hinzu. Er bestimmt die "Hitbox" des Spielers oder die Grenzen seines Kollisionsbereichs. Für diesen Charakter ist ein CapsuleShape2D
Node am besten geeignet. Klicken Sie im Inspektor neben "Shape" auf "[leer]" -> "Neues CapsuleShape2D". Verwenden Sie die zwei Anfasser und verändern die Form so, dass sie das Sprite überdeckt:

Wenn Sie fertig sind sollte Ihre Player
Szene die folgende Struktur haben:

Speichern Sie die Szene nach diesen Änderungen wieder ab.
Den Spieler Bewegen¶
Jetzt müssen wir einige Funktionen hinzufügen, die wir von einem integrierten Node nicht erhalten können, also fügen wir ein Skript hinzu. Klicken Sie auf den Node Player
und dann auf die Schaltfläche "Skript hinzufügen":

Im Fenster mit den Skripteinstellungen können Sie die Standardeinstellungen beibehalten. Klicken Sie einfach auf "Erstellen":
Bemerkung
Wenn Sie ein C#-Skript oder andere Sprachen verwenden wollen, wählen Sie die Sprache aus dem Auswahlmenü "Sprache", bevor Sie auf Erstellen klicken.

Bemerkung
Wenn dies Ihr erster Kontakt mit GDScript ist, lesen Sie bitte Skripten, bevor Sie fortfahren.
Beginnen Sie, indem Sie die Member-Variablen deklarieren, die dieses Objekt benötigt:
extends Area2D
export var speed = 400 # How fast the player will move (pixels/sec).
var screen_size # Size of the game window.
public class Player : Area2D
{
[Export]
public int Speed = 400; // How fast the player will move (pixels/sec).
private Vector2 _screenSize; // Size of the game window.
}
Wenn Sie das Schlüsselwort export
für die erste Variable speed
verwenden, ermöglicht es Ihnen dessen Wert im Inspektor einzustellen. Dies kann für Werte nützlich sein, die Sie wie die integrierten Eigenschaften eines Nodes anpassen möchten. Klicken Sie auf den Node Player
, und die Eigenschaft wird jetzt im Bereich "Script Variables" des Inspektors angezeigt. Denken Sie daran, wenn Sie den Wert hier ändern, wird der im Skript verwendete Wert überschrieben.
Warnung
Wenn Sie C# verwenden, müssen Sie die Projekt-Bausteine neu übersetzen, um neue Exportvariablen oder Signale sichtbar zu machen. Das kann manuell durch einen Klick auf das Wort "Mono" im unteren Fensterbereich geschehen, es wird das Mono-Panel eingeblendet und ein anschließender Klick auf "Build Project" führt die Aktion aus.

Die Funktion _ready()
wird aufgerufen, wenn ein Node in den Szenenbaum eintritt, was ein guter Zeitpunkt ist, um die Größe des Spielfensters zu ermitteln:
func _ready():
screen_size = get_viewport_rect().size
public override void _Ready()
{
_screenSize = GetViewport().Size;
}
Jetzt können wir die Funktion _process()
verwenden, um festzulegen, was der Spieler tun soll. _process()
wird mit jedem Frame aufgerufen, deshalb werden wir es verwenden, um Elemente unseres Spiels zu aktualisieren, von denen wir erwarten, dass sie sich häufig ändern werden. Für den Spieler müssen wir Folgendes tun:
Auf Eingabe prüfen.
In die angegebene Richtung bewegen.
Die entsprechende Animation abspielen.
Zuerst müssen wir auf Eingaben prüfen - drückt der Spieler eine Taste? Für dieses Spiel haben wir 4 Richtungseingaben zu überprüfen. Eingabeaktionen werden in den Projekteinstellungen unter "Eingabe-Zuordnung" definiert. Sie können benutzerdefinierte Ereignisse definieren und ihnen verschiedene Tasten, Mausereignisse oder andere Eingaben zuweisen. Für diese Demo werden wir die Standardereignisse verwenden, die den Pfeiltasten auf der Tastatur zugeordnet sind.
Sie können herausfinden, ob eine Taste gedrückt wird, indem Sie Input.is_action_pressed()
benutzen, welches true
zurückgibt, wenn eine Taste gedrückt wird und false
wenn nicht.
func _process(delta):
var velocity = Vector2() # The player's movement vector.
if Input.is_action_pressed("ui_right"):
velocity.x += 1
if Input.is_action_pressed("ui_left"):
velocity.x -= 1
if Input.is_action_pressed("ui_down"):
velocity.y += 1
if Input.is_action_pressed("ui_up"):
velocity.y -= 1
if velocity.length() > 0:
velocity = velocity.normalized() * speed
$AnimatedSprite.play()
else:
$AnimatedSprite.stop()
public override void _Process(float delta)
{
var velocity = new Vector2(); // The player's movement vector.
if (Input.IsActionPressed("ui_right"))
{
velocity.x += 1;
}
if (Input.IsActionPressed("ui_left"))
{
velocity.x -= 1;
}
if (Input.IsActionPressed("ui_down"))
{
velocity.y += 1;
}
if (Input.IsActionPressed("ui_up"))
{
velocity.y -= 1;
}
var animatedSprite = GetNode<AnimatedSprite>("AnimatedSprite");
if (velocity.Length() > 0)
{
velocity = velocity.Normalized() * Speed;
animatedSprite.Play();
}
else
{
animatedSprite.Stop();
}
}
Wir fangen damit an, die velocity
(Geschwindigkeit) auf (0,0)
zu setzen - standardmäßig sollte sich der Spieler nicht bewegen. Dann überprüfen wir jede Eingabe und addieren/subtrahieren von der velocity
, um eine Gesamtrichtung zu erhalten. Wenn Sie beispielsweise rechts
und runter
gleichzeitig halten, ist der resultierende velocity
-Vektor (1, 1)
. In diesem Fall, da wir eine horizontale und vertikale Bewegung addieren, würde sich der Spieler schneller bewegen, als wenn er sich nur horizontal bewegen würde.
Wir können das verhindern, indem wir die Geschwindigkeit normalisieren, was bedeutet, dass wir ihre Länge auf 1
festlegen, anschließend multiplizieren wir mit der gewünschten Geschwindigkeit. Das sorgt dafür, dass keine schnelle diagonale Bewegung mehr stattfindet.
Tipp
Wenn Sie noch nie zuvor Vektor-Mathematik verwendet haben oder eine Auffrischung benötigen, finden Sie eine Erklärung zur Vektorverwendung in Godot unter Vektormathematik. Es ist gut zu wissen, wird aber für den Rest dieser Anleitung nicht notwendig sein.
Wir prüfen auch, ob sich der Spieler bewegt, damit wir die AnimatedSprite-Animation mit play()
und stop()
, starten oder stoppen können.
$
ist eine Abkürzung fürget_node()
. Im obigen Code ist$AnimatedSprite.play()
dasselbe wieget_node("AnimatedSprite").play()
.
Tipp
In GDScript gibt $
den Node am relativen Pfad zum aktuellen Node zurück oder null
, wenn der Node nicht gefunden wird. Da AnimatedSprite ein Unterobjekt des aktuellen Nodes ist, können wir $AnimatedSprite
verwenden.
Jetzt, da wir eine Bewegungsrichtung haben, können wir die Position des Spielers aktualisieren und mit clamp()
verhindern, dass er den Bildschirm verlässt. clamp()
beschränkt dabei einen Wert auf einen angegebenen Bereich. Am Ende der _process
Funktion fügen wir also folgendes hinzufügen:
position += velocity * delta
position.x = clamp(position.x, 0, screen_size.x)
position.y = clamp(position.y, 0, screen_size.y)
Position += velocity * delta;
Position = new Vector2(
x: Mathf.Clamp(Position.x, 0, _screenSize.x),
y: Mathf.Clamp(Position.y, 0, _screenSize.y)
);
Tipp
Der Parameter delta in der Funktion `__process ()`bezieht sich auf die Frame-Länge - die Zeit, die der vorherige Frame für die Fertigstellung benötigt hat. Durch die Verwendung dieses Werts wird sichergestellt, dass Ihre Bewegung auch dann konstant bleibt, wenn sich die Bildrate ändert.
Klicken Sie auf "Szene abspielen" (F6
) und stellen sicher, dass der Spieler sich auf dem Bildschirm in alle Richtungen bewegen kann.
Warnung
Wenn ein Fehler im "Debugger" Bereich auftaucht, der sagt
Attempt to call function 'play' in base 'null instance' on a null instance
bedeutet das wahrscheinlich, dass der Node-Namen des AnimatedSprite falsch geschrieben wurde. Node-Namen sind case-sensitive und $NodeName
oder get_node("NodeName")
müssen mit dem Namen übereinstimmen, der im Szenenbaum zu sehen ist.
Animationen¶
Nachdem sich der Player bewegen kann, müssen wir die von AnimatedSprite abgespielte Animation anhand seiner Richtung ändern. Wir haben die "Walk" -Animation, die den Spieler zeigt, wie er nach rechts geht. Diese Animation sollte horizontal gespiegelt werden, indem die Eigenschaft flip_h
für die Bewegung nach links verwendet wird. Wir haben auch die "Auf" -Animation, die vertikal mit flip_v
für die Abwärtsbewegung gespiegelt werden sollte. Platzieren wir diesen Code am Ende der Funktion _process()
:
if velocity.x != 0:
$AnimatedSprite.animation = "walk"
$AnimatedSprite.flip_v = false
# See the note below about boolean assignment
$AnimatedSprite.flip_h = velocity.x < 0
elif velocity.y != 0:
$AnimatedSprite.animation = "up"
$AnimatedSprite.flip_v = velocity.y > 0
if (velocity.x != 0)
{
animatedSprite.Animation = "walk";
animatedSprite.FlipV = false;
// See the note below about boolean assignment
animatedSprite.FlipH = velocity.x < 0;
}
else if (velocity.y != 0)
{
animatedSprite.Animation = "up";
animatedSprite.FlipV = velocity.y > 0;
}
Bemerkung
Die booleschen Zuweisungen im obigen Code sind eine gebräuchliche Abkürzung für Programmierer. Da wir einen Vergleichstest (boolescher Wert) durchführen und auch einen booleschen Wert zuweisen, können wir beide gleichzeitig durchführen. Betrachten Sie diesen Code im Vergleich zur obigen einzeiligen booleschen Zuweisung:
if velocity.x < 0:
$AnimatedSprite.flip_h = true
else:
$AnimatedSprite.flip_h = false
if (velocity.x < 0)
{
animatedSprite.FlipH = true;
}
else
{
animatedSprite.FlipH = false;
}
Spielen Sie die Szene erneut ab und überprüfen Sie, ob die Animationen in jeder Richtung korrekt sind.
Tipp
Ein allgemeiner Fehler ist hier, die Namen der Animationen falsch zu benennen. Die Animationsnamen in dem SpriteFrames Bereich müssen mit dem, was im Code geschrieben steht, übereinstimmen. Wenn die Animation "Walk"
heißt, dann muss auch ein großgeschriebenes "W" im Code stehen.
Spielen Sie die Szene noch einmal ab und überprüfen, ob die Animationen in jede Richtung korrekt sind. Wenn Sie sich sicher sind, dass die Bewegung korrekt funktioniert, fügen Sie diese Zeile zu _ready()
hinzu, damit der Spieler zu Beginn des Spiels ausgeblendet wird:
hide()
Hide();
Vorbereitung auf Kollisionen¶
Wir wollen, dass der Player
erkennt, wann er von einem Feind getroffen wird, aber wir haben uns noch keine Feinde erstellt! Das ist in Ordnung, denn wir werden Godots Signal-Funktionalität nutzen, damit es funktioniert.
Fügen Sie folgendes oben im Skript nach extends Area2D
hinzu:
signal hit
// Don't forget to rebuild the project so the editor knows about the new signal.
[Signal]
public delegate void Hit();
Dies definiert ein benutzerdefiniertes Signal namens "hit", das wir von unserem Spieler aussenden (send out) lassen, wenn es mit einem Gegner kollidiert. Wir werden Area2D
verwenden, um die Kollision zu erkennen. Wählen Sie den Node Player
und klicken auf die Registerkarte "Node" neben der Registerkarte Inspektor, um die Liste der Signale zu sehen, die der Spieler ausgeben kann:

Beachten Sie, dass unser benutzerdefiniertes "hit"-Signal auch da ist! Da unsere Feinde RigidBody2D
Nodes sein werden, brauchen wir das Signal body_entered(body: Node)
; dieses wird ausgesendet, wenn ein Körper den Spieler berührt. Klicken Sie auf "Verbinden..." und dann, im "Signal verbinden"-Fenster, wieder auf "Verbinden". Wir müssen keine dieser Einstellungen ändern - Godot erstellt automatisch eine Funktion im Skript des Spielers. Diese Funktion wird immer dann aufgerufen, wenn das Signal ausgelöst wird - sie handhabt das Signal.

Beachten Sie das grüne Symbol, was anzeigt, dass das Signal mit dieser Funktion verbunden ist. Fügen Sie diesen Code zu der Funktion hinzu:
func _on_Player_body_entered(body):
hide() # Player disappears after being hit.
emit_signal("hit")
$CollisionShape2D.set_deferred("disabled", true)
public void OnPlayerBodyEntered(PhysicsBody2D body)
{
Hide(); // Player disappears after being hit.
EmitSignal("Hit");
GetNode<CollisionShape2D>("CollisionShape2D").SetDeferred("disabled", true);
}
Jedes Mal, wenn ein Feind den Spieler trifft, wird das Signal ausgesendet. Wir müssen die Kollision des Spielers deaktivieren, damit das Treffer
-Signal nicht mehr als einmal ausgelöst wird.
Bemerkung
Das Deaktivieren der Kollisionsform während der Kollisionsberechnung der Engine kann einen Fehler auslösen. Die Verwendung von set_deferred()
weist Godot an, mit dem Deaktivieren der Form bis zu einem sicheren Zeitpunkt zu warten.
Das letzte was wir tun müssen ist eine Funktion hinzuzufügen, die wir aufrufen können, um den Spieler beim erneuten Start des Spiels zurückzusetzen.
func start(pos):
position = pos
show()
$CollisionShape2D.disabled = false
public void Start(Vector2 pos)
{
Position = pos;
Show();
GetNode<CollisionShape2D>("CollisionShape2D").Disabled = false;
}
Feind-Szene¶
Jetzt ist es an der Zeit die Feinde zu erstellen, denen der Spieler ausweichen muss. Ihr Verhalten wird nicht allzu komplex sein: Gegner werden zufällig an den Rändern des Bildschirms erscheinen und sich in einer zufälligen Richtung in einer geraden Linie fortbewegen, um danach geradeaus weiterzugehen.
Wir werden dies in eine Mob
-Szene einbauen, die wir dann instanziieren um beliebig viele unabhängige Gegner im Spiel erstellen zu können.
Bemerkung
Siehe Instanziieren, um mehr über das Instanzieren zu erfahren.
Node einrichten¶
Klicken Sie auf Szene -> Neue Szene und fügen Sie die folgenden Nodes hinzu:
RigidBody2D (genannt
Mob
)
Vergessen Sie nicht, die Unterobjekte so einzustellen, dass sie nicht ausgewählt werden können, wie Sie es bei der Player-Szene getan haben.
Stellen Sie in den RigidBody2D Eigenschaften Gravity Scale
auf 0
, damit der Gegner nicht nach unten fällt. Klicken Sie außerdem unter dem Abschnitt PhysicsBody2D
auf die Eigenschaft Mask
und deaktivieren das erste Kontrollkästchen. Dadurch wird sichergestellt, dass die Gegner nicht miteinander kollidieren.

Richten Sie das AnimatedSprite so ein, wie wir es für den Spieler getan haben. Dieses Mal haben wir drei Animationen: fly
, swim
, und walk
. Zu jeder Animation gibt es zwei Bilder im "art" Ordner.
Setzen Sie die "Geschwindigkeit (FPS)" für alle Animationen auf 3
.

Stelle die Eigenschaft Playing
im Inspektor auf "On".
Wir werden eine der Animationen zufällig auswählen, sodass die Gegner eine gewisse Vielfalt haben.
Wie die Spieler-Bilder müssen auch diese Gegner-Bilder verkleinert werden. Setze die Scale
Eigenschaft von AnimatedSprite
auf (0.75, 0.75)
.
Wie in der Szene Player
, fügen Sie eine CapsuleShape2D
für die Kollision hinzu. Um die Form mit dem Bild auszurichten, müssen Sie die Eigenschaft Rotation Degrees
auf 90
(unter Node2D
-Transform im Inspektor) einstellen.
Speichern Sie die Szene.
Feind-Skript¶
Fügen Sie ein Skript zur Mob
-Szene hinzu und fügen Sie im Skript folgende Member-Variablen ein:
extends RigidBody2D
export var min_speed = 150 # Minimum speed range.
export var max_speed = 250 # Maximum speed range.
public class Mob : RigidBody2D
{
// Don't forget to rebuild the project so the editor knows about the new export variables.
[Export]
public int MinSpeed = 150; // Minimum speed range.
[Export]
public int MaxSpeed = 250; // Maximum speed range.
}
Wir wählen mit einem Zufallswert zwischen min_speed
und max_speed
, wie schnell sich jeder Gegner bewegen wird (es wäre trivial, würden sich alle mit der selben Geschwindigkeit fortbewegen). Stelle sie im Inspektor auf 150
und 250
. Wir haben auch einen Array mit den Namen der drei Animationen, mit dessen Hilfe wir eine Zufällige auswählen können.
Lassen Sie uns nun den Rest des Skripts betrachten. In _ready()
wählen wir zufällig einen der drei Animationstypen:
func _ready():
var mob_types = $AnimatedSprite.frames.get_animation_names()
$AnimatedSprite.animation = mob_types[randi() % mob_types.size()]
// C# doesn't implement GDScript's random methods, so we use 'System.Random' as an alternative.
static private Random _random = new Random();
public override void _Ready()
{
var animSprite = GetNode<AnimatedSprite>("AnimatedSprite");
var mobTypes = animSprite.Frames.GetAnimationNames();
animSprite.Animation = mobTypes[_random.Next(0, mobTypes.Length)];
}
Zunächst werden wir die Liste der Animationsnamen von der Eigenschaft frames
im AnimatedSprite erhalten. Diese gibt ein Array zuruück, dass alle drei Animationsnamen enthält: ["walk", "swim", "fly"]
.
Wir werden eine zufällige Zahl zwischen 0
und 2
auswählen müssen, um einen von diesen Namen aus der Liste auswählen zu können (Arrays starten bei 0
). randi() % n
wählt eine zufällige Ganzzahl zwischen 0
und n-1
aus.
Bemerkung
Sie müssen randomize()
verwenden, wenn Sie möchten, dass Ihre Reihenfolge der "Zufallszahlen" bei jedem erneuten Ausführen der Szene unterschiedlich ist. Wir werden randomize()
in unserer Main
Szene verwenden, so dass wir es hier nicht brauchen. randi() % n
ist der Standardweg, um eine zufällige ganze Zahl zwischen 0
und n-1
zu erhalten.
Der letzte Schritt ist es, die Gagner (Mobs) dazu zu bringen, sich selbst zu löschen, wenn sie den Bildschirm verlassen. Verbinden Sie das Signal screen_exited()
vom Node VisibilityNotifier2D
und füge diesen Code hinzu:
func _on_VisibilityNotifier2D_screen_exited():
queue_free()
public void OnVisibilityNotifier2DScreenExited()
{
QueueFree();
}
Damit ist die Szene des Gegners fertig.
Main-Szene¶
Jetzt ist es an der Zeit, alles zusammenzubringen. Erstellen Sie eine neue Szene und fügen einen Node namens Main
hinzu. Stellen Sie sicher, dass Sie einen Node und nicht einen Node2D erstellen. Klicken Sie auf die Knopf "Instance" und wählen Ihre gespeicherte Player.tscn
.

Fügen Sie nun die folgenden Nodes als untergeordnete Elemente von Main
hinzu und benenne sie wie abgebildet (Werte sind in Sekunden):
Timer (genannt
MobTimer
) - um zu steuern, wie oft Gegner erscheinenTimer (genannt
ScoreTimer
) - um die Punktzahl jede Sekunde zu erhöhenTimer (genannt
StartTimer
) - um eine Verzögerung vor dem Start zu gebenPosition2D (genannt
StartPosition
) - um die Startposition des Spielers anzuzeigen
Stellen Sie die Eigenschaft Wait Time
von jedem der Timer
Nodes wie folgt ein:
MobTimer
:0.5
ScoreTimer
:1
StartTimer
:2
Stellen Sie zusätzlich die Eigenschaft One Shot
von StartTimer
auf "An" und die Position
des StartPosition
-Nodes auf (240, 450)
.
Gegner (Mobs) erzeugen¶
Der Main-Node wird neue Gegner hervorbringen, und wir möchten, dass sie an einer beliebigen Stelle am Rande des Bildschirms erscheinen. Fügen Sie einen Path2D <class_Path2D>`Node namens ``MobPath` als Unterobjekt von Main
hinzu. Wenn Sie Path2D
auswählen, sehen Sie oben im Editor einige neue Schaltflächen:

Wählen Sie die Mittlere ("Punkt hinzufügen (in leerem Raum)"). Zeichnen Sie den Pfad, indem Sie durch klicken auf die dargestellten Ecken die Punkte hinzuzufügen. Damit die Punkte am Gitter einrasten, stellen Sie sicher, dass "Gitter-Einrasten benutzen(Shift+G)" aktiviert ist. Diese Option findet sich links vom "Schloss"-Symbol und wird als ein "Magnet neben sich schneidender Linien" dargestellt.

Wichtig
Zeichnen Sie den Pfad im Uhrzeigersinn, sonst erscheinen Ihre Gegner nach außen statt nach innen!

Nachdem Sie den Punkt 4
im Bild platziert haben, klicken Sie auf die Schaltfläche "Kurve schließen" und Ihre Kurve ist vollständig.
Jetzt, nachdem der Pfad definiert ist, fügen Sie einen PathFollow2D Node als Unterobjekt von MobPath
hinzu und nenne ihn MobSpawnLocation
. Dieser Node dreht sich automatisch und folgt dem Pfad, während er sich bewegt, so dass wir damit eine beliebige Position und Richtung entlang des Pfades auswählen können.
Die Szene sollte so aussehen:

Main-Skript¶
Füge ein Skript zu Main
hinzu. Am Anfang des Skripts verwenden wir export (PackedScene)
, damit wir die Mob-Szene auswählen können, die wir als Instanz verwenden wollen.
extends Node
export (PackedScene) var Mob
var score
func _ready():
randomize()
public class Main : Node
{
// Don't forget to rebuild the project so the editor knows about the new export variable.
[Export]
public PackedScene Mob;
private int _score;
// We use 'System.Random' as an alternative to GDScript's random methods.
private Random _random = new Random();
public override void _Ready()
{
}
// We'll use this later because C# doesn't support GDScript's randi().
private float RandRange(float min, float max)
{
return (float)_random.NextDouble() * (max - min) + min;
}
}
Klicken Sie auf den Main
-Node und Sie werden die Mob
Eigenschaft im Inspektor unter "Skript Variablen" sehen.
Man kann einen Wert für eine Eigenschaft auf zwei Wege hinzufügen:
Ziehen Sie
Mob.tscn
aus dem Panel "FileSystem" und legen Sie es in der EigenschaftMob
unter den Skript-Variablen des NodesMain
ab.Klicken Sie auf den Pfeil nach unten neben "[leer]" und wählen "Lade". Klicken Sie anschließend auf
Mob.tscn
.
Wählen Sie den „Player“-Node im Szenenbaum aus und klicken dann auf die "Node"-Registerkarte. Als nächstes stellen Sie sicher, dass "Signale" ausgewählt ist.
Sie sollten eine Liste der Signale für den Player
-Node sehen. Suchen und doppelklicken Sie das hit
Signal (oder Rechtsklicke darauf und drücken "Verbinden..."). Dies wird das Fenster für die Signal-Verbindung öffnen. Wir wollen eine neue Funktion namens game_over
erstellen, welche angeben wird, was passieren muss, wenn das Spiel beendet wurde. Schreiben Sie "game_over" in das "Empfängermethode"-Eingabefeld unten im Fenster und klicken auf "Verbinden". Fügen Sie dann sowohl den folgenden Code zur neuen Funktion, als auch eine new_game
-Funktion, die alles für ein neues Spiel einstellen wird:
func game_over():
$ScoreTimer.stop()
$MobTimer.stop()
func new_game():
score = 0
$Player.start($StartPosition.position)
$StartTimer.start()
public void GameOver()
{
GetNode<Timer>("MobTimer").Stop();
GetNode<Timer>("ScoreTimer").Stop();
}
public void NewGame()
{
_score = 0;
var player = GetNode<Player>("Player");
var startPosition = GetNode<Position2D>("StartPosition");
player.Start(startPosition.Position);
GetNode<Timer>("StartTimer").Start();
}
Verbinden Sie nun das Signal Timeout()
von jedem der Timer-Nodes (StartTimer
, ScoreTimer
` und MobTimer
) mit dem Main-Skript. StartTimer
startet die anderen beiden Timer. ScoreTimer
erhöht die Punktzahl um 1.
func _on_StartTimer_timeout():
$MobTimer.start()
$ScoreTimer.start()
func _on_ScoreTimer_timeout():
score += 1
public void OnStartTimerTimeout()
{
GetNode<Timer>("MobTimer").Start();
GetNode<Timer>("ScoreTimer").Start();
}
public void OnScoreTimerTimeout()
{
_score++;
}
In _on_MobTimer_timeout()
erstellt eine Mob-Instanz, wählt einen zufälligen Startpunkt entlang des Path2D
aus und setzt den Gegner in Bewegung. Der PathFollow2D
-Node dreht sich automatisch, wenn er dem Pfad folgt. Wir verwenden diesen, um die Richtung des Gegners sowie dessen Position auszuwählen.
Beachten Sie, dass der Szene mit add_child()
eine neue Instanz hinzugefügt werden muss.
func _on_MobTimer_timeout():
# Choose a random location on Path2D.
$MobPath/MobSpawnLocation.offset = randi()
# Create a Mob instance and add it to the scene.
var mob = Mob.instance()
add_child(mob)
# Set the mob's direction perpendicular to the path direction.
var direction = $MobPath/MobSpawnLocation.rotation + PI / 2
# Set the mob's position to a random location.
mob.position = $MobPath/MobSpawnLocation.position
# Add some randomness to the direction.
direction += rand_range(-PI / 4, PI / 4)
mob.rotation = direction
# Set the velocity (speed & direction).
mob.linear_velocity = Vector2(rand_range(mob.min_speed, mob.max_speed), 0)
mob.linear_velocity = mob.linear_velocity.rotated(direction)
public void OnMobTimerTimeout()
{
// Choose a random location on Path2D.
var mobSpawnLocation = GetNode<PathFollow2D>("MobPath/MobSpawnLocation");
mobSpawnLocation.Offset = _random.Next();
// Create a Mob instance and add it to the scene.
var mobInstance = (RigidBody2D)Mob.Instance();
AddChild(mobInstance);
// Set the mob's direction perpendicular to the path direction.
float direction = mobSpawnLocation.Rotation + Mathf.Pi / 2;
// Set the mob's position to a random location.
mobInstance.Position = mobSpawnLocation.Position;
// Add some randomness to the direction.
direction += RandRange(-Mathf.Pi / 4, Mathf.Pi / 4);
mobInstance.Rotation = direction;
// Choose the velocity.
mobInstance.LinearVelocity = new Vector2(RandRange(150f, 250f), 0).Rotated(direction);
}
Wichtig
Warum PI
? In Funktionen die Winkel erfordern, verwendet GDScript Bogenmaß, nicht Grad. Wenn Sie sich beim Arbeiten mit Grad wohler fühlen, müssen Sie die Funktionen deg2rad()
und rad2deg()
verwenden, um zwischen beiden zu konvertieren.
Die Szene testen¶
Lassen Sie uns die Szene testen, um sicherzustellen, dass alles funktioniert. Fügen Sie folgendes _ready()
hinzu:
func _ready():
randomize()
new_game()
public override void _Ready()
{
NewGame();
}
}
Lassen Sie uns auch Main
zu unserer "Main-Szene" hinzufügen - die automatisch läuft, wenn das Spiel startet. Drücken Sie dazu den "Start"-Knopf und wählen Main.tscn
im Dialog.
Es sollte jetzt möglich sein den Spieler zu bewegen, erscheinende Gegner zu sehen, und den Spieler verschwinden zu sehen, wenn er von einem Gegner getroffen wird.
Wenn Sie sicher sind, dass alles läuft, löschen Sie den Funktionsaufruf new_game()
von _ready()
.
HUD¶
Das letzte Bauteil, das unser Spiel braucht, ist eine Benutzeroberfläche: eine Schnittstelle, um Dinge wie Punktestand, eine "Game Over"-Meldung und einen Neustart-Button anzuzeigen. Erstellen Sie eine neue Szene und füge einen CanvasLayer Node namens HUD
hinzu. "HUD" steht für "Heads-up Display", ein Informationsanzeige, das als Überlagerung über der Spielansicht erscheint.
Mit dem Node CanvasLayer können wir unsere Oberflächenelemente auf einer Ebene über dem Rest des Spiels zeichnen, so dass die angezeigten Informationen nicht durch irgendwelche Spielelemente wie den Spieler oder Gegner verdeckt werden.
Das HUD sollte die folgenden Informationen anzeigen:
Punktzahl, geändert durch
ScoreTimer
.Eine Nachricht, wie z.B. "Game Over" oder "Get Ready!"
Eine "Start"-Taste, um das Spiel zu starten.
Der Basis-Node für UI-Elemente ist Control. Um unsere Benutzeroberfläche zu erstellen, verwenden wir zwei Arten vom Control Node: Label und Button.
Erstelle das Folgende als Unterobjekte des HUD
-Nodes:
Label genannt
ScoreLabel
.Label genannt
Message
.Button genannt
StartButton
.Timer genannt
MessageTimer
.
Klicken Sie auf das ScoreLabel
und geben eine Zahl in das Text
-Feld im Inspektor ein. Die Standardschriftart für Control
-Nodes ist klein und lässt sich nicht gut skalieren. In den Spielassets ist eine Schriftdatei namens "Xolonium-Regular.ttf" enthalten. Um diese Schriftart zu verwenden, gehen Sie für jeden der drei Control
-Nodes wie folgt vor:
Wählen Sie unter "Custom Fonts" "Neues DynamicFont"

Klicken Sie auf den von Ihnen hinzugefügten "DynamicFont" und wählen unter "Font/Font Data" "Lade" und wählen Sie die Datei "Xolonium-Regular.ttf". Ändern Sie die Schriftgröße (
Size
) auf64
.

Sobald Sie das auf dem ScoreLabel
erledigt haben, können Sie den Pfeil nach unten drücken neben der DynamicFont-Eigenschaft und wählen "Kopieren", dann "Einfügen" in den gleichen Ort bei den anderen zwei Control-Nodes.
Bemerkung
Anker und Ränder: Control
-Nodes haben eine Position und Größe, aber sie haben auch Anker und Ränder. Anker definieren den Ursprung - den Bezugspunkt für die Kanten des Nodes. Die Ränder werden automatisch aktualisiert, wenn Sie einen Control-Node verschieben oder in der Größe ändern. Sie stellen den Abstand von den Kanten des Control-Nodes zu seinem Anker dar. Siehe Benutzeroberflächen gestalten mit Kontroll-Nodes für weitere Details.
Ordnen Sie die Nodes wie unten gezeigt an. Klicken Sie auf den "Layout"-Knopf, um das Layout eines Control-Nodes festzulegen:

Sie können die Nodes manuell verschieben um sie zu platzieren oder verwenden Sie die folgenden Einstellungen für eine genauere Platzierung:
ScoreLabel (HighScore)¶
Layout : "Oben groß"
Text :
0
Align : "Center"
Nachricht¶
Layout : "HCenter groß"
Text :
Dodge the Creeps!
Align : "Center"
Autowrap : "An"
StartButton (Startknopf)¶
Text :
Start
Layout : "Mitte unten"
Margin :
Top:
-200
Bottom:
-100
Stellen Sie im MessageTimer
die Wait Time
auf 2
und stellen Sie die One Shot
Eigenschaft auf "An".
Fügen Sie nun dieses Skript zu HUD
hinzu:
extends CanvasLayer
signal start_game
public class HUD : CanvasLayer
{
// Don't forget to rebuild the project so the editor knows about the new signal.
[Signal]
public delegate void StartGame();
}
Das Signal start_game
signalisiert dem Node Main
, dass die Taste gedrückt wurde.
func show_message(text):
$Message.text = text
$Message.show()
$MessageTimer.start()
public void ShowMessage(string text)
{
var message = GetNode<Label>("Message");
message.Text = text;
message.Show();
GetNode<Timer>("MessageTimer").Start();
}
Diese Funktion wird aufgerufen, wenn wir eine Nachricht vorübergehend anzeigen möchten, z.B. "Get Ready".
func show_game_over():
show_message("Game Over")
# Wait until the MessageTimer has counted down.
yield($MessageTimer, "timeout")
$Message.text = "Dodge the\nCreeps!"
$Message.show()
# Make a one-shot timer and wait for it to finish.
yield(get_tree().create_timer(1), "timeout")
$StartButton.show()
async public void ShowGameOver()
{
ShowMessage("Game Over");
var messageTimer = GetNode<Timer>("MessageTimer");
await ToSignal(messageTimer, "timeout");
var message = GetNode<Label>("Message");
message.Text = "Dodge the\nCreeps!";
message.Show();
await ToSignal(GetTree().CreateTimer(1), "timeout");
GetNode<Button>("StartButton").Show();
}
Diese Funktion wird aufgerufen, wenn der Spieler verliert. "Game Over" wird für 2 Sekunden angezeigt und wechselt dann zum Titelbildschirm. Nach einer kurzen Pause wird die "Start"-Schaltfläche angezeigt.
Bemerkung
Falls eine kurze Pause benötigt wird, kann als Alternative zum Timer-Node die Funktion create_timer()
von SceneTree verwendet werden. Das kann sehr nützlich sein, um eine Verzögerung hinzuzufügen. Wie im obigen Code, wo wir ein wenig warten wollen, bevor der "Start"-Knopf angezeigt wird.
func update_score(score):
$ScoreLabel.text = str(score)
public void UpdateScore(int score)
{
GetNode<Label>("ScoreLabel").Text = score.ToString();
}
Diese Funktion wird von Main
aufgerufen, wenn sich der Punktestand ändert.
Verbinden Sie jeweils das timeout()
-Signal von MessageTimer
und das pressed()
-Signal von StartButton
und fügen Sie den folgenden Code in die neuen Funktionen ein:
func _on_StartButton_pressed():
$StartButton.hide()
emit_signal("start_game")
func _on_MessageTimer_timeout():
$Message.hide()
public void OnStartButtonPressed()
{
GetNode<Button>("StartButton").Hide();
EmitSignal("StartGame");
}
public void OnMessageTimerTimeout()
{
GetNode<Label>("Message").Hide();
}
Das HUD mit Main verbinden¶
Jetzt, da wir die HUD
-Szene erstellt haben, gehen Sie zurück zu Main
und platziere die HUD
-Szene in Main
genauso wie die Player
-Szene. Der gesamte Szenenbaum sollte so aussehen, also stellen Sie sicher, dass nichts fehlt:

Nun müssen wir die HUD
-Funktionalität mit unserem Main
-Skript verbinden. Dies erfordert einige Ergänzungen in der Main
-Szene:
Verbinden Sie auf der Registerkarte "Node" das Signal start_game
des HUD mit der Funktion new_game()
des Main-Nodes, indem Sie "new_game" in die "Empfängermethode" im Fenster "Ein Signal mit einer Methode verbinden" eingeben. Stellen Sie sicher, dass das grüne Verbindungssymbol jetzt neben func new_game()
im Skript angezeigt wird.
Aktualisieren Sie in new_game()
die Punkteanzeige und zeigen Sie die Meldung "Get Ready" an:
$HUD.update_score(score)
$HUD.show_message("Get Ready")
var hud = GetNode<HUD>("HUD");
hud.UpdateScore(_score);
hud.ShowMessage("Get Ready!");
In game_over()
müssen wir die entsprechende HUD
Funktion aufrufen:
$HUD.show_game_over()
GetNode<HUD>("HUD").ShowGameOver();
Abschließend fügen Sie nachfolgendes in _on_ScoreTimer_timeout()
hinzu, um die Anzeige mit den sich ändernden Punkten zu synchronisieren:
$HUD.update_score(score)
GetNode<HUD>("HUD").UpdateScore(_score);
Jetzt können Sie spielen! Klicken Sie auf die Schaltfläche "Projekt abspielen (F5)". Sie werden aufgefordert, eine Hauptszene auszuwählen. Wählen Sie dann Main.tscn
aus.
Alte Gegner entfernen¶
Wenn Sie bis zum "Game Over" spielen und dann ein neues Spiel starten, befinden sich die Gegner des vorherigen Spiel noch auf dem Bildschirm. Es wäre besser wenn diese beim Start eines neuen Spiels verschwinden würden. Wir benötigen einen Weg um alle Gegner zu entfernen und dies geschieht mit der "Gruppen" Funktion.
Wählen Sie in der Szene Mob
den Wurzel-Node und klicken Sie auf die Registerkarte "Node" neben dem Inspektor (die gleiche Stelle, an der Sie die Signale des Nodes finden). Klicken Sie neben "Signale" auf "Gruppen", und Sie können einen neuen Gruppennamen eingeben und auf "Hinzufügen" klicken.

Nun werden alle Gegner in der Gruppe "mobs" sein. Wir können dann die folgende Zeile zur Funktion game_over()
in Main
hinzufügen:
get_tree().call_group("mobs", "queue_free")
GetTree().CallGroup("mobs", "queue_free");
Die Funktion call_group()
ruft die benannte Funktion auf jedem Node in einer Gruppe auf - in diesem Fall sagen wir jedem Gegner, dass er sich selbst löschen soll.
Fertigstellung¶
Wir haben jetzt alle Funktionen für unser Spiel fertiggestellt. Im Folgenden werden einige Schritte beschrieben, um etwas mehr „Tiefe“ hinzuzufügen, die das Spielerlebnis verbessert. Zögern Sie nicht, das Gameplay mit Ihren eigenen Ideen zu erweitern.
Hintergrund¶
Der voreingestellte graue Hintergrund ist nicht sehr ansprechend, ändern wir also die Farbe. Eine Möglichkeit, dies zu tun ist die Verwendung eines ColorRect <class_ColorRect> -Nodes. Machen Sie ihn zum ersten Node unter ``Main`, damit er hinter dem anderen Node gezeichnet wird. ColorRect
hat nur eine Eigenschaft: Color
. Wählen Sie eine Farbe aus die Ihnen gefällt und ziehe die Größe des ColorRect
so, dass es den Bildschirm abdeckt.
Wenn Sie ein Hintergrundbild haben, können Sie dies stattdessen mittels TextureRect
Node hinzufügen.
Soundeffekte¶
Sound und Musik können effektive Mittel sein, das Spielerlebnis attraktiver zu gestalten. In Ihrem Ordner mit den Spielressourcen befinden sich zwei Sounddateien: „House in a Forest Loop.ogg“ für Hintergrundmusik und „gameover.wav“ für den Fall, dass der Spieler verliert.
Fügen Sie zwei AudioStreamPlayer Nodes als Unterelemente von Main
hinzu. Benennen Sie einen davon Music
und den anderen DeathSound
. Klicken Sie bei jedem Node auf die Eigenschaft Stream
, klicken Sie auf „Lade“ und wählen Sie die entsprechende Audiodatei aus.
Um die Musik abzuspielen, fügen Sie $Music.play()
in der new_game()
Funktion und $Music.stop()
in der game_over()
Funktion hinzu.
Fügen Sie abschließend $DeathSound.play()
in der game_over()
Funktion hinzu.
Tastenkürzel¶
Da das Spiel mit Tastatur gesteuert wird, wäre es praktisch, wenn wir das Spiel auch durch Drücken einer Taste auf der Tastatur starten könnten. Eine Möglichkeit hierzu ist die Verwendung der „Shortcut“ -Eigenschaft des Button
-Nodes.
Wählen Sie in der HUD
-Szene den StartButton
aus, und suchen im Inspektor nach seiner Shortcut-Eigenschaft. Wählen Sie "Neues Shortcut" und klicke auf das Shortcut-Element. Eine zweite „Shortcut“-Eigenschaft wird angezeigt. Wählen Sie „Neue InputEventAction“ und klicken auf das neue „InputEvent“-Element. Geben Sie schließlich in der Eigenschaft Action den Namen ui_select
ein. Dies ist das Standard Eingabe-Ereignis, das der Leertaste zugeordnet ist.

Wenn nun die Start-Schaltfläche angezeigt wird, können Sie entweder darauf klicken oder die Leertaste drücken, um das Spiel zu starten.
Projektdateien¶
- Eine vollständige Version dieses Projekts finden Sie unter: