07.killing_player.rst 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  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 *Area* node, which
  10. works well for hitboxes.
  11. Hitbox with the Area node
  12. -------------------------
  13. Head back to the *Player* scene and add a new *Area* node. Name it
  14. *MobDetector*. Add a *CollisionShape* node as a child of it.
  15. |image0|
  16. In the *Inspector*, assign a cylinder shape to it.
  17. |image1|
  18. Here is a trick you can use to make the collisions only happen when the
  19. player is on the ground or close to it. You can reduce the cylinder's
  20. height and move it up to the top of the character. This way, when the
  21. player jumps, the shape will be too high up for the enemies to collide
  22. with it.
  23. |image2|
  24. You also want the cylinder to be wider than the sphere. This way, the
  25. player gets hit before colliding and being pushed on top of the
  26. monster's collision box.
  27. The wider the cylinder, the more easily the player will get killed.
  28. Next, select the *MobDetector* node again, and in the *Inspector*, turn
  29. off its *Monitorable* property. This makes it so other physics nodes
  30. cannot detect the area. The complementary *Monitoring* property allows
  31. it to detect collisions. Then, remove the *Collision -> Layer* and set
  32. the mask to the "enemies" layer.
  33. |image3|
  34. When areas detect a collision, they emit signals. We're going to connect
  35. one to the *Player* node. In the *Node* tab, double-click the
  36. ``body_entered`` signal and connect it to the *Player*.
  37. |image4|
  38. The *MobDetector* will emit ``body_entered`` when a *KinematicBody* or a
  39. *RigidBody* node enters it. As it only masks the "enemies" physics
  40. layers, it will only detect the *Mob* nodes.
  41. Code-wise, we're going to do two things: emit a signal we'll later use
  42. to end the game and destroy the player. We can wrap these operations in
  43. a ``die()`` function that helps us put a descriptive label on the code.
  44. .. tabs::
  45. .. code-tab:: gdscript GDScript
  46. # Emitted when the player was hit by a mob.
  47. # Put this at the top of the script.
  48. signal hit
  49. # And this function at the bottom.
  50. func die():
  51. emit_signal("hit")
  52. queue_free()
  53. func _on_MobDetector_body_entered(_body):
  54. die()
  55. .. code-tab:: csharp
  56. // Don't forget to rebuild the project so the editor knows about the new signal.
  57. // Emitted when the player was hit by a mob.
  58. [Signal]
  59. public delegate void HitEventHandler();
  60. // ...
  61. private void Die()
  62. {
  63. EmitSignal(nameof(Hit));
  64. QueueFree();
  65. }
  66. // We also specified this function name in PascalCase in the editor's connection window
  67. public void OnMobDetectorBodyEntered(Node body)
  68. {
  69. Die();
  70. }
  71. Try the game again by pressing :kbd:`F5`. If everything is set up correctly,
  72. the character should die when an enemy runs into it.
  73. However, note that this depends entirely on the size and position of the
  74. *Player* and the *Mob*\ 's collision shapes. You may need to move them
  75. and resize them to achieve a tight game feel.
  76. Ending the game
  77. ---------------
  78. We can use the *Player*\ 's ``hit`` signal to end the game. All we need
  79. to do is connect it to the *Main* node and stop the *MobTimer* in
  80. reaction.
  81. Open ``Main.tscn``, select the *Player* node, and in the *Node* dock,
  82. connect its ``hit`` signal to the *Main* node.
  83. |image5|
  84. Get and stop the timer in the ``_on_Player_hit()`` function.
  85. .. tabs::
  86. .. code-tab:: gdscript GDScript
  87. func _on_Player_hit():
  88. $MobTimer.stop()
  89. .. code-tab:: csharp
  90. // We also specified this function name in PascalCase in the editor's connection window
  91. public void OnPlayerHit()
  92. {
  93. GetNode<Timer>("MobTimer").Stop();
  94. }
  95. If you try the game now, the monsters will stop spawning when you die,
  96. and the remaining ones will leave the screen.
  97. You can pat yourself in the back: you prototyped a complete 3D game,
  98. even if it's still a bit rough.
  99. From there, we'll add a score, the option to retry the game, and you'll
  100. see how you can make the game feel much more alive with minimalistic
  101. animations.
  102. Code checkpoint
  103. ---------------
  104. Here are the complete scripts for the *Main*, *Mob*, and *Player* nodes,
  105. for reference. You can use them to compare and check your code.
  106. Starting with ``Main.gd``.
  107. .. tabs::
  108. .. code-tab:: gdscript GDScript
  109. extends Node
  110. export(PackedScene) var mob_scene
  111. func _ready():
  112. randomize()
  113. func _on_MobTimer_timeout():
  114. # Create a new instance of the Mob scene.
  115. var mob = mob_scene.instance()
  116. # Choose a random location on the SpawnPath.
  117. var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
  118. # And give it a random offset.
  119. mob_spawn_location.unit_offset = randf()
  120. # Communicate the spawn location and the player's location to the mob.
  121. var player_position = $Player.transform.origin
  122. mob.initialize(mob_spawn_location.translation, player_position)
  123. # Spawn the mob by adding it to the Main scene.
  124. add_child(mob)
  125. func _on_Player_hit():
  126. $MobTimer.stop()
  127. .. code-tab:: csharp
  128. public class Main : Node
  129. {
  130. #pragma warning disable 649
  131. [Export]
  132. public PackedScene MobScene;
  133. #pragma warning restore 649
  134. public override void _Ready()
  135. {
  136. GD.Randomize();
  137. }
  138. public void OnMobTimerTimeout()
  139. {
  140. // Create a new instance of the Mob scene.
  141. var mob = (Mob)MobScene.Instance();
  142. // Choose a random location on the SpawnPath.
  143. // We store the reference to the SpawnLocation node.
  144. var mobSpawnLocation = GetNode<PathFollow>("SpawnPath/SpawnLocation");
  145. // And give it a random offset.
  146. mobSpawnLocation.UnitOffset = GD.Randf();
  147. // Communicate the spawn location and the player's location to the mob.
  148. Vector3 playerPosition = GetNode<Player>("Player").Transform.origin;
  149. mob.Initialize(mobSpawnLocation.Translation, playerPosition);
  150. // Spawn the mob by adding it to the Main scene.
  151. AddChild(mob);
  152. }
  153. public void OnPlayerHit()
  154. {
  155. GetNode<Timer>("MobTimer").Stop();
  156. }
  157. }
  158. Next is ``Mob.gd``.
  159. .. tabs::
  160. .. code-tab:: gdscript GDScript
  161. extends KinematicBody
  162. # Emitted when the player jumped on the mob.
  163. signal squashed
  164. # Minimum speed of the mob in meters per second.
  165. @export var min_speed = 10
  166. # Maximum speed of the mob in meters per second.
  167. @export var max_speed = 18
  168. var velocity = Vector3.ZERO
  169. func _physics_process(_delta):
  170. move_and_slide(velocity)
  171. func initialize(start_position, player_position):
  172. look_at_from_position(start_position, player_position, Vector3.UP)
  173. rotate_y(rand_range(-PI / 4, PI / 4))
  174. var random_speed = rand_range(min_speed, max_speed)
  175. velocity = Vector3.FORWARD * random_speed
  176. velocity = velocity.rotated(Vector3.UP, rotation.y)
  177. func squash():
  178. emit_signal("squashed")
  179. queue_free()
  180. func _on_VisibilityNotifier_screen_exited():
  181. queue_free()
  182. .. code-tab:: csharp
  183. public class Mob : KinematicBody
  184. {
  185. // Emitted when the played jumped on the mob.
  186. [Signal]
  187. public delegate void SquashedEventHandler();
  188. // Minimum speed of the mob in meters per second
  189. [Export]
  190. public int MinSpeed = 10;
  191. // Maximum speed of the mob in meters per second
  192. [Export]
  193. public int MaxSpeed = 18;
  194. private Vector3 _velocity = Vector3.Zero;
  195. public override void _PhysicsProcess(float delta)
  196. {
  197. MoveAndSlide(_velocity);
  198. }
  199. public void Initialize(Vector3 startPosition, Vector3 playerPosition)
  200. {
  201. LookAtFromPosition(startPosition, playerPosition, Vector3.Up);
  202. RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
  203. float randomSpeed = (float)GD.RandRange(MinSpeed, MaxSpeed);
  204. _velocity = Vector3.Forward * randomSpeed;
  205. _velocity = _velocity.Rotated(Vector3.Up, Rotation.y);
  206. }
  207. public void Squash()
  208. {
  209. EmitSignal(nameof(Squashed));
  210. QueueFree();
  211. }
  212. public void OnVisibilityNotifierScreenExited()
  213. {
  214. QueueFree();
  215. }
  216. }
  217. Finally, the longest script, ``Player.gd``.
  218. .. tabs::
  219. .. code-tab:: gdscript GDScript
  220. extends KinematicBody
  221. # Emitted when a mob hit the player.
  222. signal hit
  223. # How fast the player moves in meters per second.
  224. @export var speed = 14
  225. # The downward acceleration when in the air, in meters per second squared.
  226. @export var fall_acceleration = 75
  227. # Vertical impulse applied to the character upon jumping in meters per second.
  228. @export var jump_impulse = 20
  229. # Vertical impulse applied to the character upon bouncing over a mob in meters per second.
  230. @export var bounce_impulse = 16
  231. var velocity = Vector3.ZERO
  232. func _physics_process(delta):
  233. var direction = Vector3.ZERO
  234. if Input.is_action_pressed("move_right"):
  235. direction.x += 1
  236. if Input.is_action_pressed("move_left"):
  237. direction.x -= 1
  238. if Input.is_action_pressed("move_back"):
  239. direction.z += 1
  240. if Input.is_action_pressed("move_forward"):
  241. direction.z -= 1
  242. if direction != Vector3.ZERO:
  243. direction = direction.normalized()
  244. $Pivot.look_at(translation + direction, Vector3.UP)
  245. velocity.x = direction.x * speed
  246. velocity.z = direction.z * speed
  247. # Jumping.
  248. if is_on_floor() and Input.is_action_just_pressed("jump"):
  249. velocity.y += jump_impulse
  250. velocity.y -= fall_acceleration * delta
  251. velocity = move_and_slide(velocity, Vector3.UP)
  252. for index in range(get_slide_count()):
  253. var collision = get_slide_collision(index)
  254. if collision.collider.is_in_group("mob"):
  255. var mob = collision.collider
  256. if Vector3.UP.dot(collision.normal) > 0.1:
  257. mob.squash()
  258. velocity.y = bounce_impulse
  259. func die():
  260. emit_signal("hit")
  261. queue_free()
  262. func _on_MobDetector_body_entered(_body):
  263. die()
  264. .. code-tab:: csharp
  265. public class Player : KinematicBody
  266. {
  267. // Emitted when the player was hit by a mob.
  268. [Signal]
  269. public delegate void HitEventHandler();
  270. // How fast the player moves in meters per second.
  271. [Export]
  272. public int Speed = 14;
  273. // The downward acceleration when in the air, in meters per second squared.
  274. [Export]
  275. public int FallAcceleration = 75;
  276. // Vertical impulse applied to the character upon jumping in meters per second.
  277. [Export]
  278. public int JumpImpulse = 20;
  279. // Vertical impulse applied to the character upon bouncing over a mob in meters per second.
  280. [Export]
  281. public int BounceImpulse = 16;
  282. private Vector3 _velocity = Vector3.Zero;
  283. public override void _PhysicsProcess(float delta)
  284. {
  285. var direction = Vector3.Zero;
  286. if (Input.IsActionPressed("move_right"))
  287. {
  288. direction.x += 1f;
  289. }
  290. if (Input.IsActionPressed("move_left"))
  291. {
  292. direction.x -= 1f;
  293. }
  294. if (Input.IsActionPressed("move_back"))
  295. {
  296. direction.z += 1f;
  297. }
  298. if (Input.IsActionPressed("move_forward"))
  299. {
  300. direction.z -= 1f;
  301. }
  302. if (direction != Vector3.Zero)
  303. {
  304. direction = direction.Normalized();
  305. GetNode<Spatial>("Pivot").LookAt(Translation + direction, Vector3.Up);
  306. }
  307. _velocity.x = direction.x * Speed;
  308. _velocity.z = direction.z * Speed;
  309. // Jumping.
  310. if (IsOnFloor() && Input.IsActionJustPressed("jump"))
  311. {
  312. _velocity.y += JumpImpulse;
  313. }
  314. _velocity.y -= FallAcceleration * delta;
  315. _velocity = MoveAndSlide(_velocity, Vector3.Up);
  316. for (int index = 0; index < GetSlideCount(); index++)
  317. {
  318. KinematicCollision collision = GetSlideCollision(index);
  319. if (collision.Collider is Mob mob && mob.IsInGroup("mob"))
  320. {
  321. if (Vector3.Up.Dot(collision.Normal) > 0.1f)
  322. {
  323. mob.Squash();
  324. _velocity.y = BounceImpulse;
  325. }
  326. }
  327. }
  328. }
  329. private void Die()
  330. {
  331. EmitSignal(nameof(Hit));
  332. QueueFree();
  333. }
  334. public void OnMobDetectorBodyEntered(Node body)
  335. {
  336. Die();
  337. }
  338. }
  339. See you in the next lesson to add the score and the retry option.
  340. .. |image0| image:: img/07.killing_player/01.adding_area_node.png
  341. .. |image1| image:: img/07.killing_player/02.cylinder_shape.png
  342. .. |image2| image:: img/07.killing_player/03.cylinder_in_editor.png
  343. .. |image3| image:: img/07.killing_player/04.mob_detector_properties.png
  344. .. |image4| image:: img/07.killing_player/05.body_entered_signal.png
  345. .. |image5| image:: img/07.killing_player/06.player_hit_signal.png