mirror of
https://github.com/godotengine/godot-docs.git
synced 2026-01-07 02:12:07 +03:00
705 lines
30 KiB
ReStructuredText
705 lines
30 KiB
ReStructuredText
.. _doc_fps_tutorial_part_three:
|
|
|
|
Part 3
|
|
======
|
|
|
|
Part Overview
|
|
-------------
|
|
|
|
In this part we will be limiting our guns by giving them ammo. We will also
|
|
be giving the player the ability to reload, and we will be adding sounds when the
|
|
guns fire.
|
|
|
|
.. image:: img/PartThreeFinished.png
|
|
|
|
By the end of this part, the player will have limited ammo, the ability to reload,
|
|
and sounds will play when the player fires and changes weapons.
|
|
|
|
.. note:: You are assumed to have finished :ref:`part two <doc_fps_tutorial_part_two>` before moving on to this part of the tutorial.
|
|
|
|
Let's get started!
|
|
|
|
|
|
Changing levels
|
|
---------------
|
|
|
|
Now that we have a fully working FPS, let's move to a more FPS like level. Open up ``Test_Level.tscn``.
|
|
``Test_Level.tscn`` is a complete custom FPS level created for the purpose of this tutorial. Press ``F6`` to
|
|
play the open scene, or press the "play current scene button", and give it a whirl.
|
|
|
|
.. warning:: There will (likely) be the occasional random freeze as you go through the level. This is a known
|
|
issue.
|
|
|
|
If you find any way to solve it, please let me know on the Github repository, the Godot forums,
|
|
or on Twitter! Be sure to include ``@TwistedTwigleg`` so I will have a greater chance of seeing it!
|
|
|
|
You might have noticed there are several boxes and cylinders placed throughout the level. They are :ref:`RigidBody <class_RigidBody>`
|
|
nodes we can place ``RigidBody_hit_test.gd`` on and then they will react to being hit with bullets, so lets do that!
|
|
|
|
Select ``Center_room`` and open it up. From there select ``Physics_objects`` and open that up. You'll find there are
|
|
``6`` crates in a seemingly random order. Go select one of them and press the "Open in Editor" button. It's the one that
|
|
looks like a little movie slide.
|
|
|
|
.. note:: The reason the objects seem to be placed in a random order is because all of the objects were copied and pasted around
|
|
in the Godot editor to save on time. If you want to move any of the nodes around, it is highly suggested to just
|
|
left click inside the editor viewport to get the node you want, and then move it around with the :ref:`Spatial <class_Spatial>` gizmo.
|
|
|
|
This will bring you to the crate's scene. From there, select the ``Crate`` :ref:`RigidBody <class_RigidBody>` (the one that is the root of the scene)
|
|
and scroll down in the inspector until you get to the script section. From there, click the drop down and select "Load". Chose
|
|
``RigidBody_hit_test.gd`` and then return to ``Test_Level.tscn``.
|
|
|
|
Now open ``Upper_room``, select ``Physics_objects``, and chose one of the cylinder :ref:`RigidBody <class_RigidBody>` nodes.
|
|
Press the "Open in Editor" button beside one of the cylinders. This will bring you to the cylinder's scene.
|
|
|
|
From there, select the ``Cylinder`` :ref:`RigidBody <class_RigidBody>` (the one that is the root of the scene)
|
|
and scroll down in the inspector until you get to the script section. From there, click the drop down and select "Load". Chose
|
|
``RigidBody_hit_test.gd`` and then return to ``Test_Level.tscn``.
|
|
|
|
Now you can fire at the boxes and cylinders and they will react to your bullets just like the cubes in ``Testing_Area.tscn``!
|
|
|
|
|
|
Adding ammo
|
|
-----------
|
|
|
|
Now that we've got working guns, lets give them a limited amount of ammo.
|
|
|
|
Lets define some more global variables in ``Player.gd``, ideally nearby the other gun related variables:
|
|
|
|
::
|
|
|
|
var ammo_for_guns = {"PISTOL":60, "RIFLE":160, "KNIFE":1}
|
|
var ammo_in_guns = {"PISTOL":20, "RIFLE":80, "KNIFE":1}
|
|
const AMMO_IN_MAGS = {"PISTOL":20, "RIFLE":80, "KNIFE":1}
|
|
|
|
|
|
Here is what these variables will be doing for us:
|
|
|
|
- ``ammo_for_guns``: The amount of ammo we have in reserve for each weapon/gun.
|
|
- ``ammo_in_guns``: The amount of ammo currently inside the weapon/gun.
|
|
- ``AMMO_IN_MAGS``: How much ammo is in a fully filled weapon/gun.
|
|
|
|
.. note:: There is no reason we've included ammo for the knife, so feel free to remove the knife's ammo
|
|
if you desire.
|
|
|
|
Depending on how you program melee weapons, you may need to define an ammo count even if the
|
|
weapon does not use ammo. Some games use extremely short range 'guns' as their melee weapons,
|
|
and in those cases you may need to define ammo for your melee weapons.
|
|
|
|
_________
|
|
|
|
Now we need to add a few ``if`` checks to ``_physics_process``.
|
|
|
|
We need to make sure we have ammo in our gun before we try to fire a bullet.
|
|
Go find the line that checks for the fire action being pressed and add the following new
|
|
bits of code:
|
|
|
|
::
|
|
|
|
# NOTE: You should have this if condition in your _physics_process function
|
|
# Firing the weapons
|
|
if Input.is_action_pressed("fire"):
|
|
if current_gun == "PISTOL":
|
|
if ammo_in_guns["PISTOL"] > 0: # NEW CODE
|
|
if animation_manager.current_state == "Pistol_idle":
|
|
animation_manager.set_animation("Pistol_fire")
|
|
|
|
elif current_gun == "RIFLE":
|
|
if ammo_in_guns["RIFLE"] > 0: # NEW CODE
|
|
if animation_manager.current_state == "Rifle_idle":
|
|
animation_manager.set_animation("Rifle_fire")
|
|
|
|
elif current_gun == "KNIFE":
|
|
if animation_manager.current_state == "Knife_idle":
|
|
animation_manager.set_animation("Knife_fire")
|
|
|
|
These two additional ``if`` checks make sure we have a bullet to fire before setting our firing animation.
|
|
|
|
While we're still in ``_physics_process``, let's also add a way to track how much ammo we have. Find the line that
|
|
has ``UI_status_label.text = "HEALTH: " + str(health)`` in ``_physics_process`` and replace it with the following:
|
|
|
|
::
|
|
|
|
# HUD (UI)
|
|
if current_gun == "UNARMED" or current_gun == "KNIFE":
|
|
UI_status_label.text = "HEALTH: " + str(health)
|
|
else:
|
|
UI_status_label.text = "HEALTH: " + str(health) + "\nAMMO:" + \
|
|
str(ammo_in_guns[current_gun]) + "/" + str(ammo_for_guns[current_gun])
|
|
|
|
.. tip:: Did you now that you can combine two lines using ``\``? We're using it here
|
|
so we do not have a extremely long line of code all on one line by splitting it
|
|
into two lines!
|
|
|
|
This will show the player how much ammo they currently have and how much ammo they currently have in reserve, only for
|
|
the appropriate weapons (not unarmed or the knife). Regardless of the currently selected weapon/gun, we will always show
|
|
how much health the player has
|
|
|
|
.. note:: we cannot just add ``ammo_for_guns[current_gun]`` or ``ammo_in_guns[current_gun]`` to the ``string`` we
|
|
are passing in to the :ref:`Label <class_Label>`. Instead we have to cast them from ``floats`` to ``strings``, which is what we are doing
|
|
by using ``str()``.
|
|
|
|
For more information on casting, see this page from wiki books:
|
|
https://en.wikibooks.org/wiki/Computer_Programming/Type_conversion
|
|
|
|
.. warning:: We are currently not using the player's health just yet in the tutorial. We will start
|
|
using health for the player and objects when we include turrets and targets in later parts.
|
|
|
|
|
|
Now we need to remove a bullet from the gun when we fire. To do that, we just need to add a few lines in
|
|
``fire_bullet``:
|
|
|
|
::
|
|
|
|
func fire_bullet():
|
|
if changing_gun == true:
|
|
return
|
|
|
|
# Pistol bullet handling: Spawn a bullet object!
|
|
if current_gun == "PISTOL":
|
|
var clone = bullet_scene.instance()
|
|
var scene_root = get_tree().root.get_children()[0]
|
|
scene_root.add_child(clone)
|
|
|
|
clone.global_transform = $Rotation_helper/Gun_fire_points/Pistol_point.global_transform
|
|
# The bullet is a little too small (by default), so let's make it bigger!
|
|
clone.scale = Vector3(4, 4, 4)
|
|
|
|
ammo_in_guns["PISTOL"] -= 1 # NEW CODE
|
|
|
|
# Rifle bullet handeling: Send a raycast!
|
|
elif current_gun == "RIFLE":
|
|
var ray = $Rotation_helper/Gun_fire_points/Rifle_point/RayCast
|
|
ray.force_raycast_update()
|
|
|
|
if ray.is_colliding():
|
|
var body = ray.get_collider()
|
|
if body.has_method("bullet_hit"):
|
|
body.bullet_hit(RIFLE_DAMAGE, ray.get_collision_point())
|
|
|
|
ammo_in_guns["RIFLE"] -= 1 # NEW CODE
|
|
|
|
# Knife bullet(?) handeling: Use an area!
|
|
elif current_gun == "KNIFE":
|
|
var area = $Rotation_helper/Gun_fire_points/Knife_point/Area
|
|
var bodies = area.get_overlapping_bodies()
|
|
|
|
for body in bodies:
|
|
if body.has_method("bullet_hit"):
|
|
body.bullet_hit(KNIFE_DAMAGE, area.global_transform.origin)
|
|
|
|
|
|
Go play the project again! Now you'll lose ammo as you fire, until you reach zero and
|
|
cannot fire anymore.
|
|
|
|
Adding reloading
|
|
----------------
|
|
|
|
Now that we can empty our gun, we need a way to refill it!
|
|
|
|
First, let's start by
|
|
adding another global variable. Add ``var reloading_gun = false`` somewhere along with your
|
|
other global variables, preferably near the other gun related variables.
|
|
|
|
_________
|
|
|
|
Now we need to add several things to ``_physics_process``.
|
|
|
|
First, let's make sure we cannot change guns while reloading.
|
|
We need to change the weapon changing code to include the following:
|
|
|
|
::
|
|
|
|
# Was "if changing_gun == false"
|
|
if changing_gun == false and reloading_gun == false:
|
|
if Input.is_key_pressed(KEY_1):
|
|
current_gun = "UNARMED"
|
|
changing_gun = true
|
|
elif Input.is_key_pressed(KEY_2):
|
|
current_gun = "KNIFE"
|
|
changing_gun = true
|
|
elif Input.is_key_pressed(KEY_3):
|
|
current_gun = "PISTOL"
|
|
changing_gun = true
|
|
elif Input.is_key_pressed(KEY_4):
|
|
current_gun = "RIFLE"
|
|
changing_gun = true
|
|
|
|
Now the player cannot change guns while reloading.
|
|
|
|
_________
|
|
|
|
Ideally we want the player to be able to reload when they chose, so lets given them
|
|
the ability to reload when they press the ``reload`` action. Add the following
|
|
somewhere in ``_physics_process``, ideally nearby your other input related code:
|
|
|
|
::
|
|
|
|
# Reloading
|
|
if reloading_gun == false:
|
|
if Input.is_action_just_pressed("reload"):
|
|
if current_gun == "PISTOL" or current_gun == "RIFLE"
|
|
if animation_manager.current_state != "Pistol_reload" and animation_manager.current_state != "Rifle_reload":
|
|
reloading_gun = true
|
|
|
|
First we see if the player is already reloading. If they are not, then we check if they've pressed
|
|
the reloading action. If they have pressed the ``reload`` action, we then check if they are using
|
|
a weapon that has the ability to be reloaded. Finally, we make sure they are not already
|
|
in a reloading animation. If they are not, we set ``reloading_gun`` to ``true``.
|
|
|
|
We do not want to do our reloading processing here with the input in an effort to keep game logic
|
|
separate from input logic. Keeping them separate makes the code easier to debug, and as a bonus it
|
|
keeps the input logic from being overly bloated.
|
|
|
|
_________
|
|
|
|
Finally, we need to add the code that actually handles reloading. Add the following code to ``_physics_process``,
|
|
ideally somewhere underneath the reloading input code you just inputted:
|
|
|
|
::
|
|
|
|
# Reloading logic
|
|
if reloading_gun == true:
|
|
var can_reload = false
|
|
|
|
if current_gun == "PISTOL":
|
|
if animation_manager.current_state == "Pistol_idle":
|
|
can_reload = true
|
|
elif current_gun == "RIFLE":
|
|
if animation_manager.current_state == "Rifle_idle":
|
|
can_reload = true
|
|
elif current_gun == "KNIFE":
|
|
can_reload = false
|
|
reloading_gun = false
|
|
else:
|
|
can_reload = false
|
|
reloading_gun = false
|
|
|
|
if ammo_for_guns[current_gun] <= 0 or ammo_in_guns[current_gun] == AMMO_IN_MAGS[current_gun]:
|
|
can_reload = false
|
|
reloading_gun = false
|
|
|
|
|
|
if can_reload == true:
|
|
var ammo_needed = AMMO_IN_MAGS[current_gun] - ammo_in_guns[current_gun]
|
|
|
|
if ammo_for_guns[current_gun] >= ammo_needed:
|
|
ammo_for_guns[current_gun] -= ammo_needed
|
|
ammo_in_guns[current_gun] = AMMO_IN_MAGS[current_gun]
|
|
else:
|
|
ammo_in_guns[current_gun] += ammo_for_guns[current_gun]
|
|
ammo_for_guns[current_gun] = 0
|
|
|
|
if current_gun == "PISTOL":
|
|
animation_manager.set_animation("Pistol_reload")
|
|
elif current_gun == "RIFLE":
|
|
animation_manager.set_animation("Rifle_reload")
|
|
|
|
reloading_gun = false
|
|
|
|
|
|
Lets go over what this code does.
|
|
|
|
_________
|
|
|
|
First we check if ``reloading_gun`` is ``true``. If it is we then go through a series of checks
|
|
to see if we can reload or not. We use ``can_reload`` as a variable to track whether or not
|
|
it is possible to reload.
|
|
|
|
We go through series of checks for each weapon. For the pistol and the rifle we check if
|
|
we're in an idle state or not. If we are, then we set ``can_reload`` to ``true``.
|
|
|
|
For the knife we do not want to reload, because you cannot reload a knife, so we set ``can_reload`` and ``reloading_gun``
|
|
to ``false``. If we are using a weapon that we do not have a ``if`` or ``elif`` check for, we set
|
|
``can_reload`` and ``reloading_gun`` to ``false``, as we do not want to be able to reload a weapon we are unaware of.
|
|
|
|
Next we check if we have ammo in reserve for the gun in question. We also check to make sure the gun we are trying to reload
|
|
is not already full of ammo. If the gun does not have ammo in reserve or the gun is already full, we set
|
|
``can_reload`` and ``reloading_gun`` to ``false``.
|
|
|
|
If we've made it through all those checks and we can reload, then we have a few more steps to take.
|
|
|
|
First we assign the ammo we are needing to fill the gun fully to the ``ammo_needed`` variable.
|
|
We just subtract the amount of ammo we currently have in our gun by the amount of ammo in a full magazine.
|
|
|
|
Then we check if have enough ammo in reserves to fill the gun fully. If we do, we subtract the amount of ammo
|
|
we need to refill our gun from the reserves, and we set the amount of ammo in the gun to full.
|
|
|
|
If we do not have enough ammo in reserves to fill the gun, we add all of the ammo left in reserves to our
|
|
gun and then set the ammo in reserves to zero, making it empty.
|
|
|
|
Regardless of how much ammo we've added to the gun, we set our animation to the reloading animation for the current gun.
|
|
|
|
Finally, we set ``reloading_gun`` to false because we have finished reloading the gun.
|
|
|
|
_________
|
|
|
|
Go test the project again, and you'll find you can reload your gun when it is not
|
|
full and when there is ammo left in the ammo reserves.
|
|
|
|
_________
|
|
|
|
Personally, I like the guns to automatically start reloading if we try to fire them
|
|
when they have no ammo in them, so lets add that! Add the following code to the input code for
|
|
firing the guns:
|
|
|
|
::
|
|
|
|
# Firing the weapons
|
|
if Input.is_action_pressed("fire"):
|
|
if current_gun == "PISTOL":
|
|
if ammo_in_guns["PISTOL"] > 0:
|
|
if animation_manager.current_state == "Pistol_idle":
|
|
animation_manager.set_animation("Pistol_fire")
|
|
# NEW CODE!
|
|
else:
|
|
reloading_gun = true
|
|
|
|
elif current_gun == "RIFLE":
|
|
if ammo_in_guns["RIFLE"] > 0:
|
|
if animation_manager.current_state == "Rifle_idle":
|
|
animation_manager.set_animation("Rifle_fire")
|
|
# NEW CODE!
|
|
else:
|
|
reloading_gun = true
|
|
|
|
elif current_gun == "KNIFE":
|
|
if animation_manager.current_state == "Knife_idle":
|
|
animation_manager.set_animation("Knife_fire")
|
|
|
|
Now whenever the player tries to fire the gun when it's empty, we automatically
|
|
set ``reloading_gun`` to true, which will reload the gun if possible.
|
|
|
|
Adding sounds
|
|
-------------
|
|
|
|
Finally, let's add some sounds that play when we are reloading, changing guns, and when we
|
|
are firing them.
|
|
|
|
.. tip:: There are no game sounds provided in this tutorial (for legal reasons).
|
|
https://gamesounds.xyz/ is a collection of **"royalty free or public domain music and sounds suitable for games"**.
|
|
I used Gamemaster's Gun Sound Pack, which can be found in the Sonniss.com GDC 2017 Game Audio Bundle.
|
|
|
|
The video tutorial will briefly show how to edit the audio files for use in the tutorial.
|
|
|
|
Open up ``SimpleAudioPlayer.tscn``. It is simply a :ref:`Spatial <class_Spatial>` with a :ref:'AudioStreamPlayer <class_AudioStreamPlayer>' as it's child.
|
|
|
|
.. note:: The reason this is called a 'simple' audio player is because we are not taking performance into account
|
|
and because the code is designed to provide sound in the simplest way possible. This will likely change
|
|
in a future part.
|
|
|
|
If you want to use 3D audio, so it sounds like it's coming from a location in 3D space, right click
|
|
the :ref:'AudioStreamPlayer <class_AudioStreamPlayer>' and select "Change type".
|
|
|
|
This will open the node browser. Navigate to :ref:'AudioStreamPlayer3D <class_AudioStreamPlayer3D>' and select "change".
|
|
In the source for this tutorial, we will be using :ref:'AudioStreamPlayer <class_AudioStreamPlayer>', but you can optionally
|
|
use :ref:'AudioStreamPlayer3D <class_AudioStreamPlayer3D>' if you desire, and the code provided below will work regardless of which
|
|
one you chose.
|
|
|
|
Create a new script and call it "SimpleAudioPlayer.gd". Attach it to the :ref:`Spatial <class_Spatial>` in ``SimpleAudioPlayer.tscn``
|
|
and insert the following code:
|
|
|
|
::
|
|
|
|
extends Spatial
|
|
|
|
# All of the audio files.
|
|
# You will need to provide your own sound files.
|
|
var audio_pistol_shot = preload("res://path_to_your_audio_here")
|
|
var audio_gun_cock = preload("res://path_to_your_audio_here")
|
|
var audio_rifle_shot = preload("res://path_to_your_audio_here")
|
|
|
|
var audio_node = null
|
|
|
|
func _ready():
|
|
audio_node = $AudioStreamPlayer
|
|
audio_node.connect("finished", self, "destroy_self")
|
|
audio_node.stop()
|
|
|
|
|
|
func play_sound(sound_name, position=null):
|
|
if sound_name == "Pistol_shot":
|
|
audio_node.stream = audio_pistol_shot
|
|
elif sound_name == "Rifle_shot":
|
|
audio_node.stream = audio_rifle_shot
|
|
elif sound_name == "Gun_cock":
|
|
audio_node.stream = audio_gun_cock
|
|
else:
|
|
print ("UNKNOWN STREAM")
|
|
queue_free()
|
|
return
|
|
|
|
# If you are using a AudioPlayer3D, then uncomment these lines to set the position.
|
|
# if position != null:
|
|
# audio_node.global_transform.origin = position
|
|
|
|
audio_node.play()
|
|
|
|
|
|
func destroy_self():
|
|
audio_node.stop()
|
|
queue_free()
|
|
|
|
|
|
.. tip:: By setting ``position`` to ``null`` by default in ``play_sound``, we are making it an optional argument,
|
|
meaning position doesn't necessarily have to be passed in to call the ``play_sound``.
|
|
|
|
Let's go over what's happening here:
|
|
|
|
_________
|
|
|
|
In ``_ready`` we get the :ref:'AudioStreamPlayer <class_AudioStreamPlayer>' and connect it's ``finished`` signal to ourselves.
|
|
It doesn't matter if it's :ref:'AudioStreamPlayer <class_AudioStreamPlayer>' or :ref:'AudioStreamPlayer3D <class_AudioStreamPlayer3D>' node,
|
|
as they both have the finished signal. To make sure it is not playing any sounds, we call ``stop`` on the :ref:'AudioStreamPlayer <class_AudioStreamPlayer>'.
|
|
|
|
.. warning:: Make sure your sound files are **not** set to loop! If it is set to loop
|
|
the sounds will continue to play infinitely and the script will not work!
|
|
|
|
The ``play_sound`` function is what we will be calling from ``Player.gd``. We check if the sound
|
|
is one of the three possible sounds, and if it is we set the audio stream for our :ref:'AudioStreamPlayer <class_AudioStreamPlayer>'
|
|
to the correct sound.
|
|
|
|
If it is an unknown sound, we print an error message to the console and free ourselves.
|
|
|
|
If you are using a :ref:'AudioStreamPlayer3D <class_AudioStreamPlayer3D>', remove the ``#`` to set the position of
|
|
the audio player node so it plays at the correct position.
|
|
|
|
Finally, we tell the :ref:'AudioStreamPlayer <class_AudioStreamPlayer>' to play.
|
|
|
|
When the :ref:'AudioStreamPlayer <class_AudioStreamPlayer>' is finished playing the sound, it will call ``destroy_self`` because
|
|
we connected the ``finished`` signal in ``_ready``. We stop the :ref:'AudioStreamPlayer <class_AudioStreamPlayer>' and free ourself
|
|
to save on resources.
|
|
|
|
.. note:: This system is extremely simple and has some major flaws:
|
|
One flaw is we have to pass in a string value to play a sound. While it is relatively simple
|
|
to remember the names of the three sounds, it can be increasingly complex when you have more sounds.
|
|
Ideally we'd place these sounds in some sort of container with exposed variables so we do not have
|
|
to remember the name(s) of each sound effect we want to play.
|
|
|
|
Another flaw is we cannot play looping sounds effects, nor background music easily with this system.
|
|
Because we cannot play looping sounds, certain effects like footstep sounds are harder to accomplish
|
|
because we then have to keep track of whether or not there is a sound effect *and* whether or not we
|
|
need to continue playing it.
|
|
|
|
_________
|
|
|
|
With that done, lets open up ``Player.gd`` again.
|
|
First we need to load the ``SimpleAudioPlayer.tscn``. Place the following code in your global variables:
|
|
|
|
::
|
|
|
|
var simple_audio_player = preload("res://SimpleAudioPlayer.tscn")
|
|
|
|
Now we just need to instance the simple audio player when we need it, and then call it's
|
|
``play_sound`` function and pass the name of the sound we want to play. To make the process simpler,
|
|
let's create a ``create_sound`` function:
|
|
|
|
::
|
|
|
|
func create_sound(sound_name, position=null):
|
|
var audio_clone = simple_audio_player.instance()
|
|
var scene_root = get_tree().root.get_children()[0]
|
|
scene_root.add_child(audio_clone)
|
|
audio_clone.play_sound(sound_name, position)
|
|
|
|
Lets walk through what this function does:
|
|
|
|
_________
|
|
|
|
The first line instances the ``simple_audio_player.tscn`` scene and assigns it to a variable,
|
|
named ``audio_clone``.
|
|
|
|
The second line gets the scene root, using one large assumption. We first get this node's :ref:`SceneTree <class_SceneTree>`,
|
|
and then access the root node, which in this case is the :ref:`Viewport <class_Viewport>` this entire game is running under.
|
|
Then we get the first child of the :ref:`Viewport <class_Viewport>`, which in our case happens to be the root node in
|
|
``Test_Area.tscn`` or ``Test_Level.tscn``. We are making a huge assumption that the first child of the root
|
|
is the root node that our player is under, which could not always be the case.
|
|
|
|
If this doesn't make sense to you, don't worry too much about it. The second line of code only doesn't work
|
|
reliably if you have multiple scenes loaded as childs to the root node at a time, which will rarely happen for most projects. This is really
|
|
only potentially a issue depending on how you handle scene loading.
|
|
|
|
The third line adds our newly created ``SimpleAudioPlayer`` scene to be a child of the scene root. This
|
|
works exactly the same as when we are spawning bullets.
|
|
|
|
Finally, we call the ``play_sound`` function and pass in the arguments we're given. This will call
|
|
``SimpleAudioPlayer.gd``'s ``play_sound`` function with the passed in arguments.
|
|
|
|
_________
|
|
|
|
Now all that is left is playing the sounds when we want to. First, let's play the shooting sounds
|
|
when a bullet is fired. Go to ``fire_bullet`` and add the following:
|
|
|
|
::
|
|
|
|
func fire_bullet():
|
|
if changing_gun == true:
|
|
return
|
|
|
|
# Pistol bullet handling: Spawn a bullet object!
|
|
if current_gun == "PISTOL":
|
|
var clone = bullet_scene.instance()
|
|
var scene_root = get_tree().root.get_children()[0]
|
|
scene_root.add_child(clone)
|
|
|
|
clone.global_transform = $Rotation_helper/Gun_fire_points/Pistol_point.global_transform
|
|
# The bullet is a little too small (by default), so let's make it bigger!
|
|
clone.scale = Vector3(4, 4, 4)
|
|
|
|
ammo_in_guns["PISTOL"] -= 1
|
|
create_sound("Pistol_shot", clone.global_transform.origin); # NEW CODE
|
|
|
|
# Rifle bullet handeling: Send a raycast!
|
|
elif current_gun == "RIFLE":
|
|
var ray = Rotation_helper/Gun_fire_points/Rifle_point/RayCast
|
|
ray.force_raycast_update()
|
|
|
|
if ray.is_colliding():
|
|
var body = ray.get_collider()
|
|
if body.has_method("bullet_hit"):
|
|
body.bullet_hit(RIFLE_DAMAGE, ray.get_collision_point())
|
|
|
|
ammo_in_guns["RIFLE"] -= 1
|
|
create_sound("Rifle_shot", ray.global_transform.origin); # NEW CODE
|
|
|
|
# Knife bullet(?) handeling: Use an area!
|
|
elif current_gun == "KNIFE":
|
|
var area = $Rotation_helper/Gun_fire_points/Knife_point/Area
|
|
var bodies = area.get_overlapping_bodies()
|
|
|
|
for body in bodies:
|
|
if body.has_method("bullet_hit"):
|
|
body.bullet_hit(KNIFE_DAMAGE, area.global_transform.origin)
|
|
|
|
Now we will play the shooting noise for both the pistol and the rifle when a bullet is created.
|
|
|
|
.. note:: We are passing in the positions of the ends of the guns using the bullet object's
|
|
global :ref:`Transform <class_transform>` and the :ref:`Raycast <class_raycast>`'s global :ref:`Transform <class_transform>`.
|
|
If you are not using a :ref:`AudioStreamPlayer3D <class_AudioStreamPlayer3D>` node, you can optionally leave the positions out and only
|
|
pass in the name of the sound you want to play.
|
|
|
|
Finally, lets play the sound of a gun being cocked when we reload and when we change weapons.
|
|
Add the following to our reloading logic section of ``_physics_process``:
|
|
|
|
::
|
|
|
|
# Reloading logic
|
|
if reloading_gun == true:
|
|
var can_reload = false
|
|
|
|
if current_gun == "PISTOL":
|
|
if animation_manager.current_state == "Pistol_idle":
|
|
can_reload = true
|
|
elif current_gun == "RIFLE":
|
|
if animation_manager.current_state == "Rifle_idle":
|
|
can_reload = true
|
|
elif current_gun == "KNIFE":
|
|
can_reload = false
|
|
reloading_gun = false
|
|
else:
|
|
can_reload = false
|
|
reloading_gun = false
|
|
|
|
if ammo_for_guns[current_gun] <= 0 or ammo_in_guns[current_gun] == AMMO_IN_MAGS[current_gun]:
|
|
can_reload = false
|
|
reloading_gun = false
|
|
|
|
|
|
if can_reload == true:
|
|
var ammo_needed = AMMO_IN_MAGS[current_gun] - ammo_in_guns[current_gun]
|
|
|
|
if ammo_for_guns[current_gun] >= ammo_needed:
|
|
ammo_for_guns[current_gun] -= ammo_needed
|
|
ammo_in_guns[current_gun] = AMMO_IN_MAGS[current_gun]
|
|
else:
|
|
ammo_in_guns[current_gun] += ammo_for_guns[current_gun]
|
|
ammo_for_guns[current_gun] = 0
|
|
|
|
if current_gun == "PISTOL":
|
|
animation_manager.set_animation("Pistol_reload")
|
|
elif current_gun == "RIFLE":
|
|
animation_manager.set_animation("Rifle_reload")
|
|
|
|
reloading_gun = false
|
|
create_sound("Gun_cock", camera.global_transform.origin) # NEW CODE
|
|
|
|
And add this code to the changing weapons section of ``_physics_process``:
|
|
|
|
::
|
|
|
|
if changing_gun == true:
|
|
if current_gun != "PISTOL":
|
|
if animation_manager.current_state == "Pistol_idle":
|
|
animation_manager.set_animation("Pistol_unequip")
|
|
if current_gun != "RIFLE":
|
|
if animation_manager.current_state == "Rifle_idle":
|
|
animation_manager.set_animation("Rifle_unequip")
|
|
if current_gun != "KNIFE":
|
|
if animation_manager.current_state == "Knife_idle":
|
|
animation_manager.set_animation("Knife_unequip")
|
|
|
|
if current_gun == "UNARMED":
|
|
if animation_manager.current_state == "Idle_unarmed":
|
|
changing_gun = false
|
|
|
|
elif current_gun == "KNIFE":
|
|
if animation_manager.current_state == "Knife_idle":
|
|
changing_gun = false
|
|
if animation_manager.current_state == "Idle_unarmed":
|
|
animation_manager.set_animation("Knife_equip")
|
|
|
|
elif current_gun == "PISTOL":
|
|
if animation_manager.current_state == "Pistol_idle":
|
|
changing_gun = false
|
|
if animation_manager.current_state == "Idle_unarmed":
|
|
animation_manager.set_animation("Pistol_equip")
|
|
|
|
create_sound("Gun_cock", camera.global_transform.origin) # NEW CODE
|
|
|
|
elif current_gun == "RIFLE":
|
|
if animation_manager.current_state == "Rifle_idle":
|
|
changing_gun = false
|
|
if animation_manager.current_state == "Idle_unarmed":
|
|
animation_manager.set_animation("Rifle_equip")
|
|
|
|
create_sound("Gun_cock", camera.global_transform.origin) # NEW CODE
|
|
|
|
Now whatever sound you have assigned to "Gun_cock" will play when you reload and when you
|
|
change to either the pistol or the rifle.
|
|
|
|
|
|
Final notes
|
|
-----------
|
|
|
|
.. image:: img/FinishedTutorialPicture.png
|
|
|
|
Now you have a fully working single player FPS!
|
|
|
|
You can find the completed project here: :download:`Godot_FPS_Finished.zip <files/Godot_FPS_Finished.zip>`
|
|
|
|
.. tip:: The finished project source is hosted on Github as well: https://github.com/TwistedTwigleg/Godot_FPS_Tutorial
|
|
|
|
You can also download all of the ``.blend`` files used here: :download:`Godot_FPS_BlenderFiles.zip <files/Godot_FPS_BlenderFiles.zip>`
|
|
|
|
.. note:: The finished project source files contain the same exact code, just written in a different order.
|
|
This is because the finished project source files are what the tutorial is based on.
|
|
|
|
The finished project code was written in the order that features were created, not necessarily
|
|
in a order that is ideal for learning.
|
|
|
|
Other than that, the source is exactly the same, just with helpful comments explaining what
|
|
each part does.
|
|
|
|
The skybox is created by **StumpyStrust** and can be found at OpenGameArt.org. https://opengameart.org/content/space-skyboxes-0
|
|
|
|
The font used is **Titillium-Regular**, and is licensed under the SIL Open Font License, Version 1.1.
|
|
|
|
The skybox was convert to a 360 equirectangular image using this tool: https://www.360toolkit.co/convert-cubemap-to-spherical-equirectangular.html
|
|
|
|
While no sounds are provided, you can find many game ready sounds at https://gamesounds.xyz/
|
|
|
|
.. warning:: OpenGameArt.org, 360toolkit.co, the creator(s) of Titillium-Regular, and GameSounds.xyz are in no way involved in this tutorial.
|
|
|
|
__________
|
|
|
|
In :ref:`part four <doc_fps_tutorial_part_four>` we will be refactoring/rewriting ``Player.gd`` to a more modular format, as well as adding joypad support!
|