Teil 4

Teilübersicht

In diesem Teil werden wir Gesundheits-Pickups, Munitions-Pickups und vom Spieler zerstörbare Ziele hinzufügen, Unterstützung für Joypads einbauen und die Möglichkeit hinzufügen, Waffen mit dem Scrollrad zu wechseln.

../../../_images/PartFourFinished.png

Bemerkung

You are assumed to have finished Teil 3 before moving on to this part of the tutorial. The finished project from Teil 3 will be the starting project for part 4

Lassen Sie uns anfangen!

Joypad-Eingabe hinzufügen

Bemerkung

In Godot wird jeder Gamecontroller als Joypad bezeichnet. Dazu gehören: Konsolensteuerungen, Joysticks (wie bei Flugsimulatoren), Lenkräder (wie bei Fahrsimulatoren), VR-Steuerungen und vieles mehr!

Firstly, we need to change a few things in our project's input map. Open up the project settings and select the Input Map tab.

Now we need to add some joypad buttons to our various actions. Click the plus icon and select Joy Button.

../../../_images/ProjectSettingsAddKey.png

Sie können jedes gewünschte Tastenlayout verwenden. Stellen Sie sicher, dass das ausgewählte Gerät auf 0 eingestellt ist. Im fertigen Projekt werden wir Folgendes verwenden:

  • movement_sprint: Gerät 0, Knopf 4 (L, L1)
  • fire: Gerät 0, Knopf 0 (PS Kreuz, XBox A, Nintendo B)
  • reload: Gerät 0, Knopf 0 (PS Quadrat, XBox X, Nintendo Y)
  • flashlight: Gerät 0, Knopf 12 (D-Pad hoch)
  • shift_weapon_positive: Gerät 0, Knopf 15 (D-Pad rechts)
  • shift_weapon_negative: Gerät 0, Knopf 14 (D-Pad links)
  • fire_grenade: Gerät 0, Knopf 1 (PS Kreis, XBox B, Nintendo A).

Bemerkung

Diese sind bereits für Sie eingerichtet, wenn Sie die Starter-Assets heruntergeladen haben

Wenn Sie mit der Eingabe zufrieden sind, schließen Sie die Projekteinstellungen und speichern Sie diese.


Öffnen wir nun Player.gd und fügen Joypad-Eingaben hinzu.

Zuerst müssen wir einige neue Klassenvariablen definieren. Fügen Sie in Player.gd die folgenden Klassenvariablen hinzu:

# You may need to adjust depending on the sensitivity of your joypad
var JOYPAD_SENSITIVITY = 2
const JOYPAD_DEADZONE = 0.15

Lassen Sie uns durchgehen, was jeder von diesen tut:

  • JOYPAD_SENSITIVITY: So schnell bewegen die Joysticks des Pads die Kamera.
  • JOYPAD_DEADZONE: Die tote Zone für das Joypad. Je nach Joypad müssen Sie möglicherweise Anpassungen vornehmen.

Bemerkung

Viele Joypads zittern um einen bestimmten Punkt. Um dem entgegenzuwirken ignorieren wir jede Bewegung innerhalb eines Radius von JOYPAD_DEADZONE. Wenn wir diese Bewegung nicht ignorieren, würde die Kamera zittern.

Außerdem definieren wir JOYPAD_SENSITIVITY als Variable anstelle einer Konstante, da wir sie später ändern werden.

Jetzt können wir mit Joypad-Eingaben beginnen!


Fügen Sie in process_input den folgenden Code direkt vor ``input_movement_vector = input_movement_vector.normalized() `` hinzu:

# Add joypad input if one is present
if Input.get_connected_joypads().size() > 0:

    var joypad_vec = Vector2(0, 0)

    if OS.get_name() == "Windows":
        joypad_vec = Vector2(Input.get_joy_axis(0, 0), -Input.get_joy_axis(0, 1))
    elif OS.get_name() == "X11":
        joypad_vec = Vector2(Input.get_joy_axis(0, 1), Input.get_joy_axis(0, 2))
    elif OS.get_name() == "OSX":
        joypad_vec = Vector2(Input.get_joy_axis(0, 1), Input.get_joy_axis(0, 2))

    if joypad_vec.length() < JOYPAD_DEADZONE:
        joypad_vec = Vector2(0, 0)
    else:
        joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))

    input_movement_vector += joypad_vec
