杀死玩家

我们可以通过跳到敌人身上来杀死他们,但玩家仍然不能死亡。让我们来解决这个问题。

我们希望检测到被敌人击中与压扁敌人时的不同。我们希望玩家在地板上移动时死亡,但如果他们在空中,则不会死亡。我们可以使用向量数学来区分这两种碰撞。但是,我们将使用 Area3D 节点,该节点适用于命中框。

使用 Area 节点制作攻击框

回到 player.tscn 场景,添加一个新的 Area3D 子节点。把它命名为 MobDetector(小怪检测器)。添加一个 CollisionShape3D 节点作为它的一个子节点。

image0

检查器中,给它指定一个圆柱体形状。

image1

这里有一个技巧,你可以用它来使碰撞只发生在玩家在地面上或靠近地面时。你可以降低圆柱体的高度并将其向上移动到角色的顶部。这样,当玩家跳跃时,形状会太高,敌人无法与之碰撞。

image2

你还希望圆柱体比球体更宽。这样一来,玩家在碰撞之前就会被击中,并被推到怪物的碰撞盒之上。

圆柱体越宽,玩家就越容易被杀死。

接下来,再次选择 MobDetector 节点,并在检查器中, 关闭Monitorable 属性。这使得其他物理节点无法检测到这个区域。补充的 Monitoring 属性允许它检测碰撞。然后,清除 Collision -> Layer,并将掩码设置为“enemies”层。

image3

当区域检测到碰撞时,它们会发出信号。我们要将一个信号连接到 Player 节点。在节点选项卡中,双击 body_entered 信号并将其连接到 Player

image4

当一个 CharacterBody3DRigidBody3D 节点进入它时,MobDetector 将发出 body_entered 信号。由于它只遮罩了“enemies”物理层,它将只检测 Mob 节点。

从代码上看,我们要做两件事:发出一个信号,我们以后会用来结束游戏,并销毁玩家。我们可以用 die() 函数来包装这些操作,帮助我们给代码贴上描述性标签。

GDScriptC#

  1. # Emitted when the player was hit by a mob.
  2. # Put this at the top of the script.
  3. signal hit
  4. # And this function at the bottom.
  5. func die():
  6. hit.emit()
  7. queue_free()
  8. func _on_mob_detector_body_entered(body):
  9. die()
  1. // Don't forget to rebuild the project so the editor knows about the new signal.
  2. // Emitted when the player was hit by a mob.
  3. [Signal]
  4. public delegate void HitEventHandler();
  5. // ...
  6. private void Die()
  7. {
  8. EmitSignal(SignalName.Hit);
  9. QueueFree();
  10. }
  11. // We also specified this function name in PascalCase in the editor's connection window.
  12. private void OnMobDetectorBodyEntered(Node3D body)
  13. {
  14. Die();
  15. }

结束游戏

我们可以利用 Playerhit 信号来结束游戏。我们所要做的就是将它连接到 Main 节点上,在处理时停止 MobTimer

打开 main.tscn 场景,选中 Player 节点,然后在节点面板中把 hit 信号连接到 Main 节点。

image5

_on_player_hit() 函数中获取并停止计时器。

GDScriptC#

  1. func _on_player_hit():
  2. $MobTimer.stop()
  1. // We also specified this function name in PascalCase in the editor's connection window.
  2. private void OnPlayerHit()
  3. {
  4. GetNode<Timer>("MobTimer").Stop();
  5. }

如果你现在试玩游戏,你死亡后就会停止刷怪,现有的怪物会离开屏幕。

同时注意到在玩家死亡时,游戏不再崩溃或报错。 这是因为我们停止了 Mobtimer,也就不再触发 _on_mob_timer_timeout() 函数了.

另外请注意,敌人与玩家碰撞并死亡取决于 PlayerMob 的碰撞形状的大小和位置。你可能需要移动它们,调整它们的大小,以达到紧凑的游戏感觉。

