your_first_game.rst 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309
  1. .. _doc_your_first_game:
  2. Your first game
  3. ===============
  4. Overview
  5. --------
  6. This tutorial will guide you through making your first Godot
  7. project. You will learn how the Godot editor works, how to structure
  8. a project, and how to build a 2D game.
  9. .. note:: This project is an introduction to the Godot engine. It
  10. assumes that you have some programming experience already. If
  11. you're new to programming entirely, you should start here:
  12. :ref:`doc_scripting`.
  13. The game is called "Dodge the Creeps!". Your character must move and
  14. avoid the enemies for as long as possible. Here is a preview of the
  15. final result:
  16. .. image:: img/dodge_preview.gif
  17. **Why 2D?** 3D games are much more complex than 2D ones. You should stick to 2D
  18. until you have a good understanding of the game development process and how to
  19. use Godot.
  20. Project setup
  21. -------------
  22. Launch Godot and create a new project. Then, download
  23. :download:`dodge_assets.zip <files/dodge_assets.zip>`. This contains the
  24. images and sounds you'll be using to make the game. Unzip these files in your
  25. project folder.
  26. .. note:: For this tutorial, we will assume you are familiar with the
  27. Godot editor. If you haven't read :ref:`doc_scenes_and_nodes`, do so now
  28. for an explanation of setting up a project and using the editor.
  29. This game is designed for portrait mode, so we need to adjust the size of the
  30. game window. Click on Project -> Project Settings -> Display -> Window and
  31. set "Width" to ``480`` and "Height" to ``720``.
  32. Also in this section, under the "Stretch" options, set ``Mode`` to "2d" and
  33. ``Aspect`` to "keep". This ensures that the game scales consistently on
  34. different sized screens.
  35. Organizing the project
  36. ~~~~~~~~~~~~~~~~~~~~~~
  37. In this project, we will make 3 independent scenes: ``Player``,
  38. ``Mob``, and ``HUD``, which we will combine into the game's ``Main``
  39. scene. In a larger project, it might be useful to create folders to hold
  40. the various scenes and their scripts, but for this relatively small
  41. game, you can save your scenes and scripts in the project's root folder,
  42. identified by ``res://``. You can see your project folders in the FileSystem
  43. Dock in the lower left corner:
  44. .. image:: img/filesystem_dock.png
  45. Player scene
  46. ------------
  47. The first scene will define the ``Player`` object. One of the benefits
  48. of creating a separate Player scene is that we can test it separately, even
  49. before we've created other parts of the game.
  50. Node structure
  51. ~~~~~~~~~~~~~~
  52. To begin, we need to choose a root node for the player object. As a general rule,
  53. a scene's root node should reflect the object's desired functionality - what the
  54. object *is*. Click the "Other Node" button and add an :ref:`Area2D <class_Area2D>`
  55. node to the scene.
  56. .. image:: img/add_node.png
  57. Godot will display a warning icon next to the node in the scene tree. You can
  58. ignore it for now. We will address it later.
  59. With ``Area2D`` we can detect objects that overlap or run into the player.
  60. Change the node's name to ``Player`` by double-clicking on it. Now that we've
  61. set the scene's root node, we can add additional nodes to give it more
  62. functionality.
  63. Before we add any children to the ``Player`` node, we want to make sure we don't
  64. accidentally move or resize them by clicking on them. Select the node and
  65. click the icon to the right of the lock; its tooltip says "Makes sure the object's children
  66. are not selectable."
  67. .. image:: img/lock_children.png
  68. Save the scene. Click Scene -> Save, or press :kbd:`Ctrl + S` on Windows/Linux or :kbd:`Cmd + S` on macOS.
  69. .. note:: For this project, we will be following the Godot naming conventions.
  70. - **GDScript**: Classes (nodes) use PascalCase, variables and
  71. functions use snake_case, and constants use ALL_CAPS (See
  72. :ref:`doc_gdscript_styleguide`).
  73. - **C#**: Classes, export variables and methods use PascalCase,
  74. private fields use _camelCase, local variables and parameters use
  75. camelCase (See :ref:`doc_c_sharp_styleguide`). Be careful to type
  76. the method names precisely when connecting signals.
  77. Sprite animation
  78. ~~~~~~~~~~~~~~~~
  79. Click on the ``Player`` node and add an :ref:`AnimatedSprite <class_AnimatedSprite>` node as a
  80. child. The ``AnimatedSprite`` will handle the appearance and animations
  81. for our player. Notice that there is a warning symbol next to the node.
  82. An ``AnimatedSprite`` requires a :ref:`SpriteFrames <class_SpriteFrames>` resource, which is a
  83. list of the animations it can display. To create one, find the
  84. ``Frames`` property in the Inspector and click "[empty]" ->
  85. "New SpriteFrames". Click again to open the "SpriteFrames" panel:
  86. .. image:: img/spriteframes_panel.png
  87. On the left is a list of animations. Click the "default" one and rename
  88. it to "walk". Then click the "New Animation" button to create a second animation
  89. named "up". Find the player images in the "FileSystem" tab - they're in the
  90. ``art`` folder you unzipped earlier. Drag the two images for each animation, named
  91. ``playerGrey_up[1/2]`` and ``playerGrey_walk[1/2]``, into the "Animation Frames"
  92. side of the panel for the corresponding animation:
  93. .. image:: img/spriteframes_panel2.png
  94. The player images are a bit too large for the game window, so we need to
  95. scale them down. Click on the ``AnimatedSprite`` node and set the ``Scale``
  96. property to ``(0.5, 0.5)``. You can find it in the Inspector under the
  97. ``Node2D`` heading.
  98. .. image:: img/player_scale.png
  99. Finally, add a :ref:`CollisionShape2D <class_CollisionShape2D>` as a child
  100. of ``Player``. This will determine the player's "hitbox", or the
  101. bounds of its collision area. For this character, a ``CapsuleShape2D``
  102. node gives the best fit, so next to "Shape" in the Inspector, click
  103. "[empty]"" -> "New CapsuleShape2D". Using the two size handles, resize the
  104. shape to cover the sprite:
  105. .. image:: img/player_coll_shape.png
  106. When you're finished, your ``Player`` scene should look like this:
  107. .. image:: img/player_scene_nodes.png
  108. Make sure to save the scene again after these changes.
  109. Moving the player
  110. ~~~~~~~~~~~~~~~~~
  111. Now we need to add some functionality that we can't get from a built-in
  112. node, so we'll add a script. Click the ``Player`` node and click the
  113. "Attach Script" button:
  114. .. image:: img/add_script_button.png
  115. In the script settings window, you can leave the default settings alone. Just
  116. click "Create":
  117. .. note:: If you're creating a C# script or other languages, select the
  118. language from the `language` drop down menu before hitting create.
  119. .. image:: img/attach_node_window.png
  120. .. note:: If this is your first time encountering GDScript, please read
  121. :ref:`doc_scripting` before continuing.
  122. Start by declaring the member variables this object will need:
  123. .. tabs::
  124. .. code-tab:: gdscript GDScript
  125. extends Area2D
  126. export var speed = 400 # How fast the player will move (pixels/sec).
  127. var screen_size # Size of the game window.
  128. .. code-tab:: csharp
  129. public class Player : Area2D
  130. {
  131. [Export]
  132. public int Speed = 400; // How fast the player will move (pixels/sec).
  133. private Vector2 _screenSize; // Size of the game window.
  134. }
  135. Using the ``export`` keyword on the first variable ``speed`` allows us to
  136. set its value in the Inspector. This can be handy for values that you
  137. want to be able to adjust just like a node's built-in properties. Click on
  138. the ``Player`` node and you'll see the property now appears in the "Script
  139. Variables" section of the Inspector. Remember, if you change the value here, it
  140. will override the value written in the script.
  141. .. warning:: If you're using C#, you need to (re)build the project assemblies
  142. whenever you want to see new export variables or signals. This
  143. build can be manually triggered by clicking the word "Mono" at the
  144. bottom of the editor window to reveal the Mono Panel, then
  145. clicking the "Build Project" button.
  146. .. image:: img/export_variable.png
  147. The ``_ready()`` function is called when a node enters the scene tree,
  148. which is a good time to find the size of the game window:
  149. .. tabs::
  150. .. code-tab:: gdscript GDScript
  151. func _ready():
  152. screen_size = get_viewport_rect().size
  153. .. code-tab:: csharp
  154. public override void _Ready()
  155. {
  156. _screenSize = GetViewport().Size;
  157. }
  158. Now we can use the ``_process()`` function to define what the player will do.
  159. ``_process()`` is called every frame, so we'll use it to update
  160. elements of our game, which we expect will change often. For the player, we
  161. need to do the following:
  162. - Check for input.
  163. - Move in the given direction.
  164. - Play the appropriate animation.
  165. First, we need to check for input - is the player pressing a key? For
  166. this game, we have 4 direction inputs to check. Input actions are defined
  167. in the Project Settings under "Input Map". Here, you can define custom events and
  168. assign different keys, mouse events, or other inputs to them. For this demo,
  169. we will use the default events that are assigned to the arrow keys on the
  170. keyboard.
  171. You can detect whether a key is pressed using
  172. ``Input.is_action_pressed()``, which returns ``true`` if it's pressed
  173. or ``false`` if it isn't.
  174. .. tabs::
  175. .. code-tab:: gdscript GDScript
  176. func _process(delta):
  177. var velocity = Vector2() # The player's movement vector.
  178. if Input.is_action_pressed("ui_right"):
  179. velocity.x += 1
  180. if Input.is_action_pressed("ui_left"):
  181. velocity.x -= 1
  182. if Input.is_action_pressed("ui_down"):
  183. velocity.y += 1
  184. if Input.is_action_pressed("ui_up"):
  185. velocity.y -= 1
  186. if velocity.length() > 0:
  187. velocity = velocity.normalized() * speed
  188. $AnimatedSprite.play()
  189. else:
  190. $AnimatedSprite.stop()
  191. .. code-tab:: csharp
  192. public override void _Process(float delta)
  193. {
  194. var velocity = new Vector2(); // The player's movement vector.
  195. if (Input.IsActionPressed("ui_right"))
  196. {
  197. velocity.x += 1;
  198. }
  199. if (Input.IsActionPressed("ui_left"))
  200. {
  201. velocity.x -= 1;
  202. }
  203. if (Input.IsActionPressed("ui_down"))
  204. {
  205. velocity.y += 1;
  206. }
  207. if (Input.IsActionPressed("ui_up"))
  208. {
  209. velocity.y -= 1;
  210. }
  211. var animatedSprite = GetNode<AnimatedSprite>("AnimatedSprite");
  212. if (velocity.Length() > 0)
  213. {
  214. velocity = velocity.Normalized() * Speed;
  215. animatedSprite.Play();
  216. }
  217. else
  218. {
  219. animatedSprite.Stop();
  220. }
  221. }
  222. We start by setting the ``velocity`` to ``(0, 0)`` - by default, the player
  223. should not be moving. Then we check each input and add/subtract from the
  224. ``velocity`` to obtain a total direction. For example, if you hold ``right``
  225. and ``down`` at the same time, the resulting ``velocity`` vector will be
  226. ``(1, 1)``. In this case, since we're adding a horizontal and a vertical
  227. movement, the player would move *faster* diagonally than if it just moved horizontally.
  228. We can prevent that if we *normalize* the velocity, which means we set
  229. its *length* to ``1``, then multiply by the desired speed. This means no
  230. more fast diagonal movement.
  231. .. tip:: If you've never used vector math before, or need a refresher,
  232. you can see an explanation of vector usage in Godot at :ref:`doc_vector_math`.
  233. It's good to know but won't be necessary for the rest of this tutorial.
  234. We also check whether the player is moving so we can call ``play()`` or ``stop()``
  235. on the AnimatedSprite.
  236. ``$`` is shorthand for ``get_node()``.
  237. So in the code above, ``$AnimatedSprite.play()`` is the same as ``get_node("AnimatedSprite").play()``.
  238. .. tip:: In GDScript, ``$`` returns the node at the relative path from the current node, or returns ``null`` if the node is not found.
  239. Since AnimatedSprite is a child of the current node, we can use ``$AnimatedSprite``.
  240. Now that we have a movement direction, we can update the player's position. We
  241. can also use ``clamp()`` to prevent it from leaving the screen. *Clamping* a value
  242. means restricting it to a given range. Add the following to the bottom of
  243. the ``_process`` function (make sure it's not indented under the `else`):
  244. .. tabs::
  245. .. code-tab:: gdscript GDScript
  246. position += velocity * delta
  247. position.x = clamp(position.x, 0, screen_size.x)
  248. position.y = clamp(position.y, 0, screen_size.y)
  249. .. code-tab:: csharp
  250. Position += velocity * delta;
  251. Position = new Vector2(
  252. x: Mathf.Clamp(Position.x, 0, _screenSize.x),
  253. y: Mathf.Clamp(Position.y, 0, _screenSize.y)
  254. );
  255. .. tip:: The `delta` parameter in the `_process()` function refers to the
  256. *frame length* - the amount of time that the previous frame took to
  257. complete. Using this value ensures that your movement will remain
  258. consistent even if the frame rate changes.
  259. Click "Play Scene" (:kbd:`F6`, :kbd:`Cmd + R` on macOS) and confirm you can move the player
  260. around the screen in all directions.
  261. .. warning:: If you get an error in the "Debugger" panel that says
  262. ``Attempt to call function 'play' in base 'null instance' on a null instance``
  263. this likely means you spelled the name of the AnimatedSprite node wrong.
  264. Node names are case-sensitive and ``$NodeName`` must match the name
  265. you see in the scene tree.
  266. Choosing animations
  267. ~~~~~~~~~~~~~~~~~~~
  268. Now that the player can move, we need to change which animation the
  269. AnimatedSprite is playing based on its direction. We have the "walk"
  270. animation, which shows the player walking to the right. This animation should
  271. be flipped horizontally using the ``flip_h`` property for left movement. We also
  272. have the "up" animation, which should be flipped vertically with ``flip_v``
  273. for downward movement. Let's place this code at the end of the ``_process()``
  274. function:
  275. .. tabs::
  276. .. code-tab:: gdscript GDScript
  277. if velocity.x != 0:
  278. $AnimatedSprite.animation = "walk"
  279. $AnimatedSprite.flip_v = false
  280. # See the note below about boolean assignment
  281. $AnimatedSprite.flip_h = velocity.x < 0
  282. elif velocity.y != 0:
  283. $AnimatedSprite.animation = "up"
  284. $AnimatedSprite.flip_v = velocity.y > 0
  285. .. code-tab:: csharp
  286. if (velocity.x != 0)
  287. {
  288. animatedSprite.Animation = "walk";
  289. animatedSprite.FlipV = false;
  290. // See the note below about boolean assignment
  291. animatedSprite.FlipH = velocity.x < 0;
  292. }
  293. else if (velocity.y != 0)
  294. {
  295. animatedSprite.Animation = "up";
  296. animatedSprite.FlipV = velocity.y > 0;
  297. }
  298. .. Note:: The boolean assignments in the code above are a common shorthand
  299. for programmers. Since we're doing a comparison test (boolean) and also
  300. *assigning* a boolean value, we can do both at the same time. Consider
  301. this code versus the one-line boolean assignment above:
  302. .. tabs::
  303. .. code-tab :: gdscript GDScript
  304. if velocity.x < 0:
  305. $AnimatedSprite.flip_h = true
  306. else:
  307. $AnimatedSprite.flip_h = false
  308. .. code-tab:: csharp
  309. if (velocity.x < 0)
  310. {
  311. animatedSprite.FlipH = true;
  312. }
  313. else
  314. {
  315. animatedSprite.FlipH = false;
  316. }
  317. Play the scene again and check that the animations are correct in each
  318. of the directions.
  319. .. tip:: A common mistake here is to type the names of the animations wrong. The
  320. animation names in the SpriteFrames panel must match what you type in the
  321. code. If you named the animation ``"Walk"``, you must also use a capital
  322. "W" in the code.
  323. When you're sure the movement is working correctly, add this line to ``_ready()``,
  324. so the player will be hidden when the game starts:
  325. .. tabs::
  326. .. code-tab:: gdscript GDScript
  327. hide()
  328. .. code-tab:: csharp
  329. Hide();
  330. Preparing for collisions
  331. ~~~~~~~~~~~~~~~~~~~~~~~~
  332. We want ``Player`` to detect when it's hit by an enemy, but we haven't
  333. made any enemies yet! That's OK, because we're going to use Godot's
  334. *signal* functionality to make it work.
  335. Add the following at the top of the script, after ``extends Area2D``:
  336. .. tabs::
  337. .. code-tab:: gdscript GDScript
  338. signal hit
  339. .. code-tab:: csharp
  340. // Don't forget to rebuild the project so the editor knows about the new signal.
  341. [Signal]
  342. public delegate void Hit();
  343. This defines a custom signal called "hit" that we will have our player
  344. emit (send out) when it collides with an enemy. We will use ``Area2D`` to
  345. detect the collision. Select the ``Player`` node and click the "Node" tab
  346. next to the Inspector tab to see the list of signals the player can emit:
  347. .. image:: img/player_signals.png
  348. Notice our custom "hit" signal is there as well! Since our enemies are
  349. going to be ``RigidBody2D`` nodes, we want the
  350. ``body_entered(body: Node)`` signal. This signal will be emitted when a
  351. body contacts the player. Click "Connect.." and the "Connect a Signal" window
  352. appears. We don't need to change any of these settings so click "Connect" again.
  353. Godot will automatically create a function in your player's script.
  354. .. image:: img/player_signal_connection.png
  355. Note the green icon indicating that a signal is connected to this function. Add
  356. this code to the function:
  357. .. tabs::
  358. .. code-tab:: gdscript GDScript
  359. func _on_Player_body_entered(body):
  360. hide() # Player disappears after being hit.
  361. emit_signal("hit")
  362. $CollisionShape2D.set_deferred("disabled", true)
  363. .. code-tab:: csharp
  364. public void OnPlayerBodyEntered(PhysicsBody2D body)
  365. {
  366. Hide(); // Player disappears after being hit.
  367. EmitSignal("Hit");
  368. GetNode<CollisionShape2D>("CollisionShape2D").SetDeferred("disabled", true);
  369. }
  370. Each time an enemy hits the player, the signal is going to be emitted. We need
  371. to disable the player's collision so that we don't trigger the ``hit`` signal
  372. more than once.
  373. .. Note:: Disabling the area's collision shape can cause an error if it happens
  374. in the middle of the engine's collision processing. Using ``set_deferred()``
  375. tells Godot to wait to disable the shape until it's safe to do so.
  376. The last piece is to add a function we can call to reset the player when
  377. starting a new game.
  378. .. tabs::
  379. .. code-tab:: gdscript GDScript
  380. func start(pos):
  381. position = pos
  382. show()
  383. $CollisionShape2D.disabled = false
  384. .. code-tab:: csharp
  385. public void Start(Vector2 pos)
  386. {
  387. Position = pos;
  388. Show();
  389. GetNode<CollisionShape2D>("CollisionShape2D").Disabled = false;
  390. }
  391. Enemy scene
  392. -----------
  393. Now it's time to make the enemies our player will have to dodge. Their
  394. behavior will not be very complex: mobs will spawn randomly at the edges
  395. of the screen, choose a random direction, and move in a straight line.
  396. We'll create a ``Mob`` scene, which we can then *instance* to create any
  397. number of independent mobs in the game.
  398. .. note:: See :ref:`doc_instancing` to learn more about instancing.
  399. Node setup
  400. ~~~~~~~~~~
  401. Click Scene -> New Scene and add the following nodes:
  402. - :ref:`RigidBody2D <class_RigidBody2D>` (named ``Mob``)
  403. - :ref:`AnimatedSprite <class_AnimatedSprite>`
  404. - :ref:`CollisionShape2D <class_CollisionShape2D>`
  405. - :ref:`VisibilityNotifier2D <class_VisibilityNotifier2D>`
  406. Don't forget to set the children so they can't be selected, like you did with the
  407. Player scene.
  408. In the :ref:`RigidBody2D <class_RigidBody2D>` properties, set ``Gravity Scale`` to ``0``, so
  409. the mob will not fall downward. In addition, under the
  410. ``PhysicsBody2D`` section, click the ``Mask`` property and
  411. uncheck the first box. This will ensure the mobs do not collide with each other.
  412. .. image:: img/set_collision_mask.png
  413. Set up the :ref:`AnimatedSprite <class_AnimatedSprite>` like you did for the player.
  414. This time, we have 3 animations: ``fly``, ``swim``, and ``walk``. There are two
  415. images for each animation in the art folder.
  416. Adjust the "Speed (FPS)" to ``3`` for all animations.
  417. .. image:: img/mob_animations.gif
  418. Set the ``Playing`` property in the Inspector to “On”.
  419. We'll select one of these animations randomly so that the mobs will have some variety.
  420. Like the player images, these mob images need to be scaled down. Set the
  421. ``AnimatedSprite``'s ``Scale`` property to ``(0.75, 0.75)``.
  422. As in the ``Player`` scene, add a ``CapsuleShape2D`` for the
  423. collision. To align the shape with the image, you'll need to set the
  424. ``Rotation Degrees`` property to ``90`` (under "Transform" in the Inspector).
  425. Save the scene.
  426. Enemy script
  427. ~~~~~~~~~~~~
  428. Add a script to the ``Mob`` and add the following member variables:
  429. .. tabs::
  430. .. code-tab:: gdscript GDScript
  431. extends RigidBody2D
  432. export var min_speed = 150 # Minimum speed range.
  433. export var max_speed = 250 # Maximum speed range.
  434. .. code-tab:: csharp
  435. public class Mob : RigidBody2D
  436. {
  437. // Don't forget to rebuild the project so the editor knows about the new export variables.
  438. [Export]
  439. public int MinSpeed = 150; // Minimum speed range.
  440. [Export]
  441. public int MaxSpeed = 250; // Maximum speed range.
  442. }
  443. When we spawn a mob, we'll pick a random value between ``min_speed`` and
  444. ``max_speed`` for how fast each mob will move (it would be boring if they
  445. were all moving at the same speed).
  446. Now let's look at the rest of the script. In ``_ready()`` we randomly
  447. choose one of the three animation types:
  448. .. tabs::
  449. .. code-tab:: gdscript GDScript
  450. func _ready():
  451. var mob_types = $AnimatedSprite.frames.get_animation_names()
  452. $AnimatedSprite.animation = mob_types[randi() % mob_types.size()]
  453. .. code-tab:: csharp
  454. // C# doesn't implement GDScript's random methods, so we use 'System.Random' as an alternative.
  455. static private Random _random = new Random();
  456. public override void _Ready()
  457. {
  458. var animSprite = GetNode<AnimatedSprite>("AnimatedSprite");
  459. var mobTypes = animSprite.Frames.GetAnimationNames();
  460. animSprite.Animation = mobTypes[_random.Next(0, mobTypes.Length)];
  461. }
  462. First, we get the list of animation names from the AnimatedSprite's ``frames``
  463. property. This returns an Array containing all three animation names:
  464. ``["walk", "swim", "fly"]``.
  465. We then need to pick a random number between ``0`` and ``2`` to select one of these
  466. names from the list (array indices start at ``0``). ``randi() % n`` selects a
  467. random integer between ``0`` and ``n-1``.
  468. .. note:: You must use ``randomize()`` if you want your sequence of "random"
  469. numbers to be different every time you run the scene. We're going
  470. to use ``randomize()`` in our ``Main`` scene, so we won't need it here.
  471. The last piece is to make the mobs delete themselves when they leave the
  472. screen. Connect the ``screen_exited()`` signal of the ``VisibilityNotifier2D``
  473. node and add this code:
  474. .. tabs::
  475. .. code-tab:: gdscript GDScript
  476. func _on_VisibilityNotifier2D_screen_exited():
  477. queue_free()
  478. .. code-tab:: csharp
  479. public void OnVisibilityNotifier2DScreenExited()
  480. {
  481. QueueFree();
  482. }
  483. This completes the `Mob` scene.
  484. Main scene
  485. ----------
  486. Now it's time to bring it all together. Create a new scene and add a
  487. :ref:`Node <class_Node>` named ``Main``. Ensure you create a Node, **not** a
  488. Node2D. Click the "Instance" button and select your
  489. saved ``Player.tscn``.
  490. .. image:: img/instance_scene.png
  491. Now, add the following nodes as children of ``Main``, and name them as
  492. shown (values are in seconds):
  493. - :ref:`Timer <class_Timer>` (named ``MobTimer``) - to control how often mobs spawn
  494. - :ref:`Timer <class_Timer>` (named ``ScoreTimer``) - to increment the score every second
  495. - :ref:`Timer <class_Timer>` (named ``StartTimer``) - to give a delay before starting
  496. - :ref:`Position2D <class_Position2D>` (named ``StartPosition``) - to indicate the player's start position
  497. Set the ``Wait Time`` property of each of the ``Timer`` nodes as
  498. follows:
  499. - ``MobTimer``: ``0.5``
  500. - ``ScoreTimer``: ``1``
  501. - ``StartTimer``: ``2``
  502. In addition, set the ``One Shot`` property of ``StartTimer`` to "On" and
  503. set ``Position`` of the ``StartPosition`` node to ``(240, 450)``.
  504. Spawning mobs
  505. ~~~~~~~~~~~~~
  506. The Main node will be spawning new mobs, and we want them to appear at a
  507. random location on the edge of the screen. Add a :ref:`Path2D <class_Path2D>` node named
  508. ``MobPath`` as a child of ``Main``. When you select ``Path2D``,
  509. you will see some new buttons at the top of the editor:
  510. .. image:: img/path2d_buttons.png
  511. Select the middle one ("Add Point") and draw the path by clicking to add
  512. the points at the corners shown. To have the points snap to the grid, make
  513. sure "Use Grid Snap" and "Use Snap" are both selected. These options can be
  514. found to the left of the "Lock" button, appearing as a magnet next to some
  515. dots and intersecting lines, respectively.
  516. .. image:: img/grid_snap_button.png
  517. .. important:: Draw the path in *clockwise* order, or your mobs will spawn
  518. pointing *outwards* instead of *inwards*!
  519. .. image:: img/draw_path2d.gif
  520. After placing point ``4`` in the image, click the "Close Curve" button and
  521. your curve will be complete.
  522. Now that the path is defined, add a :ref:`PathFollow2D <class_PathFollow2D>`
  523. node as a child of ``MobPath`` and name it ``MobSpawnLocation``. This node will
  524. automatically rotate and follow the path as it moves, so we can use it
  525. to select a random position and direction along the path.
  526. Your scene should look like this:
  527. .. image:: img/main_scene_nodes.png
  528. Main script
  529. ~~~~~~~~~~~
  530. Add a script to ``Main``. At the top of the script, we use
  531. ``export (PackedScene)`` to allow us to choose the Mob scene we want to
  532. instance.
  533. .. tabs::
  534. .. code-tab:: gdscript GDScript
  535. extends Node
  536. export (PackedScene) var Mob
  537. var score
  538. func _ready():
  539. randomize()
  540. .. code-tab:: csharp
  541. public class Main : Node
  542. {
  543. // Don't forget to rebuild the project so the editor knows about the new export variable.
  544. [Export]
  545. public PackedScene Mob;
  546. private int _score;
  547. // We use 'System.Random' as an alternative to GDScript's random methods.
  548. private Random _random = new Random();
  549. public override void _Ready()
  550. {
  551. }
  552. // We'll use this later because C# doesn't support GDScript's randi().
  553. private float RandRange(float min, float max)
  554. {
  555. return (float)_random.NextDouble() * (max - min) + min;
  556. }
  557. }
  558. Click the ``Main`` node and you will see the ``Mob`` property in the Inspector
  559. under "Script Variables".
  560. You can assign this property's value in two ways:
  561. - Drag ``Mob.tscn`` from the "FileSystem" panel and drop it in the
  562. ``Mob`` property .
  563. - Click the down arrow next to "[empty]" and choose "Load". Select
  564. ``Mob.tscn``.
  565. Next, select the ``Player`` node in the Scene dock, and access the Node dock on
  566. the sidebar. Make sure to have the Signals tab selected in the Node dock.
  567. You should see a list of the signals for the ``Player`` node. Find and
  568. double-click the ``hit`` signal in the list (or right-click it and select
  569. "Connect..."). This will open the signal connection dialog. We want to make
  570. a new function named ``game_over``, which will handle what needs to happen when
  571. a game ends.
  572. Type "game_over" in the "Receiver Method" box at the bottom of the
  573. signal connection dialog and click "Connect". Add the following code to the
  574. new function, as well as a ``new_game`` function that will set everything up
  575. for a new game:
  576. .. tabs::
  577. .. code-tab:: gdscript GDScript
  578. func game_over():
  579. $ScoreTimer.stop()
  580. $MobTimer.stop()
  581. func new_game():
  582. score = 0
  583. $Player.start($StartPosition.position)
  584. $StartTimer.start()
  585. .. code-tab:: csharp
  586. public void GameOver()
  587. {
  588. GetNode<Timer>("MobTimer").Stop();
  589. GetNode<Timer>("ScoreTimer").Stop();
  590. }
  591. public void NewGame()
  592. {
  593. _score = 0;
  594. var player = GetNode<Player>("Player");
  595. var startPosition = GetNode<Position2D>("StartPosition");
  596. player.Start(startPosition.Position);
  597. GetNode<Timer>("StartTimer").Start();
  598. }
  599. Now connect the ``timeout()`` signal of each of the Timer nodes (``StartTimer``,
  600. ``ScoreTimer`` , and ``MobTimer``) to the main script. ``StartTimer`` will start
  601. the other two timers. ``ScoreTimer`` will increment the score by 1.
  602. .. tabs::
  603. .. code-tab:: gdscript GDScript
  604. func _on_StartTimer_timeout():
  605. $MobTimer.start()
  606. $ScoreTimer.start()
  607. func _on_ScoreTimer_timeout():
  608. score += 1
  609. .. code-tab:: csharp
  610. public void OnStartTimerTimeout()
  611. {
  612. GetNode<Timer>("MobTimer").Start();
  613. GetNode<Timer>("ScoreTimer").Start();
  614. }
  615. public void OnScoreTimerTimeout()
  616. {
  617. _score++;
  618. }
  619. In ``_on_MobTimer_timeout()``, we will create a mob instance, pick a
  620. random starting location along the ``Path2D``, and set the mob in
  621. motion. The ``PathFollow2D`` node will automatically rotate as it
  622. follows the path, so we will use that to select the mob's direction as
  623. well as its position.
  624. Note that a new instance must be added to the scene using ``add_child()``.
  625. .. tabs::
  626. .. code-tab:: gdscript GDScript
  627. func _on_MobTimer_timeout():
  628. # Choose a random location on Path2D.
  629. $MobPath/MobSpawnLocation.offset = randi()
  630. # Create a Mob instance and add it to the scene.
  631. var mob = Mob.instance()
  632. add_child(mob)
  633. # Set the mob's direction perpendicular to the path direction.
  634. var direction = $MobPath/MobSpawnLocation.rotation + PI / 2
  635. # Set the mob's position to a random location.
  636. mob.position = $MobPath/MobSpawnLocation.position
  637. # Add some randomness to the direction.
  638. direction += rand_range(-PI / 4, PI / 4)
  639. mob.rotation = direction
  640. # Set the velocity (speed & direction).
  641. mob.linear_velocity = Vector2(rand_range(mob.min_speed, mob.max_speed), 0)
  642. mob.linear_velocity = mob.linear_velocity.rotated(direction)
  643. .. code-tab:: csharp
  644. public void OnMobTimerTimeout()
  645. {
  646. // Choose a random location on Path2D.
  647. var mobSpawnLocation = GetNode<PathFollow2D>("MobPath/MobSpawnLocation");
  648. mobSpawnLocation.Offset = _random.Next();
  649. // Create a Mob instance and add it to the scene.
  650. var mobInstance = (RigidBody2D)Mob.Instance();
  651. AddChild(mobInstance);
  652. // Set the mob's direction perpendicular to the path direction.
  653. float direction = mobSpawnLocation.Rotation + Mathf.Pi / 2;
  654. // Set the mob's position to a random location.
  655. mobInstance.Position = mobSpawnLocation.Position;
  656. // Add some randomness to the direction.
  657. direction += RandRange(-Mathf.Pi / 4, Mathf.Pi / 4);
  658. mobInstance.Rotation = direction;
  659. // Choose the velocity.
  660. mobInstance.LinearVelocity = new Vector2(RandRange(150f, 250f), 0).Rotated(direction);
  661. }
  662. .. important:: Why ``PI``? In functions requiring angles, GDScript uses *radians*,
  663. not degrees. If you're more comfortable working with
  664. degrees, you'll need to use the ``deg2rad()`` and
  665. ``rad2deg()`` functions to convert between the two.
  666. Testing the scene
  667. ~~~~~~~~~~~~~~~~~
  668. Let's test the scene to make sure everything is working. Add this to ``_ready()``:
  669. .. tabs::
  670. .. code-tab:: gdscript GDScript
  671. func _ready():
  672. randomize()
  673. new_game()
  674. .. code-tab:: csharp
  675. public override void _Ready()
  676. {
  677. NewGame();
  678. }
  679. }
  680. Let's also assign ``Main`` as our "Main Scene" - the one that runs automatically
  681. when the game launches. Press the "Play" button and select ``Main.tscn`` when
  682. prompted.
  683. You should be able to move the player around, see mobs spawning, and see the player
  684. disappear when hit by a mob.
  685. When you're sure everything is working, remove the call to ``new_game()`` from
  686. ``_ready()``.
  687. HUD
  688. ---
  689. The final piece our game needs is a UI: an interface to display things
  690. like score, a "game over" message, and a restart button. Create a new
  691. scene, and add a :ref:`CanvasLayer <class_CanvasLayer>` node named ``HUD``. "HUD"
  692. stands for "heads-up display", an informational display that appears as an
  693. overlay on top of the game view.
  694. The :ref:`CanvasLayer <class_CanvasLayer>` node lets us draw our UI elements on
  695. a layer above the rest of the game, so that the information it displays isn't
  696. covered up by any game elements like the player or mobs.
  697. The HUD needs to display the following information:
  698. - Score, changed by ``ScoreTimer``.
  699. - A message, such as "Game Over" or "Get Ready!"
  700. - A "Start" button to begin the game.
  701. The basic node for UI elements is :ref:`Control <class_Control>`. To create our UI,
  702. we'll use two types of :ref:`Control <class_Control>` nodes: :ref:`Label <class_Label>`
  703. and :ref:`Button <class_Button>`.
  704. Create the following as children of the ``HUD`` node:
  705. - :ref:`Label <class_Label>` named ``ScoreLabel``.
  706. - :ref:`Label <class_Label>` named ``Message``.
  707. - :ref:`Button <class_Button>` named ``StartButton``.
  708. - :ref:`Timer <class_Timer>` named ``MessageTimer``.
  709. Click on the ``ScoreLabel`` and type a number into the ``Text`` field in the
  710. Inspector. The default font for ``Control`` nodes is small and doesn't scale
  711. well. There is a font file included in the game assets called
  712. "Xolonium-Regular.ttf". To use this font, do the following:
  713. 1. Under "Custom Fonts", choose "New DynamicFont"
  714. .. image:: img/custom_font1.png
  715. 2. Click on the "DynamicFont" you added, and under "Font/Font Data",
  716. choose "Load" and select the "Xolonium-Regular.ttf" file. You must
  717. also set the font's ``Size``. A setting of ``64`` works well.
  718. .. image:: img/custom_font2.png
  719. Once you've done this on the ``ScoreLabel``, you can click the down arrow next
  720. to the DynamicFont property and choose "Copy", then "Paste" it in the same place
  721. on the other two Control nodes.
  722. .. note:: **Anchors and Margins:** ``Control`` nodes have a position and size,
  723. but they also have anchors and margins. Anchors define the
  724. origin - the reference point for the edges of the node. Margins
  725. update automatically when you move or resize a control node. They
  726. represent the distance from the control node's edges to its anchor.
  727. Arrange the nodes as shown below. Click the "Layout" button to
  728. set a Control node's layout:
  729. .. image:: img/ui_anchor.png
  730. You can drag the nodes to place them manually, or for more precise
  731. placement, use the following settings:
  732. ScoreLabel
  733. ~~~~~~~~~~
  734. - *Layout* : "Top Wide"
  735. - *Text* : ``0``
  736. - *Align* : "Center"
  737. Message
  738. ~~~~~~~~~~~~
  739. - *Layout* : "HCenter Wide"
  740. - *Text* : ``Dodge the Creeps!``
  741. - *Align* : "Center"
  742. - *Autowrap* : "On"
  743. StartButton
  744. ~~~~~~~~~~~
  745. - *Text* : ``Start``
  746. - *Layout* : "Center Bottom"
  747. - *Margin* :
  748. - Top: ``-200``
  749. - Bottom: ``-100``
  750. On the ``MessageTimer``, set the ``Wait Time`` to ``2`` and set the ``One Shot``
  751. property to "On".
  752. Now add this script to ``HUD``:
  753. .. tabs::
  754. .. code-tab:: gdscript GDScript
  755. extends CanvasLayer
  756. signal start_game
  757. .. code-tab:: csharp
  758. public class HUD : CanvasLayer
  759. {
  760. // Don't forget to rebuild the project so the editor knows about the new signal.
  761. [Signal]
  762. public delegate void StartGame();
  763. }
  764. The ``start_game`` signal tells the ``Main`` node that the button
  765. has been pressed.
  766. .. tabs::
  767. .. code-tab:: gdscript GDScript
  768. func show_message(text):
  769. $Message.text = text
  770. $Message.show()
  771. $MessageTimer.start()
  772. .. code-tab:: csharp
  773. public void ShowMessage(string text)
  774. {
  775. var message = GetNode<Label>("Message");
  776. message.Text = text;
  777. message.Show();
  778. GetNode<Timer>("MessageTimer").Start();
  779. }
  780. This function is called when we want to display a message
  781. temporarily, such as "Get Ready".
  782. .. tabs::
  783. .. code-tab:: gdscript GDScript
  784. func show_game_over():
  785. show_message("Game Over")
  786. # Wait until the MessageTimer has counted down.
  787. yield($MessageTimer, "timeout")
  788. $Message.text = "Dodge the\nCreeps!"
  789. $Message.show()
  790. # Make a one-shot timer and wait for it to finish.
  791. yield(get_tree().create_timer(1), "timeout")
  792. $StartButton.show()
  793. .. code-tab:: csharp
  794. async public void ShowGameOver()
  795. {
  796. ShowMessage("Game Over");
  797. var messageTimer = GetNode<Timer>("MessageTimer");
  798. await ToSignal(messageTimer, "timeout");
  799. var message = GetNode<Label>("Message");
  800. message.Text = "Dodge the\nCreeps!";
  801. message.Show();
  802. await ToSignal(GetTree().CreateTimer(1), "timeout");
  803. GetNode<Button>("StartButton").Show();
  804. }
  805. This function is called when the player loses. It will show "Game
  806. Over" for 2 seconds, then return to the title screen and, after a brief pause,
  807. show the "Start" button.
  808. .. note:: When you need to pause for a brief time, an alternative to using a
  809. Timer node is to use the SceneTree's ``create_timer()`` function. This
  810. can be very useful to add delays such as in the above code, where we want
  811. to wait some time before showing the "Start" button.
  812. .. tabs::
  813. .. code-tab:: gdscript GDScript
  814. func update_score(score):
  815. $ScoreLabel.text = str(score)
  816. .. code-tab:: csharp
  817. public void UpdateScore(int score)
  818. {
  819. GetNode<Label>("ScoreLabel").Text = score.ToString();
  820. }
  821. This function is called by ``Main`` whenever the score changes.
  822. Connect the ``timeout()`` signal of ``MessageTimer`` and the
  823. ``pressed()`` signal of ``StartButton`` and add the following code to the new
  824. functions:
  825. .. tabs::
  826. .. code-tab:: gdscript GDScript
  827. func _on_StartButton_pressed():
  828. $StartButton.hide()
  829. emit_signal("start_game")
  830. func _on_MessageTimer_timeout():
  831. $Message.hide()
  832. .. code-tab:: csharp
  833. public void OnStartButtonPressed()
  834. {
  835. GetNode<Button>("StartButton").Hide();
  836. EmitSignal("StartGame");
  837. }
  838. public void OnMessageTimerTimeout()
  839. {
  840. GetNode<Label>("Message").Hide();
  841. }
  842. Connecting HUD to Main
  843. ~~~~~~~~~~~~~~~~~~~~~~
  844. Now that we're done creating the ``HUD`` scene, go back to ``Main``.
  845. Instance the ``HUD`` scene in ``Main`` like you did the ``Player`` scene. The
  846. scene tree should look like this, so make sure you didn't miss anything:
  847. .. image:: img/completed_main_scene.png
  848. Now we need to connect the ``HUD`` functionality to our ``Main`` script.
  849. This requires a few additions to the ``Main`` scene:
  850. In the Node tab, connect the HUD's ``start_game`` signal to the
  851. ``new_game()`` function of the Main node by typing "new_game" in the "Receiver
  852. Method" in the "Connect a Signal" window. Verify that the green connection icon
  853. now appears next to ``func new_game()`` in the script.
  854. In ``new_game()``, update the score display and show the "Get Ready"
  855. message:
  856. .. tabs::
  857. .. code-tab:: gdscript GDScript
  858. $HUD.update_score(score)
  859. $HUD.show_message("Get Ready")
  860. .. code-tab:: csharp
  861. var hud = GetNode<HUD>("HUD");
  862. hud.UpdateScore(_score);
  863. hud.ShowMessage("Get Ready!");
  864. In ``game_over()`` we need to call the corresponding ``HUD`` function:
  865. .. tabs::
  866. .. code-tab:: gdscript GDScript
  867. $HUD.show_game_over()
  868. .. code-tab:: csharp
  869. GetNode<HUD>("HUD").ShowGameOver();
  870. Finally, add this to ``_on_ScoreTimer_timeout()`` to keep the display in
  871. sync with the changing score:
  872. .. tabs::
  873. .. code-tab:: gdscript GDScript
  874. $HUD.update_score(score)
  875. .. code-tab:: csharp
  876. GetNode<HUD>("HUD").UpdateScore(_score);
  877. Now you're ready to play! Click the "Play the Project" button. You will
  878. be asked to select a main scene, so choose ``Main.tscn``.
  879. Removing old creeps
  880. ~~~~~~~~~~~~~~~~~~~
  881. If you play until "Game Over" and then start a new game right away, the creeps
  882. from the previous game may still be on the screen. It would be better if they
  883. all disappeared at the start of a new game. We just need a way to tell *all* the
  884. mobs to remove themselves. We can do this with the "group" feature.
  885. In the ``Mob`` scene, select the root node and click the "Node" tab next to the
  886. Inspector (the same place where you find the node's signals). Next to "Signals",
  887. click "Groups" and you can type a new group name and click "Add".
  888. .. image:: img/group_tab.png
  889. Now all mobs will be in the "mobs" group. We can then add the following line to
  890. the ``game_over()`` function in ``Main``:
  891. .. tabs::
  892. .. code-tab:: gdscript GDScript
  893. get_tree().call_group("mobs", "queue_free")
  894. .. code-tab:: csharp
  895. GetTree().CallGroup("mobs", "queue_free");
  896. The ``call_group()`` function calls the named function on every node in a group -
  897. in this case we are telling every mob to delete itself.
  898. Finishing up
  899. ------------
  900. We have now completed all the functionality for our game. Below are some
  901. remaining steps to add a bit more "juice" to improve the game
  902. experience. Feel free to expand the gameplay with your own ideas.
  903. Background
  904. ~~~~~~~~~~
  905. The default gray background is not very appealing, so let's change its
  906. color. One way to do this is to use a :ref:`ColorRect <class_ColorRect>` node.
  907. Make it the first node under ``Main`` so that it will be drawn behind the other
  908. nodes. ``ColorRect`` only has one property: ``Color``. Choose a color
  909. you like and select "Layout" -> "Full Rect" so that it covers the screen.
  910. You could also add a background image, if you have one, by using a
  911. ``TextureRect`` node instead.
  912. Sound effects
  913. ~~~~~~~~~~~~~
  914. Sound and music can be the single most effective way to add appeal to
  915. the game experience. In your game assets folder, you have two sound
  916. files: "House In a Forest Loop.ogg" for background music, and
  917. "gameover.wav" for when the player loses.
  918. Add two :ref:`AudioStreamPlayer <class_AudioStreamPlayer>` nodes as children of ``Main``. Name one of
  919. them ``Music`` and the other ``DeathSound``. On each one, click on the
  920. ``Stream`` property, select "Load", and choose the corresponding audio
  921. file.
  922. To play the music, add ``$Music.play()`` in the ``new_game()`` function
  923. and ``$Music.stop()`` in the ``game_over()`` function.
  924. Finally, add ``$DeathSound.play()`` in the ``game_over()`` function.
  925. Keyboard shortcut
  926. ~~~~~~~~~~~~~~~~~
  927. Since the game is played with keyboard controls, it would be convenient if we
  928. could also start the game by pressing a key on the keyboard. We can do this
  929. with the "Shortcut" property of the ``Button`` node.
  930. In the ``HUD`` scene, select the ``StartButton`` and find its *Shortcut* property
  931. in the Inspector. Select "New Shortcut" and click on the "Shortcut" item. A
  932. second *Shortcut* property will appear. Select "New InputEventAction" and click
  933. the new "InputEventAction". Finally, in the *Action* property, type the name ``ui_select``.
  934. This is the default input event associated with the spacebar.
  935. .. image:: img/start_button_shortcut.png
  936. Now when the start button appears, you can either click it or press :kbd:`Space`
  937. to start the game.
  938. Project files
  939. -------------
  940. You can find a completed version of this project at these locations:
  941. - https://github.com/kidscancode/Godot3_dodge/releases
  942. - https://github.com/godotengine/godot-demo-projects