|
@@ -6,1295 +6,731 @@ Part 4
|
|
Part Overview
|
|
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.
|
|
|
|
|
|
+In this part we will be adding health pick ups, ammo pick ups, targets we can destroy, 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.
|
|
|
|
|
|
+.. image:: img/PartFourFinished.png
|
|
|
|
|
|
-.. 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.
|
|
|
|
|
|
+.. note:: You are assumed to have finished :ref:`doc_fps_tutorial_part_three` before moving on to this part of the tutorial.
|
|
|
|
+
|
|
|
|
+ The finished project from :ref:`doc_fps_tutorial_part_three` will be the starting project for part 4
|
|
|
|
|
|
Let's get started!
|
|
Let's get started!
|
|
|
|
|
|
|
|
+Adding joypad input
|
|
|
|
+-------------------
|
|
|
|
|
|
-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``.
|
|
|
|
|
|
+.. 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.
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+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.
|
|
|
|
|
|
-.. 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!
|
|
|
|
|
|
+Now we need to add some joypad buttons to our various actions. Click the plus icon and select ``Joy Button``.
|
|
|
|
|
|
|
|
+.. image:: img/ProjectSettingsAddKey.png
|
|
|
|
|
|
-Breaking it down
|
|
|
|
-----------------
|
|
|
|
|
|
+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:
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+* 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)``
|
|
|
|
+* fire_grenade: ``Device 0, Button 1 (PS Circle, XBox B, Nintendo A).``
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+.. note:: These are already set up for you if you downloaded the starter assets
|
|
|
|
|
|
-Another benefit of using smaller functions is they are generally easier to debug!
|
|
|
|
|
|
+Once you are happy with the input, close the project settings and save.
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+Now let's open up ``Player.gd`` and add joypad input.
|
|
|
|
|
|
-Create new function called `process_input` and add the following code:
|
|
|
|
|
|
+First, we need to define a few new global variables. Add the following global variables to ``Player.gd``:
|
|
|
|
|
|
::
|
|
::
|
|
|
|
|
|
- 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!
|
|
|
|
|
|
+ # You may need to adjust depending on the sensitivity of your joypad
|
|
|
|
+ var JOYPAD_SENSITIVITY = 2
|
|
|
|
+ const JOYPAD_DEADZONE = 0.15
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+Let's go over what each of these do:
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+* ``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.
|
|
|
|
|
|
-Breaking down ``KinematicBody`` movement
|
|
|
|
-________________________________________
|
|
|
|
|
|
+.. 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.
|
|
|
|
+
|
|
|
|
+ Also, we are defining ``JOYPAD_SENSITIVITY`` as a variable instead of a constant because we'll later be changing it.
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+Now we are ready to start handling joypad input!
|
|
|
|
|
|
-Create a new function and call it ``process_movement``. Lets add the following code:
|
|
|
|
|
|
+______
|
|
|
|
+
|
|
|
|
+In ``process_input`` add the following code, just before ``input_movement_vector = input_movement_vector.normalized()``:
|
|
|
|
|
|
::
|
|
::
|
|
|
|
|
|
- func process_movement(delta):
|
|
|
|
- var grav = norm_grav
|
|
|
|
-
|
|
|
|
- dir.y = 0
|
|
|
|
- dir = dir.normalized()
|
|
|
|
-
|
|
|
|
- vel.y += delta*grav
|
|
|
|
-
|
|
|
|
- var hvel = vel
|
|
|
|
- hvel.y = 0
|
|
|
|
|
|
+ # Add joypad input, if there is a joypad
|
|
|
|
+ if Input.get_connected_joypads().size() > 0:
|
|
|
|
|
|
- var target = dir
|
|
|
|
- if is_spriting:
|
|
|
|
- target *= MAX_SPRINT_SPEED
|
|
|
|
- else:
|
|
|
|
- target *= MAX_SPEED
|
|
|
|
|
|
+ var joypad_vec = Vector2(0, 0)
|
|
|
|
|
|
- var accel
|
|
|
|
- if dir.dot(hvel) > 0:
|
|
|
|
- if is_spriting:
|
|
|
|
- accel = SPRINT_ACCEL
|
|
|
|
- else:
|
|
|
|
- accel = ACCEL
|
|
|
|
|
|
+ if OS.get_name() == "Windows":
|
|
|
|
+ joypad_vec = Vector2(Input.get_joy_axis(0, 0), -Input.get_joy_axis(0, 1))
|
|
|
|
+ elif OS.get_name() == "X11":
|
|
|
|
+ joypad_vec = Vector2(Input.get_joy_axis(0, 1), Input.get_joy_axis(0, 2))
|
|
|
|
+ elif OS.get_name() == "OSX":
|
|
|
|
+ joypad_vec = Vector2(Input.get_joy_axis(0, 1), Input.get_joy_axis(0, 2))
|
|
|
|
+
|
|
|
|
+ if joypad_vec.length() < JOYPAD_DEADZONE:
|
|
|
|
+ joypad_vec = Vector2(0, 0)
|
|
else:
|
|
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))
|
|
|
|
|
|
+ joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))
|
|
|
|
|
|
-Thankfully nothing is has changed here, all we've done is moved the code out of ``_physics_process``.
|
|
|
|
|
|
+ input_movement_vector += joypad_vec
|
|
|
|
|
|
-.. 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))``.
|
|
|
|
|
|
+Let's go over what we're doing.
|
|
|
|
|
|
-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``.
|
|
|
|
|
|
+First we check to see if there is a connected joypad.
|
|
|
|
|
|
|
|
+If there is a joypad connected, we then get its left stick axes for right/left and up/down.
|
|
|
|
+Because a wired Xbox 360 controller has different joystick axis mapping based on OS, we use different axes based on
|
|
|
|
+the OS.
|
|
|
|
|
|
-Changing the weapon code structure
|
|
|
|
-----------------------------------
|
|
|
|
|
|
+.. warning:: This tutorial assumes you are using a XBox 360 wired controller.
|
|
|
|
+ Also, I do not (currently) have access to a Mac computer, so the joystick axes may need changing.
|
|
|
|
+ If they do, please open a GitHub issue on the Godot documentation repository!
|
|
|
|
|
|
-So far, we have not really changed the structure of the code, we've just shuffled it around, so lets change that.
|
|
|
|
|
|
+Next we check to see if the joypad vector length is within the ``JOYPAD_DEADZONE`` radius.
|
|
|
|
+If it is, we set ``joypad_vec`` to an empty Vector2. If it is not, we use a scaled Radial Dead zone for precise dead zone calculating.
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+.. note:: You can find a great article explaining all about how to handle joypad/controller dead zones here:
|
|
|
|
+ https://www.third-helix.com/2013/04/12/doing-thumbstick-dead-zones-right.html
|
|
|
|
+
|
|
|
|
+ We're using a translated version of the scaled radial dead zone code provided in that article.
|
|
|
|
+ The article is a great read, and I highly suggest giving it a look!
|
|
|
|
|
|
-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``.
|
|
|
|
|
|
+Finally, we add ``joypad_vec`` to ``input_movement_vector``.
|
|
|
|
|
|
-Our weapon scripts are going to do four things: They're going to handle *firing*, *reloading*, *equipping*, and *unequipping*.
|
|
|
|
|
|
+.. 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!
|
|
|
|
+
|
|
|
|
+______
|
|
|
|
|
|
-Add the following code to ``Weapon_Pistol.gd``:
|
|
|
|
|
|
+Make a new function called ``process_view_input`` and add the following:
|
|
|
|
|
|
::
|
|
::
|
|
|
|
|
|
- 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")
|
|
|
|
|
|
+ func process_view_input(delta):
|
|
|
|
|
|
- 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:
|
|
|
|
|
|
+ if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
|
|
|
|
+ return
|
|
|
|
|
|
-______
|
|
|
|
|
|
+ # NOTE: Until some bugs relating to captured mouses are fixed, we cannot put the mouse view
|
|
|
|
+ # rotation code here. Once the bug(s) are fixed, code for mouse view rotation code will go here!
|
|
|
|
|
|
-First lets look at the constants and go over what each will do:
|
|
|
|
|
|
+ # ----------------------------------
|
|
|
|
+ # Joypad rotation
|
|
|
|
|
|
-* ``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``).
|
|
|
|
|
|
+ var joypad_vec = Vector2()
|
|
|
|
+ if Input.get_connected_joypads().size() > 0:
|
|
|
|
|
|
-______
|
|
|
|
|
|
+ if OS.get_name() == "Windows":
|
|
|
|
+ joypad_vec = Vector2(Input.get_joy_axis(0, 2), Input.get_joy_axis(0, 3))
|
|
|
|
+ elif OS.get_name() == "X11":
|
|
|
|
+ joypad_vec = Vector2(Input.get_joy_axis(0, 3), Input.get_joy_axis(0, 4))
|
|
|
|
+ elif OS.get_name() == "OSX":
|
|
|
|
+ joypad_vec = Vector2(Input.get_joy_axis(0, 3), Input.get_joy_axis(0, 4))
|
|
|
|
|
|
-Notice how we do not do anything in ``_ready``.
|
|
|
|
|
|
+ if joypad_vec.length() < JOYPAD_DEADZONE:
|
|
|
|
+ joypad_vec = Vector2(0, 0)
|
|
|
|
+ else:
|
|
|
|
+ joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+ rotation_helper.rotate_x(deg2rad(joypad_vec.y * JOYPAD_SENSITIVITY))
|
|
|
|
|
|
-.. note:: This is just a design choice. Depending on your project, it may be better to use ``get_node`` in the
|
|
|
|
- weapon scripts.
|
|
|
|
|
|
+ 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:
|
|
|
|
|
|
-Lets look at ``fire_weapon``.
|
|
|
|
|
|
+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.
|
|
|
|
|
|
-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``.
|
|
|
|
|
|
+Next we define a new :ref:`Vector2 <class_Vector2>` called ``joypad_vec``. This will hold the right joystick position. Based on the OS, we set its values so
|
|
|
|
+it is mapped to the proper axes for the right joystick.
|
|
|
|
|
|
-.. 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.
|
|
|
|
|
|
+.. warning:: As stated above, I do not (currently) has access to a Mac computer, so the joystick axes may need changing. If they do,
|
|
|
|
+ please open a GitHub issue on the Godot documentation repository!
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+We then account for the joypad's dead zone, just like in ``process_input``.
|
|
|
|
|
|
-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)
|
|
|
|
|
|
+Then we rotate ``rotation_helper`` and our KinematicBody using ``joypad_vec``.
|
|
|
|
|
|
-.. warning:: Do not forgot to change ``const BULLET_DAMAGE`` to ``var BULLET_DAMAGE`` in ``Bullet_script.gd``!
|
|
|
|
|
|
+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``.
|
|
|
|
|
|
-Then we remove one from the ammo in our weapon and play a sound (if we have sounds).
|
|
|
|
|
|
+.. note:: Due to few mouse related bugs on Windows, we cannot put mouse rotation in ``process_view`` as well.
|
|
|
|
+ Once these bugs are fixed, this will likely be updated to place the mouse rotation here as well.
|
|
|
|
|
|
-.. 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.
|
|
|
|
|
|
+Finally, we clamp the camera's rotation so we cannot look upside down.
|
|
|
|
|
|
______
|
|
______
|
|
|
|
|
|
-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 last thing you need to do is add ``process_view_input`` to ``_physics_process``.
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+Once ``process_view_input`` is added to ``_physics_process``, you should be able to play using a joypad!
|
|
|
|
|
|
-.. 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.
|
|
|
|
|
|
+.. 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
|
|
|
|
+-------------------------------
|
|
|
|
|
|
-If it is, we then calculate how much ammo we need to fill the weapon.
|
|
|
|
|
|
+Let's add one input related feature before we start working on the pick ups and target. Let's add the ability to change weapons using the scroll wheel on the mouse.
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+Open up ``Player.gd`` and add the following global variables:
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+::
|
|
|
|
+
|
|
|
|
+ var mouse_scroll_value = 0
|
|
|
|
+ const MOUSE_SENSITIVITY_SCROLL_WHEEL = 0.08
|
|
|
|
|
|
-We then play the reloading animation and play a sound. We return ``true`` to signal we have successfully reloaded.
|
|
|
|
|
|
+Let's go over what each of these new variables will be doing:
|
|
|
|
|
|
-If we cannot reload because ``reload_weapon`` is ``false``, we return ``false`` to signal we did not successfully reload.
|
|
|
|
|
|
+* ``mouse_scroll_value``: The value of the mouse scroll wheel.
|
|
|
|
+* ``MOUSE_SENSITIVITY_SCROLL_WHEEL``: How much a single scroll action increases mouse_scroll_value
|
|
|
|
|
|
______
|
|
______
|
|
|
|
|
|
-For ``equip_weapon`` we first check if the player is in the pistol's idle state.
|
|
|
|
|
|
+Now let's add the following to ``_input``:
|
|
|
|
|
|
-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``.
|
|
|
|
|
|
+::
|
|
|
|
+
|
|
|
|
+ if event is InputEventMouseButton and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
|
|
|
|
+ if event.button_index == BUTTON_WHEEL_UP or event.button_index == BUTTON_WHEEL_DOWN:
|
|
|
|
+ if event.button_index == BUTTON_WHEEL_UP:
|
|
|
|
+ mouse_scroll_value += MOUSE_SENSITIVITY_SCROLL_WHEEL
|
|
|
|
+ elif event.button_index == BUTTON_WHEEL_DOWN:
|
|
|
|
+ mouse_scroll_value -= MOUSE_SENSITIVITY_SCROLL_WHEEL
|
|
|
|
+
|
|
|
|
+ mouse_scroll_value = clamp(mouse_scroll_value, 0, WEAPON_NUMBER_TO_NAME.size()-1)
|
|
|
|
+
|
|
|
|
+ if changing_weapon == false:
|
|
|
|
+ if reloading_weapon == false:
|
|
|
|
+ var round_mouse_scroll_value = int(round(mouse_scroll_value))
|
|
|
|
+ if WEAPON_NUMBER_TO_NAME[round_mouse_scroll_value] != current_weapon_name:
|
|
|
|
+ changing_weapon_name = WEAPON_NUMBER_TO_NAME[round_mouse_scroll_value]
|
|
|
|
+ changing_weapon = true
|
|
|
|
+ mouse_scroll_value = round_mouse_scroll_value
|
|
|
|
|
|
-.. 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.
|
|
|
|
|
|
+
|
|
|
|
+Let's go over what's happening here:
|
|
|
|
|
|
-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``.
|
|
|
|
|
|
+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.
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+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.
|
|
|
|
|
|
-``unequip_weapon`` is extremely similar to ``equip_weapon``, but the checks are in reverse.
|
|
|
|
|
|
+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``.
|
|
|
|
|
|
-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``.
|
|
|
|
|
|
+.. 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.
|
|
|
|
|
|
-Finally, if we did not return ``true``, we return false.
|
|
|
|
|
|
+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``.
|
|
|
|
|
|
-As with ``equip_weapon``, we want to return false by default because we will be calling this function until it returns true.
|
|
|
|
|
|
+.. 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 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:
|
|
|
|
|
|
+One more thing we need to change is in ``process_input``. In the code for changing weapons, add the following right after the line ``changing_weapon = true``:
|
|
|
|
|
|
::
|
|
::
|
|
|
|
|
|
- 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)
|
|
|
|
|
|
+ mouse_scroll_value = weapon_change_number
|
|
|
|
+
|
|
|
|
+Now our scroll value we be changed with the keyboard input. If we did not change this, our scroll value will be out of sync. If the scroll wheel is out of
|
|
|
|
+sync, scrolling forwards or backwards would not transition to the next/last weapon, but rather the next/last weapon the scroll wheel changed to.
|
|
|
|
|
|
|
|
+______
|
|
|
|
|
|
- func reload_weapon():
|
|
|
|
- return false;
|
|
|
|
|
|
+Now you can change weapons using the scroll wheel! Go give it a whirl!
|
|
|
|
|
|
- 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
|
|
|
|
|
|
+Adding the health pick ups
|
|
|
|
+--------------------------
|
|
|
|
|
|
- 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
|
|
|
|
|
|
+Now that our player has health and ammo, we ideally need a way to replenish those resources.
|
|
|
|
|
|
-There are only a few things to note here.
|
|
|
|
|
|
+Open up ``Health_Pickup.tscn``.
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+Expand ``Holder`` if it's not already expanded. Notice how we have two Spatial nodes, one called ``Health_Kit`` and another called ``Health_Kit_Small``.
|
|
|
|
|
|
-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``.
|
|
|
|
|
|
+This is because we're actually going to be making two sizes of health pick ups, one small and one large/normal. ``Health_Kit`` and ``Health_Kit_Small`` just
|
|
|
|
+have a single :ref:`MeshInstance <class_MeshInstance>` as their children.
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+Next expand ``Health_Pickup_Trigger``. This is an :ref:`Area <class_Area>` node we're going to use to check if the player has walked close enough to pick up
|
|
|
|
+the health kit. If you expand it you'll find two collision shapes, one for each size. We will be using a different collision shape size based on the size of the
|
|
|
|
+health pick up, so the smaller health pick up has a trigger collision shape closer to it's size.
|
|
|
|
|
|
-______
|
|
|
|
|
|
+The last thing to note is how we have a :ref:`AnimationPlayer <class_AnimationPlayer>` node so the health kit spins around slowly and bobs up and down.
|
|
|
|
|
|
-Finally, select ``Rifle_point``, create a new script called ``Weapon_Rifle.gd``, and add the following code:
|
|
|
|
|
|
+Select ``Health_Pickup`` and add a new script called ``Health_Pickup.gd``. Add the following:
|
|
|
|
|
|
::
|
|
::
|
|
|
|
|
|
extends Spatial
|
|
extends Spatial
|
|
|
|
|
|
- var ammo_in_weapon = 80;
|
|
|
|
- var spare_ammo = 160;
|
|
|
|
- const AMMO_IN_MAG = 80;
|
|
|
|
- const DAMAGE = 4;
|
|
|
|
|
|
+ export (int, "full size", "small") var kit_size = 0 setget kit_size_change
|
|
|
|
|
|
- const CAN_RELOAD = true;
|
|
|
|
- const RELOADING_ANIM_NAME = "Rifle_reload"
|
|
|
|
- const IDLE_ANIM_NAME = "Rifle_idle"
|
|
|
|
- const FIRE_ANIM_NAME = "Rifle_fire"
|
|
|
|
|
|
+ # 0 = full size pickup, 1 = small pickup
|
|
|
|
+ const HEALTH_AMOUNTS = [70, 30]
|
|
|
|
|
|
- var is_weapon_enabled = false;
|
|
|
|
|
|
+ const RESPAWN_TIME = 20
|
|
|
|
+ var respawn_timer = 0
|
|
|
|
|
|
- var player_node = null;
|
|
|
|
|
|
+ var is_ready = false
|
|
|
|
|
|
func _ready():
|
|
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())
|
|
|
|
|
|
+ $Holder/Health_Pickup_Trigger.connect("body_entered", self, "trigger_body_entered")
|
|
|
|
|
|
- ammo_in_weapon -= 1;
|
|
|
|
|
|
+ is_ready = true
|
|
|
|
|
|
- player_node.create_sound("Rifle_shot", ray.global_transform.origin)
|
|
|
|
|
|
+ kit_size_change_values(0, false)
|
|
|
|
+ kit_size_change_values(1, false)
|
|
|
|
+ kit_size_change_values(kit_size, true)
|
|
|
|
|
|
|
|
|
|
- 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")
|
|
|
|
|
|
+ func _physics_process(delta):
|
|
|
|
+ if respawn_timer > 0:
|
|
|
|
+ respawn_timer -= delta
|
|
|
|
|
|
- player_node.create_sound("Gun_cock", player_node.camera.global_transform.origin)
|
|
|
|
-
|
|
|
|
- return false
|
|
|
|
|
|
+ if respawn_timer <= 0:
|
|
|
|
+ kit_size_change_values(kit_size, true)
|
|
|
|
|
|
- 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.
|
|
|
|
|
|
+ func kit_size_change(value):
|
|
|
|
+ if is_ready:
|
|
|
|
+ kit_size_change_values(kit_size, false)
|
|
|
|
+ kit_size = value
|
|
|
|
+ kit_size_change_values(kit_size, true)
|
|
|
|
+ else:
|
|
|
|
+ kit_size = value
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+ func kit_size_change_values(size, enable):
|
|
|
|
+ if size == 0:
|
|
|
|
+ $Holder/Health_Pickup_Trigger/Shape_Kit.disabled = !enable
|
|
|
|
+ $Holder/Health_Kit.visible = enable
|
|
|
|
+ elif size == 1:
|
|
|
|
+ $Holder/Health_Pickup_Trigger/Shape_Kit_Small.disabled = !enable
|
|
|
|
+ $Holder/Health_Kit_Small.visible = enable
|
|
|
|
|
|
-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]``.
|
|
|
|
|
|
+ func trigger_body_entered(body):
|
|
|
|
+ if body.has_method("add_health"):
|
|
|
|
+ body.add_health(HEALTH_AMOUNTS[kit_size])
|
|
|
|
+ respawn_timer = RESPAWN_TIME
|
|
|
|
+ kit_size_change_values(kit_size, false)
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+Let's go over what this script is doing, starting with its global variables:
|
|
|
|
|
|
-.. 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``.
|
|
|
|
|
|
+* ``kit_size``: The size of the health pick up. Notice how we're using a ``setget`` function to tell if it's changed.
|
|
|
|
+* ``HEALTH_AMMOUNTS``: The amount of health each pick up in each size contains.
|
|
|
|
+* ``RESPAWN_TIME``: The amount of time, in seconds, it takes for the health pick up to respawn
|
|
|
|
+* ``respawn_timer``: A variable used to track how long the health pick up has been waiting to respawn.
|
|
|
|
+* ``is_ready``: A variable to track whether the ``_ready`` function has been called or not.
|
|
|
|
|
|
-Next we flip the aim point by ``180`` degrees so it doesn't fire backwards.
|
|
|
|
|
|
+We're using ``is_ready`` because ``setget`` functions are called before ``_ready``, we need to ignore the
|
|
|
|
+first kit_size_change call, because we cannot access child nodes until ``_ready`` is called. If we did not ignore the
|
|
|
|
+first ``setget`` call, we would get several errors in the debugger.
|
|
|
|
|
|
-.. warning:: The reason behind rotating the gun aim point is explained in :ref:`part 2 <doc_fps_tutorial_part_two>`
|
|
|
|
|
|
+Also, notice how we're using a exported variable. This is so we can change the size of the health pick up in the editor, for each pick up. This makes it where
|
|
|
|
+we do not have to make two scenes for the two sizes, since we can easily change sizes in the editor using the exported variable.
|
|
|
|
|
|
-Finally, we set ``current_weapon_name`` and ``changing_weapon_name`` to ``UNARMED`` so our starting weapon is ``UNARMED``.
|
|
|
|
|
|
+.. tip:: See :ref:`doc_GDScript` and scroll down to the Exports section for a list of of export hints you can use.
|
|
|
|
|
|
______
|
|
______
|
|
|
|
|
|
-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``.
|
|
|
|
|
|
+Let's look at ``_ready``:
|
|
|
|
|
|
-Finishing ``process_input``
|
|
|
|
-___________________________
|
|
|
|
|
|
+First we connect the ``body_entered`` signal from our ``Health_Pickup_Trigger`` to the ``trigger_body_entered`` function. This makes is where any
|
|
|
|
+body that enters the :ref:`Area <class_Area>` triggers the ``trigger_body_entered`` function.
|
|
|
|
|
|
-First, lets change ``process_input`` so our weapon related code works with the new weapon system.
|
|
|
|
|
|
+Next we set ``is_ready`` to ``true`` so we can use our ``setget`` function.
|
|
|
|
|
|
-First, delete all of the weapon related code in `process_input`. This is the includes:
|
|
|
|
-Changing weapons, Reloading, and Firing.
|
|
|
|
|
|
+Then we hide all of the possible kits and their collision shapes using ``kit_size_change_values``. The first argument is the size of the kit, while the second argument
|
|
|
|
+is whether to enable or disable the collision shape and mesh at that size.
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+Then we make only the kit size we selected visible, calling ``kit_size_change_values`` and passing in ``kit_size`` and ``true``, so the size at ``kit_size`` is enabled.
|
|
|
|
|
|
______
|
|
______
|
|
|
|
|
|
-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 let's look at ``kit_size_changed``.
|
|
|
|
|
|
-Next we check each of the four number keys and we assign ``weapon_change_number`` to their value if they are pressed.
|
|
|
|
|
|
+The first thing we do is check to see if ``is_ready`` is ``true``.
|
|
|
|
|
|
-.. 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.
|
|
|
|
|
|
+If ``is_ready`` is ``true``, we then make whatever kit is currently assigned to ``kit_size`` disabled using ``kit_size_change_values``, passing in ``kit_size`` and ``false``.
|
|
|
|
|
|
-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``.
|
|
|
|
|
|
+Then we assign ``kit_size`` to the new value passed in, ``value``. Then we call ``kit_size_change_values`` passing in ``kit_size`` again, but this time
|
|
|
|
+with the second argument as ``true`` so we enable it. Because we changed ``kit_size`` to the passed in value, this will make whatever kit size we passed in visible.
|
|
|
|
|
|
-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``.
|
|
|
|
|
|
+If ``is_ready`` is not ``true``, we simply assign ``kit_size`` to the passed in ``value``.
|
|
|
|
|
|
______
|
|
______
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+Now let's look at ``kit_size_change_values``.
|
|
|
|
|
|
-.. tip:: We check for ``null`` because we do not want to reload ``UNARMED``!
|
|
|
|
|
|
+The first thing we do is check to see which size we're using. Based on which size we're wanting to enable/disable, we want to get different nodes.
|
|
|
|
|
|
-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 get the collision shape for the node corresponding to ``size`` and disable it based on the ``enabled`` passed in argument/variable.
|
|
|
|
|
|
-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``.
|
|
|
|
|
|
+.. note:: Why are we using ``!enable`` instead of ``enable``? This is so when we say we want to enable the node, we can pass in ``true``, but since
|
|
|
|
+ :ref:`CollisionShape <class_CollisionShape>` uses disabled instead of enabled, we need to flip it. By flipping it, we can enable the collision shape
|
|
|
|
+ and make the mesh visible when ``true`` is passed in.
|
|
|
|
+
|
|
|
|
+We then get the correct :ref:`Spatial <class_Spatial>` node holding the mesh and set its visibility to ``enable``.
|
|
|
|
|
|
-If ``is_reloading`` is still ``false``, we then set ``reloading_weapon`` to true so we can process the reloading weapon logic in ``process_reloading``.
|
|
|
|
|
|
+This function may be a little confusing, try to think of it like this: We're enabling/disabling the proper nodes for ``size`` using ``enabled``. This is so we cannot pick up
|
|
|
|
+health for a size that is not visible, and so only the mesh for the proper size will be visible.
|
|
|
|
|
|
______
|
|
______
|
|
|
|
|
|
-Finally, we have the firing section.
|
|
|
|
|
|
+Finally, let's look at ``trigger_body_entered``.
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+The first thing we do is see whether or not the body that just entered has a method/function called ``add_health``. If it does, we then
|
|
|
|
+call ``add_health`` and pass in the health provided by the current kit size.
|
|
|
|
|
|
-Next we get the current weapon and assign it to ``current_weapon``. We then check to make sure it is not equal to ``null``.
|
|
|
|
|
|
+Then we set ``respawn_timer`` to ``RESPAWN_TIME`` so we have to wait before we can get health again. Finally, call ``kit_size_change_values``,
|
|
|
|
+passing in ``kit_size`` and ``false`` so the kit at ``kit_size`` is invisible until we've waited long enough to respawn.
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+The last thing we need to do before we can use this health pick up is add a few things to our player.
|
|
|
|
|
|
-Adding our new input map actions
|
|
|
|
-________________________________
|
|
|
|
|
|
+Open up ``Player.gd`` and add the following global variable:
|
|
|
|
|
|
-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!
|
|
|
|
|
|
+::
|
|
|
|
+
|
|
|
|
+ const MAX_HEALTH = 150
|
|
|
|
+
|
|
|
|
+* ``MAX_HEALTH``: The maximum amount of health a player can have.
|
|
|
|
+
|
|
|
|
+Now we need to add the ``add_health`` function to our player. Add the following to ``Player.gd``:
|
|
|
|
|
|
-.. image:: img/ProjectSettingsAddAction.png
|
|
|
|
|
|
+::
|
|
|
|
+
|
|
|
|
+ func add_health(additional_health):
|
|
|
|
+ health += additional_health
|
|
|
|
+ health = clamp(health, 0, MAX_HEALTH)
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+Let's quickly go over what this does.
|
|
|
|
|
|
-Scroll down to the bottom of the list and click the little plus sign next to one of the newly created actions.
|
|
|
|
|
|
+We first add ``additional_health`` to our current health. We then clamp the health so that it cannot exceed a value higher than ``MAX_HEALTH``, nor a value lower
|
|
|
|
+than ``0``.
|
|
|
|
|
|
-.. image:: img/ProjectSettingsAddKey.png
|
|
|
|
|
|
+_______
|
|
|
|
+
|
|
|
|
+With that done, now we can collect health! Go place a few ``Health_Pickup`` scenes around and give it a try. You can change the size of the health pick up in the editor
|
|
|
|
+when a ``Health_Pickup`` instanced scene is selected, from a convenient drop down.
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+Adding the ammo pick ups
|
|
|
|
+------------------------
|
|
|
|
|
|
-Once you've assigned whatever keys you want to both actions, close the project settings and save.
|
|
|
|
|
|
+While adding health is good and all, we can't really reap the rewards from it since nothing can (currently) damage us.
|
|
|
|
+Let's add some ammo pick ups next!
|
|
|
|
|
|
-Adding ``process_changing_weapons``
|
|
|
|
-___________________________________
|
|
|
|
|
|
+Open up ``Ammo_Pickup.tscn``. Notice how it's structured exactly the same as ``Health_Pickup.tscn``, just with the meshes and trigger collision shapes changed slightly to adjust
|
|
|
|
+for the difference in mesh sizes.
|
|
|
|
|
|
-Lets make the weapon changing logic next. Open up ``Player.gd`` and add the following function:
|
|
|
|
|
|
+Select ``Ammo_Pickup`` and add a new script called ``Ammo_Pickup.gd``. Add the following:
|
|
|
|
|
|
::
|
|
::
|
|
|
|
|
|
- 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 = ""
|
|
|
|
|
|
+ extends Spatial
|
|
|
|
|
|
-Lets go over what's happening here.
|
|
|
|
|
|
+ export (int, "full size", "small") var kit_size = 0 setget kit_size_change
|
|
|
|
|
|
-First we check to make sure ``changing_weapon`` is ``true``.
|
|
|
|
|
|
+ # 0 = full size pickup, 1 = small pickup
|
|
|
|
+ const AMMO_AMOUNTS = [4, 1]
|
|
|
|
+
|
|
|
|
+ const RESPAWN_TIME = 20
|
|
|
|
+ var respawn_timer = 0
|
|
|
|
|
|
-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``.
|
|
|
|
|
|
+ var is_ready = false
|
|
|
|
|
|
-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``.
|
|
|
|
|
|
+ func _ready():
|
|
|
|
+
|
|
|
|
+ $Holder/Ammo_Pickup_Trigger.connect("body_entered", self, "trigger_body_entered")
|
|
|
|
+
|
|
|
|
+ is_ready = true
|
|
|
|
+
|
|
|
|
+ kit_size_change_values(0, false)
|
|
|
|
+ kit_size_change_values(1, false)
|
|
|
|
+
|
|
|
|
+ kit_size_change_values(kit_size, true)
|
|
|
|
|
|
-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``).
|
|
|
|
|
|
+ func _physics_process(delta):
|
|
|
|
+ if respawn_timer > 0:
|
|
|
|
+ respawn_timer -= delta
|
|
|
|
+
|
|
|
|
+ if respawn_timer <= 0:
|
|
|
|
+ kit_size_change_values(kit_size, true)
|
|
|
|
|
|
-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``.
|
|
|
|
|
|
+ func kit_size_change(value):
|
|
|
|
+ if is_ready:
|
|
|
|
+ kit_size_change_values(kit_size, false)
|
|
|
|
+ kit_size = value
|
|
|
|
+
|
|
|
|
+ kit_size_change_values(kit_size, true)
|
|
|
|
+ else:
|
|
|
|
+ kit_size = value
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+ func kit_size_change_values(size, enable):
|
|
|
|
+ if size == 0:
|
|
|
|
+ $Holder/Ammo_Pickup_Trigger/Shape_Kit.disabled = !enable
|
|
|
|
+ $Holder/Ammo_Kit.visible = enable
|
|
|
|
+ elif size == 1:
|
|
|
|
+ $Holder/Ammo_Pickup_Trigger/Shape_Kit_Small.disabled = !enable
|
|
|
|
+ $Holder/Ammo_Kit_Small.visible = enable
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+ func trigger_body_entered(body):
|
|
|
|
+ if body.has_method("add_ammo"):
|
|
|
|
+ body.add_ammo(AMMO_AMOUNTS[kit_size])
|
|
|
|
+ respawn_timer = RESPAWN_TIME
|
|
|
|
+ kit_size_change_values(kit_size, false)
|
|
|
|
|
|
-Adding ``process_reloading``
|
|
|
|
-____________________________
|
|
|
|
|
|
+You may have noticed this code looks almost exactly the same as the health pick up. That's because it largely is the same! Only a few things
|
|
|
|
+have been changed, and that's what we're going to go over.
|
|
|
|
|
|
-Let's finish up our new modular weapon system and add ``process_reloading``. Make a new function called ``process_reloading`` and add the following:
|
|
|
|
|
|
+First, notice how we have ``AMMO_AMOUNTS`` instead of ``HEALTH_AMMOUNTS``. ``AMMO_AMOUNTS`` will be how many ammo clips/magazines we add to the current weapon.
|
|
|
|
+(Unlike ``HEALTH_AMMOUNTS`` which was how many health points, we instead just add an entire clip for the current weapon, instead of the raw ammo amount)
|
|
|
|
|
|
-::
|
|
|
|
-
|
|
|
|
-
|
|
|
|
- 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
|
|
|
|
|
|
+The only other thing to notice is in ``trigger_body_entered`` we're checking and calling a function called ``add_ammo``, not ``add_health``.
|
|
|
|
|
|
-Let's go over what's this function does.
|
|
|
|
|
|
+Other than those two small changes, everything else is exactly the same as the health pickup!
|
|
|
|
|
|
-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:
|
|
|
|
|
|
+All we need to do make the ammo pick ups work is add a new function to our player. Open ``Player.gd`` and add the following function:
|
|
|
|
|
|
::
|
|
::
|
|
|
|
|
|
- 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``
|
|
|
|
-_____________________
|
|
|
|
-
|
|
|
|
|
|
+ func add_ammo(additional_ammo):
|
|
|
|
+ if (current_weapon_name != "UNARMED"):
|
|
|
|
+ if (weapons[current_weapon_name].CAN_REFILL == true):
|
|
|
|
+ weapons[current_weapon_name].spare_ammo += weapons[current_weapon_name].AMMO_IN_MAG * additional_ammo
|
|
|
|
|
|
-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:
|
|
|
|
|
|
+Let's go over what this function does.
|
|
|
|
|
|
-::
|
|
|
|
-
|
|
|
|
- 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)
|
|
|
|
|
|
+The first thing we check is to see whether we're using ``UNARMED`` or not. Because ``UNARMED`` does not have a node/script, we want to make sure we're not using
|
|
|
|
+``UNARMED`` before trying to get the node/script attached to ``current_weapon_name``.
|
|
|
|
|
|
-
|
|
|
|
-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.
|
|
|
|
|
|
+Next we check to see if the current weapon can be refilled. If the current weapon can, we add a full clip/magazine worth of ammo to the weapon by
|
|
|
|
+multiplying the current weapon's ``AMMO_IN_MAG`` variable times however much ammo clips we're adding (``additional_ammo``).
|
|
|
|
|
|
-The only major change is how we get the amount counts in the current weapon.
|
|
|
|
|
|
+_______
|
|
|
|
|
|
-______
|
|
|
|
|
|
+With that done, you should now be able to get additional ammo! Go place some ammo pick ups in one/both/all of the scenes and give it a try!
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+.. note:: Notice how we're not limiting the amount of ammo you can carry. To limit the amount of ammo each weapon can carry, you just need to add a additional variable to
|
|
|
|
+ each weapon's script, and then clamp the weapon's ``spare_ammo`` variable after adding ammo in ``add_ammo``.
|
|
|
|
|
|
-Now that we've refactored ``Player.gd``, lets add something new: Let's allow our plays to play using a joypad!
|
|
|
|
|
|
+Adding breakable targets
|
|
|
|
+------------------------
|
|
|
|
|
|
-Adding joypad input
|
|
|
|
--------------------
|
|
|
|
|
|
+Before we end this part, let's add some targets.
|
|
|
|
|
|
-.. 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.
|
|
|
|
|
|
+Open up ``Target.tscn`` and take a look at the scenes in the scene tree.
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+First, notice how we're not using a :ref:`RigidBody <class_RigidBody>` node, but rather a :ref:`StaticBody <class_StaticBody>` node instead.
|
|
|
|
+The reason behind this is our non-broken targets will not be moving anywhere, using a :ref:`RigidBody <class_RigidBody>` would be more hassle then
|
|
|
|
+its worth, since all it has to do is stay still.
|
|
|
|
|
|
-Now we need to add some joypad buttons to our various actions. Click the plus icon and select ``Joy Button``.
|
|
|
|
|
|
+.. tip:: We also save a tiny bit of performance using a :ref:`StaticBody <class_StaticBody>` over a :ref:`RigidBody <class_RigidBody>`
|
|
|
|
|
|
-.. image:: img/ProjectSettingsAddKey.png
|
|
|
|
|
|
+The other thing to note is we have a node called ``Broken_Target_Holder``. This node is going to hold a spawned/instanced scene called
|
|
|
|
+``Broken_Target.tscn``. Open up ``Broken_Target.tscn``.
|
|
|
|
|
|
-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:
|
|
|
|
|
|
+Notice how the target is broken up into five pieces, each a :ref:`RigidBody <class_RigidBody>` node. We're going to spawn/instance this scene when the target takes too much damage
|
|
|
|
+and needs to be destroyed. Then we're going to hide the non-broken target, so it looks like the target shattered rather than a shattered target was
|
|
|
|
+spawned/instanced.
|
|
|
|
|
|
-* 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)``
|
|
|
|
|
|
+While you still have ``Broken_Target.tscn`` open, attach ``RigidBody_hit_test.gd`` to all of the :ref:`RigidBody <class_RigidBody>` nodes. This will make
|
|
|
|
+it where we can shoot at the broken pieces and they will react to the bullets.
|
|
|
|
|
|
-Once you are happy with the input, close the project settings and save.
|
|
|
|
|
|
+Alright, now switch back to ``Target.tscn``, select the ``Target`` :ref:`StaticBody <class_StaticBody>` node and created a new script called ``Target.gd``.
|
|
|
|
|
|
-______
|
|
|
|
-
|
|
|
|
-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``:
|
|
|
|
|
|
+Add the following code to ``Target.gd``:
|
|
|
|
|
|
::
|
|
::
|
|
|
|
|
|
- # You may need to adjust depending on the sensitivity of your joypad
|
|
|
|
- const JOYPAD_SENSITIVITY = 2
|
|
|
|
- const JOYPAD_DEADZONE = 0.15
|
|
|
|
|
|
+ extends StaticBody
|
|
|
|
|
|
-Lets go over what each of these do:
|
|
|
|
|
|
+ const TARGET_HEALTH = 40
|
|
|
|
+ var current_health = 40
|
|
|
|
|
|
-* ``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.
|
|
|
|
|
|
+ var broken_target_holder
|
|
|
|
|
|
-.. 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.
|
|
|
|
|
|
+ # The collision shape for the target.
|
|
|
|
+ # NOTE: this is for the whole target, not the pieces of the target
|
|
|
|
+ var target_collision_shape
|
|
|
|
|
|
-Now we are ready to start handling joypad input!
|
|
|
|
|
|
+ const TARGET_RESPAWN_TIME = 14
|
|
|
|
+ var target_respawn_timer = 0
|
|
|
|
|
|
-______
|
|
|
|
-
|
|
|
|
-In ``process_input`` add the following code, just before ``input_movement_vector = input_movement_vector.normalized()``:
|
|
|
|
|
|
+ export (PackedScene) var destroyed_target
|
|
|
|
|
|
-::
|
|
|
|
-
|
|
|
|
- # 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.
|
|
|
|
|
|
+ func _ready():
|
|
|
|
+ broken_target_holder = get_parent().get_node("Broken_Target_Holder")
|
|
|
|
+ target_collision_shape = $Collision_Shape
|
|
|
|
|
|
-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!
|
|
|
|
-
|
|
|
|
-______
|
|
|
|
|
|
+ func _physics_process(delta):
|
|
|
|
+ if target_respawn_timer > 0:
|
|
|
|
+ target_respawn_timer -= delta
|
|
|
|
+
|
|
|
|
+ if target_respawn_timer <= 0:
|
|
|
|
+
|
|
|
|
+ for child in broken_target_holder.get_children():
|
|
|
|
+ child.queue_free()
|
|
|
|
+
|
|
|
|
+ target_collision_shape.disabled = false
|
|
|
|
+ visible = true
|
|
|
|
+ current_health = TARGET_HEALTH
|
|
|
|
|
|
-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
|
|
|
|
|
|
+ func bullet_hit(damage, bullet_hit_pos):
|
|
|
|
+ current_health -= damage
|
|
|
|
|
|
- var joypad_vec = Vector2()
|
|
|
|
- if Input.get_connected_joypads().size() > 0:
|
|
|
|
|
|
+ if current_health <= 0:
|
|
|
|
+ var clone = destroyed_target.instance()
|
|
|
|
+ broken_target_holder.add_child(clone)
|
|
|
|
|
|
- # 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))
|
|
|
|
|
|
+ for rigid in clone.get_children():
|
|
|
|
+ if rigid is RigidBody:
|
|
|
|
+ var center_in_rigid_space = broken_target_holder.global_transform.origin - rigid.global_transform.origin
|
|
|
|
+ var direction = (rigid.transform.origin - center_in_rigid_space).normalized()
|
|
|
|
+ # Apply the impulse with some additional force (I find 12 works nicely)
|
|
|
|
+ rigid.apply_impulse(center_in_rigid_space, direction * 12 * damage)
|
|
|
|
|
|
- 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:
|
|
|
|
|
|
+ target_respawn_timer = TARGET_RESPAWN_TIME
|
|
|
|
+
|
|
|
|
+ target_collision_shape.disabled = true
|
|
|
|
+ visible = false
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+Let's go over what this script does, starting with the global variables:
|
|
|
|
|
|
-.. 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!
|
|
|
|
|
|
+* ``TARGET_HEALTH``: The amount of damage needed to break a fully healed target.
|
|
|
|
+* ``current_health``: The amount of health this target currently has.
|
|
|
|
+* ``broken_target_holder``: A variable to hold the ``Broken_Target_Holder`` node so we can use it easily.
|
|
|
|
+* ``target_collision_shape``: A variable to hold the :ref:`CollisionShape <class_CollisionShape>` for the non-broken target.
|
|
|
|
+* ``TARGET_RESPAWN_TIME``: The length of time, in seconds, it takes for a target to respawn.
|
|
|
|
+* ``target_respawn_timer``: A variable to track how long a target has been broken.
|
|
|
|
+* ``destroyed_target``: A :ref:`PackedScene <class_PackedScene>` to hold the broken target scene.
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+Notice how we're using an exported variable (a :ref:`PackedScene <class_PackedScene>`) to get the broken target scene instead of
|
|
|
|
+using ``preload``. By using an exported variable, we can chose the scene from the editor, and when/if we need to use a different scene,
|
|
|
|
+it's as easy as selecting a different scene in the editor, we don't need to go to the code to change the scene we're using.
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+Let's look at ``_ready``.
|
|
|
|
|
|
-We then account for the joypad's dead zone, just like in ``process_input``.
|
|
|
|
|
|
+The first thing we do is get the broken target holder and assign it to ``broken_target_holder``. Notice how we're using ``get_parent().get_node()`` here, instead
|
|
|
|
+of ``$``. If you want to use ``$``, then you'd need to change ``get_parent().get_node()`` to ``$"../Broken_Target_Holder"``.
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+.. note:: At the time of when this was written, I did not realize you can use ``$"../NodeName"`` to get the parent nodes using ``$``, which is why ``get_parent().get_node()``
|
|
|
|
+ is used instead.
|
|
|
|
|
|
-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``.
|
|
|
|
|
|
+Next we get the collision shape and assign it to ``target_collision_shape``. The reason we need to collision shape is because even when the mesh is invisible, the
|
|
|
|
+collision shape will still exist in the physics world. This makes it where the player can interact with a non-broken target even though it's invisible, which is
|
|
|
|
+not what we want. To get around this, we will disable/enable the collision shape as we make the mesh visible/invisible.
|
|
|
|
|
|
-.. 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.
|
|
|
|
|
|
+Next let's look at ``_physics_process``.
|
|
|
|
|
|
-______
|
|
|
|
|
|
+We're only going to be using ``_physics_process`` for respawning, and so the first thing we do is check to see if ``target_respawn_timer`` is more than ``0``.
|
|
|
|
|
|
-If everything is setup correctly, you can now play around using a joypad!
|
|
|
|
|
|
+If it is, we then remove ``delta`` from it.
|
|
|
|
|
|
-.. 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
|
|
|
|
--------------------------------
|
|
|
|
|
|
+Then we check to see if ``target_respawn_timer`` is ``0`` or less. The reason behind this is since we just removed ``delta`` from ``target_respawn_timer``, if it's
|
|
|
|
+``0`` or less then we've just got here, effectively allowing us to do whatever we need to do when the timer is finished.
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+In this case, we want to respawn our target.
|
|
|
|
|
|
-Open up ``Player.gd`` and add the following global variables:
|
|
|
|
|
|
+The first thing we do is remove all children in the broken target holder. We do this by iterating over all of the children in ``broken_target_holder`` and free them.
|
|
|
|
|
|
-::
|
|
|
|
-
|
|
|
|
- var mouse_scroll_value = 0
|
|
|
|
- const MOUSE_SENSITIVITY_SCROLL_WHEEL = 0.08
|
|
|
|
|
|
+Next we enable our collision shape by setting its ``disabled`` boolean to ``false``.
|
|
|
|
|
|
-Lets go over what each of these new varibles will be doing:
|
|
|
|
|
|
+Then we make ourselves, and all of our children nodes, visible.
|
|
|
|
|
|
-* ``mouse_scroll_value``: The value of the mouse scroll wheel.
|
|
|
|
-* ``MOUSE_SENSITIVITY_SCROLL_WHEEL``: How much a single scroll action increases mouse_scroll_value
|
|
|
|
|
|
+Finally, we reset ``current_health`` to ``TARGET_HEALTH``.
|
|
|
|
|
|
______
|
|
______
|
|
|
|
|
|
-Now lets add the following to ``_input``:
|
|
|
|
|
|
+Finally, let's look at ``bullet_hit``.
|
|
|
|
|
|
-::
|
|
|
|
-
|
|
|
|
- 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
|
|
|
|
|
|
+The first the we do is remove however much damage the bullet does from our health.
|
|
|
|
|
|
-
|
|
|
|
-Let's go over what's happening here:
|
|
|
|
|
|
+Next we check to see if we're at ``0`` health or lower. If we are, then we've just died and need to spawn a broken target.
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+We first instance a new destroyed target scene, and assign it to a new variable, ``clone``.
|
|
|
|
|
|
-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 add ``clone`` as a child of our broken target holder.
|
|
|
|
|
|
-Next we clamp mouse scroll value to assure it is inside the range of our weapons.
|
|
|
|
|
|
+For an added bonus, we want to make all of the target pieces explode outwards. Do to this, we iterate over all of the children in ``clone``.
|
|
|
|
|
|
-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``.
|
|
|
|
|
|
+For each child, we first check to see if it's a :ref:`RigidBody <class_RigidBody>` node. If it is, we then calculate the center position of the target relative
|
|
|
|
+to the child node. Then we figure out which direction we are relative to the center. Using those calculated variables, we push the child from the calculated center,
|
|
|
|
+in the direction away from the center, using the damage of the bullet as the force.
|
|
|
|
|
|
-.. note:: We 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.
|
|
|
|
|
|
+.. note:: We multiply the damage by ``12`` so it has a more dramatic effect. You can change this to a higher or lower value depending on how explosive you want
|
|
|
|
+ your targets to shatter.
|
|
|
|
|
|
-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``.
|
|
|
|
|
|
+Next we set our respawn timer for our non-broken target. We set it to ``TARGET_RESPAWN_TIME``, so it takes ``TARGET_RESPAWN_TIME`` many seconds to respawn.
|
|
|
|
|
|
-.. 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.
|
|
|
|
|
|
+Then we disable the non-broken target's collision shape, and set our visibility to ``false``.
|
|
|
|
|
|
______
|
|
______
|
|
|
|
|
|
-Now you can change weapons using the scroll wheel! Go give it a whirl!
|
|
|
|
|
|
+.. warning:: Make sure to set the exported ``destroyed_target`` value for ``Target.tscn`` in the editor! Otherwise the targets will not be destroyed
|
|
|
|
+ and you will get an error!
|
|
|
|
+
|
|
|
|
+With that done, go place some ``Target.tscn`` instances around in one/both/all of the levels. You should find they explode into five pieces after they've taken enough
|
|
|
|
+damage. After a little while, they'll respawn into a whole target again.
|
|
|
|
|
|
Final notes
|
|
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
|
|
.. 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!
|
|
|
|
|
|
+Now you can use a joypad, change weapons with the mouse's scroll wheel, replenish your health and ammo, and break targets with your weapons.
|
|
|
|
|
|
-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 the next part, :ref:`doc_fps_tutorial_part_five`, we're going to add grenades to our player, give our player the ability to grab and throw objects, and
|
|
|
|
+add turrets!
|
|
|
|
|
|
-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.
|
|
|
|
|
|
+.. warning:: If you ever get lost, be sure to read over the code again!
|
|
|
|
|
|
|
|
+ You can download the finished project for this part here: :download:`Godot_FPS_Part_4.zip <files/Godot_FPS_Part_4.zip>`
|
|
|
|
|