第4部分

Part overview

In this part, we will be adding health pickups, ammo pickups, targets the player can destroy, support for joypads, and add the ability to change weapons with the scroll wheel.

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

注解

在继续本教程的这一部分之前,假设您已完成 第3部分。 完成的项目来自 :ref:`doc_fps_tutorial_part_three`将成为第4部分的起始项目

让我们开始吧!

添加游戏手柄输入

注解

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!

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.

现在我们需要为我们的各种动作添加一些游戏手柄按钮。 单击加号图标,然后选择``欢乐按钮``。

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

随意使用您想要的任何按钮布局。 确保所选设备设置为``0``。 在完成的项目中,我们将使用以下内容:

  • movement_sprint:设备0,按钮4(L,L1)
  • fire:设备0,按钮0(PS Cross,XBox A,Nintendo B)
  • 重新加载:设备0,按钮0(PS Square,XBox X,Nintendo Y)
  • 手电筒:设备0,按钮12(D-Pad Up)
  • shift_weapon_positive:设备0,按钮15(D-Pad右)
  • shift_weapon_negative:设备0,按钮14(D-Pad Left)
  • fire_grenade:设备0,按钮1(PS圈,XBox B,任天堂A).

注解

如果您下载了启动资源,则已为您设置了这些内容

对输入感到满意后,关闭项目设置并保存。


现在让我们打开 Player.gd 并添加joypad输入。

首先,我们需要定义一些新的类变量。 将以下类变量添加到``Player.gd``:

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

Let’s go over what each of these does:

  • JOYPAD SENSITIVITY:这是游戏手柄操纵杆移动相机的速度。
  • JOYPAD DEADZONE:游戏手柄的死区。 您可能需要根据您的游戏手柄进行调整。

注解

Many joypads jitter around a certain point. To counter this, we ignore any movement within a radius of JOYPAD_DEADZONE. If we did not ignore said movement, the camera would jitter.

此外,我们将 JOYPAD_SENSITIVITY 定义为变量而不是常量,因为我们稍后会更改它。

现在我们准备开始处理游戏手柄输入了!


In process_input, add the following code just before input_movement_vector = input_movement_vector.normalized():

Xbox Controller

PlayStation Controller

  1. # Add joypad input if one is present
  2. if Input.get_connected_joypads().size() > 0:
  3. var joypad_vec = Vector2(0, 0)
  4. if OS.get_name() == "Windows":
  5. joypad_vec = Vector2(Input.get_joy_axis(0, 0), -Input.get_joy_axis(0, 1))
  6. elif OS.get_name() == "X11":
  7. joypad_vec = Vector2(Input.get_joy_axis(0, 1), Input.get_joy_axis(0, 2))
  8. elif OS.get_name() == "OSX":
  9. joypad_vec = Vector2(Input.get_joy_axis(0, 1), Input.get_joy_axis(0, 2))
  10. if joypad_vec.length() < JOYPAD_DEADZONE:
  11. joypad_vec = Vector2(0, 0)
  12. else:
  13. joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))
  14. input_movement_vector += joypad_vec
  1. # Add joypad input if one is present
  2. if Input.get_connected_joypads().size() > 0:
  3. var joypad_vec = Vector2(0, 0)
  4. if OS.get_name() == "Windows" or OS.get_name() == "X11":
  5. joypad_vec = Vector2(Input.get_joy_axis(0, 0), -Input.get_joy_axis(0, 1))
  6. elif OS.get_name() == "OSX":
  7. joypad_vec = Vector2(Input.get_joy_axis(0, 1), Input.get_joy_axis(0, 2))
  8. if joypad_vec.length() < JOYPAD_DEADZONE:
  9. joypad_vec = Vector2(0, 0)
  10. else:
  11. joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))
  12. input_movement_vector += joypad_vec

让我们回顾一下我们正在做的事情。

Firstly, we check to see if there is a connected joypad.

如果连接了一个游戏手柄,我们就可以获得左/右和左/上的左摇杆轴。 由于有线Xbox 360控制器具有基于OS的不同操纵杆轴映射,因此我们将基于OS使用不同的轴。

