03.coding_the_player.rst 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. .. _doc_your_first_2d_game_coding_the_player:
  2. Coding the player
  3. =================
  4. In this lesson, we'll add player movement, animation, and set it up to detect
  5. collisions.
  6. To do so, we need to add some functionality that we can't get from a built-in
  7. node, so we'll add a script. Click the ``Player`` node and click the "Attach
  8. Script" button:
  9. .. image:: img/add_script_button.png
  10. In the script settings window, you can leave the default settings alone. Just
  11. click "Create":
  12. .. note:: If you're creating a C# script or other languages, select the language
  13. from the `language` drop down menu before hitting create.
  14. .. image:: img/attach_node_window.png
  15. .. note:: If this is your first time encountering GDScript, please read
  16. :ref:`doc_scripting` before continuing.
  17. Start by declaring the member variables this object will need:
  18. .. tabs::
  19. .. code-tab:: gdscript GDScript
  20. extends Area2D
  21. export var speed = 400 # How fast the player will move (pixels/sec).
  22. var screen_size # Size of the game window.
  23. .. code-tab:: csharp
  24. using Godot;
  25. using System;
  26. public class Player : Area2D
  27. {
  28. [Export]
  29. public int Speed = 400; // How fast the player will move (pixels/sec).
  30. public Vector2 ScreenSize; // Size of the game window.
  31. }
  32. Using the ``export`` keyword on the first variable ``speed`` allows us to set
  33. its value in the Inspector. This can be handy for values that you want to be
  34. able to adjust just like a node's built-in properties. Click on the ``Player``
  35. node and you'll see the property now appears in the "Script Variables" section
  36. of the Inspector. Remember, if you change the value here, it will override the
  37. value written in the script.
  38. .. warning:: If you're using C#, you need to (re)build the project assemblies
  39. whenever you want to see new export variables or signals. This
  40. build can be manually triggered by clicking the word "Mono" at the
  41. bottom of the editor window to reveal the Mono Panel, then clicking
  42. the "Build Project" button.
  43. .. image:: img/export_variable.png
  44. The ``_ready()`` function is called when a node enters the scene tree, which is
  45. a good time to find the size of the game window:
  46. .. tabs::
  47. .. code-tab:: gdscript GDScript
  48. func _ready():
  49. screen_size = get_viewport_rect().size
  50. .. code-tab:: csharp
  51. public override void _Ready()
  52. {
  53. ScreenSize = GetViewportRect().Size;
  54. }
  55. Now we can use the ``_process()`` function to define what the player will do.
  56. ``_process()`` is called every frame, so we'll use it to update elements of our
  57. game, which we expect will change often. For the player, we need to do the
  58. following:
  59. - Check for input.
  60. - Move in the given direction.
  61. - Play the appropriate animation.
  62. First, we need to check for input - is the player pressing a key? For this game,
  63. we have 4 direction inputs to check. Input actions are defined in the Project
  64. Settings under "Input Map". Here, you can define custom events and assign
  65. different keys, mouse events, or other inputs to them. For this game, we will
  66. just use the default events called "ui_right" etc that are assigned to the arrow
  67. keys on the keyboard.
  68. You can detect whether a key is pressed using ``Input.is_action_pressed()``,
  69. which returns ``true`` if it's pressed or ``false`` if it isn't.
  70. .. tabs::
  71. .. code-tab:: gdscript GDScript
  72. func _process(delta):
  73. var velocity = Vector2.ZERO # The player's movement vector.
  74. if Input.is_action_pressed("ui_right"):
  75. velocity.x += 1
  76. if Input.is_action_pressed("ui_left"):
  77. velocity.x -= 1
  78. if Input.is_action_pressed("ui_down"):
  79. velocity.y += 1
  80. if Input.is_action_pressed("ui_up"):
  81. velocity.y -= 1
  82. if velocity.length() > 0:
  83. velocity = velocity.normalized() * speed
  84. $AnimatedSprite.play()
  85. else:
  86. $AnimatedSprite.stop()
  87. .. code-tab:: csharp
  88. public override void _Process(float delta)
  89. {
  90. var velocity = Vector2.Zero; // The player's movement vector.
  91. if (Input.IsActionPressed("ui_right"))
  92. {
  93. velocity.x += 1;
  94. }
  95. if (Input.IsActionPressed("ui_left"))
  96. {
  97. velocity.x -= 1;
  98. }
  99. if (Input.IsActionPressed("ui_down"))
  100. {
  101. velocity.y += 1;
  102. }
  103. if (Input.IsActionPressed("ui_up"))
  104. {
  105. velocity.y -= 1;
  106. }
  107. var animatedSprite = GetNode<AnimatedSprite>("AnimatedSprite");
  108. if (velocity.Length() > 0)
  109. {
  110. velocity = velocity.Normalized() * Speed;
  111. animatedSprite.Play();
  112. }
  113. else
  114. {
  115. animatedSprite.Stop();
  116. }
  117. }
  118. We start by setting the ``velocity`` to ``(0, 0)`` - by default, the player
  119. should not be moving. Then we check each input and add/subtract from the
  120. ``velocity`` to obtain a total direction. For example, if you hold ``right`` and
  121. ``down`` at the same time, the resulting ``velocity`` vector will be ``(1, 1)``.
  122. In this case, since we're adding a horizontal and a vertical movement, the
  123. player would move *faster* diagonally than if it just moved horizontally.
  124. We can prevent that if we *normalize* the velocity, which means we set its
  125. *length* to ``1``, then multiply by the desired speed. This means no more fast
  126. diagonal movement.
  127. .. tip:: If you've never used vector math before, or need a refresher, you can
  128. see an explanation of vector usage in Godot at :ref:`doc_vector_math`.
  129. It's good to know but won't be necessary for the rest of this tutorial.
  130. We also check whether the player is moving so we can call ``play()`` or
  131. ``stop()`` on the AnimatedSprite.
  132. ``$`` is shorthand for ``get_node()``. So in the code above,
  133. ``$AnimatedSprite.play()`` is the same as
  134. ``get_node("AnimatedSprite").play()``.
  135. .. tip:: In GDScript, ``$`` returns the node at the relative path from the
  136. current node, or returns ``null`` if the node is not found. Since
  137. AnimatedSprite is a child of the current node, we can use
  138. ``$AnimatedSprite``.
  139. Now that we have a movement direction, we can update the player's position. We
  140. can also use ``clamp()`` to prevent it from leaving the screen. *Clamping* a
  141. value means restricting it to a given range. Add the following to the bottom of
  142. the ``_process`` function (make sure it's not indented under the `else`):
  143. .. tabs::
  144. .. code-tab:: gdscript GDScript
  145. position += velocity * delta
  146. position.x = clamp(position.x, 0, screen_size.x)
  147. position.y = clamp(position.y, 0, screen_size.y)
  148. .. code-tab:: csharp
  149. Position += velocity * delta;
  150. Position = new Vector2(
  151. x: Mathf.Clamp(Position.x, 0, ScreenSize.x),
  152. y: Mathf.Clamp(Position.y, 0, ScreenSize.y)
  153. );
  154. .. tip:: The `delta` parameter in the `_process()` function refers to the *frame
  155. length* - the amount of time that the previous frame took to complete.
  156. Using this value ensures that your movement will remain consistent even
  157. if the frame rate changes.
  158. Click "Play Scene" (:kbd:`F6`, :kbd:`Cmd + R` on macOS) and confirm you can move
  159. the player around the screen in all directions.
  160. .. warning:: If you get an error in the "Debugger" panel that says
  161. ``Attempt to call function 'play' in base 'null instance' on a null
  162. instance``
  163. this likely means you spelled the name of the AnimatedSprite node
  164. wrong. Node names are case-sensitive and ``$NodeName`` must match
  165. the name you see in the scene tree.
  166. Choosing animations
  167. ~~~~~~~~~~~~~~~~~~~
  168. Now that the player can move, we need to change which animation the
  169. AnimatedSprite is playing based on its direction. We have the "walk" animation,
  170. which shows the player walking to the right. This animation should be flipped
  171. horizontally using the ``flip_h`` property for left movement. We also have the
  172. "up" animation, which should be flipped vertically with ``flip_v`` for downward
  173. movement. Let's place this code at the end of the ``_process()`` function:
  174. .. tabs::
  175. .. code-tab:: gdscript GDScript
  176. if velocity.x != 0:
  177. $AnimatedSprite.animation = "walk"
  178. $AnimatedSprite.flip_v = false
  179. # See the note below about boolean assignment.
  180. $AnimatedSprite.flip_h = velocity.x < 0
  181. elif velocity.y != 0:
  182. $AnimatedSprite.animation = "up"
  183. $AnimatedSprite.flip_v = velocity.y > 0
  184. .. code-tab:: csharp
  185. if (velocity.x != 0)
  186. {
  187. animatedSprite.Animation = "walk";
  188. animatedSprite.FlipV = false;
  189. // See the note below about boolean assignment.
  190. animatedSprite.FlipH = velocity.x < 0;
  191. }
  192. else if (velocity.y != 0)
  193. {
  194. animatedSprite.Animation = "up";
  195. animatedSprite.FlipV = velocity.y > 0;
  196. }
  197. .. Note:: The boolean assignments in the code above are a common shorthand for
  198. programmers. Since we're doing a comparison test (boolean) and also
  199. *assigning* a boolean value, we can do both at the same time. Consider
  200. this code versus the one-line boolean assignment above:
  201. .. tabs::
  202. .. code-tab :: gdscript GDScript
  203. if velocity.x < 0:
  204. $AnimatedSprite.flip_h = true
  205. else:
  206. $AnimatedSprite.flip_h = false
  207. .. code-tab:: csharp
  208. if (velocity.x < 0)
  209. {
  210. animatedSprite.FlipH = true;
  211. }
  212. else
  213. {
  214. animatedSprite.FlipH = false;
  215. }
  216. Play the scene again and check that the animations are correct in each of the
  217. directions.
  218. .. tip:: A common mistake here is to type the names of the animations wrong. The
  219. animation names in the SpriteFrames panel must match what you type in
  220. the code. If you named the animation ``"Walk"``, you must also use a
  221. capital "W" in the code.
  222. When you're sure the movement is working correctly, add this line to
  223. ``_ready()``, so the player will be hidden when the game starts:
  224. .. tabs::
  225. .. code-tab:: gdscript GDScript
  226. hide()
  227. .. code-tab:: csharp
  228. Hide();
  229. Preparing for collisions
  230. ~~~~~~~~~~~~~~~~~~~~~~~~
  231. We want ``Player`` to detect when it's hit by an enemy, but we haven't made any
  232. enemies yet! That's OK, because we're going to use Godot's *signal*
  233. functionality to make it work.
  234. Add the following at the top of the script, after ``extends Area2D``:
  235. .. tabs::
  236. .. code-tab:: gdscript GDScript
  237. signal hit
  238. .. code-tab:: csharp
  239. // Don't forget to rebuild the project so the editor knows about the new signal.
  240. [Signal]
  241. public delegate void Hit();
  242. This defines a custom signal called "hit" that we will have our player emit
  243. (send out) when it collides with an enemy. We will use ``Area2D`` to detect the
  244. collision. Select the ``Player`` node and click the "Node" tab next to the
  245. Inspector tab to see the list of signals the player can emit:
  246. .. image:: img/player_signals.png
  247. Notice our custom "hit" signal is there as well! Since our enemies are going to
  248. be ``RigidBody2D`` nodes, we want the ``body_entered(body: Node)`` signal. This
  249. signal will be emitted when a body contacts the player. Click "Connect.." and
  250. the "Connect a Signal" window appears. We don't need to change any of these
  251. settings so click "Connect" again. Godot will automatically create a function in
  252. your player's script.
  253. .. image:: img/player_signal_connection.png
  254. Note the green icon indicating that a signal is connected to this function. Add
  255. this code to the function:
  256. .. tabs::
  257. .. code-tab:: gdscript GDScript
  258. func _on_Player_body_entered(body):
  259. hide() # Player disappears after being hit.
  260. emit_signal("hit")
  261. # Must be deferred as we can't change physics properties on a physics callback.
  262. $CollisionShape2D.set_deferred("disabled", true)
  263. .. code-tab:: csharp
  264. public void OnPlayerBodyEntered(PhysicsBody2D body)
  265. {
  266. Hide(); // Player disappears after being hit.
  267. EmitSignal(nameof(Hit));
  268. // Must be deferred as we can't change physics properties on a physics callback.
  269. GetNode<CollisionShape2D>("CollisionShape2D").SetDeferred("disabled", true);
  270. }
  271. Each time an enemy hits the player, the signal is going to be emitted. We need
  272. to disable the player's collision so that we don't trigger the ``hit`` signal
  273. more than once.
  274. .. Note:: Disabling the area's collision shape can cause an error if it happens
  275. in the middle of the engine's collision processing. Using
  276. ``set_deferred()`` tells Godot to wait to disable the shape until it's
  277. safe to do so.
  278. The last piece is to add a function we can call to reset the player when
  279. starting a new game.
  280. .. tabs::
  281. .. code-tab:: gdscript GDScript
  282. func start(pos):
  283. position = pos
  284. show()
  285. $CollisionShape2D.disabled = false
  286. .. code-tab:: csharp
  287. public void Start(Vector2 pos)
  288. {
  289. Position = pos;
  290. Show();
  291. GetNode<CollisionShape2D>("CollisionShape2D").Disabled = false;
  292. }
  293. With the player working, we'll work on the enemy in the next lesson.