04.mob_scene.rst 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. :article_outdated: True
  2. .. _doc_first_3d_game_designing_the_mob_scene:
  3. Designing the mob scene
  4. =======================
  5. In this part, you're going to code the monsters, which we'll call mobs. In the
  6. next lesson, we'll spawn them randomly around the playable area.
  7. Let's design the monsters themselves in a new scene. The node structure is going
  8. to be similar to the ``player.tscn`` scene.
  9. Create a scene with, once again, a :ref:`CharacterBody3D <class_CharacterBody3D>` node as its root. Name it
  10. ``Mob``. Add a child node :ref:`Node3D <class_Node3D>`, name it ``Pivot``. And drag and drop
  11. the file ``mob.glb`` from the *FileSystem* dock onto the ``Pivot`` to add the
  12. monster's 3D model to the scene.
  13. .. image:: img/04.mob_scene/drag_drop_mob.webp
  14. You can rename the newly created ``mob`` node
  15. into ``Character``.
  16. |image0|
  17. We need a collision shape for our body to work. Right-click on the ``Mob`` node,
  18. the scene's root, and click *Add Child Node*.
  19. |image1|
  20. Add a :ref:`CollisionShape3D <class_CollisionShape3D>`.
  21. |image2|
  22. In the *Inspector*, assign a *BoxShape3D* to the *Shape* property.
  23. .. image:: img/01.game_setup/08.create_box_shape3D.jpg
  24. We should change its size to fit the 3D model better. You can do so
  25. interactively by clicking and dragging on the orange dots.
  26. The box should touch the floor and be a little thinner than the model. Physics
  27. engines work in such a way that if the player's sphere touches even the box's
  28. corner, a collision will occur. If the box is a little too big compared to the
  29. 3D model, you may die at a distance from the monster, and the game will feel
  30. unfair to the players.
  31. |image4|
  32. Notice that my box is taller than the monster. It is okay in this game because
  33. we're looking at the scene from above and using a fixed perspective. Collision
  34. shapes don't have to match the model exactly. It's the way the game feels when
  35. you test it that should dictate their form and size.
  36. Removing monsters off-screen
  37. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  38. We're going to spawn monsters at regular time intervals in the game level. If
  39. we're not careful, their count could increase to infinity, and we don't want
  40. that. Each mob instance has both a memory and a processing cost, and we don't
  41. want to pay for it when the mob is outside the screen.
  42. Once a monster leaves the screen, we don't need it anymore, so we should delete it.
  43. Godot has a node that detects when objects leave the screen,
  44. :ref:`VisibleOnScreenNotifier3D <class_VisibleOnScreenNotifier3D>`, and we're going to use it to destroy our mobs.
  45. .. note::
  46. When you keep instancing an object, there's a technique you can
  47. use to avoid the cost of creating and destroying instances all the time
  48. called pooling. It consists of pre-creating an array of objects and reusing
  49. them over and over.
  50. When working with GDScript, you don't need to worry about this. The main
  51. reason to use pools is to avoid freezes with garbage-collected languages
  52. like C# or Lua. GDScript uses a different technique to manage memory,
  53. reference counting, which doesn't have that caveat. You can learn more
  54. about that here: :ref:`doc_gdscript_basics_memory_management`.
  55. Select the ``Mob`` node and add a child node :ref:`VisibleOnScreenNotifier3D <class_VisibleOnScreenNotifier3D>`. Another
  56. box, pink this time, appears. When this box completely leaves the screen, the
  57. node will emit a signal.
  58. |image5|
  59. Resize it using the orange dots until it covers the entire 3D model.
  60. |image6|
  61. Coding the mob's movement
  62. -------------------------
  63. Let's implement the monster's motion. We're going to do this in two steps.
  64. First, we'll write a script on the ``Mob`` that defines a function to initialize
  65. the monster. We'll then code the randomized spawn mechanism in the ``main.tscn`` scene
  66. and call the function from there.
  67. Attach a script to the ``Mob``.
  68. |image7|
  69. Here's the movement code to start with. We define two properties, ``min_speed``
  70. and ``max_speed``, to define a random speed range, which we will later use to define ``CharacterBody3D.velocity``.
  71. .. tabs::
  72. .. code-tab:: gdscript GDScript
  73. extends CharacterBody3D
  74. # Minimum speed of the mob in meters per second.
  75. @export var min_speed = 10
  76. # Maximum speed of the mob in meters per second.
  77. @export var max_speed = 18
  78. func _physics_process(_delta):
  79. move_and_slide()
  80. .. code-tab:: csharp
  81. using Godot;
  82. public partial class Mob : CharacterBody3D
  83. {
  84. // Don't forget to rebuild the project so the editor knows about the new export variable.
  85. // Minimum speed of the mob in meters per second
  86. [Export]
  87. public int MinSpeed { get; set; } = 10;
  88. // Maximum speed of the mob in meters per second
  89. [Export]
  90. public int MaxSpeed { get; set; } = 18;
  91. public override void _PhysicsProcess(double delta)
  92. {
  93. MoveAndSlide();
  94. }
  95. }
  96. Similarly to the player, we move the mob every frame by calling the function
  97. ``CharacterBody3D.move_and_slide()``. This time, we don't update
  98. the ``velocity`` every frame; we want the monster to move at a constant speed
  99. and leave the screen, even if it were to hit an obstacle.
  100. We need to define another function to calculate the ``CharacterBody3D.velocity``. This
  101. function will turn the monster towards the player and randomize both its angle
  102. of motion and its velocity.
  103. The function will take a ``start_position``,the mob's spawn position, and the
  104. ``player_position`` as its arguments.
  105. We position the mob at ``start_position`` and turn it towards the player using
  106. the ``look_at_from_position()`` method, and randomize the angle by rotating a
  107. random amount around the Y axis. Below, ``randf_range()`` outputs a random value
  108. between ``-PI / 4`` radians and ``PI / 4`` radians.
  109. .. tabs::
  110. .. code-tab:: gdscript GDScript
  111. # This function will be called from the Main scene.
  112. func initialize(start_position, player_position):
  113. # We position the mob by placing it at start_position
  114. # and rotate it towards player_position, so it looks at the player.
  115. look_at_from_position(start_position, player_position, Vector3.UP)
  116. # Rotate this mob randomly within range of -90 and +90 degrees,
  117. # so that it doesn't move directly towards the player.
  118. rotate_y(randf_range(-PI / 4, PI / 4))
  119. .. code-tab:: csharp
  120. // This function will be called from the Main scene.
  121. public void Initialize(Vector3 startPosition, Vector3 playerPosition)
  122. {
  123. // We position the mob by placing it at startPosition
  124. // and rotate it towards playerPosition, so it looks at the player.
  125. LookAtFromPosition(startPosition, playerPosition, Vector3.Up);
  126. // Rotate this mob randomly within range of -90 and +90 degrees,
  127. // so that it doesn't move directly towards the player.
  128. RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
  129. }
  130. We got a random position, now we need a ``random_speed``. ``randi_range()`` will be useful as it gives random int values, and we will use ``min_speed`` and ``max_speed``.
  131. ``random_speed`` is just an integer, and we just use it to multiply our ``CharacterBody3D.velocity``. After ``random_speed`` is applied, we rotate ``CharacterBody3D.velocity`` Vector3 towards the player.
  132. .. tabs::
  133. .. code-tab:: gdscript GDScript
  134. func initialize(start_position, player_position):
  135. # ...
  136. # We calculate a random speed (integer)
  137. var random_speed = randi_range(min_speed, max_speed)
  138. # We calculate a forward velocity that represents the speed.
  139. velocity = Vector3.FORWARD * random_speed
  140. # We then rotate the velocity vector based on the mob's Y rotation
  141. # in order to move in the direction the mob is looking.
  142. velocity = velocity.rotated(Vector3.UP, rotation.y)
  143. .. code-tab:: csharp
  144. public void Initialize(Vector3 startPosition, Vector3 playerPosition)
  145. {
  146. // ...
  147. // We calculate a random speed (integer).
  148. int randomSpeed = GD.RandRange(MinSpeed, MaxSpeed);
  149. // We calculate a forward velocity that represents the speed.
  150. Velocity = Vector3.Forward * randomSpeed;
  151. // We then rotate the velocity vector based on the mob's Y rotation
  152. // in order to move in the direction the mob is looking.
  153. Velocity = Velocity.Rotated(Vector3.Up, Rotation.Y);
  154. }
  155. Leaving the screen
  156. ------------------
  157. We still have to destroy the mobs when they leave the screen. To do so, we'll
  158. connect our :ref:`VisibleOnScreenNotifier3D <class_VisibleOnScreenNotifier3D>` node's ``screen_exited`` signal to the ``Mob``.
  159. Head back to the 3D viewport by clicking on the *3D* label at the top of the
  160. editor. You can also press :kbd:`Ctrl + F2` (:kbd:`Alt + 2` on macOS).
  161. |image8|
  162. Select the :ref:`VisibleOnScreenNotifier3D <class_VisibleOnScreenNotifier3D>` node and on the right side of the interface,
  163. navigate to the *Node* dock. Double-click the ``screen_exited()`` signal.
  164. |image9|
  165. Connect the signal to the ``Mob``
  166. |image10|
  167. This will take you back to the script editor and add a new function for you,
  168. ``_on_visible_on_screen_notifier_3d_screen_exited()``. From it, call the ``queue_free()``
  169. method. This function destroy the instance it's called on.
  170. .. tabs::
  171. .. code-tab:: gdscript GDScript
  172. func _on_visible_on_screen_notifier_3d_screen_exited():
  173. queue_free()
  174. .. code-tab:: csharp
  175. // We also specified this function name in PascalCase in the editor's connection window
  176. private void OnVisibilityNotifierScreenExited()
  177. {
  178. QueueFree();
  179. }
  180. Our monster is ready to enter the game! In the next part, you will spawn
  181. monsters in the game level.
  182. Here is the complete ``Mob.gd`` script for reference.
  183. .. tabs::
  184. .. code-tab:: gdscript GDScript
  185. extends CharacterBody3D
  186. # Minimum speed of the mob in meters per second.
  187. @export var min_speed = 10
  188. # Maximum speed of the mob in meters per second.
  189. @export var max_speed = 18
  190. func _physics_process(_delta):
  191. move_and_slide()
  192. # This function will be called from the Main scene.
  193. func initialize(start_position, player_position):
  194. # We position the mob by placing it at start_position
  195. # and rotate it towards player_position, so it looks at the player.
  196. look_at_from_position(start_position, player_position, Vector3.UP)
  197. # Rotate this mob randomly within range of -90 and +90 degrees,
  198. # so that it doesn't move directly towards the player.
  199. rotate_y(randf_range(-PI / 4, PI / 4))
  200. # We calculate a random speed (integer)
  201. var random_speed = randi_range(min_speed, max_speed)
  202. # We calculate a forward velocity that represents the speed.
  203. velocity = Vector3.FORWARD * random_speed
  204. # We then rotate the velocity vector based on the mob's Y rotation
  205. # in order to move in the direction the mob is looking.
  206. velocity = velocity.rotated(Vector3.UP, rotation.y)
  207. func _on_visible_on_screen_notifier_3d_screen_exited():
  208. queue_free()
  209. .. code-tab:: csharp
  210. using Godot;
  211. public partial class Mob : CharacterBody3D
  212. {
  213. // Minimum speed of the mob in meters per second.
  214. [Export]
  215. public int MinSpeed { get; set; } = 10;
  216. // Maximum speed of the mob in meters per second.
  217. [Export]
  218. public int MaxSpeed { get; set; } = 18;
  219. public override void _PhysicsProcess(double delta)
  220. {
  221. MoveAndSlide();
  222. }
  223. // This function will be called from the Main scene.
  224. public void Initialize(Vector3 startPosition, Vector3 playerPosition)
  225. {
  226. // We position the mob by placing it at startPosition
  227. // and rotate it towards playerPosition, so it looks at the player.
  228. LookAtFromPosition(startPosition, playerPosition, Vector3.Up);
  229. // Rotate this mob randomly within range of -90 and +90 degrees,
  230. // so that it doesn't move directly towards the player.
  231. RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
  232. // We calculate a random speed (integer).
  233. int randomSpeed = GD.RandRange(MinSpeed, MaxSpeed);
  234. // We calculate a forward velocity that represents the speed.
  235. Velocity = Vector3.Forward * randomSpeed;
  236. // We then rotate the velocity vector based on the mob's Y rotation
  237. // in order to move in the direction the mob is looking.
  238. Velocity = Velocity.Rotated(Vector3.Up, Rotation.Y);
  239. }
  240. // We also specified this function name in PascalCase in the editor's connection window
  241. private void OnVisibilityNotifierScreenExited()
  242. {
  243. QueueFree();
  244. }
  245. }
  246. .. |image0| image:: img/04.mob_scene/01.initial_three_nodes.png
  247. .. |image1| image:: img/04.mob_scene/02.add_child_node.png
  248. .. |image2| image:: img/04.mob_scene/03.scene_with_collision_shape.png
  249. .. |image4| image:: img/04.mob_scene/05.box_final_size.png
  250. .. |image5| image:: img/04.mob_scene/06.visibility_notifier.png
  251. .. |image6| image:: img/04.mob_scene/07.visibility_notifier_bbox_resized.png
  252. .. |image7| image:: img/04.mob_scene/08.mob_attach_script.png
  253. .. |image8| image:: img/04.mob_scene/09.switch_to_3d_workspace.png
  254. .. |image9| image:: img/04.mob_scene/10.node_dock.webp
  255. .. |image10| image:: img/04.mob_scene/11.connect_signal.webp