第2部分

零件概述

在这一部分,我们将给玩家武器。

../../../_images/PartTwoFinished.png

在这一部分的结尾,你将有一个玩家,可以发射手枪,步枪,并使用刀攻击。玩家现在也将有转换动画,武器将与环境中的物体交互。

注解

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

我们开始吧!

制作处理动画的系统

首先,我们需要一种方法来处理不断变化的动画。打开 Player.tscn 并选择 AnimationPlayer 结点 (Player > Rotation_Helper > Model > Animation_Player

创建一个名为 AnimationPlayer_Manager.gd 把它附在 AnimationPlayer .

将以下代码添加到 AnimationPlayer_Manager.gd

extends AnimationPlayer

# Structure -> Animation name :[Connecting Animation states]
var states = {
    "Idle_unarmed":["Knife_equip", "Pistol_equip", "Rifle_equip", "Idle_unarmed"],

    "Pistol_equip":["Pistol_idle"],
    "Pistol_fire":["Pistol_idle"],
    "Pistol_idle":["Pistol_fire", "Pistol_reload", "Pistol_unequip", "Pistol_idle"],
    "Pistol_reload":["Pistol_idle"],
    "Pistol_unequip":["Idle_unarmed"],

    "Rifle_equip":["Rifle_idle"],
    "Rifle_fire":["Rifle_idle"],
    "Rifle_idle":["Rifle_fire", "Rifle_reload", "Rifle_unequip", "Rifle_idle"],
    "Rifle_reload":["Rifle_idle"],
    "Rifle_unequip":["Idle_unarmed"],

    "Knife_equip":["Knife_idle"],
    "Knife_fire":["Knife_idle"],
    "Knife_idle":["Knife_fire", "Knife_unequip", "Knife_idle"],
    "Knife_unequip":["Idle_unarmed"],
}

var animation_speeds = {
    "Idle_unarmed":1,

    "Pistol_equip":1.4,
    "Pistol_fire":1.8,
    "Pistol_idle":1,
    "Pistol_reload":1,
    "Pistol_unequip":1.4,

    "Rifle_equip":2,
    "Rifle_fire":6,
    "Rifle_idle":1,
    "Rifle_reload":1.45,
    "Rifle_unequip":2,

    "Knife_equip":1,
    "Knife_fire":1.35,
    "Knife_idle":1,
    "Knife_unequip":1,
}

var current_state = null
var callback_function = null

func _ready():
    set_animation("Idle_unarmed")
    connect("animation_finished", self, "animation_ended")

func set_animation(animation_name):
    if animation_name == current_state:
        print ("AnimationPlayer_Manager.gd -- WARNING: animation is already ", animation_name)
        return true


    if has_animation(animation_name):
        if current_state != null:
            var possible_animations = states[current_state]
            if animation_name in possible_animations:
                current_state = animation_name
                play(animation_name, -1, animation_speeds[animation_name])
                return true
            else:
                print ("AnimationPlayer_Manager.gd -- WARNING: Cannot change to ", animation_name, " from ", current_state)
                return false
        else:
            current_state = animation_name
            play(animation_name, -1, animation_speeds[animation_name])
            return true
    return false


func animation_ended(anim_name):

    # UNARMED transitions
    if current_state == "Idle_unarmed":
        pass
    # KNIFE transitions
    elif current_state == "Knife_equip":
        set_animation("Knife_idle")
    elif current_state == "Knife_idle":
        pass
    elif current_state == "Knife_fire":
        set_animation("Knife_idle")
    elif current_state == "Knife_unequip":
        set_animation("Idle_unarmed")
    # PISTOL transitions
    elif current_state == "Pistol_equip":
        set_animation("Pistol_idle")
    elif current_state == "Pistol_idle":
        pass
    elif current_state == "Pistol_fire":
        set_animation("Pistol_idle")
    elif current_state == "Pistol_unequip":
        set_animation("Idle_unarmed")
    elif current_state == "Pistol_reload":
        set_animation("Pistol_idle")
    # RIFLE transitions
    elif current_state == "Rifle_equip":
        set_animation("Rifle_idle")
    elif current_state == "Rifle_idle":
        pass;
    elif current_state == "Rifle_fire":
        set_animation("Rifle_idle")
    elif current_state == "Rifle_unequip":
        set_animation("Idle_unarmed")
    elif current_state == "Rifle_reload":
        set_animation("Rifle_idle")

func animation_callback():
    if callback_function == null:
        print ("AnimationPlayer_Manager.gd -- WARNING: No callback function for the animation to call!")
    else:
        callback_function.call_func()

让我们来看看这个脚本在做什么:


让我们从这个脚本的类变量开始:

  • states :保存动画状态的字典。(下文进一步解释)

  • animation_speeds 一本字典,用来保存我们想要播放动画的所有速度。

  • current_state :保存当前动画状态名称的变量。

  • callback_function :保存回调函数的变量。(下文进一步解释)

如果您熟悉状态机,那么您可能已经注意到 states 结构类似于基本状态机。以下是大致的方法 states 设置:

states 是一个字典,其中键是当前状态的名称,值是一个数组,其中包含我们可以转换到的所有动画(状态)。例如,如果我们当前在 Idle_unarmed 国家,我们只能过渡到 Knife_equipPistol_equipRifle_equipIdle_unarmed .

如果我们试图转换到一个状态,而这个状态不包含在我们所处状态的可能转换状态中,那么我们会收到一条警告消息,动画不会改变。我们还可以自动从一些状态转换到其他状态,如下所述 animation_ended

注解

为了保持本教程的简单性,我们没有使用“正确的”状态机。如果您有兴趣了解更多有关状态机的信息,请参阅以下文章:

  • (python示例)https://dev.to/karn/building-a-simple-state-machine-in-python

  • (C# 示例)https://www.codeproject.com/articles/489136/understandingplusandplusimplementingplusstateplusp

  • (wiki文章)https://en.wikipedia.org/wiki/finite-state_machine

animation_speeds 是每个动画播放的速度。有些动画有点慢,为了让一切看起来更流畅,我们需要以更快的速度播放。

小技巧

注意,所有的射击动画都比它们的正常速度快。以后记得这个!

current_state 将保留当前动画状态的名称。

最后, callback_function 将是 FuncRef 在适当的动画帧中由播放器生成子弹。一 FuncRef 允许我们将一个函数作为参数传入,有效地允许我们从另一个脚本调用一个函数,这就是我们稍后将如何使用它。


现在让我们看看 _ready .

首先,我们将动画设置为 Idle_unarmed 使用 set_animation 函数,所以我们肯定从动画开始。

接下来我们连接 animation_finished 向此脚本发送信号并将其分配给调用 animation_ended . 这意味着动画完成后, animation_ended 将被调用。


让我们看看 set_animation 下一步。

set_animation changes the animation to the animation named animation_name if we can transition to it. In other words, if the animation state we are currently in has the passed in animation state name in states, then we will change to that animation.

首先,我们检查传入的动画名称是否与当前播放的动画名称相同。如果它们相同,那么我们将向控制台写入警告并返回 true .

其次,我们看看 AnimationPlayer 具有名称为的动画 animation_name 使用 has_animation . 如果没有,我们就回去 false .

第三,我们检查 current_state 已设置。如果 current_statenot 当前设置,然后我们设置 current_state 传递给传入的动画名称并告诉 AnimationPlayer 开始播放混合时间为 -1 以设定的速度 animation_speeds 然后我们回来 true .

注解

混合时间是将两个动画混合在一起的时间。

通过输入一个值 -1 ,新动画立即播放,覆盖已播放的任何动画。

如果你把 1 ,在一秒钟内,新动画将以增加的强度播放,在仅播放新动画之前将两个动画混合一秒钟。这将导致动画之间的平滑过渡,当您从行走动画更改为运行动画时,这看起来非常好。

我们将混合时间设置为 -1 因为我们想立即改变动画。

如果我们有一个州在 current_state 然后我们得到所有可能的状态,我们可以转换到。

如果动画名称在可能的转换列表中,我们设置 current_state 传递到动画 (animation_name )告诉 AnimationPlayer 以混合时间播放动画 -1 以设定的速度 animation_speeds 然后返回 true .


现在让我们看看 animation_ended .

animation_ended 是将由调用的函数 AnimationPlayer 播放完动画后。

对于某些动画状态,完成后可能需要转换到另一个状态。为了处理这个问题,我们检查每个可能的动画状态。如果我们需要,我们将过渡到另一个状态。

警告

如果您使用自己的动画模型,请确保没有动画设置为循环。循环动画不发送 animation_finished 当它们到达动画的末尾并将再次循环时发出信号。

注解

中的转换 animation_ended 理想情况下是 states 但是为了使教程更容易理解,我们将在 animation_ended .


最后,还有 animation_callback . 此函数将由动画中的调用方法跟踪调用。如果我们有 FuncRef 指派给 callback_function ,然后我们调用传入函数。如果我们没有 FuncRef 指派给 callback_function ,我们向控制台打印一个警告。

小技巧

试运行 Testing_Area.tscn 以确保没有运行时问题。如果游戏运行,但没有任何变化,那么一切都正常工作。

准备好动画

现在我们有了一个可以工作的动画管理器,我们需要从播放器脚本中调用它。在这之前,我们需要在我们的射击动画中设置一些动画回调轨迹。

打开 Player.tscn 如果您没有打开它并导航到 AnimationPlayer 结点 (Player > Rotation_Helper > Model > Animation_Player

我们需要在三个动画上附加一个调用方法轨迹:手枪、步枪和刀的射击动画。让我们从手枪开始。单击“动画”下拉列表并选择“手枪射击”。

现在向下滚动到动画轨迹列表的底部。列表中的最后一项应为 Armature/Skeleton:Left_UpperPointer . 现在在列表上方,单击时间线左侧的“添加曲目”按钮。

../../../_images/AnimationPlayerAddTrack.png

这将打开一个有几个选择的窗口。我们希望添加一个调用方法跟踪,因此单击读取“调用方法跟踪”的选项。这将打开一个窗口,显示整个节点树。导航到 AnimationPlayer 节点,选择它,然后按OK。

../../../_images/AnimationPlayerCallFuncTrack.png

现在,在动画轨迹列表的底部,您将看到一个绿色轨迹,显示“动画播放器”。现在我们需要添加调用回调函数的点。擦洗时间线,直到达到枪口开始闪烁的位置。

注解

时间线是存储动画中所有点的窗口。每个小点代表一个动画数据点。

清除时间线意味着在动画中移动我们自己。所以当我们说“擦洗时间线直到你到达一个点”,我们的意思是通过动画窗口移动直到你到达时间线上的点。

另外,枪口是子弹出来的终点。枪口闪光是子弹射出时从枪口射出的闪光。枪口有时也被称为枪管。

小技巧

要在清除时间线时获得更好的控制,请按 control 然后用鼠标滚轮向前滚动以放大。向后滚动将缩小。

您还可以通过更改中的值来更改时间线清除捕捉的方式。 Step (s) 到一个较低/较高的值。

一旦你到达你喜欢的点,右击“动画播放器”的行,然后按插入键。在空名称字段中,输入 animation_callback 新闻界 enter .

../../../_images/AnimationPlayerInsertKey.png

现在,当我们播放此动画时,调用方法轨迹将在动画的特定点触发。


让我们为步枪和刀射击动画重复这个过程!

注解

因为这个过程和手枪的过程完全相同,所以这个过程解释的深度要小一些。如果你迷路了,就按照上面的步骤去做!它完全一样,只是在不同的动画上。

从“动画”下拉列表中转到“步枪射击”动画。单击列表上方的“添加轨迹”按钮,在到达动画轨迹列表底部后添加调用方法轨迹。找到枪口开始闪烁的点,然后右键单击并按 Insert Key 在轨迹上的该位置添加调用方法跟踪点。

在弹出窗口的名称字段中键入“animation_callback”,然后按 enter .

现在我们需要将回调方法跟踪应用于刀动画。选择“刀火”动画并滚动到动画轨迹的底部。点击列表上方的“添加轨迹”按钮,添加一个方法轨迹。接下来,在动画的前三分之一周围找到一个点,以将动画回调方法点放置在该点上。

注解

我们不会真的开火,而且这个动画是一个刺伤的动画,而不是一个开火的动画。在本教程中,我们将重用刀的枪发射逻辑,因此动画以与其他动画一致的样式命名。

从那里右键单击时间线并单击“插入键”。将“animation_callback”放入“name”字段,然后按 enter .

小技巧

一定要保存你的工作!

完成后,我们就可以开始添加向玩家脚本开火的能力了!我们需要设置最后一个场景:子弹对象的场景。

创建项目符号场景

在电子游戏中有几种方法可以处理枪的子弹。在本教程系列中,我们将探索两种更常见的方法:对象和光线投射。


两种方法之一是使用项目符号对象。这将是一个穿越世界并处理自己碰撞代码的对象。在这种方法中,我们按照我们的枪所面向的方向创建/生成一个子弹对象,然后它向前移动。

这种方法有几个优点。首先,我们不必把子弹储存在播放器里。我们可以简单地创建子弹,然后继续前进,用手柄检查子弹本身是否有碰撞,向它碰撞的物体发送适当的信号,然后摧毁它自己。

另一个优势是我们可以有更复杂的子弹运动。如果我们想让子弹随着时间的推移轻轻落下,我们可以让控制子弹的脚本慢慢地把子弹推到地面上。使用一个物体也会让子弹花时间到达它的目标,它不会立即击中它所指向的任何东西。这感觉更现实,因为现实生活中没有什么东西能瞬间从一个点移动到另一个点。

其中一个巨大的缺点是性能。虽然让每个项目符号计算它们自己的路径并处理它们自己的碰撞允许很大的灵活性,但这是以性能为代价的。用这种方法,我们每一步都在计算每一颗子弹的运动,虽然这可能不是几十颗子弹的问题,但当你有几百颗子弹时,它可能会成为一个巨大的问题。

尽管击中了性能,许多第一人称射击包括一些形式的物体子弹。火箭发射器是一个主要的例子,因为在许多第一人称射击者中,火箭不只是在目标位置立即爆炸。你也可以用手榴弹找到子弹作为物体很多次,因为它们通常在爆炸前在世界各地反弹。

注解

虽然我不能肯定这是事实,但这些游戏 可能 以某种形式或其他形式使用子弹物体:(这些都是我观察到的。 他们可能完全错了 . 我从来没有工作过 any 以下游戏)

  • 光环(火箭发射器、碎裂手榴弹、狙击步枪、野蛮射击等)

  • 命运号(火箭发射器,手榴弹,聚变步枪,狙击步枪,超级移动,等等)

  • 使命召唤(火箭发射器、手榴弹、弹道刀、十字弓等)

  • 战场(火箭发射器、手榴弹、克莱莫尔、迫击炮等)

子弹物体的另一个缺点是网络化。项目符号对象必须将位置(至少)与连接到服务器的所有客户端同步。

虽然我们没有实现任何形式的网络(就像它在整个教程系列中那样),但是在创建第一人称射击运动员时,特别是在将来计划添加某种形式的网络时,要记住这一点。


处理子弹碰撞的另一种方法是光线投射。

这种方法在枪械中极为常见,因为枪械的弹头移动速度很快,很少随时间改变弹道。

我们不是创造一个子弹物体并把它送入太空,而是从枪管/枪口开始向前发射射线。我们将光线投射的原点设置为子弹的起始位置,根据长度,我们可以调整子弹“穿过”空间的距离。

注解

虽然我不能肯定这是事实,但这些游戏 可能 以某种形式或另一种形式使用光线投射:(这些完全来自我的观察。 他们可能完全错了 . 我从来没有工作过 any 以下游戏)

  • 光环(突击步枪、DMR、战斗步枪、圣约卡宾枪、斯巴达激光等等)

  • 命运号(自动步枪、脉冲步枪、侦察步枪、手炮、机枪等)

  • 使命召唤(突击步枪、轻机枪、亚机枪、手枪等)

  • 战场(突击步枪、SMG、卡宾枪、手枪等)

这种方法的一个巨大优势是它的性能很轻。在太空中发射几百条射线是 much 对计算机来说,计算比发送几百个子弹物体更容易。

另一个好处是,我们可以立即知道我们是否击中了什么东西,而不是在我们要求它的时候。对于网络来说,这一点很重要,因为我们不需要在互联网上同步子弹的移动,我们只需要发送光线投射是否命中。

然而,人造丝确实有一些缺点。一个主要的缺点是我们不能轻易地将光线投射到除直线以外的任何地方。这意味着我们只能在一条直线上发射,不管我们的射线长度有多长。您可以通过在不同位置投射多条光线来创建子弹移动的假象,但这不仅很难在代码中实现,而且对性能的影响也更大。

另一个缺点是我们看不到子弹。对于子弹物体,如果我们在其上附加一个网格,我们实际上可以看到子弹穿过空间,但是由于光线投射会立即发生,所以我们没有一种合适的方式来显示子弹。可以从光线投射的原点到光线投射碰撞的点绘制一条线,这是显示光线投射的常用方法之一。另一种方法就是根本不绘制光线,因为理论上子弹移动得太快,我们的眼睛根本看不到它。


让我们把子弹的物体设置好。这是我们的手枪在调用“Pistol_Fire”动画回调函数时将创建的。

打开 Bullet_Scene.tscn . 场景包含 Spatial 节点名为bullet,具有 MeshInstance 和一个 Area 用一个 CollisionShape 孩子们。

创建一个名为 Bullet_script.gd 并将其连接到 Bullet Spatial .

我们要把整个子弹物体移到根部 (Bullet )我们将使用 Area 检查我们是否与某物相撞

注解

我们为什么要用 Area 而不是一个 RigidBody ?我们不使用 RigidBody 是因为我们不希望子弹与其他物体相互作用 RigidBody 节点。通过使用 Area 我们保证没有其他人 RigidBody 节点(包括其他项目符号)将受到影响。

另一个原因很简单,因为它更容易检测与 Area 你说什么?

下面是控制子弹的脚本:

extends Spatial

var BULLET_SPEED = 70
var BULLET_DAMAGE = 15

const KILL_TIMER = 4
var timer = 0

var hit_something = false

func _ready():
    $Area.connect("body_entered", self, "collided")


func _physics_process(delta):
    var forward_dir = global_transform.basis.z.normalized()
    global_translate(forward_dir * BULLET_SPEED * delta)

    timer += delta
    if timer >= KILL_TIMER:
        queue_free()


func collided(body):
    if hit_something == false:
        if body.has_method("bullet_hit"):
            body.bullet_hit(BULLET_DAMAGE, global_transform)

    hit_something = true
    queue_free()

让我们看一下剧本:


首先,我们定义一些类变量:

  • BULLET_SPEED :子弹运动的速度。

  • BULLET_DAMAGE :子弹会对任何与之碰撞的物体造成伤害。

  • KILL_TIMER 子弹能持续多长时间而不打中任何东西。

  • timer :跟踪子弹存活时间的浮球。

  • hit_something :一个布尔值,用于跟踪我们是否击中了什么东西。

除了 timerhit_something 所有这些变量都会改变子弹与世界的互动方式。

注解

我们使用杀伤计时器的原因是,我们没有一个可以让子弹永远飞行的情况。通过使用杀戮计时器,我们可以确保没有子弹会永远旅行并消耗资源。

小技巧

如在 第1部分 ,我们有几个全部大写的类变量。这背后的原因与 第1部分 :我们希望将这些变量视为常量,但我们希望能够更改它们。在这种情况下,我们以后需要改变这些子弹的伤害和速度,所以我们需要它们是变量而不是常量。


_ready 我们设置了区域的 body_entered 向我们自己发出信号,以便它调用 collided 当身体进入该区域时起作用。


_physics_process 获取项目符号的本地 Z 轴。如果您在本地模式下查看场景,您会发现项目符号面向正本地 Z 轴。

下一步,我们将整个子弹转换成前进方向,乘以我们的速度和时间。

之后,我们将delta时间添加到计时器中,并检查计时器是否已达到大于或等于 KILL_TIME 常量。如果有,我们用 queue_free 释放子弹。


collided 我们检查一下我们有没有撞到什么东西。

记住这一点 collided 仅当主体已进入 Area 节点。如果子弹还没有与某个物体碰撞,我们将继续检查子弹与之碰撞的物体是否具有一个调用的函数/方法 bullet_hit . 如果是这样的话,我们就称之为它,并通过子弹的伤害和子弹的全局变换,这样我们就可以得到子弹的旋转和位置。

注解

在里面 collided ,传入的主体可以是 StaticBodyRigidBodyKinematicBody

我们设置子弹的 hit_something 变量到 true 因为不管子弹与之碰撞的物体是否 bullet_hit 函数/方法,它击中了一些东西,所以我们需要确保子弹没有击中任何其他东西。

然后我们用 queue_free .

小技巧

你可能想知道为什么我们 hit_something 变量如果我们用 queue_free 一碰到什么东西。

我们需要追踪是否撞到了什么东西的原因是 queue_free 不会立即释放节点,因此在Godot有机会释放之前,子弹可能会与另一个物体相撞。通过跟踪子弹是否击中了什么东西,我们可以确保子弹只击中一个物体。


在我们重新开始对播放器进行编程之前,让我们快速看一下 Player.tscn . 打开 Player.tscn 再一次。

展开 Rotation_Helper 注意它有两个节点: Gun_Fire_PointsGun_Aim_Point .

Gun_aim_point 就是子弹要瞄准的地方。注意它是如何与屏幕中心对齐并在Z轴上向前拉一段距离的。 Gun_aim_point 当子弹行进时,一定会与之相撞。

注解

有一个不可见的网格实例用于调试。网格是一个小球体,可以直观地显示子弹将瞄准的目标。

打开 Gun_Fire_Points 你会再找到三个 Spatial 节点,每个武器一个。

打开 Rifle_Point 你会发现 Raycast 节点。我们将在这里发射来复枪子弹的光线。光线投射的长度将决定我们的子弹将行进多远。

我们正在使用 Raycast 节点来处理步枪的子弹,因为我们想快速发射大量的子弹。如果我们使用bullet对象,很可能会在旧机器上遇到性能问题。

注解

如果你想知道这些点的位置是从哪里来的,它们是每种武器末端的大致位置。你可以通过 AnimationPlayer ,选择其中一个点火动画并在时间轴上拖动。每个武器的点应该与每个武器的末端对齐。

打开 Knife_Point 你会发现 Area 节点。我们正在使用 Area 因为我们只关心所有靠近我们的身体,因为我们的刀子不会射入太空。如果我们做一把飞刀,我们很可能会产生一个看起来像刀的子弹物体。

最后,我们有 Pistol_Point . 这就是我们将要创建/实例化子弹对象的地方。这里我们不需要任何额外的节点,因为子弹可以处理所有它自己的碰撞检测。

既然我们已经了解了如何处理我们的其他武器,以及在哪里产生子弹,那么我们就开始着手让它们发挥作用。

注解

如果需要,还可以查看HUD节点。除了用单曲外,没有什么特别的 Label ,我们将不会接触任何这些节点。检查 与控制节点的设计接口 有关使用GUI节点的教程。

制造第一件武器

让我们从手枪开始,为每件武器编写代码。

Select Pistol_Point (Player -> Rotation_Helper -> Gun_Fire_Points -> Pistol_Point) and create a new script called Weapon_Pistol.gd.

将以下代码添加到 Weapon_Pistol.gd

extends Spatial

const DAMAGE = 15

const IDLE_ANIM_NAME = "Pistol_idle"
const FIRE_ANIM_NAME = "Pistol_fire"

var is_weapon_enabled = false

var bullet_scene = preload("Bullet_Scene.tscn")

var player_node = null

func _ready():
    pass

func fire_weapon():
    var clone = bullet_scene.instance()
    var scene_root = get_tree().root.get_children()[0]
    scene_root.add_child(clone)

    clone.global_transform = self.global_transform
    clone.scale = Vector3(4, 4, 4)
    clone.BULLET_DAMAGE = DAMAGE

func equip_weapon():
    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        is_weapon_enabled = true
        return true

    if player_node.animation_manager.current_state == "Idle_unarmed":
        player_node.animation_manager.set_animation("Pistol_equip")

    return false

func unequip_weapon():
    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        if player_node.animation_manager.current_state != "Pistol_unequip":
            player_node.animation_manager.set_animation("Pistol_unequip")

    if player_node.animation_manager.current_state == "Idle_unarmed":
        is_weapon_enabled = false
        return true
    else:
        return false

让我们来看看脚本是如何工作的。


首先,我们在脚本中定义一些需要的类变量:

  • DAMAGE :单个子弹造成的伤害。

  • IDLE_ANIM_NAME :手枪闲置动画的名称。

  • FIRE_ANIM_NAME :手枪射击动画的名称。

  • is_weapon_enabled :用于检查此武器是否在使用/启用的变量。

  • bullet_scene :我们之前处理过的子弹场景。

  • player_node :要保存的变量 Player.gd .

我们之所以定义这些变量中的大多数,是因为我们可以在 Player.gd .

我们制造的每一种武器都会有这些变量(减 bullet_scene )所以我们有一个一致的接口来与 Player.gd . 通过在每个武器中使用相同的变量/函数,我们可以与它们交互而不必知道我们使用的是哪种武器,这使得我们的代码更加模块化,因为我们可以添加武器而不必更改其中的大部分代码。 Player.gd 它就可以工作了。

我们可以把所有的代码都写进去 Player.gd ,但之后 Player.gd 随着我们增加武器,管理将越来越困难。通过使用具有一致接口的模块化设计,我们可以 Player.gd 漂亮整洁,同时也使添加/删除/修改武器变得更容易。


_ready 我们只是简单地忽略了它。

不过,有一点值得注意,那就是假设我们将填写 Player.gd 在某个时刻。

我们假设 Player.gd 在调用 Weapon_Pistol.gd .

虽然这会导致球员无法传球(因为我们忘记了),但我们必须有一长串 get_parent 调用遍历场景树以检索播放器。这个看起来不漂亮 (get_parent().get_parent().get_parent() 等等),假设我们会记得把自己传给 Player.gd .


下一步让我们看看 fire_weapon

我们要做的第一件事就是举一个我们之前做的子弹场景。

小技巧

通过实例化场景,我们正在创建一个新节点,其中包含我们所实例化的场景中的所有节点,从而有效地克隆该场景。

然后我们添加一个 clone 到当前场景根的第一个子节点。通过这样做,我们将使它成为当前加载场景的根节点的子节点。

换句话说,我们正在添加 clone 作为当前加载/打开场景中第一个节点(场景树顶部的任何节点)的子节点。如果当前加载/打开的场景是 Testing_Area.tscn ,我们将添加 clone 作为一个子节点 Testing_Area ,该场景中的根节点。

警告

正如下面关于添加声音的小节中提到的,这个方法做了一个假设。这将在后面的“添加声音”部分中解释 第3部分

接下来,我们将克隆的全局转换设置为 Pistol_Aim_Point 的全局转换。我们这样做的原因是子弹是在手枪末端产生的。

你可以看到 Pistol_Aim_Point 通过单击 AnimationPlayer 滚动浏览 Pistol_fire . 你会发现枪口的位置或多或少在枪口处。

接下来我们把它放大一倍 4 因为子弹场景在默认情况下有点太小了。

然后我们设置子弹的伤害 (BULLET_DAMAGE )一枪子弹造成的伤害 (DAMAGE


现在让我们看看 equip_weapon

我们首先要做的是检查动画管理器是否在手枪的闲置动画中。如果我们在手枪的闲置动画中,我们就设置 is_weapon_enabledtrue 然后返回 true 因为手枪已经装备好了。

因为我们知道我们的手枪 equip 动画自动切换到手枪的空闲动画,如果我们在手枪的空闲动画中,手枪必须已完成设备动画的播放。

注解

我们知道这些动画将转换,因为我们编写了代码使它们在 Animation_Manager.gd

接下来,我们检查玩家是否在 Idle_unarmed 动画状态。因为所有未激活的动画都会进入这个状态,并且因为任何武器都可以从这个状态装备,所以我们将动画更改为 Pistol_equip 如果玩家在 Idle_unarmed 状态。

因为我们知道 Pistol_equip 将过渡到 Pistol_idle ,我们不需要再为装备武器做任何额外的处理,但是由于我们还不能装备手枪,所以我们回来了。 false .


最后,让我们看看 unequip_weapon

unequip_weapon 类似于 equip_weapon 但是相反,我们是反向检查。

首先,我们检查播放器是否处于空闲动画状态。然后我们检查以确保玩家不在 Pistol_unequip 动画。如果玩家不在 Pistol_unequip 动画,我们要播放 pistol_unequip 动画。

注解

你可能想知道为什么我们要检查玩家是否在手枪的闲置动画中,然后确保玩家不会在之后就失去平衡。追加支票的原因是我们(在极少数情况下)可以打电话 unequip_weapon 在我们有机会处理之前两次 set_animation ,因此我们添加了这个额外的检查,以确保unequip动画可以播放。

接下来,我们检查球员是否在 Idle_unarmed ,这是我们将要转换到的动画状态 Pistol_unequip . 如果玩家在 Idle_unarmed ,然后我们开始 is_weapon_enabledfalse 因为我们不再使用这个武器,然后返回 true 因为我们成功地解开了手枪。

如果玩家不在 Idle_unarmed ,我们回来了 false 因为我们还没有成功地拔出手枪。

制造另外两种武器

既然我们已经有了手枪所需的所有代码,接下来我们就添加步枪和刀的代码。

Select Rifle_Point (Player -> Rotation_Helper -> Gun_Fire_Points -> Rifle_Point) and create a new script called Weapon_Rifle.gd, then add the following:

extends Spatial

const DAMAGE = 4

const IDLE_ANIM_NAME = "Rifle_idle"
const FIRE_ANIM_NAME = "Rifle_fire"

var is_weapon_enabled = false

var player_node = null

func _ready():
    pass

func fire_weapon():
    var ray = $Ray_Cast
    ray.force_raycast_update()

    if ray.is_colliding():
        var body = ray.get_collider()

        if body == player_node:
            pass
        elif body.has_method("bullet_hit"):
            body.bullet_hit(DAMAGE, ray.global_transform)

func equip_weapon():
    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        is_weapon_enabled = true
        return true

    if player_node.animation_manager.current_state == "Idle_unarmed":
        player_node.animation_manager.set_animation("Rifle_equip")

    return false

func unequip_weapon():

    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        if player_node.animation_manager.current_state != "Rifle_unequip":
            player_node.animation_manager.set_animation("Rifle_unequip")

    if player_node.animation_manager.current_state == "Idle_unarmed":
        is_weapon_enabled = false
        return true

    return false

大部分都和 Weapon_Pistol.gd ,所以我们只想看看发生了什么变化: fire_weapon .

我们要做的第一件事就是 Raycast 节点,它是 Rifle_Point .

接下来我们强制 Raycast 使用更新 force_raycast_update . 这将迫使 Raycast 当我们称之为碰撞检测时,这意味着我们将与3D物理世界进行帧完美碰撞检查。

然后我们检查一下 Raycast 与某物相撞。

如果 Raycast 与某物发生碰撞,我们首先得到它与之碰撞的物体。这可能是 StaticBodyRigidBody ,或者 KinematicBody .

接下来,我们要确保我们碰撞的身体不是玩家,因为我们(可能)不想让玩家有能力向自己的脚开枪。

如果主体不是参与者,那么我们将检查它是否具有调用的函数/方法 bullet_hit . 如果是这样的话,我们就称之为它,并传递这个子弹造成的伤害。 (DAMAGE )和 Raycast 这样我们就能知道子弹是从哪个方向来的。


现在我们要做的就是写下刀的代码。

Select Knife_Point (Player -> Rotation_Helper -> Gun_Fire_Points -> Knife_Point) and create a new script called Weapon_Knife.gd, then add the following:

extends Spatial

const DAMAGE = 40

const IDLE_ANIM_NAME = "Knife_idle"
const FIRE_ANIM_NAME = "Knife_fire"

var is_weapon_enabled = false

var player_node = null

func _ready():
    pass

func fire_weapon():
    var area = $Area
    var bodies = area.get_overlapping_bodies()

    for body in bodies:
        if body == player_node:
            continue

        if body.has_method("bullet_hit"):
            body.bullet_hit(DAMAGE, area.global_transform)

func equip_weapon():
    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        is_weapon_enabled = true
        return true

    if player_node.animation_manager.current_state == "Idle_unarmed":
        player_node.animation_manager.set_animation("Knife_equip")

    return false

func unequip_weapon():

    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        player_node.animation_manager.set_animation("Knife_unequip")

    if player_node.animation_manager.current_state == "Idle_unarmed":
        is_weapon_enabled = false
        return true

    return false

和一样 Weapon_Rifle.gd 唯一的区别在于 fire_weapon ,让我们来看看:

我们要做的第一件事就是 Area 的子节点 Knife_Point .

接下来我们要把所有的碰撞体 Area 使用 get_overlapping_bodies . 这将返回每个与 Area .

接下来我们要检查每一具尸体。

首先我们检查确认身体不是玩家,因为我们不想让玩家能够刺伤自己。如果身体是玩家,我们使用 continue 所以我们跳下去看下一具尸体 bodies .

如果我们没有跳到下一具尸体上,我们会检查尸体是否有 bullet_hit 功能/方法。如果是这样的话,我们称之为“单刀划过的伤害” (DAMAGE )以及 Area .

注解

虽然我们可以尝试计算刀准确击中的大致位置,但我们不打算这样做,因为使用 Area 他的位置工作得很好,而计算每个身体大致位置所需的额外时间也不值得这么做。

使武器发挥作用

让我们开始让武器发挥作用吧 Player.gd .

首先,让我们从添加武器所需的一些类变量开始:

# Place before _ready
var animation_manager

var current_weapon_name = "UNARMED"
var weapons = {"UNARMED":null, "KNIFE":null, "PISTOL":null, "RIFLE":null}
const WEAPON_NUMBER_TO_NAME = {0:"UNARMED", 1:"KNIFE", 2:"PISTOL", 3:"RIFLE"}
const WEAPON_NAME_TO_NUMBER = {"UNARMED":0, "KNIFE":1, "PISTOL":2, "RIFLE":3}
var changing_weapon = false
var changing_weapon_name = "UNARMED"

var health = 100

var UI_status_label

让我们来看看这些新变量将做什么:

  • animation_manager :这将保存 AnimationPlayer 节点及其脚本,这是我们之前编写的。

  • current_weapon_name :我们当前使用的武器的名称。它有四个可能的值: UNARMEDKNIFEPISTOLRIFLE .

  • weapons :保存所有武器节点的字典。

  • WEAPON_NUMBER_TO_NAME :允许我们将武器编号转换为其名称的字典。我们用这个换武器。

  • WEAPON_NAME_TO_NUMBER :允许我们从武器名称转换为其编号的字典。我们用这个换武器。

  • changing_weapon :一个用于跟踪我们是否在更换枪支/武器的布尔值。

  • changing_weapon_name :我们要更改的武器的名称。

  • health :我们的玩家有多健康。在本部分教程中,我们将不使用它。

  • UI_status_label :一个标签,显示我们有多少健康,有多少弹药,我们的枪和储备。


接下来我们需要增加一些东西 _ready . 这是新的 _ready 功能:

func _ready():
    camera = $Rotation_Helper/Camera
    rotation_helper = $Rotation_Helper

    animation_manager = $Rotation_Helper/Model/Animation_Player
    animation_manager.callback_function = funcref(self, "fire_bullet")

    Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

    weapons["KNIFE"] = $Rotation_Helper/Gun_Fire_Points/Knife_Point
    weapons["PISTOL"] = $Rotation_Helper/Gun_Fire_Points/Pistol_Point
    weapons["RIFLE"] = $Rotation_Helper/Gun_Fire_Points/Rifle_Point

    var gun_aim_point_pos = $Rotation_Helper/Gun_Aim_Point.global_transform.origin

    for weapon in weapons:
        var weapon_node = weapons[weapon]
        if weapon_node != null:
            weapon_node.player_node = self
            weapon_node.look_at(gun_aim_point_pos, Vector3(0, 1, 0))
            weapon_node.rotate_object_local(Vector3(0, 1, 0), deg2rad(180))

    current_weapon_name = "UNARMED"
    changing_weapon_name = "UNARMED"

    UI_status_label = $HUD/Panel/Gun_label
    flashlight = $Rotation_Helper/Flashlight

让我们回顾一下发生了什么变化。

首先我们得到 AnimationPlayer 节点并将其分配给 animation_manager 变量。然后我们将回调函数设置为 FuncRef 那就叫做球员的 fire_bullet 功能。现在我们还没有写 fire_bullet 功能,但我们很快就到。

接下来,我们获取所有武器节点并将它们分配给 weapons . 这将允许我们只使用武器节点的名称访问它们 (KNIFEPISTOLRIFLE

然后我们得到 Gun_Aim_Point 的全球定位,以便我们可以旋转玩家的武器瞄准它。

然后我们把每件武器 weapons .

我们先得到武器节点。如果武器节点不是 null ,然后设置 player_node 此脚本的变量 (Player.gd )那我们就来看看 gun_aim_point_pos 使用 look_at 函数,然后将其旋转 180 学位 Y 轴。

注解

我们把所有的武器点旋转 180 他们的学位 Y 因为我们的相机是朝后的。如果我们不把所有的武器点旋转 180 度,所有的武器都会向后发射。

然后我们开始 current_weapon_namechanging_weapon_nameUNARMED .

最后,我们得到了用户界面 Label 从我们的抬头显示器。


让我们添加一个新的函数调用 _physics_process 所以我们可以换武器。以下是新代码:

func _physics_process(delta):
    process_input(delta)
    process_movement(delta)
    process_changing_weapons(delta)

现在我们打电话来 process_changing_weapons .


现在让我们添加所有玩家输入的武器代码 process_input . 添加以下代码:

# ----------------------------------
# Changing weapons.
var weapon_change_number = WEAPON_NAME_TO_NUMBER[current_weapon_name]

if Input.is_key_pressed(KEY_1):
    weapon_change_number = 0
if Input.is_key_pressed(KEY_2):
    weapon_change_number = 1
if Input.is_key_pressed(KEY_3):
    weapon_change_number = 2
if Input.is_key_pressed(KEY_4):
    weapon_change_number = 3

if Input.is_action_just_pressed("shift_weapon_positive"):
    weapon_change_number += 1
if Input.is_action_just_pressed("shift_weapon_negative"):
    weapon_change_number -= 1

weapon_change_number = clamp(weapon_change_number, 0, WEAPON_NUMBER_TO_NAME.size() - 1)

if changing_weapon == false:
    if WEAPON_NUMBER_TO_NAME[weapon_change_number] != current_weapon_name:
        changing_weapon_name = WEAPON_NUMBER_TO_NAME[weapon_change_number]
        changing_weapon = true
# ----------------------------------

# ----------------------------------
# Firing the weapons
if Input.is_action_pressed("fire"):
    if changing_weapon == false:
        var current_weapon = weapons[current_weapon_name]
        if current_weapon != null:
            if animation_manager.current_state == current_weapon.IDLE_ANIM_NAME:
                animation_manager.set_animation(current_weapon.FIRE_ANIM_NAME)
# ----------------------------------

让我们从我们如何更换武器开始,回顾一下附加的内容。

首先,我们得到当前武器的编号并将其分配给 weapon_change_number .

然后我们检查是否按了数字键(键1-4)。如果是的话,我们就定了 weapon_change_number 映射到该键处的值。

注解

原因键1映射到 0 是因为列表中的第一个元素映射到零,而不是一个。大多数编程语言中的大多数列表/数组访问器开始于 0 而不是 1 . 有关详细信息,请参阅https://en.wikipedia.org/wiki/Zero-based_numbering。

接下来我们检查一下 shift_weapon_positiveshift_weapon_negative 已按下。如果其中一个是,我们加/减 1weapon_change_number .

因为玩家可能已经移动了 weapon_change_number 在玩家拥有的武器数量之外,我们将其夹紧,使其不能超过玩家拥有的最大武器数量,并确保 weapon_change_number0 或者更多。

然后我们检查确保玩家没有更换武器。如果玩家不是,我们会检查玩家想要换成的武器是否是新武器,而不是玩家当前使用的武器。如果玩家想要换成的武器是一种新武器,我们就设置 changing_weapon_name 武器在 weapon_change_number 并设置 changing_weapon 成真。

为了发射武器,我们首先检查 fire 操作被按下。然后我们检查确保玩家没有更换武器。接下来我们得到当前武器的武器节点。

如果当前武器节点不等于空,并且玩家在其 IDLE_ANIM_NAME 状态,我们将玩家的动画设置为当前武器的 FIRE_ANIM_NAME .


让我们添加 process_changing_weapons 下一步。

添加以下代码:

func process_changing_weapons(delta):
    if changing_weapon == true:

        var weapon_unequipped = false
        var current_weapon = weapons[current_weapon_name]

        if current_weapon == null:
            weapon_unequipped = true
        else:
            if current_weapon.is_weapon_enabled == true:
                weapon_unequipped = current_weapon.unequip_weapon()
            else:
                weapon_unequipped = true

        if weapon_unequipped == true:

            var weapon_equipped = false
            var weapon_to_equip = weapons[changing_weapon_name]

            if weapon_to_equip == null:
                weapon_equipped = true
            else:
                if weapon_to_equip.is_weapon_enabled == false:
                    weapon_equipped = weapon_to_equip.equip_weapon()
                else:
                    weapon_equipped = true

            if weapon_equipped == true:
                changing_weapon = false
                current_weapon_name = changing_weapon_name
                changing_weapon_name = ""

让我们来看看这里发生了什么:

我们要做的第一件事就是确保我们已经收到了改变武器的信息。我们通过确保 changing_weaponstrue .

接下来我们定义一个变量 (weapon_unequipped )所以我们可以检查当前的武器是否成功地未装备。

然后我们从 weapons .

如果现在的武器不是 null ,然后我们需要检查武器是否启用。如果武器被启用,我们称之为 unequip_weapon 函数,这样它将启动unequip动画。如果武器未启用,我们设置 weapon_unequippedtrue 因为武器已经成功地未装备。

如果现在的武器是 null ,然后我们可以简单地设置 weapon_unequippedtrue . 我们进行此检查的原因是没有武器脚本/节点 UNARMED ,但也没有动画 UNARMED 所以我们可以开始装备玩家想要的武器。

如果玩家成功地解除了当前武器的装备 (weapon_unequipped == true )我们需要装备新武器。

首先我们定义一个新的变量 (weapon_equipped )用于跟踪玩家是否成功装备了新武器。

然后我们得到玩家想要换成的武器。如果玩家想换成的武器不是 null 然后我们检查它是否启用。如果未启用,我们将其称为 equip_weapon 所以它开始装备武器。如果武器被启用,我们设置 weapon_equippedtrue .

如果玩家想换成的武器是 null 我们只需设置 weapon_equippedtrue 因为我们没有任何节点/脚本 UNARMED 我们也没有任何动画。

最后,我们检查玩家是否成功装备了新武器。如果他这样做了,我们就开始 changing_weaponfalse 因为玩家不再更换武器。我们也设置了 current_weapon_namechanging_weapon_name 既然现在的武器变了,然后我们就开始 changing_weapon_name 到空字符串。


现在,我们需要给玩家增加一个功能,然后玩家就可以开始发射武器了!

我们需要补充 fire_bullet ,将由 AnimationPlayer 在那些时候,我们在 AnimationPlayer 功能轨迹:

func fire_bullet():
    if changing_weapon == true:
        return

    weapons[current_weapon_name].fire_weapon()

我们来看看这个函数的作用:

首先,我们检查玩家是否在更换武器。如果玩家在换武器,我们不想射击,所以我们 return .

小技巧

调用 return 停止调用函数的其余部分。在这种情况下,我们不会返回变量,因为我们只对不运行其余代码感兴趣,并且在调用此函数时也不会查找返回的变量。

然后我们通过调用它来告诉玩家当前使用的武器 fire_weapon 功能。

小技巧

还记得我们提到的射击动画的速度比其他动画快吗?通过改变射击动画的速度,你可以改变武器发射子弹的速度!


在我们准备测试新武器之前,我们还有一点工作要做。

创建一些测试对象

通过转到脚本窗口,单击“文件”,然后选择“新建”来创建新脚本。命名此脚本 RigidBody_hit_test 确保它延伸 RigidBody .

现在我们需要添加此代码:

extends RigidBody

const BASE_BULLET_BOOST = 9;

func _ready():
    pass

func bullet_hit(damage, bullet_global_trans):
    var direction_vect = bullet_global_trans.basis.z.normalized() * BASE_BULLET_BOOST;

    apply_impulse((bullet_global_trans.origin - global_transform.origin).normalized(), direction_vect * damage)

让我们来看看 bullet_hit 作品:

首先,我们得到子弹的前进方向向量。这样我们就可以知道子弹将从哪个方向击中 RigidBody . 我们用这个来推动 RigidBody 与子弹的方向相同。

注解

我们需要通过 BASE_BULLET_BOOST 所以子弹装得更厉害,移动 RigidBody 以可见方式显示节点。你可以设定 BASE_BULLET_BOOST 当项目符号与 RigidBody .

然后我们用 apply_impulse .

首先,我们需要计算脉冲的位置。因为 apply_impulse 获取相对于 RigidBody ,我们需要计算 RigidBody 为了子弹。我们通过减去 RigidBody 从项目符号的全局原点/位置开始的全局原点/位置。这让我们距离 RigidBody 为了子弹。我们规范化这个向量,这样碰撞器的大小就不会影响子弹移动 RigidBody .

最后,我们需要计算冲量的力。为此,我们使用子弹所面对的方向,并乘以子弹的伤害。这是一个很好的结果,对于更强大的子弹,我们会得到更强大的结果。


现在我们需要将这个脚本附加到 RigidBody 我们想要影响的节点。

打开 Testing_Area.tscn 并选择所有作为父对象的多维数据集 Cubes 节点。

小技巧

如果选择顶部立方体,然后按住 shift 并选择最后一个立方体,Godot将选择之间的所有立方体!

一旦选定了所有的多维数据集,就可以在检查器中向下滚动,直到进入“脚本”部分。单击下拉列表并选择“加载”。打开新创建的 RigidBody_hit_test.gd 脚本。

最后的注释

../../../_images/PartTwoFinished.png

那是很多代码!但是现在,做了这些,你就可以去测试你的武器了!

你现在应该能够向立方体发射尽可能多的子弹,它们会随着子弹的碰撞而移动。

第3部分 我们会给武器加弹药,还有一些声音!

警告

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

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