第4部分

部分概述

在这部分, 我们将增加健康拾取, 弹药拾取, 玩家可以摧毁的目标, 支持手柄, 并增加用滚轮更换武器的功能.

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

注解

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

让我们开始吧!

添加游戏手柄输入

注解

在Godot中, 任何游戏控制器都被称为游戏手柄(joypad). 这包括: 控制台控制器, 操纵杆(Joysticks )(如飞行模拟器), 车轮(如用于驾驶模拟器),VR控制器等等!

首先, 我们需要在项目的键位映射中更改一些内容. 打开项目设置并选择 键位映射(Input Map) 选项卡.

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

../../../_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:

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

让我们回顾一下这些做法:

  • JOYPAD SENSITIVITY: 这是游戏手柄操纵杆移动相机的速度.

  • JOYPAD DEADZONE: 游戏手柄的死区. 您可能需要根据您的游戏手柄进行调整.

注解

许多游戏手柄在某一点上抖动. 为了解决这个问题, 我们忽略半径为JOYPAD_DEADZONE内的任何移动. 如果我们不忽略所说的运动, 相机就会抖动.

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

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


process_input 中添加以下代码, 就在 input_movement_vector = input_movement_vector.normalized() 之前:

# 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

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

首先, 我们检查是否有连接的游戏手柄.

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

警告

本教程假设您使用的是XBox 360或Playstation有线控制器. 此外, 我目前没有访问Mac计算机, 因此操纵杆轴可能需要更改. 如果有, 请在Godot文档存储库中打开GitHub issue ! 谢谢!

接下来, 我们检查joypad向量长度是否在 JOYPAD_DEZONE 半径内. 如果是, 我们将 joypad_vec 设置为一个空的Vector2. 如果不在范围内, 使用一个按比例的径向死区来精确计算死区.

注解

您可以在这里找到一篇很棒的文章解释如何处理游戏手柄/控制器死区: 这里.

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

最后, 我们将 joypad_vec 添加到 input_movement_vector .

小技巧

还记得我们如何归一化 input_movement_vector 吗?这就是原因!如果我们不对 input_movement_vector 进行归一化处理, 如果玩家用键盘和游戏手柄向同一个方向推进, 就会使移动变得更快!


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

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
   # ----------------------------------

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

首先我们检查鼠标模式. 如果鼠标模式不是 MOUSE_MODE_CAPTURED , 我们想要返回, 将跳过下面的代码.

接下来我们定义一个新的 Vector2 命名为 joypad_vec . 这将保持正确的操纵杆位置. 基于操作系统, 我们设置其值, 使其映射到右侧操纵杆的正确轴.

警告

如上所述, 我(目前)没有访问Mac苹果计算机, 因此操纵杆轴可能需要更改. 如果有, 请在Godot文档存储库中打开GitHub问题! 谢谢!

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

然后, 我们使用 joypad_vec 旋转 rotation_helper 和玩家的 KinematicBody .

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

注解

由于Windows上一些与鼠标有关的错误, 我们不能将鼠标旋转也放在 process_view 中. 一旦这些bug被修复, 我们可能会更新, 将鼠标旋转也放在 process_view_input 中.

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


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

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

注解

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

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

添加鼠标滚轮输入

在开始研究拾取器和目标之前, 我们再增加一个与输入相关的功能. 增加使用鼠标上的滚轮更换武器的功能.

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

var mouse_scroll_value = 0
const MOUSE_SENSITIVITY_SCROLL_WHEEL = 0.08

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

  • mouse_scroll_value: 鼠标滚轮的值.

  • MOUSE_SENSITIVITY_SCROLL_WHEEL: 单个滚动操作增加了多少mouse_scroll_value


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

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

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

首先, 我们检查该事件是否是 InputEventMouseButton 事件, 鼠标模式是否是 MOUSE_MODE_CAPTURED . 然后, 我们检查按钮索引是否是 BUTTON_WHEEL_UPBUTTON_WHEEL_DOWN .

