diff --git a/tutorials/3d/fps_tutorial/files/Godot_FPS_BlenderFiles.zip b/tutorials/3d/fps_tutorial/files/Godot_FPS_BlenderFiles.zip new file mode 100644 index 000000000..c706e2a04 Binary files /dev/null and b/tutorials/3d/fps_tutorial/files/Godot_FPS_BlenderFiles.zip differ diff --git a/tutorials/3d/fps_tutorial/files/Godot_FPS_Finished.zip b/tutorials/3d/fps_tutorial/files/Godot_FPS_Finished.zip new file mode 100644 index 000000000..e48ae3ec6 Binary files /dev/null and b/tutorials/3d/fps_tutorial/files/Godot_FPS_Finished.zip differ diff --git a/tutorials/3d/fps_tutorial/files/Godot_FPS_Starter.zip b/tutorials/3d/fps_tutorial/files/Godot_FPS_Starter.zip new file mode 100644 index 000000000..b06214b7b Binary files /dev/null and b/tutorials/3d/fps_tutorial/files/Godot_FPS_Starter.zip differ diff --git a/tutorials/3d/fps_tutorial/img/AnimationPlayerAddPoint.png b/tutorials/3d/fps_tutorial/img/AnimationPlayerAddPoint.png new file mode 100644 index 000000000..5e30ff535 Binary files /dev/null and b/tutorials/3d/fps_tutorial/img/AnimationPlayerAddPoint.png differ diff --git a/tutorials/3d/fps_tutorial/img/AnimationPlayerAddTrack.png b/tutorials/3d/fps_tutorial/img/AnimationPlayerAddTrack.png new file mode 100644 index 000000000..db0181351 Binary files /dev/null and b/tutorials/3d/fps_tutorial/img/AnimationPlayerAddTrack.png differ diff --git a/tutorials/3d/fps_tutorial/img/AnimationPlayerCallFuncTrack.png b/tutorials/3d/fps_tutorial/img/AnimationPlayerCallFuncTrack.png new file mode 100644 index 000000000..c5cca9e8b Binary files /dev/null and b/tutorials/3d/fps_tutorial/img/AnimationPlayerCallFuncTrack.png differ diff --git a/tutorials/3d/fps_tutorial/img/AnimationPlayerEditPoints.png b/tutorials/3d/fps_tutorial/img/AnimationPlayerEditPoints.png new file mode 100644 index 000000000..669df01f9 Binary files /dev/null and b/tutorials/3d/fps_tutorial/img/AnimationPlayerEditPoints.png differ diff --git a/tutorials/3d/fps_tutorial/img/FinishedTutorialPicture.png b/tutorials/3d/fps_tutorial/img/FinishedTutorialPicture.png new file mode 100644 index 000000000..9490f1a4f Binary files /dev/null and b/tutorials/3d/fps_tutorial/img/FinishedTutorialPicture.png differ diff --git a/tutorials/3d/fps_tutorial/img/LocalSpaceExample.png b/tutorials/3d/fps_tutorial/img/LocalSpaceExample.png new file mode 100644 index 000000000..61e6a8e71 Binary files /dev/null and b/tutorials/3d/fps_tutorial/img/LocalSpaceExample.png differ diff --git a/tutorials/3d/fps_tutorial/img/LocalSpaceExampleGizmo.png b/tutorials/3d/fps_tutorial/img/LocalSpaceExampleGizmo.png new file mode 100644 index 000000000..bb24fc834 Binary files /dev/null and b/tutorials/3d/fps_tutorial/img/LocalSpaceExampleGizmo.png differ diff --git a/tutorials/3d/fps_tutorial/img/LocalSpaceExample_3D.png b/tutorials/3d/fps_tutorial/img/LocalSpaceExample_3D.png new file mode 100644 index 000000000..3fa07c3d4 Binary files /dev/null and b/tutorials/3d/fps_tutorial/img/LocalSpaceExample_3D.png differ diff --git a/tutorials/3d/fps_tutorial/img/PartOneFinished.png b/tutorials/3d/fps_tutorial/img/PartOneFinished.png new file mode 100644 index 000000000..45595bc86 Binary files /dev/null and b/tutorials/3d/fps_tutorial/img/PartOneFinished.png differ diff --git a/tutorials/3d/fps_tutorial/img/PartThreeFinished.png b/tutorials/3d/fps_tutorial/img/PartThreeFinished.png new file mode 100644 index 000000000..1fed43dde Binary files /dev/null and b/tutorials/3d/fps_tutorial/img/PartThreeFinished.png differ diff --git a/tutorials/3d/fps_tutorial/img/PartTwoFinished.png b/tutorials/3d/fps_tutorial/img/PartTwoFinished.png new file mode 100644 index 000000000..df89db708 Binary files /dev/null and b/tutorials/3d/fps_tutorial/img/PartTwoFinished.png differ diff --git a/tutorials/3d/fps_tutorial/img/PlayerSceneTree.png b/tutorials/3d/fps_tutorial/img/PlayerSceneTree.png new file mode 100644 index 000000000..322517137 Binary files /dev/null and b/tutorials/3d/fps_tutorial/img/PlayerSceneTree.png differ diff --git a/tutorials/3d/fps_tutorial/img/WorldSpaceExample.png b/tutorials/3d/fps_tutorial/img/WorldSpaceExample.png new file mode 100644 index 000000000..ceae55dae Binary files /dev/null and b/tutorials/3d/fps_tutorial/img/WorldSpaceExample.png differ diff --git a/tutorials/3d/fps_tutorial/img/WorldSpaceExample_3D.png b/tutorials/3d/fps_tutorial/img/WorldSpaceExample_3D.png new file mode 100644 index 000000000..08972c776 Binary files /dev/null and b/tutorials/3d/fps_tutorial/img/WorldSpaceExample_3D.png differ diff --git a/tutorials/3d/fps_tutorial/index.rst b/tutorials/3d/fps_tutorial/index.rst new file mode 100644 index 000000000..41331edf8 --- /dev/null +++ b/tutorials/3d/fps_tutorial/index.rst @@ -0,0 +1,10 @@ +FPS tutorial +============ + +.. toctree:: + :maxdepth: 1 + :name: toc-3D-fps-tutorial + + part_one + part_two + part_three diff --git a/tutorials/3d/fps_tutorial/part_one.rst b/tutorials/3d/fps_tutorial/part_one.rst new file mode 100644 index 000000000..34927d7f2 --- /dev/null +++ b/tutorials/3d/fps_tutorial/part_one.rst @@ -0,0 +1,556 @@ +.. _doc_fps_tutorial_part_one: + +Part 1 +====== + +Tutorial introduction +--------------------- + +.. image:: img/FinishedTutorialPicture.png + +This tutorial series will show you how to make a single player FPS game. + +Throughout the course of these tutorials, we will cover how: + +- To make a first person character, with sprinting and a flash light. +- To make a simple animation state machine for handling animation transitions. +- To add a pistol, rifle, and knife to the first person character. +- To add ammo and reloading to weapons that consume ammo. +- To add sounds that play when the guns fire. + +.. note:: While this tutorial can be completed by beginners, it is highly + advised to complete :ref:`doc_your_first_game`, + if you are new to Godot and/or game development **before** going through + this tutorial series. + + Remember: Making 3D games is much harder than making 2D games. If you do not know + how to make 2D games you will likely struggle making 3D games. + + This tutorial assumes you know have experience working with the Godot editor, + have basic programming experience in GDScript, and have basic experience in game development. + +You can find the start assets for this parts 1 through 3 here: :download:`Godot_FPS_Starter.zip ` + +.. warning:: A video version of this tutorial series is coming soon! + +The provided starter assets contain a animated 3D model, a bunch of 3D models for making levels, +and a few scenes already configured for this tutorial. + +All assets provided are created by me (TwistedTwigleg) unless otherwise noted, and are +released under the ``MIT`` license. + +.. note:: The skybox is created by **StumpyStrust** on OpenGameArt. The skybox used is + licensed under ``CC0``. + + The font used is **Titillium-Regular**, and is licensed under the ``SIL Open Font License, Version 1.1``. + +.. note:: You can find the finished project for parts 1 through 3 at the bottom of + :ref:`doc_fps_tutorial_part_three`. + +Part Overview +------------- + +In this part we will be making a first person player that can move around +the environment. + +.. image:: img/PartOneFinished.png + +By the end of this part you will have a working first person character with a +mouse based camera that can walk, jump, and sprint around the game environment in +any direction + +Getting everything setup +------------------------ +Launch Godot and open up the project included in the starter assets. + +.. note:: While these assets are not necessarily required to use the scripts provided in this tutorial, + they will make the tutorial much easier to follow as there are several pre-setup scenes we + will be using throughout the tutorial series. + +First, go open the project settings and go to the "Input Map" tab. You'll find several +actions have already been defined. We will be using these actions for our player. +Feel free to change the keys bound to these actions if you want. + +While we still have the project settings open, quickly go check if MSAA (MultiSample Anti-Aliasing) +is turned off. We want to make sure MSAA is off because otherwise we will get strange red lines +between the tiles in our level later. + +.. tip:: The reason we get those red lines is because we are using lowpoly models + with low resolution textures. MSAA tries to reduce jagged edges between models and + because we are using lowpoly and low resolution textures in this project, + we need to turn it off to avoid texture bleeding. + + A bonus with turning off MSAA is we get a more 'retro' looking result. + +_________ + +Lets take a second to see what we have in the starter assets. + +Included in the starter assets are five scenes: ``BulletScene.tscn``, ``Player.tscn``, +``SimpleAudioPlayer.tscn``, ``TestingArea.tscn``, and ``TestLevel.tscn``. + +We will visit all of these scenes later, but for now open up ``Player.tscn``. + +.. note:: There are a bunch of scenes and a few textures in the ``Assets`` folder. You can look at these if you want, + but we will not be directly using them in this tutorial. + +Making the FPS movement logic +----------------------------- + +Once you have ``Player.tscn`` open, let's take a quick look at how it is setup + +.. image:: img/PlayerSceneTree.png + +First, notice how the player's collision shapes are setup. Using a vertical pointing +capsule as the collision shape for the player is fairly common in most first person games. + +We are adding a small square to the 'feet' of the player so the player does not +feel like they are balancing on a single point. + +.. note:: Many times player will notice how the collision shape being circular when + they walk to an edge and slide off. We are adding the small square at the + bottom of the capsule to reduce sliding on, and around, edges. + +Another thing to notice is how many nodes are children of ``Rotation_helper``. This is because +``Rotation_helper`` contains all of the nodes we want to rotate on the ``X`` axis (up and down). +The reason behind this is so we rotate ``Player`` on the ``Y`` axis, and ``Rotation_helper`` on +the ``X`` axis. + +.. note:: If we did not use ``Rotation_helper`` then we'd likely have cases where we are rotating + both the ``X`` and ``Y`` axes at the same time. This can lead to undesirable results, as we then + could rotate on all three axes in some cases. + +_________ + +Attach a new script to the ``Player`` node and call it ``Player.gd``. + +Lets programming our player by adding the ability to move around, look around with the mouse, and jump. +Add the following code to ``Player.gd``: + +:: + + extends KinematicBody + + const norm_grav = -24.8 + var vel = Vector3() + const MAX_SPEED = 20 + const JUMP_SPEED = 18 + const ACCEL = 3.5 + + const DEACCEL= 16 + const MAX_SLOPE_ANGLE = 40 + + var camera + var camera_holder + + # You may need to adjust depending on the sensitivity of your mouse + const MOUSE_SENSITIVITY = 0.05 + + var flashlight + + func _ready(): + camera = get_node("Rotation_helper/Camera") + camera_holder = get_node("Rotation_helper") + + set_physics_process(true) + + Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) + set_process_input(true) + + flashlight = get_node("Rotation_helper/Flashlight") + + func _physics_process(delta): + var dir = Vector3() + var cam_xform = camera.get_global_transform() + + if Input.is_action_pressed("movement_forward"): + dir += -cam_xform.basis.z.normalized() + if Input.is_action_pressed("movement_backward"): + dir += cam_xform.basis.z.normalized() + if Input.is_action_pressed("movement_left"): + dir += -cam_xform.basis.x.normalized() + if Input.is_action_pressed("movement_right"): + dir += cam_xform.basis.x.normalized() + + if is_on_floor(): + if Input.is_action_just_pressed("movement_jump"): + vel.y = JUMP_SPEED + + if Input.is_action_just_pressed("flashlight"): + if flashlight.is_visible_in_tree(): + flashlight.hide() + else: + flashlight.show() + + dir.y = 0 + dir = dir.normalized() + + var grav = norm_grav + vel.y += delta*grav + + var hvel = vel + hvel.y = 0 + + var target = dir + target *= MAX_SPEED + + var accel + if dir.dot(hvel) > 0: + accel = ACCEL + else: + accel = DEACCEL + + hvel = hvel.linear_interpolate(target, accel*delta) + vel.x = hvel.x + vel.z = hvel.z + vel = move_and_slide(vel,Vector3(0,1,0), 0.05, 4, deg2rad(MAX_SLOPE_ANGLE)) + + # (optional, but highly useful) Capturing/Freeing the cursor + if Input.is_action_just_pressed("ui_cancel"): + if Input.get_mouse_mode() == Input.MOUSE_MODE_VISIBLE: + Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) + else: + Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) + + func _input(event): + if event is InputEventMouseMotion && Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED: + camera_holder.rotate_x(deg2rad(event.relative.y * MOUSE_SENSITIVITY)) + self.rotate_y(deg2rad(event.relative.x * MOUSE_SENSITIVITY * -1)) + + var camera_rot = camera_holder.rotation_degrees + camera_rot.x = clamp(camera_rot.x, -70, 70) + camera_holder.rotation_degrees = camera_rot + +This is a lot of code, so let's break it down from top to bottom: + +_________ + +First, we define some global variables to dictate how our player will move about the world. + +.. note:: Throughout this tutorial, *variables defined outside functions will be + referred to as "global variables"*. This is because we can access any of these + variables from any place in the script. We can "globally" access them, hence the + name. + +Lets go through each of the global variables: + +- ``norm_grav``: How strong gravity pulls us down while we are walking. +- ``vel``: Our :ref:`KinematicBody `'s velocity. +- ``MAX_SPEED``: The fastest speed we can reach. Once we hit this speed, we will not go any faster. +- ``JUMP_SPEED``: How high we can jump. +- ``ACCEL``: How fast we accelerate. The higher the value, the faster we get to max speed. +- ``DEACCEL``: How fast we are going to decelerate. The higher the value, the faster we will come to a complete stop. +- ``MAX_SLOPE_ANGLE``: The steepest angle we can climb. +- ``camera``: The :ref:`Camera ` node. +- ``rotation_helper``: A :ref:`Spatial ` node holding everything we want to rotate on the X axis (up and down). +- ``MOUSE_SENSITIVITY``: How sensitive the mouse is. I find a value of ``0.05`` works well for my mouse, but you may need to change it based on how sensitive your mouse is. +- ``flashlight``: A :ref:`Spotlight ` node that will act as our player's flashlight. + +You can tweak many of these variables to get different results. For example, by lowering ``normal_gravity`` and/or +increasing ``JUMP_SPEED`` you can get a more 'floaty' feeling character. +Feel free to experiment! + +_________ + +Now lets look at the ``_ready`` function: + +First we get the ``camera`` and ``rotation_helper`` nodes and store them into their variables. +After that we set ``_physics_process`` to ``true``. Then we need to set the mouse mode to captured. + +This will hide the mouse and keep it at the center of the screen. We do this for two reasons: +The first reason being we do not want to the player to see their mouse cursor as they play. +The second reason is because we do not want the cursor to leave the game window. If the cursor leaves +the game window there could be instances where the player clicks outside the window, and then the game +would lose focus. To assure neither of these issues happen, we capture the mouse cursor. + +.. note:: see :ref:`Input documentation ` for the various mouse modes. We will only be using + ``MOUSE_MODE_CAPTURED`` and ``MOUSE_MODE_VISIBLE`` in this tutorial series. + +Finally, we call ``set_process_input(true)``. We need to use ``_input`` so we can rotate the player and +camera when there is mouse motion. + +_________ + +Next is ``_physics_process``: + +We define a directional vector (``dir``) for storing the direction the player intends to move. + +Next we get the camera's global transform and store it as well, into the ``cam_xform`` variable. + +Now we check for directional input. If we find that the player is moving, we get the ``camera``'s directional +vector in the direction we are wanting to move towards and add (or subtract) it to ``dir``. + +Many have found directional vectors confusing, so lets take a second to explain how they work: + +_________ + +World space can be defined as: The space in which all objects are placed in, relative to a constant origin point. +Every object, no matter if it is 2D or 3D, has a position in world space. + +To put it another way: world space is the space in a universe where every object's position, rotation, and scale +can be measured by a known, fixed point called the origin. + +In Godot, the origin is at position ``(0, 0, 0)`` with a rotation of ``(0, 0, 0)`` and a scale of ``(1, 1, 1)``. + +.. note:: When you open up the Godot editor and select a :ref:`Spatial ` based node, a gizmo pops up. + Each of the arrows points using world space directions by default. + +If you want to move using the world space directional vectors, you'd do something like this: + +:: + + if Input.is_action_pressed("movement_forward"): + node.translate(Vector3(0, 0, 1)) + if Input.is_action_pressed("movement_backward"): + node.translate(Vector3(0, 0, -1)) + if Input.is_action_pressed("movement_left"): + node.translate(Vector3(1, 0, 0)) + if Input.is_action_pressed("movement_right"): + node.translate(Vector3(-1, 0, 0)) + +.. note:: Notice how we do not need to do any calculations to get world space directional vectors. + We can just define a few :ref:`Vector3 ` variables and input the values pointing in each direction. + +Here is what world space looks like in 2D: + +.. note:: The following images are just examples. Each arrow/rectangle represents a directional vector + +.. image:: img/WorldSpaceExample.png + +And here is what it looks like for 3D: + +.. image:: img/WorldSpaceExample_3D.png + +Notice how in both examples, the rotation of the node does not change the directional arrows. +This is because world space is a constant. No matter how you translate, rotate, or scale an object, world +space will *always point in the same direction*. + +Local space is different, because it takes the rotation of the object into account. + +Local space can be defined as follows: +The space in which a object's position is the origin of the universe. Because the position +of the origin can be at ``N`` many locations, the values derived from local space change +with the position of the origin. + +.. note:: This stack overflow question has a much better explanation of world space and local space. + + https://gamedev.stackexchange.com/questions/65783/what-are-world-space-and-eye-space-in-game-development + (Local space and eye space are essentially the same thing in this context) + +To get a :ref:`Spatial ` node's local space, we need to get its :ref:`Transform `, so then we +can get the :ref:`Basis ` from the :ref:`Transform `. + +Each :ref:`Basis ` has three vectors: ``X``, ``Y``, and ``Z``. +Each of those vectors point towards each of the local space vectors coming from that object. + +To use the a :ref:`Spatial ` node's local directional vectors, we use this code: + +:: + + if Input.is_action_pressed("movement_forward"): + node.translate(node.global_transform.basis.z.normalized()) + if Input.is_action_pressed("movement_backward"): + node.translate(-node.global_transform.basis.z.normalized()) + if Input.is_action_pressed("movement_left"): + node.translate(node.global_transform.basis.x.normalized()) + if Input.is_action_pressed("movement_right"): + node.translate(-node.global_transform.basis.x.normalized()) + +Here is what local space looks like in 2D: + +.. image:: img/LocalSpaceExample.png + +And here is what it looks like for 3D: + +.. image:: img/LocalSpaceExample_3D.png + +Here is what the :ref:`Spatial ` gizmo shows when you are using local space mode. +Notice how the arrows follow the rotation of the object on the left, which looks exactly +the same as the 3D example for local space. + +.. note:: You can change between local and world space modes by pressing the little cube button + when you have a :ref:`Spatial ` based node selected. + +.. image:: img/LocalSpaceExampleGizmo.png + +Local vectors are confusing even for more experienced game developers, so do not worry if this all doesn't make a +lot of sense. The key thing to remember about local vectors is that we are using local coordinates to get direction +from the object's point of view, as opposed to using world vectors which give direction from the world's point of view. + +_________ + +Back to ``_physics_process``: + +When the player pressed any of the directional movement actions, we get the local vector pointing in that direction +and add it to ``dir``. + +.. note:: Because the camera is rotated by ``-180`` degrees, we have to flip the directional vectors. + Normally forward would be the positive Z axis, so using ``basis.z.normalized()`` would work, + but we are using ``-basis.z.normalized()`` because our camera's Z axis faces backwards in relation + to the rest of the player. + +Next we check if the player is on the floor using :ref:`KinematicBody `'s ``is_on_floor`` function. If it is, then we +check to see if the "movement_jump" action has just been pressed. If it has, then we set our ``Y`` velocity to +``JUMP_SPEED``. + +Next we check if the flash light action was just pressed. If it was, we then check if the flash light +is visible, or hidden. If it is visible, we hide it. If it is hidden, we make it visible. + +Next we assure that our movement vector does not have any movement on the ``Y`` axis, and then we normalize it. +We set a variable to our normal gravity and apply that gravity to our velocity. + +.. note:: If you are wondering why we are seemingly needlessly assigning a new variable + for gravity, it is because later we will be changing gravity. + +After that we assign our velocity to a new variable (called ``hvel``) and remove any movement on the ``Y`` axis. +Next we set a new variable (``target``) to our direction vector. Then we multiply that by our max speed +so we know how far we will can move in the direction provided by ``dir``. + +After that we make a new variable for our acceleration, named ``accel``. We then take the dot product +of ``hvel`` to see if we are moving according to ``hvel``. Remember, ``hvel`` does not have any +``Y`` velocity, meaning we are only checking if we are moving forwards, backwards, left, or right. +If we are moving, then we set ``accel`` to our ``ACCEL`` constant so we accelerate, otherwise we set ``accel` to +our ``DEACCEL`` constant so we decelerate. + +Finally, we interpolate our horizontal velocity, set our ``X`` and ``Z`` velocity to the interpolated horizontal velocity, +and then call ``move_and_slide`` to let the :ref:`KinematicBody ` handle moving through the physics world. + +.. tip:: All of the code in ``_physics_process`` is almost exactly the same as the movement code from the Kinematic Character demo! + The only thing that is different is how we use the directional vectors, and the flash light! + +You can optionally add some code to capture and free the mouse cursor when "ui_cancel" is +pressed. While entirely optional, it is highly recommended for debugging purposes. + +_________ + +The final function we have is the ``_input`` function, and thankfully it's fairly short: + +First we make sure that the event we are dealing with is a :ref:`InputEventMouseMotion ` event. +We also want to check if the cursor is captured, as we do not want to rotate if it is not. + +.. tip:: See :ref:`Mouse and input coordinates ` for a list of + possible input events. + +If the event is indeed a mouse motion event and the cursor is captured, we rotate +based on the mouse motion provided by :ref:`InputEventMouseMotion `. + +First we rotate the ``rotation_helper`` node on the ``X`` axis, using the relative mouse motion's +``Y`` value, provided by :ref:`InputEventMouseMotion `. + +Then we rotate the entire :ref:`KinematicBody ` on the ``Y`` axis by the relative mouse motion's ``X`` value. + +.. tip:: Godot converts relative mouse motion into a :ref:`Vector2 ` where mouse movement going + up and down is ``1`` and ``-1`` respectively. Right and Left movement is + ``1`` and ``-1`` respectively. + + Because of how we are rotating the player, we multiply the relative mouse motion's + ``X`` value by ``-1`` so mouse motion going left and right rotates the player left and right + in the same direction. + +Finally, we clamp the ``rotation_helper``'s ``X`` rotation to be between ``-70`` and ``70`` +degrees so we cannot rotate ourselves upside down. + +_________ + +To test the code open up the scene named ``Testing_Area.tscn`` if it's not already opened up. We will be using +this scene as we go through the tutorial, so be sure to keep it open in one of your scene tabs. + +Go ahead and test your code either by pressing ``F4`` with ``Testing_Area.tscn`` as the open tab, by pressing the +play button in the top right corner, or by pressing ``F6``. +You should now be able to walk around, jump in the air, and look around using the mouse. + +Giving the player the option to sprint +-------------------------------------- + +Before we get to making the weapons work, there is one more thing we should add. +Many FPS games have an option to sprint, and we can easily add sprinting to our player, +so let's do that! + +First we need a few more global variables in our player script: + +:: + + const sprint_grav = -30.8 + const MAX_SPRINT_SPEED = 30 + const SPRINT_ACCEL = 18 + const SPRINT_JUMP_SPEED = 24 + var is_sprinting = false + +All of these variables work exactly the same as the non sprinting variables with +similar names. The only that's different is ``is_sprinting``, which is a boolean to track +whether the player is currently sprinting. + +Now we just need to change some of the code in our ``_physics_process`` function +so we can add the ability to sprint. + +First, we want to change gravity when we are sprinting. The reason behind this is +we want the player to feel a little more weighty when sprinting. If we were not also +increasing the max speed and jump height, we wouldn't need to change gravity, but since +we are, we need to make a few changes. + +The first change is we need to replace ``var grav = norm_grav`` with the code below: + +:: + + # Was "var grav = norm_grav" + var grav = 0 + if Input.is_action_pressed("movement_sprint"): + is_sprinting = true + grav = sprint_grav + else: + is_sprinting = false; + grav = norm_grav + + +Next we need to set our max speed to the higher speed if we are sprinting, and we also need +to change our acceleration to the new acceleration: + +:: + + var target = dir + # NEW CPDE. Replaces "target *= MAX_SPEED" + if is_sprinting: + target *= MAX_SPRINT_SPEED + else: + target *= MAX_SPEED + + # Same code as before: + var accel + if dir.dot(hvel) > 0: + # New code. Replaces "accel = ACCEL" + if is_sprinting: + accel = SPRINT_ACCEL + else: + accel = ACCEL + else: + accel = DEACCEL + + +Finally, we need to increase the jump height when we are sprinting: + +:: + + # Same code as before + if is_on_floor(): + if Input.is_action_just_pressed("movement_jump"): + # NEW CODE. replaces "vel.y = JUMP_SPEED" + if is_sprinting: + vel.y = SPRINT_JUMP_SPEED + else: + vel.y = JUMP_SPEED + +Now you should be able to sprint if you press the shift button! Go give it a +whirl! You can change the sprint related global variables to make the player faster, to reduce +gravity, and to accelerate faster. + +Phew! That was a lot of work. Now you have a fully working first person character! + +In :ref:`doc_fps_tutorial_part_two` we will add some guns to our player character. + +.. note:: At this point we've recreated the Kinematic character demo with sprinting! + +.. tip:: Currently the player script would be at an ideal state for making all sorts of + first person games. For example: Horror games, platformer games, adventure games, and more! + +.. warning:: If you ever get lost, be sure to read over the code again! You can also + download the finished project at the bottom of :ref:`doc_fps_tutorial_part_three`. diff --git a/tutorials/3d/fps_tutorial/part_three.rst b/tutorials/3d/fps_tutorial/part_three.rst new file mode 100644 index 000000000..9afbc250f --- /dev/null +++ b/tutorials/3d/fps_tutorial/part_three.rst @@ -0,0 +1,705 @@ +.. _doc_fps_tutorial_part_three: + +Part 3 +====== + +Part Overview +------------- + +In this part we will be limiting our guns by giving them ammo. We will also +be giving the player the ability to reload, and we will be adding sounds when the +guns fire. + +.. image:: img/PartThreeFinished.png + +By the end of this part, the player will have limited ammo, the ability to reload, +and sounds will play when the player fires and changes weapons. + +.. note:: You are assumed to have finished :ref:`doc_fps_tutorial_part_two` before moving on to this part of the tutorial. + +Let's get started! + + +Changing levels +--------------- + +Now that we have a fully working FPS, let's move to a more FPS like level. Open up ``Test_Level.tscn``. +``Test_Level.tscn`` is a complete custom FPS level created for the purpose of this tutorial. Press ``F6`` to +play the open scene, or press the "play current scene button", and give it a whirl. + +You might have noticed there are several boxes and cylinders placed throughout the level. They are :ref:`RigidBody ` +nodes we can place ``RigidBody_hit_test.gd`` on and then they will react to being hit with bullets, so lets do that! + +Select ``Center_room`` and open it up. From there select ``Physics_objects`` and open that up. You'll find there are +``6`` crates in a seemingly random order. Go select one of them and press the "Open in Editor" button. It's the one that +looks like a little movie slide. + +.. note:: The reason the objects seem to be placed in a random order is because all of the objects were copied and pasted around + in the Godot editor to save on time. If you want to move any of the nodes around, it is highly suggested to just + left click inside the editor viewport to get the node you want, and then move it around with the :ref:`Spatial ` gizmo. + +This will bring you to the crate's scene. From there, select the ``Crate`` :ref:`RigidBody ` (the one that is the root of the scene) +and scroll down in the inspector until you get to the script section. From there, click the drop down and select "Load". Chose +``RigidBody_hit_test.gd`` and then return to ``Test_Level.tscn``. + +Now open ``Upper_room``, select ``Physics_objects``, and chose one of the cylinder :ref:`RigidBody ` nodes. +Press the "Open in Editor" button beside one of the cylinders. This will bring you to the cylinder's scene. + +From there, select the ``Cylinder`` :ref:`RigidBody ` (the one that is the root of the scene) +and scroll down in the inspector until you get to the script section. From there, click the drop down and select "Load". Chose +``RigidBody_hit_test.gd`` and then return to ``Test_Level.tscn``. + +Now you can fire at the boxes and cylinders and they will react to your bullets just like the cubes in ``Testing_Area.tscn``! + + +Adding ammo +----------- + +Now that we've got working guns, lets give them a limited amount of ammo. + +Lets define some more global variables in ``Player.gd``, ideally nearby the other gun related variables: + +:: + + var ammo_for_guns = {"PISTOL":60, "RIFLE":160, "KNIFE":1} + var ammo_in_guns = {"PISTOL":20, "RIFLE":80, "KNIFE":1} + const AMMO_IN_MAGS = {"PISTOL":20, "RIFLE":80, "KNIFE":1} + + +Here is what these variables will be doing for us: + +- ``ammo_for_guns``: The amount of ammo we have in reserve for each weapon/gun. +- ``ammo_in_guns``: The amount of ammo currently inside the weapon/gun. +- ``AMMO_IN_MAGS``: How much ammo is in a fully filled weapon/gun. + +.. note:: There is no reason we've included ammo for the knife, so feel free to remove the knife's ammo + if you desire. + + Depending on how you program melee weapons, you may need to define an ammo count even if the + weapon does not use ammo. Some games use extremely short range 'guns' as their melee weapons, + and in those cases you may need to define ammo for your melee weapons. + +_________ + +Now we need to add a few ``if`` checks to ``_physics_process``. + +We need to make sure we have ammo in our gun before we try to fire a bullet. +Go find the line that checks for the fire action being pressed and add the following new +bits of code: + +:: + + # NOTE: You should have this if condition in your _physics_process function + # Firing the weapons + if Input.is_action_pressed("fire"): + if current_gun == "PISTOL": + if ammo_in_guns["PISTOL"] > 0: # NEW CODE + if animation_manager.current_state == "Pistol_idle": + animation_manager.set_animation("Pistol_fire") + + elif current_gun == "RIFLE": + if ammo_in_guns["RIFLE"] > 0: # NEW CODE + if animation_manager.current_state == "Rifle_idle": + animation_manager.set_animation("Rifle_fire") + + elif current_gun == "KNIFE": + if animation_manager.current_state == "Knife_idle": + animation_manager.set_animation("Knife_fire") + +These two additional ``if`` checks make sure we have a bullet to fire before setting our firing animation. + +While we're still in ``_physics_process``, let's also add a way to track how much ammo we have. Find the line that +has ``UI_status_label.text = "HEALTH: " + str(health)`` in ``_physics_process`` and replace it with the following: + +:: + + # HUD (UI) + if current_gun == "UNARMED" or current_gun == "KNIFE": + UI_status_label.text = "HEALTH: " + str(health) + else: + UI_status_label.text = "HEALTH: " + str(health) + "\nAMMO:" + \ + str(ammo_in_guns[current_gun]) + "/" + str(ammo_for_guns[current_gun]) + +.. tip:: Did you now that you can combine two lines using ``\``? We're using it here + so we do not have a extremely long line of code all on one line by splitting it + into two lines! + +This will show the player how much ammo they currently have and how much ammo they currently have in reserve, only for +the appropriate weapons (not unarmed or the knife). Regardless of the currently selected weapon/gun, we will always show +how much health the player has + +.. note:: we cannot just add ``ammo_for_guns[current_gun]`` or ``ammo_in_guns[current_gun]`` to the ``string`` we + are passing in to the :ref:`Label `. Instead we have to cast them from ``floats`` to ``strings``, which is what we are doing + by using ``str()``. + + For more information on casting, see this page from wiki books: + https://en.wikibooks.org/wiki/Computer_Programming/Type_conversion + +.. warning:: We are currently not using the player's health just yet in the tutorial. We will start + using health for the player and objects when we include turrets and targets in later parts. + + +Now we need to remove a bullet from the gun when we fire. To do that, we just need to add a few lines in +``fire_bullet``: + +:: + + func fire_bullet(): + if changing_gun == true: + return + + # Pistol bullet handling: Spawn a bullet object! + if current_gun == "PISTOL": + var clone = bullet_scene.instance() + var scene_root = get_tree().root.get_children()[0] + scene_root.add_child(clone) + + clone.global_transform = get_node("Rotation_helper/Gun_fire_points/Pistol_point").global_transform + # The bullet is a little too small (by default), so let's make it bigger! + clone.scale = Vector3(4, 4, 4) + + ammo_in_guns["PISTOL"] -= 1 # NEW CODE + + # Rifle bullet handeling: Send a raycast! + elif current_gun == "RIFLE": + var ray = get_node("Rotation_helper/Gun_fire_points/Rifle_point/RayCast") + ray.force_raycast_update() + + if ray.is_colliding(): + var body = ray.get_collider() + if body.has_method("bullet_hit"): + body.bullet_hit(RIFLE_DAMAGE, ray.get_collision_point()) + + ammo_in_guns["RIFLE"] -= 1 # NEW CODE + + # Knife bullet(?) handeling: Use an area! + elif current_gun == "KNIFE": + var area = get_node("Rotation_helper/Gun_fire_points/Knife_point/Area") + var bodies = area.get_overlapping_bodies() + + for body in bodies: + if body.has_method("bullet_hit"): + body.bullet_hit(KNIFE_DAMAGE, area.global_transform.origin) + + +Go play the project again! Now you'll lose ammo as you fire, until you reach zero and +cannot fire anymore. + +Adding reloading +---------------- + +Now that we can empty our gun, we need a way to refill it! + +First, let's start by +adding another global variable. Add ``var reloading_gun = false`` somewhere along with your +other global variables, preferably near the other gun related variables. + +_________ + +Now we need to add several things to ``_physics_process``. + +First, let's make sure we cannot change guns while reloading. +We need to change the weapon changing code to include the following: + +:: + + # Was "if changing_gun == false" + if changing_gun == false and reloading_gun == false: + if Input.is_key_pressed(KEY_1): + current_gun = "UNARMED" + changing_gun = true + elif Input.is_key_pressed(KEY_2): + current_gun = "KNIFE" + changing_gun = true + elif Input.is_key_pressed(KEY_3): + current_gun = "PISTOL" + changing_gun = true + elif Input.is_key_pressed(KEY_4): + current_gun = "RIFLE" + changing_gun = true + +Now the player cannot change guns while reloading. + +_________ + +Ideally we want the player to be able to reload when they chose, so lets given them +the ability to reload when they press the ``reload`` action. Add the following +somewhere in ``_physics_process``, ideally nearby your other input related code: + +:: + + # Reloading + if reloading_gun == false: + if Input.is_action_just_pressed("reload"): + if animation_manager.current_state != "Pistol_reload" and animation_manager.current_state != "Rifle_reload": + reloading_gun = true + +First we see if the player is already reloading. If they are not, then we check if they've pressed +the reloading action. If they have pressed the ``reload`` action, we then make sure they are not already +in a reloading animation. If they are not, we set ``reloading_gun`` to ``true``. + +We do not want to do our reloading processing here with the input in an effort to keep game logic +separate from input logic. Keeping them separate makes the code easier to debug, and as a bonus it +keeps the input logic from being overly bloated. + +_________ + +Finally, we need to add the code that actually handles reloading. Add the following code to ``_physics_process``, +ideally somewhere underneath the reloading input code you just inputted: + +:: + + # Reloading logic + if reloading_gun == true: + var can_reload = false + + if current_gun == "PISTOL": + if animation_manager.current_state == "Pistol_idle": + can_reload = true + elif current_gun == "RIFLE": + if animation_manager.current_state == "Rifle_idle": + can_reload = true + elif current_gun == "KNIFE": + can_reload = false + reloading_gun = false + else: + can_reload = false + reloading_gun = false + + if ammo_for_guns[current_gun] <= 0 or ammo_in_guns[current_gun] == AMMO_IN_MAGS[current_gun]: + can_reload = false + reloading_gun = false + + + if can_reload == true: + var ammo_needed = AMMO_IN_MAGS[current_gun] - ammo_in_guns[current_gun] + + if ammo_for_guns[current_gun] >= ammo_needed: + ammo_for_guns[current_gun] -= ammo_needed + ammo_in_guns[current_gun] = AMMO_IN_MAGS[current_gun] + else: + ammo_in_guns[current_gun] += ammo_for_guns[current_gun] + ammo_for_guns[current_gun] = 0 + + if current_gun == "PISTOL": + animation_manager.set_animation("Pistol_reload") + elif current_gun == "RIFLE": + animation_manager.set_animation("Rifle_reload") + + reloading_gun = false + + +Lets go over what this code does. + +_________ + +First we check if ``reloading_gun`` is ``true``. If it is we then go through a series of checks +to see if we can reload or not. We use ``can_reload`` as a variable to track whether or not +it is possible to reload. + +We go through series of checks for each weapon. For the pistol and the rifle we check if +we're in an idle state or not. If we are, then we set ``can_reload`` to ``true``. + +For the knife we do not want to reload, because you cannot reload a knife, so we set ``can_reload`` and ``reloading_gun`` +to ``false``. If we are using a weapon that we do not have a ``if`` or ``elif`` check for, we set +``can_reload`` and ``reloading_gun`` to ``false``, as we do not want to be able to reload a weapon we are unaware of. + +Next we check if we have ammo in reserve for the gun in question. We also check to make sure the gun we are trying to reload +is not already full of ammo. If the gun does not have ammo in reserve or the gun is already full, we set +``can_reload`` and ``reloading_gun`` to ``false``. + +If we've made it through all those checks and we can reload, then we have a few more steps to take. + +First we assign the ammo we are needing to fill the gun fully to the ``ammo_needed`` variable. +We just subtract the amount of ammo we currently have in our gun by the amount of ammo in a full magazine. + +Then we check if have enough ammo in reserves to fill the gun fully. If we do, we subtract the amount of ammo +we need to refill our gun from the reserves, and we set the amount of ammo in the gun to full. + +If we do not have enough ammo in reserves to fill the gun, we add all of the ammo left in reserves to our +gun and then set the ammo in reserves to zero, making it empty. + +Regardless of how much ammo we've added to the gun, we set our animation to the reloading animation for the current gun. + +Finally, we set ``reloading_gun`` to false because we have finished reloading the gun. + +_________ + +Go test the project again, and you'll find you can reload your gun when it is not +full and when there is ammo left in the ammo reserves. + +_________ + +Personally, I like the guns to automatically start reloading if we try to fire them +when they have no ammo in them, so lets add that! Add the following code to the input code for +firing the guns: + +:: + + # Firing the weapons + if Input.is_action_pressed("fire"): + if current_gun == "PISTOL": + if ammo_in_guns["PISTOL"] > 0: + if animation_manager.current_state == "Pistol_idle": + animation_manager.set_animation("Pistol_fire") + # NEW CODE! + else: + reloading_gun = true + + elif current_gun == "RIFLE": + if ammo_in_guns["RIFLE"] > 0: + if animation_manager.current_state == "Rifle_idle": + animation_manager.set_animation("Rifle_fire") + # NEW CODE! + else: + reloading_gun = true + + elif current_gun == "KNIFE": + if animation_manager.current_state == "Knife_idle": + animation_manager.set_animation("Knife_fire") + +Now whenever the player tries to fire the gun when it's empty, we automatically +set ``reloading_gun`` to true, which will reload the gun if possible. + +Adding sounds +------------- + +Finally, let's add some sounds that play when we are reloading, changing guns, and when we +are firing them. + +.. tip:: There are no game sounds provided in this tutorial (for legal reasons). + https://gamesounds.xyz/ is a collection of **"royalty free or public domain music and sounds suitable for games"**. + I used Gamemaster's Gun Sound Pack, which can be found in the Sonniss.com GDC 2017 Game Audio Bundle. + + The video tutorial will briefly show how to edit the audio files for use in the tutorial. + +Open up ``SimpleAudioPlayer.tscn``. It is simply a :ref:`Spatial ` with a :ref:'AudioStreamPlayer ' as it's child. + +.. note:: The reason this is called a 'simple' audio player is because we are not taking performance into account + and because the code is designed to provide sound in the simplest way possible. This will likely change + in a future part. + +If you want to use 3D audio, so it sounds like it's coming from a location in 3D space, right click +the :ref:'AudioStreamPlayer ' and select "Change type". + +This will open the node browser. Navigate to :ref:'AudioStreamPlayer3D ' and select "change". +In the source for this tutorial, we will be using :ref:'AudioStreamPlayer ', but you can optionally +use :ref:'AudioStreamPlayer3D ' if you desire, and the code provided below will work regardless of which +one you chose. + +Create a new script and call it "SimpleAudioPlayer.gd". Attach it to the :ref:`Spatial ` in ``SimpleAudioPlayer.tscn`` +and insert the following code: + +:: + + extends Spatial + + # All of the audio files. + # You will need to provide your own sound files. + var audio_pistol_shot = preload("res://path_to_your_audio_here") + var audio_gun_cock = preload("res://path_to_your_audio_here") + var audio_rifle_shot = preload("res://path_to_your_audio_here") + + var audio_node = null + + func _ready(): + audio_node = get_node("AudioStreamPlayer") + audio_node.connect("finished", self, "destroy_self") + audio_node.stop() + + + func play_sound(sound_name, position=null): + if sound_name == "Pistol_shot": + audio_node.stream = audio_pistol_shot + elif sound_name == "Rifle_shot": + audio_node.stream = audio_rifle_shot + elif sound_name == "Gun_cock": + audio_node.stream = audio_gun_cock + else: + print ("UNKNOWN STREAM") + queue_free() + return + + # If you are using a AudioPlayer3D, then uncomment these lines to set the position. + # if position != null: + # audio_node.global_transform.origin = position + + audio_node.play() + + + func destroy_self(): + audio_node.stop() + queue_free() + + +.. tip:: By setting ``position`` to ``null`` by default in ``play_sound``, we are making it an optional argument, + meaning position doesn't necessarily have to be passed in to call the ``play_sound``. + +Let's go over what's happening here: + +_________ + +In ``_ready`` we get the :ref:'AudioStreamPlayer ' and connect it's ``finished`` signal to ourselves. +It doesn't matter if it's :ref:'AudioStreamPlayer ' or :ref:'AudioStreamPlayer3D ' node, +as they both have the finished signal. To make sure it is not playing any sounds, we call ``stop`` on the :ref:'AudioStreamPlayer '. + +.. warning:: Make sure your sound files are **not** set to loop! If it is set to loop + the sounds will continue to play infinitely and the script will not work! + +The ``play_sound`` function is what we will be calling from ``Player.gd``. We check if the sound +is one of the three possible sounds, and if it is we set the audio stream for our :ref:'AudioStreamPlayer ' +to the correct sound. + +If it is an unknown sound, we print an error message to the console and free ourselves. + +If you are using a :ref:'AudioStreamPlayer3D ', remove the ``#`` to set the position of +the audio player node so it plays at the correct position. + +Finally, we tell the :ref:'AudioStreamPlayer ' to play. + +When the :ref:'AudioStreamPlayer ' is finished playing the sound, it will call ``destroy_self`` because +we connected the ``finished`` signal in ``_ready``. We stop the :ref:'AudioStreamPlayer ' and free ourself +to save on resources. + +.. note:: This system is extremely simple and has some major flaws: + One flaw is we have to pass in a string value to play a sound. While it is relatively simple + to remember the names of the three sounds, it can be increasingly complex when you have more sounds. + Ideally we'd place these sounds in some sort of container with exposed variables so we do not have + to remember the name(s) of each sound effect we want to play. + + Another flaw is we cannot play looping sounds effects, nor background music easily with this system. + Because we cannot play looping sounds, certain effects like footstep sounds are harder to accomplish + because we then have to keep track of whether or not there is a sound effect *and* whether or not we + need to continue playing it. + +_________ + +With that done, lets open up ``Player.gd`` again. +First we need to load the ``SimpleAudioPlayer.tscn``. Place the following code in your global variables: + +:: + + var simple_audio_player = preload("res://SimpleAudioPlayer.tscn") + +Now we just need to instance the simple audio player when we need it, and then call it's +``play_sound`` function and pass the name of the sound we want to play. To make the process simpler, +let's create a ``create_sound`` function: + +:: + + func create_sound(sound_name, position=null): + var audio_clone = simple_audio_player.instance() + var scene_root = get_tree().root.get_children()[0] + scene_root.add_child(audio_clone) + audio_clone.play_sound(sound_name, position) + +Lets walk through what this function does: + +_________ + +The first line instances the ``simple_audio_player.tscn`` scene and assigns it to a variable, +named ``audio_clone``. + +The second line gets the scene root, using one large assumption. We first get this node's :ref:`SceneTree `, +and then access the root node, which in this case is the :ref:`Viewport ` this entire game is running under. +Then we get the first child of the :ref:`Viewport `, which in our case happens to be the root node in +``Test_Area.tscn`` or ``Test_Level.tscn``. We are making a huge assumption that the first child of the root +is the root node that our player is under, which could not always be the case. + +If this doesn't make sense to you, don't worry too much about it. The second line of code only doesn't work +reliably if you have multiple scenes loaded as childs to the root node at a time, which will rarely happen for most projects. This is really +only potentially a issue depending on how you handle scene loading. + +The third line adds our newly created ``SimpleAudioPlayer`` scene to be a child of the scene root. This +works exactly the same as when we are spawning bullets. + +Finally, we call the ``play_sound`` function and pass in the arguments we're given. This will call +``SimpleAudioPlayer.gd``'s ``play_sound`` function with the passed in arguments. + +_________ + +Now all that is left is playing the sounds when we want to. First, let's play the shooting sounds +when a bullet is fired. Go to ``fire_bullet`` and add the following: + +:: + + func fire_bullet(): + if changing_gun == true: + return + + # Pistol bullet handling: Spawn a bullet object! + if current_gun == "PISTOL": + var clone = bullet_scene.instance() + var scene_root = get_tree().root.get_children()[0] + scene_root.add_child(clone) + + clone.global_transform = get_node("Rotation_helper/Gun_fire_points/Pistol_point").global_transform + # The bullet is a little too small (by default), so let's make it bigger! + clone.scale = Vector3(4, 4, 4) + + ammo_in_guns["PISTOL"] -= 1 + create_sound("Pistol_shot", clone.global_transform.origin); # NEW CODE + + # Rifle bullet handeling: Send a raycast! + elif current_gun == "RIFLE": + var ray = get_node("Rotation_helper/Gun_fire_points/Rifle_point/RayCast") + ray.force_raycast_update() + + if ray.is_colliding(): + var body = ray.get_collider() + if body.has_method("bullet_hit"): + body.bullet_hit(RIFLE_DAMAGE, ray.get_collision_point()) + + ammo_in_guns["RIFLE"] -= 1 + create_sound("Rifle_shot", ray.global_transform.origin); # NEW CODE + + # Knife bullet(?) handeling: Use an area! + elif current_gun == "KNIFE": + var area = get_node("Rotation_helper/Gun_fire_points/Knife_point/Area") + var bodies = area.get_overlapping_bodies() + + for body in bodies: + if body.has_method("bullet_hit"): + body.bullet_hit(KNIFE_DAMAGE, area.global_transform.origin) + +Now we will play the shooting noise for both the pistol and the rifle when a bullet is created. + +.. note:: We are passing in the positions of the ends of the guns using the bullet object's + global :ref:`Transform ` and the :ref:`Raycast `'s global :ref:`Transform `. + If you are not using a :ref:`AudioStreamPlayer3D ` node, you can optionally leave the positions out and only + pass in the name of the sound you want to play. + +Finally, lets play the sound of a gun being cocked when we reload and when we change weapons. +Add the following to our reloading logic section of ``_physics_process``: + +:: + + # Reloading logic + if reloading_gun == true: + var can_reload = false + + if current_gun == "PISTOL": + if animation_manager.current_state == "Pistol_idle": + can_reload = true + elif current_gun == "RIFLE": + if animation_manager.current_state == "Rifle_idle": + can_reload = true + elif current_gun == "KNIFE": + can_reload = false + reloading_gun = false + else: + can_reload = false + reloading_gun = false + + if ammo_for_guns[current_gun] <= 0 or ammo_in_guns[current_gun] == AMMO_IN_MAGS[current_gun]: + can_reload = false + reloading_gun = false + + + if can_reload == true: + var ammo_needed = AMMO_IN_MAGS[current_gun] - ammo_in_guns[current_gun] + + if ammo_for_guns[current_gun] >= ammo_needed: + ammo_for_guns[current_gun] -= ammo_needed + ammo_in_guns[current_gun] = AMMO_IN_MAGS[current_gun] + else: + ammo_in_guns[current_gun] += ammo_for_guns[current_gun] + ammo_for_guns[current_gun] = 0 + + if current_gun == "PISTOL": + animation_manager.set_animation("Pistol_reload") + elif current_gun == "RIFLE": + animation_manager.set_animation("Rifle_reload") + + reloading_gun = false + create_sound("Gun_cock", camera.global_transform.origin) # NEW CODE + +And add this code to the changing weapons section of ``_physics_process``: + +:: + + if changing_gun == true: + if current_gun != "PISTOL": + if animation_manager.current_state == "Pistol_idle": + animation_manager.set_animation("Pistol_unequip") + if current_gun != "RIFLE": + if animation_manager.current_state == "Rifle_idle": + animation_manager.set_animation("Rifle_unequip") + if current_gun != "KNIFE": + if animation_manager.current_state == "Knife_idle": + animation_manager.set_animation("Knife_unequip") + + if current_gun == "UNARMED": + if animation_manager.current_state == "Idle_unarmed": + changing_gun = false + + elif current_gun == "KNIFE": + if animation_manager.current_state == "Knife_idle": + changing_gun = false + if animation_manager.current_state == "Idle_unarmed": + animation_manager.set_animation("Knife_equip") + + elif current_gun == "PISTOL": + if animation_manager.current_state == "Pistol_idle": + changing_gun = false + if animation_manager.current_state == "Idle_unarmed": + animation_manager.set_animation("Pistol_equip") + + create_sound("Gun_cock", camera.global_transform.origin) # NEW CODE + + elif current_gun == "RIFLE": + if animation_manager.current_state == "Rifle_idle": + changing_gun = false + if animation_manager.current_state == "Idle_unarmed": + animation_manager.set_animation("Rifle_equip") + + create_sound("Gun_cock", camera.global_transform.origin) # NEW CODE + +Now whatever sound you have assigned to "Gun_cock" will play when you reload and when you +change to either the pistol or the rifle. + + +Final notes +----------- + +.. image:: img/FinishedTutorialPicture.png + +Now you have a fully working single player FPS! + +You can find the completed project here: :download:`Godot_FPS_Finished.zip ` + +.. tip:: The finished project source is hosted on Github as well: https://github.com/TwistedTwigleg/Godot_FPS_Tutorial + +You can also download all of the ``.blend`` files used here: :download:`Godot_FPS_BlenderFiles.zip ` + +.. note:: The finished project source files contain the same exact code, just written in a different order. + This is because the finished project source files are what the tutorial is based on. + + The finished project code was written in the order that features were created, not necessarily + in a order that is ideal for learning. + + Other than that, the source is exactly the same, just with helpful comments explaining what + each part does. + +The skybox is created by **StumpyStrust** and can be found at OpenGameArt.org. https://opengameart.org/content/space-skyboxes-0 + +The font used is **Titillium-Regular**, and is licensed under the SIL Open Font License, Version 1.1. + +The skybox was convert to a 360 equirectangular image using this tool: https://www.360toolkit.co/convert-cubemap-to-spherical-equirectangular.html + +While no sounds are provided, you can find many game ready sounds at https://gamesounds.xyz/ + +.. warning:: OpenGameArt.org, 360toolkit.co, the creator(s) of Titillium-Regular, and GameSounds.xyz are in no way involved in this tutorial. + +__________ + +In future parts we will be adding the following: + +- Adding a spawning system +- Adding grenades +- Adding turrets and targets +- Adding a sound manager +- Adding ammo and health pickups +- Refining and cleaning up the code + +.. warning:: All plans are subject to change without warning! diff --git a/tutorials/3d/fps_tutorial/part_two.rst b/tutorials/3d/fps_tutorial/part_two.rst new file mode 100644 index 000000000..f68ce2c3a --- /dev/null +++ b/tutorials/3d/fps_tutorial/part_two.rst @@ -0,0 +1,1004 @@ +.. _doc_fps_tutorial_part_two: + +Part 2 +====== + +Part Overview +------------- + +In this part we will be giving our player weapons to play with. + +.. image:: img/PartTwoFinished.png + +By the end of this part, you will have a player that can fire a pistol, +rifle, and attack using a knife. The player will also now have animations with transitions, +and the weapons can interact with objects in the environment. + +.. note:: You are assumed to have finished :ref:`doc_fps_tutorial_part_one` before moving on to this part of the tutorial. + +Let's get started! + +Making a system to handle animations +------------------------------------ + +First we need a way to handle changing animations. Open up ``Player.tscn`` and select the :ref:`AnimationPlayer ` +Node (``Player``->``Rotation_helper``->``Model``->``AnimationPlayer``). + +Create a new script called ``AnimationPlayer_Manager.gd`` and attach that to the :ref:`AnimationPlayer `. + +Add the following code to ``AnimationPlayer_Manager.gd``: + +:: + + # Structure -> Animation name :[Connecting Animation states] + var states = { + "Idle_unarmed":["Knife_equip", "Pistol_equip", "Rifle_equip", "Idle_unarmed"], + + "Pistol_equip":["Pistol_idle"], + "Pistol_fire":["Pistol_idle"], + "Pistol_idle":["Pistol_fire", "Pistol_reload", "Pistol_unequip", "Pistol_idle"], + "Pistol_reload":["Pistol_idle"], + "Pistol_unequip":["Idle_unarmed"], + + "Rifle_equip":["Rifle_idle"], + "Rifle_fire":["Rifle_idle"], + "Rifle_idle":["Rifle_fire", "Rifle_reload", "Rifle_unequip", "Rifle_idle"], + "Rifle_reload":["Rifle_idle"], + "Rifle_unequip":["Idle_unarmed"], + + "Knife_equip":["Knife_idle"], + "Knife_fire":["Knife_idle"], + "Knife_idle":["Knife_fire", "Knife_unequip", "Knife_idle"], + "Knife_unequip":["Idle_unarmed"], + } + + var animation_speeds = { + "Idle_unarmed":1, + + "Pistol_equip":1.4, + "Pistol_fire":1.8, + "Pistol_idle":1, + "Pistol_reload":1, + "Pistol_unequip":1.4, + + "Rifle_equip":2, + "Rifle_fire":6, + "Rifle_idle":1, + "Rifle_reload":1.45, + "Rifle_unequip":2, + + "Knife_equip":1, + "Knife_fire":1.35, + "Knife_idle":1, + "Knife_unequip":1, + } + + var current_state = null + var callback_function = null + + func _ready(): + set_animation("Idle_unarmed") + connect("animation_finished", self, "animation_ended") + + + + func set_animation(animation_name): + if animation_name == current_state: + print ("AnimationPlayer_Manager.gd -- WARNING: animation is already ", animation_name) + return true + + if has_animation(animation_name) == true: + if current_state != null: + var possible_animations = states[current_state] + if animation_name in possible_animations: + current_state = animation_name + play(animation_name, -1, animation_speeds[animation_name]) + return true + else: + print ("AnimationPlayer_Manager.gd -- WARNING: Cannot change to ", animation_name, " from ", current_state) + return false + else: + current_state = animation_name + play(animation_name, -1, animation_speeds[animation_name]) + return true + return false + + + func animation_ended(anim_name): + # UNARMED transitions + if current_state == "Idle_unarmed": + pass + # KNIFE transitions + elif current_state == "Knife_equip": + set_animation("Knife_idle") + elif current_state == "Knife_idle": + pass + elif current_state == "Knife_fire": + set_animation("Knife_idle") + elif current_state == "Knife_unequip": + set_animation("Idle_unarmed") + # PISTOL transitions + elif current_state == "Pistol_equip": + set_animation("Pistol_idle") + elif current_state == "Pistol_idle": + pass + elif current_state == "Pistol_fire": + set_animation("Pistol_idle") + elif current_state == "Pistol_unequip": + set_animation("Idle_unarmed") + elif current_state == "Pistol_reload": + set_animation("Pistol_idle") + # RIFLE transitions + elif current_state == "Rifle_equip": + set_animation("Rifle_idle") + elif current_state == "Rifle_idle": + pass; + elif current_state == "Rifle_fire": + set_animation("Rifle_idle") + elif current_state == "Rifle_unequip": + set_animation("Idle_unarmed") + elif current_state == "Rifle_reload": + set_animation("Rifle_idle") + + func animation_callback(): + if callback_function == null: + print ("AnimationPlayer_Manager.gd -- WARNING: No callback function for the animation to call!") + else: + callback_function.call_func() + +Lets go over what this script is doing: + +_________ + +Lets start with this script's global variables: + +- ``states``: A dictionary for holding our animation states. (Further explanation below) +- ``animation_speeds``: A dictionary for holding all of the speeds we want to play our animations at. +- ``current_state``: A variable for holding the name of the animation state we are currently in. +- ``callback_function``: A variable for holding the callback function. (Further explanation below) + +If you are familiar with state machines, then you may have noticed that ``states`` is structured +like a basic state machine. Here is roughly how ``states`` is set up: + +``states`` is a dictionary with the key being the name of the current state, and the value being +an array holding all of the states we can transition to. For example, if we are in currently in +state ``Idle_unarmed``, we can only transition to ``Knife_equip``, ``Pistol_equip``, ``Rifle_equip``, and +``Idle_unarmed``. + +If we try to transition to a state that is not included in our possible transitions states, +then we get a warning message and the animation does not change. We will also automatically +transition from some states into others, as will be explained further below in ``animation_ended`` + +.. note:: For the sake of keeping this tutorial simple we are not using a 'proper' + state machine. If you are interested to know more about state machines, + see the following articles: + + - (Python example) https://dev.to/karn/building-a-simple-state-machine-in-python + - (C# example) https://www.codeproject.com/Articles/489136/UnderstandingplusandplusImplementingplusStateplusP + - (Wiki article) https://en.wikipedia.org/wiki/Finite-state_machine + + In a future part of this tutorial series we may revise this script to include a proper state machine. + +``animation_speeds`` is how fast each animation will play. Some of the animations are a little slow +and in an effort to make everything smooth, we need to play them at faster speeds than some +of the others. + +-- note:: Notice that all of the firing animations are faster than their normal speed. Remember this for later! + +``current_state`` will hold the name of the animation state we are currently in. + +Finally, ``callback_function`` will be a :ref:`FuncRef ` passed in by our player for spawning bullets +at the proper frame of animation. A :ref:`FuncRef ` allows us to pass in a function as an argument, +effectively allowing us to call a function from another script, which is how we will use it later. + +_________ + +Now lets look at ``_ready``. First we are setting our animation to ``Idle_unarmed``, using the ``set_animation`` function, +so we for sure start in that animation. Next we connect the ``animation_finished`` signal to this script and assign +it to call ``animation_ended``. + +_________ + +Lets look at ``set_animation`` next. + +``set_animation`` sets the animation to the that of the passed in +animation state *if* we can transition to it. In other words, if the animation state we are currently in +has the passed in animation state name in ``states``, then we will change to that animation. + +First we check if the passed in animation is the same as the animation state we are currently in. +If it is, then we write a warning to the console and return ``true``. + +Next we see if :ref:`AnimationPlayer ` has the passed in animation using ``has_animation``. If it does not, we return ``false``. + +Then we check if ``current_state`` is set or not. If ``current_state`` is *not* currently set, we +set ``current_state`` to the passed in animation and tell :ref:`AnimationPlayer ` to start playing the animation with +a blend time of ``-1`` and at the speed set in ``animation_speeds`` and then we return ``true``. + +If we have a state in ``current_state``, then we get all of the possible states we can transition to. +If the animation name is in the array of possible transitions, then we set ``current_state`` to the passed +in animation, tell :ref:`AnimationPlayer ` to play the animation with a blend time of ``-1`` at the speed set in ``animation_speeds`` +and then we return ``true``. + +_________ + +Now lets look at ``animation_ended``. + +``animation_ended`` is the function that will be called by :ref:`AnimationPlayer ` when it's done playing a animation. + + +For certain animation states, we may need to transition into another state when its finished. To handle this, we +check for every possible animation state. If we need to, we transition into another state. + +.. warning:: If you are using your own animated models, make sure that none of the animations are set + to loop. Looping animations do not send the ``animation_finished`` signal when they reach + the end of the animation and are about to loop. + +.. note:: the transitions in ``animation_ended`` ideally would be part of the data in ``states``, but in + an effort to make the tutorial easier to understand, we'll just hard code each state transition + in ``animation_ended``. + +_________ + +Finally we have ``animation_callback``. This function will be called by a function track in our animations. +If we have a :ref:`FuncRef ` assigned to ``callback_function``, then we call that passed in function. If we do not +have a :ref:`FuncRef ` assigned to ``callback_function``, we print out a warning to the console. + +.. tip:: Try running ``Testing_Area.tscn`` just to make sure there is no runtime issues. If the game runs but nothing + seems to have changed, then everything is working correctly. + +Getting the animations ready +---------------------------- + +Now that we have a working animation manager, we need to call it from our player script. +Before that though, we need to set some animation callback tracks in our firing animations. + +Open up ``Player.tscn`` if you don't have it open and navigate to the :ref:`AnimationPlayer ` node +(``Player``->``Rotation_helper``->``Model``->``AnimationPlayer``). + +We need to attach a function track to three of our animations: The firing animation for the pistol, rifle, and knife. +Let's start with the pistol. Click the animation drop down list and select "Pistol_fire". + +Now scroll down to the very bottom of the list of animation tracks. The final item in the list should read +``Armature/Skeleton:Left_UpperPointer``. Now at the bottom of the list, click the plus icon on the bottom +bar of animation window, right plus right next to the loop button and the up arrow. + +.. image:: img/AnimationPlayerAddTrack.png + +This will bring up a window with three choices. We're wanting to add a function callback track, so click the +option that reads "Add Call Func Track". This will open a window showing the entire node tree. Navigate to the +:ref:`AnimationPlayer ` node, select it, and press OK. + +.. image:: img/AnimationPlayerCallFuncTrack.png + +Now at the bottom of list of animation tracks you will have a green track that reads "AnimationPlayer". +Now we need to add the point where we want to call our callback function. Scrub the timeline until you +reach the point where the muzzle just starts to flash. + +.. note:: The timeline is the window where all of the points in our animation are stored. Each of the little + points represents a point of animation data. + + Scrubbing the timeline means moving ourselves through the animation. So when we say "scrub the timeline + until you reach a point", what we mean is move through the animation window until you reach the a point + on the timeline. + + Also, the muzzle of a gun is the end point where the bullet comes out. The muzzle flash is the flash of + light that escapes the muzzle when a bullet is fired. The muzzle is also sometimes referred to as the + barrel of the gun. + +.. tip:: For finer control when scrubbing the timeline, press ``control`` and scroll forwards with the mouse wheel to zoom in. + Scrolling backwards will zoom out. + + You can also change how the timeline scrubbing snaps by changing the value in ``Step (s)`` to a lower/higher value. + +Once you get to a point you like, press the little green plus symbol on the far right side of the +``AnimationPlayer`` track. This will place a little green point at the position you are currently +at in the animation on your ``AnimationPlayer`` track. + +.. image:: img/AnimationPlayerAddPoint.png + +Now we have one more step before we are done with the pistol. Select the "enable editing of individual keys" +button on the far right corner of the animation window. It looks like a pencil with a little point beside it. + +.. image:: img/AnimationPlayerEditPoints.png + +Once you've click that, a new window will open on the right side. Now click the green point on the ``AnimationPlayer`` +track. This will bring up the information associated with that point in the timeline. In the empty name field, enter +"animation_callback" and press ``enter``. + +Now when we are playing this animation the callback function will be triggered at that specific point of the animation. + +.. warning:: Be sure to press the "enable editing of individual keys" button again to turn off the ability to edit individual keys + so you cannot change one of the transform tracks by accident! + +_________ + +Let's repeat the process for the rifle and knife firing animations! + +.. note:: Because the process is exactly the same as the pistol, the process is going to explained in a little less depth. + Follow the steps in the above if you get lost! It is exactly the same, just on a different animation. + +Go to the "Rifle_fire" animation from the animation drop down. Add the function callback track once you reach the bottom of the +animation track list by clicking the little plus icon at the bottom of the screen. Find the point where the muzzle just starts +to flash and click the little green plus symbol to add a function callback point at that position on the track. + +Next, click the "enable editing of individual keys" button, the button with a plus at the bottom right side of the animation window. +Select the newly created function callback point, put "animation_callback" into the name field and press ``enter``. +Click the "enable editing of individual keys" button again to turn off individual key editing. +so we cannot change one of the transform tracks by accident. + +Now we just need to apply the callback function track to the knife animation. Select the "Knife_fire" animation and scroll to the bottom of the +animation tracks. Click the plus symbol at the bottom of the animation window and add a function callback track. +Next find a point around the first third of the animation to place the animation callback function point at. + +.. note:: We will not actually be firing the knife, and the animation really is a stabbing animation rather than a firing one. + For this tutorial we are just reusing the gun firing logic for our knife, so the animation has been named in a style that + is consistent with the other animations. + +From there click the little green plus to add a function callback point at the current position. Then click the "enable editing of individual keys" +button, the button with a plus at the bottom right side of the animation window. +Select the newly created function callback point, put "animation_callback" into the name field and press ``enter``. +Click the "enable editing of individual keys" button again to turn off individual key editing. +so we cannot change one of the transform tracks by accident. + +.. tip:: Be sure to save your work! + +With that done, we are almost ready to start adding the ability to fire to our player script! We just need to setup one last scene: +The scene for our bullet object. + +Creating the bullet scene +------------------------- + +There are several ways to handle a gun's bullets in video games. In this tutorial series, +we will be exploring two of the more common ways: Objects, and raycasts. + +_________ + +One of the two ways is using a bullet object. This will be a object that travels through the world and handles +its own collision code. This method we create/spawn a bullet object in the direction our gun is facing, and then +it sends itself forward. + +There are several advantages to this method. The first being we do not have to store the bullets in our player. We can simply create the bullet +and then move on, and the bullet itself with handle checking for collisions, sending the proper signal(s) to the object it collides with, and destroying itself. + +Another advantage is we can have more complex bullet movement. If we want to make the bullet fall ever so slightly as time goes on, we can make the bullet +controlling script slowly push the bullet towards the ground. Using a object also makes the bullet take time to reach its target, it doesn't just instantly +hit whatever its pointed at. This feels more realistic because nothing in real life really moves instantly from one point to another. + +One of the huge disadvantages performance. While having each bullet calculate their own paths and handle their own collision allows for a lot of flexibility, +it comes at the cost of performance. With this method we are calculating every bullet's movement every step, and while this may not be a problem for a few dozen +bullets, it can become a huge problem when you potentially have several hundred bullets. + +Despite the performance hit, many first person shooters include some form of object bullets. Rocket launchers are a prime example because in many +first person shooters, Rockets do not just instantly explode at their target position. You can also find bullets as object many times with grenades +because they generally bounce around the world before exploding. + +.. note:: While I cannot say for sure this is the case, these games *probably* use bullet objects in some form or another: + (These are entirely from my observations. **They may be entirely wrong**. I have never worked on **any** of the following games) + + - Halo (Rocket launchers, fragment grenades, sniper rifles, brute shot, and more) + - Destiny (Rocket launchers, grenades, fusion rifles, sniper rifles, super moves, and more) + - Call of Duty (Rocket launchers, grenades, ballistic knifes, crossbows, and more) + - Battlefield (Rocket launchers, grenades, claymores, mortars, and more) + +Another disadvantage with bullet objects is networking. Bullet objects have to sync the positions (at least) with however many clients are connected +to the server. + +While we are not implementing any form of networking (as that would be it's own entire tutorial series), it is a consideration +to keep in mind when creating your first person shooter, especially if you plan on adding some form of networking in the future. + +_________ + +The other way of handling bullet collisions we will be looking at, is raycasting. + +This method is extremely common in guns that have fast moving bullets that rarely change trajectory change over time. + +Instead of creating a bullet object and sending it through space, we instead send a ray starting from the barrel/muzzle of the gun forwards. +We set the raycast's origin to the starting position of the bullet, and based on the length we can adjust how far the bullet 'travels' through space. + +.. note:: While I cannot say for sure this is the case, these games *probably* use raycasts in some form or another: + (These are entirely from my observations. **They may be entirely wrong**. I have never worked on **any** of the following games) + + - Halo (Assault rifles, DMRs, battle rifles, covenant carbine, spartan laser, and more) + - Destiny (Auto rifles, pulse rifles, scout rifles, hand cannons, machine guns, and more) + - Call of Duty (Assault rifles, light machine guns, sub machine guns, pistols, and more) + - Battlefield (Assault rifles, SMGs, carbines, pistols, and more) + +One huge advantage for this method is it's really light on performance. +Sending a couple hundred rays through space is *way* easier for the computer to calculate than sending a couple hundred +bullet objects. + +Another advantage is we can instantly know if we've hit something or not exactly when we call for it. For networking this is important because we do not need +to sync the bullet movements over the Internet, we just need to send whether or not the raycast hit. + +Raycasting does have some disadvantages though. One major disadvantage is we cannot easily cast a ray in anything but a linear line. +This means we can only fire in a straight line for however long our ray length is. You can create the illusion of bullet movement by casting +multiple rays at different positions, but not only is this hard to implement in code, it is also is heavier on performance. + +Another disadvantage is we cannot see the bullet. With bullet objects we can actually see the bullet travel through space if we attach a mesh +to it, but because raycasts happen instantly, we do not really have a decent way of showing the bullets. You could draw a line from the origin of the +raycast to the point where the raycast collided, and that is one popular way of showing raycasts. Another way is simply not drawing the raycast +at all, because theoretically the bullets move so fast our eyes could not see it anyway. + +_________ + +Lets get the bullet object setup. This is what our pistol will create when the "Pistol_fire" animation callback function is called. + +Open up ``Bullet_Scene.tscn``. The scene contains :ref:`Spatial ` node called bullet, with a :ref:`MeshInstance ` +and an :ref:`Area ` with a :ref:`CollisionShape ` childed to it. + +Create a new script called ``Bullet_script.gd`` and attach it to the ``Bullet`` :ref:`Spatial `. + +We are going to move the entire bullet object at the root (``Bullet``). We will be using the :ref:`Area ` to check whether or not we've collided with something + +.. note:: Why are we using a :ref:`Area ` and not a :ref:`RigidBody `? The mean reason we're not using a :ref:`RigidBody ` + is because we do not want the bullet to interact with other :ref:`RigidBody ` nodes. + By using an :ref:`Area ` we are assuring that none of the other :ref:`RigidBody ` nodes, including other bullets, will be effected. + + Another reason is simply because it is easier to detect collisions with a :ref:`Area `! + +Here's the script that will control our bullet: + +:: + + extends Spatial + + const BULLET_SPEED = 80 + const BULLET_DAMAGE = 15 + + const KILL_TIMER = 4 + var timer = 0 + + var hit_something = false + + func _ready(): + get_node("Area").connect("body_entered", self, "collided") + set_physics_process(true) + + + func _physics_process(delta): + var forward_dir = global_transform.basis.z.normalized() + global_translate(forward_dir * BULLET_SPEED * delta) + + timer += delta; + if timer >= KILL_TIMER: + queue_free() + + + func collided(body): + if hit_something == false: + if body.has_method("bullet_hit"): + body.bullet_hit(BULLET_DAMAGE, self.global_transform.origin) + + hit_something = true + queue_free() + + +Lets go through the script: + +_________ + +First we define a few global variables: + +- ``BULLET_SPEED``: The speed the bullet travels at. +- ``BULLET_DAMAGE``: The damage the bullet will cause to whatever it collides with. +- ``KILL_TIMER``: How long the bullet can last without hitting anything. +- ``timer``: A float for tracking how long we've been alive. +- ``hit_something``: A boolean for tracking whether or not we've hit something. + +With the exception of ``timer`` and ``hit_something``, all of these variables +change how the bullet interacts with the world. + +.. note:: The reason we are using a kill timer is so we do not have a case where we + get a bullet traveling forever. By using a kill timer, we can assure that + no bullets will just travel forever and consume resources. + +_________ + +In ``_ready`` we set the area's ``body_entered`` signal to ourself so that it calls +the ``collided`` function. Then we set ``_physics_process`` to ``true``. + +_________ + +``_physics_process`` gets the bullet's local ``Z`` axis. If you look in at the scene +in local mode, you will find that the bullet faces the positive local ``Z`` axis. + +Next we translate the entire bullet by that forward direction, multiplying in our speed and delta time. + +After that we add delta time to our timer and check if the timer has as long or longer +than our ``KILL_TIME`` constant. If it has, we use ``queue_free`` to free ourselves. + +_________ + +In ``collided`` we check if we've hit something yet or not. + +Remember that ``collided`` is +only called when a body has entered the :ref:`Area ` node. If we have not already collided with +something, we the proceed to check if the body we've collided with has a function/method +called ``bullet_hit``. If it does, we call it and pass in our damage and our position. + +.. note:: in ``collided``, the passed in body can be a :ref:`StaticBody `, + :ref:`RigidBody `, or :ref:`KinematicBody ` + +Then we set ``hit_something`` to ``true`` because regardless of whether or not the body +the bullet collided with has the ``bullet_hit`` function/method, it have hit something. + +Then we free the bullet using ``queue_free``. + +.. tip:: You may be wondering why we even have a ``hit_something`` variable if we + free the bullet using ``queue_free`` as soon as it hits something. + + The reason we need to track whether we've hit something or not is because ``queue_free`` + does not immediately free the node, so the bullet could collide with another body + before Godot has a chance to free it. By tracking if the bullet has hit something + we can make sure that the bullet will only hit one object. + + +_________ + +Before we start programming the player again, let's take a quick look at ``Player.tscn``. +Open up ``Player.tscn`` again. + +Expand ``Rotation_helper`` and notice how it has two nodes: ``Gun_fire_points`` and +``Gun_aim_point``. + +``Gun_aim_point`` is the point that the bullets will be aiming at. Notice how it +is lined up with the center of the screen and pulled a distance forward on the Z +axis. ``Gun_aim_point`` will serve as the point where the bullets will for sure collied +with as it goes along. + +.. note:: There is a invisible mesh instance for debugging purposes. The mesh is + a small sphere that visually shows where the bullets will be aiming at. + +Open up ``Gun_fire_points`` and you'll find three more :ref:`Spatial ` nodes, one for each +weapon. + +Open up ``Rifle_point`` and you'll find a :ref:`Raycast ` node. This is where +we will be sending the raycasts for our rilfe's bullets. +The length of the raycast will dictate how far our the bullets will travel. + +We are using a :ref:`Raycast ` node to handle the rifle's bullet because +we want to fire lots of bullets quickly. If we use bullet objects, it is quite possible +we could run into performance issues on older machines. + +.. note:: If you are wondering where the positions of the points came from, they + are the rough positions of the ends of each weapon. You can see this by + going to ``AnimationPlayer``, selecting one of the firing animations + and scrubbing through the timeline. The point for each weapon should mostly line + up with the end of each weapon. + +Open up ``Knife_point`` and you'll find a :ref:`Area ` node. We are using a :ref:`Area ` for the knife +because we only care for all of the bodies close to us, and because our knife does +not fire into space. If we were making a throwing knife, we would likely spawn a bullet +object that looks like a knife. + +Finally, we have ``Pistol point``. This is the point where we will be creating/instancing +our bullet objects. We do not need any additional nodes here, as the bullet handles all +of its own collision detection. + +Now that we've seen how we will handle our other weapons, and where we will spawn the bullets, +let's start working on making them work. + +.. note:: You can also look at the HUD nodes if you want. There is nothing fancy there and other + than using a single :ref:`Label `, we will not be touching any of those nodes. + Check :ref:`doc_design_interfaces_with_the_control_nodes` for a tutorial on using GUI nodes. + + The GUI provided in this tutorial is *very* basic. Maybe in a later part we will + revise the GUI, but for now we are going to just use this GUI as it will serve our needs for now. + +Making the weapons work +----------------------- + +Lets start making the weapons work in ``Player.gd``. + +First lets start by adding some global variables we'll need for the weapons: + +:: + + # Place before _ready + var animation_manager; + + var current_gun = "UNARMED" + var changing_gun = false + + var bullet_scene = preload("Bullet_Scene.tscn") + + var health = 100 + + const RIFLE_DAMAGE = 4 + const KNIFE_DAMAGE = 40 + + var UI_status_label + +Let's go over what these new variables will do: + +- ``animation_manager``: This will hold the :ref:`AnimationPlayer ` node and its script, which we wrote previously. +- ``current_gun``: This is the name of the gun we are currently using. It has four possible values: ``UNARMED``, ``KNIFE``, ``PISTOL``, and ``RIFLE``. +- ``changing_gun``: A boolean to track whether or not we are changing guns/weapons. +- ``bullet_scene``: The bullet scene we worked on earlier, ``Bullet_Scene.tscn``. We need to load it here so we can create/spawn it when the pistol fires +- ``health``: How much health our player has. In this part of the tutorial we will not really be using it. +- ``RIFLE_DAMAGE``: How much damage a single rifle bullet causes. +- ``KNIFE_DAMAGE``: How much damage a single knife stab/swipe causes. +- ``UI_status_label``: A label to show how much health we have, and how much ammo we have both in our gun and in reserves. + +_________ + +Next we need to add a few things in ``_ready``. Here's the new ``_ready`` function: + +:: + + func _ready(): + camera = get_node("Rotation_helper/Camera") + rotation_helper = get_node("Rotation_helper") + + animation_manager = get_node("Rotation_helper/Model/AnimationPlayer") + animation_manager.callback_function = funcref(self, "fire_bullet") + + set_physics_process(true) + + Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) + set_process_input(true) + + # Make sure the bullet spawn point, the raycast, and the knife area are all aiming at the center of the screen + var gun_aim_point_pos = get_node("Rotation_helper/Gun_aim_point").global_transform.origin + get_node("Rotation_helper/Gun_fire_points/Pistol_point").look_at(gun_aim_point_pos, Vector3(0, 1, 0)) + get_node("Rotation_helper/Gun_fire_points/Rifle_point").look_at(gun_aim_point_pos, Vector3(0, 1, 0)) + get_node("Rotation_helper/Gun_fire_points/Knife_point").look_at(gun_aim_point_pos, Vector3(0, 1, 0)) + + # Because we have the camera rotated by 180 degrees, we need to rotate the points around by 180 + # degrees on their local Y axis because otherwise the bullets will fire backwards + get_node("Rotation_helper/Gun_fire_points/Pistol_point").rotate_object_local(Vector3(0, 1, 0), deg2rad(180)) + get_node("Rotation_helper/Gun_fire_points/Rifle_point").rotate_object_local(Vector3(0, 1, 0), deg2rad(180)) + get_node("Rotation_helper/Gun_fire_points/Knife_point").rotate_object_local(Vector3(0, 1, 0), deg2rad(180)) + + UI_status_label = get_node("HUD/Panel/Gun_label") + flashlight = get_node("Rotation_helper/Flashlight") + +Let's go over what's changed. + +First we get the :ref:`AnimationPlayer ` node and assign it to our animation_manager variable. Then we set the callback function +to a :ref:`FuncRef ` that will call the player's ``fire_bullet`` function. Right now we haven't written our fire_bullet function, +but we'll get there soon. + +Then we get all of the weapon points and call each of their ``look_at``. +This will make sure they all are facing the gun aim point, which is in the center of our camera at a certain distance back. + +Next we rotate all of those weapon points by ``180`` degrees on their ``Y`` axis. This is because our camera is pointing backwards. +If we did not rotate all of these weapon points by ``180`` degrees, all of the weapons would fire backwards at ourselves. + +Finally, we get the UI :ref:`Label ` from our HUD. + +_________ + +Lets add a few things to ``_physics_process`` so we can fire our weapons. Here's the new code: + +:: + + func _physics_process(delta): + var dir = Vector3() + var cam_xform = camera.get_global_transform() + + + if Input.is_action_pressed("movement_forward"): + dir += -cam_xform.basis.z.normalized() + if Input.is_action_pressed("movement_backward"): + dir += cam_xform.basis.z.normalized() + if Input.is_action_pressed("movement_left"): + dir += -cam_xform.basis.x.normalized() + if Input.is_action_pressed("movement_right"): + dir += cam_xform.basis.x.normalized() + + + if is_on_floor(): + if Input.is_action_just_pressed("movement_jump"): + if is_sprinting: + vel.y = SPRINT_JUMP_SPEED + else: + vel.y = JUMP_SPEED + + if Input.is_action_just_pressed("flashlight"): + if flashlight.is_visible_in_tree(): + flashlight.hide() + else: + flashlight.show() + + dir.y = 0 + dir = dir.normalized() + + var grav = 0 + if Input.is_action_pressed("movement_sprint"): + is_sprinting = true + grav = sprint_grav + else: + is_sprinting = false; + grav = norm_grav + + vel.y += delta*grav + + var hvel = vel + hvel.y = 0 + + var target = dir + if is_sprinting: + target *= MAX_SPRINT_SPEED + else: + target *= MAX_SPEED + + var accel + if dir.dot(hvel) > 0: + if is_sprinting: + accel = SPRINT_ACCEL + else: + accel = ACCEL + else: + accel = DEACCEL + + hvel = hvel.linear_interpolate(target, accel*delta) + vel.x = hvel.x + vel.z = hvel.z + vel = move_and_slide(vel,Vector3(0,1,0), 0.05, 4, deg2rad(MAX_SLOPE_ANGLE)) + + + if Input.is_action_just_pressed("ui_cancel"): + if Input.get_mouse_mode() == Input.MOUSE_MODE_VISIBLE: + Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) + else: + Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) + + # NEW CODE + if changing_gun == false: + if Input.is_key_pressed(KEY_1): + current_gun = "UNARMED" + changing_gun = true + elif Input.is_key_pressed(KEY_2): + current_gun = "KNIFE" + changing_gun = true + elif Input.is_key_pressed(KEY_3): + current_gun = "PISTOL" + changing_gun = true + elif Input.is_key_pressed(KEY_4): + current_gun = "RIFLE" + changing_gun = true + + if changing_gun == true: + if current_gun != "PISTOL": + if animation_manager.current_state == "Pistol_idle": + animation_manager.set_animation("Pistol_unequip") + if current_gun != "RIFLE": + if animation_manager.current_state == "Rifle_idle": + animation_manager.set_animation("Rifle_unequip") + if current_gun != "KNIFE": + if animation_manager.current_state == "Knife_idle": + animation_manager.set_animation("Knife_unequip") + + if current_gun == "UNARMED": + if animation_manager.current_state == "Idle_unarmed": + changing_gun = false + + elif current_gun == "KNIFE": + if animation_manager.current_state == "Knife_idle": + changing_gun = false + if animation_manager.current_state == "Idle_unarmed": + animation_manager.set_animation("Knife_equip") + + elif current_gun == "PISTOL": + if animation_manager.current_state == "Pistol_idle": + changing_gun = false + if animation_manager.current_state == "Idle_unarmed": + animation_manager.set_animation("Pistol_equip") + + elif current_gun == "RIFLE": + if animation_manager.current_state == "Rifle_idle": + changing_gun = false + if animation_manager.current_state == "Idle_unarmed": + animation_manager.set_animation("Rifle_equip") + + + # Firing the weapons + if Input.is_action_pressed("fire"): + if current_gun == "PISTOL": + if animation_manager.current_state == "Pistol_idle": + animation_manager.set_animation("Pistol_fire") + + elif current_gun == "RIFLE": + if animation_manager.current_state == "Rifle_idle": + animation_manager.set_animation("Rifle_fire") + + elif current_gun == "KNIFE": + if animation_manager.current_state == "Knife_idle": + animation_manager.set_animation("Knife_fire") + + # HUD (UI) + UI_status_label.text = "HEALTH: " + str(health) + + +Lets go over it one chunk at a time: + +_________ + +First we have a if check to see if ``changing_gun`` is equal to ``false``. If it is, we +then check to see if the number keys ``1`` through ``4`` are pressed. If one of the keys +are pressed, we set current gun to the name of each weapon assigned to each key and set +``changing_gun`` to ``true``. + +Then we check if ``changing_gun`` is ``true``. If it is ``true``, we then go through a series of checks. +The first set of checks is checking if we are in a idle animation that is not the weapon/gun we are trying +to change to, as then we'd be stuck in a loop. + +Then we check if we are in an unarmed state. If we are and the newly selected 'weapon' +is ``UNARMED``, then we set ``changing_gun`` to ``false``. + +If we are trying to change to any of the other weapons, we first check if we are in the +desired weapon's idle state. If we are, then we've successfully changed weapons and set +``changing_gun`` to false. + +If we are not in the desired weapon's idle state, we then check if we are in the idle unarmed state. +This is because all unequip animations transition to idle unarmed, and because we can transition to +any equip animation from idle unarmed. + +If we are in the idle unarmed state, we set the animation to the equip animation for the +desired weapon. Once the equip animation is finished, it will change to the idle state for that +weapon, which will pass the ``if`` check above. + +_________ + +For firing the weapons we first check if the ``fire`` action is pressed or not. +If the fire action is pressed, we then check which weapon we are using. + +If we are in a weapon's idle state, we then call set the animation to the weapon's fire animation. + +_________ + +Now, we just need to add one more function to the player, and then the player is ready to start shooting! + +We just need to add ``fire_bullet``, which will be called when by the :ref:`AnimationPlayer ` at those +points we set earlier in the :ref:`AnimationPlayer ` function track: + +:: + + func fire_bullet(): + if changing_gun == true: + return + + # Pistol bullet handling: Spawn a bullet object! + if current_gun == "PISTOL": + var clone = bullet_scene.instance() + var scene_root = get_tree().root.get_children()[0] + scene_root.add_child(clone) + + clone.global_transform = get_node("Rotation_helper/Gun_fire_points/Pistol_point").global_transform + # The bullet is a little too small (by default), so let's make it bigger! + clone.scale = Vector3(4, 4, 4) + + # Rifle bullet handeling: Send a raycast! + elif current_gun == "RIFLE": + var ray = get_node("Rotation_helper/Gun_fire_points/Rifle_point/RayCast") + ray.force_raycast_update() + + if ray.is_colliding(): + var body = ray.get_collider() + if body.has_method("bullet_hit"): + body.bullet_hit(RIFLE_DAMAGE, ray.get_collision_point()) + + # Knife bullet(?) handeling: Use an area! + elif current_gun == "KNIFE": + var area = get_node("Rotation_helper/Gun_fire_points/Knife_point/Area") + var bodies = area.get_overlapping_bodies() + + for body in bodies: + if body.has_method("bullet_hit"): + body.bullet_hit(KNIFE_DAMAGE, area.global_transform.origin) + + +Lets go over what this function is doing: + +_________ + +First we check if we are changing weapons or not. If we are changing weapons, we do not want shoot so we just ``return``. + +.. tip:: Calling ``return`` stops the rest of the function from being called. In this case, we are not returning a variable + because we are only interested in not running the rest of the code, and because we are not looking for a returned + variable either when we call this function. + +Next we check which weapon we are using. + +If we are using a pistol, we first create a ``Bullet_Scene.tscn`` instance and assign it to +a variable named ``clone``. Then we get the root node in the :ref:`SceneTree `, which happens to be a :ref:`Viewport `. +We then get the first child of the :ref:`Viewport ` and assign it to the ``scene_root`` variable. + +We then add our newly instanced/created bullet as a child of ``scene_root``. + +.. warning:: As mentioned later below in the section on adding sounds, this method makes a assumption. This will be explained later + in the section on adding sounds in :ref:`doc_fps_tutorial_part_three` + +Next we set the global :ref:`Transform ` of the bullet to that of the pistol bullet spawn point we +talked about earlier. + +Finally, we set the scale a little bigger because the bullet normally is too small to see. + +_______ + +For the rifle, we first get the :ref:`Raycast ` node and assign it to a variable called ``ray``. +Then we call :ref:`Raycast `'s ``force_raycast_update`` function. + +``force_raycast_update`` sends the :ref:`Raycast ` out and collects the collision data as soon as we call it, +meaning we get frame perfect collision data and we do not need to worry about performance issues by having the +:ref:`Raycast ` enabled all the time. + +Next we check if the :ref:`Raycast ` collided with anything. If it has, we then get the collision body +it collided with. If the body has the ``bullet_hit`` method/function, we then call it and pass +in ``RIFLE_DAMAGE`` and the position where the :ref:`Raycast ` collided. + +.. tip:: Remember how we mentioned the speed of the animations for firing was faster than + the other animations? By changing the firing animation speeds, you can change how + fast the weapon fires bullets! + +_______ + +For the knife we first get the :ref:`Area ` node and assign it to a variable named ``area``. +Then we get all of the collision bodies inside the :ref:`Area `. We loop through each one +and check if they have the ``bullet_hit`` method/function. If they do, we call it and pass +in ``KNIFE_DAMAGE`` and the global position of :ref:`Area `. + +.. note:: While we could attempt to calculate a rough location for where the knife hit, we + do not bother because using the area's position works well enough and the extra time + needed to calculate a rough position for each body is not really worth the effort. + +_______ + + +Before we are ready to test our new weapons, we still have just a little bit of work to do. + +Creating some test subjects +--------------------------- + +Create a new script by going to the scripting window, clicking "file", and selecting new. +Name this script "RigidBody_hit_test" and make sure it extends :ref:`RigidBody `. + +Now we just need to add this code: + +:: + + extends RigidBody + + func _ready(): + pass + + func bullet_hit(damage, bullet_hit_pos): + var direction_vect = self.global_transform.origin - bullet_hit_pos + direction_vect = direction_vect.normalized() + + self.apply_impulse(bullet_hit_pos, direction_vect * damage) + + +Lets go over how ``bullet_hit`` works: + +First we get the direction from the bullet pointing towards our global :ref:`Transform `. +We do this by subtracting the bullet's hit position from the :ref:`RigidBody `'s position. +This results in a :ref:`Vector3 ` that we can use to tell the direction the bullet collided into the +:ref:`RigidBody ` at. + +We then normalize it so we do not get crazy results from collisions on the extremes +of the collision shape attached to the :ref:`RigidBody `. Without normalizing shots farther +away from the center of the :ref:`RigidBody ` would cause a more noticeable reaction than +those closer to the center. + +Finally, we apply an impulse at the passed in bullet collision position. With the force +being the directional vector times the damage the bullet is supposed to cause. This makes +the :ref:`RigidBody ` seem to move in response to the bullet colliding into it. + +_______ + +Now we just need to attach this script to all of the :ref:`RigidBody ` nodes we want to effect. + +Open up ``Testing_Area.tscn`` and select all of the cubes parented to the ``Cubes`` node. + +.. tip:: If you select the top cube, and then hold down ``shift`` and select the last cube, Godot will + select all of the cubes in between! + +Once you have all of the cubes selected, scroll down in the inspector until you get to the +the "scripts" section. Click the drop down and select "Load". Open your newly created ``RigidBody_hit_test.gd`` script. + +With that done, go give your guns a whirl! You should now be able to fire as many bullets as you want on the cubes and +they will move in response to the bullets colliding into them. + +In :ref:`doc_fps_tutorial_part_three`, we will add ammo to the guns, as well as some sounds! diff --git a/tutorials/3d/index.rst b/tutorials/3d/index.rst index 0bda63e12..34ec845fd 100644 --- a/tutorials/3d/index.rst +++ b/tutorials/3d/index.rst @@ -18,3 +18,4 @@ working_with_3d_skeletons inverse_kinematics mesh_generation_with_heightmap_and_shaders + fps_tutorial/index