# Add joypad input if one is present
if Input.get_connected_joypads().size() > 0:

    var joypad_vec = Vector2(0, 0)

    if OS.get_name() == "Windows" or OS.get_name() == "X11":
        joypad_vec = Vector2(Input.get_joy_axis(0, 0), -Input.get_joy_axis(0, 1))
    elif OS.get_name() == "OSX":
        joypad_vec = Vector2(Input.get_joy_axis(0, 1), Input.get_joy_axis(0, 2))

    if joypad_vec.length() < JOYPAD_DEADZONE:
        joypad_vec = Vector2(0, 0)
    else:
        joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))

    input_movement_vector += joypad_vec

Lassen Sie uns einmal durchgehen was wir tun.

Zuerst prüfen wir ob ein vorhandenes Joypad angeschlossen ist.

Wenn ein Joypad angeschlossen ist, erhalten wir die linken Steuerachsen für rechts/links und oben/unten. Da ein kabelgebundener Xbox 360-Controller je nach Betriebssystem unterschiedliche Joystick-Achsenzuordnungen aufweist, werden je nach Betriebssystem unterschiedliche Achsen verwendet.

Warnung

In dieser Anleitung wird davon ausgegangen, dass Sie eine XBox 360 oder einen kabelgebundenen PlayStation-Controller verwenden. Außerdem wurde dies (bisher) an keinen Mac-Computer getestet, sodass die Joystick-Achsen möglicherweise geändert werden müssen. Wenn dies der Fall ist, öffnen Sie bitte ein GitHub-Problem im Godot-Dokumentations-Repository! Vielen Dank!

Next, we check to see if the joypad vector length is within the JOYPAD_DEADZONE radius. If it is, we set joypad_vec to an empty Vector2. If it is not, we use a scaled Radial Dead zone for precise dead zone calculation.

Bemerkung

You can find a great article explaining all about how to handle joypad/controller dead zones here.

Wir verwenden eine übersetzte Version des in diesem Artikel bereitgestellten skalierten Codes für die radiale Totzone. Der Artikel ist eine großartige Lektüre, und ich empfehle dringend, ihn sich anzusehen!

Schließlich wird joypad_vec zu input_movement_vector hinzugefügt.

Tipp

Remember how we normalize input_movement_vector? This is why! If we did not normalize input_movement_vector, the player could move faster if they pushed in the same direction with both the keyboard and the joypad!


Erstellen Sie eine neue Funktion mit dem Namen process_view_input und fügen Sie Folgendes hinzu:

func process_view_input(delta):

    if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
        return

    # NOTE: Until some bugs relating to captured mice are fixed, we cannot put the mouse view
    # rotation code here. Once the bug(s) are fixed, code for mouse view rotation code will go here!

    # ----------------------------------
    # Joypad rotation

    var joypad_vec = Vector2()
    if Input.get_connected_joypads().size() > 0:

        if OS.get_name() == "Windows":
            joypad_vec = Vector2(Input.get_joy_axis(0, 2), Input.get_joy_axis(0, 3))
        elif OS.get_name() == "X11":
            joypad_vec = Vector2(Input.get_joy_axis(0, 3), Input.get_joy_axis(0, 4))
        elif OS.get_name() == "OSX":
            joypad_vec = Vector2(Input.get_joy_axis(0, 3), Input.get_joy_axis(0, 4))

        if joypad_vec.length() < JOYPAD_DEADZONE:
            joypad_vec = Vector2(0, 0)
        else:
            joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))

        rotation_helper.rotate_x(deg2rad(joypad_vec.y * JOYPAD_SENSITIVITY))

        rotate_y(deg2rad(joypad_vec.x * JOYPAD_SENSITIVITY * -1))

        var camera_rot = rotation_helper.rotation_degrees
        camera_rot.x = clamp(camera_rot.x, -70, 70)
        rotation_helper.rotation_degrees = camera_rot
    # ----------------------------------
