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.
BIN
tutorials/3d/fps_tutorial/files/Godot_FPS_BlenderFiles.zip
Normal file
BIN
tutorials/3d/fps_tutorial/files/Godot_FPS_Finished.zip
Normal file
BIN
tutorials/3d/fps_tutorial/files/Godot_FPS_Starter.zip
Normal file
BIN
tutorials/3d/fps_tutorial/img/AnimationPlayerAddPoint.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
tutorials/3d/fps_tutorial/img/AnimationPlayerAddTrack.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
tutorials/3d/fps_tutorial/img/AnimationPlayerCallFuncTrack.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
tutorials/3d/fps_tutorial/img/AnimationPlayerEditPoints.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
tutorials/3d/fps_tutorial/img/FinishedTutorialPicture.png
Normal file
|
After Width: | Height: | Size: 450 KiB |
BIN
tutorials/3d/fps_tutorial/img/LocalSpaceExample.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
tutorials/3d/fps_tutorial/img/LocalSpaceExampleGizmo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
tutorials/3d/fps_tutorial/img/LocalSpaceExample_3D.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
tutorials/3d/fps_tutorial/img/PartOneFinished.png
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
tutorials/3d/fps_tutorial/img/PartThreeFinished.png
Normal file
|
After Width: | Height: | Size: 474 KiB |
BIN
tutorials/3d/fps_tutorial/img/PartTwoFinished.png
Normal file
|
After Width: | Height: | Size: 274 KiB |
BIN
tutorials/3d/fps_tutorial/img/PlayerSceneTree.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
tutorials/3d/fps_tutorial/img/WorldSpaceExample.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
tutorials/3d/fps_tutorial/img/WorldSpaceExample_3D.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
10
tutorials/3d/fps_tutorial/index.rst
Normal file
@@ -0,0 +1,10 @@
|
||||
FPS tutorial
|
||||
============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:name: toc-3D-fps-tutorial
|
||||
|
||||
part_one
|
||||
part_two
|
||||
part_three
|
||||
556
tutorials/3d/fps_tutorial/part_one.rst
Normal 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`.
|
||||
705
tutorials/3d/fps_tutorial/part_three.rst
Normal 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!
|
||||