Den Spieler töten¶
Wir können Feinde töten, indem wir auf sie springen, aber der Spieler kann trotzdem nicht sterben. Lass uns das beheben.
Wir wollen, dass ein Treffer von einem Gegner anders erkannt wird, als wenn er zerquetscht 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 Area Node verwenden. Dieser funktioniert gut für Hitboxen .
Hitbox mit dem Area-Node¶
Gehen Sie zurück zur Szene Player und fügen Sie einen neuen Knoten Area hinzu. Nennen Sie ihn MobDetector. Fügen Sie einen CollisionShape Node als untergeordnetes Element hinzu.
In the Inspector, assign a cylinder shape to it.
Here is a trick you can use to make the collisions only happen when the player is on the ground or close to it. You can reduce the cylinder's height and move it up to the top of the character. This way, when the player jumps, the shape will be too high up for the enemies to collide with it.
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.
Wählen Sie als Nächstes den Node MobDetector erneut aus, und deaktivieren Sie im Inspector die Eigenschaft Monitorable. Dadurch können andere Physiknodes den Bereich nicht erkennen. Die ergänzende Eigenschaft Monitoring ermöglicht es ihm, Kollisionen zu erkennen. Entfernen Sie dann die Collision -> Layer und setzen Sie die Maske auf die Ebene "Feinde".
Wenn Bereiche eine Kollision erkennen, senden sie Signale aus. Wir werden eines der Signale mit dem Player Knoten verbinden. Doppelklicken Sie auf der Registerkarte Node auf das Signal body_entered
und verbinden Sie es mit dem Player.
Der MobDetector gibt body_entered
aus, wenn ein KinematicBody oder ein RigidBody Knoten ihn berührt. Da er nur die "Enemy"-Physikebenen maskiert, wird er nur die Mob Nodes erkennen.
Code-mäßig werden wir zwei Dinge tun: Ein Signal senden 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():
emit_signal("hit")
queue_free()
func _on_MobDetector_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 Hit();
// ...
private void Die()
{
EmitSignal(nameof(Hit));
QueueFree();
}
// We also specified this function name in PascalCase in the editor's connection window
public void OnMobDetectorBodyEntered(Node body)
{
Die();
}
Testen sie das Spiel erneut indem sie F5 Drücken. Wenn alles richtig eingereichtet ist, sollte der Charakter sterben sobald ein Gegner gegen ihn läuft.
Beachten Sie jedoch, dass dies vollständig von der Größe und Position der Kollisionsformen des Players und des Mobs abhängt. Möglicherweise müssen Sie sie etwas verschieben oder ihre größe verändern, um ein gutes Spielgefühl zu erreichen.
Das Spiel beenden¶
Wir können das Signal hit
des Players verwenden um das Spiel zu beenden. Alles was wir dazu tun müssen, ist das Signal mit der Main Node zu verbinden und als Reaktion den MobTimer auszuschalten.
Öffnen Sie Main.tscn
, wählen sie das Player Node und verbinden Sie in dem Node Panel das hit
Signal mit der Main Node.
Rufen Sie den Timer in der _on_Player_hit()
Funktion ab und Stoppen Sie ihn.
func _on_Player_hit():
$MobTimer.stop()
// We also specified this function name in PascalCase in the editor's connection window
public 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ächstest fügen wir einen Punkestand, die Option das Spiel zu wiederholen und Sie werden sehen wie Sie mit ein paar minimalen Animationen das Spiel lebendiger gestalten können.
Code-Checkpoint¶
Hier sind die vollständigen Skripte der Main, Mob und Spieler Nodes als Referenz. Sie können sie verwenden um Ihren Code zu vergleichen und zu überprüfen.
Beginnend mit Main.gd
.
extends Node
export(PackedScene) var mob_scene
func _ready():
randomize()
func _on_MobTimer_timeout():
# Create a new instance of the Mob scene.
var mob = mob_scene.instance()
# Choose a random location on the SpawnPath.
var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
# And give it a random offset.
mob_spawn_location.unit_offset = randf()
# Communicate the spawn location and the player's location to the mob.
var player_position = $Player.transform.origin
mob.initialize(mob_spawn_location.translation, player_position)
# Spawn the mob by adding it to the Main scene.
add_child(mob)
func _on_Player_hit():
$MobTimer.stop()
public class Main : Node
{
#pragma warning disable 649
[Export]
public PackedScene MobScene;
#pragma warning restore 649
public override void _Ready()
{
GD.Randomize();
}
public void OnMobTimerTimeout()
{
// Create a new instance of the Mob scene.
var mob = (Mob)MobScene.Instance();
// Choose a random location on the SpawnPath.
// We store the reference to the SpawnLocation node.
var mobSpawnLocation = GetNode<PathFollow>("SpawnPath/SpawnLocation");
// And give it a random offset.
mobSpawnLocation.UnitOffset = GD.Randf();
// Communicate the spawn location and the player's location to the mob.
Vector3 playerPosition = GetNode<Player>("Player").Transform.origin;
mob.Initialize(mobSpawnLocation.Translation, playerPosition);
// Spawn the mob by adding it to the Main scene.
AddChild(mob);
}
public void OnPlayerHit()
{
GetNode<Timer>("MobTimer").Stop();
}
}
Als nächstes Mob.gd
.
extends KinematicBody
# Emitted when the player jumped on the mob.
signal squashed
# 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
var velocity = Vector3.ZERO
func _physics_process(_delta):
move_and_slide(velocity)
func initialize(start_position, player_position):
look_at_from_position(start_position, player_position, Vector3.UP)
rotate_y(rand_range(-PI / 4, PI / 4))
var random_speed = rand_range(min_speed, max_speed)
velocity = Vector3.FORWARD * random_speed
velocity = velocity.rotated(Vector3.UP, rotation.y)
func squash():
emit_signal("squashed")
queue_free()
func _on_VisibilityNotifier_screen_exited():
queue_free()
public class Mob : KinematicBody
{
// Emitted when the played jumped on the mob.
[Signal]
public delegate void Squashed();
// Minimum speed of the mob in meters per second
[Export]
public int MinSpeed = 10;
// Maximum speed of the mob in meters per second
[Export]
public int MaxSpeed = 18;
private Vector3 _velocity = Vector3.Zero;
public override void _PhysicsProcess(float delta)
{
MoveAndSlide(_velocity);
}
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
{
LookAtFromPosition(startPosition, playerPosition, Vector3.Up);
RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
float randomSpeed = (float)GD.RandRange(MinSpeed, MaxSpeed);
_velocity = Vector3.Forward * randomSpeed;
_velocity = _velocity.Rotated(Vector3.Up, Rotation.y);
}
public void Squash()
{
EmitSignal(nameof(Squashed));
QueueFree();
}
public void OnVisibilityNotifierScreenExited()
{
QueueFree();
}
}
Schließlich das längste Skript, Player.gd
.
extends KinematicBody
# Emitted when a mob hit the player.
signal hit
# How fast the player moves in meters per second.
export var speed = 14
# The downward acceleration when 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 velocity = Vector3.ZERO
func _physics_process(delta):
var direction = Vector3.ZERO
if Input.is_action_pressed("move_right"):
direction.x += 1
if Input.is_action_pressed("move_left"):
direction.x -= 1
if Input.is_action_pressed("move_back"):
direction.z += 1
if Input.is_action_pressed("move_forward"):
direction.z -= 1
if direction != Vector3.ZERO:
direction = direction.normalized()
$Pivot.look_at(translation + direction, Vector3.UP)
velocity.x = direction.x * speed
velocity.z = direction.z * speed
# Jumping.
if is_on_floor() and Input.is_action_just_pressed("jump"):
velocity.y += jump_impulse
velocity.y -= fall_acceleration * delta
velocity = move_and_slide(velocity, Vector3.UP)
for index in range(get_slide_count()):
var collision = get_slide_collision(index)
if collision.collider.is_in_group("mob"):
var mob = collision.collider
if Vector3.UP.dot(collision.normal) > 0.1:
mob.squash()
velocity.y = bounce_impulse
func die():
emit_signal("hit")
queue_free()
func _on_MobDetector_body_entered(_body):
die()
public class Player : KinematicBody
{
// Emitted when the player was hit by a mob.
[Signal]
public delegate void Hit();
// How fast the player moves in meters per second.
[Export]
public int Speed = 14;
// The downward acceleration when in the air, in meters per second squared.
[Export]
public int FallAcceleration = 75;
// Vertical impulse applied to the character upon jumping in meters per second.
[Export]
public int JumpImpulse = 20;
// Vertical impulse applied to the character upon bouncing over a mob in meters per second.
[Export]
public int BounceImpulse = 16;
private Vector3 _velocity = Vector3.Zero;
public override void _PhysicsProcess(float delta)
{
var direction = Vector3.Zero;
if (Input.IsActionPressed("move_right"))
{
direction.x += 1f;
}
if (Input.IsActionPressed("move_left"))
{
direction.x -= 1f;
}
if (Input.IsActionPressed("move_back"))
{
direction.z += 1f;
}
if (Input.IsActionPressed("move_forward"))
{
direction.z -= 1f;
}
if (direction != Vector3.Zero)
{
direction = direction.Normalized();
GetNode<Spatial>("Pivot").LookAt(Translation + direction, Vector3.Up);
}
_velocity.x = direction.x * Speed;
_velocity.z = direction.z * Speed;
// Jumping.
if (IsOnFloor() && Input.IsActionJustPressed("jump"))
{
_velocity.y += JumpImpulse;
}
_velocity.y -= FallAcceleration * delta;
_velocity = MoveAndSlide(_velocity, Vector3.Up);
for (int index = 0; index < GetSlideCount(); index++)
{
KinematicCollision collision = GetSlideCollision(index);
if (collision.Collider is Mob mob && mob.IsInGroup("mob"))
{
if (Vector3.Up.Dot(collision.Normal) > 0.1f)
{
mob.Squash();
_velocity.y = BounceImpulse;
}
}
}
}
private void Die()
{
EmitSignal(nameof(Hit));
QueueFree();
}
public void OnMobDetectorBodyEntered(Node body)
{
Die();
}
}
Wir sehen uns in der nächsten Lektion, um den Punktestand und die Option das Spiel zu wiederholen hinzuzufügen.