func process_view_input(delta):

   if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
       return

   # NOTE: Until some bugs relating to captured mice are fixed, we cannot put the mouse view
   # rotation code here. Once the bug(s) are fixed, code for mouse view rotation code will go here!

   # ----------------------------------
   # Joypad rotation

   var joypad_vec = Vector2()
   if Input.get_connected_joypads().size() > 0:

       if OS.get_name() == "Windows" or OS.get_name() == "X11":
           joypad_vec = Vector2(Input.get_joy_axis(0, 2), Input.get_joy_axis(0, 3))
       elif OS.get_name() == "OSX":
           joypad_vec = Vector2(Input.get_joy_axis(0, 3), Input.get_joy_axis(0, 4))

       if joypad_vec.length() < JOYPAD_DEADZONE:
           joypad_vec = Vector2(0, 0)
       else:
           joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))

       rotation_helper.rotate_x(deg2rad(joypad_vec.y * JOYPAD_SENSITIVITY))

       rotate_y(deg2rad(joypad_vec.x * JOYPAD_SENSITIVITY * -1))

       var camera_rot = rotation_helper.rotation_degrees
       camera_rot.x = clamp(camera_rot.x, -70, 70)
       rotation_helper.rotation_degrees = camera_rot
   # ----------------------------------

Lassen Sie uns das einmal durchgehen:

Zunächst überprüfen wir den Mausmodus: ist dieser nicht MOUSE_MODE_CAPTURED, möchten wir zurückkehren, wodurch der folgende Code übersprungen wird.

Next, we define a new 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.

Warnung

As stated above, I do not (currently) have access to a Mac computer, so the joystick axes may need changing. If they do, please open a GitHub issue on the Godot documentation repository! Thanks!

We then account for the joypad's dead zone, exactly like in process_input.

Then, we rotate rotation_helper and the player's KinematicBody using joypad_vec.

Notice how the code that handles rotating the player and rotation_helper is exactly the same as the code in _input. All we've done is change the values to use joypad_vec and JOYPAD_SENSITIVITY.

Bemerkung

Due to a few mouse-related bugs on Windows, we cannot put mouse rotation in process_view as well. Once these bugs are fixed, this will likely be updated to place the mouse rotation here in process_view_input as well.

Schließlich blockieren wir die Drehung der Kamera, damit der Spieler nicht verkehrt herum schauen kann.


The last thing we need to do is add process_view_input to _physics_process.

Once process_view_input is added to _physics_process, you should be able to play using a joypad!

Bemerkung

I decided not to use the joypad triggers for firing because we'd then have to do some more axis managing, and because I prefer to use a shoulder buttons to fire.

If you want to use the triggers for firing, you will need to change how firing works in process_input. You need to get the axis values for the triggers, and check if it's over a certain value, say 0.8 for example. If it is, you add the same code as when the fire action was pressed.

Maus-Scrollrad-Eingabe hinzufügen

Fügen wir noch eine weitere eingabebezogene Funktion hinzu, bevor wir mit der Arbeit an den Pickups und dem Ziel beginnen. Fügen wir die Möglichkeit hinzu Waffen mit dem Mausrad zu wechseln.

Öffnen Sie Player.gd und fügen Sie die folgenden Klassenvariablen hinzu:

var mouse_scroll_value = 0
const MOUSE_SENSITIVITY_SCROLL_WHEEL = 0.08

Lassen Sie uns durchgehen, was jede dieser neuen Variablen tun wird:

  • mouse_scroll_value: Der Wert des Mausrads.
  • MOUSE_SENSITIVITY_SCROLL_WHEEL: Um wie viel eine einzige Mausrad-Bewegung den "mouse_scroll_value" erhöht

Fügen wir nun Folgendes zu _input hinzu:

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

Lassen Sie uns durchgehen was hier passiert:

Firstly, we check if the event is an InputEventMouseButton event and that the mouse mode is MOUSE_MODE_CAPTURED. Then, we check to see if the button index is either a BUTTON_WHEEL_UP or BUTTON_WHEEL_DOWN index.

If the event's index is indeed a button wheel index, we then check to see if it is a BUTTON_WHEEL_UP or BUTTON_WHEEL_DOWN index. Based on whether it is up or down, we add or subtract MOUSE_SENSITIVITY_SCROLL_WHEEL to/from mouse_scroll_value.

Als nächstes blockieren wir den Mausrad-Bewegungswert um sicherzustellen, dass er innerhalb des Bereichs auswählbarer Waffen liegt.

