remove FPS tutorial to match master branch

This commit is contained in:
Nathan Lovato
2021-11-24 21:18:56 -06:00
parent e16e8a463e
commit 4d97da48dc
32 changed files with 0 additions and 5599 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 577 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

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

View File

@@ -1,967 +0,0 @@
.. _doc_fps_tutorial_part_five:
Part 5
======
Part overview
-------------
In this part, we're going to add grenades to the player, give the player the ability to grab and throw objects, and add turrets!
.. image:: img/PartFiveFinished.png
.. note:: You are assumed to have finished :ref:`doc_fps_tutorial_part_four` before moving on to this part of the tutorial.
The finished project from :ref:`doc_fps_tutorial_part_four` will be the starting project for part 5
Let's get started!
Adding grenades
---------------
Firstly, let's give the player some grenades to play with. Open up ``Grenade.tscn``.
There are a few things to note here, the first and foremost being that the grenades are going to use :ref:`RigidBody <class_RigidBody>` nodes.
We're going to use :ref:`RigidBody <class_RigidBody>` nodes for our grenades so they bounce around the world in a (somewhat) realistic manner.
The second thing to note is ``Blast_Area``. This is an :ref:`Area <class_Area>` node that will represent the blast radius of the grenade.
Finally, the last thing to note is ``Explosion``. This is the :ref:`Particles <class_Particles>` node that will emit an explosion effect when
the grenade explodes. One thing to note here is that we have ``One shot`` enabled. This is so we emit all the particles at once. The particles are also emitted using world
coordinates instead of local coordinates, so we have ``Local Coords`` unchecked as well.
.. note:: If you want, you can see how the particles are set up by looking through the particle's ``Process Material`` and ``Draw Passes``.
Let's write the code needed for the grenade. Select ``Grenade`` and make a new script called ``Grenade.gd``. Add the following:
::
extends RigidBody
const GRENADE_DAMAGE = 60
const GRENADE_TIME = 2
var grenade_timer = 0
const EXPLOSION_WAIT_TIME = 0.48
var explosion_wait_timer = 0
var rigid_shape
var grenade_mesh
var blast_area
var explosion_particles
func _ready():
rigid_shape = $Collision_Shape
grenade_mesh = $Grenade
blast_area = $Blast_Area
explosion_particles = $Explosion
explosion_particles.emitting = false
explosion_particles.one_shot = true
func _process(delta):
if grenade_timer < GRENADE_TIME:
grenade_timer += delta
return
else:
if explosion_wait_timer <= 0:
explosion_particles.emitting = true
grenade_mesh.visible = false
rigid_shape.disabled = true
mode = RigidBody.MODE_STATIC
var bodies = blast_area.get_overlapping_bodies()
for body in bodies:
if body.has_method("bullet_hit"):
body.bullet_hit(GRENADE_DAMAGE, body.global_transform.looking_at(global_transform.origin, Vector3(0, 1, 0)))
# This would be the perfect place to play a sound!
if explosion_wait_timer < EXPLOSION_WAIT_TIME:
explosion_wait_timer += delta
if explosion_wait_timer >= EXPLOSION_WAIT_TIME:
queue_free()
Let's go over what's happening, starting with the class variables:
* ``GRENADE_DAMAGE``: The amount of damage the grenade causes when it explodes.
* ``GRENADE_TIME``: The amount of time the grenade takes (in seconds) to explode once it's created/thrown.
* ``grenade_timer``: A variable for tracking how long the grenade has been created/thrown.
* ``EXPLOSION_WAIT_TIME``: The amount of time needed (in seconds) to wait before we destroy the grenade scene after the explosion
* ``explosion_wait_timer``: A variable for tracking how much time has passed since the grenade exploded.
* ``rigid_shape``: The :ref:`CollisionShape <class_CollisionShape>` for the grenade's :ref:`RigidBody <class_RigidBody>`.
* ``grenade_mesh``: The :ref:`MeshInstance <class_MeshInstance>` for the grenade.
* ``blast_area``: The blast :ref:`Area <class_Area>` used to damage things when the grenade explodes.
* ``explosion_particles``: The :ref:`Particles <class_Particles>` that come out when the grenade explodes.
Notice how ``EXPLOSION_WAIT_TIME`` is a rather strange number (``0.48``). This is because we want ``EXPLOSION_WAIT_TIME`` to be equal to the length of time
the explosion particles are emitting, so when the particles are done we destroy/free the grenade. We calculate ``EXPLOSION_WAIT_TIME`` by taking the particle's life time
and dividing it by the particle's speed scale. This gets us the exact time the explosion particles will last.
______
Now let's turn our attention to ``_ready``.
First we get all the nodes we'll need and assign them to the proper class variables.
We need to get the :ref:`CollisionShape <class_CollisionShape>` and :ref:`MeshInstance <class_MeshInstance>` because similarly to the target in :ref:`doc_fps_tutorial_part_four`,
we will be hiding the grenade's mesh and disabling the collision shape when the grenade explodes.
The reason we need to get the blast :ref:`Area <class_Area>` is so we can damage everything inside it when the grenade explodes. We'll be using code similar to the knife
code in the player. We need the :ref:`Particles <class_Particles>` so we can emit particles when the grenade explodes.
After we get all the nodes and assign them to their class variables, we then make sure the explosion particles are not emitting, and that they are set to
emit in one shot. This is to be extra sure the particles will behave the way we expect them to.
______
Now let's look at ``_process``.
Firstly, we check to see if the ``grenade_timer`` is less than ``GRENADE_TIME``. If it is, we add ``delta`` and return. This is so the grenade has to wait ``GRENADE_TIME`` seconds
before exploding, allowing the :ref:`RigidBody <class_RigidBody>` to move around.
If ``grenade_timer`` is at ``GRENADE_TIMER`` or higher, we then need to check if the grenade has waited long enough and needs to explode. We do this by checking to see
if ``explosion_wait_timer`` is equal to ``0`` or less. Since we will be adding ``delta`` to ``explosion_wait_timer`` right after, whatever code under the check
will only be called once, right when the grenade has waited long enough and needs to explode.
If the grenade has waited long enough to explode, we first tell the ``explosion_particles`` to emit. Then we make ``grenade_mesh`` invisible, and disable ``rigid_shape``, effectively
hiding the grenade.
We then set the :ref:`RigidBody <class_RigidBody>`'s mode to ``MODE_STATIC`` so the grenade does not move.
Then we get all the bodies in ``blast_area``, check to see if they have the ``bullet_hit`` method/function, and if they do, we call it and pass in ``GRENADE_DAMAGE`` and
the transform from the body looking at the grenade. This makes it where the bodies exploded by the grenade will explode outwards from the grenade's position.
We then check to see if ``explosion_wait_timer`` is less than ``EXPLOSION_WAIT_TIME``. If it is, we add ``delta`` to ``explosion_wait_timer``.
Next, we check to see if ``explosion_wait_timer`` is greater than or equal to ``EXPLOSION_WAIT_TIME``. Because we added ``delta``, this will only be called once.
If ``explosion_wait_timer`` is greater or equal to ``EXPLOSION_WAIT_TIME``, the grenade has waited long enough to let the :ref:`Particles <class_Particles>` play
and we can free/destroy the grenade, as we no longer need it.
______
Let's quickly get the sticky grenade set up too. Open up ``Sticky_Grenade.tscn``.
``Sticky_Grenade.tscn`` is almost identical to ``Grenade.tscn``, with one small addition. We now have a second
:ref:`Area <class_Area>`, called ``Sticky_Area``. We will be using ``Stick_Area`` to detect when the sticky grenade has collided with
the environment and needs to stick to something.
Select ``Sticky_Grenade`` and make a new script called ``Sticky_Grenade.gd``. Add the following:
::
extends RigidBody
const GRENADE_DAMAGE = 40
const GRENADE_TIME = 3
var grenade_timer = 0
const EXPLOSION_WAIT_TIME = 0.48
var explosion_wait_timer = 0
var attached = false
var attach_point = null
var rigid_shape
var grenade_mesh
var blast_area
var explosion_particles
var player_body
func _ready():
rigid_shape = $Collision_Shape
grenade_mesh = $Sticky_Grenade
blast_area = $Blast_Area
explosion_particles = $Explosion
explosion_particles.emitting = false
explosion_particles.one_shot = true
$Sticky_Area.connect("body_entered", self, "collided_with_body")
func collided_with_body(body):
if body == self:
return
if player_body != null:
if body == player_body:
return
if attached == false:
attached = true
attach_point = Spatial.new()
body.add_child(attach_point)
attach_point.global_transform.origin = global_transform.origin
rigid_shape.disabled = true
mode = RigidBody.MODE_STATIC
func _process(delta):
if attached == true:
if attach_point != null:
global_transform.origin = attach_point.global_transform.origin
if grenade_timer < GRENADE_TIME:
grenade_timer += delta
return
else:
if explosion_wait_timer <= 0:
explosion_particles.emitting = true
grenade_mesh.visible = false
rigid_shape.disabled = true
mode = RigidBody.MODE_STATIC
var bodies = blast_area.get_overlapping_bodies()
for body in bodies:
if body.has_method("bullet_hit"):
body.bullet_hit(GRENADE_DAMAGE, body.global_transform.looking_at(global_transform.origin, Vector3(0, 1, 0)))
# This would be the perfect place to play a sound!
if explosion_wait_timer < EXPLOSION_WAIT_TIME:
explosion_wait_timer += delta
if explosion_wait_timer >= EXPLOSION_WAIT_TIME:
if attach_point != null:
attach_point.queue_free()
queue_free()
The code above is almost identical to the code for ``Grenade.gd``, so let's just go over what's changed.
Firstly, we have a few more class variables:
* ``attached``: A variable for tracking whether or not the sticky grenade has attached to a :ref:`PhysicsBody <class_PhysicsBody>`.
* ``attach_point``: A variable to hold a :ref:`Spatial <class_Spatial>` that will be at the position where the sticky grenade collided.
* ``player_body``: The player's :ref:`KinematicBody <class_KinematicBody>`.
They have been added to enable the sticky grenade to stick to any :ref:`PhysicsBody <class_PhysicsBody>` it might hit. We also now
need the player's :ref:`KinematicBody <class_KinematicBody>` so the sticky grenade does not stick to the player when the player throws it.
______
Now let's look at the small change in ``_ready``. In ``_ready`` we've added a line of code so when any body enters ``Stick_Area``,
the ``collided_with_body`` function is called.
______
Next let's take a look at ``collided_with_body``.
Firstly, we make sure the sticky grenade is not colliding with itself.
Because the sticky :ref:`Area <class_Area>` does not know it's attached to the grenade's :ref:`RigidBody <class_RigidBody>`,
we need to make sure it's not going to stick to itself by checking to make sure the body it has collided with is not itself.
If we have collided with ourself, we ignore it by returning.
We then check to see if we have something assigned to ``player_body``, and if the body the sticky grenade has collided with is the player that threw it.
If the body the sticky grenade has collided with is indeed ``player_body``, we ignore it by returning.
Next, we check if the sticky grenade has attached to something already or not.
If the sticky grenade is not attached, we then set ``attached`` to ``true`` so we know the sticky grenade has attached to something.
We then make a new :ref:`Spatial <class_Spatial>` node, and make it a child of the body the sticky grenade collided with. We then set the :ref:`Spatial <class_Spatial>`'s position
to the sticky grenade's current global position.
.. note:: Because we've added the :ref:`Spatial <class_Spatial>` as a child of the body the sticky grenade has collided with, it will follow along with said body.
We can then use this :ref:`Spatial <class_Spatial>` to set the sticky grenade's position, so it is always at the same position relative to the body it collided with.
We then disable ``rigid_shape`` so the sticky grenade is not constantly moving whatever body it has collided with.
Finally, we set our mode to ``MODE_STATIC`` so the grenade does not move.
______
Finally, lets go over the few changes in ``_process``.
Now we're checking to see if the sticky grenade is attached right at the top of ``_process``.
If the sticky grenade is attached, we then make sure the attached point is not equal to ``null``.
If the attached point is not equal to ``null``, we set the sticky grenade's global position (using its global :ref:`Transform <class_Transform>`'s origin) to the global position of
the :ref:`Spatial <class_Spatial>` assigned to ``attach_point`` (using its global :ref:`Transform <class_Transform>`'s origin).
The only other change is now before we free/destroy the sticky grenade is to check to see if the sticky grenade has an attached point.
If it does, we also call ``queue_free`` on the attach point, so it's also freed/destroyed.
Adding grenades to the player
-----------------------------
Now we need to add some code to ``Player.gd`` so we can use the grenades.
Firstly, open up ``Player.tscn`` and expand the node tree until you get to ``Rotation_Helper``. Notice how in
``Rotation_Helper`` we have a node called ``Grenade_Toss_Pos``. This is where we will be spawning the grenades.
Also notice how it's slightly rotated on the ``X`` axis, so it's not pointing straight, but rather slightly up. By changing
the rotation of ``Grenade_Toss_Pos``, you can change the angle at which the grenades are tossed.
Okay, now let's start making the grenades work with the player. Add the following class variables to ``Player.gd``:
::
var grenade_amounts = {"Grenade":2, "Sticky Grenade":2}
var current_grenade = "Grenade"
var grenade_scene = preload("res://Grenade.tscn")
var sticky_grenade_scene = preload("res://Sticky_Grenade.tscn")
const GRENADE_THROW_FORCE = 50
* ``grenade_amounts``: The amount of grenades the player is currently carrying (for each type of grenade).
* ``current_grenade``: The name of the grenade the player is currently using.
* ``grenade_scene``: The grenade scene we worked on earlier.
* ``sticky_grenade_scene``: The sticky grenade scene we worked on earlier.
* ``GRENADE_THROW_FORCE``: The force at which the player will throw the grenades.
Most of these variables are similar to how we have our weapons set up.
.. tip:: While it's possible to make a more modular grenade system, I found it was not worth the additional complexity for just two grenades.
If you were going to make a more complex FPS with more grenades, you'd likely want to make a system for grenades similar to how we have the weapons set up.
______
Now we need to add some code in ``_process_input`` Add the following to ``_process_input``:
::
# ----------------------------------
# Changing and throwing grenades
if Input.is_action_just_pressed("change_grenade"):
if current_grenade == "Grenade":
current_grenade = "Sticky Grenade"
elif current_grenade == "Sticky Grenade":
current_grenade = "Grenade"
if Input.is_action_just_pressed("fire_grenade"):
if grenade_amounts[current_grenade] > 0:
grenade_amounts[current_grenade] -= 1
var grenade_clone
if current_grenade == "Grenade":
grenade_clone = grenade_scene.instance()
elif current_grenade == "Sticky Grenade":
grenade_clone = sticky_grenade_scene.instance()
# Sticky grenades will stick to the player if we do not pass ourselves
grenade_clone.player_body = self
get_tree().root.add_child(grenade_clone)
grenade_clone.global_transform = $Rotation_Helper/Grenade_Toss_Pos.global_transform
grenade_clone.apply_impulse(Vector3(0, 0, 0), grenade_clone.global_transform.basis.z * GRENADE_THROW_FORCE)
# ----------------------------------
Let's go over what's happening here.
Firstly, we check to see if the ``change_grenade`` action has just been pressed. If it has, we then check to see which grenade the player is
currently using. Based on the name of the grenade the player is currently using, we change ``current_grenade`` to the opposite grenade name.
Next we check to see if the ``fire_grenade`` action has just been pressed. If it has, we then check to see if the player has more than ``0`` grenades for the
current grenade type selected.
If the player has more than ``0`` grenades, we then remove one from the grenade amounts for the current grenade.
Then, based on the grenade the player is currently using, we instance the proper grenade scene and assign it to ``grenade_clone``.
Next we add ``grenade_clone`` as a child of the node at the root and set its global :ref:`Transform <class_Transform>` to
``Grenade_Toss_Pos``'s global :ref:`Transform <class_Transform>`. Finally, we apply an impulse to the grenade so that it is launched forward, relative
to the ``Z`` directional vector of ``grenade_clone``'s.
______
Now the player can use both types of grenades, but there are still a few things we should probably add before we move on to adding the other things.
We still need a way to show the player how many grenades are left, and we should probably add a way to get more grenades when the player picks up ammo.
Firstly, let's change some of the code in ``Player.gd`` to show how many grenades are left. Change ``process_UI`` to the following:
::
func process_UI(delta):
if current_weapon_name == "UNARMED" or current_weapon_name == "KNIFE":
# First line: Health, second line: Grenades
UI_status_label.text = "HEALTH: " + str(health) + \
"\n" + current_grenade + ": " + str(grenade_amounts[current_grenade])
else:
var current_weapon = weapons[current_weapon_name]
# First line: Health, second line: weapon and ammo, third line: grenades
UI_status_label.text = "HEALTH: " + str(health) + \
"\nAMMO: " + str(current_weapon.ammo_in_weapon) + "/" + str(current_weapon.spare_ammo) + \
"\n" + current_grenade + ": " + str(grenade_amounts[current_grenade])
Now we'll show how many grenades the player has left in the UI.
While we're still in ``Player.gd``, let's add a function to add grenades to the player. Add the following function to ``Player.gd``:
::
func add_grenade(additional_grenade):
grenade_amounts[current_grenade] += additional_grenade
grenade_amounts[current_grenade] = clamp(grenade_amounts[current_grenade], 0, 4)
Now we can add a grenade using ``add_grenade``, and it will automatically be clamped to a maximum of ``4`` grenades.
.. tip:: You can change the ``4`` to a constant if you want. You'd need to make a new global constant, something like ``MAX_GRENADES``, and
then change the clamp from ``clamp(grenade_amounts[current_grenade], 0, 4)`` to ``clamp(grenade_amounts[current_grenade], 0, MAX_GRENADES)``
If you do not want to limit how many grenades the player can carry, remove the line that clamps the grenades altogether!
Now we have a function to add grenades, let's open up ``AmmoPickup.gd`` and use it!
Open up ``AmmoPickup.gd`` and go to the ``trigger_body_entered`` function. Change it to the following:
::
func trigger_body_entered(body):
if body.has_method("add_ammo"):
body.add_ammo(AMMO_AMOUNTS[kit_size])
respawn_timer = RESPAWN_TIME
kit_size_change_values(kit_size, false)
if body.has_method("add_grenade"):
body.add_grenade(GRENADE_AMOUNTS[kit_size])
respawn_timer = RESPAWN_TIME
kit_size_change_values(kit_size, false)
Now we are also checking to see if the body has the ``add_grenade`` function. If it does, we call it like we call ``add_ammo``.
You may have noticed we are using a new constant we have not defined yet, ``GRENADE_AMOUNTS``. Let's add it! Add the following class variable
to ``AmmoPickup.gd`` with the other class variables:
::
const GRENADE_AMOUNTS = [2, 0]
* ``GRENADE_AMOUNTS``: The amount of grenades each pickup contains.
Notice how the second element in ``GRENADE_AMOUNTS`` is ``0``. This is so the small ammo pickup does not give the player
any additional grenades.
______
Now you should be able to throw grenades! Go give it a try!
Adding the ability to grab and throw RigidBody nodes to the player
------------------------------------------------------------------
Next, let's give the player the ability to pick up and throw :ref:`RigidBody <class_RigidBody>` nodes.
Open up ``Player.gd`` and add the following class variables:
::
var grabbed_object = null
const OBJECT_THROW_FORCE = 120
const OBJECT_GRAB_DISTANCE = 7
const OBJECT_GRAB_RAY_DISTANCE = 10
* ``grabbed_object``: A variable to hold the grabbed :ref:`RigidBody <class_RigidBody>` node.
* ``OBJECT_THROW_FORCE``: The force with which the player throws the grabbed object.
* ``OBJECT_GRAB_DISTANCE``: The distance away from the camera at which the player holds the grabbed object.
* ``OBJECT_GRAB_RAY_DISTANCE``: The distance the :ref:`Raycast <class_Raycast>` goes. This is the player's grab distance.
With that done, all we need to do is add some code to ``process_input``:
::
# ----------------------------------
# Grabbing and throwing objects
if Input.is_action_just_pressed("fire_grenade") and current_weapon_name == "UNARMED":
if grabbed_object == null:
var state = get_world().direct_space_state
var center_position = get_viewport().size / 2
var ray_from = camera.project_ray_origin(center_position)
var ray_to = ray_from + camera.project_ray_normal(center_position) * OBJECT_GRAB_RAY_DISTANCE
var ray_result = state.intersect_ray(ray_from, ray_to, [self, $Rotation_Helper/Gun_Fire_Points/Knife_Point/Area])
if !ray_result.empty():
if ray_result["collider"] is RigidBody:
grabbed_object = ray_result["collider"]
grabbed_object.mode = RigidBody.MODE_STATIC
grabbed_object.collision_layer = 0
grabbed_object.collision_mask = 0
else:
grabbed_object.mode = RigidBody.MODE_RIGID
grabbed_object.apply_impulse(Vector3(0, 0, 0), -camera.global_transform.basis.z.normalized() * OBJECT_THROW_FORCE)
grabbed_object.collision_layer = 1
grabbed_object.collision_mask = 1
grabbed_object = null
if grabbed_object != null:
grabbed_object.global_transform.origin = camera.global_transform.origin + (-camera.global_transform.basis.z.normalized() * OBJECT_GRAB_DISTANCE)
# ----------------------------------
Let's go over what's happening.
Firstly, we check to see if the action pressed is the ``fire`` action, and that the player is using the ``UNARMED`` 'weapon'.
This is because we only want the player to be able to pick up and throw objects when the player is not using any weapons. This is a design choice,
but I feel it gives ``UNARMED`` a use.
Next we check to see whether or not ``grabbed_object`` is ``null``.
______
If ``grabbed_object`` is ``null``, we want to see if we can pick up a :ref:`RigidBody <class_RigidBody>`.
We first get the direct space state from the current :ref:`World <class_World>`. This is so we can cast a ray entirely from code, instead of having to
use a :ref:`Raycast <class_Raycast>` node.
.. note:: See :ref:`Ray-casting <doc_ray-casting>` for more information on raycasting in Godot.
Then we get the center of the screen by dividing the current :ref:`Viewport <class_Viewport>` size in half. We then get the ray's origin point and end point using
``project_ray_origin`` and ``project_ray_normal`` from the camera. If you want to know more about how these functions work, see :ref:`Ray-casting <doc_ray-casting>`.
Next we send the ray into the space state and see if it gets a result. We add the player and the knife's :ref:`Area <class_Area>` as two exceptions so the player cannot carry
themselves or the knife's collision :ref:`Area <class_Area>`.
Then we check to see if we got a result back from the ray. If no object has collided with the ray, an empty Dictionary will be returned. If the Dictionary is not empty (i.e. at least one object has collided), we then see if the collider the ray collided with is a :ref:`RigidBody <class_RigidBody>`.
If the ray collided with a :ref:`RigidBody <class_RigidBody>`, we set ``grabbed_object`` to the collider the ray collided with. We then set the mode on
the :ref:`RigidBody <class_RigidBody>` we collided with to ``MODE_STATIC`` so it doesn't move in our hands.
Finally, we set the grabbed :ref:`RigidBody <class_RigidBody>`'s collision layer and collision mask to ``0``.
This will make the grabbed :ref:`RigidBody <class_RigidBody>` have no collision layer or mask, which means it will not be able to collide with anything as long as we are holding it.
.. note::
See :ref:`Physics introduction <doc_physics_introduction_collision_layer_code_example>`
for more information on Godot collision masks.
______
If ``grabbed_object`` is not ``null``, then we need to throw the :ref:`RigidBody <class_RigidBody>` the player is holding.
We first set the mode of the :ref:`RigidBody <class_RigidBody>` we are holding to ``MODE_RIGID``.
.. note:: This is making a rather large assumption that all the rigid bodies will be using ``MODE_RIGID``. While that is the case for this tutorial series,
that may not be the case in other projects.
If you have rigid bodies with different modes, you may need to store the mode of the :ref:`RigidBody <class_RigidBody>` you
have picked up into a class variable so you can change it back to the mode it was in before you picked it up.
Then we apply an impulse to send it flying forward. We send it flying in the direction the camera is facing, using the force we set in the ``OBJECT_THROW_FORCE`` variable.
We then set the grabbed :ref:`RigidBody <class_RigidBody>`'s collision layer and mask to ``1``, so it can collide with anything on layer ``1`` again.
.. note:: This is, once again, making a rather large assumption that all the rigid bodies will be only on collision layer ``1``, and all collision masks will be on layer ``1``.
If you are using this script in other projects, you may need to store the collision layer/mask of the :ref:`RigidBody <class_RigidBody>` in a variable before you change them to ``0``, so you would have the original collision layer/mask to set for them when you are reversing the process.
Finally, we set ``grabbed_object`` to ``null`` since the player has successfully thrown the held object.
______
The last thing we do is check to see whether or not ``grabbed_object`` is equal to ``null``, outside all of the grabbing/throwing related code.
.. note:: While technically not input related, it's easy enough to place the code moving the grabbed object here
because it's only two lines, and then all of the grabbing/throwing code is in one place
If the player is holding an object, we set its global position to the camera's position plus ``OBJECT_GRAB_DISTANCE`` in the direction the camera is facing.
______
Before we test this, we need to change something in ``_physics_process``. While the player is holding an object, we do not
want the player to be able to change weapons or reload, so change ``_physics_process`` to the following:
::
func _physics_process(delta):
process_input(delta)
process_view_input(delta)
process_movement(delta)
if grabbed_object == null:
process_changing_weapons(delta)
process_reloading(delta)
# Process the UI
process_UI(delta)
Now the player cannot change weapons or reload while holding an object.
Now you can grab and throw RigidBody nodes while you're in the ``UNARMED`` state! Go give it a try!
Adding a turret
---------------
Next, let's make a turret to shoot the player!
Open up ``Turret.tscn``. Expand ``Turret`` if it's not already expanded.
Notice how the turret is broken up into several parts: ``Base``, ``Head``, ``Vision_Area``, and a ``Smoke`` :ref:`Particles <class_Particles>` node.
Open up ``Base`` and you'll find it's a :ref:`StaticBody <class_StaticBody>` and a mesh. Open up ``Head`` and you'll find there are several meshes,
a :ref:`StaticBody <class_StaticBody>` and a :ref:`Raycast <class_Raycast>` node.
One thing to note with the ``Head`` is that the raycast will be where the turret's bullets will fire from if we are using raycasting. We also have two meshes called
``Flash`` and ``Flash_2``. These will be the muzzle flash that briefly shows when the turret fires.
``Vision_Area`` is an :ref:`Area <class_Area>` we'll use as the turret's ability to see. When something enters ``Vision_Area``, we'll assume the turret can see it.
``Smoke`` is a :ref:`Particles <class_Particles>` node that will play when the turret is destroyed and repairing.
______
Now that we've looked at how the scene is set up, lets start writing the code for the turret. Select ``Turret`` and create a new script called ``Turret.gd``.
Add the following to ``Turret.gd``:
::
extends Spatial
export (bool) var use_raycast = false
const TURRET_DAMAGE_BULLET = 20
const TURRET_DAMAGE_RAYCAST = 5
const FLASH_TIME = 0.1
var flash_timer = 0
const FIRE_TIME = 0.8
var fire_timer = 0
var node_turret_head = null
var node_raycast = null
var node_flash_one = null
var node_flash_two = null
var ammo_in_turret = 20
const AMMO_IN_FULL_TURRET = 20
const AMMO_RELOAD_TIME = 4
var ammo_reload_timer = 0
var current_target = null
var is_active = false
const PLAYER_HEIGHT = 3
var smoke_particles
var turret_health = 60
const MAX_TURRET_HEALTH = 60
const DESTROYED_TIME = 20
var destroyed_timer = 0
var bullet_scene = preload("Bullet_Scene.tscn")
func _ready():
$Vision_Area.connect("body_entered", self, "body_entered_vision")
$Vision_Area.connect("body_exited", self, "body_exited_vision")
node_turret_head = $Head
node_raycast = $Head/Ray_Cast
node_flash_one = $Head/Flash
node_flash_two = $Head/Flash_2
node_raycast.add_exception(self)
node_raycast.add_exception($Base/Static_Body)
node_raycast.add_exception($Head/Static_Body)
node_raycast.add_exception($Vision_Area)
node_flash_one.visible = false
node_flash_two.visible = false
smoke_particles = $Smoke
smoke_particles.emitting = false
turret_health = MAX_TURRET_HEALTH
func _physics_process(delta):
if is_active == true:
if flash_timer > 0:
flash_timer -= delta
if flash_timer <= 0:
node_flash_one.visible = false
node_flash_two.visible = false
if current_target != null:
node_turret_head.look_at(current_target.global_transform.origin + Vector3(0, PLAYER_HEIGHT, 0), Vector3(0, 1, 0))
if turret_health > 0:
if ammo_in_turret > 0:
if fire_timer > 0:
fire_timer -= delta
else:
fire_bullet()
else:
if ammo_reload_timer > 0:
ammo_reload_timer -= delta
else:
ammo_in_turret = AMMO_IN_FULL_TURRET
if turret_health <= 0:
if destroyed_timer > 0:
destroyed_timer -= delta
else:
turret_health = MAX_TURRET_HEALTH
smoke_particles.emitting = false
func fire_bullet():
if use_raycast == true:
node_raycast.look_at(current_target.global_transform.origin + Vector3(0, PLAYER_HEIGHT, 0), Vector3(0, 1, 0))
node_raycast.force_raycast_update()
if node_raycast.is_colliding():
var body = node_raycast.get_collider()
if body.has_method("bullet_hit"):
body.bullet_hit(TURRET_DAMAGE_RAYCAST, node_raycast.get_collision_point())
ammo_in_turret -= 1
else:
var clone = bullet_scene.instance()
var scene_root = get_tree().root.get_children()[0]
scene_root.add_child(clone)
clone.global_transform = $Head/Barrel_End.global_transform
clone.scale = Vector3(8, 8, 8)
clone.BULLET_DAMAGE = TURRET_DAMAGE_BULLET
clone.BULLET_SPEED = 60
ammo_in_turret -= 1
node_flash_one.visible = true
node_flash_two.visible = true
flash_timer = FLASH_TIME
fire_timer = FIRE_TIME
if ammo_in_turret <= 0:
ammo_reload_timer = AMMO_RELOAD_TIME
func body_entered_vision(body):
if current_target == null:
if body is KinematicBody:
current_target = body
is_active = true
func body_exited_vision(body):
if current_target != null:
if body == current_target:
current_target = null
is_active = false
flash_timer = 0
fire_timer = 0
node_flash_one.visible = false
node_flash_two.visible = false
func bullet_hit(damage, bullet_hit_pos):
turret_health -= damage
if turret_health <= 0:
smoke_particles.emitting = true
destroyed_timer = DESTROYED_TIME
This is quite a bit of code, so let's break it down function by function. Let's first look at the class variables:
* ``use_raycast``: An exported boolean so we can change whether the turret uses objects or raycasting for bullets.
* ``TURRET_DAMAGE_BULLET``: The amount of damage a single bullet scene does.
* ``TURRET_DAMAGE_RAYCAST``: The amount of damage a single :ref:`Raycast <class_Raycast>` bullet does.
* ``FLASH_TIME``: The amount of time (in seconds) the muzzle flash meshes are visible.
* ``flash_timer``: A variable for tracking how long the muzzle flash meshes have been visible.
* ``FIRE_TIME``: The amount of time (in seconds) needed to fire a bullet.
* ``fire_timer``: A variable for tracking how much time has passed since the turret last fired.
* ``node_turret_head``: A variable to hold the ``Head`` node.
* ``node_raycast``: A variable to hold the :ref:`Raycast <class_Raycast>` node attached to the turret's head.
* ``node_flash_one``: A variable to hold the first muzzle flash :ref:`MeshInstance <class_MeshInstance>`.
* ``node_flash_two``: A variable to hold the second muzzle flash :ref:`MeshInstance <class_MeshInstance>`.
* ``ammo_in_turret``: The amount of ammo currently in the turret.
* ``AMMO_IN_FULL_TURRET``: The amount of ammo in a full turret.
* ``AMMO_RELOAD_TIME``: The amount of time it takes the turret to reload.
* ``ammo_reload_timer``: A variable for tracking how long the turret has been reloading.
* ``current_target``: The turret's current target.
* ``is_active``: A variable for tracking whether the turret is able to fire at the target.
* ``PLAYER_HEIGHT``: The amount of height we're adding to the target so we're not shooting at its feet.
* ``smoke_particles``: A variable to hold the smoke particles node.
* ``turret_health``: The amount of health the turret currently has.
* ``MAX_TURRET_HEALTH``: The amount of health a fully healed turret has.
* ``DESTROYED_TIME``: The amount of time (in seconds) it takes for a destroyed turret to repair itself.
* ``destroyed_timer``: A variable for tracking the amount of time a turret has been destroyed.
* ``bullet_scene``: The bullet scene the turret fires (same scene as the player's pistol)
Whew, that's quite a few class variables!
______
Let's go through ``_ready`` next.
Firstly, we get the vision area and connect the ``body_entered`` and ``body_exited`` signals to ``body_entered_vision`` and ``body_exited_vision``, respectively.
We then get all the nodes and assign them to their respective variables.
Next, we add some exceptions to the :ref:`Raycast <class_Raycast>` so the turret cannot hurt itself.
Then we make both flash meshes invisible at start, since we are not going to be firing during ``_ready``.
We then get the smoke particles node and assign it to the ``smoke_particles`` variable. We also set ``emitting`` to ``false`` to ensure the particles are
not emitting until the turret is broken.
Finally, we set the turret's health to ``MAX_TURRET_HEALTH`` so it starts at full health.
______
Now let's go through ``_physics_process``.
Firstly, we check whether the turret is active. If the turret is active, we want to process the firing code.
Next, if ``flash_timer`` is greater than zero, meaning the flash meshes are visible, we want to remove
delta from ``flash_timer``. If ``flash_timer`` gets to zero or less after we've subtracted ``delta``, we want to hide
both of the flash meshes.
Next, we check whether the turret has a target. If the turret has a target, we make the turret head look at it, adding ``PLAYER_HEIGHT`` so it is not
aiming at the player's feet.
We then check whether the turret's health is greater than zero. If it is, we then check whether there is ammo in the turret.
If there is, we then check whether ``fire_timer`` is greater than zero. If it is, the turret cannot fire and we need to
remove ``delta`` from ``fire_timer``. If ``fire_timer`` is less than or equal to zero, the turret can fire a bullet, so we call the ``fire_bullet`` function.
If there isn't any ammo in the turret, we check whether ``ammo_reload_timer`` is greater than zero. If it is,
we subtract ``delta`` from ``ammo_reload_timer``. If ``ammo_reload_timer`` is less than or equal to zero, we set ``ammo_in_turret`` to ``AMMO_IN_FULL_TURRET`` because
the turret has waited long enough to refill its ammo.
Next, we check whether the turret's health is less than or equal to ``0`` outside of whether it is active or not. If the turret's health is zero or less, we then
check whether ``destroyed_timer`` is greater than zero. If it is, we subtract ``delta`` from ``destroyed_timer``.
If ``destroyed_timer`` is less than or equal to zero, we set ``turret_health`` to ``MAX_TURRET_HEALTH`` and stop emitting smoke particles by setting ``smoke_particles.emitting`` to
``false``.
______
Next let's go through ``fire_bullet``.
Firstly, we check whether the turret is using a raycast.
The code for using a raycast is almost entirely the same as the code in the rifle from :ref:`doc_fps_tutorial_part_two`, so
I'm only going to go over it briefly.
We first make the raycast look at the target, ensuring the raycast will hit the target if nothing is in the way. We then force the raycast to update so we get a frame
perfect collision check. We then check whether the raycast has collided with anything. If it has, we then check
whether the collided body has the ``bullet_hit`` method. If it does, we call it and pass in the damage a single raycast bullet does along with the raycast's transform.
We then subtract ``1`` from ``ammo_in_turret``.
If the turret is not using a raycast, we spawn a bullet object instead. This code is almost entirely the same as the code in the pistol from :ref:`doc_fps_tutorial_part_two`, so
like with the raycast code, I'm only going to go over it briefly.
We first make a bullet clone and assign it to ``clone``. We then add that as a child of the root node. We set the bullet's global transform to
the barrel end, scale it up since it's too small, and set its damage and speed using the turret's constant class variables. We then subtract ``1`` from
``ammo_in_turret``.
Then, regardless of which bullet method we used, we make both of the muzzle flash meshes visible. We set ``flash_timer`` and ``fire_timer``
to ``FLASH_TIME`` and ``FIRE_TIME``, respectively. We then check whether the turret has used the last bullet in its ammo. If it has,
we set ``ammo_reload_timer`` to ``AMMO_RELOAD_TIME`` so the turret reloads.
______
Let's look at ``body_entered_vision`` next, and thankfully it is rather short.
We first check whether the turret currently has a target by checking if ``current_target`` is equal to ``null``.
If the turret does not have a target, we then check whether the body that has just entered the vision :ref:`Area <class_Area>` is a :ref:`KinematicBody <class_KinematicBody>`.
.. note:: We're assuming the turret should only fire at :ref:`KinematicBody <class_KinematicBody>` nodes since that is what the player is using.
If the body that just entered the vision :ref:`Area <class_Area>` is a :ref:`KinematicBody <class_KinematicBody>`, we set ``current_target`` to the body, and set ``is_active`` to
``true``.
______
Now let's look at ``body_exited_vision``.
Firstly, we check whether the turret has a target. If it does, we then check whether the body that has just left the turret's vision :ref:`Area <class_Area>`
is the turret's target.
If the body that has just left the vision :ref:`Area <class_Area>` is the turret's current target, we set ``current_target`` to ``null``, set ``is_active`` to ``false``, and reset
all the variables related to firing the turret since the turret no longer has a target to fire at.
______
Finally, let's look at ``bullet_hit``.
We first subtract however much damage the bullet causes from the turret's health.
Then, we check whether the turret has been destroyed (health being zero or less).
If the turret is destroyed, we start emitting the smoke particles and set ``destroyed_timer`` to ``DESTROYED_TIME`` so the turret has to wait before being repaired.
______
Whew, with all of that done and coded, we only have one last thing to do before the turret is ready for use. Open up ``Turret.tscn`` if it's not already open and
select one of the :ref:`StaticBody <class_StaticBody>` nodes from either ``Base`` or ``Head``. Create a new script called ``TurretBodies.gd`` and attach it to whichever
:ref:`StaticBody <class_StaticBody>` you have selected.
Add the following code to ``TurretBodies.gd``:
::
extends StaticBody
export (NodePath) var path_to_turret_root
func _ready():
pass
func bullet_hit(damage, bullet_hit_pos):
if path_to_turret_root != null:
get_node(path_to_turret_root).bullet_hit(damage, bullet_hit_pos)
All this code does is call ``bullet_hit`` on whatever node to which ``path_to_turret_root`` leads. Go back to the editor and assign the :ref:`NodePath <class_NodePath>`
to the ``Turret`` node.
Now select the other :ref:`StaticBody <class_StaticBody>` node (either in ``Body`` or ``Head``) and assign ``TurretBodies.gd`` script to it. Once the script is
attached, assign again the :ref:`NodePath <class_NodePath>` to the ``Turret`` node.
______
The last thing we need to do is add a way for the player to be hurt. Since all the bullets use the ``bullet_hit`` function, we need to add that function to the player.
Open ``Player.gd`` and add the following:
::
func bullet_hit(damage, bullet_hit_pos):
health -= damage
With all that done, you should have fully operational turrets! Go place a few in one/both/all of the scenes and give them a try!
Final notes
-----------
.. image:: img/PartFiveFinished.png
Now you can pick up :ref:`RigidBody <class_RigidBody>` nodes and throw grenades. We now also have turrets to fire at the player.
In :ref:`doc_fps_tutorial_part_six`, we're going to add a main menu and a pause menu,
add a respawn system for the player, and change/move the sound system so we can use it from any script.
.. warning:: If you ever get lost, be sure to read over the code again!
You can download the finished project for this part here: :download:`Godot_FPS_Part_5.zip <files/Godot_FPS_Part_5.zip>`

View File

@@ -1,790 +0,0 @@
.. _doc_fps_tutorial_part_four:
Part 4
======
Part overview
-------------
In this part, we will be adding health pickups, ammo pickups, targets the player can destroy, support for joypads, and add the ability to change weapons with the scroll wheel.
.. image:: img/PartFourFinished.png
.. note:: You are assumed to have finished :ref:`doc_fps_tutorial_part_three` before moving on to this part of the tutorial.
The finished project from :ref:`doc_fps_tutorial_part_three` will be the starting project for part 4
Let's get started!
Adding joypad input
-------------------
.. note:: In Godot, any game controller is referred to as a joypad. This includes:
Console controllers, Joysticks (like for flight simulators), Wheels (like for driving simulators), VR Controllers, and more!
Firstly, we need to change a few things in our project's input map. Open up the project settings and select the ``Input Map`` tab.
Now we need to add some joypad buttons to our various actions. Click the plus icon and select ``Joy Button``.
.. image:: img/ProjectSettingsAddKey.png
Feel free to use whatever button layout you want. Make sure that the device selected is set to ``0``. In the finished project, we will be using the following:
* movement_sprint: ``Device 0, Button 4 (L, L1)``
* fire: ``Device 0, Button 0 (PS Cross, XBox A, Nintendo B)``
* reload: ``Device 0, Button 0 (PS Square, XBox X, Nintendo Y)``
* flashlight: ``Device 0, Button 12 (D-Pad Up)``
* shift_weapon_positive: ``Device 0, Button 15 (D-Pad Right)``
* shift_weapon_negative: ``Device 0, Button 14 (D-Pad Left)``
* fire_grenade: ``Device 0, Button 1 (PS Circle, XBox B, Nintendo A).``
.. note:: These are already set up for you if you downloaded the starter assets
Once you are happy with the input, close the project settings and save.
______
Now let's open up ``Player.gd`` and add joypad input.
First, we need to define a few new class variables. Add the following class variables to ``Player.gd``:
::
# You may need to adjust depending on the sensitivity of your joypad
var JOYPAD_SENSITIVITY = 2
const JOYPAD_DEADZONE = 0.15
Let's go over what each of these does:
* ``JOYPAD_SENSITIVITY``: This is how fast the joypad's joysticks will move the camera.
* ``JOYPAD_DEADZONE``: The dead zone for the joypad. You may need to adjust depending on your joypad.
.. note:: Many joypads jitter around a certain point. To counter this, we ignore any movement
within a radius of JOYPAD_DEADZONE. If we did not ignore said movement, the camera would jitter.
Also, we are defining ``JOYPAD_SENSITIVITY`` as a variable instead of a constant because we'll later be changing it.
Now we are ready to start handling joypad input!
______
In ``process_input``, add the following code just before ``input_movement_vector = input_movement_vector.normalized()``:
.. tabs::
.. code-tab:: gdscript Xbox Controller
# Add joypad input if one is present
if Input.get_connected_joypads().size() > 0:
var joypad_vec = Vector2(0, 0)
if OS.get_name() == "Windows":
joypad_vec = Vector2(Input.get_joy_axis(0, 0), -Input.get_joy_axis(0, 1))
elif OS.get_name() == "X11":
joypad_vec = Vector2(Input.get_joy_axis(0, 1), Input.get_joy_axis(0, 2))
elif OS.get_name() == "OSX":
joypad_vec = Vector2(Input.get_joy_axis(0, 1), Input.get_joy_axis(0, 2))
if joypad_vec.length() < JOYPAD_DEADZONE:
joypad_vec = Vector2(0, 0)
else:
joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))
input_movement_vector += joypad_vec
.. code-tab:: gdscript PlayStation Controller
# Add joypad input if one is present
if Input.get_connected_joypads().size() > 0:
var joypad_vec = Vector2(0, 0)
if OS.get_name() == "Windows" or OS.get_name() == "X11":
joypad_vec = Vector2(Input.get_joy_axis(0, 0), -Input.get_joy_axis(0, 1))
elif OS.get_name() == "OSX":
joypad_vec = Vector2(Input.get_joy_axis(0, 1), Input.get_joy_axis(0, 2))
if joypad_vec.length() < JOYPAD_DEADZONE:
joypad_vec = Vector2(0, 0)
else:
joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))
input_movement_vector += joypad_vec
Let's go over what we're doing.
Firstly, we check to see if there is a connected joypad.
If there is a joypad connected, we then get its left stick axes for right/left and up/down.
Because a wired Xbox 360 controller has different joystick axis mapping based on OS, we will use different axes based on
the OS.
.. warning:: This tutorial assumes you are using a XBox 360 or a PlayStation wired controller.
Also, I do not (currently) have access to a Mac computer, so the joystick axes may need changing.
If they do, please open a GitHub issue on the Godot documentation repository! Thanks!
Next, we check to see if the joypad vector length is within the ``JOYPAD_DEADZONE`` radius.
If it is, we set ``joypad_vec`` to an empty Vector2. If it is not, we use a scaled Radial Dead zone for precise dead zone calculation.
.. note:: You can find a great article explaining all about how to handle joypad/controller dead zones
`here <https://web.archive.org/web/20191208161810/http://www.third-helix.com/2013/04/12/doing-thumbstick-dead-zones-right.html>`__.
We're using a translated version of the scaled radial dead zone code provided in that article.
The article is a great read, and I highly suggest giving it a look!
Finally, we add ``joypad_vec`` to ``input_movement_vector``.
.. tip:: Remember how we normalize ``input_movement_vector``? This is why! If we did not normalize ``input_movement_vector``, the player could
move faster if they pushed in the same direction with both the keyboard and the joypad!
______
Make a new function called ``process_view_input`` and add the following:
.. tabs::
.. code-tab:: gdscript Xbox Controller
func process_view_input(delta):
if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
return
# NOTE: Until some bugs relating to captured mice are fixed, we cannot put the mouse view
# rotation code here. Once the bug(s) are fixed, code for mouse view rotation code will go here!
# ----------------------------------
# Joypad rotation
var joypad_vec = Vector2()
if Input.get_connected_joypads().size() > 0:
if OS.get_name() == "Windows":
joypad_vec = Vector2(Input.get_joy_axis(0, 2), Input.get_joy_axis(0, 3))
elif OS.get_name() == "X11":
joypad_vec = Vector2(Input.get_joy_axis(0, 3), Input.get_joy_axis(0, 4))
elif OS.get_name() == "OSX":
joypad_vec = Vector2(Input.get_joy_axis(0, 3), Input.get_joy_axis(0, 4))
if joypad_vec.length() < JOYPAD_DEADZONE:
joypad_vec = Vector2(0, 0)
else:
joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))
rotation_helper.rotate_x(deg2rad(joypad_vec.y * JOYPAD_SENSITIVITY))
rotate_y(deg2rad(joypad_vec.x * JOYPAD_SENSITIVITY * -1))
var camera_rot = rotation_helper.rotation_degrees
camera_rot.x = clamp(camera_rot.x, -70, 70)
rotation_helper.rotation_degrees = camera_rot
# ----------------------------------
.. code-tab:: gdscript PlayStation Controller
func process_view_input(delta):
if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
return
# NOTE: Until some bugs relating to captured mice are fixed, we cannot put the mouse view
# rotation code here. Once the bug(s) are fixed, code for mouse view rotation code will go here!
# ----------------------------------
# Joypad rotation
var joypad_vec = Vector2()
if Input.get_connected_joypads().size() > 0:
if OS.get_name() == "Windows" or OS.get_name() == "X11":
joypad_vec = Vector2(Input.get_joy_axis(0, 2), Input.get_joy_axis(0, 3))
elif OS.get_name() == "OSX":
joypad_vec = Vector2(Input.get_joy_axis(0, 3), Input.get_joy_axis(0, 4))
if joypad_vec.length() < JOYPAD_DEADZONE:
joypad_vec = Vector2(0, 0)
else:
joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))
rotation_helper.rotate_x(deg2rad(joypad_vec.y * JOYPAD_SENSITIVITY))
rotate_y(deg2rad(joypad_vec.x * JOYPAD_SENSITIVITY * -1))
var camera_rot = rotation_helper.rotation_degrees
camera_rot.x = clamp(camera_rot.x, -70, 70)
rotation_helper.rotation_degrees = camera_rot
# ----------------------------------
Let's go over what's happening:
Firstly, we check the mouse mode. If the mouse mode is not ``MOUSE_MODE_CAPTURED``, we want to return, which will skip the code below.
Next, we define a new :ref:`Vector2 <class_Vector2>` called ``joypad_vec``. This will hold the right joystick position. Based on the OS, we set its values so
it is mapped to the proper axes for the right joystick.
.. warning:: As stated above, I do not (currently) have access to a Mac computer, so the joystick axes may need changing. If they do,
please open a GitHub issue on the Godot documentation repository! Thanks!
We then account for the joypad's dead zone, exactly like in ``process_input``.
Then, we rotate ``rotation_helper`` and the player's :ref:`KinematicBody <class_KinematicBody>` using ``joypad_vec``.
Notice how the code that handles rotating the player and ``rotation_helper`` is exactly the same as the
code in ``_input``. All we've done is change the values to use ``joypad_vec`` and ``JOYPAD_SENSITIVITY``.
.. note:: Due to a few mouse-related bugs on Windows, we cannot put mouse rotation in ``process_view`` as well.
Once these bugs are fixed, this will likely be updated to place the mouse rotation here in ``process_view_input`` as well.
Finally, we clamp the camera's rotation so the player cannot look upside down.
______
The last thing we need to do is add ``process_view_input`` to ``_physics_process``.
Once ``process_view_input`` is added to ``_physics_process``, you should be able to play using a joypad!
.. note:: I decided not to use the joypad triggers for firing because we'd then have to do some more axis managing, and because I prefer to use a shoulder buttons to fire.
If you want to use the triggers for firing, you will need to change how firing works in ``process_input``. You need to get the axis values for the triggers,
and check if it's over a certain value, say ``0.8`` for example. If it is, you add the same code as when the ``fire`` action was pressed.
Adding mouse scroll wheel input
-------------------------------
Let's add one more input related feature before we start working on the pickups and the target. Let's add the ability to change weapons using the scroll wheel on the mouse.
Open up ``Player.gd`` and add the following class variables:
::
var mouse_scroll_value = 0
const MOUSE_SENSITIVITY_SCROLL_WHEEL = 0.08
Let's go over what each of these new variables will be doing:
* ``mouse_scroll_value``: The value of the mouse scroll wheel.
* ``MOUSE_SENSITIVITY_SCROLL_WHEEL``: How much a single scroll action increases mouse_scroll_value
______
Now let's add the following to ``_input``:
::
if event is InputEventMouseButton and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
if event.button_index == BUTTON_WHEEL_UP or event.button_index == BUTTON_WHEEL_DOWN:
if event.button_index == BUTTON_WHEEL_UP:
mouse_scroll_value += MOUSE_SENSITIVITY_SCROLL_WHEEL
elif event.button_index == BUTTON_WHEEL_DOWN:
mouse_scroll_value -= MOUSE_SENSITIVITY_SCROLL_WHEEL
mouse_scroll_value = clamp(mouse_scroll_value, 0, WEAPON_NUMBER_TO_NAME.size() - 1)
if changing_weapon == false:
if reloading_weapon == false:
var round_mouse_scroll_value = int(round(mouse_scroll_value))
if WEAPON_NUMBER_TO_NAME[round_mouse_scroll_value] != current_weapon_name:
changing_weapon_name = WEAPON_NUMBER_TO_NAME[round_mouse_scroll_value]
changing_weapon = true
mouse_scroll_value = round_mouse_scroll_value
Let's go over what's happening here:
Firstly, we check if the event is an ``InputEventMouseButton`` event and that the mouse mode is ``MOUSE_MODE_CAPTURED``.
Then, we check to see if the button index is either a ``BUTTON_WHEEL_UP`` or ``BUTTON_WHEEL_DOWN`` index.
If the event's index is indeed a button wheel index, we then check to see if it is a ``BUTTON_WHEEL_UP`` or ``BUTTON_WHEEL_DOWN`` index.
Based on whether it is up or down, we add or subtract ``MOUSE_SENSITIVITY_SCROLL_WHEEL`` to/from ``mouse_scroll_value``.
Next, we clamp mouse scroll value to ensure it is inside the range of selectable weapons.
We then check to see if the player is changing weapons or reloading. If the player is doing neither, we round ``mouse_scroll_value`` and cast it to an ``int``.
.. note:: We are casting ``mouse_scroll_value`` to an ``int`` so we can use it as a key in our dictionary. If we left it as a float,
we would get an error when we tried to run the project.
Next, we check to see if the weapon name at ``round_mouse_scroll_value`` is not equal to the current weapon name using ``WEAPON_NUMBER_TO_NAME``.
If the weapon is different from the player's current weapon, we assign ``changing_weapon_name``, set ``changing_weapon`` to ``true`` so the player will change weapons in
``process_changing_weapon``, and set ``mouse_scroll_value`` to ``round_mouse_scroll_value``.
.. tip:: The reason we are setting ``mouse_scroll_value`` to the rounded scroll value is because we do not want the player to keep their
mouse scroll wheel just in between values, giving them the ability to switch almost extremely fast. By assigning ``mouse_scroll_value``
to ``round_mouse_scroll_value``, we ensure that each weapon takes exactly the same amount of scrolling to change.
______
One more thing we need to change is in ``process_input``. In the code for changing weapons, add the following right after the line ``changing_weapon = true``:
::
mouse_scroll_value = weapon_change_number
Now the scroll value will be changed with the keyboard input. If we did not change this, the scroll value would be out of sync. If the scroll wheel were out of
sync, scrolling forwards or backwards would not transition to the next/last weapon, but rather the next/last weapon the scroll wheel changed to.
______
Now you can change weapons using the scroll wheel! Go give it a whirl!
Adding the health pickups
-------------------------
Now that the player has health and ammo, we ideally need a way to replenish those resources.
Open up ``Health_Pickup.tscn``.
Expand ``Holder`` if it's not already expanded. Notice how we have two Spatial nodes, one called ``Health_Kit`` and another called ``Health_Kit_Small``.
This is because we're actually going to be making two sizes of health pickups, one small and one large/normal. ``Health_Kit`` and ``Health_Kit_Small`` only
have a single :ref:`MeshInstance <class_MeshInstance>` as their children.
Next expand ``Health_Pickup_Trigger``. This is an :ref:`Area <class_Area>` node we're going to use to check if the player has walked close enough to pick up
the health kit. If you expand it, you'll find two collision shapes, one for each size. We will be using a different collision shape size based on the size of the
health pickup, so the smaller health pickup has a trigger collision shape closer to its size.
The last thing to note is how we have an :ref:`AnimationPlayer <class_AnimationPlayer>` node so the health kit bobs and spins around slowly.
Select ``Health_Pickup`` and add a new script called ``Health_Pickup.gd``. Add the following:
::
extends Spatial
export (int, "full size", "small") var kit_size = 0 setget kit_size_change
# 0 = full size pickup, 1 = small pickup
const HEALTH_AMOUNTS = [70, 30]
const RESPAWN_TIME = 20
var respawn_timer = 0
var is_ready = false
func _ready():
$Holder/Health_Pickup_Trigger.connect("body_entered", self, "trigger_body_entered")
is_ready = true
kit_size_change_values(0, false)
kit_size_change_values(1, false)
kit_size_change_values(kit_size, true)
func _physics_process(delta):
if respawn_timer > 0:
respawn_timer -= delta
if respawn_timer <= 0:
kit_size_change_values(kit_size, true)
func kit_size_change(value):
if is_ready:
kit_size_change_values(kit_size, false)
kit_size = value
kit_size_change_values(kit_size, true)
else:
kit_size = value
func kit_size_change_values(size, enable):
if size == 0:
$Holder/Health_Pickup_Trigger/Shape_Kit.disabled = !enable
$Holder/Health_Kit.visible = enable
elif size == 1:
$Holder/Health_Pickup_Trigger/Shape_Kit_Small.disabled = !enable
$Holder/Health_Kit_Small.visible = enable
func trigger_body_entered(body):
if body.has_method("add_health"):
body.add_health(HEALTH_AMOUNTS[kit_size])
respawn_timer = RESPAWN_TIME
kit_size_change_values(kit_size, false)
Let's go over what this script is doing, starting with its class variables:
* ``kit_size``: The size of the health pickup. Notice how we're using a ``setget`` function to tell if it's changed.
* ``HEALTH_AMMOUNTS``: The amount of health each pickup in each size contains.
* ``RESPAWN_TIME``: The amount of time, in seconds, it takes for the health pickup to respawn
* ``respawn_timer``: A variable used to track how long the health pickup has been waiting to respawn.
* ``is_ready``: A variable to track whether the ``_ready`` function has been called or not.
We're using ``is_ready`` because ``setget`` functions are called before ``_ready``; we need to ignore the
first kit_size_change call, because we cannot access child nodes until ``_ready`` is called. If we did not ignore the
first ``setget`` call, we would get several errors in the debugger.
Also, notice how we are using an exported variable. This is so we can change the size of the health pickups in the editor. This makes it so
we do not have to make two scenes for the two sizes, since we can easily change sizes in the editor using the exported variable.
.. tip:: See :ref:`doc_GDScript` and scroll down to the Exports section for a list of export hints you can use.
______
Let's look at ``_ready``:
Firstly, we connect the ``body_entered`` signal from the ``Health_Pickup_Trigger`` to the ``trigger_body_entered`` function. This makes it so any
body that enters the :ref:`Area <class_Area>` triggers the ``trigger_body_entered`` function.
Next, we set ``is_ready`` to ``true`` so we can use the ``setget`` function.
Then we hide all the possible kits and their collision shapes using ``kit_size_change_values``. The first argument is the size of the kit, while the second argument
is whether to enable or disable the collision shape and mesh at that size.
Then we make only the kit size we selected visible, calling ``kit_size_change_values`` and passing in ``kit_size`` and ``true``, so the size at ``kit_size`` is enabled.
______
Next let's look at ``kit_size_change``.
The first thing we do is check to see if ``is_ready`` is ``true``.
If ``is_ready`` is ``true``, we then make whatever kit already assigned to ``kit_size`` disabled using ``kit_size_change_values``, passing in ``kit_size`` and ``false``.
Then we assign ``kit_size`` to the new value passed in, ``value``. Then we call ``kit_size_change_values`` passing in ``kit_size`` again, but this time
with the second argument as ``true`` so we enable it. Because we changed ``kit_size`` to the passed in value, this will make whatever kit size was passed in visible.
If ``is_ready`` is not ``true``, we simply assign ``kit_size`` to the passed in ``value``.
______
Now let's look at ``kit_size_change_values``.
The first thing we do is check to see which size was passed in. Based on which size we want to enable/disable, we want to get different nodes.
We get the collision shape for the node corresponding to ``size`` and disable it based on the ``enabled`` passed in argument/variable.
.. note:: Why are we using ``!enable`` instead of ``enable``? This is so when we say we want to enable the node, we can pass in ``true``, but since
:ref:`CollisionShape <class_CollisionShape>` uses disabled instead of enabled, we need to flip it. By flipping it, we can enable the collision shape
and make the mesh visible when ``true`` is passed in.
We then get the correct :ref:`Spatial <class_Spatial>` node holding the mesh and set its visibility to ``enable``.
This function may be a little confusing; try to think of it like this: We're enabling/disabling the proper nodes for ``size`` using ``enabled``. This is so we cannot pick up
health for a size that is not visible, and so only the mesh for the proper size will be visible.
______
Finally, let's look at ``trigger_body_entered``.
The first thing we do is check whether or not the body that has just entered has a method/function called ``add_health``. If it does, we then
call ``add_health`` and pass in the health provided by the current kit size.
Then we set ``respawn_timer`` to ``RESPAWN_TIME`` so the player has to wait before the player can get health again. Finally, call ``kit_size_change_values``,
passing in ``kit_size`` and ``false`` so the kit at ``kit_size`` is invisible until it has waited long enough to respawn.
_______
The last thing we need to do before the player can use this health pickup is add a few things to ``Player.gd``.
Open up ``Player.gd`` and add the following class variable:
::
const MAX_HEALTH = 150
* ``MAX_HEALTH``: The maximum amount of health a player can have.
Now we need to add the ``add_health`` function to the player. Add the following to ``Player.gd``:
::
func add_health(additional_health):
health += additional_health
health = clamp(health, 0, MAX_HEALTH)
Let's quickly go over what this does.
We first add ``additional_health`` to the player's current health. We then clamp the health so that it cannot take on a value higher than ``MAX_HEALTH``, nor a value lower
than ``0``.
_______
With that done, the player can now collect health! Go place a few ``Health_Pickup`` scenes around and give it a try. You can change the size of the health pickup in the editor
when a ``Health_Pickup`` instanced scene is selected, from a convenient drop down.
Adding the ammo pickups
-----------------------
While adding health is good and all, we can't reap the rewards from adding it since nothing can (currently) damage us.
Let's add some ammo pickups next!
Open up ``Ammo_Pickup.tscn``. Notice how it's structured exactly the same as ``Health_Pickup.tscn``, but with the meshes and trigger collision shapes changed slightly to account
for the difference in mesh sizes.
Select ``Ammo_Pickup`` and add a new script called ``Ammo_Pickup.gd``. Add the following:
::
extends Spatial
export (int, "full size", "small") var kit_size = 0 setget kit_size_change
# 0 = full size pickup, 1 = small pickup
const AMMO_AMOUNTS = [4, 1]
const RESPAWN_TIME = 20
var respawn_timer = 0
var is_ready = false
func _ready():
$Holder/Ammo_Pickup_Trigger.connect("body_entered", self, "trigger_body_entered")
is_ready = true
kit_size_change_values(0, false)
kit_size_change_values(1, false)
kit_size_change_values(kit_size, true)
func _physics_process(delta):
if respawn_timer > 0:
respawn_timer -= delta
if respawn_timer <= 0:
kit_size_change_values(kit_size, true)
func kit_size_change(value):
if is_ready:
kit_size_change_values(kit_size, false)
kit_size = value
kit_size_change_values(kit_size, true)
else:
kit_size = value
func kit_size_change_values(size, enable):
if size == 0:
$Holder/Ammo_Pickup_Trigger/Shape_Kit.disabled = !enable
$Holder/Ammo_Kit.visible = enable
elif size == 1:
$Holder/Ammo_Pickup_Trigger/Shape_Kit_Small.disabled = !enable
$Holder/Ammo_Kit_Small.visible = enable
func trigger_body_entered(body):
if body.has_method("add_ammo"):
body.add_ammo(AMMO_AMOUNTS[kit_size])
respawn_timer = RESPAWN_TIME
kit_size_change_values(kit_size, false)
You may have noticed this code looks almost exactly the same as the health pickup. That's because it largely is the same! Only a few things
have been changed, and that's what we're going to go over.
Firstly, notice the change to ``AMMO_AMOUNTS`` from ``HEALTH_AMMOUNTS``. ``AMMO_AMOUNTS`` will be how many ammo clips/magazines the pickup adds to the current weapon.
(Unlike in the case of ``HEALTH_AMMOUNTS``, which has stood for how many health points would be awarded, we add an entire clip to the current weapon instead of the raw ammo amount)
The only other thing to notice is in ``trigger_body_entered``. We're checking for the existence of and calling a function called ``add_ammo`` instead of ``add_health``.
Other than those two small changes, everything else is the same as the health pickup!
_______
All we need to do to make the ammo pickups work is add a new function to the player. Open ``Player.gd`` and add the following function:
::
func add_ammo(additional_ammo):
if (current_weapon_name != "UNARMED"):
if (weapons[current_weapon_name].CAN_REFILL == true):
weapons[current_weapon_name].spare_ammo += weapons[current_weapon_name].AMMO_IN_MAG * additional_ammo
Let's go over what this function does.
The first thing we check is whether the player is ``UNARMED``. Because ``UNARMED`` does not have a node/script, we want to make sure the player is not
``UNARMED`` before trying to get the node/script attached to ``current_weapon_name``.
Next, we check to see if the current weapon can be refilled. If the current weapon can, we add a full clip/magazine worth of ammo to the weapon by
multiplying the current weapon's ``AMMO_IN_MAG`` value by however many ammo clips we're adding (``additional_ammo``).
_______
With that done, you should now be able to get additional ammo! Go place some ammo pickups in one/both/all of the scenes and give it a try!
.. note:: Notice how we're not limiting the amount of ammo you can carry. To limit the amount of ammo each weapon can carry, you need to add an additional variable to
each weapon's script, and then clamp the weapon's ``spare_ammo`` variable after adding ammo in ``add_ammo``.
Adding breakable targets
------------------------
Before we end this part, let's add some targets.
Open up ``Target.tscn`` and take a look at the scenes in the scene tree.
Firstly, notice how we're not using a :ref:`RigidBody <class_RigidBody>` node, but a :ref:`StaticBody <class_StaticBody>` one.
The reason behind this is our non-broken targets will not be moving anywhere; using a :ref:`RigidBody <class_RigidBody>` would be more hassle than
it's worth since all it has to do is stay still.
.. tip:: We also save a tiny bit of performance using a :ref:`StaticBody <class_StaticBody>` over a :ref:`RigidBody <class_RigidBody>`.
The other thing to note is we have a node called ``Broken_Target_Holder``. This node is going to hold a spawned/instanced scene called
``Broken_Target.tscn``. Open up ``Broken_Target.tscn``.
Notice how the target is broken up into five pieces, each a :ref:`RigidBody <class_RigidBody>` node. We're going to spawn/instance this scene when the target takes too much damage
and needs to be destroyed. Then, we're going to hide the non-broken target, so it looks like the target shattered rather than a shattered target was
spawned/instanced.
While you still have ``Broken_Target.tscn`` open, attach ``RigidBody_hit_test.gd`` to all of the :ref:`RigidBody <class_RigidBody>` nodes. This will make
it so the player can shoot at the broken pieces and they will react to the bullets.
Alright, now switch back to ``Target.tscn``, select the ``Target`` :ref:`StaticBody <class_StaticBody>` node and create a new script called ``Target.gd``.
Add the following code to ``Target.gd``:
::
extends StaticBody
const TARGET_HEALTH = 40
var current_health = 40
var broken_target_holder
# The collision shape for the target.
# NOTE: this is for the whole target, not the pieces of the target.
var target_collision_shape
const TARGET_RESPAWN_TIME = 14
var target_respawn_timer = 0
export (PackedScene) var destroyed_target
func _ready():
broken_target_holder = get_parent().get_node("Broken_Target_Holder")
target_collision_shape = $Collision_Shape
func _physics_process(delta):
if target_respawn_timer > 0:
target_respawn_timer -= delta
if target_respawn_timer <= 0:
for child in broken_target_holder.get_children():
child.queue_free()
target_collision_shape.disabled = false
visible = true
current_health = TARGET_HEALTH
func bullet_hit(damage, bullet_transform):
current_health -= damage
if current_health <= 0:
var clone = destroyed_target.instance()
broken_target_holder.add_child(clone)
for rigid in clone.get_children():
if rigid is RigidBody:
var center_in_rigid_space = broken_target_holder.global_transform.origin - rigid.global_transform.origin
var direction = (rigid.transform.origin - center_in_rigid_space).normalized()
# Apply the impulse with some additional force (I find 12 works nicely).
rigid.apply_impulse(center_in_rigid_space, direction * 12 * damage)
target_respawn_timer = TARGET_RESPAWN_TIME
target_collision_shape.disabled = true
visible = false
Let's go over what this script does, starting with the class variables:
* ``TARGET_HEALTH``: The amount of damage needed to break a fully healed target.
* ``current_health``: The amount of health this target currently has.
* ``broken_target_holder``: A variable to hold the ``Broken_Target_Holder`` node so we can use it easily.
* ``target_collision_shape``: A variable to hold the :ref:`CollisionShape <class_CollisionShape>` for the non-broken target.
* ``TARGET_RESPAWN_TIME``: The length of time, in seconds, it takes for a target to respawn.
* ``target_respawn_timer``: A variable to track how long a target has been broken.
* ``destroyed_target``: A :ref:`PackedScene <class_PackedScene>` to hold the broken target scene.
Notice how we're using an exported variable (a :ref:`PackedScene <class_PackedScene>`) to get the broken target scene instead of
using ``preload``. By using an exported variable, we can choose the scene from the editor, and if we need to use a different scene,
it's as easy as selecting a different scene in the editor; we don't need to go to the code to change the scene we're using.
______
Let's look at ``_ready``.
The first thing we do is get the broken target holder and assign it to ``broken_target_holder``. Notice how we're using ``get_parent().get_node()`` here, instead
of ``$``. If you wanted to use ``$``, then you'd need to change ``get_parent().get_node()`` to ``$"../Broken_Target_Holder"``.
.. note:: At the time of when this was written, I did not realize you can use ``$"../NodeName"`` to get the parent nodes using ``$``, which is why ``get_parent().get_node()``
is used instead.
Next, we get the collision shape and assign it to ``target_collision_shape``. The reason we need the collision shape is because even when the mesh is invisible, the
collision shape will still exist in the physics world. This makes it so the player could interact with a non-broken target even though it's invisible, which is
not what we want. To get around this, we will disable/enable the collision shape as we make the mesh visible/invisible.
______
Next let's look at ``_physics_process``.
We're only going to be using ``_physics_process`` for respawning, and so the first thing we do is check to see if ``target_respawn_timer`` is greater than ``0``.
If it is, we then subtract ``delta`` from it.
Then we check to see if ``target_respawn_timer`` is ``0`` or less. The reason behind this is since we just removed ``delta`` from ``target_respawn_timer``, if it's
``0`` or less, then the target just got here, effectively allowing us to do whatever we need to do when the timer is finished.
In this case, we want to respawn the target.
The first thing we do is remove all children in the broken target holder. We do this by iterating over all of the children in ``broken_target_holder`` and free them using ``queue_free``.
Next, we enable the collision shape by setting its ``disabled`` boolean to ``false``.
Then we make the target, and all of its children nodes, visible again.
Finally, we reset the target's health (``current_health``) to ``TARGET_HEALTH``.
______
Finally, let's look at ``bullet_hit``.
The first thing we do is subtract however much damage the bullet does from the target's health.
Next we check to see if the target is at ``0`` health or lower. If it is, the target has just died and we need to spawn a broken target.
We first instance a new destroyed target scene, and assign it to a new variable, a ``clone``.
Next we add the ``clone`` as a child of the broken target holder.
For bonus effect, we want to make all the target pieces explode outwards. To do this, we iterate over all the children in ``clone``.
For each child, we first check to see if it's a :ref:`RigidBody <class_RigidBody>` node. If it is, we then calculate the center position of the target relative
to the child node. Then we figure out which direction the child node is relative to the center. Using those calculated variables, we push the child from the calculated center,
in the direction away from the center, using the damage of the bullet as the force.
.. note:: We multiply the damage by ``12`` so it has a more dramatic effect. You can change this to a higher or lower value depending on how explosively you want
your targets to shatter.
Next, we set the target's respawn timer. We set the timer to ``TARGET_RESPAWN_TIME``, so it takes ``TARGET_RESPAWN_TIME`` in seconds until it is respawned.
Then we disable the non-broken target's collision shape, and set the target's visibility to ``false``.
______
.. warning:: Make sure to set the exported ``destroyed_target`` value for ``Target.tscn`` in the editor! Otherwise the targets will not be destroyed
and you will get an error!
With that done, go place some ``Target.tscn`` instances around in one/both/all of the levels. You should find they explode into five pieces after they've taken enough
damage. After a little while, they'll respawn into a whole target again.
Final notes
-----------
.. image:: img/PartFourFinished.png
Now you can use a joypad, change weapons with the mouse's scroll wheel, replenish your health and ammo, and break targets with your weapons.
In the next part, :ref:`doc_fps_tutorial_part_five`, we're going to add grenades to our player, give our player the ability to grab and throw objects, and
add turrets!
.. warning:: If you ever get lost, be sure to read over the code again!
You can download the finished project for this part here: :download:`Godot_FPS_Part_4.zip <files/Godot_FPS_Part_4.zip>`

