第5部分

零件概述

在这部分中,我们将向玩家添加手榴弹,让玩家能够抓取和投掷物体,并添加炮塔!

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

注解

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

我们开始吧!

添加手榴弹

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

这里有几点需要注意,首先是手榴弹要用 RigidBody 节点。我们要用 RigidBody 我们手榴弹的节点,因此它们以(某种程度上)现实的方式在世界各地反弹。

第二点要注意的是 Blast_Area . 这是一个 Area 表示手榴弹爆炸半径的节点。

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

注解

如果需要,可以通过查看粒子的 Process MaterialDraw Passes .

让我们编写手榴弹所需的代码。选择 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_shapeCollisionShape 手榴弹的 RigidBody .

  • grenade_meshMeshInstance 手榴弹。

  • blast_area :爆炸 Area 用来在手榴弹爆炸时损坏东西。

  • explosion_particlesParticles 当手榴弹爆炸的时候就会出来。

注意如何 EXPLOSION_WAIT_TIME 是个相当奇怪的数字 (0.48 )这是因为我们想要 EXPLOSION_WAIT_TIME 为了等于爆炸粒子发射的时间长度,所以当粒子发射完后,我们摧毁/释放手榴弹。我们计算 EXPLOSION_WAIT_TIME 用粒子的寿命除以粒子的速度刻度。这使我们知道爆炸粒子将持续的确切时间。


现在让我们把注意力转移到 _ready .

首先,我们得到所有需要的节点,并将它们分配给适当的类变量。

我们需要得到 CollisionShapeMeshInstance 因为与目标相似 第4部分 当手榴弹爆炸时,我们将隐藏手榴弹的网格并禁用碰撞形状。

我们需要爆炸的原因 Area 这样我们就能在手榴弹爆炸时破坏里面的一切。我们将在播放器中使用与刀代码类似的代码。我们需要 Particles 所以我们可以在手榴弹爆炸时发射粒子。

在获取所有节点并将它们分配给类变量之后,我们将确保爆炸粒子不会发射,并且它们设置为一次发射。这是为了更加确定粒子的行为是否符合我们的预期。


现在让我们看看 _process .

首先,我们检查 grenade_timer 小于 GRENADE_TIME . 如果是,我们补充说 delta 然后回来。所以手榴弹必须等待 GRENADE_TIME 爆炸前几秒钟,允许 RigidBody 四处走动。

如果 grenade_timer 位于 GRENADE_TIMER 或者更高,我们需要检查手榴弹是否等得够久,需要爆炸。我们通过检查 explosion_wait_timer 等于 0 或者更少。因为我们将添加 deltaexplosion_wait_timer 之后,不管检查下的代码是什么,只要手榴弹等了足够长的时间需要爆炸,就只能调用一次。

如果手榴弹等了足够长的时间爆炸,我们首先告诉 explosion_particles 发射。然后我们做 grenade_mesh 不可见和禁用 rigid_shape 有效地隐藏了手榴弹。

然后我们设置 RigidBody 的模式到 MODE_STATIC 所以手榴弹不动。

然后我们把所有的尸体都放进去 blast_area ,检查是否有 bullet_hit 方法/函数,如果有,我们调用它并传入 GRENADE_DAMAGE 从尸体看手榴弹的转变。这使得由手榴弹爆炸的尸体将从手榴弹的位置向外爆炸。

然后我们检查一下 explosion_wait_timer 小于 EXPLOSION_WAIT_TIME . 如果是,我们补充说 deltaexplosion_wait_timer .

接下来,我们检查一下 explosion_wait_timer 大于或等于 EXPLOSION_WAIT_TIME . 因为我们添加了 delta ,将只调用一次。如果 explosion_wait_timer 大于或等于 EXPLOSION_WAIT_TIME 手榴弹已经等得够久了 Particles 玩游戏,我们可以释放/摧毁手榴弹,因为我们不再需要它。


我们也快把粘手榴弹放好。打开 Sticky_Grenade.tscn .

Sticky_Grenade.tscn 几乎等同于 Grenade.tscn 一小部分。我们现在有时间 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 我们来回顾一下发生了什么变化。