如果事件的索引确实是按钮轮索引, 我们再检查它是 BUTTON_WHEEL_UP 还是 BUTTON_WHEEL_DOWN . 根据它是向上还是向下, 我们向 mouse_scroll_value 加减 mouse_SENSITIVITY_SCROLL_WHEEL .

接下来, 我们限制鼠标滚动值, 确保它在可选择的武器范围内.

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

注解

我们把 mouse_scroll_value 转为 int , 这样就可以把它作为字典中的一个键. 如果把它作为一个浮点数, 当我们试图运行项目时, 会得到一个错误.

接下来, 我们用 WEAPON_NUMBER_TO_NAME 检查 round_mouse_scroll_value 处的武器名称是否与当前的武器名称不相等. 如果武器与玩家当前的武器不同, 我们就分配 changing_weapon_name , 设置 changing_weapontrue , 这样玩家就会在 process_changing_weapon 中更换武器, 并设置 mouse_scroll_valueround_mouse_scroll_value .

小技巧

我们之所以将 mouse_scroll_value 设置为四舍五入的滚动值, 是因为我们不希望玩家的鼠标滚轮仅仅停留在数值之间, 从而使它们能够快速切换. 通过将 mouse_scroll_value 分配给 round_mouse_scroll_value , 我们可以保证每把武器需要完全相同的滚动量来改变.


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

mouse_scroll_value = weapon_change_number

现在滚动值将随着键盘输入而改变. 如果我们不改变这个, 滚动值就会不同步, 向前或向后滚动将不会过渡到下一个或最后一个武器, 而是滚轮改变的下一个或最后一个武器.


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

增加健康拾取

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

打开 Health_Pickup.tscn .

如果尚未展开, 请展开 Holder . 注意我们如何有两个Spatial节点, 一个叫做 Health_Kit , 另一个称为 Health_Kit_Small .

这是因为我们实际上要做两种尺寸的健康拾音, 一种是小的, 一种是大的和正常的. Health_KitHealth_Kit_Small 只有一个 MeshInstance 作为它们的子级.

接下来展开 Health_Pickup_Trigger . 这是一个 Area 节点, 我们要用它来检查玩家是否已经走到足够近的地方去捡健康装备. 如果你展开它, 会发现两个碰撞形状, 每个尺寸都有一个. 我们将根据健康拾取器的大小使用不同的碰撞形状大小, 较小的健康拾取器有一个更接近其大小的触发碰撞形状.

最后要注意的是, 我们有一个 AnimationPlayer 节点, 所以健康装备会慢慢地摆动和旋转.

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

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)

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

  • kit_size: 健康拾取器的大小. 请注意, 我们是如何使用 setget 函数来判断它是否发生了变化.

  • HEALTH_AMMOUNTS : 每种尺寸的拾取物所含的健康量.

  • RESPAWN_TIME : 回复拾取品重生所需的时间(以秒为单位)

  • respawn_timer: 一个变量, 用于追踪回复拾取物重生所剩的时间.

  • is_ready: 一个变量, 用于跟踪是否已调用 _ready 函数.

我们使用 is_ready 是因为 setget 函数在 _ready 之前被调用;我们需要忽略第一个kit_size_change调用, 因为在 _ready 被调用之前, 我们不能访问子节点. 如果不忽略第一个 setget 的调用, 会在调试器中获得几个错误.

另外, 请注意我们是如何使用一个导出的变量. 这是为了让我们可以在编辑器中改变健康拾取器的大小. 这使得我们不必为两种尺寸制作两个场景, 因为我们可以很容易地在编辑器中使用导出的变量改变尺寸.

小技巧

请参阅 GDScript 基础 并向下滚动到Exports部分, 以获取可以使用的导出提示列表.


让我们来看看 _ready:

首先, 我们将 Health_Pickup_Triggerbody_entered 信号连接到 trigger_body_entered 函数. 这样, 任何进入 Area 的物体都会触发 trigger_body_entered 函数.