We then check to see if the player is changing weapons or reloading. If the player is doing neither, we round mouse_scroll_value and cast it to an int.

Bemerkung

We are casting mouse_scroll_value to an int so we can use it as a key in our dictionary. If we left it as a float, we would get an error when we tried to run the project.

Next, we check to see if the weapon name at round_mouse_scroll_value is not equal to the current weapon name using WEAPON_NUMBER_TO_NAME. If the weapon is different from the player's current weapon, we assign changing_weapon_name, set changing_weapon to true so the player will change weapons in process_changing_weapon, and set mouse_scroll_value to round_mouse_scroll_value.

Tipp

The reason we are setting mouse_scroll_value to the rounded scroll value is because we do not want the player to keep their mouse scroll wheel just in between values, giving them the ability to switch almost extremely fast. By assigning mouse_scroll_value to round_mouse_scroll_value, we ensure that each weapon takes exactly the same amount of scrolling to change.


One more thing we need to change is in process_input. In the code for changing weapons, add the following right after the line changing_weapon = true:

mouse_scroll_value = weapon_change_number

Now the scroll value will be changed with the keyboard input. If we did not change this, the scroll value would be out of sync. If the scroll wheel were out of sync, scrolling forwards or backwards would not transition to the next/last weapon, but rather the next/last weapon the scroll wheel changed to.


Jetzt können Sie Waffen mit dem Scrollrad wechseln! Probieren Sie es aus!

Hinzufügen der Gesundheits-Pickups

Jetzt da der Spieler Gesundheit und Munition hat, brauchen wir idealerweise einen Weg, um diese Ressourcen wieder aufzufüllen.

Öffnen Sie Health_Pickup.tscn.

Expand Holder if it's not already expanded. Notice how we have two Spatial nodes, one called Health_Kit and another called Health_Kit_Small.

This is because we're actually going to be making two sizes of health pickups, one small and one large/normal. Health_Kit and Health_Kit_Small only have a single MeshInstance as their children.

Next expand Health_Pickup_Trigger. This is an Area node we're going to use to check if the player has walked close enough to pick up the health kit. If you expand it, you'll find two collision shapes, one for each size. We will be using a different collision shape size based on the size of the health pickup, so the smaller health pickup has a trigger collision shape closer to its size.

The last thing to note is how we have an AnimationPlayer node so the health kit bobs and spins around slowly.

Select Health_Pickup and add a new script called Health_Pickup.gd. Add the following:

extends Spatial

export (int, "full size", "small") var kit_size = 0 setget kit_size_change

# 0 = full size pickup, 1 = small pickup
const HEALTH_AMOUNTS = [70, 30]

const RESPAWN_TIME = 20
var respawn_timer = 0

var is_ready = false

func _ready():

    $Holder/Health_Pickup_Trigger.connect("body_entered", self, "trigger_body_entered")

    is_ready = true

    kit_size_change_values(0, false)
    kit_size_change_values(1, false)
    kit_size_change_values(kit_size, true)


func _physics_process(delta):
    if respawn_timer > 0:
        respawn_timer -= delta

        if respawn_timer <= 0:
            kit_size_change_values(kit_size, true)


func kit_size_change(value):
    if is_ready:
        kit_size_change_values(kit_size, false)
        kit_size = value
        kit_size_change_values(kit_size, true)
    else:
        kit_size = value


func kit_size_change_values(size, enable):
    if size == 0:
        $Holder/Health_Pickup_Trigger/Shape_Kit.disabled = !enable
        $Holder/Health_Kit.visible = enable
    elif size == 1:
        $Holder/Health_Pickup_Trigger/Shape_Kit_Small.disabled = !enable
        $Holder/Health_Kit_Small.visible = enable


func trigger_body_entered(body):
    if body.has_method("add_health"):
        body.add_health(HEALTH_AMOUNTS[kit_size])
        respawn_timer = RESPAWN_TIME
        kit_size_change_values(kit_size, false)

