第5部分

部分概述

在这部分,我们要给玩家增加手榴弹,让玩家拥有抓取和投掷物体的能力,并添加炮塔!

../../../_images/PartFiveFinished.png

注解

在继续本教程的这一部分之前,我们假设您已经完成了 第4部分. 完成的项目来自 :ref:`doc_fps_tutorial_part_four`将成为第5部分的起始项目

让我们开始吧!

添加手榴弹

首先,我们给玩家一些手榴弹,打开 Grenade.tscn .

这里有几件事要注意,首先是手榴弹要使用 RigidBody 节点.我们要为手榴弹使用 RigidBody 节点,这样它们就会以有种表现现实的方式在世界范围内弹跳.

第二点需要注意的是 Blast_Area. 这是一个 Area 节点,它代表手榴弹的爆炸半径.

最后,要注意的是 Explosion .这是 Particles 节点,当手雷爆炸时,会发出爆炸效果.需注意,我们启用了 One shot ,是为了一次发射所有的粒子.粒子使用世界坐标而不是局部坐标发射的,所以没有选中 Local Coords .

注解

如果需要,您可以通过查看粒子的”过程材质”和”绘制过程”来查看粒子是如何设置的.

让我们编写手榴弹所需的代码. 选择 Grenade 并制作一个名为 Grenade.gd 的新脚本. 添加以下内容:

  1. extends RigidBody
  2. const GRENADE_DAMAGE = 60
  3. const GRENADE_TIME = 2
  4. var grenade_timer = 0
  5. const EXPLOSION_WAIT_TIME = 0.48
  6. var explosion_wait_timer = 0
  7. var rigid_shape
  8. var grenade_mesh
  9. var blast_area
  10. var explosion_particles
  11. func _ready():
  12. rigid_shape = $Collision_Shape
  13. grenade_mesh = $Grenade
  14. blast_area = $Blast_Area
  15. explosion_particles = $Explosion
  16. explosion_particles.emitting = false
  17. explosion_particles.one_shot = true
  18. func _process(delta):
  19. if grenade_timer < GRENADE_TIME:
  20. grenade_timer += delta
  21. return
  22. else:
  23. if explosion_wait_timer <= 0:
  24. explosion_particles.emitting = true
  25. grenade_mesh.visible = false
  26. rigid_shape.disabled = true
  27. mode = RigidBody.MODE_STATIC
  28. var bodies = blast_area.get_overlapping_bodies()
  29. for body in bodies:
  30. if body.has_method("bullet_hit"):
  31. body.bullet_hit(GRENADE_DAMAGE, body.global_transform.looking_at(global_transform.origin, Vector3(0, 1, 0)))
  32. # This would be the perfect place to play a sound!
  33. if explosion_wait_timer < EXPLOSION_WAIT_TIME:
  34. explosion_wait_timer += delta
  35. if explosion_wait_timer >= EXPLOSION_WAIT_TIME:
  36. 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 .

首先,我们得到需要的所有节点,并将它们分配到合适的类变量中.