接下来, 我们将 is_ready 设置为 true , 这样我们就可以使用 setget 函数.

然后我们使用 kit_size_change_values 隐藏所有可能的套件和它们的碰撞形状. 第一个参数是套件的大小, 第二个参数是启用或禁用该大小的碰撞形状和网格.

然后我们只选择我们选择的工具包大小, 调用 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 .

我们要做的第一件事就是检查一下传入的是哪个大小. 根据我们要启用或禁用的大小, 希望获得不同的节点.

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

注解

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

然后我们得到正确的 Spatial 节点保存网格并将其可见性设置为 enable .

这个功能可能会让人有点困惑, 试着这样想. 我们使用 enabled 启用或禁用 size 的适当节点. 这是为了让我们不能为一个不可见的尺寸拾取健康, 所以只有适当尺寸的网格才会可见.


最后, 让我们看一下 trigger_body_entered .

我们要做的第一件事是检查刚刚进入的物体是否有一个叫做 add_health 的方法或函数. 如果有, 我们就调用 add_health , 并将当前套件大小提供的健康状况传入.

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


在玩家使用这个健康拾取器之前, 我们需要做的最后一件事就是在 Player.gd 中添加一些东西.

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

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

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

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

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

我们首先将 additional_health 加到玩家的当前健康值上. 然后控制健康值使其不能高于 MAX_HEALTH , 也不能低于 0 .


做好了这些, 玩家就可以收集健康了!去在周围放置几个 Health_Pickup 的场景, 然后试一试. 当选择了 Health_Pickup 实例场景时, 你可以在编辑器中通过一个方便的下拉菜单来改变健康拾取器的大小.

添加弹药拾取器

虽然增加健康是好事, 但是我们不能从增加健康中获得奖励, 因为目前没有什么东西可以伤害我们. 接下来让我们增加一些弹药拾取吧!

打开 Ammo_Pickup.tscn . 请注意它的结构与 Health_Pickup.tscn 完全相同, 但网格和触发碰撞形状略有改变, 以考虑到网格大小的不同.

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

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)

你可能已经注意到这段代码看起来和健康拾取几乎一模一样. 那是因为它基本上是一样的!只有一些东西被改变了, 这就是我们要介绍的.

首先, 请注意将 HEALTH_AMMOUNTS 改为 AMMO_AMOUNTS . AMMO_AMOUNTS 是指拾取时给当前武器增加多少弹药夹或弹夹.(与 HEALTH_AMMOUNTS 不同的是, HEALTH_AMMOUNTS 代表的是将获得多少健康点数, 我们将为当前武器增加整个弹夹, 而不是原始弹药量)

唯一要注意的是在 trigger_body_entered 中. 我们正在检查是否存在并调用名为 add_ammo 而不是 add_health 的函数.

除了这两处小改动, 其他的都和健康拾取一样!


为了让弹药拾取器运作, 我们需要做的是在播放器中添加一个新的函数. 打开 Player.gd 并添加以下函数:

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

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

我们首先检查的是玩家是否是 UNARMED . 因为 UNARMED 没有节点或脚本, 所以在尝试获取 current_weapon_name 的节点或脚本之前, 我们要确定玩家不是 UNARMED .

接下来我们检查当前的武器是否可以重新填充. 如果当前的武器可以, 我们通过将当前武器的 AMMO_IN_MAG 变量乘以额外的弹夹数目(additional_ammo), 来为武器添加完整的弹夹/弹仓所容纳的弹药量.


做好了这些, 你现在应该可以获得额外的弹药了!去在一个或两个或所有的场景中放置一些弹药拾取器, 然后试一试吧!

注解

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

添加易碎目标

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

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

首先, 请注意我们没有使用 RigidBody 节点, 而是使用 StaticBody 节点. 原因是我们的不可破碎目标不会移动到任何地方;使用一个 RigidBody 会比它更麻烦, 因为它要做的就是保持不动.

