Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
Springen und Zerstampfen von Monstern
In diesem Teil werden wir die Fähigkeit hinzufügen, zu springen und die Monster zu zerstampfen. In der nächsten Lektion werden wir dafür sorgen, dass der Spieler stirbt, wenn ein Monster ihn am Boden trifft.
Zuerst müssen wir einige Einstellungen in Bezug auf Physik-Interaktionen ändern. Wir betreten die Welt der Physik-Layer.
Steuerung der Physik-Interaktionen
Physics-Bodies haben Zugriff auf zwei komplementäre Eigenschaften: Ebenen und Masken. Ebenen definieren auf welcher/-n Physik-Ebene(n) sich ein Objekt befindet.
Masken steuern die Ebenen, auf die ein Body hört und die er erkennt. Dies wirkt sich auf die Kollisionserkennung aus. Wenn zwei Bodies miteinander interagieren sollen, muss mindestens einer eine Maske haben, die dem anderen entspricht.
Wenn das verwirrend ist, keine Sorge, wir werden gleich drei Beispiele sehen.
Wichtig ist, dass Sie Ebenen und Masken verwenden können, um Physik-Interaktionen zu filtern, die Performance zu steuern und um zusätzliche Bedingungen in Ihrem Code überflüssig zu machen.
Standardmäßig sind alle Physics-Bodies und Flächen sowohl auf die Ebene als auch auf die Maske 1
eingestellt. Das bedeutet, dass sie alle miteinander kollidieren.
Physik-Ebenen werden durch Zahlen dargestellt, aber wir können ihnen Namen geben, um den Überblick darüber zu behalten, welche es sind.
Ebenen-Namen festlegen
Geben wir unseren Physikebenen einen Namen. Gehen Sie zu Projekt -> Projekteinstellungen.
Navigieren Sie im linken Menü nach unten zu Ebenen-Namen -> 3D-Physik. Auf der rechten Seite sehen Sie eine Liste von Ebenen mit einem Feld neben jeder von ihnen. Dort können Sie ihre Namen festlegen. Nennen Sie die ersten drei Ebenen player, enemies und world.
Jetzt können wir sie unseren Physik-Nodes zuweisen.
Ebenen und Masken zuweisen
Wählen Sie in der Main Szene den Ground
-Node. Erweitern Sie im Inspektor den Abschnitt Kollision. Dort sehen Sie die Ebenen und Masken des Nodes als ein Raster von Buttons.
Der Boden ist Teil der Welt, also soll er Teil der dritten Ebene sein. Klicken Sie auf den beleuchteten Button, um die erste Ebene auszuschalten, und schalten Sie die dritte Ebene ein. Schalten Sie dann die Maske aus, indem Sie sie anklicken.
Wie bereits erwähnt, erlaubt die Eigenschaft Maske einem Node, auf die Interaktion mit anderen Physik-Objekten zu hören, aber wir brauchen ihn nicht, um Kollisionen zu bekommen. Ground
muss auf nichts hören; er ist nur dazu da, um zu verhindern, dass Kreaturen herunterfallen.
Beachten Sie, dass Sie auf den "..."-Button auf der rechten Seite der Eigenschaften klicken können, um eine Liste der benannten Checkboxen anzuzeigen.
Als nächstes sind der Player
und der Mob
dran. Öffnen Sie player.tscn
durch einen Doppelklick auf die Datei im Dateisystem-Dock.
Wählen Sie den Player-Node aus und setzen Sie seine Kollision -> Maske sowohl auf "enemies" als auch auf "world". Sie können die Default-Eigenschaft Ebene so lassen, wie sie ist, da die erste Ebene die Ebene "player" ist.
Öffnen Sie dann die Mob-Szene durch Doppelklick auf mob.tscn
und wählen Sie den Mob
-Node aus.
Setzen Sie Collision -> Layer auf "enemies" und entfernen sie alle Auswahlfelder in der Collision -> Mask. So dass die Masken leer bleiben.
Diese Einstellungen bedeuten, dass die Monster einander durchdringen. Wenn Sie möchten, dass die Monster miteinander kollidieren und aneinander entlanggleiten, schalten Sie die Maske "enemies" ein.
Bemerkung
Die Mobs brauchen die "world"-Ebene nicht zu maskieren, da sie sich nur auf der XZ-Ebene bewegen. Wir wenden keine Schwerkraft auf sie an.
Springen
Die Sprungmechanik selbst erfordert nur zwei Codezeilen. Öffnen Sie das Skript Player. Wir brauchen einen Wert, um die Stärke des Sprungs zu kontrollieren, und aktualisieren _physics_process()
, um den Sprung zu erstellen.
Fügen sie am Anfang des Skripts nach der Zeile, die fall_acceleration
definiert, jump_impulse
ein.
#...
# Vertical impulse applied to the character upon jumping in meters per second.
@export var jump_impulse = 20
// Don't forget to rebuild the project so the editor knows about the new export variable.
// ...
// Vertical impulse applied to the character upon jumping in meters per second.
[Export]
public int JumpImpulse { get; set; } = 20;
Innerhalb von _physics_process()
fügen Sie den folgenden Code vor dem Codeblock move_and_slide()
ein.
func _physics_process(delta):
#...
# Jumping.
if is_on_floor() and Input.is_action_just_pressed("jump"):
target_velocity.y = jump_impulse
#...
public override void _PhysicsProcess(double delta)
{
// ...
// Jumping.
if (IsOnFloor() && Input.IsActionJustPressed("jump"))
{
_targetVelocity.Y = JumpImpulse;
}
// ...
}
Das ist alles, was man zum Springen braucht!
Die Methode is_on_floor()
ist eine Hilfsfunktion der Klasse CharacterBody3D
. Sie gibt true
zurück, wenn der Body in diesem Frame mit dem Boden kollidiert ist. Das ist der Grund, warum wir die Schwerkraft auf den Player anwenden: damit wir mit dem Boden kollidieren, anstatt über ihm zu schweben wie die Monster.
Wenn der Charakter auf dem Boden steht und der Spieler auf "springen" drückt, geben wir ihm sofort eine Menge vertikaler Geschwindigkeit. In Spielen muss die Steuerung wirklich reaktionsschnell sein, und solche sofortigen Geschwindigkeitsschübe sind zwar unrealistisch, fühlen sich aber toll an.
Beachten Sie, dass die Y-Achse positiv nach oben verläuft. Das ist anders als in 2D, wo die Y-Achse positiv nach unten verläuft.
Monster zerstampfen
Fügen wir als Nächstes die Stampf-Mechanik hinzu. Wir werden den Charakter über Monster springen lassen und sie gleichzeitig töten.
Wir müssen Kollisionen mit einem Monster erkennen und sie von Kollisionen mit dem Boden unterscheiden. Dazu können wir Godots group-Tagging-Feature verwenden.
Öffnen Sie die Szene mob.tscn
erneut und wählen Sie den Mob-Node. Gehen Sie zum Node-Dock auf der rechten Seite, um eine Liste der Signale zu sehen. Das Node-Dock hat zwei Tabs: Signale, die Sie bereits verwendet haben, und Gruppen, mit denen Sie den Nodes Tags zuweisen können.
Klicken Sie darauf, um ein Feld zu öffnen, in das Sie einen Tag-Namen eingeben können. Geben Sie "mob" in das Feld ein und klicken Sie auf den Hinzufügen-Button.
Ein Icon erscheint im Szenen-Dock, um anzuzeigen, dass der Node Teil mindestens einer Gruppe ist.
Wir können nun die Gruppe aus dem Code heraus verwenden, um Kollisionen mit Monstern von Kollisionen mit dem Boden zu unterscheiden.
Die Stampf-Mechanik programmieren
Gehen Sie zurück zum Skript Player, um den Stampf und den Sprung zu programmieren.
Am Anfang des Skripts brauchen wir eine weitere Variable, bounce_impulse
. Wenn wir einen Gegner zerstampfen, wollen wir nicht unbedingt, dass der Charakter so hoch fliegt wie beim Springen.
# Vertical impulse applied to the character upon bouncing over a mob in
# meters per second.
@export var bounce_impulse = 16
// Don't forget to rebuild the project so the editor knows about the new export variable.
// Vertical impulse applied to the character upon bouncing over a mob in meters per second.
[Export]
public int BounceImpulse { get; set; } = 16;
Dann, nach dem Springen Codeblock, den wir oben in _physics_process()
hinzugefügt haben, fügen Sie die folgende Schleife ein. Mit move_and_slide()
bewegt Godot den Body manchmal mehrmals hintereinander, um die Bewegung des Charakters zu glätten. Wir müssen also eine Schleife über alle Kollisionen ausführen, die passiert sein könnten.
In jeder Iteration der Schleife prüfen wir, ob wir auf einem Mob gelandet sind. Wenn ja, töten wir ihn und hüpfen weiter.
Mit diesem Code wird die Schleife nicht durchlaufen, wenn in einem bestimmten Frame keine Kollisionen aufgetreten sind.
func _physics_process(delta):
#...
# Iterate through all collisions that occurred this frame
for index in range(get_slide_collision_count()):
# We get one of the collisions with the player
var collision = get_slide_collision(index)
# If there are duplicate collisions with a mob in a single frame
# the mob will be deleted after the first collision, and a second call to
# get_collider will return null, leading to a null pointer when calling
# collision.get_collider().is_in_group("mob").
# This block of code prevents processing duplicate collisions.
if collision.get_collider() == null:
continue
# If the collider is with a mob
if collision.get_collider().is_in_group("mob"):
var mob = collision.get_collider()
# we check that we are hitting it from above.
if Vector3.UP.dot(collision.get_normal()) > 0.1:
# If so, we squash it and bounce.
mob.squash()
target_velocity.y = bounce_impulse
# Prevent further duplicate calls.
break
public override void _PhysicsProcess(double delta)
{
// ...
// Iterate through all collisions that occurred this frame.
for (int index = 0; index < GetSlideCollisionCount(); index++)
{
// We get one of the collisions with the player.
KinematicCollision3D collision = GetSlideCollision(index);
// If the collision is with a mob.
// With C# we leverage typing and pattern-matching
// instead of checking for the group we created.
if (collision.GetCollider() is Mob mob)
{
// We check that we are hitting it from above.
if (Vector3.Up.Dot(collision.GetNormal()) > 0.1f)
{
// If so, we squash it and bounce.
mob.Squash();
_targetVelocity.Y = BounceImpulse;
// Prevent further duplicate calls.
break;
}
}
}
}
Das sind eine Menge neuer Funktionen. Hier finden Sie weitere Informationen über sie.
Die Funktionen get_slide_collision_count()
und get_slide_collision()
kommen beide von der Klasse CharacterBody3D und sind mit move_and_slide()
verbunden.
get_slide_collision()
gibt ein KinematicCollision3D Objekt zurück, das Informationen darüber enthält, wo und wie die Kollision stattfand. Zum Beispiel benutzen wir die Eigenschaft get_collider
, um zu überprüfen, ob wir mit einem "Mob" kollidiert sind, indem wir is_in_group()
auf ihn anwenden: collision.get_collider().is_in_group("mob")
.
Bemerkung
Die Methode is_in_group()
ist auf jedem Node verfügbar.
Um zu überprüfen, ob wir auf dem Monster landen, verwenden wir das Vektor-Skalarprodukt: Vector3.UP.dot(collision.get_normal()) > 0.1
. Die Kollisionsnormale ist ein 3D-Vektor, der senkrecht zu der Ebene steht, in der die Kollision stattgefunden hat. Mit Hilfe des Skalarprodukts können wir ihn mit der Aufwärtsrichtung vergleichen.
Wenn das Ergebnis des Skalarprodukts größer ist als 0
, befinden sich die beiden Vektoren in einem Winkel von weniger als 90 Grad. Ein Wert, der größer als 0.1
ist, bedeutet, dass wir uns ungefähr über dem Monster befinden.
Nachdem wir die Squash- und Bounce-Logik abgearbeitet haben, beenden wir die Schleife vorzeitig mit der break
-Anweisung, um weitere doppelte Aufrufe von mob.squash()
zu verhindern, die sonst zu unbeabsichtigten Fehlern führen könnten, wie z.B. das mehrfache Zählen der Punkte für einen Kill.
Wir rufen eine undefinierte Funktion, mob.squash()
, auf, also müssen wir sie der Mob-Klasse hinzufügen.
Öffnen Sie das Skript mob.gd
, indem Sie im Dock FileSystem darauf doppelklicken. Oben im Skript möchten wir ein neues Signal mit dem Namen squashed
definieren. Und unten können Sie die Squash-Funktion hinzufügen, mit der wir das Signal senden und den Mob zerstören.
# Emitted when the player jumped on the mob.
signal squashed
# ...
func squash():
squashed.emit()
queue_free()
// Don't forget to rebuild the project so the editor knows about the new signal.
// Emitted when the player jumped on the mob.
[Signal]
public delegate void SquashedEventHandler();
// ...
public void Squash()
{
EmitSignal(SignalName.Squashed);
QueueFree();
}
Bemerkung
Wenn Sie C# verwenden, erstellt Godot automatisch die entsprechenden Ereignisse für alle Signale, die auf EventHandler enden, siehe C#-Signale.
In der nächsten Lektion werden wir das Signal verwenden, um Punkte zum Punktestand zu addieren.
Damit sollten Sie in der Lage sein, Monster zu töten, indem Sie auf sie springen. Sie können F5 drücken, um das Spiel auszuprobieren und main.tscn
als Hauptszene für Ihr Projekt festzulegen.
Allerdings wird der Spieler noch nicht sterben. Daran werden wir im nächsten Teil arbeiten.