第2部分

Part overview

在这部分中,我们将为游戏角色提供武器。

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

到这部分结束时,您将拥有一个可以使用小刀发射手枪,步枪和攻击的游戏角色。 游戏角色现在还将拥有过渡动画,并且武器将与环境中的对象进行交互。

注解

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

让我们开始吧!

制作系统来处理动画

首先,我们需要一种方法来处理不断变化的动画。 打开 Player.tscn 并选择 AnimationPlayer Node(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: A dictionary for holding all the speeds at which we want to play our animations.
  • current_state:一个变量,用于保存我们当前所处的动画状态的名称。
  • callback_function:用于保存回调函数的变量。 (以下进一步说明)

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

states is a dictionary with the key being the name of the current state, and the value being an array holding all the animations (states) we can transition to. For example, if we are currently in the Idle_unarmed state, we can only transition to Knife_equip, Pistol_equip, Rifle_equip, and Idle_unarmed.

如果我们尝试转换到未包含在我们所处状态的可能转换状态中的状态,那么我们会收到警告消息并且动画不会更改。 我们也可以自动从一些状态转换到其他状态,这将在下面的“animation_ended”中进一步解释

注解

For the sake of keeping this tutorial simple, we are not using a ‘proper’ state machine. If you are interested to know more about state machines, see the following articles:

  • (Python示例)https://dev.to/karn/building-a-simple-state-machine-in-python
  • (C#示例)https://www.codeproject.com/Articles/489136/UnderstandingplusandplusImplementingplusStateplusP
  • (维基文章)https://en.wikipedia.org/wiki/Finite-state_machine

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

小技巧

请注意,所有触发动画都比正常速度快。 请记住以后再说!

``current_state``将保存我们当前所处的动画状态的名称。

最后, callback_function 将是一个 FuncRef 由游戏角色传入,用于在适当的动画帧中生成子弹。 答 FuncRef 允许我们传递一个函数作为参数,有效地允许我们从另一个脚本调用一个函数,这是我们以后使用它的方式。


Now let’s look at _ready.

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

接下来,我们将 animation_finished 信号连接到此脚本并将其指定为调用 animation_ended 。 这意味着每当动画完成时,都会调用 animation_ended


让我们看看下面的 set_animation

set_animation``将动画更改为名为``animation_name *的动画*如果*我们可以转换到它。 换句话说,如果我们当前所处的动画状态在“states”中有传递的动画状态名称,那么我们将更改为该动画。

Firstly, we check if the passed in animation name is the same name as the animation currently playing. If they are the same, then we write a warning to the console and return true.

Secondly, we see if AnimationPlayer has the animation with the name animation_name using has_animation. If it does not, we return false.

Thirdly, we check whether current_state is set. If current_state is not currently set, then we set current_state to the passed in animation name and tell AnimationPlayer to start playing the animation with a blend time of -1 at the speed set in animation_speeds and then we return true.

注解

混合时间是将两个动画混合/混合多长时间。

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

如果您输入一个“1”的值,一秒钟后新动画将以增加的力量播放,将两个动画混合在一起一秒钟,然后再播放新动画。 这导致动画之间的平滑过渡,当您从步行动画更改为正在运行的动画时,这看起来很棒。

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

If we have a state in current_state, then we get all the possible states we can transition to.

如果动画名称在可能的转换列表中,我们将 current_state 设置为传入的动画(animation_name),告诉 AnimationPlayer 以混合时间播放动画 在 animation_speeds 中设置的速度为`-1 -1`并返回 true


现在让我们来看看 animation_ended

``animation_ended``是一个函数,它将被调用 AnimationPlayer 当它完成播放动画时。

For certain animation states, we may need to transition into another state when it’s finished. To handle this, we check for every possible animation state. If we need to, we will transition into another state.

警告

如果您使用自己的动画模型,请确保没有动画设置为循环。 循环动画在到达动画结束时不会发送 animation_finished 信号,并且即将再循环。

注解

The transitions in animation_ended would ideally be part of the data in states, but in an effort to make the tutorial easier to understand, we’ll hard code each state transition in animation_ended.


Finally, there is animation_callback. This function will be called by a call method track in our animations. If we have a FuncRef assigned to callback_function, then we call that passed in function. If we do not have a FuncRef assigned to callback_function, we print out a warning to the console.

小技巧

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

准备好动画

Now that we have a working animation manager, we need to call it from our player script. Before that, though, we need to set some animation callback tracks in our firing animations.

打开 Player.tscn 如果您没有打开并导航到 AnimationPlayer node(Player ->` Rotation_Helper` ->` Model ` ->` Animation_Player`)。

We need to attach a call method track to three of our animations: The firing animation for the pistol, rifle, and knife. Let’s start with the pistol. Click the animation drop down list and select “Pistol_fire”.

Now scroll down to the bottom of the list of animation tracks. The final item in the list should read Armature/Skeleton:Left_UpperPointer. Now above the list, click the “Add track” button, to the left of the time line

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

This will bring up a window with a few choices. We want to add a call method track, so click the option that reads “Call Method Track”. This will open a window showing the entire node tree. Navigate to the AnimationPlayer node, select it, and press OK.

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

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

注解

The timeline is the window where all the points in our animation are stored. Each of the little points represents a point of animation data.

擦洗时间轴意味着让我们自己完成动画。 因此,当我们说“擦洗时间线直到达到某个点”时,我们的意思是在动画窗口中移动,直到到达时间轴上的点。

而且,枪口是枪弹出来的终点。 枪口闪光是当子弹射击时逃离枪口的闪光。 枪口有时也被称为枪管。

小技巧

要在删除时间线时进行更精细的控制,请按“控制”并使用鼠标滚轮向前滚动以放大。向后滚动将缩小。

您还可以通过将``Step(s)```中的值更改为更低/更高的值来更改时间线清理捕捉的方式。

Once you get to a point you like, right click on the row for “Animation Player” and press insert key. In the empty name field, enter animation_callback and press enter.

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

Now when we are playing this animation the call method track will be triggered at that specific point of the animation.


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

注解

因为这个过程与手枪完全相同,所以这个过程的解释会稍微深入一点。 如果您迷路,请按照上面的步骤! 它完全相同,只是在不同的动画上。

Go to the “Rifle_fire” animation from the animation drop down. Add the call method track once you reach the bottom of the animation track list by clicking the “Add Track” button above the list. Find the point where the muzzle starts to flash and right click and press Insert Key to add a call method track point at that position on the track.

在弹出窗口的名称字段中输入“动画_回调”,然后按“回车”键。

Now we need to apply the callback method track to the knife animation. Select the “Knife_fire” animation and scroll to the bottom of the animation tracks. Click the “Add Track” button above the list and add a method track. Next find a point around the first third of the animation to place the animation callback method point at.

注解

我们实际上不会开枪,动画是一个刺伤动画而不是射击动画。 在本教程中,我们重复使用枪械射击逻辑,因此动画的命名风格与其他动画一致。

从那里右键单击时间轴并单击“插入关键帧”。将“动画_回调”放入名称字段并按“回车”。

小技巧

一定要保存您的工作!

With that done, we are almost ready to start adding the ability to fire to our player script! We need to set up one last scene: The scene for our bullet object.

创建子弹场景

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


两种方法之一是使用子弹对象。 这将是一个穿越世界并处理自己的碰撞代码的对象。 在这种方法中,我们在枪的方向上创建/生成一个子弹对象,然后向前行进。

这种方法有几个优点。 首先,我们不必将子弹存储在我们的游戏角色中。 我们可以简单地创建子弹然后继续前进,子弹本身用手柄检查碰撞,将正确的信号发送到它碰撞的对象,并自行销毁。

Another advantage is we can have more complex bullet movement. If we want to make the bullet fall ever so slightly as time goes on, we can make the bullet controlling script slowly push the bullet towards the ground. Using an object also makes the bullet take time to reach its target, it doesn’t instantly hit whatever it’s pointed at. This feels more realistic because nothing in real life moves instantly from one point to another.

性能的一个巨大缺点。 虽然让每个子弹计算他们自己的路径并处理他们自己的碰撞可以提供很大的灵活性,但这需要以性能为代价。 通过这种方法,我们每一步计算每个子弹的运动,虽然这可能不是几十个子弹的问题,但当您可能有几百个子弹时,它可能会成为一个巨大的问题。

尽管性能受到了影响,但许多第一人称射击游戏包括某种形式的物体子弹。 火箭发射器是一个很好的示例,因为在许多第一人称射击游戏中,火箭不会立即在目标位置爆炸。 您也可以用手榴弹多次发现子弹作为物体,因为它们通常会在爆炸前在世界各地反弹。

注解

虽然我不能肯定地说是这种情况,但这些游戏 可能 以某种形式使用子弹物体:(这些完全来自我的观察。 它们可能完全错误 。我从未参与** 任何**以下游戏)

  • Halo (Rocket launchers, fragmentation grenades, sniper rifles, brute shot, and more)
  • 命运(火箭发射器,手榴弹,聚变步枪,狙击步枪,超级动作等)
  • 使命召唤(火箭发射器,手榴弹,弹道刀,弩等)
  • 战场(火箭发射器,手榴弹,claymores,迫击炮等)

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

虽然我们没有实现任何形式的网络(因为它将在其自己的整个教程系列中),但在创建第一人称射击游戏时要牢记这一点,特别是如果您计划在未来添加某种形式的网络。


The other way of handling bullet collisions we will be looking at is raycasting.

这种方法在具有快速移动的子弹的枪支中非常常见,这些子弹很少随时间改变轨道。

我们不是创建一个子弹对象并通过空间发送它,而是从枪的枪管/枪口向前发送一条射线。 我们将光线投射的原点设置为子弹的起始位置,并根据长度调整子弹在空间中``行进``的距离。

注解

虽然我不能肯定地说是这种情况,但这些游戏*可能会以某种形式使用光线投射:(这些完全来自我的观察。 它们可能完全错误 。我从来没有工作**任何 **以下游戏)

  • Halo(突击步枪,DMR,战斗步枪,契约卡宾枪,斯巴达激光等)
  • 命运(自动步枪,脉冲步枪,侦察步枪,手枪,机关枪等)
  • 使命召唤(突击步枪,轻型机枪,子机枪,手枪等)
  • 战场(突击步枪,SMG,卡宾枪,手枪等)

One huge advantage of this method is that it’s light on performance. Sending a couple hundred rays through space is much easier for the computer to calculate than sending a couple hundred bullet objects.

另一个优点是我们可以立即知道我们是否在确实遇到了什么,或者当我们要求它时。 对于网络而言,这很重要,因为我们不需要通过互联网同步子弹移动,我们只需要发送光线投射。

Raycasting does have some disadvantages, though. One major disadvantage is we cannot easily cast a ray in anything but a linear line. This means we can only fire in a straight line for however long our ray length is. You can create the illusion of bullet movement by casting multiple rays at different positions, but not only is this hard to implement in code, it is also heavier on performance.

另一个缺点是我们看不到子弹。 对于子弹物体,我们实际上可以看到子弹穿过空间,如果我们将一个网格附加到它上面,但由于光线投射立即发生,我们没有一个体面的方式来显示子弹。 您可以从光线投射的原点到光线投射相撞的点绘制一条线,这是显示光线投影的一种流行方式。 另一种方法是根本不绘制光线投射,因为从理论上讲,子弹移动得如此之快,我们的眼睛无论如何都看不到它。


Let’s get the bullet object set up. This is what our pistol will create when the “Pistol_fire” animation callback function is called.

打开 Bullet_Scene.tscn 。 场景包含 Spatial 节点名为bullet,带有 MeshInstanceArea 带有 CollisionShape children 它。

创建一个名为 Bullet_script.gd 的新脚本并将其附加到``Bullet`` Spatial

我们将在根部移动整个子弹对象(Bullet)。 我们将使用 Area 来检查我们是否与某些东西相撞

注解

Why are we using an Area and not a RigidBody? The main reason we’re not using a RigidBody is because we do not want the bullet to interact with other RigidBody nodes. By using an Area we are ensuring that none of the other RigidBody nodes, including other bullets, will be effected.

另一个原因很简单,因为用以下方法检测碰撞更容易 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()

Let’s go through the script:


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

  • BULLET_SPEED: The speed at which the bullet travels.
  • BULLET_DAMAGE: The damage the bullet will cause to anything with which it collides.
  • KILL_TIMER:子弹可以持续多久而不击中任何东西。
  • timer:一个用于跟踪子弹活着多久的浮子。
  • hit_something:一个布尔值,用于跟踪我们是否击中了某些东西。

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

注解

The reason we are using a kill timer is so we do not have a case where we get a bullet travelling forever. By using a kill timer, we can ensure that no bullets will travel forever and consume resources.

小技巧

第1部分,我们有几个全部大写的类变量。 这背后的原因与下面给出的原因相同 第1部分:我们希望将这些变量视为常量,但我们希望能够更改它们。 在这种情况下,我们稍后需要更改这些子弹的损坏和速度,因此我们需要它们是变量而不是常量。


_ready 中,我们将区域的 body_entered 信号设置为我们自己,以便在物体进入该区域时调用 collided 函数。


_physics_process gets the bullet’s local Z axis. If you look at the scene in local mode, you will find that the bullet faces the positive local Z axis.

接下来,我们按照前进方向翻译整个项目符号,乘以我们的速度和增量时间。

After that we add delta time to our timer and check whether the timer has reached a value as big or greater than our KILL_TIME constant. If it has, we use queue_free to free the bullet.


In collided we check whether we’ve hit something yet.

请记住,只有当一个实体进入 Area 节点时才会调用 collided 。 如果子弹尚未与某些东西发生碰撞,我们将继续检查子弹发生碰撞的物体是否具有名为“bullet_hit”的功能/方法。 如果是这样,我们称之为并传递子弹的伤害和子弹的全局变换,这样我们就可以获得子弹的旋转和位置。

注解

collided 中,传入的物体可以是 StaticBodyRigidBody,或者 KinematicBody

我们将子弹的 hit_something 变量设置为 true ,因为无论子弹碰撞的物体是否具有 bullet_hit 函数/方法,它都会击中某些东西,所以我们需要制作 确定子弹没有击中任何其他东西。

然后我们使用 queue_free 释放子弹。

小技巧

您可能想知道为什么我们甚至有一个 hit_something 变量,如果我们在命中某事时立即使用 queue_free 释放子弹。

The reason we need to track whether we’ve hit something or not is because queue_free does not immediately free the node, so the bullet could collide with another body before Godot has a chance to free it. By tracking whether the bullet has hit something, we can make sure that the bullet will only hit one object.


在我们再次开始编程游戏角色之前,让我们快速看一下 Player.tscn 。 再次打开 Player.tscn

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

``Gun_aim_point``是子弹瞄准的点。 注意它是如何与屏幕中心对齐并在Z轴上向前拉一段距离。 ``Gun_aim_point``将作为子弹在进行时肯定会碰撞的点。

注解

There is a invisible mesh instance for debugging purposes. The mesh is a small sphere that visually shows at which target the bullets will be aiming.

打开 Gun_Fire_Points ,您会发现另外三个 Spatial 节点,每个武器一个。

打开 Rifle_Point 您会找到一个 Raycast 节点。 这是我们将为我们的步枪的子弹发送光线投射的地方。 光线投射的长度将决定我们的子弹将走多远。

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

注解

If you are wondering from where the positions of the points came, they are the rough positions of the ends of each weapon. You can see this by going to AnimationPlayer, selecting one of the firing animations and scrubbing through the timeline. The point for each weapon should mostly line up with the end of each weapon.

Open up Knife_Point and you’ll find an Area node. We are using an Area for the knife because we only care for all the bodies close to us, and because our knife does not fire into space. If we were making a throwing knife, we would likely spawn a bullet object that looks like a knife.

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

现在我们已经看到了我们将如何处理我们的其他武器,以及我们将在哪里产生子弹,让我们开始努力让它们发挥作用。

注解

如果需要,您还可以查看HUD节点。 除了使用单个参考之外没有任何花哨的东西 Label,我们不会触及任何这些节点。 检查 :ref:`doc_design_interfaces_with_the_control_nodes`获取有关使用GUI节点的教程。

创造第一个武器

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

选择 Pistol_Point (Player ->` Rotation_Helper` ->` Gun_Fire_Points` ->` Pistol_Point`)并创建一个名为``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 中使用它们。

Each of the weapons we’ll make will have all these variables (minus bullet_scene) so we have a consistent interface to interact with in Player.gd. By using the same variables/functions in each weapon, we can interact with them without having to know which weapon we are using, which makes our code much more modular because we can add weapons without having to change much of the code in Player.gd and it will just work.

We could write all the code in Player.gd, but then Player.gd will get increasingly harder to manage as we add weapons. By using a modular design with a consistent interface, we can keep Player.gd nice and neat, while also making it easier to add/remove/modify weapons.


_ready 中我们简单地将它传递过来。

但有一点值得注意,我们假设我们会在某些时候填写“Player.gd”。

我们假设 Player.gd 会在调用 Weapon_Pistol.gd 中的任何函数之前自行传递。

虽然这可能导致游戏角色没有进入自己的情况(因为我们忘记),但我们必须有一长串 get_parent 调用来遍历场景树以检索游戏角色。 这看起来不太漂亮(``get_parent()。get_parent()。get_parent()``依此类推)假设我们会记得将自己传递给``Player.gd``中的每个武器,这是相对安全的。


接下来让我们看看``fire_weapon``:

我们做的第一件事就是我们之前制作的子弹场景。

小技巧

By instancing the scene, we are creating a new node holding all the node(s) in the scene we instanced, effectively cloning that scene.

Then we add a clone to the first child node of the root of the scene we are currently in. By doing this, we’re making it a child of the root node of the currently loaded scene.

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

警告

As mentioned later below in the section on adding sounds, this method makes an assumption. This will be explained later in the section on adding sounds in 第3部分

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

You can see that Pistol_Aim_Point is positioned right at the end of the pistol by clicking the AnimationPlayer and scrolling through Pistol_fire. You’ll find the position is more or less at the end of the pistol when it fires.

接下来我们将它扩大一倍因为“4”,因为子弹场景默认情况下有点太小了。

Then we set the bullet’s damage (BULLET_DAMAGE) to the amount of damage a single pistol bullet does (DAMAGE).


现在让我们来看看`equip_weapon``:

The first thing we do is check to see whether the animation manager is in the pistol’s idle animation. If we are in the pistol’s idle animation, we set is_weapon_enabled to true and return true because the pistol has successfully been equipped.

因为我们知道我们的手枪的“装备”动画会自动转换为手枪的空闲动画,如果我们在手枪的空闲动画中,手枪必须完成播放装备动画。

注解

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

接下来我们检查游戏角色是否处于“Idle_unarmed”动画状态。 因为所有非装备动画都会进入这种状态,并且因为任何武器都可以从这种状态装备,所以如果游戏角色处于“Idle_unarmed”状态,我们会将动画更改为“Pistol_equip”。

既然我们知道 Pistol_equip 将转换为 Pistol_idle ,我们不需要再为武器配备额外的处理,但由于我们还没能装备手枪,我们返回 false


最后,让我们看看 unequip_weapon:

unequip_weapon``类似于``equip_weapon,但我们却反过来检查。

First we check to see whether the player is in the idle animation state. Then we check to make sure the player is not in the Pistol_unequip animation. If the player is not in the Pistol_unequip animation, we want to play the pistol_unequip animation.

注解

You may be wondering why we are checking to see whether the player is in the pistol’s idle animation, and then making sure the player is not unequipping right after. The reason behind the additional check is because we could (in rare cases) call unequip_weapon twice before we’ve had a chance to process set_animation, so we add this additional check to make sure the unequip animation plays.

Next we check to see whether the player is in Idle_unarmed, which is the animation state we will transition into from Pistol_unequip. If the player is in Idle_unarmed, then we set is_weapon_enabled to false since we are no longer using this weapon, and return true because we have successfully unequipped the pistol.

如果游戏角色不在“Idle_unarmed”中,我们会返回“false”,因为我们尚未成功装备手枪。

制造另外两种武器

Now that we have all the code we’ll need for the pistol, let’s add the code for the rifle and knife next.

选择 Rifle_Point (Player ->` Rotation_Helper` ->` Gun_Fire_Points` ->` Rifle_Point`)并创建一个名为``Weapon_Rifle.gd``的新脚本,然后添加 下列:

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 的子节点。

接下来我们使用 force_raycast_update 强制执行 Raycast 更新。 这将迫使 Raycast 在我们调用它时检测碰撞,这意味着我们可以与3D物理世界进行帧完美碰撞检查。

然后我们检查 Raycast 是否与某些东西相撞。

如果 Raycast 与某些东西相撞,我们首先得到它碰撞的碰撞体。 这可以是 StaticBodyRigidBody,或者a KinematicBody

接下来我们要确保我们碰到的物体不是游戏角色,因为我们(可能)不想让游戏角色有能力在脚下射击。

If the body is not the player, we then check to see if it has a function/method called bullet_hit. If it does, we call it and pass in the amount of damage this bullet does (DAMAGE), and the global transform of the Raycast so we can tell from which direction the bullet came.


现在我们需要做的就是为刀编写代码。

选择 Knife_Point (Player ->` Rotation_Helper` ->` Gun_Fire_Points` ->` Knife_Point`)并创建一个名为``Weapon_Knife.gd``的新脚本,然后添加 下列:

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 ,所以让我们看一下:

我们要做的第一件事就是得到 Knife_PointArea 子节点。

Next we want to get all the collision bodies inside the Area using get_overlapping_bodies. This will return a list of every body that touches the Area.

我们接下来想要浏览每一个机构。

首先我们检查以确保物体不是游戏角色,因为我们不想让游戏角色能够刺伤自己。 如果物体是游戏角色,我们使用 continue ,所以我们跳过并看着 bodies 中的下一个物体。

如果我们没有跳到下一个物体,我们检查物体是否有 bullet_hit 函数/方法。 如果确实如此,我们称之为,传递单刀划动所造成的伤害量(DAMAGE)和全局变换 Area

注解

虽然我们可以尝试计算刀准确击中的粗略位置,但我们不会这样做,因为使用 Area 的位置运行良好,并且计算粗略位置所需的额外时间 每个人都不值得努力。

制造武器

Let’s start making the weapons work in Player.gd.

First let’s start by adding some class variables we’ll need for the weapons:

# 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

Let’s go over what these new variables will do:

  • animation_manager:这将保存 AnimationPlayer 节点及其脚本,我们之前写过。
  • current_weapon_name:我们当前使用的武器的名称。 它有四个可能的值: UNARMEDKNIFEPISTOLRIFLE
  • weapons: A dictionary that will hold all the weapon nodes.
  • WEAPON_NUMBER_TO_NAME:允许我们从武器编号转换为其名称的字典。 我们将用它来换武器。
  • WEAPON_NAME_TO_NUMBER:一个字典,允许我们从武器的名称转换为它的号码。 我们将用它来换武器。
  • changing_weapon:一个布尔值,用于跟踪我们是否正在改变枪支/武器。
  • changing_weapon_name:我们想要改变的武器的名称。
  • health:我们的球员有多少健康。 在本教程的这一部分中,我们将不会使用它。
  • UI_status_label: A label to show how much health we have, and how much ammo we have both in our gun and in reserve.

接下来我们需要在 _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 函数,但我们很快就会到达那里。

Next we get all the weapon nodes and assign them to weapons. This will allow us to access the weapon nodes only with their name (KNIFE, PISTOL, or RIFLE).

然后我们得到 Gun_Aim_Point 的全球位置,这样我们就可以旋转游戏角色的武器来瞄准它。

然后我们通过“武器”中的每一件武器。

我们首先得到武器节点。 如果武器节点不是“null”,那么我们将它的 player_node 变量设置为这个脚本(Player.gd)。 然后我们使用 look_at 函数查看 gun_aim_point_pos ,然后在 Y 轴上旋转 180 度。

注解

我们将所有这些武器点在它们的“Y”轴上旋转“180”度,因为我们的相机指向后方。 如果我们没有将所有这些武器点旋转“180”度,那么所有武器都会向后射击。

然后我们将 current_weapon_namechanging_weapon_name 设置为 UNARMED

最后,我们从我们的HUD获取UI Label


Let’s add a new function call to _physics_process so we can change weapons. Here’s the new code:

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

现在我们将调用 process_changing_weapons


Now let’s add all the player input code for the weapons in process_input. Add the following code:

# ----------------------------------
# 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)
# ----------------------------------

Let’s go over the additions, starting with how we’re changing weapons.

首先,我们得到当前武器的数字并将其分配给 weapon_change_number

然后我们检查是否按下了任何数字键(键1-4)。 如果是,我们将 weapon_change_number 设置为该键映射的值。

注解

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

Next we check to see if shift_weapon_positive or shift_weapon_negative is pressed. If one of them is, we add/subtract 1 from weapon_change_number.

因为游戏角色可能已经在游戏角色拥有的武器数量之外移动了 weapon_change_number ,我们将其钳制,使其不能超过游戏角色拥有的最大武器数量,并确保 weapon_change_number 为“0”。 或者更多。

Then we check to make sure the player is not already changing weapons. If the player is not, we then check to see if the weapon the player wants to change to is a new weapon and not the weapon the player is currently using. If the weapon the player is wanting to change to is a new weapon, we then set changing_weapon_name to the weapon at weapon_change_number and set changing_weapon to true.

为了发射武器,我们首先检查是否按下了 fire 动作。 然后我们检查确保游戏角色没有更换武器。 接下来,我们获得当前武器的武器节点。

If the current weapon node does not equal null, and the player is in its IDLE_ANIM_NAME state, we set the player’s animation to the current weapon’s FIRE_ANIM_NAME.


Let’s add process_changing_weapons next.

添加以下代码:

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),这样我们就可以检查当前的武器是否已成功装备。

然后我们从“武器”中获取当前的武器。

If the current weapon is not null, then we need to check whether the weapon is enabled. If the weapon is enabled, we call its unequip_weapon function so it will start the unequip animation. If the weapon is not enabled, we set weapon_unequipped to true because the weapon has successfully been unequipped.

如果当前武器是“null”,那么我们可以简单地将 weapon_unequipped 设置为 true 。 我们做这个检查的原因是因为 UNARMED 没有武器脚本/节点,但是 UNARMED 也没有动画,所以我们可以开始装备游戏角色想要改变的武器。

如果游戏角色已成功装备当前武器(weapon_unequipped == true),我们需要装备新武器。

首先,我们定义一个新变量(weapon_equipped),用于跟踪游戏角色是否成功装备了新武器。

然后我们得到游戏角色想要改变的武器。 如果游戏角色想要改变的武器不是“空”,那么我们检查它是否被启用。 如果它没有启用,我们称其为 equip_weapon 函数,因此它开始装备武器。 如果武器已启用,我们将 weapon_equipped 设置为 true

如果游戏角色想要改变的武器是“null”,我们只需将 weapon_equipped 设置为 true ,因为我们没有“UNARMED”的任何节点/脚本,我们也没有 任何动画。

Finally, we check to see whether the player has successfully equipped the new weapon. If (s)he has done so, we set changing_weapon to false because the player is no longer changing weapons. We also set current_weapon_name to changing_weapon_name since the current weapon has changed, and then we set changing_weapon_name to an empty string.


现在,我们需要为游戏角色增加一个功能,然后游戏角色就可以开始射击武器!

我们需要添加 fire_bullet ,它将由 AnimationPlayer 调用,我们在前面设置的那些点 AnimationPlayer 函数轨道:

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

    weapons[current_weapon_name].fire_weapon()

Let’s go over what this function does:

First we check to see whether the player is changing weapons. If the player is changing weapons, we do not want shoot, so we 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)

