殺死玩家
我們可以通過跳到敵人身上來殺死他們,但玩家仍然不能死亡。讓我們來解決這個問題。
我們希望偵測到被敵人擊中與壓扁敵人時的不同。我們希望玩家在地板上移動時死亡,但如果他們在空中,則不會死亡。我們可以使用向量數學來區分這兩種碰撞。但是,我們將使用 Area3D 節點,該節點適用於命中框。
使用 Area 節點製作攻擊框
回到 player.tscn 場景,新增一個子節點 Area3D,命名為 MobDetector。接著再新增一個 CollisionShape3D 作為它的子節點。

在*屬性檢視器*中,給它指定一個圓柱體形狀。

這裡有一個技巧,你可以用它來使碰撞只發生在玩家在地面上或靠近地面時。您可以降低圓柱體的高度並將其向上移動到角色的頂部。這樣,當玩家跳躍時,形狀會太高,敵人無法與之碰撞。

你還希望圓柱體比球體更寬。這樣一來,玩家在碰撞之前就會被擊中,並被推到怪物的碰撞盒之上。
圓柱體越寬,玩家就越容易被殺死。
接下來,再次選擇 MobDetector 節點,並在*屬性檢視器*中, 關閉 其 Monitorable 屬性。這使得其他物理節點無法偵測到這個區域。補充的 Monitoring 屬性允許它偵測碰撞。然後,清除 Collision -> Layer,並將遮罩設定為“enemies”層。

當區域偵測到碰撞時,它們會發出訊號。我們要將一個訊號連接到 Player 節點。在*節點*分頁中,按兩下 body_entered 訊號並將其連接到 Player\\

當一個 CharacterBody3D 或 RigidBody3D 節點進入它時,MobDetector 將發出 body_entered 訊號。由於它只遮罩了“enemies”實體層,它將只偵測 Mob 節點。
從程式碼上看,我們要做兩件事:發出一個訊號,我們以後會用來結束遊戲,並銷毀玩家。我們可以用 die() 函式來包裝這些操作,幫助我們給程式碼貼上描述性標籤。
# 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();
}
執行遊戲
我們可以利用 ''Player'' 的 ''hit'' 信號來結束遊戲。我們所要做的就是將它連接到 ''Main'' 節點上,在處理時停止 ''MobTimer''。
打開 main.tscn 場景,選中 Player 節點,然後在*節點*面板中把 hit 訊號連接到 Main 節點。

在 _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();
}
如果你現在試玩遊戲,你死亡後就會停止刷怪,現有的怪物會離開螢幕。
另請注意,當玩家死亡時,遊戲不再崩潰或顯示錯誤訊息了。 這是因為我們停止了 MobTimer,它不再觸發 _on_mob_timer_timeout() 函式。
另外請注意,敵人與玩家碰撞並死亡取決於 ''Player'' 和 ''Mob'' 的碰撞形狀的大小和位置。你可能需要移動它們,調整它們的大小,以達到緊湊的遊戲感覺。
你可以拍拍自己的背了:你已經完成了整個 3D 遊戲的原型,即使它還有些粗糙。
在此基礎上,我們將會新增計分、重啟遊戲的選項,你還會看到如何使用簡單的動畫讓遊戲變得更加活靈活現。
設定/移除中斷點
這些是 Main、Mob、Player 節點的完整腳本,僅供參考。你可以把它們和你的程式碼進行對比檢查。
首先是 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();
}
}
接下來是 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 -45 and +45 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 -45 and +45 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();
}
}
最後,最長的腳本,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()
# Setting the basis property will affect the rotation of the node.
$Pivot.basis = Basis.looking_at(direction)
# 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();
// Setting the basis property will affect the rotation of the node.
GetNode<Node3D>("Pivot").Basis = Basis.LookingAt(direction);
}
// 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();
}
}
在下一節課中我們會新增計分和重試選項,再見。