我们需要得到 CollisionShapeMeshInstance 因为类似于 :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 的新脚本. 添加以下内容:

  1. extends RigidBody
  2. const GRENADE_DAMAGE = 40
  3. const GRENADE_TIME = 3
  4. var grenade_timer = 0
  5. const EXPLOSION_WAIT_TIME = 0.48
  6. var explosion_wait_timer = 0
  7. var attached = false
  8. var attach_point = null
  9. var rigid_shape
  10. var grenade_mesh
  11. var blast_area
  12. var explosion_particles
  13. var player_body
  14. func _ready():
  15. rigid_shape = $Collision_Shape
  16. grenade_mesh = $Sticky_Grenade
  17. blast_area = $Blast_Area
  18. explosion_particles = $Explosion
  19. explosion_particles.emitting = false
  20. explosion_particles.one_shot = true
  21. $Sticky_Area.connect("body_entered", self, "collided_with_body")
  22. func collided_with_body(body):
  23. if body == self:
  24. return
  25. if player_body != null:
  26. if body == player_body:
  27. return
  28. if attached == false:
  29. attached = true
  30. attach_point = Spatial.new()
  31. body.add_child(attach_point)
  32. attach_point.global_transform.origin = global_transform.origin
  33. rigid_shape.disabled = true
  34. mode = RigidBody.MODE_STATIC
  35. func _process(delta):
  36. if attached == true:
  37. if attach_point != null:
  38. global_transform.origin = attach_point.global_transform.origin
  39. if grenade_timer < GRENADE_TIME:
  40. grenade_timer += delta
  41. return
  42. else:
  43. if explosion_wait_timer <= 0:
  44. explosion_particles.emitting = true
  45. grenade_mesh.visible = false
  46. rigid_shape.disabled = true
  47. mode = RigidBody.MODE_STATIC
  48. var bodies = blast_area.get_overlapping_bodies()
  49. for body in bodies:
  50. if body.has_method("bullet_hit"):
  51. body.bullet_hit(GRENADE_DAMAGE, body.global_transform.looking_at(global_transform.origin, Vector3(0, 1, 0)))
  52. # This would be the perfect place to play a sound!
  53. if explosion_wait_timer < EXPLOSION_WAIT_TIME:
  54. explosion_wait_timer += delta
  55. if explosion_wait_timer >= EXPLOSION_WAIT_TIME:
  56. if attach_point != null:
  57. attach_point.queue_free()
  58. 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``:

  1. var grenade_amounts = {"Grenade":2, "Sticky Grenade":2}
  2. var current_grenade = "Grenade"
  3. var grenade_scene = preload("res://Grenade.tscn")
  4. var sticky_grenade_scene = preload("res://Sticky_Grenade.tscn")
  5. const GRENADE_THROW_FORCE = 50
  • grenade_amounts:游戏角色当前携带的手榴弹数量(针对每种类型的手榴弹).

  • current_grenade:游戏角色目前正在使用的手榴弹的名称.

  • grenade_scene:我们之前工作的手榴弹场景.

  • sticky_grenade_scene:我们之前工作过的粘手榴弹场景.

  • GRENADE_THROW_FORCE:游戏角色投掷手榴弹的力量.

大多数这些变量与我们设置武器的方式类似.

小技巧

虽然可以制作更模块化的手榴弹系统,但我发现仅仅两枚手榴弹的额外复杂性是不值得的. 如果您打算制造一个更复杂的FPS和更多的手榴弹,您可能想要建立一个类似于我们如何设置武器的手榴弹系统.


现在我们需要在 _process_input 中添加一些代码.将以下内容添加到``_process_input``:

  1. # ----------------------------------
  2. # Changing and throwing grenades
  3. if Input.is_action_just_pressed("change_grenade"):
  4. if current_grenade == "Grenade":
  5. current_grenade = "Sticky Grenade"
  6. elif current_grenade == "Sticky Grenade":
  7. current_grenade = "Grenade"
  8. if Input.is_action_just_pressed("fire_grenade"):
  9. if grenade_amounts[current_grenade] > 0:
  10. grenade_amounts[current_grenade] -= 1
  11. var grenade_clone
  12. if current_grenade == "Grenade":
  13. grenade_clone = grenade_scene.instance()
  14. elif current_grenade == "Sticky Grenade":
  15. grenade_clone = sticky_grenade_scene.instance()
  16. # Sticky grenades will stick to the player if we do not pass ourselves
  17. grenade_clone.player_body = self
  18. get_tree().root.add_child(grenade_clone)
  19. grenade_clone.global_transform = $Rotation_Helper/Grenade_Toss_Pos.global_transform
  20. grenade_clone.apply_impulse(Vector3(0, 0, 0), grenade_clone.global_transform.basis.z * GRENADE_THROW_FORCE)
  21. # ----------------------------------

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