警告

This tutorial assumes you are using a XBox 360 or a PlayStation 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! Thanks!

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.

注解

您可以在这里找到一篇很棒的文章解释如何处理游戏手柄/控制器死区:http://www.third-helix.com/2013/04/12/doing-thumbstick-dead-zones-right.html__

我们正在使用该文章中提供的缩放径向死区代码的翻译版本。 这篇文章很精彩,我强烈建议您看看!

最后,我们将 joypad_vec 添加到 input_movement_vector

小技巧

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!


创建一个名为 process_view_input 的新函数并添加以下内容:

Xbox Controller

PlayStation Controller

  1. func process_view_input(delta):
  2. if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
  3. return
  4. # NOTE: Until some bugs relating to captured mice are fixed, we cannot put the mouse view
  5. # rotation code here. Once the bug(s) are fixed, code for mouse view rotation code will go here!
  6. # ----------------------------------
  7. # Joypad rotation
  8. var joypad_vec = Vector2()
  9. if Input.get_connected_joypads().size() > 0:
  10. if OS.get_name() == "Windows":
  11. joypad_vec = Vector2(Input.get_joy_axis(0, 2), Input.get_joy_axis(0, 3))
  12. elif OS.get_name() == "X11":
  13. joypad_vec = Vector2(Input.get_joy_axis(0, 3), Input.get_joy_axis(0, 4))
  14. elif OS.get_name() == "OSX":
  15. joypad_vec = Vector2(Input.get_joy_axis(0, 3), Input.get_joy_axis(0, 4))
  16. if joypad_vec.length() < JOYPAD_DEADZONE:
  17. joypad_vec = Vector2(0, 0)
  18. else:
  19. joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))
  20. rotation_helper.rotate_x(deg2rad(joypad_vec.y * JOYPAD_SENSITIVITY))
  21. rotate_y(deg2rad(joypad_vec.x * JOYPAD_SENSITIVITY * -1))
  22. var camera_rot = rotation_helper.rotation_degrees
  23. camera_rot.x = clamp(camera_rot.x, -70, 70)
  24. rotation_helper.rotation_degrees = camera_rot
  25. # ----------------------------------
  1. func process_view_input(delta):
  2. if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
  3. return
  4. # NOTE: Until some bugs relating to captured mice are fixed, we cannot put the mouse view
  5. # rotation code here. Once the bug(s) are fixed, code for mouse view rotation code will go here!
  6. # ----------------------------------
  7. # Joypad rotation
  8. var joypad_vec = Vector2()
  9. if Input.get_connected_joypads().size() > 0:
  10. if OS.get_name() == "Windows" or OS.get_name() == "X11":
  11. joypad_vec = Vector2(Input.get_joy_axis(0, 2), Input.get_joy_axis(0, 3))
  12. elif OS.get_name() == "OSX":
  13. joypad_vec = Vector2(Input.get_joy_axis(0, 3), Input.get_joy_axis(0, 4))
  14. if joypad_vec.length() < JOYPAD_DEADZONE:
  15. joypad_vec = Vector2(0, 0)
  16. else:
  17. joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))
  18. rotation_helper.rotate_x(deg2rad(joypad_vec.y * JOYPAD_SENSITIVITY))
  19. rotate_y(deg2rad(joypad_vec.x * JOYPAD_SENSITIVITY * -1))
  20. var camera_rot = rotation_helper.rotation_degrees
  21. camera_rot.x = clamp(camera_rot.x, -70, 70)
  22. rotation_helper.rotation_degrees = camera_rot
  23. # ----------------------------------

让我们回顾一下发生的事情:

Firstly, we check the mouse mode. If the mouse mode is not MOUSE_MODE_CAPTURED, we want to return, which will skip the code below.

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.

警告

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!

然后我们考虑了joypad的死区,就像在 process_input 中一样。

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

注意处理旋转游戏角色和 rotation_helper 的代码与 _input 中的代码完全相同。 我们所做的就是更改值以使用 joypad_vecJOYPAD_SENSITIVITY

注解

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.

