1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300 |
- .. _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 function 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.
|