第4部分

零件概述

在这一部分中,我们将添加健康拾音器、弹药拾音器、玩家可以摧毁的目标、对游戏手柄的支持以及使用滚动轮更换武器的能力。

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

注解

你应该已经完成了 第3部分 在继续本教程的这一部分之前。完成的项目来自 第3部分 将是第4部分的开始项目

我们开始吧!

添加操纵手柄输入

注解

在Godot中,任何游戏控制器都被称为游戏手柄。这包括:控制台控制器、操纵杆(如用于飞行模拟器)、轮子(如用于驾驶模拟器)、虚拟现实控制器等等!

首先,我们需要更改项目输入图中的一些内容。打开项目设置并选择 Input Map 标签。

现在我们需要在我们的各种动作中添加一些控制面板按钮。单击加号图标并选择 Joy Button .

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

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

  • 运动冲刺: Device 0, Button 4 (L, L1)

  • 火: Device 0, Button 0 (PS Cross, XBox A, Nintendo B)

  • 重新加载: Device 0, Button 0 (PS Square, XBox X, Nintendo Y)

  • 手电筒: Device 0, Button 12 (D-Pad Up)

  • 移动武器: Device 0, Button 15 (D-Pad Right)

  • 移动武器负: Device 0, Button 14 (D-Pad Left)

  • 火榴弹: Device 0, Button 1 (PS Circle, XBox B, Nintendo A).

注解

如果您下载了Starter资产,这些已经为您设置了

一旦您对输入满意,关闭项目设置并保存。


现在让我们开门 Player.gd 并添加操纵手柄输入。

首先,我们需要定义一些新的类变量。将以下类变量添加到 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_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控制器基于操作系统具有不同的操纵杆轴映射,因此我们将使用基于操作系统的不同轴。

警告

本教程假设您使用的是Xbox 360或PlayStation有线控制器。此外,我(目前)没有访问Mac计算机的权限,因此操纵杆轴可能需要更改。如果有,请在Godot文档库中打开一个Github问题!谢谢!

接下来,我们检查操纵手柄向量长度是否在 JOYPAD_DEADZONE 半径。如果是的话,我们就定了 joypad_vec 到空向量2。如果不是这样的话,我们使用一个比例径向死区来精确计算死区。

注解

你可以在这里找到一篇很好的文章来解释如何处理操纵手柄/控制器死区:http://www.third-helix.com/2013/04/12/doing-thumbstick-dead-zones-right.html

我们使用的是本文中提供的缩放径向死区代码的翻译版本。这篇文章读得很好,我强烈建议你看看!

最后,我们补充说 joypad_vecinput_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问题!谢谢!

然后我们解释了游戏手柄的死区,就像 process_input .

然后,我们旋转 rotation_helper 和球员的 KinematicBody 使用 joypad_vec .

注意处理旋转播放器和 rotation_helper 与中的代码完全相同 _input . 我们所做的就是更改要使用的值 joypad_vecJOYPAD_SENSITIVITY .

注解

由于Windows上有一些与鼠标相关的错误,我们无法将鼠标旋转 process_view 也。一旦这些错误被修复,这很可能会被更新以将鼠标旋转放置在此处。 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 :单个滚动动作增加鼠标滚动值的程度


现在,我们将以下内容添加到 _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_UPBUTTON_WHEEL_DOWN 索引。根据它是向上还是向下,我们加上或减去 MOUSE_SENSITIVITY_SCROLL_WHEEL 收件人/发件人 mouse_scroll_value .

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

然后我们检查玩家是否在更换武器或重新装载。如果球员两个都不做,我们回合 mouse_scroll_value 把它扔给 int .

注解

我们在铸造 mouse_scroll_valueint 所以我们可以把它作为字典中的一个键。如果我们把它作为一个浮点,当我们试图运行这个项目时会得到一个错误。

接下来,我们检查武器名称是否在 round_mouse_scroll_value 不等于使用的当前武器名称 WEAPON_NUMBER_TO_NAME . 如果武器与玩家当前的武器不同,我们指定 changing_weapon_name ,集合 changing_weapontrue 所以玩家将在 process_changing_weapon 并设置 mouse_scroll_valueround_mouse_scroll_value .

小技巧

我们设置的原因 mouse_scroll_value 对于圆形滚动值,是因为我们不希望播放机将鼠标滚轮保持在两个值之间,从而使它们能够以几乎极快的速度切换。通过分配 mouse_scroll_valueround_mouse_scroll_value 我们确保每件武器需要完全相同的滚动量来改变。


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

mouse_scroll_value = weapon_change_number