最后,我们夹住相机的旋转,这样游戏角色就不会颠倒过来。


我们需要做的最后一件事是将 process_view_input 添加到 _physics_process 中。

一旦 process_view_input 被添加到 _physics_process ,您应该能够使用游戏手柄玩!

注解

我决定不使用游戏手柄来触发,因为我们必须做更多的轴管理,因为我更喜欢使用肩部按钮来开火。

如果您想使用触发器进行触发,您需要在 process_input 中改变触发的工作方式。 您需要获取触发器的轴值,并检查它是否超过某个值,例如“0.8”。 如果是,则添加与按下 fire 动作时相同的代码。

添加鼠标滚轮输入

Let’s add one more input related feature before we start working on the pickups and the target. Let’s add the ability to change weapons using the scroll wheel on the mouse.

打开 Player.gd 并添加以下类变量:

  1. var mouse_scroll_value = 0
  2. const MOUSE_SENSITIVITY_SCROLL_WHEEL = 0.08

让我们回顾一下这些新变量将要做的事情:

  • mouse_scroll_value:鼠标滚轮的值。
  • MOUSE_SENSITIVITY_SCROLL_WHEEL:单个滚动操作增加了多少mouse_scroll_value

现在让我们将以下内容添加到 _input 中:

  1. if event is InputEventMouseButton and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
  2. if event.button_index == BUTTON_WHEEL_UP or event.button_index == BUTTON_WHEEL_DOWN:
  3. if event.button_index == BUTTON_WHEEL_UP:
  4. mouse_scroll_value += MOUSE_SENSITIVITY_SCROLL_WHEEL
  5. elif event.button_index == BUTTON_WHEEL_DOWN:
  6. mouse_scroll_value -= MOUSE_SENSITIVITY_SCROLL_WHEEL
  7. mouse_scroll_value = clamp(mouse_scroll_value, 0, WEAPON_NUMBER_TO_NAME.size() - 1)
  8. if changing_weapon == false:
  9. if reloading_weapon == false:
  10. var round_mouse_scroll_value = int(round(mouse_scroll_value))
  11. if WEAPON_NUMBER_TO_NAME[round_mouse_scroll_value] != current_weapon_name:
  12. changing_weapon_name = WEAPON_NUMBER_TO_NAME[round_mouse_scroll_value]
  13. changing_weapon = true
  14. mouse_scroll_value = round_mouse_scroll_value

让我们回顾一下这里发生的事情:

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.

Next, we clamp mouse scroll value to ensure it is inside the range of selectable weapons.

然后我们检查游戏角色是在换武器还是重装。 如果游戏角色两者都没做,我们将 mouse_scroll_value 舍入并将其转换为 int

注解

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.

小技巧

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.


我们需要改变的另一件事是 process_input 。 在更改武器的代码中,在“changing_weapon = true”行之后添加以下内容:

  1. 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.


现在您可以使用滚轮更换武器了! 去试试吧!

Adding the health pickups

既然游戏角色拥有健康和弹药,我们理想情况下需要一种补充这些资源的方法。

打开 Health_Pickup.tscn

如果尚未展开,请展开 Holder 。 注意我们如何有两个Spatial节点,一个叫做 Health_Kit ,另一个称为 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.

