07.killing_player.rst 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. .. _doc_first_3d_game_killing_the_player:
  2. Killing the player
  3. ==================
  4. We can kill enemies by jumping on them, but the player still can't die.
  5. Let's fix this.
  6. We want to detect being hit by an enemy differently from squashing them.
  7. We want the player to die when they're moving on the floor, but not if
  8. they're in the air. We could use vector math to distinguish the two
  9. kinds of collisions. Instead, though, we will use an :ref:`Area3D <class_Area3D>` node, which
  10. works well for hitboxes.
  11. Hitbox with the Area node
  12. -------------------------
  13. Head back to the ``player.tscn`` scene and add a new child node :ref:`Area3D <class_Area3D>`. Name it
  14. ``MobDetector``
  15. Add a :ref:`CollisionShape3D <class_CollisionShape3D>` node as a child of it.
  16. |image0|
  17. In the *Inspector*, assign a cylinder shape to it.
  18. |image1|
  19. Here is a trick you can use to make the collisions only happen when the
  20. player is on the ground or close to it. You can reduce the cylinder's
  21. height and move it up to the top of the character. This way, when the
  22. player jumps, the shape will be too high up for the enemies to collide
  23. with it.
  24. |image2|
  25. You also want the cylinder to be wider than the sphere. This way, the
  26. player gets hit before colliding and being pushed on top of the
  27. monster's collision box.
  28. The wider the cylinder, the more easily the player will get killed.
  29. Next, select the ``MobDetector`` node again, and in the *Inspector*, turn
  30. **off** its *Monitorable* property. This makes it so other physics nodes
  31. cannot detect the area. The complementary *Monitoring* property allows
  32. it to detect collisions. Then, remove the *Collision -> Layer* and set
  33. the mask to the "enemies" layer.
  34. |image3|
  35. When areas detect a collision, they emit signals. We're going to connect
  36. one to the ``Player`` node. Select ``MobDetector`` and go to *Inspector*'s *Node* tab, double-click the
  37. ``body_entered`` signal and connect it to the ``Player``
  38. |image4|
  39. The *MobDetector* will emit ``body_entered`` when a :ref:`CharacterBody3D <class_CharacterBody3D>` or a
  40. :ref:`RigidBody3D <class_RigidBody3D>` node enters it. As it only masks the "enemies" physics
  41. layers, it will only detect the ``Mob`` nodes.
  42. Code-wise, we're going to do two things: emit a signal we'll later use
  43. to end the game and destroy the player. We can wrap these operations in
  44. a ``die()`` function that helps us put a descriptive label on the code.
  45. .. tabs::
  46. .. code-tab:: gdscript GDScript
  47. # Emitted when the player was hit by a mob.
  48. # Put this at the top of the script.
  49. signal hit
  50. # And this function at the bottom.
  51. func die():
  52. hit.emit()
  53. queue_free()
  54. func _on_mob_detector_body_entered(body):
  55. die()
  56. .. code-tab:: csharp
  57. // Don't forget to rebuild the project so the editor knows about the new signal.
  58. // Emitted when the player was hit by a mob.
  59. [Signal]
  60. public delegate void HitEventHandler();
  61. // ...
  62. private void Die()
  63. {
  64. EmitSignal(SignalName.Hit);
  65. QueueFree();
  66. }
  67. // We also specified this function name in PascalCase in the editor's connection window.
  68. private void OnMobDetectorBodyEntered(Node3D body)
  69. {
  70. Die();
  71. }
  72. Ending the game
  73. ---------------
  74. We can use the ``Player``\ 's ``hit`` signal to end the game. All we need
  75. to do is connect it to the ``Main`` node and stop the ``MobTimer`` in
  76. reaction.
  77. Open ``main.tscn``, select the ``Player`` node, and in the *Node* dock,
  78. connect its ``hit`` signal to the ``Main`` node.
  79. |image5|
  80. Get the timer, and stop it, in the ``_on_player_hit()`` function.
  81. .. tabs::
  82. .. code-tab:: gdscript GDScript
  83. func _on_player_hit():
  84. $MobTimer.stop()
  85. .. code-tab:: csharp
  86. // We also specified this function name in PascalCase in the editor's connection window.
  87. private void OnPlayerHit()
  88. {
  89. GetNode<Timer>("MobTimer").Stop();
  90. }
  91. If you try the game now, the monsters will stop spawning when you die,
  92. and the remaining ones will leave the screen.
  93. Notice also that the game no longer crashes or displays an error when the player dies. Because
  94. we are stopping the MobTimer, it no longer triggers the ``_on_mob_timer_timeout()`` function.
  95. Also note that the enemy colliding with the player and dying depends on the size and position of the
  96. ``Player`` and the ``Mob``\ 's collision shapes. You may need to move them
  97. and resize them to achieve a tight game feel.
  98. You can pat yourself on the back: you prototyped a complete 3D game,
  99. even if it's still a bit rough.
  100. From there, we'll add a score, the option to retry the game, and you'll
  101. see how you can make the game feel much more alive with minimalistic
  102. animations.
  103. Code checkpoint
  104. ---------------
  105. Here are the complete scripts for the ``Main``, ``Mob``, and ``Player`` nodes,
  106. for reference. You can use them to compare and check your code.
  107. Starting with ``main.gd``.
  108. .. tabs::
  109. .. code-tab:: gdscript GDScript
  110. extends Node
  111. @export var mob_scene: PackedScene
  112. func _on_mob_timer_timeout():
  113. # Create a new instance of the Mob scene.
  114. var mob = mob_scene.instantiate()
  115. # Choose a random location on the SpawnPath.
  116. # We store the reference to the SpawnLocation node.
  117. var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
  118. # And give it a random offset.
  119. mob_spawn_location.progress_ratio = randf()
  120. var player_position = $Player.position
  121. mob.initialize(mob_spawn_location.position, player_position)
  122. # Spawn the mob by adding it to the Main scene.
  123. add_child(mob)
  124. func _on_player_hit():
  125. $MobTimer.stop()
  126. .. code-tab:: csharp
  127. using Godot;
  128. public partial class Main : Node
  129. {
  130. [Export]
  131. public PackedScene MobScene { get; set; }
  132. private void OnMobTimerTimeout()
  133. {
  134. // Create a new instance of the Mob scene.
  135. Mob mob = MobScene.Instantiate<Mob>();
  136. // Choose a random location on the SpawnPath.
  137. // We store the reference to the SpawnLocation node.
  138. var mobSpawnLocation = GetNode<PathFollow3D>("SpawnPath/SpawnLocation");
  139. // And give it a random offset.
  140. mobSpawnLocation.ProgressRatio = GD.Randf();
  141. Vector3 playerPosition = GetNode<Player>("Player").Position;
  142. mob.Initialize(mobSpawnLocation.Position, playerPosition);
  143. // Spawn the mob by adding it to the Main scene.
  144. AddChild(mob);
  145. }
  146. private void OnPlayerHit()
  147. {
  148. GetNode<Timer>("MobTimer").Stop();
  149. }
  150. }
  151. Next is ``mob.gd``.
  152. .. tabs::
  153. .. code-tab:: gdscript GDScript
  154. extends CharacterBody3D
  155. # Minimum speed of the mob in meters per second.
  156. @export var min_speed = 10
  157. # Maximum speed of the mob in meters per second.
  158. @export var max_speed = 18
  159. # Emitted when the player jumped on the mob
  160. signal squashed
  161. func _physics_process(_delta):
  162. move_and_slide()
  163. # This function will be called from the Main scene.
  164. func initialize(start_position, player_position):
  165. # We position the mob by placing it at start_position
  166. # and rotate it towards player_position, so it looks at the player.
  167. look_at_from_position(start_position, player_position, Vector3.UP)
  168. # Rotate this mob randomly within range of -45 and +45 degrees,
  169. # so that it doesn't move directly towards the player.
  170. rotate_y(randf_range(-PI / 4, PI / 4))
  171. # We calculate a random speed (integer)
  172. var random_speed = randi_range(min_speed, max_speed)
  173. # We calculate a forward velocity that represents the speed.
  174. velocity = Vector3.FORWARD * random_speed
  175. # We then rotate the velocity vector based on the mob's Y rotation
  176. # in order to move in the direction the mob is looking.
  177. velocity = velocity.rotated(Vector3.UP, rotation.y)
  178. func _on_visible_on_screen_notifier_3d_screen_exited():
  179. queue_free()
  180. func squash():
  181. squashed.emit()
  182. queue_free() # Destroy this node
  183. .. code-tab:: csharp
  184. using Godot;
  185. public partial class Mob : CharacterBody3D
  186. {
  187. // Emitted when the played jumped on the mob.
  188. [Signal]
  189. public delegate void SquashedEventHandler();
  190. // Minimum speed of the mob in meters per second
  191. [Export]
  192. public int MinSpeed { get; set; } = 10;
  193. // Maximum speed of the mob in meters per second
  194. [Export]
  195. public int MaxSpeed { get; set; } = 18;
  196. public override void _PhysicsProcess(double delta)
  197. {
  198. MoveAndSlide();
  199. }
  200. // This function will be called from the Main scene.
  201. public void Initialize(Vector3 startPosition, Vector3 playerPosition)
  202. {
  203. // We position the mob by placing it at startPosition
  204. // and rotate it towards playerPosition, so it looks at the player.
  205. LookAtFromPosition(startPosition, playerPosition, Vector3.Up);
  206. // Rotate this mob randomly within range of -45 and +45 degrees,
  207. // so that it doesn't move directly towards the player.
  208. RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
  209. // We calculate a random speed (integer)
  210. int randomSpeed = GD.RandRange(MinSpeed, MaxSpeed);
  211. // We calculate a forward velocity that represents the speed.
  212. Velocity = Vector3.Forward * randomSpeed;
  213. // We then rotate the velocity vector based on the mob's Y rotation
  214. // in order to move in the direction the mob is looking.
  215. Velocity = Velocity.Rotated(Vector3.Up, Rotation.Y);
  216. }
  217. public void Squash()
  218. {
  219. EmitSignal(SignalName.Squashed);
  220. QueueFree(); // Destroy this node
  221. }
  222. private void OnVisibilityNotifierScreenExited()
  223. {
  224. QueueFree();
  225. }
  226. }
  227. Finally, the longest script, ``player.gd``:
  228. .. tabs::
  229. .. code-tab:: gdscript GDScript
  230. extends CharacterBody3D
  231. signal hit
  232. # How fast the player moves in meters per second
  233. @export var speed = 14
  234. # The downward acceleration while in the air, in meters per second squared.
  235. @export var fall_acceleration = 75
  236. # Vertical impulse applied to the character upon jumping in meters per second.
  237. @export var jump_impulse = 20
  238. # Vertical impulse applied to the character upon bouncing over a mob
  239. # in meters per second.
  240. @export var bounce_impulse = 16
  241. var target_velocity = Vector3.ZERO
  242. func _physics_process(delta):
  243. # We create a local variable to store the input direction
  244. var direction = Vector3.ZERO
  245. # We check for each move input and update the direction accordingly
  246. if Input.is_action_pressed("move_right"):
  247. direction.x = direction.x + 1
  248. if Input.is_action_pressed("move_left"):
  249. direction.x = direction.x - 1
  250. if Input.is_action_pressed("move_back"):
  251. # Notice how we are working with the vector's x and z axes.
  252. # In 3D, the XZ plane is the ground plane.
  253. direction.z = direction.z + 1
  254. if Input.is_action_pressed("move_forward"):
  255. direction.z = direction.z - 1
  256. # Prevent diagonal moving fast af
  257. if direction != Vector3.ZERO:
  258. direction = direction.normalized()
  259. $Pivot.look_at(position + direction, Vector3.UP)
  260. # Ground Velocity
  261. target_velocity.x = direction.x * speed
  262. target_velocity.z = direction.z * speed
  263. # Vertical Velocity
  264. if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity
  265. target_velocity.y = target_velocity.y - (fall_acceleration * delta)
  266. # Jumping.
  267. if is_on_floor() and Input.is_action_just_pressed("jump"):
  268. target_velocity.y = jump_impulse
  269. # Iterate through all collisions that occurred this frame
  270. # in C this would be for(int i = 0; i < collisions.Count; i++)
  271. for index in range(get_slide_collision_count()):
  272. # We get one of the collisions with the player
  273. var collision = get_slide_collision(index)
  274. # If the collision is with ground
  275. if collision.get_collider() == null:
  276. continue
  277. # If the collider is with a mob
  278. if collision.get_collider().is_in_group("mob"):
  279. var mob = collision.get_collider()
  280. # we check that we are hitting it from above.
  281. if Vector3.UP.dot(collision.get_normal()) > 0.1:
  282. # If so, we squash it and bounce.
  283. mob.squash()
  284. target_velocity.y = bounce_impulse
  285. # Prevent further duplicate calls.
  286. break
  287. # Moving the Character
  288. velocity = target_velocity
  289. move_and_slide()
  290. # And this function at the bottom.
  291. func die():
  292. hit.emit()
  293. queue_free()
  294. func _on_mob_detector_body_entered(body):
  295. die()
  296. .. code-tab:: csharp
  297. using Godot;
  298. public partial class Player : CharacterBody3D
  299. {
  300. // Emitted when the player was hit by a mob.
  301. [Signal]
  302. public delegate void HitEventHandler();
  303. // How fast the player moves in meters per second.
  304. [Export]
  305. public int Speed { get; set; } = 14;
  306. // The downward acceleration when in the air, in meters per second squared.
  307. [Export]
  308. public int FallAcceleration { get; set; } = 75;
  309. // Vertical impulse applied to the character upon jumping in meters per second.
  310. [Export]
  311. public int JumpImpulse { get; set; } = 20;
  312. // Vertical impulse applied to the character upon bouncing over a mob in meters per second.
  313. [Export]
  314. public int BounceImpulse { get; set; } = 16;
  315. private Vector3 _targetVelocity = Vector3.Zero;
  316. public override void _PhysicsProcess(double delta)
  317. {
  318. // We create a local variable to store the input direction.
  319. var direction = Vector3.Zero;
  320. // We check for each move input and update the direction accordingly.
  321. if (Input.IsActionPressed("move_right"))
  322. {
  323. direction.X += 1.0f;
  324. }
  325. if (Input.IsActionPressed("move_left"))
  326. {
  327. direction.X -= 1.0f;
  328. }
  329. if (Input.IsActionPressed("move_back"))
  330. {
  331. // Notice how we are working with the vector's X and Z axes.
  332. // In 3D, the XZ plane is the ground plane.
  333. direction.Z += 1.0f;
  334. }
  335. if (Input.IsActionPressed("move_forward"))
  336. {
  337. direction.Z -= 1.0f;
  338. }
  339. // Prevent diagonal moving fast af
  340. if (direction != Vector3.Zero)
  341. {
  342. direction = direction.Normalized();
  343. GetNode<Node3D>("Pivot").LookAt(Position + direction, Vector3.Up);
  344. }
  345. // Ground Velocity
  346. _targetVelocity.X = direction.X * Speed;
  347. _targetVelocity.Z = direction.Z * Speed;
  348. // Vertical Velocity
  349. if (!IsOnFloor()) // If in the air, fall towards the floor. Literally gravity
  350. {
  351. _targetVelocity.Y -= FallAcceleration * (float)delta;
  352. }
  353. // Jumping.
  354. if (IsOnFloor() && Input.IsActionJustPressed("jump"))
  355. {
  356. _targetVelocity.Y = JumpImpulse;
  357. }
  358. // Iterate through all collisions that occurred this frame.
  359. for (int index = 0; index < GetSlideCollisionCount(); index++)
  360. {
  361. // We get one of the collisions with the player.
  362. KinematicCollision3D collision = GetSlideCollision(index);
  363. // If the collision is with a mob.
  364. if (collision.GetCollider() is Mob mob)
  365. {
  366. // We check that we are hitting it from above.
  367. if (Vector3.Up.Dot(collision.GetNormal()) > 0.1f)
  368. {
  369. // If so, we squash it and bounce.
  370. mob.Squash();
  371. _targetVelocity.Y = BounceImpulse;
  372. // Prevent further duplicate calls.
  373. break;
  374. }
  375. }
  376. }
  377. // Moving the Character
  378. Velocity = _targetVelocity;
  379. MoveAndSlide();
  380. }
  381. private void Die()
  382. {
  383. EmitSignal(SignalName.Hit);
  384. QueueFree();
  385. }
  386. private void OnMobDetectorBodyEntered(Node3D body)
  387. {
  388. Die();
  389. }
  390. }
  391. See you in the next lesson to add the score and the retry option.
  392. .. |image0| image:: img/07.killing_player/01.adding_area_node.png
  393. .. |image1| image:: img/07.killing_player/02.cylinder_shape.png
  394. .. |image2| image:: img/07.killing_player/03.cylinder_in_editor.png
  395. .. |image3| image:: img/07.killing_player/04.mob_detector_properties.webp
  396. .. |image4| image:: img/07.killing_player/05.body_entered_signal.png
  397. .. |image5| image:: img/07.killing_player/06.player_hit_signal.png