06.jump_and_squash.rst 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. .. _doc_first_3d_game_jumping_and_squashing_monsters:
  2. Jumping and squashing monsters
  3. ==============================
  4. In this part, we'll add the ability to jump and squash the monsters. In the next
  5. lesson, we'll make the player die when a monster hits them on the ground.
  6. First, we have to change a few settings related to physics interactions. Enter
  7. the world of :ref:`physics layers
  8. <doc_physics_introduction_collision_layers_and_masks>`.
  9. Controlling physics interactions
  10. --------------------------------
  11. Physics bodies have access to two complementary properties: layers and masks.
  12. Layers define on which physics layer(s) an object is.
  13. Masks control the layers that a body will listen to and detect. This affects
  14. collision detection. When you want two bodies to interact, you need at least one
  15. to have a mask corresponding to the other.
  16. If that's confusing, don't worry, we'll see three examples in a second.
  17. The important point is that you can use layers and masks to filter physics
  18. interactions, control performance, and remove the need for extra conditions in
  19. your code.
  20. By default, all physics bodies and areas are set to both layer and mask ``1``.
  21. This means they all collide with each other.
  22. Physics layers are represented by numbers, but we can give them names to keep
  23. track of what's what.
  24. Setting layer names
  25. ~~~~~~~~~~~~~~~~~~~
  26. Let's give our physics layers a name. Go to *Project -> Project Settings*.
  27. |image0|
  28. In the left menu, navigate down to *Layer Names -> 3D Physics*. You can see a
  29. list of layers with a field next to each of them on the right. You can set their
  30. names there. Name the first three layers *player*, *enemies*, and *world*,
  31. respectively.
  32. |image1|
  33. Now, we can assign them to our physics nodes.
  34. Assigning layers and masks
  35. ~~~~~~~~~~~~~~~~~~~~~~~~~~
  36. In the *Main* scene, select the ``Ground`` node. In the *Inspector*, expand the
  37. *Collision* section. There, you can see the node's layers and masks as a grid of
  38. buttons.
  39. |image2|
  40. The ground is part of the world, so we want it to be part of the third layer.
  41. Click the lit button to toggle **off** the first *Layer* and toggle **on** the third
  42. one. Then, toggle **off** the *Mask* by clicking on it.
  43. |image3|
  44. As mentioned before, the *Mask* property allows a node to listen to interaction
  45. with other physics objects, but we don't need it to have collisions. ``Ground`` doesn't need to listen to anything; it's just there to prevent
  46. creatures from falling.
  47. Note that you can click the "..." button on the right side of the properties to
  48. see a list of named checkboxes.
  49. |image4|
  50. Next up are the ``Player`` and the ``Mob``. Open ``player.tscn`` by double-clicking
  51. the file in the *FileSystem* dock.
  52. Select the *Player* node and set its *Collision -> Mask* to both "enemies" and
  53. "world". You can leave the default *Layer* property as it is, because the first layer is the
  54. "player" layer.
  55. |image5|
  56. Then, open the *Mob* scene by double-clicking on ``mob.tscn`` and select the
  57. ``Mob`` node.
  58. Set its *Collision -> Layer* to "enemies" and unset its *Collision -> Mask*,
  59. leaving the mask empty.
  60. |image6|
  61. These settings mean the monsters will move through one another. If you want the
  62. monsters to collide with and slide against each other, turn **on** the "enemies"
  63. mask.
  64. .. note::
  65. The mobs don't need to mask the "world" layer because they only move
  66. on the XZ plane. We don't apply any gravity to them by design.
  67. Jumping
  68. -------
  69. The jumping mechanic itself requires only two lines of code. Open the *Player*
  70. script. We need a value to control the jump's strength and update
  71. ``_physics_process()`` to code the jump.
  72. After the line that defines ``fall_acceleration``, at the top of the script, add
  73. the ``jump_impulse``.
  74. .. tabs::
  75. .. code-tab:: gdscript GDScript
  76. #...
  77. # Vertical impulse applied to the character upon jumping in meters per second.
  78. @export var jump_impulse = 20
  79. .. code-tab:: csharp
  80. // Don't forget to rebuild the project so the editor knows about the new export variable.
  81. // ...
  82. // Vertical impulse applied to the character upon jumping in meters per second.
  83. [Export]
  84. public int JumpImpulse { get; set; } = 20;
  85. Inside ``_physics_process()``, add the following code before the ``move_and_slide()`` codeblock.
  86. .. tabs::
  87. .. code-tab:: gdscript GDScript
  88. func _physics_process(delta):
  89. #...
  90. # Jumping.
  91. if is_on_floor() and Input.is_action_just_pressed("jump"):
  92. target_velocity.y = jump_impulse
  93. #...
  94. .. code-tab:: csharp
  95. public override void _PhysicsProcess(double delta)
  96. {
  97. // ...
  98. // Jumping.
  99. if (IsOnFloor() && Input.IsActionJustPressed("jump"))
  100. {
  101. _targetVelocity.Y = JumpImpulse;
  102. }
  103. // ...
  104. }
  105. That's all you need to jump!
  106. The ``is_on_floor()`` method is a tool from the ``CharacterBody3D`` class. It
  107. returns ``true`` if the body collided with the floor in this frame. That's why
  108. we apply gravity to the *Player*: so we collide with the floor instead of
  109. floating over it like the monsters.
  110. If the character is on the floor and the player presses "jump", we instantly
  111. give them a lot of vertical speed. In games, you really want controls to be
  112. responsive and giving instant speed boosts like these, while unrealistic, feels
  113. great.
  114. Notice that the Y axis is positive upwards. That's unlike 2D, where the Y axis
  115. is positive downwards.
  116. Squashing monsters
  117. ------------------
  118. Let's add the squash mechanic next. We're going to make the character bounce
  119. over monsters and kill them at the same time.
  120. We need to detect collisions with a monster and to differentiate them from
  121. collisions with the floor. To do so, we can use Godot's :ref:`group
  122. <doc_groups>` tagging feature.
  123. Open the scene ``mob.tscn`` again and select the *Mob* node. Go to the *Node*
  124. dock on the right to see a list of signals. The *Node* dock has two tabs:
  125. *Signals*, which you've already used, and *Groups*, which allows you to assign
  126. tags to nodes.
  127. Click on it to reveal a field where you can write a tag name. Enter "mob" in the
  128. field and click the *Add* button.
  129. |image7|
  130. An icon appears in the *Scene* dock to indicate the node is part of at least one
  131. group.
  132. |image8|
  133. We can now use the group from the code to distinguish collisions with monsters
  134. from collisions with the floor.
  135. Coding the squash mechanic
  136. ~~~~~~~~~~~~~~~~~~~~~~~~~~
  137. Head back to the *Player* script to code the squash and bounce.
  138. At the top of the script, we need another property, ``bounce_impulse``. When
  139. squashing an enemy, we don't necessarily want the character to go as high up as
  140. when jumping.
  141. .. tabs::
  142. .. code-tab:: gdscript GDScript
  143. # Vertical impulse applied to the character upon bouncing over a mob in
  144. # meters per second.
  145. @export var bounce_impulse = 16
  146. .. code-tab:: csharp
  147. // Don't forget to rebuild the project so the editor knows about the new export variable.
  148. // Vertical impulse applied to the character upon bouncing over a mob in meters per second.
  149. [Export]
  150. public int BounceImpulse { get; set; } = 16;
  151. Then, after the **Jumping** codeblock we added above in ``_physics_process()``, add the following loop. With
  152. ``move_and_slide()``, Godot makes the body move sometimes multiple times in a
  153. row to smooth out the character's motion. So we have to loop over all collisions
  154. that may have happened.
  155. In every iteration of the loop, we check if we landed on a mob. If so, we kill
  156. it and bounce.
  157. With this code, if no collisions occurred on a given frame, the loop won't run.
  158. .. tabs::
  159. .. code-tab:: gdscript GDScript
  160. func _physics_process(delta):
  161. #...
  162. # Iterate through all collisions that occurred this frame
  163. for index in range(get_slide_collision_count()):
  164. # We get one of the collisions with the player
  165. var collision = get_slide_collision(index)
  166. # If there are duplicate collisions with a mob in a single frame
  167. # the mob will be deleted after the first collision, and a second call to
  168. # get_collider will return null, leading to a null pointer when calling
  169. # collision.get_collider().is_in_group("mob").
  170. # This block of code prevents processing duplicate collisions.
  171. if collision.get_collider() == null:
  172. continue
  173. # If the collider is with a mob
  174. if collision.get_collider().is_in_group("mob"):
  175. var mob = collision.get_collider()
  176. # we check that we are hitting it from above.
  177. if Vector3.UP.dot(collision.get_normal()) > 0.1:
  178. # If so, we squash it and bounce.
  179. mob.squash()
  180. target_velocity.y = bounce_impulse
  181. # Prevent further duplicate calls.
  182. break
  183. .. code-tab:: csharp
  184. public override void _PhysicsProcess(double delta)
  185. {
  186. // ...
  187. // Iterate through all collisions that occurred this frame.
  188. for (int index = 0; index < GetSlideCollisionCount(); index++)
  189. {
  190. // We get one of the collisions with the player.
  191. KinematicCollision3D collision = GetSlideCollision(index);
  192. // If the collision is with a mob.
  193. // With C# we leverage typing and pattern-matching
  194. // instead of checking for the group we created.
  195. if (collision.GetCollider() is Mob mob)
  196. {
  197. // We check that we are hitting it from above.
  198. if (Vector3.Up.Dot(collision.GetNormal()) > 0.1f)
  199. {
  200. // If so, we squash it and bounce.
  201. mob.Squash();
  202. _targetVelocity.Y = BounceImpulse;
  203. // Prevent further duplicate calls.
  204. break;
  205. }
  206. }
  207. }
  208. }
  209. That's a lot of new functions. Here's some more information about them.
  210. The functions ``get_slide_collision_count()`` and ``get_slide_collision()`` both come from
  211. the :ref:`CharacterBody3D <class_CharacterBody3D>` class and are related to
  212. ``move_and_slide()``.
  213. ``get_slide_collision()`` returns a
  214. :ref:`KinematicCollision3D<class_KinematicCollision3D>` object that holds
  215. information about where and how the collision occurred. For example, we use its
  216. ``get_collider`` property to check if we collided with a "mob" by calling
  217. ``is_in_group()`` on it: ``collision.get_collider().is_in_group("mob")``.
  218. .. note::
  219. The method ``is_in_group()`` is available on every :ref:`Node<class_Node>`.
  220. To check that we are landing on the monster, we use the vector dot product:
  221. ``Vector3.UP.dot(collision.get_normal()) > 0.1``. The collision normal is a 3D vector
  222. that is perpendicular to the plane where the collision occurred. The dot product
  223. allows us to compare it to the up direction.
  224. With dot products, when the result is greater than ``0``, the two vectors are at
  225. an angle of fewer than 90 degrees. A value higher than ``0.1`` tells us that we
  226. are roughly above the monster.
  227. After handling the squash and bounce logic, we terminate the loop early via the ``break`` statement
  228. to prevent further duplicate calls to ``mob.squash()``, which may otherwise result in unintended bugs
  229. such as counting the score multiple times for one kill.
  230. We are calling one undefined function, ``mob.squash()``, so we have to add it to
  231. the Mob class.
  232. Open the script ``mob.gd`` by double-clicking on it in the *FileSystem* dock. At
  233. the top of the script, we want to define a new signal named ``squashed``. And at
  234. the bottom, you can add the squash function, where we emit the signal and
  235. destroy the mob.
  236. .. tabs::
  237. .. code-tab:: gdscript GDScript
  238. # Emitted when the player jumped on the mob.
  239. signal squashed
  240. # ...
  241. func squash():
  242. squashed.emit()
  243. queue_free()
  244. .. code-tab:: csharp
  245. // Don't forget to rebuild the project so the editor knows about the new signal.
  246. // Emitted when the player jumped on the mob.
  247. [Signal]
  248. public delegate void SquashedEventHandler();
  249. // ...
  250. public void Squash()
  251. {
  252. EmitSignal(SignalName.Squashed);
  253. QueueFree();
  254. }
  255. .. note::
  256. When using C#, Godot will create the appropriate events automatically for all Signals ending with `EventHandler`, see :ref:`C# Signals <doc_c_sharp_signals>`.
  257. We will use the signal to add points to the score in the next lesson.
  258. With that, you should be able to kill monsters by jumping on them. You can press
  259. :kbd:`F5` to try the game and set ``main.tscn`` as your project's main scene.
  260. However, the player won't die yet. We'll work on that in the next part.
  261. .. |image0| image:: img/06.jump_and_squash/02.project_settings.png
  262. .. |image1| image:: img/06.jump_and_squash/03.physics_layers.webp
  263. .. |image2| image:: img/06.jump_and_squash/04.default_physics_properties.webp
  264. .. |image3| image:: img/06.jump_and_squash/05.toggle_layer_and_mask.webp
  265. .. |image4| image:: img/06.jump_and_squash/06.named_checkboxes.png
  266. .. |image5| image:: img/06.jump_and_squash/07.player_physics_mask.webp
  267. .. |image6| image:: img/06.jump_and_squash/08.mob_physics_mask.webp
  268. .. |image7| image:: img/06.jump_and_squash/09.groups_tab.png
  269. .. |image8| image:: img/06.jump_and_squash/10.group_scene_icon.png