选择 Health_Pickup 并添加一个名为 Health_Pickup.gd 的新脚本。 添加以下内容:

  1. extends Spatial
  2. export (int, "full size", "small") var kit_size = 0 setget kit_size_change
  3. # 0 = full size pickup, 1 = small pickup
  4. const HEALTH_AMOUNTS = [70, 30]
  5. const RESPAWN_TIME = 20
  6. var respawn_timer = 0
  7. var is_ready = false
  8. func _ready():
  9. $Holder/Health_Pickup_Trigger.connect("body_entered", self, "trigger_body_entered")
  10. is_ready = true
  11. kit_size_change_values(0, false)
  12. kit_size_change_values(1, false)
  13. kit_size_change_values(kit_size, true)
  14. func _physics_process(delta):
  15. if respawn_timer > 0:
  16. respawn_timer -= delta
  17. if respawn_timer <= 0:
  18. kit_size_change_values(kit_size, true)
  19. func kit_size_change(value):
  20. if is_ready:
  21. kit_size_change_values(kit_size, false)
  22. kit_size = value
  23. kit_size_change_values(kit_size, true)
  24. else:
  25. kit_size = value
  26. func kit_size_change_values(size, enable):
  27. if size == 0:
  28. $Holder/Health_Pickup_Trigger/Shape_Kit.disabled = !enable
  29. $Holder/Health_Kit.visible = enable
  30. elif size == 1:
  31. $Holder/Health_Pickup_Trigger/Shape_Kit_Small.disabled = !enable
  32. $Holder/Health_Kit_Small.visible = enable
  33. func trigger_body_entered(body):
  34. if body.has_method("add_health"):
  35. body.add_health(HEALTH_AMOUNTS[kit_size])
  36. respawn_timer = RESPAWN_TIME
  37. kit_size_change_values(kit_size, false)

让我们回顾一下这个脚本正在做什么,从它的类变量开始:

  • kit_size: The size of the health pickup. Notice how we’re using a setget function to tell if it’s changed.
  • HEALTH_AMMOUNTS: The amount of health each pickup in each size contains.
  • 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:一个变量,用于跟踪是否已调用 _ready 函数。

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.

小技巧

请参阅 :ref:`doc_GDScript`并向下滚动到Exports部分,以获取可以使用的导出提示列表。


让我们来看看``_ready``:

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.

然后我们只选择我们选择的工具包大小,调用 kit_size_change_values 并传入 kit_sizetrue ,这样就可以启用 kit_size 的大小。


接下来让我们看一下 kit_size_change

我们要做的第一件事就是检查 is_ready 是否为 true

如果 is_readytrue ,那么我们使用 kit_size_change_values 制作已经分配给 kit_size 的任何工具包,传入 kit_sizefalse

然后我们将 kit_size 分配给传入的新值 value 。 然后我们再次调用 kit_size_change_values 传递 kit_size ,但这次使用第二个参数作为 true ,所以我们启用它。 因为我们将 kit_size 更改为传入的值,这将使得任何工具包大小都可见。

如果 is_ready 不是 true ,我们只需将 kit_size 分配给传入的 value


现在让我们来看看 kit_size_change_values

The first thing we do is check to see which size was passed in. Based on which size we want to enable/disable, we want to get different nodes.

我们得到对应于 size 的节点的碰撞形状,并根据参数/变量中传递的 enabled 禁用它。

注解

为什么我们使用 !enable 而不是 enable ? 当我们说要启用节点时,我们可以传入 true ,但是因为 CollisionShape 使用禁用而不是启用,我们需要翻转它。 通过翻转它,我们可以启用碰撞形状,并在传入“true”时使网格可见。

然后我们得到正确的 Spatial 节点保存网格并将其可见性设置为 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.


最后,让我们看一下 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.

然后我们将 respawn_timer 设置为 RESPAWN_TIME ,这样游戏角色必须等待游戏角色再次恢复健康状态。 最后,调用 kit_size_change_values ,传入 kit_sizefalse ,这样 kit_size 的工具包是不可见的,直到它等待足够长的时间重新生成。


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

打开 Player.gd 并添加以下类变量:

  1. const MAX_HEALTH = 150
  • MAX_HEALTH:游戏角色可以拥有的最大健康状态。

现在我们需要将“add_health”函数添加到游戏角色中。 将以下内容添加到``Player.gd``:

  1. func add_health(additional_health):
  2. health += additional_health
  3. health = clamp(health, 0, MAX_HEALTH)

让我们快点回顾一下这个问题。

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.

Adding the ammo pickups

While adding health is good and all, we can’t reap the rewards from adding it since nothing can (currently) damage us. Let’s add some ammo pickups next!

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.

