Added a FPS (First person shooter) game tutorial in three parts.

* The first part covers making a first person character.
* The second part covers adding weapons/guns to the character built in part one.
* The third part adds ammo to the guns and sounds for when the player fires.
This commit is contained in:
TwistedTwigleg
2018-02-22 13:48:08 -05:00
parent f88d8b1bdf
commit c33d9e744c
22 changed files with 2276 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,10 @@
FPS tutorial
============
.. toctree::
:maxdepth: 1
:name: toc-3D-fps-tutorial
part_one
part_two
part_three

View File

@@ -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 <files/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 <class_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 <class_Camera>` node.
- ``rotation_helper``: A :ref:`Spatial <class_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 <class_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 <class_Input>` 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 <class_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 <class_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 <class_Spatial>` node's local space, we need to get its :ref:`Transform <class_Transform>`, so then we
can get the :ref:`Basis <class_Basis>` from the :ref:`Transform <class_Transform>`.
Each :ref:`Basis <class_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 <class_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 <class_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 <class_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 <class_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 <class_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 <class_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 <doc_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 <class_InputEventMouseMotion>`.
First we rotate the ``rotation_helper`` node on the ``X`` axis, using the relative mouse motion's
``Y`` value, provided by :ref:`InputEventMouseMotion <class_InputEventMouseMotion>`.
Then we rotate the entire :ref:`KinematicBody <class_KinematicBody>` on the ``Y`` axis by the relative mouse motion's ``X`` value.
.. tip:: Godot converts relative mouse motion into a :ref:`Vector2 <class_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`.

View File

@@ -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 <class_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 <class_Spatial>` gizmo.
This will bring you to the crate's scene. From there, select the ``Crate`` :ref:`RigidBody <class_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 <class_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 <class_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 <class_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 <class_Spatial>` with a :ref:'AudioStreamPlayer <class_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 <class_AudioStreamPlayer>' and select "Change type".
This will open the node browser. Navigate to :ref:'AudioStreamPlayer3D <class_AudioStreamPlayer3D>' and select "change".
In the source for this tutorial, we will be using :ref:'AudioStreamPlayer <class_AudioStreamPlayer>', but you can optionally
use :ref:'AudioStreamPlayer3D <class_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 <class_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 <class_AudioStreamPlayer>' and connect it's ``finished`` signal to ourselves.
It doesn't matter if it's :ref:'AudioStreamPlayer <class_AudioStreamPlayer>' or :ref:'AudioStreamPlayer3D <class_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 <class_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 <class_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 <class_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 <class_AudioStreamPlayer>' to play.
When the :ref:'AudioStreamPlayer <class_AudioStreamPlayer>' is finished playing the sound, it will call ``destroy_self`` because
we connected the ``finished`` signal in ``_ready``. We stop the :ref:'AudioStreamPlayer <class_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 <class_SceneTree>`,
and then access the root node, which in this case is the :ref:`Viewport <class_Viewport>` this entire game is running under.
Then we get the first child of the :ref:`Viewport <class_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 <class_transform>` and the :ref:`Raycast <class_raycast>`'s global :ref:`Transform <class_transform>`.
If you are not using a :ref:`AudioStreamPlayer3D <class_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 <files/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 <files/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!

File diff suppressed because it is too large Load Diff