第3部分

零件概述

在这部分中,我们将通过给玩家弹药来限制他们的武器。我们也将给予玩家重新装填的能力,当武器开火时,我们将添加声音。

../../../_images/PartThreeFinished.png

注解

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

我们开始吧!

更改级别

现在我们有了一个完全工作的fps,让我们移到一个更像fps的级别。

Open up Space_Level.tscn (assets/Space_Level_Objects/Space_Level.tscn) and/or Ruins_Level.tscn (assets/Ruin_Level_Objects/Ruins_Level.tscn).

Space_Level.tscnRuins_Level.tscn 是为本教程的目的而创建的完整自定义fps级别。出版社 F6 播放打开的场景,或按 play current scene button ,给每个人一次尝试。

警告

Space_Level.tscn 对GPU的图形要求比 Ruins_Level.tscn . 如果您的计算机正在努力渲染 Space_Level.tscn ,尝试使用 Ruins_Level.tscn 相反。

你可能注意到有几个 RigidBody 在整个级别上放置的节点。我们可以安排 RigidBody_hit_test.gd 在他们身上,然后他们会对被子弹击中做出反应,所以让我们这么做吧!

请按照下面的说明操作要使用的任一(或两个)场景。

Expand "Other_Objects" and then expand "Physics_Objects".

Expand one of the "Barrel_Group" nodes and then select "Barrel_Rigid_Body" and open it using
the "Open in Editor" button.
This will bring you to the "Barrel_Rigid_Body" scene. From there, select the root node and
scroll the inspector down to the bottom.
Select the drop down arrow under the "Node" tab, and then select "Load". Navigate to
"RigidBody_hit_test.gd" and select "Open".

Return back to "Space_Level.tscn".

Expand one of the "Box_Group" nodes and then select "Crate_Rigid_Body" and open it using the
"Open in Editor" button.
This will bring you to the "Crate_Rigid_Body" scene. From there, select the root node and
scroll the inspector down to the bottom.
Select the drop down arrow under the "Node" tab, and then select "Load". Navigate to
"RigidBody_hit_test.gd" and select "Open".

Return to "Space_Level.tscn".
Expand "Misc_Objects" and then expand "Physics_Objects".

Select all the "Stone_Cube" RigidBodies and then in the inspector scroll down to the bottom.
Select the drop down arrow under the "Node" tab, and then select "Load". Navigate to
"RigidBody_hit_test.gd" and select "Open".

Return to "Ruins_Level.tscn".

现在你可以向任何一层的所有刚性物体开火,它们会对击中它们的子弹作出反应!

添加弹药

既然玩家有工作枪,我们就给他们有限的弹药。

首先,我们需要在每个武器脚本中定义一些变量。

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

var ammo_in_weapon = 10
var spare_ammo = 20
const AMMO_IN_MAG = 10
  • ammo_in_weapon :手枪中当前的弹药量

  • spare_ammo 我们为手枪保留的弹药量

  • AMMO_IN_MAG :完全重新装载武器/弹药库中的弹药量

现在我们需要做的就是在 fire_weapon .

在下面添加以下内容 Clone.BULLET_DAMAGE = DAMAGEammo_in_weapon -= 1

这将从中删除一个 ammo_in_weapon 每次玩家开火。注意,我们不检查玩家是否有足够的弹药 fire_weapon . 相反,我们将检查玩家是否有足够的弹药 Player.gd .


现在我们需要为步枪和刀都增加弹药。

注解

你可能想知道为什么我们要为这把刀增加弹药,因为它不消耗任何弹药。我们之所以想在刀上添加弹药,是因为我们有一个统一的界面来处理我们所有的武器。

如果我们没有为刀添加弹药变量,我们将不得不为刀添加检查。通过在刀上添加弹药变量,我们不需要担心我们所有的武器是否都有相同的变量。

将以下类变量添加到 Weapon_Rifle.gd

var ammo_in_weapon = 50
var spare_ammo = 100
const AMMO_IN_MAG = 50

然后将以下内容添加到 fire_weaponammo_in_weapon -= 1 . 确保 ammo_in_weapon -= 1if ray.is_colliding() 检查,这样无论玩家是否击中什么东西,玩家都会失去弹药。

现在只剩下刀子了。将以下内容添加到 Weapon_Knife.gd

var ammo_in_weapon = 1
var spare_ammo = 1
const AMMO_IN_MAG = 1

