|
@@ -0,0 +1,1300 @@
|
|
|
+.. _doc_fps_tutorial_part_four:
|
|
|
+
|
|
|
+Part 4
|
|
|
+======
|
|
|
+
|
|
|
+Part Overview
|
|
|
+-------------
|
|
|
+
|
|
|
+In this part we will be refactoring ``Player.gd`` to use a more modular format, add support for joypads, and add the ability to change weapons with the scroll wheel.
|
|
|
+
|
|
|
+.. image:: img/FinishedTutorialPicture.png
|
|
|
+
|
|
|
+While this part may not be the most interesting, it is very important. Having a clean and modular code base allows us to build
|
|
|
+more complex behaviour in the future.
|
|
|
+
|
|
|
+.. note:: You are assumed to have finished :ref:`part three <doc_fps_tutorial_part_three>` before moving on to this part of the tutorial.
|
|
|
+
|
|
|
+.. tip:: You can find the completed code for part three here: https://github.com/TwistedTwigleg/Godot_FPS_Tutorial/tree/part_3
|
|
|
+
|
|
|
+ .. image:: img/GithubDownloadZip.png
|
|
|
+
|
|
|
+ Just click the green "Clone or download" button and choose "Download Zip" to get the finished project for part 3.
|
|
|
+
|
|
|
+Let's get started!
|
|
|
+
|
|
|
+
|
|
|
+A quick note
|
|
|
+------------
|
|
|
+
|
|
|
+Before we dig into refactoring the code, let's quickly talk about *why* we want to refactor the code.
|
|
|
+
|
|
|
+First, what is refactoring? According to wikipedia:
|
|
|
+
|
|
|
+**"Code refactoring is the process of restructuring existing computer code—changing the factoring—without changing its external behaviour."**
|
|
|
+
|
|
|
+Basically, refactoring is taking code we've already written, and rewriting/restructuring it without changing what it does.
|
|
|
+
|
|
|
+Second, why refactor? There are plenty of reasons why you may want to refactor your code base, but for this tutorial there is really only three
|
|
|
+major reasons:
|
|
|
+
|
|
|
+1: By refactoring the code base we can take out certain elements from the various functions in ``player.gd`` and separate them into their own functions/scripts.
|
|
|
+``_physics_process`` benefits greatly from this, because while it does work right now, it is very confusing to navigate.
|
|
|
+
|
|
|
+2: With some careful refactoring, we can take out most of the gun logic from ``Player.gd`` and put them into their own scripts. This is key because it easily allows
|
|
|
+us to make/edit weapons and their behaviours without having to change much in ``Player.gd``.
|
|
|
+
|
|
|
+3: Currently performance in ``Player.gd`` is okay, but with some work we can make it even better! Performance was not a primary concern for the first three parts
|
|
|
+of this tutorial series, and while it still is not a major concern, we ideally want to write code with good performance when possible.
|
|
|
+
|
|
|
+All of these reasons are why we are going to refactor ``Player.gd``.
|
|
|
+
|
|
|
+What we plan on doing in this part is taking our very linear ``Player.gd`` script and make it more modular and extendible. This will allow us
|
|
|
+to more easily add features later, as well as make it easier to work with in later parts.
|
|
|
+
|
|
|
+.. note:: Even though part 4 is dedicated to refactoring ``Player.gd``, it is likely we will need to do more refactoring in later parts as we continue to add features!
|
|
|
+
|
|
|
+
|
|
|
+Breaking it down
|
|
|
+----------------
|
|
|
+
|
|
|
+Current a majority of the code in ``Player.gd`` is located in ``_physics_process``. Right now ``_physics_process`` is a huge function with several works parts.
|
|
|
+With some refactoring, we can break ``_physics_process`` into several smaller functions.
|
|
|
+
|
|
|
+Ideally we want to make these smaller functions focused on doing a small set of tasks.
|
|
|
+This makes it much easier to know where we need to add code to when we are working on new features.
|
|
|
+
|
|
|
+Another benefit of using smaller functions is they are generally easier to debug!
|
|
|
+
|
|
|
+Breaking down input processing
|
|
|
+______________________________
|
|
|
+
|
|
|
+First, lets make a function for handling all of the :ref:`Input <class_Input>` related code.
|
|
|
+This allows us to more clearly see all of our player input.
|
|
|
+
|
|
|
+Create new function called `process_input` and add the following code:
|
|
|
+
|
|
|
+::
|
|
|
+
|
|
|
+ func process_input(delta):
|
|
|
+ # ----------------------------------
|
|
|
+ # Walking
|
|
|
+
|
|
|
+ dir = Vector3()
|
|
|
+ var cam_xform = camera.get_global_transform()
|
|
|
+
|
|
|
+ var input_movement_vector = Vector2()
|
|
|
+
|
|
|
+ # Add keyboard input
|
|
|
+ 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()
|
|
|
+
|
|
|
+ dir += -cam_xform.basis.z.normalized() * input_movement_vector.y
|
|
|
+ dir += cam_xform.basis.x.normalized() * input_movement_vector.x
|
|
|
+ # ----------------------------------
|
|
|
+
|
|
|
+ # ----------------------------------
|
|
|
+ # Sprinting
|
|
|
+
|
|
|
+ if Input.is_action_pressed("movement_sprint"):
|
|
|
+ is_spriting = true
|
|
|
+ else:
|
|
|
+ is_spriting = false
|
|
|
+ # ----------------------------------
|
|
|
+
|
|
|
+ # ----------------------------------
|
|
|
+ # Jumping
|
|
|
+
|
|
|
+ if is_on_floor():
|
|
|
+ if Input.is_action_just_pressed("movement_jump"):
|
|
|
+ vel.y = JUMP_SPEED
|
|
|
+ # ----------------------------------
|
|
|
+
|
|
|
+ # ----------------------------------
|
|
|
+ # Changing weapons.
|
|
|
+
|
|
|
+ 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
|
|
|
+ # ----------------------------------
|
|
|
+
|
|
|
+ # ----------------------------------
|
|
|
+ # 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
|
|
|
+ # ----------------------------------
|
|
|
+
|
|
|
+ # ----------------------------------
|
|
|
+ # 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")
|
|
|
+ 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")
|
|
|
+ else:
|
|
|
+ reloading_gun = true
|
|
|
+
|
|
|
+ elif current_gun == "KNIFE":
|
|
|
+ if animation_manager.current_state == "Knife_idle":
|
|
|
+ animation_manager.set_animation("Knife_fire")
|
|
|
+ # ----------------------------------
|
|
|
+
|
|
|
+ # ----------------------------------
|
|
|
+ # Turning the flashlight on/off
|
|
|
+
|
|
|
+ if Input.is_action_just_pressed("flashlight"):
|
|
|
+ if flashlight.is_visible_in_tree():
|
|
|
+ flashlight.hide()
|
|
|
+ else:
|
|
|
+ flashlight.show()
|
|
|
+ # ----------------------------------
|
|
|
+
|
|
|
+ # ----------------------------------
|
|
|
+
|
|
|
+ # 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)
|
|
|
+ # ----------------------------------
|
|
|
+
|
|
|
+You may have noticed that all of the code so far is exactly the same as the :ref:`Input <class_Input>` relate code already written in ``_physics_process``,
|
|
|
+but is now all placed in one function.
|
|
|
+
|
|
|
+There are a few changes though:
|
|
|
+
|
|
|
+Because we are now calling our input code outside of ``_physics_process`` we need to change ``dir`` from a local variable to a global variable.
|
|
|
+Add ``var dir = Vector3()`` with the rest of the global variables, ideally nearby the movement code for organization.
|
|
|
+
|
|
|
+.. warning:: Do not forget to change ``dir`` to a global variable!
|
|
|
+
|
|
|
+Another change is we're not directly effecting ``dir`` any more. Before we were changing ``dir`` when a movement action was pressed. Now we are changing a new local variable,
|
|
|
+``input_movement_vector``, instead. This will later allow us to have more than one form of directional input. By multiplying ``input_movement_vector`` by the camera's
|
|
|
+directional vectors, we get the same result as when we were effecting ``dir`` directly.
|
|
|
+
|
|
|
+Notice how we are normalizing ``input_movement_vector`` as well. This is important because later when we add additional forms of directional input, we do not
|
|
|
+want to move faster if two forms of input are moving at the same time. For example, we do not want to move faster if we are pressing the ``UP`` key on the keyboard and also
|
|
|
+are pushing forward on a controller. If we did not normalize, then we'd move twice as fast! By normalizing, we make everyone move at the same speed, regardless of how many
|
|
|
+input devices they are using.
|
|
|
+
|
|
|
+Breaking down ``KinematicBody`` movement
|
|
|
+________________________________________
|
|
|
+
|
|
|
+Next we want to move all of the code relating to moving using the :ref:`KinematicBody <class_KinematicBody>` into its own function.
|
|
|
+This allows us to more clearly see what code we are sending :ref:`KinematicBody <class_KinematicBody>` and what it does.
|
|
|
+
|
|
|
+Create a new function and call it ``process_movement``. Lets add the following code:
|
|
|
+
|
|
|
+::
|
|
|
+
|
|
|
+ func process_movement(delta):
|
|
|
+ var grav = norm_grav
|
|
|
+
|
|
|
+ dir.y = 0
|
|
|
+ dir = dir.normalized()
|
|
|
+
|
|
|
+ vel.y += delta*grav
|
|
|
+
|
|
|
+ var hvel = vel
|
|
|
+ hvel.y = 0
|
|
|
+
|
|
|
+ var target = dir
|
|
|
+ if is_spriting:
|
|
|
+ target *= MAX_SPRINT_SPEED
|
|
|
+ else:
|
|
|
+ target *= MAX_SPEED
|
|
|
+
|
|
|
+ var accel
|
|
|
+ if dir.dot(hvel) > 0:
|
|
|
+ if is_spriting:
|
|
|
+ accel = SPRINT_ACCEL
|
|
|
+ else:
|
|
|
+ accel = ACCEL
|
|
|
+ else:
|
|
|
+ accel = DEACCEL
|
|
|
+
|
|
|
+ hvel = hvel.linear_interpolate(target, accel*delta)
|
|
|
+ vel.x = hvel.x
|
|
|
+ vel.z = hvel.z
|
|
|
+ vel = move_and_slide(vel,Vector3(0,1,0), 0.05, 4, deg2rad(MAX_SLOPE_ANGLE))
|
|
|
+
|
|
|
+Thankfully nothing is has changed here, all we've done is moved the code out of ``_physics_process``.
|
|
|
+
|
|
|
+.. warning:: If you are using Godot ``master`` branch (or Godot 3.1), you will need to change ``vel = move_and_slide(vel,Vector3(0,1,0), 0.05, 4, deg2rad(MAX_SLOPE_ANGLE))``
|
|
|
+ to ``vel = move_and_slide(vel,Vector3(0,1,0), true, 0.05, 4, deg2rad(MAX_SLOPE_ANGLE))``.
|
|
|
+
|
|
|
+Now when we are ready to have the :ref:`KinematicBody <class_KinematicBody>` process our movement and send us through space, all we need to do is call ``process_movement``.
|
|
|
+
|
|
|
+
|
|
|
+Changing the weapon code structure
|
|
|
+----------------------------------
|
|
|
+
|
|
|
+So far, we have not really changed the structure of the code, we've just shuffled it around, so lets change that.
|
|
|
+
|
|
|
+One of the major things we ideally want to change is how the weapon code is handled. Currently all of the weapon realted code is all in ``Player.gd``, everything
|
|
|
+from how much ammo a weapon carries, to firing bullets. While this has the advantage of having all of your code in one place, it would be much
|
|
|
+nicer if we make a weapon interface so we can create/change weapons easily without having to scroll through ``Player.gd`` to look for the bit of code we want to add/change.
|
|
|
+
|
|
|
+Open up ``Player.tscn`` and navigate to the ``Gun_fire_points`` node. Lets make the pistol first. Select ``Pistol_point`` and attach a node node and call it
|
|
|
+``Weapon_Pistol.gd``.
|
|
|
+
|
|
|
+Our weapon scripts are going to do four things: They're going to handle *firing*, *reloading*, *equipping*, and *unequipping*.
|
|
|
+
|
|
|
+Add the following code to ``Weapon_Pistol.gd``:
|
|
|
+
|
|
|
+::
|
|
|
+
|
|
|
+ extends Spatial
|
|
|
+
|
|
|
+ var ammo_in_weapon = 20;
|
|
|
+ var spare_ammo = 60;
|
|
|
+ const AMMO_IN_MAG = 20;
|
|
|
+ const DAMAGE = 15;
|
|
|
+
|
|
|
+ const CAN_RELOAD = true;
|
|
|
+
|
|
|
+ const RELOADING_ANIM_NAME = "Pistol_reload"
|
|
|
+ const IDLE_ANIM_NAME = "Pistol_idle"
|
|
|
+ const FIRE_ANIM_NAME = "Pistol_fire"
|
|
|
+
|
|
|
+ var is_weapon_enabled = false;
|
|
|
+
|
|
|
+ var bullet_scene = preload("Bullet_Scene.tscn")
|
|
|
+
|
|
|
+ var player_node = null;
|
|
|
+
|
|
|
+ func _ready():
|
|
|
+ pass;
|
|
|
+
|
|
|
+ func fire_weapon():
|
|
|
+ var clone = bullet_scene.instance()
|
|
|
+ var scene_root = get_tree().root.get_children()[0]
|
|
|
+ scene_root.add_child(clone)
|
|
|
+
|
|
|
+ clone.global_transform = self.global_transform
|
|
|
+ clone.scale = Vector3(4, 4, 4)
|
|
|
+ clone.BULLET_DAMAGE = DAMAGE;
|
|
|
+ ammo_in_weapon -= 1
|
|
|
+
|
|
|
+ player_node.create_sound("Pistol_shot", self.global_transform.origin)
|
|
|
+
|
|
|
+
|
|
|
+ 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("Pistol_reload")
|
|
|
+
|
|
|
+ player_node.create_sound("Gun_cock", player_node.camera.global_transform.origin)
|
|
|
+
|
|
|
+ return true;
|
|
|
+
|
|
|
+ return false;
|
|
|
+
|
|
|
+ func equip_weapon():
|
|
|
+ if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
|
|
|
+ is_weapon_enabled = true;
|
|
|
+ return true
|
|
|
+
|
|
|
+ if player_node.animation_manager.current_state == "Idle_unarmed":
|
|
|
+ player_node.animation_manager.set_animation("Pistol_equip")
|
|
|
+
|
|
|
+ player_node.create_sound("Gun_cock", player_node.camera.global_transform.origin)
|
|
|
+
|
|
|
+ return false
|
|
|
+
|
|
|
+ func unequip_weapon():
|
|
|
+
|
|
|
+ if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
|
|
|
+ if (player_node.animation_manager.current_state != "Pistol_unequip"):
|
|
|
+ player_node.animation_manager.set_animation("Pistol_unequip")
|
|
|
+
|
|
|
+ if player_node.animation_manager.current_state == "Idle_unarmed":
|
|
|
+ is_weapon_enabled = false;
|
|
|
+ return true
|
|
|
+ else:
|
|
|
+ return false
|
|
|
+
|
|
|
+
|
|
|
+Lets go over what is happening in this script:
|
|
|
+
|
|
|
+______
|
|
|
+
|
|
|
+First lets look at the constants and go over what each will do:
|
|
|
+
|
|
|
+* ``ammo_in_weapon``: How much ammo is *currently* in this weapon.
|
|
|
+* ``spare_ammo``: How much spare ammo we have in reserve for this weapon. ``spare_ammo + ammo_in_weapon = total ammo for this weapon``.
|
|
|
+* ``AMMO_IN_MAG``: The amount ammo needed to fill the weapon. To put it another way, the amount of ammo in each magazine.
|
|
|
+* ``DAMAGE``: The amount of damage a single bullet does.
|
|
|
+* ``CAN_RELOAD``: A boolean for tracking whether this weapon has the ability to reload.
|
|
|
+* ``RELOADING_ANIM_NAME``: The name of the reloading animation for this weapon.
|
|
|
+* ``IDLE_ANIM_NAME``: The name of the idle animation for this weapon.
|
|
|
+* ``FIRE_ANIM_NAME``: The name of the firing animation for this weapon.
|
|
|
+* ``is_weapon_enabled``: A boolean for tracking whether or not this weapon is the currently used/enabled weapon.
|
|
|
+* ``bullet_scene``: The bullet scene we created in part 2 of this tutorial.
|
|
|
+* ``player_node``: The player node and script (``Player.gd``).
|
|
|
+
|
|
|
+______
|
|
|
+
|
|
|
+Notice how we do not do anything in ``_ready``.
|
|
|
+
|
|
|
+We could try and grab the player node here, but it makes a messy ``get_node`` call, and because we already
|
|
|
+have to aim these points in ``Player.gd`` anyway, we will just pass the player node then.
|
|
|
+
|
|
|
+.. note:: This is just a design choice. Depending on your project, it may be better to use ``get_node`` in the
|
|
|
+ weapon scripts.
|
|
|
+
|
|
|
+______
|
|
|
+
|
|
|
+Lets look at ``fire_weapon``.
|
|
|
+
|
|
|
+First we make a clone of the bullet scene and add it as a child of the scene root.
|
|
|
+Next we set its global transform to ``self.global_transform``.
|
|
|
+
|
|
|
+.. note:: before we were using a ``get_node`` call to
|
|
|
+ get here because we were calling this from ``Player.gd``. Now that we are firing from the fire point itself, we do not
|
|
|
+ need to use ``get_node`` any more.
|
|
|
+
|
|
|
+Then we set its scale. As before, the bullet object is too small by default, so we scale it up so it's easier to see.
|
|
|
+
|
|
|
+Next we set its damage. This is new, but nothing crazy. To make this work, we just need to go into
|
|
|
+``Bullet_script.gd`` and change ``const BULLET_DAMAGE`` to ``var BULLET_DAMAGE``. The reason behind changing ``BULLET_DAMAGE`` from
|
|
|
+a constant to a normal variable is because we may reuse the bullet object later (for a different weapon)
|
|
|
+
|
|
|
+.. warning:: Do not forgot to change ``const BULLET_DAMAGE`` to ``var BULLET_DAMAGE`` in ``Bullet_script.gd``!
|
|
|
+
|
|
|
+Then we remove one from the ammo in our weapon and play a sound (if we have sounds).
|
|
|
+
|
|
|
+.. note:: With the exception of how we are no longer using ``get_node``, everything in ``fire_weapon`` is the same as the code
|
|
|
+ as ``Player.gd``'s ``fire_bullet`` function.
|
|
|
+
|
|
|
+______
|
|
|
+
|
|
|
+In ``reload_weapon`` we are doing things a little differently.
|
|
|
+
|
|
|
+First we define a variable to track whether or not we can reload. We then do a couple checks. The first check is checking whether
|
|
|
+or not we are in this weapon's idle animation. We do not want to reload while we are playing any other animation, so this check ensures
|
|
|
+that does not happen.
|
|
|
+
|
|
|
+The next thing we check is whether or not we have any ammo in reserve and/or if our weapon is full. We cannot reload with no spare ammo, and
|
|
|
+we do not want the player to be able to reload if the weapon is already full.
|
|
|
+
|
|
|
+.. tip:: In some games you can reload while full. Many times in these cases you lose whatever ammo was in the weapon when you reload.
|
|
|
+ For this tutorial though, we will only allow the player to reload if they do not have a full weapon.
|
|
|
+
|
|
|
+Then we check ``can_reload`` to see if it is true.
|
|
|
+
|
|
|
+If it is, we then calculate how much ammo we need to fill the weapon.
|
|
|
+
|
|
|
+If we have enough ammo in spares to fill the weapon, we remove the ammo we are taking from spares and set ``ammo_in_weapon`` to however much ammo is in a full weapon.
|
|
|
+
|
|
|
+If we do not have enough ammo in spares, we instead add all of the ammo left in spares and then set our spare ammo to zero.
|
|
|
+
|
|
|
+We then play the reloading animation and play a sound. We return ``true`` to signal we have successfully reloaded.
|
|
|
+
|
|
|
+If we cannot reload because ``reload_weapon`` is ``false``, we return ``false`` to signal we did not successfully reload.
|
|
|
+
|
|
|
+______
|
|
|
+
|
|
|
+For ``equip_weapon`` we first check if the player is in the pistol's idle state.
|
|
|
+
|
|
|
+If we are in the pistol's idle state we've successfully equipped the pistol.
|
|
|
+We set ``is_weapon_enabled`` to ``true`` because we are now using this weapon, and return ``true``.
|
|
|
+
|
|
|
+.. note:: We need ``is_weapon_enabled`` so we do not keep trying to equip/unequip the weapons over and over again. If we relied only on using
|
|
|
+ the ``equip_weapon``/``unequip_weapon`` functions, we could possibility get cases where we are stuck in a loop where we are equipping/unequipping
|
|
|
+ the same weapon over and over again.
|
|
|
+
|
|
|
+Next we check if we are in the idle unarmed state, a state where we can transition to our equip animation. If we are, then we change the animation
|
|
|
+to ``Pistol_equip`` and play a sound. Finally, we return ``false``.
|
|
|
+
|
|
|
+The reason behind returning ``false`` unless we are in our idle animation is because we will be calling this function more than once, checking to see if we
|
|
|
+have successfully equipped the pistol.
|
|
|
+
|
|
|
+______
|
|
|
+
|
|
|
+``unequip_weapon`` is extremely similar to ``equip_weapon``, but the checks are in reverse.
|
|
|
+
|
|
|
+We just check if we are in our idle state. If we are, and we are not already unequipping we set our animation to ``Pistol_unequip``.
|
|
|
+Then we check if we are in the idle animation. If we are, we set ``is_weapon_enabled`` to ``false`` because we are no longer using this weapon, and return ``true``.
|
|
|
+
|
|
|
+Finally, if we did not return ``true``, we return false.
|
|
|
+
|
|
|
+As with ``equip_weapon``, we want to return false by default because we will be calling this function until it returns true.
|
|
|
+
|
|
|
+______
|
|
|
+
|
|
|
+Now we just need to do the same thing for the knife and the rifle.
|
|
|
+
|
|
|
+There is only one minor difference with the knife and the rifle. We still define a reload function for the knife, but instead of doing
|
|
|
+anything we automatically return false.
|
|
|
+
|
|
|
+Select ``Knife_point``, created a new script called ``Weapon_Knife.gd``, and add the following:
|
|
|
+
|
|
|
+::
|
|
|
+
|
|
|
+ extends Spatial
|
|
|
+
|
|
|
+ var ammo_in_weapon = 1;
|
|
|
+ var spare_ammo = 1;
|
|
|
+ const AMMO_IN_MAG = 1;
|
|
|
+
|
|
|
+ const DAMAGE = 40;
|
|
|
+
|
|
|
+ const CAN_RELOAD = false;
|
|
|
+ const RELOADING_ANIM_NAME = ""
|
|
|
+ const IDLE_ANIM_NAME = "Knife_idle"
|
|
|
+ const FIRE_ANIM_NAME = "Knife_fire"
|
|
|
+
|
|
|
+ var is_weapon_enabled = false;
|
|
|
+
|
|
|
+ var player_node = null;
|
|
|
+
|
|
|
+ func _ready():
|
|
|
+ pass;
|
|
|
+
|
|
|
+ func fire_weapon():
|
|
|
+ var area = get_node("Area")
|
|
|
+ var bodies = area.get_overlapping_bodies()
|
|
|
+
|
|
|
+ for body in bodies:
|
|
|
+ if body.has_method("bullet_hit"):
|
|
|
+ body.bullet_hit(DAMAGE, area.global_transform.origin)
|
|
|
+
|
|
|
+
|
|
|
+ func reload_weapon():
|
|
|
+ return false;
|
|
|
+
|
|
|
+ func equip_weapon():
|
|
|
+ if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
|
|
|
+ is_weapon_enabled = true;
|
|
|
+ return true
|
|
|
+
|
|
|
+ if player_node.animation_manager.current_state == "Idle_unarmed":
|
|
|
+ player_node.animation_manager.set_animation("Knife_equip")
|
|
|
+
|
|
|
+ return false
|
|
|
+
|
|
|
+ func unequip_weapon():
|
|
|
+
|
|
|
+ if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
|
|
|
+ player_node.animation_manager.set_animation("Knife_unequip")
|
|
|
+
|
|
|
+ if player_node.animation_manager.current_state == "Idle_unarmed":
|
|
|
+ is_weapon_enabled = false;
|
|
|
+ return true
|
|
|
+
|
|
|
+ return false
|
|
|
+
|
|
|
+There are only a few things to note here.
|
|
|
+
|
|
|
+The first is we still are defining ``ammo_in_weapon``, ``spare_ammo`` and ``AMMO_IN_MAG``. The reason behind this is so our code has a consistent
|
|
|
+interface. We may later need to access these variables in all weapons, so we are adding them for the knife as a way assure all weapons have these variables.
|
|
|
+
|
|
|
+The second thing of note is in ``reload_weapon``. Because we cannot reload a knife (or at least, not this one), we just always return ``false``.
|
|
|
+
|
|
|
+The last thing to note is how ``fire_weapon``'s code is exactly the same as the code from ``Player.gd``. The firing code for all three weapons,
|
|
|
+the pistol, rifle, and knife, are exactly the same as the code in ``Player.gd``. The only differences is how we are accessing the spawn point nodes
|
|
|
+and their children.
|
|
|
+
|
|
|
+______
|
|
|
+
|
|
|
+Finally, select ``Rifle_point``, create a new script called ``Weapon_Rifle.gd``, and add the following code:
|
|
|
+
|
|
|
+::
|
|
|
+
|
|
|
+ extends Spatial
|
|
|
+
|
|
|
+ var ammo_in_weapon = 80;
|
|
|
+ var spare_ammo = 160;
|
|
|
+ const AMMO_IN_MAG = 80;
|
|
|
+ const DAMAGE = 4;
|
|
|
+
|
|
|
+ const CAN_RELOAD = true;
|
|
|
+ const RELOADING_ANIM_NAME = "Rifle_reload"
|
|
|
+ const IDLE_ANIM_NAME = "Rifle_idle"
|
|
|
+ const FIRE_ANIM_NAME = "Rifle_fire"
|
|
|
+
|
|
|
+ var is_weapon_enabled = false;
|
|
|
+
|
|
|
+ var player_node = null;
|
|
|
+
|
|
|
+ func _ready():
|
|
|
+ pass;
|
|
|
+
|
|
|
+ func fire_weapon():
|
|
|
+ var ray = get_node("RayCast")
|
|
|
+ ray.force_raycast_update()
|
|
|
+
|
|
|
+ if ray.is_colliding():
|
|
|
+ var body = ray.get_collider()
|
|
|
+ if body.has_method("bullet_hit"):
|
|
|
+ body.bullet_hit(DAMAGE, ray.get_collision_point())
|
|
|
+
|
|
|
+ ammo_in_weapon -= 1;
|
|
|
+
|
|
|
+ player_node.create_sound("Rifle_shot", ray.global_transform.origin)
|
|
|
+
|
|
|
+
|
|
|
+ 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("Rifle_reload")
|
|
|
+
|
|
|
+ player_node.create_sound("Gun_cock", player_node.camera.global_transform.origin)
|
|
|
+
|
|
|
+ return true;
|
|
|
+
|
|
|
+ return false;
|
|
|
+
|
|
|
+ func equip_weapon():
|
|
|
+ if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
|
|
|
+ is_weapon_enabled = true;
|
|
|
+ return true
|
|
|
+
|
|
|
+ if player_node.animation_manager.current_state == "Idle_unarmed":
|
|
|
+ player_node.animation_manager.set_animation("Rifle_equip")
|
|
|
+
|
|
|
+ player_node.create_sound("Gun_cock", player_node.camera.global_transform.origin)
|
|
|
+
|
|
|
+ return false
|
|
|
+
|
|
|
+ func unequip_weapon():
|
|
|
+
|
|
|
+ if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
|
|
|
+ if (player_node.animation_manager.current_state != "Rifle_unequip"):
|
|
|
+ player_node.animation_manager.set_animation("Rifle_unequip")
|
|
|
+
|
|
|
+ if player_node.animation_manager.current_state == "Idle_unarmed":
|
|
|
+ is_weapon_enabled = false;
|
|
|
+ return true
|
|
|
+
|
|
|
+ return false
|
|
|
+
|
|
|
+Thankfully the code for the rifle is exactly the same as the pistol, with ``fire_weapon`` changed to use the rifle's firing code. Other than that, everything is exactly the same,
|
|
|
+just adjusted for the rifle.
|
|
|
+
|
|
|
+Finishing refactoring ``Player.gd``
|
|
|
+-----------------------------------
|
|
|
+
|
|
|
+Now we are ready to use our newly refactored weapons in ``Player.gd``. First, we need to change some of the global variables.
|
|
|
+Find all of the constants relating to the weapons, delete them, and add the following:
|
|
|
+
|
|
|
+::
|
|
|
+
|
|
|
+ var current_weapon_name = "UNARMED"
|
|
|
+ var weapons = {"UNARMED":null, "KNIFE":null, "PISTOL":null, "RIFLE":null}
|
|
|
+ const weapon_number_to_name = {0:"UNARMED", 1:"KNIFE", 2:"PISTOL", 3:"RIFLE"}
|
|
|
+ const weapon_name_to_number = {"UNARMED":0, "KNIFE":1, "PISTOL":2, "RIFLE":3}
|
|
|
+ var changing_weapon = false
|
|
|
+ var changing_weapon_name = "UNARMED"
|
|
|
+ var reloading_weapon = false
|
|
|
+
|
|
|
+Lets go over each of these new global variables:
|
|
|
+
|
|
|
+* ``current_weapon_name``: The name of the weapon currently in use.
|
|
|
+* ``weapons``: A dictionary holding all of the weapon nodes, allowing us to access them by name instead of using ``get_node``.
|
|
|
+* ``weapon_number_to_name``: A dictionary holding all of the weapons and which number they represent.
|
|
|
+* ``weapon_name_to_number``: A dictionary holding all of the weapons numbers and which names they represent. Combined with ``weapon_number_to_name``, we can change from number to name and back.
|
|
|
+* ``changing_weapon``: A boolean to track whether we are trying to change weapons or not.
|
|
|
+* ``changing_weapon_name``: The name of the weapon we are trying to change to.
|
|
|
+* ``reloading_weapon``: A boolean to track whether we are reloading or not.
|
|
|
+
|
|
|
+We need to change ``_ready`` to the following:
|
|
|
+
|
|
|
+::
|
|
|
+
|
|
|
+ func _ready():
|
|
|
+ camera = get_node("Rotation_helper/Camera")
|
|
|
+ rotation_helper = get_node("Rotation_helper")
|
|
|
+
|
|
|
+ animation_manager = get_node("Rotation_helper/Model/AnimationPlayer")
|
|
|
+ animation_manager.callback_function = funcref(self, "fire_bullet")
|
|
|
+
|
|
|
+ set_physics_process(true)
|
|
|
+
|
|
|
+ Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
|
|
+ set_process_input(true)
|
|
|
+
|
|
|
+ weapons["KNIFE"] = get_node("Rotation_helper/Gun_fire_points/Knife_point")
|
|
|
+ weapons["PISTOL"] = get_node("Rotation_helper/Gun_fire_points/Pistol_point")
|
|
|
+ weapons["RIFLE"] = get_node("Rotation_helper/Gun_fire_points/Rifle_point")
|
|
|
+
|
|
|
+ var gun_aim_point_pos = get_node("Rotation_helper/Gun_aim_point").global_transform.origin
|
|
|
+
|
|
|
+ for weapon in weapons:
|
|
|
+ var weapon_node = weapons[weapon]
|
|
|
+ if weapon_node != null:
|
|
|
+ weapon_node.player_node = self
|
|
|
+ weapon_node.look_at(gun_aim_point_pos, Vector3(0, 1, 0))
|
|
|
+ weapon_node.rotate_object_local(Vector3(0, 1, 0), deg2rad(180))
|
|
|
+
|
|
|
+ current_weapon_name = "UNARMED"
|
|
|
+ changing_weapon_name = "UNARMED"
|
|
|
+
|
|
|
+ UI_status_label = get_node("HUD/Panel/Gun_label")
|
|
|
+ flashlight = get_node("Rotation_helper/Flashlight")
|
|
|
+
|
|
|
+Lets quickly go over the new stuff.
|
|
|
+
|
|
|
+Notice how most of the code is exactly the same as before. The only code that's changed is how
|
|
|
+we are handling the gun aim points, so let's look at those changes.
|
|
|
+
|
|
|
+First, we get all of the weapon nodes using ``get_node`` and assign them to the ``weapons`` dictionary.
|
|
|
+
|
|
|
+Then we loop through all of the weapons in the ``weapons`` dictionary. For each weapon node, we get the value assigned to that key.
|
|
|
+
|
|
|
+.. tip:: When we are using ``for X in Y`` where ``Y`` is a dictionary, ``X`` is assigned to the each **key** in the dictionary, not the value. To get the value, we
|
|
|
+ have to retrieve it using ``Y[X]``.
|
|
|
+
|
|
|
+If the weapon node is not ``null``, we set it's ``player_node`` variable to ``self``, and we make the point look at the gun aim position.
|
|
|
+
|
|
|
+.. note:: The reason we check for ``null`` is because our ``UNARMED`` weapon is ``null``. This is just a design choice, not a requirement for FPS games.
|
|
|
+ You could define a "weapon" for the UNARMED state, but in this series we are just going to use ``null``.
|
|
|
+
|
|
|
+Next we flip the aim point by ``180`` degrees so it doesn't fire backwards.
|
|
|
+
|
|
|
+.. warning:: The reason behind rotating the gun aim point is explained in :ref:`part 2 <doc_fps_tutorial_part_two>`
|
|
|
+
|
|
|
+Finally, we set ``current_weapon_name`` and ``changing_weapon_name`` to ``UNARMED`` so our starting weapon is ``UNARMED``.
|
|
|
+
|
|
|
+______
|
|
|
+
|
|
|
+Now we need to change ``_physics_process``. Delete everything in ``_physics_process`` and add the following:
|
|
|
+
|
|
|
+::
|
|
|
+
|
|
|
+ func _physics_process(delta):
|
|
|
+ process_input(delta)
|
|
|
+ #process_view_input(delta)
|
|
|
+ process_movement(delta)
|
|
|
+ process_changing_weapons(delta)
|
|
|
+ process_reloading(delta)
|
|
|
+ process_UI(delta)
|
|
|
+
|
|
|
+.. note:: You may have noticed how we have a commented out function, ``process_view_input``. We will be using this later!
|
|
|
+ For now just leave it commented out!
|
|
|
+
|
|
|
+Now we are calling each of our modular functions in order. Notice how we are still missing
|
|
|
+``process_changing_weapons``, ``process_reloading``, and ``process_UI``. Before we add those functions, lets quickly return to
|
|
|
+``process_input``.
|
|
|
+
|
|
|
+Finishing ``process_input``
|
|
|
+___________________________
|
|
|
+
|
|
|
+First, lets change ``process_input`` so our weapon related code works with the new weapon system.
|
|
|
+
|
|
|
+First, delete all of the weapon related code in `process_input`. This is the includes:
|
|
|
+Changing weapons, Reloading, and Firing.
|
|
|
+
|
|
|
+Now at the bottom of ``process_input``, add the following code:
|
|
|
+
|
|
|
+::
|
|
|
+
|
|
|
+ func process_input(delta):
|
|
|
+
|
|
|
+ # Other input code (like movement, jumping, etc) above!
|
|
|
+
|
|
|
+ # ----------------------------------
|
|
|
+ # Changing weapons.
|
|
|
+ var weapon_change_number = weapon_name_to_number[current_weapon_name]
|
|
|
+
|
|
|
+ if Input.is_key_pressed(KEY_1):
|
|
|
+ weapon_change_number = 0
|
|
|
+ if Input.is_key_pressed(KEY_2):
|
|
|
+ weapon_change_number = 1
|
|
|
+ if Input.is_key_pressed(KEY_3):
|
|
|
+ weapon_change_number = 2
|
|
|
+ if Input.is_key_pressed(KEY_4):
|
|
|
+ weapon_change_number = 3
|
|
|
+
|
|
|
+ if Input.is_action_just_pressed("shift_weapon_positive"):
|
|
|
+ weapon_change_number += 1
|
|
|
+ if Input.is_action_just_pressed("shift_weapon_negative"):
|
|
|
+ weapon_change_number -= 1
|
|
|
+
|
|
|
+ weapon_change_number = clamp(weapon_change_number, 0, weapon_number_to_name.size()-1)
|
|
|
+
|
|
|
+ if changing_weapon == false:
|
|
|
+ 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
|
|
|
+ # ----------------------------------
|
|
|
+
|
|
|
+ # ----------------------------------
|
|
|
+ # 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
|
|
|
+ # ----------------------------------
|
|
|
+
|
|
|
+ # ----------------------------------
|
|
|
+ # 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
|
|
|
+ # ----------------------------------
|
|
|
+
|
|
|
+Lets go through what each of these sections are doing.
|
|
|
+
|
|
|
+______
|
|
|
+
|
|
|
+Lets look at the weapon changing section first.
|
|
|
+
|
|
|
+The first thing we do is get the current weapon number and assign it to ``weapon_change_number``.
|
|
|
+
|
|
|
+Next we check each of the four number keys and we assign ``weapon_change_number`` to their value if they are pressed.
|
|
|
+
|
|
|
+.. note:: Most keyboards go in the order of ``1234567890``, so we when we set ``weapon_change_number``, we offset the value by ``-1`` so the first key (``1``)
|
|
|
+ is actually ``0``, which is our first weapon.
|
|
|
+
|
|
|
+Then we check if two new actions are pressed: ``shift_weapon_positive`` and ``shift_weapon_negative``. We will add these actions once we've finished
|
|
|
+going over ``process_input``.
|
|
|
+
|
|
|
+Next we clamp ``weapon_change_number`` so it cannot be higher or lower than the amount of weapons we have.
|
|
|
+
|
|
|
+.. tip:: We are making a small assumption here: We are assuming our weapons are defined in a linear pattern, where we do not have any jumps in number.
|
|
|
+
|
|
|
+ Another thing to note is we are getting the maximum value using ``weapon_to_number.size()-1``. We remove ``1`` because ``size`` returns the number
|
|
|
+ of elements in the dictionary, starting from ``1``, while GDScript accesses values starting from ``0``.
|
|
|
+
|
|
|
+We do not want to suddenly change weapons while already changing weapons or reload, so we check to make sure both variables are ``false``.
|
|
|
+
|
|
|
+Then we convert ``weapon_change_number`` to a weapon name using ``weapon_number_to_name`` and check to make sure we not trying to change to the weapon we
|
|
|
+are already using. If we are indeed changing weapons, we set ``changing_weapon_name`` to the name of the weapon at ``weapon_change_name`` using ``weapon_number_to_name``.
|
|
|
+Finally, we set ``changing_weapon`` to true so we can process the actual weapon changing logic in ``process_changing_weapons``.
|
|
|
+
|
|
|
+______
|
|
|
+
|
|
|
+For reloading we first check to make sure we are not already reload, or changing weapons.
|
|
|
+
|
|
|
+Then we check to see if the reloading action has been pressed.
|
|
|
+Next we get the current weapon and assign it to ``current_weapon``.
|
|
|
+If the current weapon is not ``null`` we then make sure this weapon can reload using the weapon's ``CAN_RELOAD`` constant.
|
|
|
+
|
|
|
+.. tip:: We check for ``null`` because we do not want to reload ``UNARMED``!
|
|
|
+
|
|
|
+Next we check get the current animation state from our animation manager, and we set ``is_reloading`` to ``false``.
|
|
|
+The reason we need ``is_reloading`` is because we need to go through each weapon and make sure we are not in it's reloading state already,
|
|
|
+because we do not want to allow the player to (potentially) reload if they are already in a reloading animation.
|
|
|
+
|
|
|
+We then go through each weapon in our ``weapons`` dictionary. We then get the weapon node, assign it to ``weapon_node`` and check to make sure it
|
|
|
+is not ``null``. If it is not ``null``, we then make sure it's ``RELOADING_ANIM_NAME`` constant to see if it is equal to the animation we are currently in. If it is,
|
|
|
+we set ``is_reloading`` to ``true``.
|
|
|
+
|
|
|
+If ``is_reloading`` is still ``false``, we then set ``reloading_weapon`` to true so we can process the reloading weapon logic in ``process_reloading``.
|
|
|
+
|
|
|
+______
|
|
|
+
|
|
|
+Finally, we have the firing section.
|
|
|
+
|
|
|
+The first thing we do is check to see if the ``fire`` action has been pressed. If it has, we then make sure we are not reloading or changing weapons.
|
|
|
+
|
|
|
+Next we get the current weapon and assign it to ``current_weapon``. We then check to make sure it is not equal to ``null``.
|
|
|
+
|
|
|
+If the current weapon is not equal to ``null``, we then make sure the weapon actually has ammo. If it does, we then check to see if we are in the weapon's idle state.
|
|
|
+If we are indeed in the weapon's idle state, we set our animation to the weapon's fire animation.
|
|
|
+
|
|
|
+If the current weapon does not have any ammo, we set ``reloading_weapon`` to true.
|
|
|
+
|
|
|
+Adding our new input map actions
|
|
|
+________________________________
|
|
|
+
|
|
|
+As mentioned above, we've defined a couple new input actions: ``shift_weapon_positive`` and ``shift_weapon_negative``.
|
|
|
+Currently these input actions do not exist in our project, so let's add them!
|
|
|
+
|
|
|
+.. image:: img/ProjectSettingsAddAction.png
|
|
|
+
|
|
|
+Open up your project settings and go to the ``Input Map`` tab. In the ``Action`` text field, type ``shift_weapon_positive`` and press enter or press the
|
|
|
+button on the side that reads ``Add``. Next write ``shift_weapon_negative`` and press enter or press the ``Add`` button.
|
|
|
+
|
|
|
+Scroll down to the bottom of the list and click the little plus sign next to one of the newly created actions.
|
|
|
+
|
|
|
+.. image:: img/ProjectSettingsAddKey.png
|
|
|
+
|
|
|
+You can assign whatever key you want to either
|
|
|
+of these actions. The finished project has the ``Equal`` and ``Kp Add`` keys assigned to ``shift_weapon_positive``. ``shift_weapon_negative`` has ``Minus`` and
|
|
|
+``Kp Subtract`` keys assigned in the finished project.
|
|
|
+
|
|
|
+Once you've assigned whatever keys you want to both actions, close the project settings and save.
|
|
|
+
|
|
|
+Adding ``process_changing_weapons``
|
|
|
+___________________________________
|
|
|
+
|
|
|
+Lets make the weapon changing logic next. Open up ``Player.gd`` and add the following function:
|
|
|
+
|
|
|
+::
|
|
|
+
|
|
|
+ func process_changing_weapons(delta):
|
|
|
+ if changing_weapon == true:
|
|
|
+
|
|
|
+ var weapon_unequipped = false
|
|
|
+ var current_weapon = weapons[current_weapon_name]
|
|
|
+
|
|
|
+ if current_weapon == null:
|
|
|
+ weapon_unequipped = true
|
|
|
+ else:
|
|
|
+ if current_weapon.is_weapon_enabled == true:
|
|
|
+ weapon_unequipped = current_weapon.unequip_weapon()
|
|
|
+ else:
|
|
|
+ weapon_unequipped = true
|
|
|
+
|
|
|
+ if weapon_unequipped == true:
|
|
|
+
|
|
|
+ var weapon_equiped = false
|
|
|
+ var weapon_to_equip = weapons[changing_weapon_name]
|
|
|
+
|
|
|
+ if weapon_to_equip == null:
|
|
|
+ weapon_equiped = true
|
|
|
+ else:
|
|
|
+ if weapon_to_equip.is_weapon_enabled == false:
|
|
|
+ weapon_equiped = weapon_to_equip.equip_weapon()
|
|
|
+ else:
|
|
|
+ weapon_equiped = true
|
|
|
+
|
|
|
+ if weapon_equiped == true:
|
|
|
+ changing_weapon = false
|
|
|
+ current_weapon_name = changing_weapon_name
|
|
|
+ changing_weapon_name = ""
|
|
|
+
|
|
|
+Lets go over what's happening here.
|
|
|
+
|
|
|
+First we check to make sure ``changing_weapon`` is ``true``.
|
|
|
+
|
|
|
+Next we make a new variable, ``weapon_unequipped``, and set it to ``false``. We will use ``weapon_unequipped`` to check whether or not the current weapon is unequipped.
|
|
|
+We then get the current weapon and assign it to ``current_weapon``.
|
|
|
+
|
|
|
+If the current weapon is ``null``, if we are ``UNARMED``, we can conclude the weapon has been successfully unequipped and set ``weapon_unequipped`` to ``true``.
|
|
|
+
|
|
|
+If the weapon is not ``null``, we check if the weapon is enabled. If the weapon is enabled, we call it's ``unequip_weapon`` function. If it is not enabled, we set ``weapon_unequipped`` to ``true``.
|
|
|
+
|
|
|
+Next we check if ``weapon_unequipped`` is ``true`` or not. Remember, ``weapon_unequipped`` will only be true if the current weapon's ``is_weapon_enabled`` variable is ``false`` (or the weapon
|
|
|
+is ``null``).
|
|
|
+
|
|
|
+If the current weapon is successfully unequipped, we then make a variable, ``weapon_equipped``. ``weapon_equipped`` will serve the same function as ``weapon_unequipped``, but instead of
|
|
|
+tracking if we've successfully unequipped the current weapon, we instead are tracking to see if the weapon we are wanting to change to has been successfully equipped.
|
|
|
+
|
|
|
+We then get the weapon we want to change to and assign it to ``weapon_to_equip``.
|
|
|
+
|
|
|
+Next we check to see if ``weapon_to_equip`` is ``null``. If it is, we set ``weapon_equipped`` to ``true`` because ``UNARMED`` does not need any additional processing.
|
|
|
+
|
|
|
+If ``weapon_to_equip`` is not null, we then check to see if the weapon is not enabled by checking it's ``is_weapon_enabled`` variable. If it is not enabled, we call ``equip_weapon``
|
|
|
+on the weapon we are wanting to equip.
|
|
|
+
|
|
|
+If the weapon we are wanting to equip is enabled, we set ``weapon_equipped`` to true.
|
|
|
+
|
|
|
+Finally, we check to see if ``weapon_equipped`` is ``true``. If it is, we set ``changing_weapon`` to ``false``, set ``current_weapon_name`` to the weapon we have changed to (``changing_weapon_name``),
|
|
|
+and we set ``changing_weapon_name`` to a empty string.
|
|
|
+
|
|
|
+Adding ``process_reloading``
|
|
|
+____________________________
|
|
|
+
|
|
|
+Let's finish up our new modular weapon system and add ``process_reloading``. Make a new funciton called ``process_reloading`` and add the following:
|
|
|
+
|
|
|
+::
|
|
|
+
|
|
|
+
|
|
|
+ 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 this function does.
|
|
|
+
|
|
|
+First we check to make sure we are wanting to reload. If we are, we then get the current weapon and assign it to ``current_weapon``.
|
|
|
+If ``current_weapon`` is not equal to ``null``, we call it's ``reload_weapon`` function.
|
|
|
+
|
|
|
+Finally, we set ``reloading_weapon`` to ``false`` because regardless of whether we've successfully reloaded, we have tried and no longer
|
|
|
+need to process weapon reloading.
|
|
|
+
|
|
|
+Changing ``fire_bullet``
|
|
|
+________________________
|
|
|
+
|
|
|
+Next we need to change ``fire_bullet`` because we are no longer actually firing the bullets in ``Player.gd``. Change ``fire_bullet`` to the following:
|
|
|
+
|
|
|
+::
|
|
|
+
|
|
|
+ func fire_bullet():
|
|
|
+ if changing_weapon == true:
|
|
|
+ return
|
|
|
+ weapons[current_weapon_name].fire_weapon()
|
|
|
+
|
|
|
+Now in ``fire_bullet`` we make sure we are not changing weapons, and if we are not we call the current weapon's ``fire_weapon`` function.
|
|
|
+
|
|
|
+
|
|
|
+Adding ``process_UI``
|
|
|
+_____________________
|
|
|
+
|
|
|
+
|
|
|
+Because we've changed how weapons work, we need to change how we update the UI.
|
|
|
+Make a new function called ``process_UI`` and add the following:
|
|
|
+
|
|
|
+::
|
|
|
+
|
|
|
+ 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)
|
|
|
+
|
|
|
+
|
|
|
+Nothing much has changed from the code that was in ``_physics_process``, we've mainly just moved the UI processing code to
|
|
|
+its own function.
|
|
|
+
|
|
|
+The only major change is how we get the amount counts in the current weapon.
|
|
|
+
|
|
|
+______
|
|
|
+
|
|
|
+Now we have successfully refactored ``Player.gd`` to use a more modular approach and the weapons now are (mainly) processed in their own scripts!
|
|
|
+Go give the game a test. If everything is written correctly you should be able to run around and shoot things just like before.
|
|
|
+
|
|
|
+Now that we've refactored ``Player.gd``, lets add something new: Let's allow our plays to play using a joypad!
|
|
|
+
|
|
|
+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.
|
|
|
+
|
|
|
+First 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 Right)``
|
|
|
+
|
|
|
+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 global variables. Add the following global variables to ``Player.gd``:
|
|
|
+
|
|
|
+::
|
|
|
+
|
|
|
+ # You may need to adjust depending on the sensitivity of your joypad
|
|
|
+ const JOYPAD_SENSITIVITY = 2
|
|
|
+ const JOYPAD_DEADZONE = 0.15
|
|
|
+
|
|
|
+Lets go over what each of these do:
|
|
|
+
|
|
|
+* ``JOYPAD_SENSITIVITY``: This is how fast our joypad joysticks will move our 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 in a
|
|
|
+ with a radius of JOYPAD_DEADZONE. If we did not ignore said movement, the camera will jitter.
|
|
|
+
|
|
|
+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()``:
|
|
|
+
|
|
|
+::
|
|
|
+
|
|
|
+ # Add joypad input, if there is a joypad
|
|
|
+ if Input.get_connected_joypads().size() > 0:
|
|
|
+ var joypad_vec = Vector2(Input.get_joy_axis(0, 0), -Input.get_joy_axis(0, 1))
|
|
|
+
|
|
|
+ if (abs(joypad_vec.x) <= JOYPAD_DEADZONE):
|
|
|
+ joypad_vec.x = 0
|
|
|
+ if (abs(joypad_vec.y) <= JOYPAD_DEADZONE):
|
|
|
+ joypad_vec.y = 0
|
|
|
+
|
|
|
+ input_movement_vector += joypad_vec
|
|
|
+
|
|
|
+Lets go over what we're doing.
|
|
|
+
|
|
|
+First we check to see if there is a connected joypad.
|
|
|
+
|
|
|
+If there is a joypad connected, we then get it's left stick axes for right/left and up/down.
|
|
|
+
|
|
|
+.. warning:: This tutorial assumes you are using a XBox 360 wired controller
|
|
|
+ on Windows. The axes needed may be different on different operating systems and/or controllers.
|
|
|
+
|
|
|
+Next we check to see if the joypad vector is within the ``JOYPAD_DEADZONE`` radius. If the ``x`` or ``y`` coordinates
|
|
|
+are within the ``JOYPAD_DEADZONE`` radius, we set it to zero.
|
|
|
+
|
|
|
+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`` players could
|
|
|
+ move faster if they are pushing in the same direction with both their keyboard and their joypad!
|
|
|
+
|
|
|
+______
|
|
|
+
|
|
|
+Remember that commented out function in ``_physics_process``? Lets add it! Remove the ``#`` in ``_physics_process`` and make a new function called ``process_view_input``.
|
|
|
+Add the following to ``process_view_input``:
|
|
|
+
|
|
|
+::
|
|
|
+
|
|
|
+ func process_view_input(delta):
|
|
|
+
|
|
|
+ if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
|
|
|
+ return
|
|
|
+
|
|
|
+ # ----------------------------------
|
|
|
+ # Joypad rotation
|
|
|
+
|
|
|
+ var joypad_vec = Vector2()
|
|
|
+ if Input.get_connected_joypads().size() > 0:
|
|
|
+
|
|
|
+ # For windows (XBOX 360)
|
|
|
+ joypad_vec = Vector2(Input.get_joy_axis(0, 2), Input.get_joy_axis(0, 3))
|
|
|
+ # For Linux (XBOX 360)
|
|
|
+ #joypad_vec = Vector2(Input.get_joy_axis(0, 3), Input.get_joy_axis(0, 4))
|
|
|
+ # For Mac (XBOX 360) Unknown, but likely:
|
|
|
+ #joypad_vec = Vector2(Input.get_joy_axis(0, 3), Input.get_joy_axis(0, 4))
|
|
|
+
|
|
|
+ if abs(joypad_vec.x) <= JOYPAD_DEADZONE:
|
|
|
+ joypad_vec.x = 0
|
|
|
+ if abs(joypad_vec.y) <= JOYPAD_DEADZONE:
|
|
|
+ joypad_vec.y = 0
|
|
|
+
|
|
|
+ rotation_helper.rotate_x(deg2rad(joypad_vec.y * JOYPAD_SENSITIVITY))
|
|
|
+ self.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:
|
|
|
+
|
|
|
+First we check the mouse mode. If the mouse mode is not ``MOUSE_MODE_CAPTURED``, we want to return, which will skip the code below.
|
|
|
+
|
|
|
+.. note:: The reason we are checking to see if the mouse mode is captured or not is because we may want to add a pause menu later. If we do,
|
|
|
+ we do not want players to move around while the game is paused if they are using a joypad!
|
|
|
+
|
|
|
+Next we define a new :ref:`Vector2 <class_Vector2>` called ``joypad_vec``. This will hold the right joystick position if there is one, and if there is not one it will
|
|
|
+default to ``(0, 0)``, which will do nothing.
|
|
|
+
|
|
|
+We then check to see if we have a joypad connected. If we do, we then assign ``joypad_vec`` to the proper axes values.
|
|
|
+
|
|
|
+.. warning:: Depending on our OS, you may need to change the axis order. The axis values proved are confirmed to work
|
|
|
+ on Linux and Windows 10 using a XBox 360 wired controller.
|
|
|
+
|
|
|
+We then account for the joypad's dead zone, just like in ``process_input``.
|
|
|
+
|
|
|
+Regardless of whehter or not there is a joypad connected, we rotate ``rotation_helper`` and ourselves using ``joypad_vec``. If we do not have a joypad connected,
|
|
|
+``joypad_vec`` will be equal to zero, which will do nothing.
|
|
|
+
|
|
|
+Notice how the code that handles rotating ourselves 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 few mouse related bugs on Windows, we cannot put mouse rotation in ``process_view`` as well. The tutorial will be updated once the bugs are fixed!
|
|
|
+
|
|
|
+Finally, we clamp the camera's rotation so we cannot look upside down.
|
|
|
+
|
|
|
+______
|
|
|
+
|
|
|
+If everything is setup correctly, you can now play around 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 button 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 proper axis value for the trigger,
|
|
|
+ and check if it's over a certain value, say ``0.8`` for example. If it is, you just add the same code as when the ``fire`` action was pressed.
|
|
|
+
|
|
|
+Adding mouse scroll wheel input
|
|
|
+-------------------------------
|
|
|
+
|
|
|
+Let's add one more feature before we close this part off. Let's add the ability to change weapons using the scroll wheel on the mouse.
|
|
|
+
|
|
|
+Open up ``Player.gd`` and add the following global variables:
|
|
|
+
|
|
|
+::
|
|
|
+
|
|
|
+ var mouse_scroll_value = 0
|
|
|
+ const MOUSE_SENSITIVITY_SCROLL_WHEEL = 0.08
|
|
|
+
|
|
|
+Lets go over what each of these new varibles 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 lets add the following to ``_input``:
|
|
|
+
|
|
|
+::
|
|
|
+
|
|
|
+ if event is InputEventMouseButton && 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:
|
|
|
+
|
|
|
+First we check if the event is a ``InputEventMouseButton`` event and that our 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/remove ``MOUSE_SENSITIVITY_SCROLL_WHEEL`` to/from ``mouse_scroll_value``.
|
|
|
+
|
|
|
+Next we clamp mouse scroll value to assure it is inside the range of our weapons.
|
|
|
+
|
|
|
+We then check to see if we are changing weapons or reloading. If we are doing neither, we round ``mouse_scroll_value`` and cast it to a ``int``.
|
|
|
+
|
|
|
+.. note:: We are casting ``mouse_scroll_value`` to a ``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 try 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 than our current weapon, we assign ``changing_weapon_name``, set ``changing_weapon`` to true so we 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 assure that each weapon takes exactly the same amount of scrolling to change.
|
|
|
+
|
|
|
+______
|
|
|
+
|
|
|
+Now you can change weapons using the scroll wheel! Go give it a whirl!
|
|
|
+
|
|
|
+Final notes
|
|
|
+-----------
|
|
|
+
|
|
|
+Now ``Player.gd`` is laid out much better, is easier to extend, we've added joypad input, and now the player can change weapons with the scroll wheel!
|
|
|
+
|
|
|
+.. tip:: You can find the finished project for part 4 here: https://github.com/TwistedTwigleg/Godot_FPS_Tutorial/tree/part_4
|
|
|
+
|
|
|
+ The completed project has helpful comments every step of the way for almost every line of code!
|
|
|
+
|
|
|
+ (Remember, you can download the completed project as a ZIP file if you want)
|
|
|
+
|
|
|
+ .. image:: img/GithubDownloadZip.png
|
|
|
+
|
|
|
+If you want to see what is coming next, and what could be coming in the future, check out this issue on the repository: https://github.com/TwistedTwigleg/Godot_FPS_Tutorial/issues/6
|
|
|
+
|
|
|
+
|
|
|
+How to make ``Test_Level.tscn`` look cool!
|
|
|
+__________________________________________
|
|
|
+
|
|
|
+One quick thing! As noted by **MagicLord** from the Godot forums, you can make ``Test_Level.tscn`` look really cool with a little tweaking!
|
|
|
+
|
|
|
+If you change the roughness values down in the Spatial materials for the provided starter assets, you get this:
|
|
|
+
|
|
|
+.. image:: img/PartFourFinished.png
|
|
|
+
|
|
|
+.. note:: Huge thanks to **MagicLord** for sharing! (Credit for the picture goes to **MagicLord** as well!)
|
|
|
+
|
|
|
+All you have to do is lower the roughness (I found a value of ``0.1`` looks nice) in ``LevelAssets_SpatialMaterial.tres`` and ``LevelAssets_Transparent_SpatialMaterial.tres``,
|
|
|
+which you can find at ``assets/Level_assets``.
|
|
|
+
|
|
|
+.. note:: Remember, you have to hit the save button or your changes to ``LevelAssets_SpatialMaterial.tres`` and/or ``LevelAssets_Transparent_SpatialMaterial.tres``
|
|
|
+ will not be saved! The save icon looks like a little floppy disk!
|
|
|
+
|
|
|
+You can also turn on SSR (Screen Space Reflections) and/or use :ref:`reflection probes <class_ReflectionProbe>`
|
|
|
+as well! Turning up the metallic value a little can also give a more realistic look.
|
|
|
+
|
|
|
+In a later part we will likely change ``Test_Level.tscn`` a bit so the sky texture does not leak through the tiles before setting
|
|
|
+the material roughness down in the finished project.
|
|
|
+
|
|
|
+
|