现在,滚动值将随键盘输入而改变。如果我们不改变这个,滚动值将不同步。如果滚轮不同步,向前或向后滚动不会转换到下一个/最后一个武器,而是转换到下一个/最后一个武器。


现在你可以用滚轮换武器了!去试一试吧!

添加健康信息

既然玩家有了健康和弹药,我们理想地需要一种方法来补充这些资源。

打开 Health_Pickup.tscn .

展开 Holder 如果它还没有扩展。注意我们有两个空间节点,一个叫做 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基础 并向下滚动到“导出”部分,以获取可以使用的导出提示列表。


让我们来看一看 _ready

首先,我们将 body_entered 信号来自 Health_Pickup_Triggertrigger_body_entered 功能。这使得任何进入 Area 触发 trigger_body_entered 功能。

接下来,我们开始 is_readytrue 所以我们可以使用 setget 功能。

然后我们用 kit_size_change_values . 第一个参数是工具包的大小,而第二个参数是启用还是禁用该大小的碰撞形状和网格。

然后我们只显示我们选择的套件大小,调用 kit_size_change_values 然后传进来 kit_sizetrue ,所以尺寸为 kit_size 启用。


下一步让我们看看 kit_size_change .

我们首先要做的是检查 is_readytrue .

如果 is_readytrue ,然后我们制作已经分配给 kit_size 禁用使用 kit_size_change_values ,传球 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 .

此函数可能有点令人困惑;请尝试这样想:我们正在为启用/禁用适当的节点 size 使用 enabled . 这是因为我们无法为不可见的大小获取健康信息,因此只有适当大小的网格才可见。


最后,让我们看看 trigger_body_entered .

我们要做的第一件事是检查刚刚输入的主体是否有一个方法/函数调用 add_health . 如果有,我们会打电话 add_health 并传递当前套件大小提供的健康信息。

然后我们开始 respawn_timerRESPAWN_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)

您可能已经注意到,此代码看起来几乎与健康信息收集完全相同。那是因为它在很大程度上是相同的!只有几件事发生了变化,这就是我们要讨论的。

首先,注意更改 AMMO_AMOUNTSHEALTH_AMMOUNTS . AMMO_AMOUNTS 将是多少弹药夹/弹匣皮卡添加到当前武器。(不同于 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 没有节点/脚本,我们要确保播放机没有 UNARMED 在尝试将节点/脚本附加到 current_weapon_name .

接下来,我们检查一下当前的武器是否可以再装满。如果当前武器可以,我们会通过乘以当前武器的 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 答: 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_timer0 或者更少。这背后的原因是因为我们刚搬走 deltatarget_respawn_timer ,如果是的话 0 或者更少,然后目标就到达了这里,有效地允许我们在计时器完成时做我们需要做的任何事情。

在这种情况下,我们要重新部署目标。

我们要做的第一件事就是把坏掉的目标架上的所有子节点都移走。我们通过遍历 broken_target_holder 然后用 queue_free .

接下来,我们通过设置碰撞形状 disabled 布尔到 false .

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

最后,我们重置目标的健康 (current_healthTARGET_HEALTH .


最后,让我们看看 bullet_hit .

我们要做的第一件事就是减去子弹对目标健康造成的伤害。

接下来我们检查目标是否在 0 健康或更低。如果是,目标已经死了,我们需要产生一个破碎的目标。

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

接下来我们添加 clone 作为一个破碎目标持有者的孩子。

为了获得额外效果,我们要使所有目标碎片向外爆炸。为此,我们将遍历 clone .

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

注解

我们把损失乘以 12 所以它有一个更加戏剧化的效果。您可以将此值更改为较高或较低的值,具体取决于您希望目标破碎的程度。

接下来,我们设置目标的重生计时器。我们将计时器设置为 TARGET_RESPAWN_TIME ,所以需要 TARGET_RESPAWN_TIME 在几秒钟内,直到它重新出现。

然后禁用未破碎目标的碰撞形状,并将目标的可见性设置为 false .


警告

确保设置导出的 destroyed_target 价值观 Target.tscn 在编辑器中!否则目标不会被摧毁,你会得到一个错误!

完成后,去放一些 Target.tscn 一个/两个/所有级别中的实例。你应该发现它们在受到足够的伤害后会爆炸成五片。过了一会儿,他们会重新变成一个完整的目标。

最后的注释

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

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

在下一部分, 第5部分 我们将向玩家添加手榴弹,让玩家能够抓取和投掷物品,并添加炮塔!

警告

如果你迷路了,一定要再读一遍代码!

您可以在此处下载此部分的已完成项目: Godot_FPS_Part_4.zip