第4部分¶
添加操纵手柄输入¶
注解
在Godot中,任何游戏控制器都被称为游戏手柄。这包括:控制台控制器、操纵杆(如用于飞行模拟器)、轮子(如用于驾驶模拟器)、虚拟现实控制器等等!
首先,我们需要更改项目输入图中的一些内容。打开项目设置并选择 Input Map
标签。
现在我们需要在我们的各种动作中添加一些控制面板按钮。单击加号图标并选择 Joy Button
.
您可以随意使用任何按钮布局。确保所选设备设置为 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_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问题!谢谢!
然后我们解释了游戏手柄的死区,就像 process_input
.
然后,我们旋转 rotation_helper
和球员的 KinematicBody 使用 joypad_vec
.
注意处理旋转播放器和 rotation_helper
与中的代码完全相同 _input
. 我们所做的就是更改要使用的值 joypad_vec
和 JOYPAD_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_UP
或 BUTTON_WHEEL_DOWN
索引。
如果事件的索引确实是一个按钮轮索引,那么我们将检查它是否是一个 BUTTON_WHEEL_UP
或 BUTTON_WHEEL_DOWN
索引。根据它是向上还是向下,我们加上或减去 MOUSE_SENSITIVITY_SCROLL_WHEEL
收件人/发件人 mouse_scroll_value
.
接下来,我们钳制鼠标滚动值以确保它在可选择武器的范围内。
然后我们检查玩家是否在更换武器或重新装载。如果球员两个都不做,我们回合 mouse_scroll_value
把它扔给 int
.
注解
我们在铸造 mouse_scroll_value
对 int
所以我们可以把它作为字典中的一个键。如果我们把它作为一个浮点,当我们试图运行这个项目时会得到一个错误。
接下来,我们检查武器名称是否在 round_mouse_scroll_value
不等于使用的当前武器名称 WEAPON_NUMBER_TO_NAME
. 如果武器与玩家当前的武器不同,我们指定 changing_weapon_name
,集合 changing_weapon
到 true
所以玩家将在 process_changing_weapon
并设置 mouse_scroll_value
到 round_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
如果它还没有扩展。注意我们有两个空间节点,一个叫做 Health_Kit
还有一个叫 Health_Kit_Small
.
这是因为我们实际上要生产两种尺寸的健康皮卡,一种是小的,一种是大的/普通的。 Health_Kit
和 Health_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_Trigger
到 trigger_body_entered
功能。这使得任何进入 Area 触发 trigger_body_entered
功能。
接下来,我们开始 is_ready
到 true
所以我们可以使用 setget
功能。
然后我们用 kit_size_change_values
. 第一个参数是工具包的大小,而第二个参数是启用还是禁用该大小的碰撞形状和网格。
然后我们只显示我们选择的套件大小,调用 kit_size_change_values
然后传进来 kit_size
和 true
,所以尺寸为 kit_size
启用。
下一步让我们看看 kit_size_change
.
我们首先要做的是检查 is_ready
是 true
.
如果 is_ready
是 true
,然后我们制作已经分配给 kit_size
禁用使用 kit_size_change_values
,传球 kit_size
和 false
.
然后我们分配 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_timer
到 RESPAWN_TIME
所以玩家必须等待,才能恢复健康。最后,打电话 kit_size_change_values
,传球 kit_size
和 false
所以工具包在 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_AMOUNTS
从 HEALTH_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_timer
是 0
或者更少。这背后的原因是因为我们刚搬走 delta
从 target_respawn_timer
,如果是的话 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
在几秒钟内,直到它重新出现。
然后禁用未破碎目标的碰撞形状,并将目标的可见性设置为 false
.
警告
确保设置导出的 destroyed_target
价值观 Target.tscn
在编辑器中!否则目标不会被摧毁,你会得到一个错误!
完成后,去放一些 Target.tscn
一个/两个/所有级别中的实例。你应该发现它们在受到足够的伤害后会爆炸成五片。过了一会儿,他们会重新变成一个完整的目标。