首先,我们检查 change_grenade 动作是否刚刚被按下.如果按下了,就检查玩家当前使用的手榴弹.根据手榴弹名称,将 current_grenade 改为相反的手榴弹名称.

接下来我们检查是否刚刚按下了 fire_grenade 动作. 如果有,我们检查游戏角色是否有超过”0”的手榴弹,用于当前选择的手榴弹类型.

如果游戏角色拥有超过”0”的手榴弹,我们就会从当前手榴弹的手榴弹数量中移除一枚. 然后,根据游戏角色当前正在使用的手榴弹,我们实例化正确的手榴弹场景并将其分配给”grenade_clone”.

接下来,我们将 grenade_clone 添加为根节点的子节点,并将其global Transform 设置为 Grenade_Toss_Pos 的global Transform. 最后,我们对手榴弹施加一个冲动,使它相对于 grenade_cloneZ 方向向量向前发射.


现在游戏角色可以使用两种类型的手榴弹,但在我们继续添加其他东西之前,我们应该添加一些东西.

我们仍然需要一种方法向游戏角色展示剩下多少手榴弹,我们应该在游戏角色拿起弹药时增加一种获得更多手榴弹的方法.

首先,修改 Player.gd 中的一些代码,以显示还剩多少颗手榴弹.将 process_UI 改为如下:

  1. func process_UI(delta):
  2. if current_weapon_name == "UNARMED" or current_weapon_name == "KNIFE":
  3. # First line: Health, second line: Grenades
  4. UI_status_label.text = "HEALTH: " + str(health) + \
  5. "\n" + current_grenade + ": " + str(grenade_amounts[current_grenade])
  6. else:
  7. var current_weapon = weapons[current_weapon_name]
  8. # First line: Health, second line: weapon and ammo, third line: grenades
  9. UI_status_label.text = "HEALTH: " + str(health) + \
  10. "\nAMMO: " + str(current_weapon.ammo_in_weapon) + "/" + str(current_weapon.spare_ammo) + \
  11. "\n" + current_grenade + ": " + str(grenade_amounts[current_grenade])

现在我们将展示游戏角色在UI中留下多少手榴弹.

当我们还在 Player.gd 时,让我们添加一个向游戏角色添加手榴弹的功能. 将以下函数添加到``Player.gd``:

  1. func add_grenade(additional_grenade):
  2. grenade_amounts[current_grenade] += additional_grenade
  3. 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 函数. 将其更改为以下内容:

  1. func trigger_body_entered(body):
  2. if body.has_method("add_ammo"):
  3. body.add_ammo(AMMO_AMOUNTS[kit_size])
  4. respawn_timer = RESPAWN_TIME
  5. kit_size_change_values(kit_size, false)
  6. if body.has_method("add_grenade"):
  7. body.add_grenade(GRENADE_AMOUNTS[kit_size])
  8. respawn_timer = RESPAWN_TIME
  9. kit_size_change_values(kit_size, false)

现在我们还要检查主体是否有 add_grenade 函数. 如果是这样,我们称之为”add_ammo”.

您可能已经注意到我们正在使用一个尚未定义的新常量, GRENADE_AMOUNTS . 我们加上吧! 使用其他类变量将以下类变量添加到``AmmoPickup.gd``:

  1. const GRENADE_AMOUNTS = [2, 0]
  • GRENADE_AMOUNTS: 每个拾取包含的手榴弹数量.

注意 GRENADE_AMOUNTS 中的第二个元素是 0 .这是为了让小弹药拾取器不给玩家任何额外的手榴弹.


现在您应该可以投掷手榴弹了! 去尝试吧!

添加抓取并将RigidBody节点投射到游戏角色的功能