因为刀子不消耗弹药,所以我们只需要增加弹药。


现在我们需要改变一件事 Player.gd 也就是说,

我们怎么开枪 process_input . 将发射武器的代码更改为:

# ----------------------------------
# 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 current_weapon.ammo_in_weapon > 0:
                if animation_manager.current_state == current_weapon.IDLE_ANIM_NAME:
                    animation_manager.set_animation(current_weapon.FIRE_ANIM_NAME)
# ----------------------------------

现在武器的弹药量有限,当玩家耗尽时,武器将停止射击。


理想情况下,我们希望让玩家能够看到还剩多少弹药。让我们创建一个新函数 process_UI .

首先,添加 process_UI(delta)_physics_process .

现在将以下内容添加到 Player.gd

func process_UI(delta):
    if current_weapon_name == "UNARMED" or current_weapon_name == "KNIFE":
        UI_status_label.text = "HEALTH: " + str(health)
    else:
        var current_weapon = weapons[current_weapon_name]
        UI_status_label.text = "HEALTH: " + str(health) + \
                "\nAMMO: " + str(current_weapon.ammo_in_weapon) + "/" + str(current_weapon.spare_ammo)

让我们回顾一下发生了什么:

首先,我们检查当前的武器是否 UNARMEDKNIFE . 如果是,我们会改变 UI_status_label 的文本,仅显示玩家的健康状况 UNARMEDKNIFE 不要消耗弹药。

如果玩家使用的武器消耗弹药,我们首先得到武器节点。

然后我们改变 UI_status_label 显示玩家健康状况的文本,以及玩家在武器中有多少弹药以及该武器有多少备用弹药。

现在我们可以看到玩家有多少弹药通过平视显示器。

增加武器的重新装载

现在玩家的弹药用完了,我们需要一种方法让玩家补充弹药。接下来我们来添加重新加载!

为了重新装载,我们需要在每个武器上添加更多的变量和函数。

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

const CAN_RELOAD = true
const CAN_REFILL = true

const RELOADING_ANIM_NAME = "Pistol_reload"
  • CAN_RELOAD :一个布尔值,用于跟踪此武器是否具有重新加载的能力

  • CAN_REFILL :一个布尔值来跟踪我们是否可以补充这个武器的备用弹药。我们不会使用 CAN_REFILL 在这一部分,但我们将在下一部分!

  • RELOADING_ANIM_NAME :此武器的重新加载动画的名称。

现在我们需要添加一个函数来处理重新加载。将以下函数添加到 Weapon_Pistol.gd

func reload_weapon():
    var can_reload = false

    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        can_reload = true

    if spare_ammo <= 0 or ammo_in_weapon == AMMO_IN_MAG:
        can_reload = false

    if can_reload == true:
        var ammo_needed = AMMO_IN_MAG - ammo_in_weapon

        if spare_ammo >= ammo_needed:
            spare_ammo -= ammo_needed
            ammo_in_weapon = AMMO_IN_MAG
        else:
            ammo_in_weapon += spare_ammo
            spare_ammo = 0

        player_node.animation_manager.set_animation(RELOADING_ANIM_NAME)

        return true

    return false

让我们回顾一下发生了什么:

首先,我们定义一个变量来查看这个武器是否可以重新装载。

然后我们检查玩家是否处于该武器的空闲动画状态,因为我们只想在玩家没有射击、装备或未装备时重新加载。

接下来,我们检查玩家是否有多余的弹药,以及武器中的弹药是否等于重新加载的武器。这样我们就可以确保玩家在没有弹药或武器已经装满弹药时无法重新装填弹药。

如果我们仍然可以重新装弹,那么我们计算重新装弹武器所需的弹药量。

如果玩家有足够的弹药来装满武器,我们就把需要的弹药从 spare_ammo 然后设置 ammo_in_weapon 全套武器/弹匣。

如果玩家没有足够的弹药,我们就把剩下的所有弹药都加进去。 spare_ammo ,然后设置 spare_ammo0 .

接下来我们播放这个武器的重新加载动画,然后返回 true .

如果玩家不能重新加载,我们会返回 false .


现在我们需要给步枪增加装弹量。打开 Weapon_Rifle.gd 并添加以下类变量:

const CAN_RELOAD = true
const CAN_REFILL = true

const RELOADING_ANIM_NAME = "Rifle_reload"

这些变量与手枪完全相同,只是 RELOADING_ANIM_NAME 改为步枪的重装动画。