选择 Ammo_Pickup 并添加一个名为 Ammo_Pickup.gd 的新脚本。 添加以下内容:

  1. extends Spatial
  2. export (int, "full size", "small") var kit_size = 0 setget kit_size_change
  3. # 0 = full size pickup, 1 = small pickup
  4. const AMMO_AMOUNTS = [4, 1]
  5. const RESPAWN_TIME = 20
  6. var respawn_timer = 0
  7. var is_ready = false
  8. func _ready():
  9. $Holder/Ammo_Pickup_Trigger.connect("body_entered", self, "trigger_body_entered")
  10. is_ready = true
  11. kit_size_change_values(0, false)
  12. kit_size_change_values(1, false)
  13. kit_size_change_values(kit_size, true)
  14. func _physics_process(delta):
  15. if respawn_timer > 0:
  16. respawn_timer -= delta
  17. if respawn_timer <= 0:
  18. kit_size_change_values(kit_size, true)
  19. func kit_size_change(value):
  20. if is_ready:
  21. kit_size_change_values(kit_size, false)
  22. kit_size = value
  23. kit_size_change_values(kit_size, true)
  24. else:
  25. kit_size = value
  26. func kit_size_change_values(size, enable):
  27. if size == 0:
  28. $Holder/Ammo_Pickup_Trigger/Shape_Kit.disabled = !enable
  29. $Holder/Ammo_Kit.visible = enable
  30. elif size == 1:
  31. $Holder/Ammo_Pickup_Trigger/Shape_Kit_Small.disabled = !enable
  32. $Holder/Ammo_Kit_Small.visible = enable
  33. func trigger_body_entered(body):
  34. if body.has_method("add_ammo"):
  35. body.add_ammo(AMMO_AMOUNTS[kit_size])
  36. respawn_timer = RESPAWN_TIME
  37. 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:

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

让我们回顾一下这个功能的作用。

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.