你可以鼓励鼓励自己了:你做出了完整 3D 游戏的原型,虽说还有点粗糙。

在此基础上,我们将会添加计分、重启游戏的选项,你还会看到如何使用简单的动画让游戏变得更加活灵活现。

代码检查点

这些是 MainMobPlayer 节点的完整脚本,仅供参考。你可以把它们和你的代码进行对比检查。

首先是 main.gd

GDScriptC#

  1. extends Node
  2. @export var mob_scene: PackedScene
  3. func _on_mob_timer_timeout():
  4. # Create a new instance of the Mob scene.
  5. var mob = mob_scene.instantiate()
  6. # Choose a random location on the SpawnPath.
  7. # We store the reference to the SpawnLocation node.
  8. var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
  9. # And give it a random offset.
  10. mob_spawn_location.progress_ratio = randf()
  11. var player_position = $Player.position
  12. mob.initialize(mob_spawn_location.position, player_position)
  13. # Spawn the mob by adding it to the Main scene.
  14. add_child(mob)
  15. func _on_player_hit():
  16. $MobTimer.stop()
  1. using Godot;
  2. public partial class Main : Node
  3. {
  4. [Export]
  5. public PackedScene MobScene { get; set; }
  6. private void OnMobTimerTimeout()
  7. {
  8. // Create a new instance of the Mob scene.
  9. Mob mob = MobScene.Instantiate<Mob>();
  10. // Choose a random location on the SpawnPath.
  11. // We store the reference to the SpawnLocation node.
  12. var mobSpawnLocation = GetNode<PathFollow3D>("SpawnPath/SpawnLocation");
  13. // And give it a random offset.
  14. mobSpawnLocation.ProgressRatio = GD.Randf();
  15. Vector3 playerPosition = GetNode<Player>("Player").Position;
  16. mob.Initialize(mobSpawnLocation.Position, playerPosition);
  17. // Spawn the mob by adding it to the Main scene.
  18. AddChild(mob);
  19. }
  20. private void OnPlayerHit()
  21. {
  22. GetNode<Timer>("MobTimer").Stop();
  23. }
  24. }

Next is mob.gd.

GDScriptC#

  1. extends CharacterBody3D
  2. # Minimum speed of the mob in meters per second.
  3. @export var min_speed = 10
  4. # Maximum speed of the mob in meters per second.
  5. @export var max_speed = 18
  6. # Emitted when the player jumped on the mob
  7. signal squashed
  8. func _physics_process(_delta):
  9. move_and_slide()
  10. # This function will be called from the Main scene.
  11. func initialize(start_position, player_position):
  12. # We position the mob by placing it at start_position
  13. # and rotate it towards player_position, so it looks at the player.
  14. look_at_from_position(start_position, player_position, Vector3.UP)
  15. # Rotate this mob randomly within range of -45 and +45 degrees,
  16. # so that it doesn't move directly towards the player.
  17. rotate_y(randf_range(-PI / 4, PI / 4))
  18. # We calculate a random speed (integer)
  19. var random_speed = randi_range(min_speed, max_speed)
  20. # We calculate a forward velocity that represents the speed.
  21. velocity = Vector3.FORWARD * random_speed
  22. # We then rotate the velocity vector based on the mob's Y rotation
  23. # in order to move in the direction the mob is looking.
  24. velocity = velocity.rotated(Vector3.UP, rotation.y)
  25. func _on_visible_on_screen_notifier_3d_screen_exited():
  26. queue_free()
  27. func squash():
  28. squashed.emit()
  29. queue_free() # Destroy this node
  1. using Godot;
  2. public partial class Mob : CharacterBody3D
  3. {
  4. // Emitted when the played jumped on the mob.
  5. [Signal]
  6. public delegate void SquashedEventHandler();
  7. // Minimum speed of the mob in meters per second
  8. [Export]
  9. public int MinSpeed { get; set; } = 10;
  10. // Maximum speed of the mob in meters per second
  11. [Export]
  12. public int MaxSpeed { get; set; } = 18;
  13. public override void _PhysicsProcess(double delta)
  14. {
  15. MoveAndSlide();
  16. }
  17. // This function will be called from the Main scene.
  18. public void Initialize(Vector3 startPosition, Vector3 playerPosition)
  19. {
  20. // We position the mob by placing it at startPosition
  21. // and rotate it towards playerPosition, so it looks at the player.
  22. LookAtFromPosition(startPosition, playerPosition, Vector3.Up);
  23. // Rotate this mob randomly within range of -45 and +45 degrees,
  24. // so that it doesn't move directly towards the player.
  25. RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
  26. // We calculate a random speed (integer)
  27. int randomSpeed = GD.RandRange(MinSpeed, MaxSpeed);
  28. // We calculate a forward velocity that represents the speed.
  29. Velocity = Vector3.Forward * randomSpeed;
  30. // We then rotate the velocity vector based on the mob's Y rotation
  31. // in order to move in the direction the mob is looking.
  32. Velocity = Velocity.Rotated(Vector3.Up, Rotation.Y);
  33. }
  34. public void Squash()
  35. {
  36. EmitSignal(SignalName.Squashed);
  37. QueueFree(); // Destroy this node
  38. }
  39. private void OnVisibilityNotifierScreenExited()
  40. {
  41. QueueFree();
  42. }
  43. }