现在我们需要补充 reload_weaponWeapon_Rifle.gd

func reload_weapon():
    var can_reload = false

    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        can_reload = true

    if spare_ammo <= 0 or ammo_in_weapon == AMMO_IN_MAG:
        can_reload = false

    if can_reload == true:
        var ammo_needed = AMMO_IN_MAG - ammo_in_weapon

        if spare_ammo >= ammo_needed:
            spare_ammo -= ammo_needed
            ammo_in_weapon = AMMO_IN_MAG
        else:
            ammo_in_weapon += spare_ammo
            spare_ammo = 0

        player_node.animation_manager.set_animation(RELOADING_ANIM_NAME)

        return true

    return false

这个密码和手枪的密码完全一样。


我们要做的最后一件事就是给刀子加上“重装”。将以下类变量添加到 Weapon_Knife.gd

const CAN_RELOAD = false
const CAN_REFILL = false

const RELOADING_ANIM_NAME = ""

由于我们都无法重新加载或重新填充刀具,因此我们将两个常量都设置为 false . 我们还定义 RELOADING_ANIM_NAME 作为空字符串,因为刀没有重新加载动画。

现在我们需要补充 reloading_weapon

func reload_weapon():
    return false

因为我们不能重新装刀,所以我们总是回来 false .

向播放机添加重新加载

现在我们需要增加一些东西 Player.gd . 首先,我们需要定义一个新的类变量:

var reloading_weapon = false
  • reloading_weapon :用于跟踪播放机当前是否正在尝试重新加载的变量。

接下来,我们需要添加另一个函数调用 _physics_process .

添加 process_reloading(delta)_physics_process . 现在 _physics_process 应该是这样的:

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

现在我们需要补充 process_reloading . 将以下函数添加到 Player.gd

func process_reloading(delta):
    if reloading_weapon == true:
        var current_weapon = weapons[current_weapon_name]
        if current_weapon != null:
            current_weapon.reload_weapon()
        reloading_weapon = false

我们来看看这里发生了什么。

首先,我们检查以确保玩家正在尝试重新加载。

如果玩家试图重新上膛,我们就得到当前的武器。如果当前武器不相等 null 我们称之为 reload_weapon 功能。

注解

如果当前武器等于 null 那么现在的武器是 UNARMED .

最后,我们开始 reloading_weaponfalse 因为,不管玩家是否成功重新加载,我们已经尝试重新加载,不再需要继续尝试。


在我们让玩家重新上膛之前,我们需要在 process_input .

我们需要改变的第一件事就是改变武器的代码。我们需要再加一张支票 (if reloading_weapon == false: )要查看玩家是否正在重新加载:

if changing_weapon == false:
    # New line of code here!
    if reloading_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

这使得玩家在重装武器时无法更换武器。

现在我们需要添加代码以在玩家按下 reload 行动。将以下代码添加到 process_input

# ----------------------------------
# Reloading
if reloading_weapon == false:
    if changing_weapon == false:
        if Input.is_action_just_pressed("reload"):
            var current_weapon = weapons[current_weapon_name]
            if current_weapon != null:
                if current_weapon.CAN_RELOAD == true:
                    var current_anim_state = animation_manager.current_state
                    var is_reloading = false
                    for weapon in weapons:
                        var weapon_node = weapons[weapon]
                        if weapon_node != null:
                            if current_anim_state == weapon_node.RELOADING_ANIM_NAME:
                                is_reloading = true
                    if is_reloading == false:
                        reloading_weapon = true
# ----------------------------------

我们来看看这里发生了什么。

首先,我们要确保玩家没有重新加载,也没有试图更换武器。

然后我们检查一下 reload 已按下操作。

如果玩家按下 reload 然后我们得到当前的武器并检查以确保它不是 null . 然后我们检查武器是否能使用 CAN_RELOAD 常数。

如果武器可以重新加载,那么我们就得到当前的动画状态,并制作一个变量来跟踪玩家是否已经重新加载。

然后我们检查每一个武器,以确保玩家没有在播放武器的重新加载动画。

如果玩家没有重新装载任何武器,我们设置 reloading_weapontrue .


我想补充的一件事是,如果你试图发射武器,但它没有弹药,武器将在哪里重新装填。

我们还需要添加一个额外的if检查 (is_reloading_weapon == false: )所以玩家不能在重装时发射当前武器。