接下来,让我们给玩家提供拾取和投掷 RigidBody 节点的能力.

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

  1. var grabbed_object = null
  2. const OBJECT_THROW_FORCE = 120
  3. const OBJECT_GRAB_DISTANCE = 7
  4. const OBJECT_GRAB_RAY_DISTANCE = 10
  • grabbed_object:一个用于保存抓取的变量 RigidBody 节点.

  • OBJECT_THROW_FORCE: 玩家投掷被抓住物体的力量.

  • OBJECT_GRAB_DISTANCE: 玩家拿着被抓住物体时离相机的距离.

  • OBJECT_GRAB_RAY_DISTANCE:距离 Raycast 去了. 这是游戏角色的抓地距离.

完成后,我们需要做的就是在 process_input 中添加一些代码:

  1. # ----------------------------------
  2. # Grabbing and throwing objects
  3. if Input.is_action_just_pressed("fire_grenade") and current_weapon_name == "UNARMED":
  4. if grabbed_object == null:
  5. var state = get_world().direct_space_state
  6. var center_position = get_viewport().size / 2
  7. var ray_from = camera.project_ray_origin(center_position)
  8. var ray_to = ray_from + camera.project_ray_normal(center_position) * OBJECT_GRAB_RAY_DISTANCE
  9. var ray_result = state.intersect_ray(ray_from, ray_to, [self, $Rotation_Helper/Gun_Fire_Points/Knife_Point/Area])
  10. if !ray_result.empty():
  11. if ray_result["collider"] is RigidBody:
  12. grabbed_object = ray_result["collider"]
  13. grabbed_object.mode = RigidBody.MODE_STATIC
  14. grabbed_object.collision_layer = 0
  15. grabbed_object.collision_mask = 0
  16. else:
  17. grabbed_object.mode = RigidBody.MODE_RIGID
  18. grabbed_object.apply_impulse(Vector3(0, 0, 0), -camera.global_transform.basis.z.normalized() * OBJECT_THROW_FORCE)
  19. grabbed_object.collision_layer = 1
  20. grabbed_object.collision_mask = 1
  21. grabbed_object = null
  22. if grabbed_object != null:
  23. grabbed_object.global_transform.origin = camera.global_transform.origin + (-camera.global_transform.basis.z.normalized() * OBJECT_GRAB_DISTANCE)
  24. # ----------------------------------

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

首先,我们检查按下的是否是 fire 动作,以及玩家是否使用了 UNARMED ‘weapon’ .这是因为我们只希望玩家在没有使用任何武器的时候,能够捡起和投掷物体.这是一种设计上的选择,但我觉得让 UNARMED 有了用途.

接下来我们检查 grabbed_object 是否为 null.


如果 grabbed_objectnull ,我们想看看我们是否可以选择 RigidBody.

我们首先从当前获得直接空间状态 World. 这样我们就可以完全从代码中投射光线,而不必使用 Raycast 节点.

注解

参见 Ray-casting 了解更多关于Godot中的射线投射的信息.

然后我们通过将当前 Viewport 大小分成两半来得到屏幕的中心. 然后我们使用摄像机的 project_ray_originproject_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 改为:

  1. func _physics_process(delta):
  2. process_input(delta)
  3. process_view_input(delta)
  4. process_movement(delta)
  5. if grabbed_object == null:
  6. process_changing_weapons(delta)
  7. process_reloading(delta)
  8. # Process the UI
  9. process_UI(delta)

现在游戏角色在拿着物体时无法改变武器或重装.

现在,当您处于”UNARMED”状态时,您可以抓住并抛出RigidBody节点! 去尝试吧!

添加一个炮塔

接下来,让我们制作一个炮塔射击游戏角色!

打开 Turret.tscn . 如果尚未展开,请展开 Turret .

请注意炮塔是如何被分解成几个部分: BaseHeadVision_AreaSmoke Particles 节点.

打开 Base ,你会发现是一个 StaticBody 和一个网格.打开 Head ,你会发现有几个网格,一个 StaticBody 和一个 Raycast 节点.

“头部”的一个注意事项是,如果我们使用光线投射,光线投射将是炮塔射击的地方. 我们还有两个网格叫做 FlashFlash_2 . 这些将是枪口闪光,简要显示炮塔开火时.