Finally, the longest script, player.gd:

GDScriptC#

  1. extends CharacterBody3D
  2. signal hit
  3. # How fast the player moves in meters per second
  4. @export var speed = 14
  5. # The downward acceleration while in the air, in meters per second squared.
  6. @export var fall_acceleration = 75
  7. # Vertical impulse applied to the character upon jumping in meters per second.
  8. @export var jump_impulse = 20
  9. # Vertical impulse applied to the character upon bouncing over a mob
  10. # in meters per second.
  11. @export var bounce_impulse = 16
  12. var target_velocity = Vector3.ZERO
  13. func _physics_process(delta):
  14. # We create a local variable to store the input direction
  15. var direction = Vector3.ZERO
  16. # We check for each move input and update the direction accordingly
  17. if Input.is_action_pressed("move_right"):
  18. direction.x = direction.x + 1
  19. if Input.is_action_pressed("move_left"):
  20. direction.x = direction.x - 1
  21. if Input.is_action_pressed("move_back"):
  22. # Notice how we are working with the vector's x and z axes.
  23. # In 3D, the XZ plane is the ground plane.
  24. direction.z = direction.z + 1
  25. if Input.is_action_pressed("move_forward"):
  26. direction.z = direction.z - 1
  27. # Prevent diagonal moving fast af
  28. if direction != Vector3.ZERO:
  29. direction = direction.normalized()
  30. $Pivot.look_at(position + direction, Vector3.UP)
  31. # Ground Velocity
  32. target_velocity.x = direction.x * speed
  33. target_velocity.z = direction.z * speed
  34. # Vertical Velocity
  35. if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity
  36. target_velocity.y = target_velocity.y - (fall_acceleration * delta)
  37. # Jumping.
  38. if is_on_floor() and Input.is_action_just_pressed("jump"):
  39. target_velocity.y = jump_impulse
  40. # Iterate through all collisions that occurred this frame
  41. # in C this would be for(int i = 0; i < collisions.Count; i++)
  42. for index in range(get_slide_collision_count()):
  43. # We get one of the collisions with the player
  44. var collision = get_slide_collision(index)
  45. # If the collision is with ground
  46. if collision.get_collider() == null:
  47. continue
  48. # If the collider is with a mob
  49. if collision.get_collider().is_in_group("mob"):
  50. var mob = collision.get_collider()
  51. # we check that we are hitting it from above.
  52. if Vector3.UP.dot(collision.get_normal()) > 0.1:
  53. # If so, we squash it and bounce.
  54. mob.squash()
  55. target_velocity.y = bounce_impulse
  56. # Prevent further duplicate calls.
  57. break
  58. # Moving the Character
  59. velocity = target_velocity
  60. move_and_slide()
  61. # And this function at the bottom.
  62. func die():
  63. hit.emit()
  64. queue_free()
  65. func _on_mob_detector_body_entered(body):
  66. die()
  1. using Godot;
  2. public partial class Player : CharacterBody3D
  3. {
  4. // Emitted when the player was hit by a mob.
  5. [Signal]
  6. public delegate void HitEventHandler();
  7. // How fast the player moves in meters per second.
  8. [Export]
  9. public int Speed { get; set; } = 14;
  10. // The downward acceleration when in the air, in meters per second squared.
  11. [Export]
  12. public int FallAcceleration { get; set; } = 75;
  13. // Vertical impulse applied to the character upon jumping in meters per second.
  14. [Export]
  15. public int JumpImpulse { get; set; } = 20;
  16. // Vertical impulse applied to the character upon bouncing over a mob in meters per second.
  17. [Export]
  18. public int BounceImpulse { get; set; } = 16;
  19. private Vector3 _targetVelocity = Vector3.Zero;
  20. public override void _PhysicsProcess(double delta)
  21. {
  22. // We create a local variable to store the input direction.
  23. var direction = Vector3.Zero;
  24. // We check for each move input and update the direction accordingly.
  25. if (Input.IsActionPressed("move_right"))
  26. {
  27. direction.X += 1.0f;
  28. }
  29. if (Input.IsActionPressed("move_left"))
  30. {
  31. direction.X -= 1.0f;
  32. }
  33. if (Input.IsActionPressed("move_back"))
  34. {
  35. // Notice how we are working with the vector's X and Z axes.
  36. // In 3D, the XZ plane is the ground plane.
  37. direction.Z += 1.0f;
  38. }
  39. if (Input.IsActionPressed("move_forward"))
  40. {
  41. direction.Z -= 1.0f;
  42. }
  43. // Prevent diagonal moving fast af
  44. if (direction != Vector3.Zero)
  45. {
  46. direction = direction.Normalized();
  47. GetNode<Node3D>("Pivot").LookAt(Position + direction, Vector3.Up);
  48. }
  49. // Ground Velocity
  50. _targetVelocity.X = direction.X * Speed;
  51. _targetVelocity.Z = direction.Z * Speed;
  52. // Vertical Velocity
  53. if (!IsOnFloor()) // If in the air, fall towards the floor. Literally gravity
  54. {
  55. _targetVelocity.Y -= FallAcceleration * (float)delta;
  56. }
  57. // Jumping.
  58. if (IsOnFloor() && Input.IsActionJustPressed("jump"))
  59. {
  60. _targetVelocity.Y = JumpImpulse;
  61. }
  62. // Iterate through all collisions that occurred this frame.
  63. for (int index = 0; index < GetSlideCollisionCount(); index++)
  64. {
  65. // We get one of the collisions with the player.
  66. KinematicCollision3D collision = GetSlideCollision(index);
  67. // If the collision is with a mob.
  68. if (collision.GetCollider() is Mob mob)
  69. {
  70. // We check that we are hitting it from above.
  71. if (Vector3.Up.Dot(collision.GetNormal()) > 0.1f)
  72. {
  73. // If so, we squash it and bounce.
  74. mob.Squash();
  75. _targetVelocity.Y = BounceImpulse;
  76. // Prevent further duplicate calls.
  77. break;
  78. }
  79. }
  80. }
  81. // Moving the Character
  82. Velocity = _targetVelocity;
  83. MoveAndSlide();
  84. }
  85. private void Die()
  86. {
  87. EmitSignal(SignalName.Hit);
  88. QueueFree();
  89. }
  90. private void OnMobDetectorBodyEntered(Node3D body)
  91. {
  92. Die();
  93. }
  94. }

在下一节课中我们会添加计分和重试选项,再见。