04.mob_scene.rst 12 KB

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