View File

@@ -1,852 +0,0 @@
.. _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 this tutorial series, we will cover how:
- To make a first person character that can move, sprint, and jump.
- To make a simple animation state machine for handling animation transitions.
- To add three weapons to the first person character, each using a different way to handle bullet collisions:
- - A knife (using an :ref:`Area <class_Area>`)
- - A pistol (Bullet scenes)
- - A rifle (using a :ref:`Raycast <class_Raycast>`)
- To add two different types of grenades to the first person character:
- - A normal grenade
- - A sticky grenade
- To add the ability to grab and throw :ref:`RigidBody <class_RigidBody>` nodes
- To add joypad input for the player
- To add ammo and reloading for all weapons that consume ammo.
- To add ammo and health pick ups
- - In two sizes: big and small
- To add an automatic turret
- - That can fire using bullet objects or a :ref:`Raycast <class_Raycast>`
- To add targets that break when they've taken enough damage
- To add sounds that play when the guns fire.
- To add a simple main menu:
- - With an options menu for changing how the game runs
- - With a level select screen
- To add a universal pause menu we can access anywhere
.. 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 have experience working with the Godot editor,
basic programming experience in GDScript, and basic experience in game development.
You can find the start assets for this tutorial here: :download:`Godot_FPS_Starter.zip <files/Godot_FPS_Starter.zip>`
The provided starter assets contain an animated 3D model, a bunch of 3D models for making levels,
and a few scenes already configured for this tutorial.
All assets provided (unless otherwise noted) were originally created by TwistedTwigleg, with changes/additions by the Godot community.
All original assets provided for this tutorial are released under the ``MIT`` license.
Feel free to use these assets however you want! All original assets belong to the Godot community, with the other assets belonging to those listed below:
.. 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``.
.. tip:: You can find the finished project for each part at the bottom of each part's page
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 who can move around the game environment,
sprint, look around with a mouse based first person camera, jump into the air, and turn a flash light on and off.
Getting everything ready
------------------------
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, 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.
_________
Let's take a second to see what we have in the starter assets.
Included in the starter assets are several scenes. For example, in ``res://`` we have 14 scenes, most of which we will be visiting as
we go through this tutorial series.
For now let's 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 exploring through ``Assets`` in this tutorial series. ``Assets`` contains all the models used
for each of the levels, as well as some textures and materials.
Making the FPS movement logic
-----------------------------
Once you have ``Player.tscn`` open, let's take a quick look at how it is set up:
.. image:: img/PlayerSceneTree.png
First, notice how the player's collision shapes are set up. 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.
We do want the 'feet' slightly higher than the bottom of the capsule so we can roll over slight edges.
Where to place the 'feet' is dependent on your levels and how you want your player to feel.
.. note:: Many times the player will notice 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 the nodes we want to rotate on the ``X`` axis (up and down).
The reason behind this is so we can rotate ``Player`` on the ``Y`` axis, and ``Rotation_helper`` on
the ``X`` axis.
.. note:: Had we not used ``Rotation_helper``, we would've likely had cases of rotating on
both the ``X`` and ``Y`` axes simultaneously, potentially further degenerating into a state of
rotation on all three axes in some cases.
See :ref:`using transforms <doc_using_transforms>` for more information
_________
Attach a new script to the ``Player`` node and call it ``Player.gd``.
Let's program our player by adding the ability to move around, look around with the mouse, and jump.
Add the following code to ``Player.gd``:
.. tabs::
.. code-tab:: gdscript GDScript
extends KinematicBody
const GRAVITY = -24.8
var vel = Vector3()
const MAX_SPEED = 20
const JUMP_SPEED = 18
const ACCEL = 4.5
var dir = Vector3()
const DEACCEL= 16
const MAX_SLOPE_ANGLE = 40
var camera
var rotation_helper
var MOUSE_SENSITIVITY = 0.05
func _ready():
camera = $Rotation_Helper/Camera
rotation_helper = $Rotation_Helper
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
func _physics_process(delta):
process_input(delta)
process_movement(delta)
func process_input(delta):
# ----------------------------------
# Walking
dir = Vector3()
var cam_xform = camera.get_global_transform()
var input_movement_vector = Vector2()
if Input.is_action_pressed("movement_forward"):
input_movement_vector.y += 1
if Input.is_action_pressed("movement_backward"):
input_movement_vector.y -= 1
if Input.is_action_pressed("movement_left"):
input_movement_vector.x -= 1
if Input.is_action_pressed("movement_right"):
input_movement_vector.x += 1
input_movement_vector = input_movement_vector.normalized()
# Basis vectors are already normalized.
dir += -cam_xform.basis.z * input_movement_vector.y
dir += cam_xform.basis.x * input_movement_vector.x
# ----------------------------------
# ----------------------------------
# Jumping
if is_on_floor():
if Input.is_action_just_pressed("movement_jump"):
vel.y = JUMP_SPEED
# ----------------------------------
# ----------------------------------
# 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 process_movement(delta):
dir.y = 0
dir = dir.normalized()
vel.y += delta * GRAVITY
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))
func _input(event):
if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
rotation_helper.rotate_x(deg2rad(event.relative.y * MOUSE_SENSITIVITY))
self.rotate_y(deg2rad(event.relative.x * MOUSE_SENSITIVITY * -1))
var camera_rot = rotation_helper.rotation_degrees
camera_rot.x = clamp(camera_rot.x, -70, 70)
rotation_helper.rotation_degrees = camera_rot
.. code-tab:: csharp
using Godot;
using System;
public class Player : KinematicBody
{
[Export]
public float Gravity = -24.8f;
[Export]
public float MaxSpeed = 20.0f;
[Export]
public float JumpSpeed = 18.0f;
[Export]
public float Accel = 4.5f;
[Export]
public float Deaccel = 16.0f;
[Export]
public float MaxSlopeAngle = 40.0f;
[Export]
public float MouseSensitivity = 0.05f;
private Vector3 _vel = new Vector3();
private Vector3 _dir = new Vector3();
private Camera _camera;
private Spatial _rotationHelper;
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
_camera = GetNode<Camera>("Rotation_Helper/Camera");
_rotationHelper = GetNode<Spatial>("Rotation_Helper");
Input.SetMouseMode(Input.MouseMode.Captured);
}
public override void _PhysicsProcess(float delta)
{
ProcessInput(delta);
ProcessMovement(delta);
}
private void ProcessInput(float delta)
{
// -------------------------------------------------------------------
// Walking
_dir = new Vector3();
Transform camXform = _camera.GlobalTransform;
Vector2 inputMovementVector = new Vector2();
if (Input.IsActionPressed("movement_forward"))
inputMovementVector.y += 1;
if (Input.IsActionPressed("movement_backward"))
inputMovementVector.y -= 1;
if (Input.IsActionPressed("movement_left"))
inputMovementVector.x -= 1;
if (Input.IsActionPressed("movement_right"))
inputMovementVector.x += 1;
inputMovementVector = inputMovementVector.Normalized();
// Basis vectors are already normalized.
_dir += -camXform.basis.z * inputMovementVector.y;
_dir += camXform.basis.x * inputMovementVector.x;
// -------------------------------------------------------------------
// -------------------------------------------------------------------
// Jumping
if (IsOnFloor())
{
if (Input.IsActionJustPressed("movement_jump"))
_vel.y = JumpSpeed;
}
// -------------------------------------------------------------------
// -------------------------------------------------------------------
// Capturing/Freeing the cursor
if (Input.IsActionJustPressed("ui_cancel"))
{
if (Input.GetMouseMode() == Input.MouseMode.Visible)
Input.SetMouseMode(Input.MouseMode.Captured);
else
Input.SetMouseMode(Input.MouseMode.Visible);
}
// -------------------------------------------------------------------
}
private void ProcessMovement(float delta)
{
_dir.y = 0;
_dir = _dir.Normalized();
_vel.y += delta * Gravity;
Vector3 hvel = _vel;
hvel.y = 0;
Vector3 target = _dir;
target *= MaxSpeed;
float accel;
if (_dir.Dot(hvel) > 0)
accel = Accel;
else
accel = Deaccel;
hvel = hvel.LinearInterpolate(target, accel * delta);
_vel.x = hvel.x;
_vel.z = hvel.z;
_vel = MoveAndSlide(_vel, new Vector3(0, 1, 0), false, 4, Mathf.Deg2Rad(MaxSlopeAngle));
}
public override void _Input(InputEvent @event)
{
if (@event is InputEventMouseMotion && Input.GetMouseMode() == Input.MouseMode.Captured)
{
InputEventMouseMotion mouseEvent = @event as InputEventMouseMotion;
_rotationHelper.RotateX(Mathf.Deg2Rad(mouseEvent.Relative.y * MouseSensitivity));
RotateY(Mathf.Deg2Rad(-mouseEvent.Relative.x * MouseSensitivity));
Vector3 cameraRot = _rotationHelper.RotationDegrees;
cameraRot.x = Mathf.Clamp(cameraRot.x, -70, 70);
_rotationHelper.RotationDegrees = cameraRot;
}
}
}
This is a lot of code, so let's break it down function by function:
.. tip:: While copy and pasting code is ill advised, as you can learn a lot from manually typing the code in, you can
copy and paste the code from this page directly into the script editor.
If you do this, all the code copied will be using spaces instead of tabs.
To convert the spaces to tabs in the script editor, click the "edit" menu and select "Convert Indent To Tabs".
This will convert all the spaces into tabs. You can select "Convert Indent To Spaces" to convert tabs back into spaces.
_________
First, we define some class variables to dictate how our player will move about the world.
.. note:: Throughout this tutorial, **variables defined outside functions will be
referred to as "class variables"**. This is because we can access any of these
variables from any place in the script.
Let's go through each of the class variables:
- ``GRAVITY``: How strong gravity pulls us down.
- ``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 quickly we accelerate. The higher the value, the sooner we get to max speed.
- ``DEACCEL``: How quickly we are going to decelerate. The higher the value, the sooner we will come to a complete stop.
- ``MAX_SLOPE_ANGLE``: The steepest angle our :ref:`KinematicBody <class_KinematicBody>` will consider as a 'floor'.
- ``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.
You can tweak many of these variables to get different results. For example, by lowering ``GRAVITY`` and/or
increasing ``JUMP_SPEED`` you can get a more 'floaty' feeling character.
Feel free to experiment!
.. note:: You may have noticed that ``MOUSE_SENSITIVITY`` is written in all caps like the other constants, but ``MOUSE_SENSITIVITY`` is not a constant.
The reason behind this is we want to treat it like a constant variable (a variable that cannot change) throughout our script, but we want to be
able to change the value later when we add customizable settings. So, in an effort to remind ourselves to treat it like a constant, it's named in all caps.
_________
Now let's look at the ``_ready`` function:
First we get the ``camera`` and ``rotation_helper`` nodes and store them into their variables.
Then we need to set the mouse mode to captured, so the mouse cannot leave the game window.
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 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 happens, 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.
_________
Next let's take a look at ``_physics_process``:
All we're doing in ``_physics_process`` is calling two functions: ``process_input`` and ``process_movement``.
``process_input`` will be where we store all the code relating to player input. We want to call it first, before
anything else, so we have fresh player input to work with.
``process_movement`` is where we'll send all the data necessary to the :ref:`KinematicBody <class_KinematicBody>`
so it can move through the game world.
_________
Let's look at ``process_input`` next:
First we set ``dir`` to an empty :ref:`Vector3 <class_Vector3>`.
``dir`` will be used for storing the direction the player intends to move towards. Because we do not
want the player's previous input to effect the player beyond a single ``process_movement`` call, we reset ``dir``.
Next we get the camera's global transform and store it as well, into the ``cam_xform`` variable.
The reason we need the camera's global transform is so we can use its directional vectors.
Many have found directional vectors confusing, so let's 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 single, 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:
.. tabs::
.. code-tab:: gdscript GDScript
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))
.. code-tab:: csharp
if (Input.IsActionPressed("movement_forward"))
node.Translate(new Vector3(0, 0, 1));
if (Input.IsActionPressed("movement_backward"))
node.Translate(new Vector3(0, 0, -1));
if (Input.IsActionPressed("movement_left"))
node.Translate(new Vector3(1, 0, 0));
if (Input.IsActionPressed("movement_right"))
node.Translate(new Vector3(-1, 0, 0));
.. note:: Notice how we do not need to do any calculations to get world space directional vectors.
We can 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 an 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 question from Game Development Stack Exchange 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 :ref:`Spatial <class_Spatial>` node's local directional vectors, we use this code:
.. tabs::
.. code-tab:: gdscript GDScript
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())
.. code-tab:: csharp
if (Input.IsActionPressed("movement_forward"))
node.Translate(node.GlobalTransform.basis.z.Normalized());
if (Input.IsActionPressed("movement_backward"))
node.Translate(-node.GlobalTransform.basis.z.Normalized());
if (Input.IsActionPressed("movement_left"))
node.Translate(node.GlobalTransform.basis.x.Normalized());
if (Input.IsActionPressed("movement_right"))
node.Translate(-node.GlobalTransform.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 :kbd:`T` or 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.
_________
Okay, back to ``process_input``:
Next we make a new variable called ``input_movement_vector`` and assign it to an empty :ref:`Vector2 <class_Vector2>`.
We will use this to make a virtual axis of sorts, to map the player's input to movement.
.. note:: This may seem overkill for just the keyboard, but this will make sense later when we add joypad input.
Based on which directional movement action is pressed, we add to or subtract from ``input_movement_vector``.
After we've checked each of the directional movement actions, we normalize ``input_movement_vector``. This makes it where ``input_movement_vector``'s values
are within a ``1`` radius unit circle.
Next we add the camera's local ``Z`` vector times ``input_movement_vector.y`` to ``dir``. This is so when the player presses forward or backwards, we add the camera's
local ``Z`` axis so the player moves forward or backwards in relation to the camera.
.. note:: Because the camera is rotated by ``-180`` degrees, we have to flip the ``Z`` directional vector.
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.
We do the same thing for the camera's local ``X`` vector, and instead of using ``input_movement_vector.y`` we instead use ``input_movement_vector.x``.
This makes it where the player moves left/right in relation to the camera when the player presses left/right.
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 the player's ``Y`` velocity to
``JUMP_SPEED``.
Because we're setting the Y velocity, the player will jump into the air.
Then we check for the ``ui_cancel`` action. This is so we can free/capture the mouse cursor when the ``escape`` button
is pressed. We do this because otherwise we'd have no way to free the cursor, meaning it would be stuck until you terminate the
runtime.
To free/capture the cursor, we check to see if the mouse is visible (freed) or not. If it is, we capture it, and if it's not, we make it visible (free it).
That's all we're doing right now for ``process_input``. We'll come back several times to this function as we add more complexities to our player.
_________
Now let's look at ``process_movement``:
First we ensure that ``dir`` does not have any movement on the ``Y`` axis by setting its ``Y`` value to zero.
Next we normalize ``dir`` to ensure we're within a ``1`` radius unit circle. This makes it where we're moving at a constant speed regardless
of whether the player is moving straight or diagonally. If we did not normalize, the player would move faster on the diagonal than when going straight.
Next we add gravity to the player by adding ``GRAVITY * delta`` to the player's ``Y`` velocity.
After that we assign the player's velocity to a new variable (called ``hvel``) and remove any movement on the ``Y`` axis.
Next we set a new variable (``target``) to the player's direction vector.
Then we multiply that by the player's max speed so we know how far the player will move in the direction provided by ``dir``.
After that we make a new variable for acceleration, named ``accel``.
We then take the dot product of ``hvel`` to see if the player is moving according to ``hvel``. Remember, ``hvel`` does not have any
``Y`` velocity, meaning we are only checking if the player is moving forwards, backwards, left, or right.
If the player is moving according to ``hvel``, then we set ``accel`` to the ``ACCEL`` constant so the player will accelerate, otherwise we set ``accel`` to
our ``DEACCEL`` constant so the player will decelerate.
Then we interpolate the horizontal velocity, set the player's ``X`` and ``Z`` velocity to the interpolated horizontal velocity,
and call ``move_and_slide`` to let the :ref:`KinematicBody <class_KinematicBody>` handle moving the player through the physics world.
.. tip:: All the code in ``process_movement`` is exactly the same as the movement code from the Kinematic Character demo!
_________
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 an :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.
.. note:: 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 relative 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 the player cannot rotate themselves upside down.
.. tip:: See :ref:`using transforms <doc_using_transforms>` for more information on rotating transforms.
_________
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 next few tutorial parts, so be sure to keep it open in one of your scene tabs.
Go ahead and test your code either by pressing :kbd:`F6` with ``Testing_Area.tscn`` as the open tab, by pressing the
play button in the top right corner, or by pressing :kbd:`F5`.
You should now be able to walk around, jump in the air, and look around using the mouse.
Giving the player a flash light and the option to sprint
--------------------------------------------------------
Before we get to making the weapons work, there are a couple more things we should add.
Many FPS games have an option to sprint and a flashlight. We can easily add these to our player,
so let's do that!
First we need a few more class variables in our player script:
.. tabs::
.. code-tab:: gdscript GDScript
const MAX_SPRINT_SPEED = 30
const SPRINT_ACCEL = 18
var is_sprinting = false
var flashlight
.. code-tab:: csharp
[Export]
public float MaxSprintSpeed = 30.0f;
[Export]
public float SprintAccel = 18.0f;
private bool _isSprinting = false;
private SpotLight _flashlight;
All the sprinting variables work exactly the same as the non sprinting variables with
similar names.
``is_sprinting`` is a boolean to track whether the player is currently sprinting, and ``flashlight`` is a variable
we will be using to hold the player's flash light node.
Now we need to add a few lines of code, starting in ``_ready``. Add the following to ``_ready``:
.. tabs::
.. code-tab:: gdscript GDScript
flashlight = $Rotation_Helper/Flashlight
.. code-tab:: csharp
_flashlight = GetNode<SpotLight>("Rotation_Helper/Flashlight");
This gets the ``Flashlight`` node and assigns it to the ``flashlight`` variable.
_________
Now we need to change some of the code in ``process_input``. Add the following somewhere in ``process_input``:
.. tabs::
.. code-tab:: gdscript GDScript
# ----------------------------------
# Sprinting
if Input.is_action_pressed("movement_sprint"):
is_sprinting = true
else:
is_sprinting = false
# ----------------------------------
# ----------------------------------
# Turning the flashlight on/off
if Input.is_action_just_pressed("flashlight"):
if flashlight.is_visible_in_tree():
flashlight.hide()
else:
flashlight.show()
# ----------------------------------
.. code-tab:: csharp
// -------------------------------------------------------------------
// Sprinting
if (Input.IsActionPressed("movement_sprint"))
_isSprinting = true;
else
_isSprinting = false;
// -------------------------------------------------------------------
// -------------------------------------------------------------------
// Turning the flashlight on/off
if (Input.IsActionJustPressed("flashlight"))
{
if (_flashlight.IsVisibleInTree())
_flashlight.Hide();
else
_flashlight.Show();
}
Let's go over the additions:
We set ``is_sprinting`` to ``true`` when the player is holding down the ``movement_sprint`` action, and ``false``
when the ``movement_sprint`` action is released. In ``process_movement`` we'll add the code that makes the player faster when
they sprint. Here in ``process_input`` we are just going to change the ``is_sprinting`` variable.
We do something similar to freeing/capturing the cursor for handling the flashlight. We first check to see if the ``flashlight`` action
was just pressed. If it was, we then check to see if ``flashlight`` is visible in the scene tree. If it is, then we hide it, and if it's not, we show it.
_________
Now we need to change a couple things in ``process_movement``. First, replace ``target *= MAX_SPEED`` with the following:
.. tabs::
.. code-tab:: gdscript GDScript
if is_sprinting:
target *= MAX_SPRINT_SPEED
else:
target *= MAX_SPEED
.. code-tab:: csharp
if (_isSprinting)
target *= MaxSprintSpeed;
else
target *= MaxSpeed;
Now instead of always multiplying ``target`` by ``MAX_SPEED``, we first check to see if the player is sprinting or not.
If the player is sprinting, we instead multiply ``target`` by ``MAX_SPRINT_SPEED``.
Now all that's left is to change the acceleration when sprinting. Change ``accel = ACCEL`` to the following:
.. tabs::
.. code-tab:: gdscript GDScript
if is_sprinting:
accel = SPRINT_ACCEL
else:
accel = ACCEL
.. code-tab:: csharp
if (_isSprinting)
accel = SprintAccel;
else
accel = Accel;
Now, when the player is sprinting, we'll use ``SPRINT_ACCEL`` instead of ``ACCEL``, which will accelerate the player faster.
_________
You should now be able to sprint if you press :kbd:`Shift`, and can toggle the flash light on and off by pressing :kbd:`F`!
Go try it out! You can change the sprint-related class variables to make the player faster or slower when sprinting!
Final notes
-----------
.. image:: img/PartOneFinished.png
Whew! 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 from first person perspective with sprinting and a flash light!
.. 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 download the finished project for this part here: :download:`Godot_FPS_Part_1.zip <files/Godot_FPS_Part_1.zip>`

File diff suppressed because it is too large Load Diff

View File

@@ -1,697 +0,0 @@
.. _doc_fps_tutorial_part_three:
Part 3
======
Part overview
-------------
In this part, we will be limiting the player's weapons by giving them ammo. We will also
be giving the player the ability to reload, and we will be adding sounds when the
weapons fire.
.. image:: img/PartThreeFinished.png
.. note:: You are assumed to have finished :ref:`doc_fps_tutorial_part_two` before moving on to this part of the tutorial.
The finished project from :ref:`doc_fps_tutorial_part_two` will be the starting project for part 3
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 ``Space_Level.tscn`` (``assets/Space_Level_Objects/Space_Level.tscn``)
and/or ``Ruins_Level.tscn`` (``assets/Ruin_Level_Objects/Ruins_Level.tscn``).
``Space_Level.tscn`` and ``Ruins_Level.tscn`` are complete custom FPS levels
created for the purpose of this tutorial. Press ``Play Current Scene`` button,
or :kbd:`F6` on keyboard, and give each a try.
.. warning:: ``Space_Level.tscn`` is more graphically demanding of the GPU than ``Ruins_Level.tscn``. If your computer is struggling to render
``Space_Level.tscn``, try using ``Ruins_Level.tscn`` instead.
.. note::
Due to Godot updates since this tutorial was published, if you are using Godot 3.2 or later, you may need to apply the following changes to the Space Level and Ruins Level scenes:
- Open ``res://assets/Space_Level_Objects/Space_Level.tscn``.
- In the Scene tree dock, select the **Floor_and_Celing** node. In the Inspector dock, if the Mesh Library field under GridMap is ``[empty]``, set it to ``Space_Level_Mesh_Lib.tres`` by dragging the file ``res://assets/Space_Level_Objects/Space_Level_Mesh_Lib.tres`` from the FileSystem dock to that field.
- Do the same for the **Walls** node.
- Open ``res://assets/Ruin_Level_Objects/Ruins_Level.tscn``.
- In the Scene tree dock, select the **Floor** node. In the Inspector dock, if the Mesh Library field under GridMap is ``[empty]``, set it to ``Ruin_Level_Mesh_Lib.tres`` by dragging the file ``res://assets/Ruin_Level_Objects/Ruin_Level_Mesh_Lib.tres`` from the FileSystem dock into that field.
- Do the same for the **Walls** node.
You might have noticed there are several :ref:`RigidBody <class_RigidBody>` nodes placed throughout the level.
We can place ``RigidBody_hit_test.gd`` on them and then they will react to being hit with bullets, so let's do that!
Follow the instructions below for either (or both) of the scenes you want to use
.. tabs::
.. code-tab:: gdscript Space_Level.tscn
Expand "Other_Objects" and then expand "Physics_Objects".
Expand one of the "Barrel_Group" nodes and then select "Barrel_Rigid_Body" and open it using
the "Open in Editor" button.
This will bring you to the "Barrel_Rigid_Body" scene. From there, select the root node and
scroll the inspector down to the bottom.
Select the drop down arrow under the "Node" tab, and then select "Load". Navigate to
"RigidBody_hit_test.gd" and select "Open".
Return back to "Space_Level.tscn".
Expand one of the "Box_Group" nodes and then select "Crate_Rigid_Body" and open it using the
"Open in Editor" button.
This will bring you to the "Crate_Rigid_Body" scene. From there, select the root node and
scroll the inspector down to the bottom.
Select the drop down arrow under the "Node" tab, and then select "Load". Navigate to
"RigidBody_hit_test.gd" and select "Open".
Return to "Space_Level.tscn".
.. code-tab:: gdscript Ruins_Level.tscn
Expand "Misc_Objects" and then expand "Physics_Objects".
Select all the "Stone_Cube" RigidBodies and then in the inspector scroll down to the bottom.
Select the drop down arrow under the "Node" tab, and then select "Load". Navigate to
"RigidBody_hit_test.gd" and select "Open".
Return to "Ruins_Level.tscn".
Now you can fire at all the rigid bodies in either level and they will react to bullets hitting them!
Adding ammo
-----------
Now that the player has working guns, let's give them a limited amount of ammo.
Firstly, we need to define a few variables in each of our weapon scripts.
Open up ``Weapon_Pistol.gd`` and add the following class variables:
::
var ammo_in_weapon = 10
var spare_ammo = 20
const AMMO_IN_MAG = 10
* ``ammo_in_weapon``: The amount of ammo currently in the pistol
* ``spare_ammo``: The amount of ammo we have left in reserve for the pistol
* ``AMMO_IN_MAG``: The amount of ammo in a fully reloaded weapon/magazine
Now all we need to do is add a single line of code to ``fire_weapon``.
Add the following right under ``Clone.BULLET_DAMAGE = DAMAGE``: ``ammo_in_weapon -= 1``
This will remove one from ``ammo_in_weapon`` every time the player fires. Notice we're not checking to see
if the player has enough ammo or not in ``fire_weapon``. Instead, we're going to check to see if the player has enough ammo in ``Player.gd``.
_______
Now we need to add ammo for both the rifle and the knife.
.. note:: You may be wondering why we are adding ammo for the knife given it does not consume any ammunition.
The reason we want to add ammo to the knife is so we have a consistent interface for all our weapons.
If we did not add ammo variables for the knife, we would have to add checks for the knife. By adding the ammo
variables to the knife, we don't need to worry about whether or not all our weapons have the same variables.
Add the following class variables to ``Weapon_Rifle.gd``:
::
var ammo_in_weapon = 50
var spare_ammo = 100
const AMMO_IN_MAG = 50
And then add the following to ``fire_weapon``: ``ammo_in_weapon -= 1``. Make sure that ``ammo_in_weapon -= 1`` is outside of the ``if ray.is_colliding()`` check so
the player loses ammo regardless of whether the player hit something or not.
Now all that's left is the knife. Add the following to ``Weapon_Knife.gd``:
::
var ammo_in_weapon = 1
var spare_ammo = 1
const AMMO_IN_MAG = 1
Because the knife does not consume ammo, that is all we need to add.
_______
Now we need to change one thing in ``Player.gd``, that is to say,
how we're firing the weapons in ``process_input``. Change the code for firing weapons to the following:
::
# ----------------------------------
# Firing the weapons
if Input.is_action_pressed("fire"):
if changing_weapon == false:
var current_weapon = weapons[current_weapon_name]
if current_weapon != null:
if current_weapon.ammo_in_weapon > 0:
if animation_manager.current_state == current_weapon.IDLE_ANIM_NAME:
animation_manager.set_animation(current_weapon.FIRE_ANIM_NAME)
# ----------------------------------
Now the weapons have a limited amount of ammo, and will stop firing when the player runs out.
_______
Ideally, we'd like to let the player be able to see how much ammo is left. Let's make a new function called ``process_UI``.
First, add ``process_UI(delta)`` to ``_physics_process``.
Now add the following to ``Player.gd``:
::
func process_UI(delta):
if current_weapon_name == "UNARMED" or current_weapon_name == "KNIFE":
UI_status_label.text = "HEALTH: " + str(health)
else:
var current_weapon = weapons[current_weapon_name]
UI_status_label.text = "HEALTH: " + str(health) + \
"\nAMMO: " + str(current_weapon.ammo_in_weapon) + "/" + str(current_weapon.spare_ammo)
Let's go over what's happening:
Firstly, we check to see if the current weapon is either ``UNARMED`` or ``KNIFE``. If it is, we
change the ``UI_status_label``'s text to only show the player's health since ``UNARMED`` and ``KNIFE`` do not consume ammo.
If the player is using a weapon that consumes ammo, we first get the weapon node.
Then we change ``UI_status_label``'s text to show the player's health, along with how much ammo the player has in the weapon
and how much spare ammo the player has for that weapon.
Now we can see how much ammo the player has through the HUD.
Adding reloading to the weapons
-------------------------------
Now that the player can run out of ammo, we need a way to let the player fill them back up. Let's add reloading next!
For reloading, we need to add a few more variables and a function to every weapon.
Open up ``Weapon_Pistol.gd`` and add the following class variables:
::
const CAN_RELOAD = true
const CAN_REFILL = true
const RELOADING_ANIM_NAME = "Pistol_reload"
* ``CAN_RELOAD``: A boolean to track whether this weapon has the ability to reload
* ``CAN_REFILL``: A boolean to track whether we can refill this weapon's spare ammo. We will not be using ``CAN_REFILL`` in this part, but we will in the next part!
* ``RELOADING_ANIM_NAME``: The name of the reloading animation for this weapon.
Now we need to add a function for handling reloading. Add the following function to ``Weapon_Pistol.gd``:
::
func reload_weapon():
var can_reload = false
if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
can_reload = true
if spare_ammo <= 0 or ammo_in_weapon == AMMO_IN_MAG:
can_reload = false
if can_reload == true:
var ammo_needed = AMMO_IN_MAG - ammo_in_weapon
if spare_ammo >= ammo_needed:
spare_ammo -= ammo_needed
ammo_in_weapon = AMMO_IN_MAG
else:
ammo_in_weapon += spare_ammo
spare_ammo = 0
player_node.animation_manager.set_animation(RELOADING_ANIM_NAME)
return true
return false
Let's go over what's happening:
First we define a variable to see whether or not this weapon can reload.
Then we check to see if the player is in this weapon's idle animation state because we only want to be able to reload when the player is not
firing, equipping, or unequipping.
Next we check to see if the player has spare ammo, and if the ammo already in the weapon is equal to a fully reloaded weapon.
This way we can ensure the player cannot reload when the player has no ammo or when the weapon is already full of ammo.
If we can still reload, then we calculate the amount of ammo needed to reload the weapon.
If the player has enough ammo to fill the weapon, we remove the ammo needed from ``spare_ammo`` and then set ``ammo_in_weapon`` to a full weapon/magazine.
If the player does not have enough ammo, we add all the ammo left in ``spare_ammo``, and then set ``spare_ammo`` to ``0``.
Next we play the reloading animation for this weapon, and then return ``true``.
If the player could not reload, we return ``false``.
_______
Now we need to add reloading to the rifle. Open up ``Weapon_Rifle.gd`` and add the following class variables:
::
const CAN_RELOAD = true
const CAN_REFILL = true
const RELOADING_ANIM_NAME = "Rifle_reload"
These variables are exactly the same as the pistol, just with ``RELOADING_ANIM_NAME`` changed to the rifle's reloading animation.
Now we need to add ``reload_weapon`` to ``Weapon_Rifle.gd``:
::
func reload_weapon():
var can_reload = false
if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
can_reload = true
if spare_ammo <= 0 or ammo_in_weapon == AMMO_IN_MAG:
can_reload = false
if can_reload == true:
var ammo_needed = AMMO_IN_MAG - ammo_in_weapon
if spare_ammo >= ammo_needed:
spare_ammo -= ammo_needed
ammo_in_weapon = AMMO_IN_MAG
else:
ammo_in_weapon += spare_ammo
spare_ammo = 0
player_node.animation_manager.set_animation(RELOADING_ANIM_NAME)
return true
return false
This code is exactly the same as the one for the pistol.
_______
The last bit we need to do for the weapons is add 'reloading' to the knife. Add the following class variables to ``Weapon_Knife.gd``:
::
const CAN_RELOAD = false
const CAN_REFILL = false
const RELOADING_ANIM_NAME = ""
Since we both cannot reload or refill a knife, we set both constants to ``false``. We also define ``RELOADING_ANIM_NAME`` as an empty string, since the knife
has no reloading animation.
Now we need to add ``reloading_weapon``:
::
func reload_weapon():
return false
Since we cannot reload a knife, we always return ``false``.
Adding reloading to the player
------------------------------
Now we need to add a few things to ``Player.gd``. First we need to define a new class variable:
::
var reloading_weapon = false
* ``reloading_weapon``: A variable to track whether or not the player is currently trying to reload.
Next we need to add another function call to ``_physics_process``.
Add ``process_reloading(delta)`` to ``_physics_process``. Now ``_physics_process`` should look something like this:
::
func _physics_process(delta):
process_input(delta)
process_movement(delta)
process_changing_weapons(delta)
process_reloading(delta)
process_UI(delta)
Now we need to add ``process_reloading``. Add the following function to ``Player.gd``:
::
func process_reloading(delta):
if reloading_weapon == true:
var current_weapon = weapons[current_weapon_name]
if current_weapon != null:
current_weapon.reload_weapon()
reloading_weapon = false
Let's go over what's happening here.
Firstly, we check to make sure the player is trying to reload.
If the player is trying to reload, we then get the current weapon. If the current weapon does not equal ``null``, we call its ``reload_weapon`` function.
.. note:: If the current weapon is equal to ``null``, then the current weapon is ``UNARMED``.
Finally, we set ``reloading_weapon`` to ``false`` because, regardless of whether the player successfully reloaded, we've tried reloading
and no longer need to keep trying.
_______
Before we can let the player reload, we need to change a few things in ``process_input``.
The first thing we need to change is in the code for changing weapons. We need to add an additional check (``if reloading_weapon == false:``) to see if the player is reloading:
::
if changing_weapon == false:
# New line of code here!
if reloading_weapon == false:
if WEAPON_NUMBER_TO_NAME[weapon_change_number] != current_weapon_name:
changing_weapon_name = WEAPON_NUMBER_TO_NAME[weapon_change_number]
changing_weapon = true
This makes it so the player cannot change weapons if the player is reloading.
Now we need to add the code to trigger a reload when the player pushes the ``reload`` action. Add the following code to ``process_input``:
::
# ----------------------------------
# Reloading
if reloading_weapon == false:
if changing_weapon == false:
if Input.is_action_just_pressed("reload"):
var current_weapon = weapons[current_weapon_name]
if current_weapon != null:
if current_weapon.CAN_RELOAD == true:
var current_anim_state = animation_manager.current_state
var is_reloading = false
for weapon in weapons:
var weapon_node = weapons[weapon]
if weapon_node != null:
if current_anim_state == weapon_node.RELOADING_ANIM_NAME:
is_reloading = true
if is_reloading == false:
reloading_weapon = true
# ----------------------------------
Let's go over what's happening here.
First we make sure the player is not reloading already, nor is the player trying to change weapons.
Then we check to see if the ``reload`` action has been pressed.
If the player has pressed ``reload``, we then get the current weapon and check to make sure it is not ``null``. Then we check to see whether the
weapon can reload or not using its ``CAN_RELOAD`` constant.
If the weapon can reload, we then get the current animation state, and make a variable for tracking whether the player is already reloading or not.
We then go through every weapon to make sure the player is not already playing that weapon's reloading animation.
If the player is not reloading any weapon, we set ``reloading_weapon`` to ``true``.
_______
One thing I like to add is where the weapon will reload itself if you try to fire it and it's out of ammo.
We also need to add an additional if check (``is_reloading_weapon == false:``) so the player cannot fire the current weapon while
reloading.
Let's change our firing code in ``process_input`` so it reloads when trying to fire an empty weapon:
::
# ----------------------------------
# Firing the weapons
if Input.is_action_pressed("fire"):
if reloading_weapon == false:
if changing_weapon == false:
var current_weapon = weapons[current_weapon_name]
if current_weapon != null:
if current_weapon.ammo_in_weapon > 0:
if animation_manager.current_state == current_weapon.IDLE_ANIM_NAME:
animation_manager.set_animation(current_weapon.FIRE_ANIM_NAME)
else:
reloading_weapon = true
# ----------------------------------
Now we check to make sure the player is not reloading before we fire the weapon, and when we have ``0`` or less ammo in the current weapon,
we set ``reloading_weapon`` to ``true`` if the player tries to fire.
This will make it so the player will try to reload when attempting to fire an empty weapon.
_______
With that done, the player can now reload! Give it a try! Now you can fire all the spare ammo for each weapon.
Adding sounds
-------------
Finally, let's add some sounds that accompany the player firing, reloading and changing weapons.
.. 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.
Open up ``Simple_Audio_Player.tscn``. It is simply a :ref:`Spatial <class_Spatial>` with an :ref:`AudioStreamPlayer <class_AudioStreamPlayer>` as its 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.
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 ``Simple_Audio_Player.gd``. Attach it to the :ref:`Spatial <class_Spatial>` in ``Simple_Audio_Player.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 = $Audio_Stream_Player
audio_node.connect("finished", self, "destroy_self")
audio_node.stop()
func play_sound(sound_name, position=null):
if audio_pistol_shot == null or audio_rifle_shot == null or audio_gun_cock == null:
print ("Audio not set!")
queue_free()
return
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 an AudioStreamPlayer3D, then uncomment these lines to set the position.
#if audio_node is AudioStreamPlayer3D:
# 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 ``play_sound``.
Let's go over what's happening here:
_________
In ``_ready``, we get the :ref:`AudioStreamPlayer <class_AudioStreamPlayer>` and connect its ``finished`` signal to the ``destroy_self`` function.
It doesn't matter if it's an :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 one of the three sounds we set the audio stream in :ref:`AudioStreamPlayer <class_AudioStreamPlayer>`
to the correct sound.
If it is an unknown sound, we print an error message to the console and free the audio player.
If you are using an :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 the audio player
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.
One of the biggest flaws with this system is we can only play sounds from ``Player.gd``.
Ideally we'd like to be able to play sounds from any script at any time.
_________
With that done, let's open up ``Player.gd`` again.
First we need to load the ``Simple_Audio_Player.tscn``. Place the following code in the class variables section of the script:
::
var simple_audio_player = preload("res://Simple_Audio_Player.tscn")
Now we need to instance the simple audio player when we need it, and then call its
``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 in ``Player.gd``:
::
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)
Let's 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, and this has a large (though safe) 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 any of the other provided levels. **We are making a huge assumption that the first child of the root node
is the root scene that the player is under, which may 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 does not work
reliably if you have multiple scenes loaded as children of the root node at a time, which will rarely happen for most projects and will not be happening in this tutorial series.
This is only potentially a issue depending on how you handle scene loading.
The third line adds our newly created ``Simple_Audio_Player`` 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 passed in to ``create_sound``. This will call
``Simple_Audio_Player.gd``'s ``play_sound`` function with the passed in arguments.
_________
Now all that is left is playing the sounds when we want to. Let's add sound to the pistol first!
Open up ``Weapon_Pistol.gd``.
Now, we want to make a noise when the player fires the pistol, so add the following to the end of the ``fire_weapon`` function:
::
player_node.create_sound("Pistol_shot", self.global_transform.origin)
Now when the player fires the pistol, we'll play the ``Pistol_shot`` sound.
To make a sound when the player reloads, we need to add the following right under ``player_node.animation_manager.set_animation(RELOADING_ANIM_NAME)`` in the
``reload_weapon`` function:
::
player_node.create_sound("Gun_cock", player_node.camera.global_transform.origin)
Now when the player reloads, we'll play the ``Gun_cock`` sound.
_________
Now let's add sounds to the rifle.
Open up ``Weapon_Rifle.gd``.
To play sounds when the rifle is fired, add the following to the end of the ``fire_weapon`` function:
::
player_node.create_sound("Rifle_shot", ray.global_transform.origin)
Now when the player fires the rifle, we'll play the ``Rifle_shot`` sound.
To make a sound when the player reloads, we need to add the following right under ``player_node.animation_manager.set_animation(RELOADING_ANIM_NAME)`` in the
``reload_weapon`` function:
::
player_node.create_sound("Gun_cock", player_node.camera.global_transform.origin)
Now when the player reloads, we'll play the ``Gun_cock`` sound.
Final notes
-----------
.. image:: img/PartThreeFinished.png
Now you have weapons with limited ammo that play sounds when you fire them!
At this point, we have all the basics of an FPS game working.
There are still a few things that would be nice to add, and we're going to add them in the next three parts!
For example, right now we have no way to add ammo to our spares, so we'll eventually run out. Also, we don't
have anything to shoot at outside of the :ref:`RigidBody <class_RigidBody>` nodes.
In :ref:`doc_fps_tutorial_part_four` we'll add some targets to shoot at, along with some health and ammo pick ups!
We're also going to add joypad support, so we can play with wired Xbox 360 controllers!
.. warning:: If you ever get lost, be sure to read over the code again!
You can download the finished project for this part here: :download:`Godot_FPS_Part_3.zip <files/Godot_FPS_Part_3.zip>`

File diff suppressed because it is too large Load Diff