part_six.rst 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041
  1. .. _doc_fps_tutorial_part_six:
  2. Part 6
  3. ======
  4. Part overview
  5. -------------
  6. In this part, we're going to add a main menu and pause menu,
  7. add a respawn system for the player, and change/move the sound system so we can use it from any script.
  8. This is the last part of the FPS tutorial; by the end of this, you will have a solid base to build amazing FPS games with Godot!
  9. .. image:: img/FinishedTutorialPicture.png
  10. .. note:: You are assumed to have finished :ref:`doc_fps_tutorial_part_five` before moving on to this part of the tutorial.
  11. The finished project from :ref:`doc_fps_tutorial_part_five` will be the starting project for part 6
  12. Let's get started!
  13. Adding the main menu
  14. --------------------
  15. Firstly, open up ``Main_Menu.tscn`` and take a look at how the scene is set up.
  16. The main menu is broken up into three different panels, each representing a different
  17. 'screen' of our main menu.
  18. .. note:: The ``Background_Animation`` node is just so the background of the menu is a bit more interesting than a solid color.
  19. It's a camera looking around the skybox, nothing fancy.
  20. Feel free to expand all the nodes and see how they're set up. Remember to keep only ``Start_Menu`` visible
  21. when you're done, as that's the screen we want to show first when we enter the main menu.
  22. Select ``Main_Menu`` (the root node) and create a new script called ``Main_Menu.gd``. Add the following:
  23. ::
  24. extends Control
  25. var start_menu
  26. var level_select_menu
  27. var options_menu
  28. export (String, FILE) var testing_area_scene
  29. export (String, FILE) var space_level_scene
  30. export (String, FILE) var ruins_level_scene
  31. func _ready():
  32. start_menu = $Start_Menu
  33. level_select_menu = $Level_Select_Menu
  34. options_menu = $Options_Menu
  35. $Start_Menu/Button_Start.connect("pressed", self, "start_menu_button_pressed", ["start"])
  36. $Start_Menu/Button_Open_Godot.connect("pressed", self, "start_menu_button_pressed", ["open_godot"])
  37. $Start_Menu/Button_Options.connect("pressed", self, "start_menu_button_pressed", ["options"])
  38. $Start_Menu/Button_Quit.connect("pressed", self, "start_menu_button_pressed", ["quit"])
  39. $Level_Select_Menu/Button_Back.connect("pressed", self, "level_select_menu_button_pressed", ["back"])
  40. $Level_Select_Menu/Button_Level_Testing_Area.connect("pressed", self, "level_select_menu_button_pressed", ["testing_scene"])
  41. $Level_Select_Menu/Button_Level_Space.connect("pressed", self, "level_select_menu_button_pressed", ["space_level"])
  42. $Level_Select_Menu/Button_Level_Ruins.connect("pressed", self, "level_select_menu_button_pressed", ["ruins_level"])
  43. $Options_Menu/Button_Back.connect("pressed", self, "options_menu_button_pressed", ["back"])
  44. $Options_Menu/Button_Fullscreen.connect("pressed", self, "options_menu_button_pressed", ["fullscreen"])
  45. $Options_Menu/Check_Button_VSync.connect("pressed", self, "options_menu_button_pressed", ["vsync"])
  46. $Options_Menu/Check_Button_Debug.connect("pressed", self, "options_menu_button_pressed", ["debug"])
  47. Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
  48. var globals = get_node("/root/Globals")
  49. $Options_Menu/HSlider_Mouse_Sensitivity.value = globals.mouse_sensitivity
  50. $Options_Menu/HSlider_Joypad_Sensitivity.value = globals.joypad_sensitivity
  51. func start_menu_button_pressed(button_name):
  52. if button_name == "start":
  53. level_select_menu.visible = true
  54. start_menu.visible = false
  55. elif button_name == "open_godot":
  56. OS.shell_open("https://godotengine.org/")
  57. elif button_name == "options":
  58. options_menu.visible = true
  59. start_menu.visible = false
  60. elif button_name == "quit":
  61. get_tree().quit()
  62. func level_select_menu_button_pressed(button_name):
  63. if button_name == "back":
  64. start_menu.visible = true
  65. level_select_menu.visible = false
  66. elif button_name == "testing_scene":
  67. set_mouse_and_joypad_sensitivity()
  68. get_node("/root/Globals").load_new_scene(testing_area_scene)
  69. elif button_name == "space_level":
  70. set_mouse_and_joypad_sensitivity()
  71. get_node("/root/Globals").load_new_scene(space_level_scene)
  72. elif button_name == "ruins_level":
  73. set_mouse_and_joypad_sensitivity()
  74. get_node("/root/Globals").load_new_scene(ruins_level_scene)
  75. func options_menu_button_pressed(button_name):
  76. if button_name == "back":
  77. start_menu.visible = true
  78. options_menu.visible = false
  79. elif button_name == "fullscreen":
  80. OS.window_fullscreen = !OS.window_fullscreen
  81. elif button_name == "vsync":
  82. OS.vsync_enabled = $Options_Menu/Check_Button_VSync.pressed
  83. elif button_name == "debug":
  84. pass
  85. func set_mouse_and_joypad_sensitivity():
  86. var globals = get_node("/root/Globals")
  87. globals.mouse_sensitivity = $Options_Menu/HSlider_Mouse_Sensitivity.value
  88. globals.joypad_sensitivity = $Options_Menu/HSlider_Joypad_Sensitivity.value
  89. Most of the code here relates to making UIs, which is outside of the purpose of this tutorial series.
  90. **We're only going to look at the UI related code briefly.**
  91. .. tip:: See :ref:`doc_ui_main_menu` and the tutorials following for better ways to make GUIs and UIs!
  92. Let's look at the class variables first.
  93. * ``start_menu``: A variable to hold the ``Start_Menu`` :ref:`Panel <class_Panel>`.
  94. * ``level_select_menu``: A variable to hold the ``Level_Select_Menu`` :ref:`Panel <class_Panel>`.
  95. * ``options_menu``: A variable to hold the ``Options_Menu`` :ref:`Panel <class_Panel>`.
  96. * ``testing_area_scene``: The path to the ``Testing_Area.tscn`` file, so we can change to it from this scene.
  97. * ``space_level_scene``: The path to the ``Space_Level.tscn`` file, so we can change to it from this scene.
  98. * ``ruins_level_scene``: The path to the ``Ruins_Level.tscn`` file, so we can change to it from this scene.
  99. .. warning:: You'll have to set the paths to the correct files in the editor before testing this script! Otherwise it will not work!
  100. ______
  101. Now let's go over ``_ready``
  102. Firstly, we get all the :ref:`Panel <class_Panel>` nodes and assign them to the proper variables.
  103. Next, we connect all the buttons ``pressed`` signals to their respective ``[panel_name_here]_button_pressed`` functions.
  104. We then set the mouse mode to ``MOUSE_MODE_VISIBLE`` to ensure whenever the player returns to this scene, the mouse will be visible.
  105. Then we get a singleton, called ``Globals``. We then set the values for the :ref:`HSlider <class_HSlider>` nodes so their values line up with the mouse and joypad sensitivity
  106. in the singleton.
  107. .. note:: We have not made the ``Globals`` singleton yet, so don't worry! We're going to make it soon!
  108. ______
  109. In ``start_menu_button_pressed``, we check to see which button is pressed.
  110. Based on the button pressed, we either change the currently visible panel, quit the application, or open the Godot website.
  111. ______
  112. In ``level_select_menu_button_pressed``, we check to see which button is pressed.
  113. If the ``back`` button has been pressed, we change the currently visible panels to return to the main menu.
  114. If one of the scene changing buttons is pressed, we fist call ``set_mouse_and_joypad_sensitivity`` so the singleton (``Globals.gd``) has the values from the :ref:`HSlider
  115. <class_HSlider>` nodes.
  116. Then, we tell the singleton to change nodes using its ``load_new_scene`` function, passing in the file path of the scene the player has selected.
  117. .. note:: Don't worry about the singleton, we'll get there soon!
  118. ______
  119. In ``options_menu_button_pressed``, we check to see which button is pressed.
  120. If the ``back`` button has been pressed, we change the currently visible panels to return to the main menu.
  121. If the ``fullscreen`` button is pressed, we toggle the :ref:`OS <class_OS>`'s full screen mode by setting it to the flipped version of its current value.
  122. If the ``vsync`` button is pressed, we set the :ref:`OS <class_OS>`'s Vsync based on the state of the Vsync check button.
  123. ______
  124. Finally, lets take a look at ``set_mouse_and_joypad_sensitivity``.
  125. Firstly, we get the ``Globals`` singleton and assign it to a local variable.
  126. We then set the ``mouse_sensitivity`` and ``joypad_sensitivity`` variables to the values in their respective :ref:`HSlider <class_HSlider>` node counterparts.
  127. Making the ``Globals`` singleton
  128. --------------------------------
  129. Now, for all this to work, we need to create the ``Globals`` singleton. Make a new script in the ``Script`` tab and call it ``Globals.gd``.
  130. .. note:: To make the ``Globals`` singleton, go to the ``Script`` tab in the editor, then click ``New`` and a ``Create Script`` box will appear, leave everything unchanged except for the ``Path`` where you need to insert the script's name ``Globals.gd``.
  131. Add the following to ``Globals.gd``.
  132. ::
  133. extends Node
  134. var mouse_sensitivity = 0.08
  135. var joypad_sensitivity = 2
  136. func _ready():
  137. pass
  138. func load_new_scene(new_scene_path):
  139. get_tree().change_scene(new_scene_path)
  140. As you can see, it's quite small and simple. As this part progresses, we will
  141. keep adding more complex logic to ``Globals.gd``, but for now, all it is doing is holding two class variables, and abstract defining how we change scenes.
  142. * ``mouse_sensitivity``: The current sensitivity for our mouse, so we can load it in ``Player.gd``.
  143. * ``joypad_sensitivity``: The current sensitivity for our joypad, so we can load it in ``Player.gd``.
  144. Right now, all we will be using ``Globals.gd`` for is a way to carry variables across scenes. Because the sensitivities of our mouse and joypad are
  145. stored in ``Globals.gd``, any changes we make in one scene (like in ``Options_Menu``) will affect the sensitivity for the player.
  146. All we're doing in ``load_new_scene`` is calling :ref:`SceneTree <class_SceneTree>`'s ``change_scene`` function, passing in the scene path given in ``load_new_scene``.
  147. That's all the code needed for ``Globals.gd`` right now! Before we can test the main menu, we first need to set ``Globals.gd`` as an autoload script.
  148. Open up the ``Project Settings`` and click the ``AutoLoad`` tab.
  149. .. image:: img/AutoloadAddSingleton.png
  150. Then select the path to ``Globals.gd`` in the ``Path`` field by clicking the button (``..``) beside it. Make sure the name in the ``Node Name`` field is ``Globals``. If you
  151. have everything like in the picture above, then press ``Add``!
  152. This will make ``Globals.gd`` a singleton/autoload script, which will allow us to access it from any script, in any scene.
  153. .. tip:: For more information on singleton/autoload scripts, see :ref:`doc_singletons_autoload`.
  154. Now that ``Globals.gd`` is a singleton/autoload script, you can test the main menu!
  155. You may want to change the main scene from ``Testing_Area.tscn`` to ``Main_Menu.tscn`` so when we export the game the player will start at the main menu. You can do this
  156. through the ``Project Settings``, under the ``General`` tab. Then in the ``Application`` category, click the ``Run`` subcategory and you can change the main scene by changing
  157. the value in ``Main Scene``.
  158. .. warning:: You'll have to set the paths to the correct files in ``Main_Menu`` in the editor before testing the main menu!
  159. Otherwise you will not be able to change scenes from the level select menu/screen.
  160. Adding the debug menu
  161. ---------------------
  162. Now, let's add a simple debugging scene so we can track things like FPS (Frames Per Second) in-game. Open up ``Debug_Display.tscn``.
  163. You can see it's a :ref:`Panel <class_Panel>` positioned in the top right corner of the screen. It has three :ref:`Labels <class_Label>`,
  164. one for displaying the FPS at which the game is running, one for showing on what OS the game is running, and a label for showing with which Godot version the game is running.
  165. Let's add the code needed to fill these :ref:`Labels <class_Label>`. Select ``Debug_Display`` and create a new script called ``Debug_Display.gd``. Add the following:
  166. ::
  167. extends Control
  168. func _ready():
  169. $OS_Label.text = "OS: " + OS.get_name()
  170. $Engine_Label.text = "Godot version: " + Engine.get_version_info()["string"]
  171. func _process(delta):
  172. $FPS_Label.text = "FPS: " + str(Engine.get_frames_per_second())
  173. Let's go over what this script does.
  174. ______
  175. In ``_ready``, we set the ``OS_Label``'s text to the name provided by :ref:`OS <class_OS>` using the ``get_name`` function. This will return the
  176. name of the OS (or Operating System) for which Godot was compiled. For example, when you are running Windows, it will return ``Windows``, while when you
  177. are running Linux, it will return ``X11``.
  178. Then, we set the ``Engine_Label``'s text to the version info provided by ``Engine.get_version_info``. ``Engine.get_version_info`` returns a dictionary full
  179. of useful information about the version of Godot which is currently running. We only care about the string version, for this label at least, so we get the string
  180. and assign that as the ``text`` in ``Engine_Label``. See :ref:`Engine <class_Engine>` for more information on the values ``get_version_info`` returns.
  181. In ``_process``, we set the text of the ``FPS_Label`` to ``Engine.get_frames_per_second``, but because ``get_frames_per_second`` returns an integer, we have to cast
  182. it to a string using ``str`` before we can add it to the :ref:`Label <class_Label>`.
  183. ______
  184. Now let's jump back to ``Main_Menu.gd`` and change the following in ``options_menu_button_pressed``:
  185. ::
  186. elif button_name == "debug":
  187. pass
  188. to this instead:
  189. ::
  190. elif button_name == "debug":
  191. get_node("/root/Globals").set_debug_display($Options_Menu/Check_Button_Debug.pressed)
  192. This will call a new function called ``set_debug_display`` in our singleton, so let's add that next!
  193. ______
  194. Open up ``Globals.gd`` and add the following class variables:
  195. ::
  196. # ------------------------------------
  197. # All the GUI/UI-related variables
  198. var canvas_layer = null
  199. const DEBUG_DISPLAY_SCENE = preload("res://Debug_Display.tscn")
  200. var debug_display = null
  201. # ------------------------------------
  202. * ``canvas_layer``: A canvas layer so the GUI/UI created in ``Globals.gd`` is always drawn on top.
  203. * ``DEBUG_DISPLAY``: The debug display scene we worked on earlier.
  204. * ``debug_display``: A variable to hold the debug display when/if there is one.
  205. Now that we have the class variables defined, we need to add a few lines to ``_ready`` so ``Globals.gd`` will have a canvas layer to use (which we will store in ``canvas_layer``).
  206. Change ``_ready`` to the following:
  207. ::
  208. func _ready():
  209. canvas_layer = CanvasLayer.new()
  210. add_child(canvas_layer)
  211. Now in ``_ready``, we create a new canvas layer, assign it to ``canvas_layer`` and add it as a child.
  212. Because ``Globals.gd`` is an autoload/singleton, Godot will make a :ref:`Node <class_Node>` when the game is launched, and it will have ``Globals.gd`` attached to it.
  213. Since Godot makes a :ref:`Node <class_Node>`, we can treat ``Globals.gd`` like any other node with regard to adding/removing children nodes.
  214. The reason we're adding a :ref:`CanvasLayer <class_CanvasLayer>` is so all our GUI and UI nodes we instance/spawn in ``Globals.gd``
  215. are always drawn on top of everything else.
  216. When adding nodes to a singleton/autoload, you have to be careful not to lose reference to any of the child nodes.
  217. This is because nodes will not be freed/destroyed when you change the active scene, meaning you can run into memory problems if you are
  218. instancing/spawning lots of nodes and you are not freeing them.
  219. ______
  220. Now we need to add ``set_debug_display`` to ``Globals.gd``:
  221. ::
  222. func set_debug_display(display_on):
  223. if display_on == false:
  224. if debug_display != null:
  225. debug_display.queue_free()
  226. debug_display = null
  227. else:
  228. if debug_display == null:
  229. debug_display = DEBUG_DISPLAY_SCENE.instance()
  230. canvas_layer.add_child(debug_display)
  231. Let's go over what's happening.
  232. First we check to see if ``Globals.gd`` is trying to turn on the debug display, or turn it off.
  233. If ``Globals.gd`` is turning off the display, we then check to see if ``debug_display`` is not equal to ``null``. If ``debug_display`` is not equal to ``null``, then ``Globals.gd``
  234. must have a debug display currently active. If ``Globals.gd`` has a debug display active, we free it using ``queue_free`` and then assign ``debug_display`` to ``null``.
  235. If ``Globals.gd`` is turning on the display, we then check to make sure ``Globals.gd`` do not already have a debug display active.
  236. We do this by making sure ``debug_display`` is equal to ``null``.
  237. If ``debug_display`` is ``null``, we instance a new ``DEBUG_DISPLAY_SCENE``, and add it as a child of ``canvas_layer``.
  238. ______
  239. With that done, we can now toggle the debug display on and off by switching the :ref:`CheckButton <class_CheckButton>` in the ``Options_Menu`` panel. Go give it a try!
  240. Notice how the debug display stays even when you change scenes from the ``Main_Menu.tscn`` to another scene (like ``Testing_Area.tscn``). This is the beauty of
  241. instancing/spawning nodes in a singleton/autoload and adding them as children to the singleton/autoload. Any of the nodes added as children of the singleton/autoload will
  242. stay for as long as the game is running, without any additional work on our part!
  243. Adding a pause menu
  244. -------------------
  245. Let's add a pause menu so we can return to the main menu when we press the ``ui_cancel`` action.
  246. Open up ``Pause_Popup.tscn``.
  247. Notice how the root node in ``Pause_Popup`` is a :ref:`WindowDialog <class_WindowDialog>`; :ref:`WindowDialog <class_WindowDialog>` inherits from
  248. :ref:`Popup <class_Popup>`, which means :ref:`WindowDialog <class_WindowDialog>` can act like a popup.
  249. Select ``Pause_Popup`` and scroll down all the way till you get to the ``Pause`` menu in the inspector. Notice how the pause mode is set to
  250. ``process`` instead of ``inherit`` like it is normally set by default. This makes it so it will continue to process even when the game is paused,
  251. which we need in order to interact with the UI elements.
  252. Now that we've looked at how ``Pause_Popup.tscn`` is set up, let's write the code to make it work. Normally, we'd attach a script to the root node of
  253. the scene, ``Pause_Popup`` in this case, but since we'll need to receive a couple of signals in ``Globals.gd``, we'll write all the code for
  254. the popup there.
  255. Open up ``Globals.gd`` and add the following class variables:
  256. ::
  257. const MAIN_MENU_PATH = "res://Main_Menu.tscn"
  258. const POPUP_SCENE = preload("res://Pause_Popup.tscn")
  259. var popup = null
  260. * ``MAIN_MENU_PATH``: The path to the main menu scene.
  261. * ``POPUP_SCENE``: The pop up scene we looked at earlier.
  262. * ``popup``: A variable to hold the pop up scene.
  263. Now we need to add ``_process`` to ``Globals.gd`` so it can respond when the ``ui_cancel`` action is pressed.
  264. Add the following to ``_process``:
  265. ::
  266. func _process(delta):
  267. if Input.is_action_just_pressed("ui_cancel"):
  268. if popup == null:
  269. popup = POPUP_SCENE.instance()
  270. popup.get_node("Button_quit").connect("pressed", self, "popup_quit")
  271. popup.connect("popup_hide", self, "popup_closed")
  272. popup.get_node("Button_resume").connect("pressed", self, "popup_closed")
  273. canvas_layer.add_child(popup)
  274. popup.popup_centered()
  275. Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
  276. get_tree().paused = true
  277. Let's go over what's happening here.
  278. ______
  279. Firstly, we check to see if the ``ui_cancel`` action is pressed. Then, we check to make sure ``Globals.gd`` does not already
  280. have a ``popup`` open by checking to see if ``popup`` is equal to ``null``.
  281. If ``Globals.gd`` do not have a pop-up open, we instance ``POPUP_SCENE`` and assign it to ``popup``.
  282. We then get the quit button and assign its ``pressed`` signal to ``popup_quit``, which we will be adding shortly.
  283. Next, we assign both the ``popup_hide`` signal from the :ref:`WindowDialog <class_WindowDialog>` and the ``pressed`` signal from the resume button
  284. to ``popup_closed``, which we will be adding shortly.
  285. Then, we add ``popup`` as a child of ``canvas_layer`` so it's drawn on top. We then tell ``popup`` to pop up at the center of the screen using ``popup_centered``.
  286. Next, we make sure the mouse mode is ``MOUSE_MODE_VISIBLE`` so the player can interact with the pop-up. If we did not do this, the player would not be able to
  287. interact with the pop up in any scene where the mouse mode is ``MOUSE_MODE_CAPTURED``.
  288. Finally, we pause the entire :ref:`SceneTree <class_SceneTree>`.
  289. .. note:: For more information on pausing in Godot, see :ref:`doc_pausing_games`
  290. ______
  291. Now, we need to add the functions to which we've connected the signals. Let's add ``popup_closed`` first.
  292. Add the following to ``Globals.gd``:
  293. ::
  294. func popup_closed():
  295. get_tree().paused = false
  296. if popup != null:
  297. popup.queue_free()
  298. popup = null
  299. ``popup_closed`` will resume the game and destroy the pop-up if there is one.
  300. ``popup_quit`` is similar, but we're also making sure the mouse is visible and changing scenes to the title screen.
  301. Add the following to ``Globals.gd``:
  302. ::
  303. func popup_quit():
  304. get_tree().paused = false
  305. Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
  306. if popup != null:
  307. popup.queue_free()
  308. popup = null
  309. load_new_scene(MAIN_MENU_PATH)
  310. ``popup_quit`` will resume the game, set the mouse mode to ``MOUSE_MODE_VISIBLE`` to ensure the mouse is visible in the main menu, destroy
  311. the pop-up if there is one, and change scenes to the main menu.
  312. ______
  313. Before we're ready to test the pop-up, we should change one thing in ``Player.gd``.
  314. Open up ``Player.gd`` and in ``process_input``, change the code for capturing/freeing the cursor to the following:
  315. Instead of:
  316. ::
  317. # Capturing/Freeing cursor
  318. if Input.is_action_just_pressed("ui_cancel"):
  319. if Input.get_mouse_mode() == Input.MOUSE_MODE_VISIBLE:
  320. Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
  321. else:
  322. Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
  323. You will leave only:
  324. ::
  325. # Capturing/Freeing cursor
  326. if Input.get_mouse_mode() == Input.MOUSE_MODE_VISIBLE:
  327. Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
  328. Now, instead of capturing/freeing the mouse, we check whether the current mouse mode is ``MOUSE_MODE_VISIBLE``. If it is, we set it back to
  329. ``MOUSE_MODE_CAPTURED``.
  330. Because the pop-up makes the mouse mode ``MOUSE_MODE_VISIBLE`` whenever you pause, we no longer have to worry about freeing and capturing the cursor in ``Player.gd``.
  331. ______
  332. Now the pause menu pop-up is finished. You can now pause at any point in the game and return to the main menu!
  333. Starting the respawn system
  334. ---------------------------
  335. Since the player can lose all their health, it would be ideal if the player died and respawned too, so let's add that next!
  336. Firstly, open up ``Player.tscn`` and expand ``HUD``. Notice how there is a :ref:`ColorRect <class_ColorRect>` called ``Death_Screen``.
  337. When the player dies, we're going to make ``Death_Screen`` visible, and show them how long they have to wait before the player is able to respawn.
  338. Open up ``Player.gd`` and add the following class variables:
  339. ::
  340. const RESPAWN_TIME = 4
  341. var dead_time = 0
  342. var is_dead = false
  343. var globals
  344. * ``RESPAWN_TIME``: The amount of time (in seconds) it takes to respawn.
  345. * ``dead_time``: A variable to track how long the player has been dead.
  346. * ``is_dead``: A variable to track whether or not the player is currently dead.
  347. * ``globals``: A variable to hold the ``Globals.gd`` singleton.
  348. ______
  349. We now need to add a couple lines to ``_ready``, so we can use ``Globals.gd`` in ``Player.gd``. Add the following to ``_ready``:
  350. ::
  351. globals = get_node("/root/Globals")
  352. global_transform.origin = globals.get_respawn_position()
  353. Now we're getting the ``Globals.gd`` singleton and assigning it to ``globals``. We also set the player's global position
  354. by setting the origin in the player's global :ref:`Transform <class_Transform>` to the position returned by ``globals.get_respawn_position``.
  355. .. note:: Don't worry, we will be adding ``get_respawn_position`` further below!
  356. ______
  357. Next, we need to make a few changes to ``_physics_process``. Change ``_physics_process`` to the following:
  358. ::
  359. func _physics_process(delta):
  360. if !is_dead:
  361. process_input(delta)
  362. process_view_input(delta)
  363. process_movement(delta)
  364. if (grabbed_object == null):
  365. process_changing_weapons(delta)
  366. process_reloading(delta)
  367. process_UI(delta)
  368. process_respawn(delta)
  369. Now the player will not be processing input or movement input when the player is dead. We are also now calling ``process_respawn``.
  370. .. note:: The ``if !is_dead:`` expression is equivalent and works in the same way as the expression ``if is_dead == false:``. And by removing the ``!`` sign from the expression we obtain the opposite expression ``if is_dead == true:``. It is just a shorter way of writing the same code functionality.
  371. We have not made ``process_respawn`` yet, so let's change that.
  372. ______
  373. Let's add ``process_respawn``. Add the following to ``Player.gd``:
  374. ::
  375. func process_respawn(delta):
  376. # If we've just died
  377. if health <= 0 and !is_dead:
  378. $Body_CollisionShape.disabled = true
  379. $Feet_CollisionShape.disabled = true
  380. changing_weapon = true
  381. changing_weapon_name = "UNARMED"
  382. $HUD/Death_Screen.visible = true
  383. $HUD/Panel.visible = false
  384. $HUD/Crosshair.visible = false
  385. dead_time = RESPAWN_TIME
  386. is_dead = true
  387. if grabbed_object != null:
  388. grabbed_object.mode = RigidBody.MODE_RIGID
  389. grabbed_object.apply_impulse(Vector3(0, 0, 0), -camera.global_transform.basis.z.normalized() * OBJECT_THROW_FORCE / 2)
  390. grabbed_object.collision_layer = 1
  391. grabbed_object.collision_mask = 1
  392. grabbed_object = null
  393. if is_dead:
  394. dead_time -= delta
  395. var dead_time_pretty = str(dead_time).left(3)
  396. $HUD/Death_Screen/Label.text = "You died\n" + dead_time_pretty + " seconds till respawn"
  397. if dead_time <= 0:
  398. global_transform.origin = globals.get_respawn_position()
  399. $Body_CollisionShape.disabled = false
  400. $Feet_CollisionShape.disabled = false
  401. $HUD/Death_Screen.visible = false
  402. $HUD/Panel.visible = true
  403. $HUD/Crosshair.visible = true
  404. for weapon in weapons:
  405. var weapon_node = weapons[weapon]
  406. if weapon_node != null:
  407. weapon_node.reset_weapon()
  408. health = 100
  409. grenade_amounts = {"Grenade":2, "Sticky Grenade":2}
  410. current_grenade = "Grenade"
  411. is_dead = false
  412. Let's go through what this function is doing.
  413. ______
  414. Firstly, we check whether the player has just died by checking if ``health`` is less than or equal to ``0`` and ``is_dead`` is ``false``.
  415. If the player has just died, we disable the collision shapes for the player. We do this to make sure the player is not blocking anything with their dead body.
  416. Next, we set ``changing_weapon`` to ``true`` and set ``changing_weapon_name`` to ``UNARMED``. This is so, if the player is using a weapon, it is put away
  417. when they dies.
  418. We then make the ``Death_Screen`` :ref:`ColorRect <class_ColorRect>` visible so the player gets a nice grey overlay over everything when they have died.
  419. We then make the rest of the UI, the ``Panel`` and ``Crosshair`` nodes, invisible.
  420. Next, we set ``dead_time`` to ``RESPAWN_TIME`` so we can start counting down how long the player has been dead. We also set ``is_dead`` to ``true`` so we know the player has died.
  421. If the player is holding an object when they died, we need to throw it. We first check whether the player is holding an object or not.
  422. If the player is holding a object, we throw it using the same code as the throwing code we added in :ref:`doc_fps_tutorial_part_five`.
  423. .. note:: The ``\n`` combination from the expression ``You have died\n`` is a command used to display the text following after it on a new line below. This is always useful when you want to nicely group displayed text in multiple lines so it looks better and is more readable by the players of your games.
  424. ______
  425. Then we check whether the player is dead. If so, we then remove ``delta`` from ``dead_time``.
  426. We then make a new variable called ``dead_time_pretty``, where we convert ``dead_time`` to a string, using only the first three characters starting from the left. This gives
  427. the player a nice looking string showing how much time the player has left to wait before the player can respawn.
  428. We then change the :ref:`Label <class_Label>` in ``Death_Screen`` to show how much time the player has left.
  429. Next we check to see if the player has waited long enough and can respawn. We do this by checking to see if ``dead_time`` is ``0`` or less.
  430. If the player has waited long enough to respawn, we set the player's position to a new respawn position provided by ``get_respawn_position``.
  431. We then enable both of the player's collision shapes so the player can collide again with the environment.
  432. Next, we make the ``Death_Screen`` invisible and make the rest of the UI, the ``Panel`` and ``Crosshair`` nodes, visible again.
  433. We then go through each weapon and call its ``reset_weapon`` function, which we will add soon.
  434. Then, we reset ``health`` to ``100``, ``grenade_amounts`` to its default values, and change ``current_grenade`` to ``Grenade``.
  435. This effectively resets these variables to their default values.
  436. Finally, we set ``is_dead`` to ``false``.
  437. ______
  438. Before we leave ``Player.gd``, we need to add one quick thing to ``_input``. Add the following at the beginning of ``_input``:
  439. ::
  440. if is_dead:
  441. return
  442. Now, when the player is dead, they cannot look around with the mouse.
  443. Finishing the respawn system
  444. ----------------------------
  445. Firstly, let's open ``Weapon_Pistol.gd`` and add the ``reset_weapon`` function. Add the following:
  446. ::
  447. func reset_weapon():
  448. ammo_in_weapon = 10
  449. spare_ammo = 20
  450. Now, when we call ``reset_weapon``, the ammo in the pistol and the ammo in the spares will be reset to their default values.
  451. Now let's add ``reset_weapon`` in ``Weapon_Rifle.gd``:
  452. ::
  453. func reset_weapon():
  454. ammo_in_weapon = 50
  455. spare_ammo = 100
  456. And add the following to ``Weapon_Knife.gd``:
  457. ::
  458. func reset_weapon():
  459. ammo_in_weapon = 1
  460. spare_ammo = 1
  461. Now all the weapons will reset when the player dies.
  462. ______
  463. Now we need to add a few things to ``Globals.gd``. Firstly, add the following class variable:
  464. ::
  465. var respawn_points = null
  466. * ``respawn_points``: A variable to hold all the respawn points in a level
  467. Because we're getting a random spawn point each time, we need to randomize the number generator. Add the following to ``_ready``:
  468. ::
  469. randomize()
  470. ``randomize`` will get us a new random seed so we get a (relatively) random string of numbers when we use any of the random functions.
  471. Now let's add ``get_respawn_position`` to ``Globals.gd``:
  472. ::
  473. func get_respawn_position():
  474. if respawn_points == null:
  475. return Vector3(0, 0, 0)
  476. else:
  477. var respawn_point = rand_range(0, respawn_points.size() - 1)
  478. return respawn_points[respawn_point].global_transform.origin
  479. Let's go over what this function does.
  480. ______
  481. Firstly, we check if ``Globals.gd`` has any ``respawn_points`` by checking whether ``respawn_points`` is ``null`` or not.
  482. If ``respawn_points`` is ``null``, we return a position of empty :ref:`Vector 3 <class_Vector3>` with the position ``(0, 0, 0)``.
  483. If ``respawn_points`` is not ``null``, we then get a random number between ``0`` and the number of elements we have in ``respawn_points``, minus ``1`` since
  484. most programming languages, including ``GDScript``, start counting from ``0`` when you are accessing elements in a list.
  485. We then return the position of the :ref:`Spatial <class_Spatial>` node at ``respawn_point`` position in ``respawn_points``.
  486. ______
  487. Before we are done with ``Globals.gd``, we need to add the following to ``load_new_scene``:
  488. ::
  489. respawn_points = null
  490. We set ``respawn_points`` to ``null`` so when/if the player gets to a level with no respawn points, we do not respawn the player
  491. at the respawn points that were in the level prior.
  492. ______
  493. Now all we need is a way to set the respawn points. Open up ``Ruins_Level.tscn`` and select ``Spawn_Points``. Add a new script called
  494. ``Respawn_Point_Setter.gd`` and attach it to ``Spawn_Points``. Add the following to ``Respawn_Point_Setter.gd``:
  495. ::
  496. extends Spatial
  497. func _ready():
  498. var globals = get_node("/root/Globals")
  499. globals.respawn_points = get_children()
  500. Now, when a node with ``Respawn_Point_Setter.gd`` has its ``_ready`` function called, all the children
  501. nodes of the node with ``Respawn_Point_Setter.gd``, ``Spawn_Points`` in the case of ``Ruins_Level.tscn``, will be added
  502. to ``respawn_points`` in ``Globals.gd``.
  503. .. warning:: Any node with ``Respawn_Point_Setter.gd`` has to be above the player in the :ref:`SceneTree <class_SceneTree>` so the respawn points are set
  504. before the player needs them in the player's ``_ready`` function.
  505. ______
  506. Now, when the player dies, they will respawn after waiting ``4`` seconds!
  507. .. note:: No spawn points are already set up for any of the levels besides ``Ruins_Level.tscn``!
  508. Adding spawn points to ``Space_Level.tscn`` is left as an exercise for the reader.
  509. Writing a sound system we can use anywhere
  510. ------------------------------------------
  511. Finally, let's make a sound system so we can play sounds from anywhere, without having to use the player.
  512. Firstly, open up ``SimpleAudioPlayer.gd`` and change it to the following:
  513. ::
  514. extends Spatial
  515. var audio_node = null
  516. var should_loop = false
  517. var globals = null
  518. func _ready():
  519. audio_node = $Audio_Stream_Player
  520. audio_node.connect("finished", self, "sound_finished")
  521. audio_node.stop()
  522. globals = get_node("/root/Globals")
  523. func play_sound(audio_stream, position=null):
  524. if audio_stream == null:
  525. print ("No audio stream passed; cannot play sound")
  526. globals.created_audio.remove(globals.created_audio.find(self))
  527. queue_free()
  528. return
  529. audio_node.stream = audio_stream
  530. # If you are using an AudioStreamPlayer3D, then uncomment these lines to set the position.
  531. #if audio_node is AudioStreamPlayer3D:
  532. # if position != null:
  533. # audio_node.global_transform.origin = position
  534. audio_node.play(0.0)
  535. func sound_finished():
  536. if should_loop:
  537. audio_node.play(0.0)
  538. else:
  539. globals.created_audio.remove(globals.created_audio.find(self))
  540. audio_node.stop()
  541. queue_free()
  542. There are several changes from the old version, first and foremost being we are no longer storing the sound files in ``SimpleAudioPlayer.gd`` anymore.
  543. This is much better for performance since we're no longer loading each audio clip when we create a sound, but instead we are forcing an audio stream to be passed
  544. in to ``play_sound``.
  545. Another change is we have a new class variable called ``should_loop``. Instead of just destroying the audio player every time it's finished, we instead want to check and see if the audio player is set to loop or not. This allows us to have audio like looping background music without having to spawn a new audio player with the music when the old one is finished.
  546. Finally, instead of being instanced/spawned in ``Player.gd``, the audio player is instead going to be spawned in ``Globals.gd`` so we can create sounds from any scene.
  547. Now the audio player stores ``Globals.gd`` singleton so when the audio player is destroyed, we can also remove it from a list in ``Globals.gd``.
  548. Let's go over the changes.
  549. ______
  550. For the class variables, we removed all the ``audio_[insert name here]`` variables since we will instead have these passed in from ``Globals.gd``.
  551. We also added two new class variables, ``should_loop`` and ``globals``. We'll use ``should_loop`` to tell whether the audio player should loop when the sound has
  552. finished, and ``globals`` will hold the ``Globals.gd`` singleton.
  553. The only change in ``_ready`` is now audio player is getting the ``Globals.gd`` singleton and assigning it to ``globals``.
  554. ``play_sound`` now expects an audio stream, named ``audio_stream``, to be passed in, instead of ``sound_name``. Instead of checking the
  555. sound name and setting the stream for the audio player, we instead check to make sure an audio stream was passed in. If an audio stream was not passed
  556. in, we print an error message, remove the audio player from a list in the ``Globals.gd`` singleton called ``created_audio``, and then free the audio player.
  557. Finally, in ``sound_finished`` we first check to see if the audio player is supposed to loop or not using ``should_loop``. If the audio player is supposed to loop,
  558. we play the sound again from the start, at position ``0.0``. If the audio player is not supposed to loop, we remove the audio player from a list in the ``Globals.gd`` singleton
  559. called ``created_audio``, and then free the audio player.
  560. ______
  561. Now that we've finished our changes to ``SimpleAudioPlayer.gd``, we now need to turn our attention to ``Globals.gd``. First, add the following class variables:
  562. ::
  563. # All the audio files.
  564. # You will need to provide your own sound files.
  565. var audio_clips = {
  566. "Pistol_shot": null, #preload("res://path_to_your_audio_here!")
  567. "Rifle_shot": null, #preload("res://path_to_your_audio_here!")
  568. "Gun_cock": null, #preload("res://path_to_your_audio_here!")
  569. }
  570. const SIMPLE_AUDIO_PLAYER_SCENE = preload("res://Simple_Audio_Player.tscn")
  571. var created_audio = []
  572. Let's go over these global variables.
  573. * ``audio_clips``: A dictionary holding all the audio clips ``Globals.gd`` can play.
  574. * ``SIMPLE_AUDIO_PLAYER_SCENE``: The simple audio player scene.
  575. * ``created_audio``: A list to hold all the simple audio players ``Globals.gd`` has created.
  576. .. note:: If you want to add additional audio, you need to add it to ``audio_clips``. No audio files are provided in this tutorial,
  577. so you will have to provide your own.
  578. One site I'd recommend is **GameSounds.xyz**.
  579. I'm using the Gamemaster audio gun sound pack included in the Sonniss' GDC Game Audio bundle for 2017.
  580. The tracks I've used (with some minor editing) are as follows:
  581. * gun_revolver_pistol_shot_04,
  582. * gun_semi_auto_rifle_cock_02,
  583. * gun_submachine_auto_shot_00_automatic_preview_01
  584. ______
  585. Now we need to add a new function called ``play_sound`` to ``Globals.gd``:
  586. ::
  587. func play_sound(sound_name, loop_sound=false, sound_position=null):
  588. if audio_clips.has(sound_name):
  589. var new_audio = SIMPLE_AUDIO_PLAYER_SCENE.instance()
  590. new_audio.should_loop = loop_sound
  591. add_child(new_audio)
  592. created_audio.append(new_audio)
  593. new_audio.play_sound(audio_clips[sound_name], sound_position)
  594. else:
  595. print ("ERROR: cannot play sound that does not exist in audio_clips!")
  596. Let's go over what this function does.
  597. Firstly, we check whether ``Globals.gd`` has an audio clip with the name ``sound_name`` in ``audio_clips``. If it does not, we print an error message.
  598. If ``Globals.gd`` has an audio clip with the name ``sound_name``, we then instance/spawn a new ``SIMPLE_AUDIO_PLAYER_SCENE`` and assign it to ``new_audio``.
  599. We then set ``should_loop``, and add ``new_audio`` as a child of ``Globals.gd``.
  600. .. note:: Remember, we have to be careful adding nodes to a singleton, since these nodes will not be destroyed when changing scenes.
  601. We add the ``new_audio`` into the ``created_audio`` list to hold all created audios.
  602. We then call ``play_sound``, passing in the audio clip associated with ``sound_name`` and the sound position.
  603. ______
  604. Before we leave ``Globals.gd``, we need to add a few lines of code to ``load_new_scene`` so when the player changes scenes, all the audio is destroyed.
  605. Add the following to ``load_new_scene``:
  606. ::
  607. for sound in created_audio:
  608. if (sound != null):
  609. sound.queue_free()
  610. created_audio.clear()
  611. Now, before ``Globals.gd`` changes scenes, it goes through each simple audio player in ``created_sounds`` and frees/destroys them. Once ``Globals.gd`` has gone through
  612. all the sounds in ``created_audio``, we clear ``created_audio`` so it no longer holds any references to any (now freed/destroyed) simple audio players.
  613. ______
  614. Let's change ``create_sound`` in ``Player.gd`` to use this new system. First, remove ``simple_audio_player`` from ``Player.gd``'s class variables since we will
  615. no longer be directly instancing/spawning sounds in ``Player.gd``.
  616. Now, change ``create_sound`` to the following:
  617. ::
  618. func create_sound(sound_name, position=null):
  619. globals.play_sound(sound_name, false, position)
  620. Now, whenever ``create_sound`` is called, we simply call ``play_sound`` in ``Globals.gd``, passing in all the arguments received.
  621. ______
  622. Now all the sounds in our FPS can be played from anywhere. All we have to do is get the ``Globals.gd`` singleton, and call ``play_sound``, pass in the name of the sound
  623. we want to play, whether we want it to loop or not, and the position from which to play the sound.
  624. For example, if you want to play an explosion sound when the grenade explodes you'd need to add a new sound to ``audio_clips`` in ``Globals.gd``,
  625. get the ``Globals.gd`` singleton, and then you just need to add something like
  626. ``globals.play_sound("explosion", false, global_transform.origin)`` in the grenades
  627. ``_process`` function, right after the grenade damages all the bodies within its blast radius.
  628. Final notes
  629. -----------
  630. .. image:: img/FinishedTutorialPicture.png
  631. Now you have a fully working single player FPS!
  632. At this point, you have a good base to build more complicated FPS games.
  633. .. warning:: If you ever get lost, be sure to read over the code again!
  634. You can download the finished project for the entire tutorial here: :download:`Godot_FPS_Part_6.zip <files/Godot_FPS_Finished.zip>`
  635. .. note:: The finished project source files contain the same code, just written in a different order.
  636. This is because the finished project source files are what the tutorial is based on.
  637. The finished project code was written in the order that features were created, not necessarily
  638. in a order that is ideal for learning.
  639. Other than that, the source is exactly the same, just with helpful comments explaining what
  640. each part does.
  641. .. tip:: The finished project source is hosted on GitHub as well: https://github.com/TwistedTwigleg/Godot_FPS_Tutorial
  642. **Please note that the code in GitHub may or may not be in sync with the tutorial in the documentation**.
  643. The code in the documentation is likely better managed and/or more up to date.
  644. If you are unsure of which to use, use the project(s) provided in the documentation, as they are maintained by the Godot community.
  645. You can download all the ``.blend`` files used in this tutorial here: :download:`Godot_FPS_BlenderFiles.zip <files/Godot_FPS_BlenderFiles.zip>`
  646. All assets provided in the started assets (unless otherwise noted) were **originally created by TwistedTwigleg, with changes/additions by the Godot community.**
  647. All original assets provided for this tutorial are released under the ``MIT`` license.
  648. Feel free to use these assets however you want! All original assets belong to the Godot community, with the other assets belonging to those listed below:
  649. The skybox is created by **StumpyStrust** and can be found at OpenGameArt.org. https://opengameart.org/content/space-skyboxes-0
  650. . The skybox is licensed under the ``CC0`` license.
  651. The font used is **Titillium-Regular**, and is licensed under the ``SIL Open Font License, Version 1.1``.
  652. The skybox was converted to a 360 equirectangular image using this tool: https://www.360toolkit.co/convert-cubemap-to-spherical-equirectangular.html
  653. While no sounds are provided, you can find many game ready sounds at https://gamesounds.xyz/
  654. .. warning:: **OpenGameArt.org, 360toolkit.co, the creator(s) of Titillium-Regular, StumpyStrust, and GameSounds.xyz are in no way involved in this tutorial.**