接下来我们检查当前的武器是否可以重新填充。 如果当前的武器可以,我们通过将当前武器的 AMMO_IN_MAG 变量乘以额外的弹夹数目(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!

注解

要注意的是,我们并没有对你携带弹药的数量进行限制。如果想要限制每件武器可携带的弹药数量,您需要在每件武器的脚本内添加一个额外变量,然后限定武器的“备用弹药”(spare_ammo)变量后,在“添加弹药”(add_ammo)变量内添加弹药。

添加易碎目标

在我们结束这一部分之前,让我们添加一些目标。

打开 Target.tscn 并查看场景树中的场景。

Firstly, notice how we’re not using a RigidBody node, but a StaticBody one. The reason behind this is our non-broken targets will not be moving anywhere; using a RigidBody would be more hassle than it’s worth since all it has to do is stay still.

小技巧

我们还使用了:ref:StaticBody <class_StaticBody> 代替 :ref:`RigidBody <class_RigidBody>`来节省一点性能。

另外需要注意的是我们有一个名为 Broken_Target_Holder 的节点。 该节点将保存一个名为 Broken_Target.tscn 的衍生/实例化场景。 打开 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.

While you still have Broken_Target.tscn open, attach RigidBody_hit_test.gd to all of the RigidBody nodes. This will make it so the player can shoot at the broken pieces and they will react to the bullets.

好吧,现在切换回 Target.tscn ,选择``Target`` StaticBody 节点并创建一个名为 Target.gd 的新脚本。

将以下代码添加到``Target.gd``:

  1. extends StaticBody
  2. const TARGET_HEALTH = 40
  3. var current_health = 40
  4. var broken_target_holder
  5. # The collision shape for the target.
  6. # NOTE: this is for the whole target, not the pieces of the target.
  7. var target_collision_shape
  8. const TARGET_RESPAWN_TIME = 14
  9. var target_respawn_timer = 0
  10. export (PackedScene) var destroyed_target
  11. func _ready():
  12. broken_target_holder = get_parent().get_node("Broken_Target_Holder")
  13. target_collision_shape = $Collision_Shape
  14. func _physics_process(delta):
  15. if target_respawn_timer > 0:
  16. target_respawn_timer -= delta
  17. if target_respawn_timer <= 0:
  18. for child in broken_target_holder.get_children():
  19. child.queue_free()
  20. target_collision_shape.disabled = false
  21. visible = true
  22. current_health = TARGET_HEALTH
  23. func bullet_hit(damage, bullet_transform):
  24. current_health -= damage
  25. if current_health <= 0:
  26. var clone = destroyed_target.instance()
  27. broken_target_holder.add_child(clone)
  28. for rigid in clone.get_children():
  29. if rigid is RigidBody:
  30. var center_in_rigid_space = broken_target_holder.global_transform.origin - rigid.global_transform.origin
  31. var direction = (rigid.transform.origin - center_in_rigid_space).normalized()
  32. # Apply the impulse with some additional force (I find 12 works nicely).
  33. rigid.apply_impulse(center_in_rigid_space, direction * 12 * damage)
  34. target_respawn_timer = TARGET_RESPAWN_TIME
  35. target_collision_shape.disabled = true
  36. visible = false

让我们回顾一下这个脚本的作用,从类变量开始:

  • TARGET_HEALTH:打破完全治疗目标所需的伤害量。
  • current_health:此目标目前的健康状况。
  • broken_target_holder:一个变量,用于保存 Broken_Target_Holder 节点,以便我们可以轻松使用它。
  • target_collision_shape:一个变量,用于保存 CollisionShape 用于未破坏的目标。
  • TARGET_RESPAWN_TIME:目标重生的时间长度(以秒为单位)。
  • target_respawn_timer:一个跟踪目标被破坏时间的变量。
  • destroyed_target:A PackedScene 来保存破碎的目标场景。

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.


让我们看看`_ready``。

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 wanted to use $, then you’d need to change get_parent().get_node() to $"../Broken_Target_Holder".

注解

在写这篇文章时,我没有意识到您可以使用``$“../ NodeName”``来使用``$``来获取父节点,这就是为什么``get_parent()。get_node( )``用来代替。

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.


接下来让我们看一下 _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 greater than 0.

If it is, we then subtract delta from it.

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.

在这种情况下,我们想重新生成目标。

我们要做的第一件事是移除破碎的目标持有者中的所有儿童。 我们通过遍历 broken_target_holder 中的所有子节点并使用 queue_free 释放它们来完成此操作。

Next, we enable the collision shape by setting its disabled boolean to false.

然后我们再次使目标及其所有子节点可见。

最后,我们将目标的健康状况(current_health)重置为``TARGET_HEALTH``。


最后,让我们看一下 bullet_hit

The first thing we do is subtract however much damage the bullet does from the target’s health.

接下来我们检查目标是否处于“0”健康状态或更低。 如果是,目标刚刚死亡,我们需要产生一个破碎的目标。

我们首先实例化一个新的被破坏的目标场景,并将其分配给一个新变量,即 clone

接下来,我们将 clone 添加为已损坏目标持有者的子项。

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

对于每个子节点,我们首先检查它是否是 RigidBody 节点。 如果是,我们然后计算目标相对于子节点的中心位置。 然后我们计算出子节点相对于中心的方向。 使用这些计算出的变量,我们将子弹从计算中心推向远离中心的方向,使用子弹的损伤作为力。

注解

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 explosively you want your targets to shatter.

Next, we set the target’s respawn timer. We set the timer to TARGET_RESPAWN_TIME, so it takes TARGET_RESPAWN_TIME in seconds until it is respawned.

然后我们禁用非破坏目标的碰撞形状,并将目标的可见性设置为“假”。


警告

确保在编辑器中为 Target.tscn 设置导出的 destroyed_target 值! 否则目标将不会被销毁,您将收到错误!

完成后,在一个/两个/所有级别中放置一些 Target.tscn 实例。 您会发现他们在受到足够的伤害后会爆炸成五件。 过了一会儿,他们会再次重生成一个整体目标。

最后的笔记

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

现在您可以使用游戏手柄,用鼠标的滚轮更换武器,补充您的健康和弹药,并用您的武器打破目标。

在下一部分中, 第5部分,我们将为我们的游戏角色添加手榴弹,让我们的游戏角色能够抓住并投掷物体,并添加炮塔!

警告

如果您迷路了,请务必再次阅读代码!

您可以在这里下载这个部分的完成项目: Godot_FPS_Part_4.zip