首先,我们还有一些类变量:

  • attached :用于跟踪粘性手榴弹是否已附加到 PhysicsBody .

  • attach_point :保存 Spatial 那将是在粘手榴弹碰撞的位置。

  • player_body :玩家的 KinematicBody .

它们被添加到任何 PhysicsBody 它可能会击中。我们现在也需要球员的 KinematicBody 所以当玩家扔手榴弹时,手榴弹不会粘在玩家身上。


现在让我们看看 _ready . 在 _ready 我们添加了一行代码,所以当任何人进入 Stick_Area , the collided_with_body 调用函数。


下一步让我们看看 collided_with_body .

首先,我们要确保手榴弹不会与自身发生碰撞。因为粘性 Area 不知道它附在手榴弹上 RigidBody 我们需要通过检查来确保它不会粘在自己身上,以确保它碰撞的物体不是它自己。如果我们与自己相撞,我们会通过返回而忽略它。

然后我们检查是否有分配给 player_body 如果粘手榴弹撞到的尸体是扔它的玩家。如果粘手榴弹撞到的尸体确实是 player_body ,我们通过返回忽略它。

接下来,我们检查一下手榴弹是否已经粘上了什么东西。

如果粘手榴弹没有连上,我们就把 attached 真的,所以我们知道粘手榴弹附在什么东西上了。

然后我们做一个新的 Spatial 节点上,又使它成为孩子身上粘手榴弹的碰撞对象。然后我们设置 Spatial 在全球的地位。

注解

因为我们添加了 Spatial 作为一个孩子的身体粘手榴弹已与碰撞,它将随着该机构。我们可以用这个 Spatial 设置粘性手榴弹的位置,使其始终与碰撞的物体处于同一位置。

然后我们就禁用了 rigid_shape 所以粘手榴弹不会一直移动,不管它撞到什么物体。最后,我们将模式设置为 MODE_STATIC 所以手榴弹不动。


最后,让我们回顾一下 _process .

现在我们检查一下手榴弹是否就在 _process .

如果粘手榴弹是附加的,那么我们要确保附加点不等于 null . 如果附着点不等于 null 我们设定了黏手榴弹的全球位置(使用其全球 Transform 的来源)的全球地位 Spatial 指派给 attach_point (使用其全局 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 作为根节点的子节点并设置其全局 TransformGrenade_Toss_Pos 的全局 Transform . 最后,我们对手榴弹施加脉冲,使其相对于 Z 方向向量 grenade_clone s。


现在玩家可以使用这两种类型的手榴弹,但在我们继续添加其他东西之前,我们可能还需要添加一些东西。

我们仍然需要一种方法来告诉玩家还剩多少手榴弹,并且我们可能应该增加一种方法来获得更多手榴弹,当玩家拿起弹药时。

首先,让我们更改一些代码 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])

现在我们将显示玩家在用户界面中还剩多少手榴弹。

当我们还在的时候 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_AMOUNTS0 . 这样一来,小弹药不会给玩家任何额外的手榴弹。


现在你应该可以扔手榴弹了!去试试吧!

增加了抓取和抛出僵尸节点到播放器的能力

接下来,让我们给运动员一个接球和投球的能力 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") 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 != null:
            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 “武器”。这是因为我们只希望玩家能够在不使用任何武器的情况下拾起和投掷物体。这是一个设计选择,但我觉得它 UNARMED 用途。

接下来我们检查一下 grabbed_objectnull .


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

我们首先从电流中得到直接的空间状态 World . 因此,我们可以完全从代码中投射光线,而不必使用 Raycast 节点。

注解

看见 Ray-casting 有关Godot的光线投射的更多信息。

然后我们用电流除以屏幕的中心 Viewport 一半大小。然后我们用 project_ray_originproject_ray_normal 从相机里。如果您想进一步了解这些函数的工作原理,请参见 Ray-casting .

接下来,我们将光线发送到空间状态,看看它是否得到结果。我们加上球员和刀子 Area 作为两个例外,玩家不能携带自己或刀子的碰撞。 Area .

然后我们检查一下光线是否有结果。如果有的话,我们就看看与射线碰撞的对撞机是不是一个 RigidBody .

