第5部分

Part overview

In this part, we're going to add grenades to the player, give the player the ability to grab and throw objects, and add turrets!

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

注解

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

让我们开始吧!

添加手榴弹

Firstly, let's give the player some grenades to play with. Open up Grenade.tscn.

There are a few things to note here, the first and foremost being that the grenades are going to use RigidBody nodes. We're going to use RigidBody nodes for our grenades so they bounce around the world in a (somewhat) realistic manner.

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

Finally, the last thing to note is Explosion. This is the Particles node that will emit an explosion effect when the grenade explodes. One thing to note here is that we have One shot enabled. This is so we emit all the particles at once. The particles are also emitted using world coordinates instead of local coordinates, so we have Local Coords unchecked as well.

注解

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

让我们编写手榴弹所需的代码。 选择 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: The Particles that come out when the grenade explodes.

注意 EXPLOSION_WAIT_TIME 是一个相当奇怪的数字(0.48)。 这是因为我们希望 EXPLOSION_WAIT_TIME 等于爆炸粒子发射的时间长度,所以当粒子完成时我们会摧毁/释放手榴弹。 我们通过获取粒子的生命时间并将其除以粒子的速度刻度来计算“EXPLOSION_WAIT_TIME”。 这让我们得到了爆炸粒子持续的确切时间。


现在让我们把注意力转向 _ready

First we get all the nodes we'll need and assign them to the proper class variables.

我们需要得到 CollisionShapeMeshInstance 因为类似于 :ref:`doc_fps_tutorial_part_four`中的目标,我们将隐藏手榴弹的网格并禁用碰撞形状 手榴弹爆炸了。

我们需要获得爆炸的原因 Area 这样我们可以在手榴弹爆炸时损坏其内部的一切。 我们将使用与游戏角色中的刀代码类似的代码。 我们需要 Particles 所以我们可以在手榴弹爆炸时发射粒子。

After we get all the nodes and assign them to their class variables, we then make sure the explosion particles are not emitting, and that they are set to emit in one shot. This is to be extra sure the particles will behave the way we expect them to.


现在让我们来看看`_process``。

Firstly, we check to see if the grenade_timer is less than GRENADE_TIME. If it is, we add delta and return. This is so the grenade has to wait GRENADE_TIME seconds before exploding, allowing the RigidBody to move around.

如果 grenade_timer 位于 GRENADE_TIMER 或更高,那么我们需要检查手榴弹是否等待了足够长的时间并且需要爆炸。 我们通过检查 explosion_wait_timer 是否等于 0 或更少来做到这一点。 因为我们将立即将 delta 添加到 explosion_wait_timer ,所以检查下的任何代码只会被调用一次,就在手榴弹等待足够长并且需要爆炸时。

如果手榴弹已经等待足够长的时间爆炸,我们首先告诉“爆炸_粒子”发射。 然后我们使 grenade_mesh 不可见,并禁用 rigid_shape ,有效地隐藏了手榴弹。

然后我们将 RigidBody <class_RigidBody>的模式设置为 ``MODE_STATIC` ,这样手榴弹就不会移动了。

Then we get all the bodies in blast_area, check to see if they have the bullet_hit method/function, and if they do, we call it and pass in GRENADE_DAMAGE and the transform from the body looking at the grenade. This makes it where the bodies exploded by the grenade will explode outwards from the grenade's position.

We then check to see if explosion_wait_timer is less than EXPLOSION_WAIT_TIME. If it is, we add delta to explosion_wait_timer.

Next, we check to see if explosion_wait_timer is greater than or equal to EXPLOSION_WAIT_TIME. Because we added delta, this will only be called once. If explosion_wait_timer is greater or equal to EXPLOSION_WAIT_TIME, the grenade has waited long enough to let the Particles play and we can free/destroy the grenade, as we no longer need it.


让我们快速设置粘性手榴弹。 打开 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 的代码完全相同,所以让我们回顾一下已经改变的代码。

