第5部分
部分概述
在这部分,我们要给玩家增加手榴弹,让玩家拥有抓取和投掷物体的能力,并添加炮塔!
注解
在继续本教程的这一部分之前,我们假设您已经完成了 第4部分. 完成的项目来自 :ref:`doc_fps_tutorial_part_four`将成为第5部分的起始项目
让我们开始吧!
添加手榴弹
首先,我们给玩家一些手榴弹,打开 Grenade.tscn
.
这里有几件事要注意,首先是手榴弹要使用 RigidBody 节点.我们要为手榴弹使用 RigidBody 节点,这样它们就会以有种表现现实的方式在世界范围内弹跳.
第二点需要注意的是 Blast_Area
. 这是一个 Area 节点,它代表手榴弹的爆炸半径.
最后,要注意的是 Explosion
.这是 Particles 节点,当手雷爆炸时,会发出爆炸效果.需注意,我们启用了 One shot
,是为了一次发射所有的粒子.粒子使用世界坐标而不是局部坐标发射的,所以没有选中 Local Coords
.
注解
如果需要,您可以通过查看粒子的”过程材质”和”绘制过程”来查看粒子是如何设置的.
让我们编写手榴弹所需的代码. 选择 Grenade
并制作一个名为 Grenade.gd
的新脚本. 添加以下内容:
extends RigidBody
const GRENADE_DAMAGE = 60
const GRENADE_TIME = 2
var grenade_timer = 0
const EXPLOSION_WAIT_TIME = 0.48
var explosion_wait_timer = 0
var rigid_shape
var grenade_mesh
var blast_area
var explosion_particles
func _ready():
rigid_shape = $Collision_Shape
grenade_mesh = $Grenade
blast_area = $Blast_Area
explosion_particles = $Explosion
explosion_particles.emitting = false
explosion_particles.one_shot = true
func _process(delta):
if grenade_timer < GRENADE_TIME:
grenade_timer += delta
return
else:
if explosion_wait_timer <= 0:
explosion_particles.emitting = true
grenade_mesh.visible = false
rigid_shape.disabled = true
mode = RigidBody.MODE_STATIC
var bodies = blast_area.get_overlapping_bodies()
for body in bodies:
if body.has_method("bullet_hit"):
body.bullet_hit(GRENADE_DAMAGE, body.global_transform.looking_at(global_transform.origin, Vector3(0, 1, 0)))
# This would be the perfect place to play a sound!
if explosion_wait_timer < EXPLOSION_WAIT_TIME:
explosion_wait_timer += delta
if explosion_wait_timer >= EXPLOSION_WAIT_TIME:
queue_free()
让我们回顾一下正在发生的事情,从类变量开始:
GRENADE DAMAGE
:手榴弹爆炸时造成的伤害量.GRENADE_TIME
:手榴弹在创建/抛出后爆炸所需的时间(以秒为单位).grenade_timer
:一个变量,用于跟踪手榴弹被创建/抛出的时间.EXPLOSION_WAIT_TIME
:爆炸后我们摧毁手榴弹场景所需的等待时间(以秒为单位)explosion_wait_timer
:一个变量,用于跟踪自手榴弹爆炸以来已经过了多少时间.rigid_shape
: CollisionShape 用于手榴弹 RigidBody.grenade_mesh
:手榴弹的参考 MeshInstance .blast_area
:爆炸 Area 用于在手榴弹爆炸时损坏东西.explosion_particles
: Particles ,当手榴弹爆炸时产生.
注意 EXPLOSION_WAIT_TIME
是一个相当奇怪的数字(0.48
). 这是因为我们希望 EXPLOSION_WAIT_TIME
等于爆炸粒子发射的时间长度,所以当粒子完成时我们会摧毁/释放手榴弹. 我们通过获取粒子的生命时间并将其除以粒子的速度刻度来计算”EXPLOSION_WAIT_TIME”. 这让我们得到了爆炸粒子持续的确切时间.
现在让我们把注意力转向 _ready
.
首先,我们得到需要的所有节点,并将它们分配到合适的类变量中.
我们需要得到 CollisionShape 和 MeshInstance 因为类似于 :ref:`doc_fps_tutorial_part_four`中的目标,我们将隐藏手榴弹的网格并禁用碰撞形状 手榴弹爆炸了.
我们需要获得爆炸的原因 Area 这样我们可以在手榴弹爆炸时损坏其内部的一切. 我们将使用与游戏角色中的刀代码类似的代码. 我们需要 Particles 所以我们可以在手榴弹爆炸时发射粒子.
当我们得到所有的节点并将它们分配给类变量后,确保爆炸粒子不会发射,并且它们被设置为一次发射.这是为了确保粒子会以我们期望的方式运行.
现在让我们来看看 _process
.
首先,我们检查 grenade_timer
是否小于 GRENADE_TIME
.如果是,我们加上 delta
并返回.这是为了让手榴弹在爆炸前必须等待 GRENADE_TIME
秒,让 RigidBody 移动.
如果 grenade_timer
位于 GRENADE_TIMER
或更高,那么我们需要检查手榴弹是否等待了足够长的时间并且需要爆炸. 我们通过检查 explosion_wait_timer
是否等于 0
或更少来做到这一点. 因为我们将立即将 delta
添加到 explosion_wait_timer
,所以检查下的任何代码只会被调用一次,就在手榴弹等待足够长并且需要爆炸时.
如果手榴弹已经等待足够长的时间爆炸,我们首先告诉”爆炸_粒子”发射. 然后我们使 grenade_mesh
不可见,并禁用 rigid_shape
,有效地隐藏了手榴弹.
然后我们将 RigidBody 的模式设置为 MODE_STATIC
,这样手榴弹就不会移动了.
然后我们得到 blast_area
中的所有尸体,检查它们是否有 bullet_hit
方法或函数,如果有,我们调用它,并传入 GRENADE_DAMAGE
和从尸体查看手榴弹的变换.这样就使得被手榴弹爆炸的尸体会从手榴弹的位置向外爆炸.
然后我们检查 explosion_wait_timer
是否小于 EXPLOSION_WAIT_TIME
.如果小于,就在 explosion_wait_timer
上加上 delta
.
接下来,我们检查 explosion_wait_timer
是否大于或等于 EXPLOSION_WAIT_TIME
.因为我们添加了 delta
,所以只调用一次.如果 explosion_wait_timer
大于或等于 EXPLOSION_WAIT_TIME
,说明手榴弹已经等待了足够长的时间,让 Particles 播放,可以释放或销毁手榴弹,因为不再需要它了.
让我们快速设置粘性手榴弹. 打开 Sticky_Grenade.tscn
.
Sticky_Grenade.tscn``几乎与``Grenade.tscn``相同,只有一个小的补充. 我们现在有第二个 :ref:`Area <class_Area>`,称为 ``Sticky_Area
. 我们将使用”Stick_Area”来检测粘性手榴弹何时与环境相撞并需要粘在某物上.
选择 Sticky_Grenade
并制作一个名为 Sticky_Grenade.gd
的新脚本. 添加以下内容:
extends RigidBody
const GRENADE_DAMAGE = 40
const GRENADE_TIME = 3
var grenade_timer = 0
const EXPLOSION_WAIT_TIME = 0.48
var explosion_wait_timer = 0
var attached = false
var attach_point = null
var rigid_shape
var grenade_mesh
var blast_area
var explosion_particles
var player_body
func _ready():
rigid_shape = $Collision_Shape
grenade_mesh = $Sticky_Grenade
blast_area = $Blast_Area
explosion_particles = $Explosion
explosion_particles.emitting = false
explosion_particles.one_shot = true
$Sticky_Area.connect("body_entered", self, "collided_with_body")
func collided_with_body(body):
if body == self:
return
if player_body != null:
if body == player_body:
return
if attached == false:
attached = true
attach_point = Spatial.new()
body.add_child(attach_point)
attach_point.global_transform.origin = global_transform.origin
rigid_shape.disabled = true
mode = RigidBody.MODE_STATIC
func _process(delta):
if attached == true:
if attach_point != null:
global_transform.origin = attach_point.global_transform.origin
if grenade_timer < GRENADE_TIME:
grenade_timer += delta
return
else:
if explosion_wait_timer <= 0:
explosion_particles.emitting = true
grenade_mesh.visible = false
rigid_shape.disabled = true
mode = RigidBody.MODE_STATIC
var bodies = blast_area.get_overlapping_bodies()
for body in bodies:
if body.has_method("bullet_hit"):
body.bullet_hit(GRENADE_DAMAGE, body.global_transform.looking_at(global_transform.origin, Vector3(0, 1, 0)))
# This would be the perfect place to play a sound!
if explosion_wait_timer < EXPLOSION_WAIT_TIME:
explosion_wait_timer += delta
if explosion_wait_timer >= EXPLOSION_WAIT_TIME:
if attach_point != null:
attach_point.queue_free()
queue_free()
上面的代码几乎与 Grenade.gd
的代码完全相同,所以让我们回顾一下已经改变的代码.
首先,我们还有几个类变量:
附加
:一个变量,用于跟踪粘性手榴弹是否附加到 PhysicsBody.attach_point
: 一个变量,用于保存 Spatial ,将位于粘性手榴弹碰撞的位置.player_body
:游戏角色的 KinematicBody.
它们添加,是为了让粘性手雷能够粘在任何可能击中的 PhysicsBody 上.我们现在还需要玩家的 KinematicBody ,这样当玩家投掷手雷时,粘性手雷就不会粘在玩家身上.
现在让我们来看看 _ready
中的小变化. 在 _ready
中我们添加了一行代码,因此当任何物体进入 Stick_Area
时,会调用 collided_with_body
函数.
接下来让我们来看看 collided_with_body
.
首先,我们要确保粘性手榴弹不与自己发生碰撞.因为粘性的 Area 不知道自己是附着在手榴弹的 RigidBody 上的,所以我们需要通过检查是否与自己碰撞的主体不是自己,来确保它不会粘到自己身上.如果我们与自己相撞了,就通过返回来忽略它.
然后我们检查一下是否有东西分配给 player_body
,如果粘手榴弹碰撞的物体是投掷它的游戏角色. 如果粘手榴弹碰到的物体确实是 player_body
,我们会通过返回来忽略它.
接下来,我们检查粘性手雷是否已经附着在某物上.
如果手榴弹被接触了,我们将 attached 设置为 true ,这样我们就知道粘性手榴弹附着在某物上.
然后我们创建一个新的 Spatial 节点,并使其成为粘性手榴弹与之碰撞的物体的子节点. 然后我们将 Spatial 的位置设置为粘性手榴弹当前的全球位置.
注解
因为我们已经添加了 Spatial 作为粘性手榴弹碰撞的物体的一个子节点,它将跟随所述物体. 然后我们可以使用它 Spatial 来设置粘性手榴弹的位置,因此它总是在相对于它碰撞的物体的相同位置.
然后我们禁用 rigid_shape
,这样粘手榴弹就不会一直移动它碰到的任何物体. 最后,我们将模式设置为”MODE_STATIC”,因此手榴弹不会移动.
最后,让我们回顾一下 _process
中的一些变化.
现在我们正在检查粘性手榴弹是否附在 _process
的顶部.
如果连接了粘性手榴弹,我们确保附加的点不等于 null
. 如果附加的点不等于 null
,我们将粘性手榴弹的全局位置(使用其global Transform 的原点)设置为 Spatial 赋值给 attach_point
(使用它的global Transform 的原点).
在我们释放/销毁粘性手榴弹之前,现在唯一的另一个变化是检查粘性手榴弹是否有附着点. 如果是这样,我们也在连接点上调用 queue_free
,因此它也被释放/销毁.
向游戏角色添加手榴弹
现在我们需要在 Player.gd
中添加一些代码,以便我们可以使用手榴弹.
首先,打开 Player.tscn
并展开节点树,直到您找到 Rotation_Helper
. 注意在 Rotation_Helper
中我们有一个名为 Grenade_Toss_Pos
的节点. 我们将在这里生成手榴弹.
还要注意它是如何在”X”轴上轻微旋转的,它不是直指,而是略微向上. 通过改变”Grenade_Toss_Pos”的旋转,您可以改变投掷手榴弹的角度.
好的,现在让我们开始让手榴弹与游戏角色一起工作. 将以下类变量添加到``Player.gd``:
var grenade_amounts = {"Grenade":2, "Sticky Grenade":2}
var current_grenade = "Grenade"
var grenade_scene = preload("res://Grenade.tscn")
var sticky_grenade_scene = preload("res://Sticky_Grenade.tscn")
const GRENADE_THROW_FORCE = 50
grenade_amounts
:游戏角色当前携带的手榴弹数量(针对每种类型的手榴弹).current_grenade
:游戏角色目前正在使用的手榴弹的名称.grenade_scene
:我们之前工作的手榴弹场景.sticky_grenade_scene
:我们之前工作过的粘手榴弹场景.GRENADE_THROW_FORCE
:游戏角色投掷手榴弹的力量.
大多数这些变量与我们设置武器的方式类似.
小技巧
虽然可以制作更模块化的手榴弹系统,但我发现仅仅两枚手榴弹的额外复杂性是不值得的. 如果您打算制造一个更复杂的FPS和更多的手榴弹,您可能想要建立一个类似于我们如何设置武器的手榴弹系统.
现在我们需要在 _process_input
中添加一些代码.将以下内容添加到``_process_input``:
# ----------------------------------
# Changing and throwing grenades
if Input.is_action_just_pressed("change_grenade"):
if current_grenade == "Grenade":
current_grenade = "Sticky Grenade"
elif current_grenade == "Sticky Grenade":
current_grenade = "Grenade"
if Input.is_action_just_pressed("fire_grenade"):
if grenade_amounts[current_grenade] > 0:
grenade_amounts[current_grenade] -= 1
var grenade_clone
if current_grenade == "Grenade":
grenade_clone = grenade_scene.instance()
elif current_grenade == "Sticky Grenade":
grenade_clone = sticky_grenade_scene.instance()
# Sticky grenades will stick to the player if we do not pass ourselves
grenade_clone.player_body = self
get_tree().root.add_child(grenade_clone)
grenade_clone.global_transform = $Rotation_Helper/Grenade_Toss_Pos.global_transform
grenade_clone.apply_impulse(Vector3(0, 0, 0), grenade_clone.global_transform.basis.z * GRENADE_THROW_FORCE)
# ----------------------------------
让我们回顾一下这里发生的事情.
首先,我们检查 change_grenade
动作是否刚刚被按下.如果按下了,就检查玩家当前使用的手榴弹.根据手榴弹名称,将 current_grenade
改为相反的手榴弹名称.
接下来我们检查是否刚刚按下了 fire_grenade
动作. 如果有,我们检查游戏角色是否有超过”0”的手榴弹,用于当前选择的手榴弹类型.
如果游戏角色拥有超过”0”的手榴弹,我们就会从当前手榴弹的手榴弹数量中移除一枚. 然后,根据游戏角色当前正在使用的手榴弹,我们实例化正确的手榴弹场景并将其分配给”grenade_clone”.
接下来,我们将 grenade_clone
添加为根节点的子节点,并将其global Transform 设置为 Grenade_Toss_Pos
的global Transform. 最后,我们对手榴弹施加一个冲动,使它相对于 grenade_clone
的 Z
方向向量向前发射.
现在游戏角色可以使用两种类型的手榴弹,但在我们继续添加其他东西之前,我们应该添加一些东西.
我们仍然需要一种方法向游戏角色展示剩下多少手榴弹,我们应该在游戏角色拿起弹药时增加一种获得更多手榴弹的方法.
首先,修改 Player.gd
中的一些代码,以显示还剩多少颗手榴弹.将 process_UI
改为如下:
func process_UI(delta):
if current_weapon_name == "UNARMED" or current_weapon_name == "KNIFE":
# First line: Health, second line: Grenades
UI_status_label.text = "HEALTH: " + str(health) + \
"\n" + current_grenade + ": " + str(grenade_amounts[current_grenade])
else:
var current_weapon = weapons[current_weapon_name]
# First line: Health, second line: weapon and ammo, third line: grenades
UI_status_label.text = "HEALTH: " + str(health) + \
"\nAMMO: " + str(current_weapon.ammo_in_weapon) + "/" + str(current_weapon.spare_ammo) + \
"\n" + current_grenade + ": " + str(grenade_amounts[current_grenade])
现在我们将展示游戏角色在UI中留下多少手榴弹.
当我们还在 Player.gd
时,让我们添加一个向游戏角色添加手榴弹的功能. 将以下函数添加到``Player.gd``:
func add_grenade(additional_grenade):
grenade_amounts[current_grenade] += additional_grenade
grenade_amounts[current_grenade] = clamp(grenade_amounts[current_grenade], 0, 4)
现在我们可以使用 add_grenade
添加一个手榴弹,它会自动被夹到最大的 4
手榴弹.
小技巧
如果需要,可以将”4”改为常量. 您需要创建一个新的全局常量,比如 MAX_GRENADES
,然后将钳位从``clamp(grenade_amounts [current_grenade],0,4)``更改为``clamp(grenade_amounts [current_grenade], 0,MAX_GRENADES)``
如果您不想限制游戏角色可以携带多少手榴弹,那就去掉完全夹住手榴弹的线!
现在我们有一个添加手榴弹的功能,让我们打开 AmmoPickup.gd
并使用它!
打开 AmmoPickup.gd
并转到 trigger_body_entered
函数. 将其更改为以下内容:
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)
if body.has_method("add_grenade"):
body.add_grenade(GRENADE_AMOUNTS[kit_size])
respawn_timer = RESPAWN_TIME
kit_size_change_values(kit_size, false)
现在我们还要检查主体是否有 add_grenade
函数. 如果是这样,我们称之为”add_ammo”.
您可能已经注意到我们正在使用一个尚未定义的新常量, GRENADE_AMOUNTS
. 我们加上吧! 使用其他类变量将以下类变量添加到``AmmoPickup.gd``:
const GRENADE_AMOUNTS = [2, 0]
GRENADE_AMOUNTS
: 每个拾取包含的手榴弹数量.
注意 GRENADE_AMOUNTS
中的第二个元素是 0
.这是为了让小弹药拾取器不给玩家任何额外的手榴弹.
现在您应该可以投掷手榴弹了! 去尝试吧!
添加抓取并将RigidBody节点投射到游戏角色的功能
接下来,让我们给玩家提供拾取和投掷 RigidBody 节点的能力.
打开 Player.gd
并添加以下类变量:
var grabbed_object = null
const OBJECT_THROW_FORCE = 120
const OBJECT_GRAB_DISTANCE = 7
const OBJECT_GRAB_RAY_DISTANCE = 10
grabbed_object
:一个用于保存抓取的变量 RigidBody 节点.OBJECT_THROW_FORCE
: 玩家投掷被抓住物体的力量.OBJECT_GRAB_DISTANCE
: 玩家拿着被抓住物体时离相机的距离.OBJECT_GRAB_RAY_DISTANCE
:距离 Raycast 去了. 这是游戏角色的抓地距离.
完成后,我们需要做的就是在 process_input
中添加一些代码:
# ----------------------------------
# Grabbing and throwing objects
if Input.is_action_just_pressed("fire_grenade") and current_weapon_name == "UNARMED":
if grabbed_object == null:
var state = get_world().direct_space_state
var center_position = get_viewport().size / 2
var ray_from = camera.project_ray_origin(center_position)
var ray_to = ray_from + camera.project_ray_normal(center_position) * OBJECT_GRAB_RAY_DISTANCE
var ray_result = state.intersect_ray(ray_from, ray_to, [self, $Rotation_Helper/Gun_Fire_Points/Knife_Point/Area])
if !ray_result.empty():
if ray_result["collider"] is RigidBody:
grabbed_object = ray_result["collider"]
grabbed_object.mode = RigidBody.MODE_STATIC
grabbed_object.collision_layer = 0
grabbed_object.collision_mask = 0
else:
grabbed_object.mode = RigidBody.MODE_RIGID
grabbed_object.apply_impulse(Vector3(0, 0, 0), -camera.global_transform.basis.z.normalized() * OBJECT_THROW_FORCE)
grabbed_object.collision_layer = 1
grabbed_object.collision_mask = 1
grabbed_object = null
if grabbed_object != null:
grabbed_object.global_transform.origin = camera.global_transform.origin + (-camera.global_transform.basis.z.normalized() * OBJECT_GRAB_DISTANCE)
# ----------------------------------
让我们回顾一下正在发生的事情.
首先,我们检查按下的是否是 fire
动作,以及玩家是否使用了 UNARMED
‘weapon’ .这是因为我们只希望玩家在没有使用任何武器的时候,能够捡起和投掷物体.这是一种设计上的选择,但我觉得让 UNARMED
有了用途.
接下来我们检查 grabbed_object
是否为 null
.
如果 grabbed_object
是 null
,我们想看看我们是否可以选择 RigidBody.
我们首先从当前获得直接空间状态 World. 这样我们就可以完全从代码中投射光线,而不必使用 Raycast 节点.
注解
参见 Ray-casting 了解更多关于Godot中的射线投射的信息.
然后我们通过将当前 Viewport 大小分成两半来得到屏幕的中心. 然后我们使用摄像机的 project_ray_origin
和 project_ray_normal
获取射线的原点和终点. 如果您想了解有关这些函数如何工作的更多信息,请参阅 Ray-casting.
接下来,我们将光线发送到空间状态,看看它是否得到了结果. 我们添加了游戏角色和刀子 Area 作为两个例外,因此游戏角色无法携带自己或刀的碰撞 Area.
然后我们检查是否从射线上得到了一个结果.如果没有对象与射线发生碰撞,将返回一个空的Dictionary.如果Dictionary不是空的,即至少有一个对象发生了碰撞,,我们再看看射线碰撞的碰撞器是否是一个 RigidBody .
如果光线与 RigidBody 相撞,我们将 grabbed_object
设置为光线与光线相撞的对撞机. 然后我们将模式设置为 RigidBody 我们与 MODE_STATIC
相撞,所以它不会在我们手中移动.
最后,我们将抓取的 RigidBody 的碰撞层和碰撞掩码设置为 0
. 这将使得抓住 RigidBody 没有碰撞层或掩码,这意味着只要我们拿着它就不会碰到任何东西.
注解
关于Godot碰撞掩码的更多信息,请参见 物理介绍.
如果 grabbed_object
不是 null
,那么我们需要抛出游戏角色持有的 RigidBody .
我们首先将所持有的 RigidBody 的模式设置为 MODE_RIGID
.
注解
这是一个相当大的假设,即所有刚体都将使用”MODE_RIGID”. 虽然本教程系列就是这种情况,但在其他项目中可能并非如此.
如果你有不同模式的刚体,可能需要把你拾取的 RigidBody 的模式存储到一个类变量中,这样就可以把它改回拾取它之前的模式.
然后我们施加冲动将它向前飞. 我们使用我们在 OBJECT_THROW_FORCE
变量中设置的力将它发送到相机朝向的方向.
然后我们将抓取的 RigidBody 的碰撞层和掩码设置为 1
,这样它就可以再次与层 1
上的任何东西碰撞.
注解
这又是一个相当大的假设,即所有刚体都只在碰撞层”1”上,所有碰撞掩模都在层”1”上. 如果您在其他项目中使用此脚本,您可能需要在变量中存储 RigidBody 的碰撞图层/掩码,然后将它们更改为”0”,这样您就可以 在您反转过程时为其设置的原始碰撞图层/蒙版.
最后,我们将 grabbed_object
设置为 null
,因为游戏角色已经成功抛出了被保持的对象.
我们做的最后一件事是在所有抓取/投掷相关代码之外检查 grabbed_object
是否等于 null
.
注解
虽然技术上没有输入相关,但是将代码移动到此处的代码很容易,因为它只有两行,然后所有的抓取/抛出代码都在一个地方
如果游戏角色持有一个物体,我们将其全局位置设置为相机的位置以及相机朝向的方向上的”OBJECT_GRAB_DISTANCE”.
在我们测试之前,我们需要在 _physics_process
中改变一些东西. 当游戏角色持有一个物体时,我们不希望游戏角色能够更换武器或重装,所以将 _physics_process
改为:
func _physics_process(delta):
process_input(delta)
process_view_input(delta)
process_movement(delta)
if grabbed_object == null:
process_changing_weapons(delta)
process_reloading(delta)
# Process the UI
process_UI(delta)
现在游戏角色在拿着物体时无法改变武器或重装.
现在,当您处于”UNARMED”状态时,您可以抓住并抛出RigidBody节点! 去尝试吧!
添加一个炮塔
接下来,让我们制作一个炮塔射击游戏角色!
打开 Turret.tscn
. 如果尚未展开,请展开 Turret
.
请注意炮塔是如何被分解成几个部分: Base
、 Head
、 Vision_Area
和 Smoke
Particles 节点.
打开 Base
,你会发现是一个 StaticBody 和一个网格.打开 Head
,你会发现有几个网格,一个 StaticBody 和一个 Raycast 节点.
“头部”的一个注意事项是,如果我们使用光线投射,光线投射将是炮塔射击的地方. 我们还有两个网格叫做 Flash
和 Flash_2
. 这些将是枪口闪光,简要显示炮塔开火时.
``Vision_Area``是 Area 我们将用作炮塔的能力. 当某些东西进入”Vision_Area”时,我们会认为炮塔可以看到它.
``Smoke``是一个 Particles 节点将在炮塔被摧毁和修复时播放.
现在我们已经了解了如何设置场景,让我们开始编写炮塔的代码. 选择 Turret
并创建一个名为 Turret.gd
的新脚本. 将以下内容添加到``Turret.gd``:
extends Spatial
export (bool) var use_raycast = false
const TURRET_DAMAGE_BULLET = 20
const TURRET_DAMAGE_RAYCAST = 5
const FLASH_TIME = 0.1
var flash_timer = 0
const FIRE_TIME = 0.8
var fire_timer = 0
var node_turret_head = null
var node_raycast = null
var node_flash_one = null
var node_flash_two = null
var ammo_in_turret = 20
const AMMO_IN_FULL_TURRET = 20
const AMMO_RELOAD_TIME = 4
var ammo_reload_timer = 0
var current_target = null
var is_active = false
const PLAYER_HEIGHT = 3
var smoke_particles
var turret_health = 60
const MAX_TURRET_HEALTH = 60
const DESTROYED_TIME = 20
var destroyed_timer = 0
var bullet_scene = preload("Bullet_Scene.tscn")
func _ready():
$Vision_Area.connect("body_entered", self, "body_entered_vision")
$Vision_Area.connect("body_exited", self, "body_exited_vision")
node_turret_head = $Head
node_raycast = $Head/Ray_Cast
node_flash_one = $Head/Flash
node_flash_two = $Head/Flash_2
node_raycast.add_exception(self)
node_raycast.add_exception($Base/Static_Body)
node_raycast.add_exception($Head/Static_Body)
node_raycast.add_exception($Vision_Area)
node_flash_one.visible = false
node_flash_two.visible = false
smoke_particles = $Smoke
smoke_particles.emitting = false
turret_health = MAX_TURRET_HEALTH
func _physics_process(delta):
if is_active == true:
if flash_timer > 0:
flash_timer -= delta
if flash_timer <= 0:
node_flash_one.visible = false
node_flash_two.visible = false
if current_target != null:
node_turret_head.look_at(current_target.global_transform.origin + Vector3(0, PLAYER_HEIGHT, 0), Vector3(0, 1, 0))
if turret_health > 0:
if ammo_in_turret > 0:
if fire_timer > 0:
fire_timer -= delta
else:
fire_bullet()
else:
if ammo_reload_timer > 0:
ammo_reload_timer -= delta
else:
ammo_in_turret = AMMO_IN_FULL_TURRET
if turret_health <= 0:
if destroyed_timer > 0:
destroyed_timer -= delta
else:
turret_health = MAX_TURRET_HEALTH
smoke_particles.emitting = false
func fire_bullet():
if use_raycast == true:
node_raycast.look_at(current_target.global_transform.origin + Vector3(0, PLAYER_HEIGHT, 0), Vector3(0, 1, 0))
node_raycast.force_raycast_update()
if node_raycast.is_colliding():
var body = node_raycast.get_collider()
if body.has_method("bullet_hit"):
body.bullet_hit(TURRET_DAMAGE_RAYCAST, node_raycast.get_collision_point())
ammo_in_turret -= 1
else:
var clone = bullet_scene.instance()
var scene_root = get_tree().root.get_children()[0]
scene_root.add_child(clone)
clone.global_transform = $Head/Barrel_End.global_transform
clone.scale = Vector3(8, 8, 8)
clone.BULLET_DAMAGE = TURRET_DAMAGE_BULLET
clone.BULLET_SPEED = 60
ammo_in_turret -= 1
node_flash_one.visible = true
node_flash_two.visible = true
flash_timer = FLASH_TIME
fire_timer = FIRE_TIME
if ammo_in_turret <= 0:
ammo_reload_timer = AMMO_RELOAD_TIME
func body_entered_vision(body):
if current_target == null:
if body is KinematicBody:
current_target = body
is_active = true
func body_exited_vision(body):
if current_target != null:
if body == current_target:
current_target = null
is_active = false
flash_timer = 0
fire_timer = 0
node_flash_one.visible = false
node_flash_two.visible = false
func bullet_hit(damage, bullet_hit_pos):
turret_health -= damage
if turret_health <= 0:
smoke_particles.emitting = true
destroyed_timer = DESTROYED_TIME
这是相当多的代码,所以让我们按功能分解它. 我们先来看一下类变量:
use_raycast
: 一个导出的布尔值,以便我们可以改变炮塔是使用对象还是使用射线发射子弹.TURRET_DAMAGE_BULLET
:单个子弹场景造成的伤害量.TURRET_DAMAGE_RAYCAST
:单个损坏的数量 Raycast bullet.FLASH_TIME
:枪口闪光网格的可见时间(以秒为单位).flash_timer
:一个变量,用于跟踪枪口闪光网格的可见时间.FIRE_TIME
:发射子弹所需的时间(以秒为单位).fire_timer
:一个变量,用于跟踪炮塔上次射击后经过的时间.node_turret_head
:一个用于保存Head
节点的变量.node_raycast
:一个变量,用于保存附加到炮塔头部的 Raycast 节点.node_flash_one
:一个用于保存第一个枪口flash的变量 MeshInstance.node_flash_two
:一个用于保存第二个枪口flash的变量 MeshInstance.ammo_in_turret
:目前炮塔中的弹药数量.AMMO_IN_FULL_TURRET
:完整炮塔中的弹药数量.AMMO_RELOAD_TIME
:炮塔重装的时间.ammo_reload_timer
:一个变量,用于跟踪炮塔重装的时间.current_target
:炮塔的当前目标.is_active
:用于跟踪炮塔是否能够射向目标的变量.PLAYER_HEIGHT
:我们添加到目标的高度,所以我们不会在它的脚下射击.smoke_particles
:用于保存烟雾粒子节点的变量.turret_health
:炮塔目前的健康状况.MAX_TURRET_HEALTH
:完全愈合的炮塔的健康量.DESTROYED_TIME
:被毁坏的炮塔修复自己所花费的时间(以秒为单位).destroyed_timer
:一个变量,用于跟踪炮塔被摧毁的时间.bullet_scene
:炮塔射击的子弹场景(与游戏角色的手枪相同的场景)
哇,这是相当多的类变量!
接下来我们来看看 _ready
.
首先,我们得到视觉区域,将 body_entered
和 body_exited
信号分别连接到 body_entered_vision
和 body_exited_vision
.
然后,我们得到所有的节点,并将它们分配到各自的变量中.
接下来,我们给 Raycast 添加一些例外情况,这样炮塔就不能伤害自己了.
然后我们在开始时使两个闪存网格都不可见,因为我们不会在”_ready”期间触发.
然后我们得到烟雾粒子节点,并将其分配给 smoke_particles
变量.将 emitting
设置为 false
,以确保炮塔在被破坏之前,粒子不会发射.
最后,我们将炮塔的生命值设置为”MAX_TURRET_HEALTH”,以便从完全健康状态开始.
现在让我们来看看 _physics_process
.
首先,我们检查炮塔是否处于激活状态.如果炮塔处于激活状态,则处理射击代码.
接下来,如果 flash_timer
大于0,意味着flash网格是可见的,我们要从 flash_timer
中删除delta.如果 flash_timer
减去 delta
后变为0或更小,要隐藏两个flash网格.
接下来,我们检查炮塔是否有目标.如果炮塔有目标,让炮塔的头部看向它,加上 PLAYER_HEIGHT
,这样它就不会瞄准玩家的脚.
然后我们检查炮塔的健康状况是否大于零,如果是,就检查炮塔里是否有弹药.
如果有,我们检查 fire_timer
是否大于0,如果大于,则炮塔不能发射,需要从 fire_timer
中删除 delta
.如果 fire_timer
小于或等于零,炮塔可以发射子弹,则调用 fire_bullet
函数.
如果炮塔内没有任何弹药,我们检查 ammo_reload_timer
是否大于零,如果大于零,我们从 ammo_reload_timer
中减去 delta
.如果 ammo_reload_timer
小于或等于零,我们将 ammo_in_turret
设置为 AMMO_IN_FULL_TURRET
,因为炮塔已经等待了足够长的时间来补充弹药.
接下来,我们检查炮塔的健康值是否小于或等于 0
,而不是它是否处于活动状态.如果炮塔的健康值为0或更少,我们检查 destroyed_timer
是否大于0.如果是,我们从 destroyed_timer
中减去 delta
.
如果 destroyed_timer
小于或等于零,我们将 turret_health
设置为 MAX_TURRET_HEALTH
并通过将 smoke_particles.emitting
设置为 false
来停止冒烟.
接下来让我们来看看 fire_bullet
.
首先,我们检查炮塔是否使用了射线投射.
使用射线投射的代码与 第2部分 中步枪的代码几乎完全相同,所以我只简单介绍一下.
我们首先让raycast看向目标,确保raycast在没有任何障碍物的情况下能够击中目标.然后我们强制raycast更新,这样我们就能得到一帧完美的碰撞检查.然后,检查raycast是否与任何东西发生了碰撞.如果有,我们检查被碰撞的物体是否有 bullet_hit
方法.如果有,我们就调用它,并将单颗raycast子弹造成的伤害和raycast的变换一起传递进来.然后我们从 ammo_in_turret
中减去 1
.
如果炮塔没有使用光线投射,我们会生成一个子弹对象. 这段代码几乎完全与手枪中的代码相同 第2部分,所以与光线播放代码一样,我只是简单地介绍一下.
我们首先制作一个子弹克隆,并将其分配给 clone
,然后我们将其添加为根节点的一个子节点.我们将子弹的全局变换设置为枪管末端,由于它太小,所以将其放大,并使用炮塔的常量类变量设置其伤害和速度.然后从 ammo_in_turret
中减去 1
.
然后,不管我们使用的是哪种子弹方式,都要让两个枪口闪光网格可见.我们将 flash_timer
和 fire_timer
分别设置为 FLASH_TIME
和 FIRE_TIME
.然后检查炮塔是否已经用完了最后一颗子弹.如果用完了,将 ammo_reload_timer
设置为 ammo_reload_TIME
,这样炮塔就会重新装弹.
让我们看看接下来的 body_entered_vision
,谢天谢地,它很短.
我们首先通过检查 current_target
是否等于 null
来检查炮塔当前是否有目标. 如果炮塔没有目标,我们就检查刚刚进入 Area 视野的物体是一个 KinematicBody .
注解
我们假设炮塔应该只对 KinematicBody 节点进行射击,因为玩家使用的就是这个节点.
如果刚进入视觉的主体 Area 是 KinematicBody,我们将 current_target
设置为body,并将 is_active
设置为 true
.
现在让我们来看看 body_exited_vision
.
首先,我们检查炮塔是否有目标.如果有,检查刚刚离开炮塔的视野的物体 :ref:`Area <class_Area>`是否为炮塔的目标.
如果刚刚离开视野 Area 的机体是炮塔的当前目标,将 current_target
设置为 null
,将 is_active
设置为 false
,并重置所有与发射炮塔有关的变量,因为炮塔已经没有目标可以发射.
最后,让我们看一下 bullet_hit
.
我们先从炮塔的健康状况中减去子弹造成的多少伤害.
然后,我们检查炮塔是否被摧毁,健康值为零或更少,如果被摧毁,就开始发射烟雾粒子,并将 destroyed_timer
设置为 DESTROYED_TIME
,这样炮塔在修复前就必须等待.
好了,所有这些都完成了,在炮塔准备使用之前,我们只有最后一件事要做.打开 Turret.tscn
如果还没有打开的话,从 Base
或 Head
中选择一个 StaticBody 节点.创建一个名为 TurretBodies.gd
的新脚本,并将其附加到你选择的任何一个 StaticBody 节点上.
将以下代码添加到``TurretBodies.gd``:
extends StaticBody
export (NodePath) var path_to_turret_root
func _ready():
pass
func bullet_hit(damage, bullet_hit_pos):
if path_to_turret_root != null:
get_node(path_to_turret_root).bullet_hit(damage, bullet_hit_pos)
这段代码所做的就是在 path_to_turret_root
所指向的任何节点上调用 bullet_hit
.回到编辑器,将 NodePath 分配给 Turret
节点.
现在选择另一个 StaticBody 节点(在 Body
或 Head
中)并为其指定 TurretBodies.gd
脚本. 附加脚本后,再次将 NodePath 分配给 Turret
节点.
最后,我们需要做的是给玩家增加一种伤害的方式.由于所有的子弹都使用 bullet_hit
函数,我们需要为玩家添加该函数.
打开 Player.gd
并添加以下内容:
func bullet_hit(damage, bullet_hit_pos):
health -= damage
完成所有这些后,您应该拥有完全可操作的炮塔! 在一个/两个/所有场景中进行几次尝试!
最后的笔记
现在您可以拿起 RigidBody 节点并投掷手榴弹. 我们现在也有炮塔射击游戏角色.
在 :ref:`doc_fps_tutorial_part_six`中,我们将添加一个主菜单和一个暂停菜单,为游戏角色添加重新生成的系统,以及更改/移动声音系统,以便我们可以从任何脚本中使用它.
警告
如果你感到迷茫,请一定要再读一遍代码!
您可以在这里下载这个部分的完成项目: Godot_FPS_Part_5.zip