Lassen Sie uns die Funktionsweise dieses Skripts durchgehen und mit seinen Klassenvariablen beginnen:

  • kit_size: The size of the health pickup. Notice how we're using a setget function to tell if it's changed.
  • HEALTH_AMMOUNTS: Die Menge an Gesundheit, die jeder Pickup in jeder Größe enthält.
  • RESPAWN_TIME: The amount of time, in seconds, it takes for the health pickup to respawn
  • respawn_timer: A variable used to track how long the health pickup has been waiting to respawn.
  • is_ready: A variable to track whether the _ready function has been called or not.

We're using is_ready because setget functions are called before _ready; we need to ignore the first kit_size_change call, because we cannot access child nodes until _ready is called. If we did not ignore the first setget call, we would get several errors in the debugger.

Also, notice how we are using an exported variable. This is so we can change the size of the health pickups in the editor. This makes it so we do not have to make two scenes for the two sizes, since we can easily change sizes in the editor using the exported variable.

Tipp

See GDScript Grundlagen and scroll down to the Exports section for a list of export hints you can use.


Schauen wir uns _ready an:

Firstly, we connect the body_entered signal from the Health_Pickup_Trigger to the trigger_body_entered function. This makes it so any body that enters the Area triggers the trigger_body_entered function.

Next, we set is_ready to true so we can use the setget function.

Then we hide all the possible kits and their collision shapes using kit_size_change_values. The first argument is the size of the kit, while the second argument is whether to enable or disable the collision shape and mesh at that size.

Then we make only the kit size we selected visible, calling kit_size_change_values and passing in kit_size and true, so the size at kit_size is enabled.


Als nächstes sehen wir uns kit_size_change an.

The first thing we do is check to see if is_ready is true.

If is_ready is true, we then make whatever kit already assigned to kit_size disabled using kit_size_change_values, passing in kit_size and false.

Then we assign kit_size to the new value passed in, value. Then we call kit_size_change_values passing in kit_size again, but this time with the second argument as true so we enable it. Because we changed kit_size to the passed in value, this will make whatever kit size was passed in visible.

If is_ready is not true, we simply assign kit_size to the passed in value.


Lassen Sie uns jetzt kit_size_change_values anschauen.

Als erstes überprüfen wir welche Größe übergeben wurde. Wir möchten verschiedene Nodes erhalten, abhängig von der Größe die wir aktivieren bzw. deaktivieren.

We get the collision shape for the node corresponding to size and disable it based on the enabled passed in argument/variable.

Bemerkung

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 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 Spatial node holding the mesh and set its visibility to enable.

This function may be a little confusing; try to think of it like this: We're enabling/disabling the proper nodes for size using enabled. This is so we cannot pick up health for a size that is not visible, and so only the mesh for the proper size will be visible.


Schließlich schauen wir auf trigger_body_entered.

The first thing we do is check whether or not the body that has just entered has a method/function called add_health. If it does, we then call add_health and pass in the health provided by the current kit size.

Then we set respawn_timer to RESPAWN_TIME so the player has to wait before the player can get health again. Finally, call kit_size_change_values, passing in kit_size and false so the kit at kit_size is invisible until it has waited long enough to respawn.


The last thing we need to do before the player can use this health pickup is add a few things to Player.gd.

Open up Player.gd and add the following class variable:

const MAX_HEALTH = 150
  • MAX_HEALTH: Die maximale Gesundheit, die ein Spieler haben kann.

Now we need to add the add_health function to the player. Add the following to Player.gd:

func add_health(additional_health):
    health += additional_health
    health = clamp(health, 0, MAX_HEALTH)

Lassen Sie uns schnell durchgehen, was dies bewirkt.

We first add additional_health to the player's current health. We then clamp the health so that it cannot take on a value higher than MAX_HEALTH, nor a value lower than 0.


With that done, the player can now collect health! Go place a few Health_Pickup scenes around and give it a try. You can change the size of the health pickup in the editor when a Health_Pickup instanced scene is selected, from a convenient drop down.

Hinzufügen der Munitions-Pickups

Das Hinzufügen von Gesundheit ist zwar schön und gut, aber wir können die Belohnungen nicht durch das Hinzufügen erhalten, da uns (derzeit) nichts schaden kann. Lassen Sie uns als nächstes einige Munitions-Pickups hinzufügen!

Open up Ammo_Pickup.tscn. Notice how it's structured exactly the same as Health_Pickup.tscn, but with the meshes and trigger collision shapes changed slightly to account for the difference in mesh sizes.