Firstly, we have a few more class variables:

  • 附加:一个变量,用于跟踪粘性手榴弹是否附加到 PhysicsBody
  • attach_point: A variable to hold a Spatial that will be at the position where the sticky grenade collided.
  • player_body:游戏角色的 KinematicBody

They have been added to enable the sticky grenade to stick to any PhysicsBody it might hit. We also now need the player's KinematicBody so the sticky grenade does not stick to the player when the player throws it.


现在让我们来看看`_ready``中的小变化。 在 _ready 中我们添加了一行代码,因此当任何物体进入 Stick_Area 时,会调用 collided_with_body 函数。


接下来让我们来看看 collided_with_body

Firstly, we make sure the sticky grenade is not colliding with itself. Because the sticky Area does not know it's attached to the grenade's RigidBody, we need to make sure it's not going to stick to itself by checking to make sure the body it has collided with is not itself. If we have collided with ourself, we ignore it by returning.

然后我们检查一下是否有东西分配给 player_body ,如果粘手榴弹碰撞的物体是投掷它的游戏角色。 如果粘手榴弹碰到的物体确实是'player_body`,我们会通过返回来忽略它。

Next, we check if the sticky grenade has attached to something already or not.

If the sticky grenade is not attached, we then set attached to true so we know the sticky grenade has attached to something.

然后我们创建一个新的 Spatial 节点,并使其成为粘性手榴弹与之碰撞的物体的子节点。 然后我们将 :ref:`Spatial <class_Spatial>的位置设置为粘性手榴弹当前的全球位置。

注解

因为我们已经添加了 Spatial 作为粘性手榴弹碰撞的物体的一个子节点,它将跟随所述物体。 然后我们可以使用它 Spatial 来设置粘性手榴弹的位置,因此它总是在相对于它碰撞的物体的相同位置。

然后我们禁用 rigid_shape ,这样粘手榴弹就不会一直移动它碰到的任何物体。 最后,我们将模式设置为“MODE_STATIC”,因此手榴弹不会移动。


最后,让我们回顾一下 _process 中的一些变化。

现在我们正在检查粘性手榴弹是否附在“_process`”的顶部。

如果连接了粘性手榴弹,我们确保附加的点不等于“null”。 如果附加的点不等于 null ,我们将粘性手榴弹的全局位置(使用其global Transform 赋值给 attach_point (使用它的global Transform 的原点)。

在我们释放/销毁粘性手榴弹之前,现在唯一的另一个变化是检查粘性手榴弹是否有附着点。 如果是这样,我们也在连接点上调用 queue_free ,因此它也被释放/销毁。

向游戏角色添加手榴弹

现在我们需要在 Player.gd 中添加一些代码,以便我们可以使用手榴弹。

Firstly, open up Player.tscn and expand the node tree until you get to Rotation_Helper. Notice how in Rotation_Helper we have a node called Grenade_Toss_Pos. This is where we will be spawning the grenades.

Also notice how it's slightly rotated on the X axis, so it's not pointing straight, but rather slightly up. By changing the rotation of Grenade_Toss_Pos, you can change the angle at which the grenades are tossed.

好的,现在让我们开始让手榴弹与游戏角色一起工作。 将以下类变量添加到``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: The force at which the player will throw the grenades.

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

小技巧

虽然可以制作更模块化的手榴弹系统,但我发现仅仅两枚手榴弹的额外复杂性是不值得的。 如果您打算制造一个更复杂的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)
# ----------------------------------

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

Firstly, we check to see if the change_grenade action has just been pressed. If it has, we then check to see which grenade the player is currently using. Based on the name of the grenade the player is currently using, we change current_grenade to the opposite grenade name.

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

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

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


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

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

Firstly, let's change some of the code in Player.gd to show how many grenades are left. Change process_UI to the following:

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: The amount of grenades each pickup contains.