如果光线与 RigidBody ,我们设置 grabbed_object 射线与对撞机相撞。然后我们将模式设置为 RigidBody 我们撞到了 MODE_STATIC 所以它不会在我们手中移动。

最后,我们设置抓取 RigidBody 的碰撞层和碰撞遮罩 0 . 这将使 RigidBody 没有碰撞层或遮罩,这意味着只要我们拿着它,它就不能与任何东西碰撞。


如果 grabbed_object 不是 null ,然后我们需要 RigidBody 玩家正在等待。

我们首先设置 RigidBody 我们坚持 MODE_RIGID .

注解

这是一个相当大的假设,所有的刚体都将使用 MODE_RIGID . 虽然这是本教程系列的情况,但在其他项目中可能不是这样。

如果有具有不同模式的刚体,则可能需要存储 RigidBody 您已经提取到一个类变量中,这样您就可以将其更改回提取前的模式。

Then we apply an impulse to send it flying forward. We send it flying in the direction the camera is facing, using the force we set in the OBJECT_THROW_FORCE variable.

然后我们把抓取的 RigidBody 的碰撞层和遮罩到 1 ,这样它就可以与层上的任何对象发生碰撞。 1 再一次。

注解

这再一次提出了一个相当大的假设,即所有的刚体都将只在碰撞层上。 1 ,所有碰撞遮罩将位于层上 1 . 如果在其他项目中使用此脚本,则可能需要存储 RigidBody 在将变量更改为之前 0 ,这样,当您反转进程时,您可以为它们设置原始碰撞层/遮罩。

最后,我们开始 grabbed_objectnull 因为玩家成功地抛出了持有的对象。


最后我们要做的是检查 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 状态!去试试吧!

添加炮塔

接下来,让我们做一个炮塔来射击玩家!

打开 Turret.tscn . 展开 Turret 如果它还没有扩展。

注意炮塔是如何分解成几个部分的: BaseHeadVision_Area 和A Smoke Particles 节点。

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

有一点需要注意 Head 如果我们使用的是光线投射,那么光线投射就是炮塔子弹的发射点。我们还有两个网格叫做 FlashFlash_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 子弹可以。

  • FLASH_TIME :枪口闪光网格可见的时间量(秒)。

  • flash_timer :跟踪枪口闪光网格可见时间的变量。

  • FIRE_TIME :发射子弹所需的时间(秒)。

  • fire_timer :跟踪炮塔上次发射后经过的时间的变量。

  • node_turret_head :保存 Head 节点。

  • node_raycast :保存 Raycast 连接到炮塔头部的节点。

  • node_flash_one :保存第一个枪口闪光的变量 MeshInstance .

  • node_flash_two :保存第二个炮口闪光的变量 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 变量。我们也设置了 emittingfalse 以确保在炮塔破裂前微粒不会释放。

最后,我们将炮塔的健康设置为 MAX_TURRET_HEALTH 所以它从完全健康开始。


现在让我们通过 _physics_process .

首先,我们检查炮塔是否激活。如果炮塔是活动的,我们要处理发射代码。

下一步,如果 flash_timer 大于零,表示闪光网格可见,我们要从 flash_timer .如果 flash_timer 减去后得到零或更少 delta ,我们要隐藏两个闪光网格。

接下来,我们检查炮塔是否有目标。如果炮塔有一个目标,我们让炮塔的头部看它,补充说 PLAYER_HEIGHT 所以不是瞄准球员的脚。

然后我们检查炮塔的健康是否大于零。如果是,我们会检查炮塔里是否有弹药。

如果有,我们会检查 fire_timer 大于零。如果是的话,炮塔不能开火,我们需要移除 deltafire_timer .如果 fire_timer 小于或等于零,炮塔可以发射子弹,所以我们称之为 fire_bullet 功能。

如果炮塔里没有弹药,我们会检查 ammo_reload_timer 大于零。如果是,我们减去 deltaammo_reload_timer .如果 ammo_reload_timer 小于或等于零,我们设置 ammo_in_turretAMMO_IN_FULL_TURRET 因为炮塔已经等了足够长的时间来补充弹药。

接下来,我们检查炮塔的健康是否小于或等于 0 不管它是否处于活动状态。如果炮塔的健康状况为零或更低,我们将检查 destroyed_timer 大于零。如果是,我们减去 deltadestroyed_timer .