Select Ammo_Pickup and add a new script called Ammo_Pickup.gd. Add the following:

extends Spatial

export (int, "full size", "small") var kit_size = 0 setget kit_size_change

# 0 = full size pickup, 1 = small pickup
const AMMO_AMOUNTS = [4, 1]

const RESPAWN_TIME = 20
var respawn_timer = 0

var is_ready = false

func _ready():

    $Holder/Ammo_Pickup_Trigger.connect("body_entered", self, "trigger_body_entered")

    is_ready = true

    kit_size_change_values(0, false)
    kit_size_change_values(1, false)

    kit_size_change_values(kit_size, true)


func _physics_process(delta):
    if respawn_timer > 0:
        respawn_timer -= delta

        if respawn_timer <= 0:
            kit_size_change_values(kit_size, true)


func kit_size_change(value):
    if is_ready:
        kit_size_change_values(kit_size, false)
        kit_size = value

        kit_size_change_values(kit_size, true)
    else:
        kit_size = value


func kit_size_change_values(size, enable):
    if size == 0:
        $Holder/Ammo_Pickup_Trigger/Shape_Kit.disabled = !enable
        $Holder/Ammo_Kit.visible = enable
    elif size == 1:
        $Holder/Ammo_Pickup_Trigger/Shape_Kit_Small.disabled = !enable
        $Holder/Ammo_Kit_Small.visible = enable


func trigger_body_entered(body):
    if body.has_method("add_ammo"):
        body.add_ammo(AMMO_AMOUNTS[kit_size])
        respawn_timer = RESPAWN_TIME
        kit_size_change_values(kit_size, false)

You may have noticed this code looks almost exactly the same as the health pickup. That's because it largely is the same! Only a few things have been changed, and that's what we're going to go over.

Firstly, notice the change to AMMO_AMOUNTS from HEALTH_AMMOUNTS. AMMO_AMOUNTS will be how many ammo clips/magazines the pickup adds to the current weapon. (Unlike in the case of HEALTH_AMMOUNTS, which has stood for how many health points would be awarded, we add an entire clip to the current weapon instead of the raw ammo amount)

The only other thing to notice is in trigger_body_entered. We're checking for the existence of and calling a function called add_ammo instead of add_health.

Other than those two small changes, everything else is the same as the health pickup!


All we need to do to make the ammo pickups work is add a new function to the player. Open Player.gd and add the following function:

func add_ammo(additional_ammo):
    if (current_weapon_name != "UNARMED"):
        if (weapons[current_weapon_name].CAN_REFILL == true):
            weapons[current_weapon_name].spare_ammo += weapons[current_weapon_name].AMMO_IN_MAG * additional_ammo

Let's go over what this function does.

The first thing we check is whether the player is UNARMED. Because UNARMED does not have a node/script, we want to make sure the player is not UNARMED before trying to get the node/script attached to current_weapon_name.

Next, we check to see if the current weapon can be refilled. If the current weapon can, we add a full clip/magazine worth of ammo to the weapon by multiplying the current weapon's AMMO_IN_MAG value by however many ammo clips we're adding (additional_ammo).


With that done, you should now be able to get additional ammo! Go place some ammo pickups in one/both/all of the scenes and give it a try!

Bemerkung

Notice how we're not limiting the amount of ammo you can carry. To limit the amount of ammo each weapon can carry, you need to add an additional variable to each weapon's script, and then clamp the weapon's spare_ammo variable after adding ammo in add_ammo.

Hinzufügen zerbrechlicher Ziele

Bevor wir diesen Teil beenden, fügen wir einige Ziele hinzu.

Öffnen Sie Target.tscn und sehen Sie sich die Szenen im Szenenbaum an.

Beachten Sie zunächst, dass wir kein RigidBody Node, sondern ein StaticBody Node verwenden. Der Grund dafür ist, dass unsere nicht gebrochenen Ziele sich nirgendwo bewegen werden; die Verwendung eines RigidBody wäre mehr Aufwand als es wert ist, da er nur stillstehen muss.

Tipp

Wir sparen auch ein kleines bisschen Leistung ein, wenn wir einen StaticBody anstatt einen RigidBody verwenden.