Notice how the second element in GRENADE_AMOUNTS is 0. This is so the small ammo pickup does not give the player any additional grenades.


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

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

Next, let's give the player the ability to pick up and throw RigidBody nodes.

打开 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: The force with which the player throws the grabbed object.
  • OBJECT_GRAB_DISTANCE: The distance away from the camera at which the player holds the grabbed object.
  • 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)
# ----------------------------------

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

Firstly, we check to see if the action pressed is the fire action, and that the player is using the UNARMED 'weapon'. This is because we only want the player to be able to pick up and throw objects when the player is not using any weapons. This is a design choice, but I feel it gives UNARMED a use.

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


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

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

注解

See Ray-casting for more information on raycasting in Godot.

然后我们通过将当前 Viewport 大小分成两半来得到屏幕的中心。 然后我们使用摄像机的 project_ray_originproject_ray_normal 获取射线的原点和终点。 如果您想了解有关这些函数如何工作的更多信息,请参阅 Ray-casting

接下来,我们将光线发送到空间状态,看看它是否得到了结果。 我们添加了游戏角色和刀子 Area 作为两个例外,因此游戏角色无法携带自己或刀的碰撞 Area

Then we check to see if we got a result back from the ray. If no object has collided with the ray, an empty Dictionary will be returned. If the Dictionary is not empty (i.e. at least one object has collided), we then see if the collider the ray collided with is a RigidBody.

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

最后,我们将抓取的 RigidBody 没有碰撞层或掩码,这意味着只要我们拿着它就不会碰到任何东西。

注解

See Physics introduction for more information on Godot collision masks.


如果 grabbed_object 不是 null ,那么我们需要抛出游戏角色持有的 :ref:`RigidBody <class_RigidBody>。

We first set the mode of the RigidBody we are holding to MODE_RIGID.

注解

这是一个相当大的假设,即所有刚体都将使用“MODE_RIGID”。 虽然本教程系列就是这种情况,但在其他项目中可能并非如此。

If you have rigid bodies with different modes, you may need to store the mode of the RigidBody you have picked up into a class variable so you can change it back to the mode it was in before you picked it up.

然后我们施加冲动将它向前飞。 我们使用我们在 OBJECT_THROW_FORCE 变量中设置的力将它发送到相机朝向的方向。

然后我们将抓取的 RigidBody <class_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

Notice how the turret is broken up into several parts: Base, Head, Vision_Area, and a Smoke Particles node.

Open up Base and you'll find it's a StaticBody and a mesh. Open up Head and you'll find there are several meshes, a StaticBody and a Raycast node.

“头部”的一个注意事项是,如果我们使用光线投射,光线投射将是炮塔射击的地方。 我们还有两个网格叫做 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: An exported boolean so we can change whether the turret uses objects or raycasting for bullets.
  • 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:炮塔射击的子弹场景(与游戏角色的手枪相同的场景)

Whew, that's quite a few class variables!


接下来我们来看看`_ready``。

Firstly, we get the vision area and connect the body_entered and body_exited signals to body_entered_vision and body_exited_vision, respectively.

We then get all the nodes and assign them to their respective variables.

Next, we add some exceptions to the Raycast so the turret cannot hurt itself.

然后我们在开始时使两个闪存网格都不可见,因为我们不会在“_ready”期间触发。

We then get the smoke particles node and assign it to the smoke_particles variable. We also set emitting to false to ensure the particles are not emitting until the turret is broken.

最后,我们将炮塔的生命值设置为“MAX_TURRET_HEALTH”,以便从完全健康状态开始。