小技巧

我们还使用了 StaticBody 代替 RigidBody 来节省一点性能.

另外需要注意的是我们有一个名为 Broken_Target_Holder 的节点. 该节点将保存一个名为 Broken_Target.tscn 的衍生/实例化场景. 打开 Broken_Target.tscn .

请注意目标是如何被分解成五块的, 每一块都是一个 RigidBody 节点. 当目标受到太多伤害需要被摧毁时, 我们将生成和实例化这个场景. 然后, 将隐藏不可破碎的目标, 所以看起来像是目标破碎了, 而不是一个破碎的目标被生成和实例化.

当你保持打开 Broken_Target.tscn 时, 将 RigidBody_hit_test.gd 附加到所有的 RigidBody 节点. 这将使玩家可以向碎块射击, 它们会对子弹做出反应.

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

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

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

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

  • TARGET_HEALTH: 打破完全治疗目标所需的伤害量.

  • current_health: 此目标目前的健康状况.

  • broken_target_holder: 一个变量, 用于保存 Broken_Target_Holder 节点, 以便我们可以轻松使用它.

  • target_collision_shape: 一个变量, 用于保存 CollisionShape 用于未破坏的目标.

  • TARGET_RESPAWN_TIME: 目标重生的时间长度(以秒为单位).

  • target_respawn_timer: 一个跟踪目标被破坏时间的变量.

  • destroyed_target:A PackedScene 来保存破碎的目标场景.

注意我们是如何使用一个导出的变量(a PackedScene)来获取破碎的目标场景, 而不是使用 preload . 通过使用导出的变量, 我们可以从编辑器中选择场景, 如果需要使用不同的场景, 就像在编辑器中选择不同的场景一样简单, 不需要到代码中去改变使用的场景.


让我们看看 _ready .

我们要做的第一件事是获取破碎的目标持有者, 并将其分配给 broken_target_holder . 注意在这里使用的是 get_parent().get_node() , 而不是 $ . 如果你想使用 $ , 那么需要将 get_parent().get_node() 改为 $ ".../Broken_Target_Holder" .

注解

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

接下来, 我们得到碰撞形状, 并将其分配给 target_collision_shape . 我们需要碰撞形状的原因是, 即使网格是不可见的, 碰撞形状仍然会存在于物理世界中. 这使得玩家可以与一个非破碎的目标进行交互, 即使它是不可见的, 这不是我们想要的. 为了解决这个问题, 我们将在使网格可见或不可见时禁用或启用碰撞形状.


接下来让我们看一下 _physics_process .

我们只使用 _physics_process 进行重新生成, 所以我们要做的第一件事就是检查 target_respawn_timer 是否大于 0 .

如果是, 我们就从中减去 delta .

然后我们检查 target_respawn_timer 是否为 0 或更少. 原因是由于刚刚从 target_respawn_timer 中减除了 delta , 如果它是 0 或更少, 那么目标刚到这里, 这实际上使我们能够在定时器完成后做任何我们需要做的事情.

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

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

接下来, 我们通过将其 disabled 布尔值设置为 false 来启用碰撞形状.

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

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


最后, 让我们看一下 bullet_hit .

首先我们要做的就是从目标的健康状况中减去子弹造成的伤害值.

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

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

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

对于奖励效果, 我们希望使所有目标碎片向外爆炸. 为此, 我们遍历 clone 中的所有子节点.

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

注解

我们将伤害乘以 12 , 因此效果更为显著. 您可以将其更改为更高或更低的值, 具体取决于您希望目标破碎的程度.

接下来, 我们设置目标的重新生成计时器. 将定时器设置为 TARGET_RESPAWN_TIME , 所以它需要 TARGET_RESPAWN_TIME 秒, 才能重新生成.

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


警告

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

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

最后的笔记

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

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

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

警告

如果你感到迷茫,请一定要再读一遍代码!

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