Die andere Sache, die es zu beachten gilt, ist, dass wir ein Node namens Broken_Target_Holder haben. Dieses Node wird eine gespawnte/instanzierte Szene namens Broken_Target.tscn enthalten. Öffnen Sie Broken_Target.tscn.

Notice how the target is broken up into five pieces, each a 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.

Solange Sie noch Broken_Target.tscn geöffnet haben, hängen Sie RigidBody_hit_test.gd an alle RigidBody Nodes an. Dadurch wird erreicht, dass der Spieler auf die zerbrochenen Stücke schießen kann und diese auf die Kugeln reagieren.

In Ordnung, wechseln Sie nun zurück zu Target.tscn, wählen den Node Target StaticBody aus und erstellen ein neues Skript mit dem Namen Target.gd.

Füge den folgenden Code zu Target.gd hinzu:

extends StaticBody

const TARGET_HEALTH = 40
var current_health = 40

var broken_target_holder

# The collision shape for the target.
# NOTE: this is for the whole target, not the pieces of the target.
var target_collision_shape

const TARGET_RESPAWN_TIME = 14
var target_respawn_timer = 0

export (PackedScene) var destroyed_target

func _ready():
    broken_target_holder = get_parent().get_node("Broken_Target_Holder")
    target_collision_shape = $Collision_Shape


func _physics_process(delta):
    if target_respawn_timer > 0:
        target_respawn_timer -= delta

        if target_respawn_timer <= 0:

            for child in broken_target_holder.get_children():
                child.queue_free()

            target_collision_shape.disabled = false
            visible = true
            current_health = TARGET_HEALTH


func bullet_hit(damage, bullet_transform):
    current_health -= damage

    if current_health <= 0:
        var clone = destroyed_target.instance()
        broken_target_holder.add_child(clone)

        for rigid in clone.get_children():
            if rigid is RigidBody:
                var center_in_rigid_space = broken_target_holder.global_transform.origin - rigid.global_transform.origin
                var direction = (rigid.transform.origin - center_in_rigid_space).normalized()
                # Apply the impulse with some additional force (I find 12 works nicely).
                rigid.apply_impulse(center_in_rigid_space, direction * 12 * damage)

        target_respawn_timer = TARGET_RESPAWN_TIME

        target_collision_shape.disabled = true
        visible = false

Lass uns durchgehen, was dieses Skript tut, beginnend mit den Klassenvariablen:

  • TARGET_HEALTH: Die Schadensmenge, die benötigt wird, um ein vollständig heiles Ziel zu zerstören.
  • current_health: Die Menge an Gesundheit, die dieses Ziel derzeit hat.
  • broken_target_holder: Eine Variable, um das `Broken_Target_Holder Node zu halten, damit wir es leicht benutzen können.
  • target_collision_shape: Eine Variable, welche die CollisionShape für das nicht zerstörte Ziel enthält.
  • TARGET_RESPAWN_TIME: Die Zeitspanne in Sekunden, die ein Ziel benötigt, um wieder zu repawnen.
  • target_respawn_timer: Eine Variable, um zu verfolgen, wie lange ein Ziel schon zerstört ist.
  • destroyed_target: A Packed Scene, um die Szene mit dem zerstörten Ziel zu halten.

Notice how we're using an exported variable (a PackedScene) to get the broken target scene instead of using preload. By using an exported variable, we can choose the scene from the editor, and if we need to use a different scene, it's as easy as selecting a different scene in the editor; we don't need to go to the code to change the scene we're using.


Schauen wir uns _ready an.

Das Erste, was wir tun, ist, das zerbrochene Ziel zu holen und ihn dem broken_target_holder zuzuordnen. Beachten Sie, wie wir hier get_parent().get_node() anstelle von $ benutzen. Wenn Sie $` benutzen wollen, müssten Sie get_parent().get_node() in $"../Broken_Target_Holder" ändern.

Bemerkung

At the time of when this was written, I did not realize you can use $"../NodeName" to get the parent nodes using $, which is why get_parent().get_node() is used instead.

Next, we get the collision shape and assign it to target_collision_shape. The reason we need the collision shape is because even when the mesh is invisible, the collision shape will still exist in the physics world. This makes it so the player could interact with a non-broken target even though it's invisible, which is not what we want. To get around this, we will disable/enable the collision shape as we make the mesh visible/invisible.


