Work in progress
The content of this page was not yet updated for Godot
4.2
and may be outdated. If you know how to improve this page or you can confirm
that it's up to date, feel free to open a pull request.
Den Spieler töten¶
Wir können Gegner töten, indem wir auf sie springen, aber der Spieler kann trotzdem nicht sterben. Lassen Sie uns das beheben.
Wir wollen, dass ein Treffer von einem Gegner anders erkannt wird, als wenn er zerstampft wird. Wir wollen, dass der Spieler stirbt, wenn er sich auf dem Boden bewegt, aber nicht, wenn er in der Luft ist. Wir könnten Vektormathematik verwenden, um die beiden Arten von Kollisionen zu unterscheiden. Stattdessen werden wir jedoch einen Area3D-Node verwenden, der gut für Hitboxen funktioniert.
Hitbox mit dem Area-Node¶
Gehen Sie zurück zur Szene player.tscn
und fügen Sie einen neuen Child-Node Area3D hinzu. Nennen Sie ihn MobDetector
. Fügen Sie einen CollisionShape3D-Node als Child davon hinzu.
Weisen Sie ihm im Inspektor eine Cylinder-Shape zu.
Es gibt einen Trick, mit dem die Kollisionen nur dann stattfinden, wenn sich der Spieler auf dem Boden oder in Bodennähe befindet. Sie können die Höhe des Zylinders verringern und ihn bis zum oberen Rand des Charakters verschieben. Wenn der Spieler dann springt, ist die Shape zu hoch, als dass die Gegner mit ihr kollidieren könnten.
Außerdem soll der Zylinder breiter sein als die Kugel. Auf diese Weise wird der Spieler getroffen, bevor er kollidiert und auf das Kollisionsfeld des Monsters geschoben wird.
Je breiter der Zylinder, desto eher wird der Spieler getötet.
Als nächstes wählen Sie erneut den MobDetector
-Node aus und schalten im Inspektor die Eigenschaft Überwachbar aus. Dadurch können andere Physik-Nodes den Bereich nicht erkennen. Die ergänzende Eigenschaft Wird überwacht ermöglicht es ihm, Kollisionen zu erkennen. Entfernen Sie dann die Eigenschaft Kollision -> Ebene und setzen Sie die Maske auf die Ebene "enemies".
Wenn Areas eine Kollision erkennen, senden sie Signale aus. Wir werden eines mit dem Player
-Node verbinden. Wählen Sie MobDetector
und gehen Sie zum Node-Tab des Inspektors Tab, doppelklicken Sie das body_entered
Signal und verbinden Sie es mit dem Player
Der MobDetector sendet body_entered
aus, wenn ein CharacterBody3D oder ein RigidBody3D-Node ihn betritt. Da es nur die "enemies"-Physikebenen maskiert, wird es nur die Mob
-Nodes erkennen.
Auf Code-Ebene werden wir zwei Dinge tun: Ein Signal aussenden, welches wir später verwenden werden, um das Spiel zu beenden und den Spieler zu zerstören. Wir können diese Vorgänge in eine die()
Funktion verpacken um dem Code eine aussagekräftige Beschriftung zu geben.
# Emitted when the player was hit by a mob.
# Put this at the top of the script.
signal hit
# And this function at the bottom.
func die():
hit.emit()
queue_free()
func _on_mob_detector_body_entered(body):
die()
// Don't forget to rebuild the project so the editor knows about the new signal.
// Emitted when the player was hit by a mob.
[Signal]
public delegate void HitEventHandler();
// ...
private void Die()
{
EmitSignal(SignalName.Hit);
QueueFree();
}
// We also specified this function name in PascalCase in the editor's connection window
private void OnMobDetectorBodyEntered(Node3D body)
{
Die();
}
Versuchen Sie, das Spiel erneut zu starten, indem Sie F5 drücken. Wenn alles richtig eingestellt ist, sollte der Charakter sterben, wenn ein Gegner in den Collider läuft. Beachten Sie, dass ohne einen Player
die folgende Zeile
var player_position = $Player.position
Vector3 playerPosition = GetNode<Player>("Player").Position;
einen Fehler liefert, weil es keinen $Player gibt!
Beachten Sie auch, dass der Gegner, der mit dem Player kollidiert und stirbt, von der Größe und Position der Kollisions-Shapes des Player
und des Mob
abhängt. Es kann sein, daß Sie sie verschieben und in der Größe verändern müssen, um ein gutes Spielgefühl zu erreichen.
Das Spiel beenden¶
Wir können das Signal Hit
von Player
benutzen, um das Spiel zu beenden. Alles, was wir tun müssen, ist, es mit dem Main
-Node zu verbinden und den MobTimer
als Folge zu stoppen.
Öffnen Sie die Datei main.tscn
, markieren Sie den Player
-Node, und verbinden Sie im Node-Dock sein hit
-Signal mit dem Main
-Node.
Holen Sie sich den Timer und stoppen Sie ihn mit der Funktion _on_player_hit()
.
func _on_player_hit():
$MobTimer.stop()
// We also specified this function name in PascalCase in the editor's connection window
private void OnPlayerHit()
{
GetNode<Timer>("MobTimer").Stop();
}
Wenn Sie das Spiel jetzt testen, hören die Monster auf zu spawnen, sobald Sie sterben und die verbleibenden Monster werden den Bildschirmbereich verlassen.
Sie können sich selbst auf die Schulter klopfen: Sie haben einen vollständigen Prototypen für ein 3D Spiel erstellt, auch wenn er noch etwas unfertig ist.
Als nächstes fügen wir einen Punkestand und die Option, das Spiel zu wiederholen hinzu, und Sie werden sehen, wie Sie mit ein paar einfachen Animationen das Spiel lebendiger gestalten können.
Code-Checkpoint¶
Hier sind die kompletten Skripte für die Main
-, Mob
- und Player
-Nodes, als Referenz. Sie können sie verwenden, um Ihren Code zu vergleichen und zu überprüfen.
Wir beginnen mit main.gd
.
extends Node
@export var mob_scene: PackedScene
func _on_mob_timer_timeout():
# Create a new instance of the Mob scene.
var mob = mob_scene.instantiate()
# Choose a random location on the SpawnPath.
# We store the reference to the SpawnLocation node.
var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
# And give it a random offset.
mob_spawn_location.progress_ratio = randf()
var player_position = $Player.position
mob.initialize(mob_spawn_location.position, player_position)
# Spawn the mob by adding it to the Main scene.
add_child(mob)
func _on_player_hit():
$MobTimer.stop()
using Godot;
public partial class Main : Node
{
[Export]
public PackedScene MobScene { get; set; }
private void OnMobTimerTimeout()
{
// Create a new instance of the Mob scene.
Mob mob = MobScene.Instantiate<Mob>();
// Choose a random location on the SpawnPath.
// We store the reference to the SpawnLocation node.
var mobSpawnLocation = GetNode<PathFollow3D>("SpawnPath/SpawnLocation");
// And give it a random offset.
mobSpawnLocation.ProgressRatio = GD.Randf();
Vector3 playerPosition = GetNode<Player>("Player").Position;
mob.Initialize(mobSpawnLocation.Position, playerPosition);
// Spawn the mob by adding it to the Main scene.
AddChild(mob);
}
private void OnPlayerHit()
{
GetNode<Timer>("MobTimer").Stop();
}
}
Als nächstes Mob.gd
.
extends CharacterBody3D
# Minimum speed of the mob in meters per second.
@export var min_speed = 10
# Maximum speed of the mob in meters per second.
@export var max_speed = 18
# Emitted when the player jumped on the mob
signal squashed
func _physics_process(_delta):
move_and_slide()
# This function will be called from the Main scene.
func initialize(start_position, player_position):
# We position the mob by placing it at start_position
# and rotate it towards player_position, so it looks at the player.
look_at_from_position(start_position, player_position, Vector3.UP)
# Rotate this mob randomly within range of -90 and +90 degrees,
# so that it doesn't move directly towards the player.
rotate_y(randf_range(-PI / 4, PI / 4))
# We calculate a random speed (integer)
var random_speed = randi_range(min_speed, max_speed)
# We calculate a forward velocity that represents the speed.
velocity = Vector3.FORWARD * random_speed
# We then rotate the velocity vector based on the mob's Y rotation
# in order to move in the direction the mob is looking.
velocity = velocity.rotated(Vector3.UP, rotation.y)
func _on_visible_on_screen_notifier_3d_screen_exited():
queue_free()
func squash():
squashed.emit()
queue_free() # Destroy this node
using Godot;
public partial class Mob : CharacterBody3D
{
// Emitted when the played jumped on the mob.
[Signal]
public delegate void SquashedEventHandler();
// Minimum speed of the mob in meters per second
[Export]
public int MinSpeed { get; set; } = 10;
// Maximum speed of the mob in meters per second
[Export]
public int MaxSpeed { get; set; } = 18;
public override void _PhysicsProcess(double delta)
{
MoveAndSlide();
}
// This function will be called from the Main scene.
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
{
// We position the mob by placing it at startPosition
// and rotate it towards playerPosition, so it looks at the player.
LookAtFromPosition(startPosition, playerPosition, Vector3.Up);
// Rotate this mob randomly within range of -90 and +90 degrees,
// so that it doesn't move directly towards the player.
RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
// We calculate a random speed (integer)
int randomSpeed = GD.RandRange(MinSpeed, MaxSpeed);
// We calculate a forward velocity that represents the speed.
Velocity = Vector3.Forward * randomSpeed;
// We then rotate the velocity vector based on the mob's Y rotation
// in order to move in the direction the mob is looking.
Velocity = Velocity.Rotated(Vector3.Up, Rotation.Y);
}
public void Squash()
{
EmitSignal(SignalName.Squashed);
QueueFree(); // Destroy this node
}
private void OnVisibilityNotifierScreenExited()
{
QueueFree();
}
}
Schließlich das längste Skript, Player.gd
:
extends CharacterBody3D
signal hit
# How fast the player moves in meters per second
@export var speed = 14
# The downward acceleration while in the air, in meters per second squared.
@export var fall_acceleration = 75
# Vertical impulse applied to the character upon jumping in meters per second.
@export var jump_impulse = 20
# Vertical impulse applied to the character upon bouncing over a mob
# in meters per second.
@export var bounce_impulse = 16
var target_velocity = Vector3.ZERO
func _physics_process(delta):
# We create a local variable to store the input direction
var direction = Vector3.ZERO
# We check for each move input and update the direction accordingly
if Input.is_action_pressed("move_right"):
direction.x = direction.x + 1
if Input.is_action_pressed("move_left"):
direction.x = direction.x - 1
if Input.is_action_pressed("move_back"):
# Notice how we are working with the vector's x and z axes.
# In 3D, the XZ plane is the ground plane.
direction.z = direction.z + 1
if Input.is_action_pressed("move_forward"):
direction.z = direction.z - 1
# Prevent diagonal moving fast af
if direction != Vector3.ZERO:
direction = direction.normalized()
$Pivot.look_at(position + direction, Vector3.UP)
# Ground Velocity
target_velocity.x = direction.x * speed
target_velocity.z = direction.z * speed
# Vertical Velocity
if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity
target_velocity.y = target_velocity.y - (fall_acceleration * delta)
# Jumping.
if is_on_floor() and Input.is_action_just_pressed("jump"):
target_velocity.y = jump_impulse
# Iterate through all collisions that occurred this frame
# in C this would be for(int i = 0; i < collisions.Count; i++)
for index in range(get_slide_collision_count()):
# We get one of the collisions with the player
var collision = get_slide_collision(index)
# If the collision is with ground
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
# Moving the Character
velocity = target_velocity
move_and_slide()
# And this function at the bottom.
func die():
hit.emit()
queue_free()
func _on_mob_detector_body_entered(body):
die()
using Godot;
public partial class Player : CharacterBody3D
{
// Emitted when the player was hit by a mob.
[Signal]
public delegate void HitEventHandler();
// How fast the player moves in meters per second.
[Export]
public int Speed { get; set; } = 14;
// The downward acceleration when in the air, in meters per second squared.
[Export]
public int FallAcceleration { get; set; } = 75;
// Vertical impulse applied to the character upon jumping in meters per second.
[Export]
public int JumpImpulse { get; set; } = 20;
// Vertical impulse applied to the character upon bouncing over a mob in meters per second.
[Export]
public int BounceImpulse { get; set; } = 16;
private Vector3 _targetVelocity = Vector3.Zero;
public override void _PhysicsProcess(double delta)
{
// We create a local variable to store the input direction.
var direction = Vector3.Zero;
// We check for each move input and update the direction accordingly.
if (Input.IsActionPressed("move_right"))
{
direction.X += 1.0f;
}
if (Input.IsActionPressed("move_left"))
{
direction.X -= 1.0f;
}
if (Input.IsActionPressed("move_back"))
{
// Notice how we are working with the vector's X and Z axes.
// In 3D, the XZ plane is the ground plane.
direction.Z += 1.0f;
}
if (Input.IsActionPressed("move_forward"))
{
direction.Z -= 1.0f;
}
// Prevent diagonal moving fast af
if (direction != Vector3.Zero)
{
direction = direction.Normalized();
GetNode<Node3D>("Pivot").LookAt(Position + direction, Vector3.Up);
}
// Ground Velocity
_targetVelocity.X = direction.X * Speed;
_targetVelocity.Z = direction.Z * Speed;
// Vertical Velocity
if (!IsOnFloor()) // If in the air, fall towards the floor. Literally gravity
{
_targetVelocity.Y -= FallAcceleration * (float)delta;
}
// Jumping.
if (IsOnFloor() && Input.IsActionJustPressed("jump"))
{
_targetVelocity.Y = JumpImpulse;
}
// 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.
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;
}
}
}
// Moving the Character
Velocity = _targetVelocity;
MoveAndSlide();
}
private void Die()
{
EmitSignal(SignalName.Hit);
QueueFree();
}
private void OnMobDetectorBodyEntered(Node3D body)
{
Die();
}
}
Wir sehen uns in der nächsten Lektion, um den Punktestand und die Option das Spiel zu wiederholen, hinzuzufügen.