``Vision_Area``是 Area 我们将用作炮塔的能力. 当某些东西进入”Vision_Area”时,我们会认为炮塔可以看到它.

``Smoke``是一个 Particles 节点将在炮塔被摧毁和修复时播放.


现在我们已经了解了如何设置场景,让我们开始编写炮塔的代码. 选择 Turret 并创建一个名为 Turret.gd 的新脚本. 将以下内容添加到``Turret.gd``:

  1. extends Spatial
  2. export (bool) var use_raycast = false
  3. const TURRET_DAMAGE_BULLET = 20
  4. const TURRET_DAMAGE_RAYCAST = 5
  5. const FLASH_TIME = 0.1
  6. var flash_timer = 0
  7. const FIRE_TIME = 0.8
  8. var fire_timer = 0
  9. var node_turret_head = null
  10. var node_raycast = null
  11. var node_flash_one = null
  12. var node_flash_two = null
  13. var ammo_in_turret = 20
  14. const AMMO_IN_FULL_TURRET = 20
  15. const AMMO_RELOAD_TIME = 4
  16. var ammo_reload_timer = 0
  17. var current_target = null
  18. var is_active = false
  19. const PLAYER_HEIGHT = 3
  20. var smoke_particles
  21. var turret_health = 60
  22. const MAX_TURRET_HEALTH = 60
  23. const DESTROYED_TIME = 20
  24. var destroyed_timer = 0
  25. var bullet_scene = preload("Bullet_Scene.tscn")
  26. func _ready():
  27. $Vision_Area.connect("body_entered", self, "body_entered_vision")
  28. $Vision_Area.connect("body_exited", self, "body_exited_vision")
  29. node_turret_head = $Head
  30. node_raycast = $Head/Ray_Cast
  31. node_flash_one = $Head/Flash
  32. node_flash_two = $Head/Flash_2
  33. node_raycast.add_exception(self)
  34. node_raycast.add_exception($Base/Static_Body)
  35. node_raycast.add_exception($Head/Static_Body)
  36. node_raycast.add_exception($Vision_Area)
  37. node_flash_one.visible = false
  38. node_flash_two.visible = false
  39. smoke_particles = $Smoke
  40. smoke_particles.emitting = false
  41. turret_health = MAX_TURRET_HEALTH
  42. func _physics_process(delta):
  43. if is_active == true:
  44. if flash_timer > 0:
  45. flash_timer -= delta
  46. if flash_timer <= 0:
  47. node_flash_one.visible = false
  48. node_flash_two.visible = false
  49. if current_target != null:
  50. node_turret_head.look_at(current_target.global_transform.origin + Vector3(0, PLAYER_HEIGHT, 0), Vector3(0, 1, 0))
  51. if turret_health > 0:
  52. if ammo_in_turret > 0:
  53. if fire_timer > 0:
  54. fire_timer -= delta
  55. else:
  56. fire_bullet()
  57. else:
  58. if ammo_reload_timer > 0:
  59. ammo_reload_timer -= delta
  60. else:
  61. ammo_in_turret = AMMO_IN_FULL_TURRET
  62. if turret_health <= 0:
  63. if destroyed_timer > 0:
  64. destroyed_timer -= delta
  65. else:
  66. turret_health = MAX_TURRET_HEALTH
  67. smoke_particles.emitting = false
  68. func fire_bullet():
  69. if use_raycast == true:
  70. node_raycast.look_at(current_target.global_transform.origin + Vector3(0, PLAYER_HEIGHT, 0), Vector3(0, 1, 0))
  71. node_raycast.force_raycast_update()
  72. if node_raycast.is_colliding():
  73. var body = node_raycast.get_collider()
  74. if body.has_method("bullet_hit"):
  75. body.bullet_hit(TURRET_DAMAGE_RAYCAST, node_raycast.get_collision_point())
  76. ammo_in_turret -= 1
  77. else:
  78. var clone = bullet_scene.instance()
  79. var scene_root = get_tree().root.get_children()[0]
  80. scene_root.add_child(clone)
  81. clone.global_transform = $Head/Barrel_End.global_transform
  82. clone.scale = Vector3(8, 8, 8)
  83. clone.BULLET_DAMAGE = TURRET_DAMAGE_BULLET
  84. clone.BULLET_SPEED = 60
  85. ammo_in_turret -= 1
  86. node_flash_one.visible = true
  87. node_flash_two.visible = true
  88. flash_timer = FLASH_TIME
  89. fire_timer = FIRE_TIME
  90. if ammo_in_turret <= 0:
  91. ammo_reload_timer = AMMO_RELOAD_TIME
  92. func body_entered_vision(body):
  93. if current_target == null:
  94. if body is KinematicBody:
  95. current_target = body
  96. is_active = true
  97. func body_exited_vision(body):
  98. if current_target != null:
  99. if body == current_target:
  100. current_target = null
  101. is_active = false
  102. flash_timer = 0
  103. fire_timer = 0
  104. node_flash_one.visible = false
  105. node_flash_two.visible = false
  106. func bullet_hit(damage, bullet_hit_pos):
  107. turret_health -= damage
  108. if turret_health <= 0:
  109. smoke_particles.emitting = true
  110. 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_enteredbody_exited 信号分别连接到 body_entered_visionbody_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_timerfire_timer 分别设置为 FLASH_TIMEFIRE_TIME .然后检查炮塔是否已经用完了最后一颗子弹.如果用完了,将 ammo_reload_timer 设置为 ammo_reload_TIME ,这样炮塔就会重新装弹.