让我们把我们的射击代码改成 process_input 因此,当试图发射空武器时,它会重新加载:

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

现在,我们检查以确保在我们发射武器之前,以及在我们拥有武器时,玩家没有重新装弹。 0 或者在现有武器中的弹药更少,我们设置 reloading_weapontrue 如果玩家试图开火。

这将使玩家在试图发射空武器时尝试重新装弹。


完成后,玩家现在可以重新加载!试试看!现在你可以为每件武器发射所有的备用弹药。

添加声音

最后,让我们添加一些伴随玩家射击、重新装载和更换武器的声音。

小技巧

本教程中没有提供游戏声音(出于法律原因)。https://gamesounds.xyz/是 “免版税或公共领域音乐和声音适合游戏” . 我使用的是GameMaster的Gun Sound Pack,可以在sonniss.com gdc 2017游戏音频包中找到。

打开 Simple_Audio_Player.tscn . 它只是一个 Spatial 用一个 AudioStreamPlayer 作为它的孩子。

注解

之所以称之为“简单”音频播放器,是因为我们没有考虑到性能,而且代码的设计是为了以最简单的方式提供声音。

如果您想使用三维音频,那么它听起来像是来自三维空间中的某个位置,请右键单击 AudioStreamPlayer 并选择“更改类型”。

这将打开节点浏览器。引导到 AudioStreamPlayer3D 并选择“更改”。在本教程的源代码中,我们将使用 AudioStreamPlayer ,但您可以选择使用 AudioStreamPlayer3D 如果您愿意,下面提供的代码将工作,无论您选择哪一个。

创建一个新脚本并调用它 Simple_Audio_Player.gd . 将其附加到 Spatial 在里面 Simple_Audio_Player.tscn 并插入以下代码:

extends Spatial

# All of the audio files.
# You will need to provide your own sound files.
var audio_pistol_shot = preload("res://path_to_your_audio_here")
var audio_gun_cock = preload("res://path_to_your_audio_here")
var audio_rifle_shot = preload("res://path_to_your_audio_here")

var audio_node = null

func _ready():
    audio_node = $Audio_Stream_Player
    audio_node.connect("finished", self, "destroy_self")
    audio_node.stop()


func play_sound(sound_name, position=null):

    if audio_pistol_shot == null or audio_rifle_shot == null or audio_gun_cock == null:
        print ("Audio not set!")
        queue_free()
        return

    if sound_name == "Pistol_shot":
        audio_node.stream = audio_pistol_shot
    elif sound_name == "Rifle_shot":
        audio_node.stream = audio_rifle_shot
    elif sound_name == "Gun_cock":
        audio_node.stream = audio_gun_cock
    else:
        print ("UNKNOWN STREAM")
        queue_free()
        return

    # If you are using an AudioStreamPlayer3D, then uncomment these lines to set the position.
    #if audio_node is AudioStreamPlayer3D:
    #    if position != null:
    #        audio_node.global_transform.origin = position

    audio_node.play()


func destroy_self():
    audio_node.stop()
    queue_free()

小技巧

通过设置 positionnull 默认在 play_sound ,我们把它作为一个可选的论点,意思是 position 不一定要通过传入才能调用 play_sound .

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


_ready ,我们得到 AudioStreamPlayer 把它连接起来 finished 向发送信号 destroy_self 功能。不管是不是 AudioStreamPlayerAudioStreamPlayer3D 节点,因为它们都有完成的信号。为了确保它不会发出任何声音,我们打电话 stopAudioStreamPlayer .

警告

确保声音文件 not 设置为循环!如果设置为循环,声音将继续无限播放,脚本将无法工作!

这个 play_sound 函数是我们要调用的对象 Player.gd . 我们检查声音是否是三种声音中的一种,如果是三种声音中的一种,我们将音频流设置为 AudioStreamPlayer 以正确的声音。

如果是未知声音,我们将向控制台打印错误消息并释放音频播放器。

如果使用 AudioStreamPlayer3D ,删除 # 设置音频播放器节点的位置,使其在正确的位置播放。

最后,我们告诉 AudioStreamPlayer 去玩。

AudioStreamPlayer 播放完声音,它会 destroy_self 因为我们连接了 finished 信号输入 _ready . 我们停止 AudioStreamPlayer 并释放音频播放器以节省资源。

注解

该系统非常简单,存在一些主要缺陷:

一个缺陷是我们必须传递一个字符串值才能播放声音。虽然记住这三个声音的名字相对简单,但当你有更多的声音时,它可能会变得越来越复杂。理想情况下,我们将这些声音放在某种容器中,容器中有公开的变量,这样我们就不必记住我们想要播放的每个声音效果的名称。

另一个缺陷是我们不能用这个系统轻松地播放循环声音效果或背景音乐。因为我们不能播放循环声音,某些效果,如脚步声,很难实现,因为我们必须跟踪是否有声音效果,以及是否需要继续播放。

这个系统最大的缺陷之一是我们只能播放来自 Player.gd . 理想情况下,我们希望能够在任何时候播放任何脚本的声音。


做完了,我们开始吧 Player.gd 再一次。首先我们需要加载 Simple_Audio_Player.tscn . 将以下代码放入脚本的类变量部分:

var simple_audio_player = preload("res://Simple_Audio_Player.tscn")

现在,我们需要实例简单的音频播放器时,我们需要它,然后调用它 play_sound 函数并传递要播放的声音的名称。为了使过程更简单,让我们创建一个 create_sound 中的函数 Player.gd

func create_sound(sound_name, position=null):
    var audio_clone = simple_audio_player.instance()
    var scene_root = get_tree().root.get_children()[0]
    scene_root.add_child(audio_clone)
    audio_clone.play_sound(sound_name, position)

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


第一行实例 Simple_Audio_Player.tscn 场景并将其分配给一个名为 audio_clone .

第二行得到场景根,这有一个很大(尽管安全)的假设。

我们首先得到这个节点的 SceneTree ,然后访问根节点,在本例中是 Viewport 整个比赛都在进行中。然后我们得到第一个孩子 Viewport ,在我们的例子中,它恰好是 Test_Area.tscn 或任何其他提供的级别。 我们做了一个巨大的假设,根节点的第一个子节点是玩家所处的根场景,这可能并不总是如此。 .

如果这对你没有意义,不要太担心。只有当一次将多个场景作为根节点的子级加载时,第二行代码才不能可靠地工作,这在大多数项目中很少发生,并且不会在本教程系列中发生。这可能只是一个问题,取决于您如何处理场景加载。

第三行添加了我们新创建的 Simple_Audio_Player 场景将是场景根的子级。这和我们生产子弹的时候完全一样。

最后,我们称之为 play_sound 函数并传入传入的参数 create_sound . 这会叫 Simple_Audio_Player.gdplay_sound 带有传入参数的函数。


现在剩下的只是在我们想播放的时候播放声音。我们先把声音加到手枪上吧!

打开 Weapon_Pistol.gd .

现在,我们想在玩家发射手枪时发出声音,所以在枪的末端添加以下内容 fire_weapon 功能:

player_node.create_sound("Pistol_shot", self.global_transform.origin)

现在,当玩家开枪时,我们将 Pistol_shot 声音。

为了在玩家重新加载时发出声音,我们需要在下面添加以下内容 player_node.animation_manager.set_animation(RELOADING_ANIM_NAME)reload_weapon 功能:

player_node.create_sound("Gun_cock", player_node.camera.global_transform.origin)

现在,当玩家重新加载时,我们将播放 Gun_cock 声音。


现在,让我们为步枪添加声音。打开 Weapon_Rifle.gd .

若要在步枪发射时播放声音,请将以下内容添加到 fire_weapon 功能:

player_node.create_sound("Rifle_shot", ray.global_transform.origin)

现在,当玩家发射步枪时,我们将播放 Rifle_shot 声音。

为了在玩家重新加载时发出声音,我们需要在下面添加以下内容 player_node.animation_manager.set_animation(RELOADING_ANIM_NAME)reload_weapon 功能:

player_node.create_sound("Gun_cock", player_node.camera.global_transform.origin)

现在,当玩家重新加载时,我们将播放 Gun_cock 声音。

最后的注释

../../../_images/PartThreeFinished.png

现在你有了弹药有限的武器,当你开火时会发出声音!

在这一点上,我们有一个FPS游戏的所有基础工作。还有一些东西可以添加,我们将在接下来的三个部分中添加它们!

例如,现在我们没有办法在我们的备件中添加弹药,所以我们最终会耗尽。而且,我们没有任何东西可以在 RigidBody 节点。

第4部分 我们会增加一些射击目标,以及一些健康和弹药收集!我们还将添加游戏手柄支持,这样我们就可以玩有线Xbox 360控制器了!

警告

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

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