Als nächstes wollen wir uns _physics_process ansehen.

We're only going to be using _physics_process for respawning, and so the first thing we do is check to see if target_respawn_timer is greater than 0.

Wenn das der Fall ist, dann subtrahieren wir davon delta.

Then we check to see if target_respawn_timer is 0 or less. The reason behind this is since we just removed delta from target_respawn_timer, if it's 0 or less, then the target just got here, effectively allowing us to do whatever we need to do when the timer is finished.

In diesem Fall wollen wir das Ziel respawnen.

The first thing we do is remove all children in the broken target holder. We do this by iterating over all of the children in broken_target_holder and free them using queue_free.

Als nächstes aktivieren wir die Kollisionsform, indem wir ihren disabled Wert auf false setzen.

Dann machen wir das Ziel und alle seine Kinder wieder sichtbar.

Schliesslich setzen wir den Gesundheitszustand des Ziels (current_health) auf TARGET_HEALTH zurück.


Lass uns zum Schluss einen Blick auf bullet_hit werfen.

Als erstes ziehen wir den Schaden, den die Kugel anrichtet, von der Gesundheit des Ziels ab.

Als Nächstes prüfen wir, ob das Ziel bei 0 Gesundheit oder niedriger liegt. Wenn das der Fall ist, ist das Ziel gerade gestorben, und wir müssen ein zerbrochenes Ziel spawnen.

Zuerst instanzieren wir eine neue zerstörte Zielszene und weisen sie einer neuen Variablen zu, einem clone.

Next we add the clone as a child of the broken target holder.

For bonus effect, we want to make all the target pieces explode outwards. To do this, we iterate over all the children in clone.

Für jeden Unter-Node prüfen wir zunächst, ob es sich um ein RigidBody Node handelt. Wenn dies der Fall ist, berechnen wir dann die Mittelposition des Ziels relativ zum untergeordneten Node. Dann finden wir heraus, in welche Richtung dieser untergeordnete Node relativ zum Zentrum liegt. Unter Verwendung dieser berechneten Variablen schieben wir den Unter-Node vom berechneten Zentrum in die Richtung weg vom Zentrum, wobei wir den Schaden des Geschosses als Kraft verwenden.

Bemerkung

Wir multiplizieren den Schaden mit 12, damit er eine dramatischere Wirkung hat. Sie können dies auf einen höheren oder niedrigeren Wert ändern, je nachdem, wie explosiv Sie Ihre Ziele zerschlagen wollen.

Als nächstes setzen wir den Respawn-Timer des Ziels. Wir setzen den Timer auf TARGET_RESPAWN_TIME, so dass TARGET_RESPAWN_TIME Sekunden benötigt werden, bis das Ziel respawned ist.

Dann deaktivieren wir die Kollisionsform des nicht zerbrochenen Ziels und stellen die Sichtbarkeit des Ziels auf false.


Warnung

Stellen Sie sicher, dass der exportierte destroyed_target Wert für Target.tscn im Editor gesetzt ist! Andernfalls werden die Ziele nicht zerstört und Sie erhalten einen Fehler!

Wenn das erledigt ist, platzieren Sie einige Target.tscn Instanzen in einer/beiden/allen Leveln. Sie sollten feststellen, dass sie in fünf Teile explodieren, nachdem sie genug Schaden genommen haben. Nach einer Weile werden sie wieder zu einem ganzen Ziel zusammengesetzt.

Schlussbemerkungen

../../../_images/PartFourFinished.png

Jetzt können Sie ein Joypad benutzen, mit dem Scrollrad der Maus die Waffen wechseln, Ihre Lebenspunkte und Munition aufstocken und mit Ihren Waffen Ziele zerstören.

Im nächsten Teil, Teil 5, werden wir unserem Spieler Granaten hinzufügen, unserem Spieler die Möglichkeit geben, Objekte zu greifen und zu werfen, und Geschütztürme hinzufügen!

Warnung

Wenn Sie jemals die Orientierung verlieren, lesen Sie den Code unbedingt noch einmal durch!

Sie können das fertige Projekt für diesen Teil hier herunterladen: Godot_FPS_Teil_4.zip