如果 destroyed_timer 小于或等于零,我们设置 turret_healthMAX_TURRET_HEALTH 通过设置停止释放烟雾颗粒 smoke_particles.emittingfalse .


下一步,让我们通过 fire_bullet .

首先,我们检查炮塔是否使用了光线投射。

使用光线投射的代码几乎与来复枪中的代码完全相同 第2部分 所以我只会简单地回顾一下。

我们首先让光线投射观察目标,确保光线投射在没有阻碍的情况下击中目标。然后我们强制光线投射更新,这样我们得到一个完美的帧碰撞检查。然后我们检查光线投射是否与任何物体碰撞。如果有,我们检查碰撞物体是否有 bullet_hit 方法。如果是这样的话,我们称之为它,并通过一个光线投射子弹与光线投射的变换一起造成的伤害。然后我们减去 1ammo_in_turret .

如果炮塔不使用光线投射,我们将生成一个子弹对象。这个密码几乎与手枪上的密码完全相同 第2部分 就像光线投射代码一样,我只会简单介绍一下。

我们先做一个子弹克隆,然后把它分配给 clone . 然后我们将其添加为根节点的子节点。我们将子弹的整体转换设置到枪管末端,由于子弹太小而将其放大,并使用炮塔的常量类变量设置其伤害和速度。然后我们减去 1ammo_in_turret .

然后,不管使用哪种子弹方法,我们都会使两个炮口闪光网格可见。我们设置 flash_timerfire_timerFLASH_TIMEFIRE_TIME ,分别。然后我们检查炮塔是否使用了弹药中的最后一颗子弹。如果有的话,我们就定了 ammo_reload_timerAMMO_RELOAD_TIME 所以炮塔重新装填。


让我们来看一看 body_entered_vision 其次,谢天谢地,它相当短。

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

注解

我们假设炮塔只能在 KinematicBody 节点,因为这是玩家正在使用的。

如果刚刚进入视野的身体 Area 是一个 KinematicBody ,我们设置 current_target 到身体,然后设置 is_activetrue .


现在让我们看看 body_exited_vision .

首先,我们检查炮塔是否有目标。如果是这样,我们会检查刚刚离开炮塔视野的那具尸体 Area 是炮塔的目标。

如果刚刚离开视野的身体 Area 是炮塔的当前目标,我们设置 current_targetnull ,集合 is_activefalse ,并重置与发射炮塔相关的所有变量,因为炮塔不再有目标要发射。


最后,让我们看看 bullet_hit .

我们首先从炮塔的健康状况中减去子弹造成的伤害。

然后,我们检查炮塔是否被摧毁(健康是零或更少)。如果炮塔被摧毁,我们开始发射烟雾粒子并设置 destroyed_timerDESTROYED_TIME 所以炮塔在修理前必须等待。


当所有这些都完成并编码之后,在炮塔准备就绪之前,我们只有最后一件事要做。打开 Turret.tscn 如果尚未打开,请选择 StaticBody 节点来自 BaseHead . 创建一个名为 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)

所有这些代码都是调用 bullet_hit 在任何节点上 path_to_turret_root 引导。返回编辑器并分配 NodePathTurret 节点。

现在选择另一个 StaticBody 节点(在 BodyHead )并分配 TurretBodies.gd 它的脚本。在附加脚本后,再次分配 NodePathTurret 节点。


我们要做的最后一件事是为球员增加受伤的方法。因为所有的子弹都使用 bullet_hit 函数,我们需要将该函数添加到播放器中。

正常开放 Player.gd 并添加以下内容:

func bullet_hit(damage, bullet_hit_pos):
    health -= damage

完成所有这些之后,您应该拥有完全可操作的炮塔!在一个/两个/所有场景中放置一些,然后尝试一下!

最后的注释

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

现在你可以去接了 RigidBody 节点和投掷手榴弹。我们现在也有炮塔向玩家开火。

第6部分 ,我们将添加一个主菜单和一个暂停菜单,为播放器添加一个重播系统,并更改/移动音响系统,以便我们可以从任何脚本使用它。

警告

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

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