Let’s go over how bullet_hit works:

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

注解

我们需要通过 BASE_BULLET_BOOST 来增加方向向量,这样子弹可以打包更多,并以可见的方式移动 RigidBody 节点。 如果在子弹与 RigidBody 发生冲突时想要更少或更多的反应,您可以将 BASE_BULLET_BOOST 设置为更低或更高的值。

然后我们使用 apply_impulse 来施加冲动。

首先,我们需要计算冲动的位置。 因为 apply_impulse 采用相对于 RigidBody 到子弹的距离。 我们通过从子弹的全局原点/位置减去 RigidBody 到子弹的距离。 我们规范化这个向量,这样对撞机的大小不会影响子弹移动的程度 RigidBody

Finally, we need to calculate the force for the impulse. For this, we use the direction the bullet is facing and multiply it by the bullet’s damage. This gives a nice result and for stronger bullets, we get a stronger result.


Now we need to attach this script to all of the RigidBody nodes we want to affect.

Open up Testing_Area.tscn and select all the cubes parented to the Cubes node.

小技巧

If you select the top cube, and then hold down shift and select the last cube, Godot will select all the cubes in-between!

Once you have all the cubes selected, scroll down in the inspector until you get to the “scripts” section. Click the drop down and select “Load”. Open your newly created RigidBody_hit_test.gd script.

最后的笔记

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

That was a lot of code! But now, with all that done, you can go and give your weapons a test!

You should now be able to fire as many bullets as you want at the cubes and they will move in response to the bullets colliding with them.

在 :ref:`doc_fps_tutorial_part_three`中,我们将为武器添加弹药以及一些声音!

警告

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

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