现在让我们来看看`_physics_process``。

Firstly, we check whether the turret is active. If the turret is active, we want to process the firing code.

Next, if flash_timer is greater than zero, meaning the flash meshes are visible, we want to remove delta from flash_timer. If flash_timer gets to zero or less after we've subtracted delta, we want to hide both of the flash meshes.

Next, we check whether the turret has a target. If the turret has a target, we make the turret head look at it, adding PLAYER_HEIGHT so it is not aiming at the player's feet.

We then check whether the turret's health is greater than zero. If it is, we then check whether there is ammo in the turret.

If there is, we then check whether fire_timer is greater than zero. If it is, the turret cannot fire and we need to remove delta from fire_timer. If fire_timer is less than or equal to zero, the turret can fire a bullet, so we call the fire_bullet function.

If there isn't any ammo in the turret, we check whether ammo_reload_timer is greater than zero. If it is, we subtract delta from ammo_reload_timer. If ammo_reload_timer is less than or equal to zero, we set ammo_in_turret to AMMO_IN_FULL_TURRET because the turret has waited long enough to refill its ammo.

Next, we check whether the turret's health is less than or equal to 0 outside of whether it is active or not. If the turret's health is zero or less, we then check whether destroyed_timer is greater than zero. If it is, we subtract delta from destroyed_timer.

如果 destroyed_timer 小于或等于零,我们将 turret_health 设置为 MAX_TURRET_HEALTH 并通过将 smoke_particles.emitting 设置为 false 来停止冒烟。


接下来让我们来看看 fire_bullet

Firstly, we check whether the turret is using a raycast.

The code for using a raycast is almost entirely the same as the code in the rifle from 第2部分, so I'm only going to go over it briefly.

We first make the raycast look at the target, ensuring the raycast will hit the target if nothing is in the way. We then force the raycast to update so we get a frame perfect collision check. We then check whether the raycast has collided with anything. If it has, we then check whether the collided body has the bullet_hit method. If it does, we call it and pass in the damage a single raycast bullet does along with the raycast's transform. We then subtract 1 from ammo_in_turret.

如果炮塔没有使用光线投射,我们会生成一个子弹对象。 这段代码几乎完全与手枪中的代码相同 第2部分,所以与光线播放代码一样,我只是简单地介绍一下。

We first make a bullet clone and assign it to clone. We then add that as a child of the root node. We set the bullet's global transform to the barrel end, scale it up since it's too small, and set its damage and speed using the turret's constant class variables. We then subtract 1 from ammo_in_turret.

Then, regardless of which bullet method we used, we make both of the muzzle flash meshes visible. We set flash_timer and fire_timer to FLASH_TIME and FIRE_TIME, respectively. We then check whether the turret has used the last bullet in its ammo. If it has, we set ammo_reload_timer to AMMO_RELOAD_TIME so the turret reloads.


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

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

注解

We're assuming the turret should only fire at KinematicBody nodes since that is what the player is using.

如果刚进入视觉的主体 AreaKinematicBody,我们将 current_target 设置为body,并将 is_active 设置为 true


现在让我们来看看 body_exited_vision

Firstly, we check whether the turret has a target. If it does, we then check whether the body that has just left the turret's vision Area is the turret's target.

If the body that has just left the vision Area is the turret's current target, we set current_target to null, set is_active to false, and reset all the variables related to firing the turret since the turret no longer has a target to fire at.


最后,让我们看一下 bullet_hit

We first subtract however much damage the bullet causes from the turret's health.

Then, we check whether the turret has been destroyed (health being zero or less). If the turret is destroyed, we start emitting the smoke particles and set destroyed_timer to DESTROYED_TIME so the turret has to wait before being repaired.


Whew, with all of that done and coded, we only have one last thing to do before the turret is ready for use. Open up Turret.tscn if it's not already open and select one of the StaticBody nodes from either Base or Head. Create a new script called TurretBodies.gd and attach it to whichever StaticBody you have selected.

将以下代码添加到``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)

All this code does is call bullet_hit on whatever node to which path_to_turret_root leads. Go back to the editor and assign the NodePath to the Turret node.

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


The last thing we need to do is add a way for the player to be hurt. Since all the bullets use the bullet_hit function, we need to add that function to the player.

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

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

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

最后的笔记

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

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

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

警告

如果您迷路了,请务必再次阅读代码!

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