让我们看看接下来的 body_entered_vision ,谢天谢地,它很短.

我们首先通过检查 current_target 是否等于 null 来检查炮塔当前是否有目标. 如果炮塔没有目标,我们就检查刚刚进入 Area 视野的物体是一个 KinematicBody .

注解

我们假设炮塔应该只对 KinematicBody 节点进行射击,因为玩家使用的就是这个节点.

如果刚进入视觉的主体 AreaKinematicBody,我们将 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 如果还没有打开的话,从 BaseHead 中选择一个 StaticBody 节点.创建一个名为 TurretBodies.gd 的新脚本,并将其附加到你选择的任何一个 StaticBody 节点上.

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

  1. extends StaticBody
  2. export (NodePath) var path_to_turret_root
  3. func _ready():
  4. pass
  5. func bullet_hit(damage, bullet_hit_pos):
  6. if path_to_turret_root != null:
  7. get_node(path_to_turret_root).bullet_hit(damage, bullet_hit_pos)

这段代码所做的就是在 path_to_turret_root 所指向的任何节点上调用 bullet_hit .回到编辑器,将 NodePath 分配给 Turret 节点.

现在选择另一个 StaticBody 节点(在 BodyHead 中)并为其指定 TurretBodies.gd 脚本. 附加脚本后,再次将 NodePath 分配给 Turret 节点.


最后,我们需要做的是给玩家增加一种伤害的方式.由于所有的子弹都使用 bullet_hit 函数,我们需要为玩家添加该函数.

打开 Player.gd 并添加以下内容:

  1. func bullet_hit(damage, bullet_hit_pos):
  2. health -= damage

完成所有这些后,您应该拥有完全可操作的炮塔! 在一个/两个/所有场景中进行几次尝试!

最后的笔记

../../../_images/PartFiveFinished.png

现在您可以拿起 RigidBody 节点并投掷手榴弹. 我们现在也有炮塔射击游戏角色.

在 :ref:`doc_fps_tutorial_part_six`中,我们将添加一个主菜单和一个暂停菜单,为游戏角色添加重新生成的系统,以及更改/移动声音系统,以便我们可以从任何脚本中使用它.

警告

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

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