第6部分

部分概述

In this part, we're going to add a main menu and pause menu, add a respawn system for the player, and change/move the sound system so we can use it from any script.

This is the last part of the FPS tutorial; by the end of this, you will have a solid base to build amazing FPS games with Godot!

../../../_images/FinishedTutorialPicture.png

注解

You are assumed to have finished 第5部分 before moving on to this part of the tutorial. The finished project from 第5部分 will be the starting project for part 6

让我们开始吧!

添加主菜单

Firstly, open up Main_Menu.tscn and take a look at how the scene is set up.

主菜单分为三个不同的面板,每个面板代表主菜单的不同“屏幕”。

注解

``Background_Animation``节点就是这样,菜单的背景比纯色更有趣。 这是一个环顾天空盒的相机,没什么特别的。

Feel free to expand all the nodes and see how they're set up. Remember to keep only Start_Menu visible when you're done, as that's the screen we want to show first when we enter the main menu.

选择 Main_Menu (根节点)并创建一个名为 Main_Menu.gd 的新脚本。 添加以下内容:

extends Control

var start_menu
var level_select_menu
var options_menu

export (String, FILE) var testing_area_scene
export (String, FILE) var space_level_scene
export (String, FILE) var ruins_level_scene

func _ready():
    start_menu = $Start_Menu
    level_select_menu = $Level_Select_Menu
    options_menu = $Options_Menu

    $Start_Menu/Button_Start.connect("pressed", self, "start_menu_button_pressed", ["start"])
    $Start_Menu/Button_Open_Godot.connect("pressed", self, "start_menu_button_pressed", ["open_godot"])
    $Start_Menu/Button_Options.connect("pressed", self, "start_menu_button_pressed", ["options"])
    $Start_Menu/Button_Quit.connect("pressed", self, "start_menu_button_pressed", ["quit"])

    $Level_Select_Menu/Button_Back.connect("pressed", self, "level_select_menu_button_pressed", ["back"])
    $Level_Select_Menu/Button_Level_Testing_Area.connect("pressed", self, "level_select_menu_button_pressed", ["testing_scene"])
    $Level_Select_Menu/Button_Level_Space.connect("pressed", self, "level_select_menu_button_pressed", ["space_level"])
    $Level_Select_Menu/Button_Level_Ruins.connect("pressed", self, "level_select_menu_button_pressed", ["ruins_level"])

    $Options_Menu/Button_Back.connect("pressed", self, "options_menu_button_pressed", ["back"])
    $Options_Menu/Button_Fullscreen.connect("pressed", self, "options_menu_button_pressed", ["fullscreen"])
    $Options_Menu/Check_Button_VSync.connect("pressed", self, "options_menu_button_pressed", ["vsync"])
    $Options_Menu/Check_Button_Debug.connect("pressed", self, "options_menu_button_pressed", ["debug"])

    Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)

    var globals = get_node("/root/Globals")
    $Options_Menu/HSlider_Mouse_Sensitivity.value = globals.mouse_sensitivity
    $Options_Menu/HSlider_Joypad_Sensitivity.value = globals.joypad_sensitivity


func start_menu_button_pressed(button_name):
    if button_name == "start":
        level_select_menu.visible = true
        start_menu.visible = false
    elif button_name == "open_godot":
        OS.shell_open("https://godotengine.org/")
    elif button_name == "options":
        options_menu.visible = true
        start_menu.visible = false
    elif button_name == "quit":
        get_tree().quit()


func level_select_menu_button_pressed(button_name):
    if button_name == "back":
        start_menu.visible = true
        level_select_menu.visible = false
    elif button_name == "testing_scene":
        set_mouse_and_joypad_sensitivity()
        get_node("/root/Globals").load_new_scene(testing_area_scene)
    elif button_name == "space_level":
        set_mouse_and_joypad_sensitivity()
        get_node("/root/Globals").load_new_scene(space_level_scene)
    elif button_name == "ruins_level":
        set_mouse_and_joypad_sensitivity()
        get_node("/root/Globals").load_new_scene(ruins_level_scene)


func options_menu_button_pressed(button_name):
    if button_name == "back":
        start_menu.visible = true
        options_menu.visible = false
    elif button_name == "fullscreen":
        OS.window_fullscreen = !OS.window_fullscreen
    elif button_name == "vsync":
        OS.vsync_enabled = $Options_Menu/Check_Button_VSync.pressed
    elif button_name == "debug":
        pass


func set_mouse_and_joypad_sensitivity():
    var globals = get_node("/root/Globals")
    globals.mouse_sensitivity = $Options_Menu/HSlider_Mouse_Sensitivity.value
    globals.joypad_sensitivity = $Options_Menu/HSlider_Joypad_Sensitivity.value

这里的大多数代码都与制作UI有关,这超出了本教程系列的目的。 我们只是简要介绍一下与UI相关的代码。

小技巧

请参阅 :ref:`doc_ui_main_menu`以及以下教程,以获得更好的GUI和UI方法!

我们先来看看类变量。

  • start_menu:一个用于保存``Start_Menu``的变量 Panel

  • level_select_menu:一个用于保存``Level_Select_Menu``的变量 Panel

  • options_menu:一个变量来保存``Options_Menu`` Panel

  • testing_area_scene:``Testing_Area.tscn``文件的路径,所以我们可以从这个场景改变它。

  • space_level_scene:``Space_Level.tscn``文件的路径,所以我们可以从这个场景改变它。

  • ruins_level_scene:``Ruins_Level.tscn``文件的路径,所以我们可以从这个场景改变它。

警告

在测试此脚本之前,您必须在编辑器中设置正确文件的路径! 否则它将无法正常工作!


现在让我们回顾一下 _ready

Firstly, we get all the Panel nodes and assign them to the proper variables.

Next, we connect all the buttons pressed signals to their respective [panel_name_here]_button_pressed functions.

We then set the mouse mode to MOUSE_MODE_VISIBLE to ensure whenever the player returns to this scene, the mouse will be visible.

然后我们得到一个叫做“Globals”的单例人士。 然后我们设置 HSlider 节点的值,使它们的值与单例中的鼠标和游戏手柄灵敏度对齐。

注解

我们还没有制作 Globals 单例,所以不用担心! 我们很快就会成功!


In start_menu_button_pressed, we check to see which button is pressed.

根据按下的按钮,我们要么更改当前可见的面板,退出应用程序,要么打开Godot网站。


level_select_menu_button_pressed 中,我们检查按下了哪个按钮。

如果按下“后退”按钮,我们会更改当前可见的面板以返回主菜单。

If one of the scene changing buttons is pressed, we fist call set_mouse_and_joypad_sensitivity so the singleton (Globals.gd) has the values from the HSlider nodes. Then, we tell the singleton to change nodes using its load_new_scene function, passing in the file path of the scene the player has selected.

注解

不要担心单例人士,我们很快就会到达那里!


options_menu_button_pressed 中,我们检查按下了哪个按钮。

如果按下“后退”按钮,我们会更改当前可见的面板以返回主菜单。

If the fullscreen button is pressed, we toggle the OS's full screen mode by setting it to the flipped version of its current value.

If the vsync button is pressed, we set the OS's Vsync based on the state of the Vsync check button.


最后,让我们来看看 set_mouse_and_joypad_sensitivity

Firstly, we get the Globals singleton and assign it to a local variable.

然后我们将 mouse_sensitivity 和``joypad_sensitivity``变量设置为它们各自的值 HSlider 节点对应物。

使 Globals 单例

Now, for all this to work, we need to create the Globals singleton. Make a new script in the Script tab and call it Globals.gd.

注解

要制作 Globals 单例,请转到编辑器中的 Script 选项卡,然后单击 New 并出现一个``Create Script``框,除了`` 路径``您需要插入脚本名称``Globals.gd``。

将以下内容添加到 Globals.gd 中。

extends Node

var mouse_sensitivity = 0.08
var joypad_sensitivity = 2

func _ready():
    pass

func load_new_scene(new_scene_path):
    get_tree().change_scene(new_scene_path)

As you can see, it's quite small and simple. As this part progresses, we will keep adding more complex logic to Globals.gd, but for now, all it is doing is holding two class variables, and abstract defining how we change scenes.

  • mouse_sensitivity:我们鼠标的当前灵敏度,所以我们可以在 Player.gd 中加载它。

  • joypad_sensitivity:我们游戏手柄的当前灵敏度,所以我们可以在 Player.gd 中加载它。

Right now, all we will be using Globals.gd for is a way to carry variables across scenes. Because the sensitivities of our mouse and joypad are stored in Globals.gd, any changes we make in one scene (like in Options_Menu) will affect the sensitivity for the player.

我们在 load_new_scene 中所做的就是调用 SceneTreechange_scene 函数,传入 load_new_scene 中给出的场景路径。

That's all the code needed for Globals.gd right now! Before we can test the main menu, we first need to set Globals.gd as an autoload script.

打开``Project Settings``并单击 AutoLoad 选项卡。

../../../_images/AutoloadAddSingleton.png

然后通过单击旁边的按钮(`)选择``Path``字段中``Globals.gd``的路径。 确保``Node Name``字段中的名称是``Globals``。 如果您拥有上图所示的所有内容,请按“添加”!

这将使 Globals.gd 成为单例/自动加载脚本,这将允许我们从任何场景中的任何脚本访问它。

小技巧

有关单例/自动加载脚本的更多信息,请参阅 单例(自动加载)

现在 Globals.gd 是一个单例/自动加载脚本,您可以测试主菜单!

您可能希望将主场景从 Testing_Area.tscn 更改为 Main_Menu.tscn ,因此当我们导出游戏时,游戏角色将从主菜单开始。 您可以通过 General 选项卡下的``Project Settings``来完成此操作。 然后在 Application 类别中,单击 Run 子类别,您可以通过更改``Main Scene``中的值来更改主场景。

警告

在测试主菜单之前,您必须在编辑器中的 Main_Menu 中设置正确文件的路径! 否则,您将无法从级别选择菜单/屏幕更改场景。

添加调试菜单

Now, let's add a simple debugging scene so we can track things like FPS (Frames Per Second) in-game. Open up Debug_Display.tscn.

You can see it's a Panel positioned in the top right corner of the screen. It has three Labels, one for displaying the FPS at which the game is running, one for showing on what OS the game is running, and a label for showing with which Godot version the game is running.

让我们添加填充这些代码所需的代码 Labels。 选择 Debug_Display 并创建一个名为 Debug_Display.gd 的新脚本。 添加以下内容:

extends Control

func _ready():
    $OS_Label.text = "OS: " + OS.get_name()
    $Engine_Label.text = "Godot version: " + Engine.get_version_info()["string"]

func _process(delta):
    $FPS_Label.text = "FPS: " + str(Engine.get_frames_per_second())

我们来看看这个脚本的功能。


In _ready, we set the OS_Label's text to the name provided by OS using the get_name function. This will return the name of the OS (or Operating System) for which Godot was compiled. For example, when you are running Windows, it will return Windows, while when you are running Linux, it will return X11.

Then, we set the Engine_Label's text to the version info provided by Engine.get_version_info. Engine.get_version_info returns a dictionary full of useful information about the version of Godot which is currently running. We only care about the string version, for this label at least, so we get the string and assign that as the text in Engine_Label. See Engine for more information on the values get_version_info returns.

In _process, we set the text of the FPS_Label to Engine.get_frames_per_second, but because get_frames_per_second returns an integer, we have to cast it to a string using str before we can add it to the Label.


现在让我们跳回到 Main_Menu.gd 并在 options_menu_button_pressed 中更改以下内容:

elif button_name == "debug":
    pass

改为:

elif button_name == "debug":
    get_node("/root/Globals").set_debug_display($Options_Menu/Check_Button_Debug.pressed)

This will call a new function called set_debug_display in our singleton, so let's add that next!


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

# ------------------------------------
# All the GUI/UI-related variables

var canvas_layer = null

const DEBUG_DISPLAY_SCENE = preload("res://Debug_Display.tscn")
var debug_display = null

# ------------------------------------
  • canvas_layer:一个画布层,因此在 Globals.gd 中创建的GUI / UI总是在顶部绘制。

  • DEBUG_DISPLAY:我们之前处理过的调试显示场景。

  • debug_display:一个变量,用于在/如果存在时保持调试显示。

现在我们已经定义了类变量,我们需要在 _ready 中添加几行,以便 Globals.gd 将使用一个画布层(我们将存储在 canvas_layer 中)。 将 _ready 更改为以下内容:

func _ready():
    canvas_layer = CanvasLayer.new()
    add_child(canvas_layer)

Now in _ready, we create a new canvas layer, assign it to canvas_layer and add it as a child. Because Globals.gd is an autoload/singleton, Godot will make a Node when the game is launched, and it will have Globals.gd attached to it. Since Godot makes a Node, we can treat Globals.gd like any other node with regard to adding/removing children nodes.

我们添加一个 CanvasLayer 的原因是为了在 Globals.gd 中实例/spawn的所有GUI和UI节点总是绘制在其他所有东西之上。

When adding nodes to a singleton/autoload, you have to be careful not to lose reference to any of the child nodes. This is because nodes will not be freed/destroyed when you change the active scene, meaning you can run into memory problems if you are instancing/spawning lots of nodes and you are not freeing them.


现在我们需要将 set_debug_display 添加到``Globals.gd``:

func set_debug_display(display_on):
    if display_on == false:
        if debug_display != null:
            debug_display.queue_free()
            debug_display = null
    else:
        if debug_display == null:
            debug_display = DEBUG_DISPLAY_SCENE.instance()
            canvas_layer.add_child(debug_display)

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

首先,我们检查 Globals.gd 是否正在尝试打开调试显示,或者将其关闭。

如果 Globals.gd 正在关闭显示,我们检查 debug_display 是否不等于 null 。 如果 debug_display 不等于 null ,那么 Globals.gd 必须有一个当前有效的调试显示。 如果 Globals.gd 的调试显示处于活动状态,我们使用 queue_free 释放它,然后将 debug_display 分配给 null

如果 Globals.gd 打开显示器,我们检查以确保 Globals.gd 没有激活调试显示。 我们通过确保 debug_display 等于 null 来做到这一点。 如果 debug_display 是``null``,我们实例化一个新的 DEBUG_DISPLAY_SCENE ,并将其添加为 canvas_layer 的子节点。


完成后,我们现在可以通过在 Options_Menu 面板中切换 CheckButton 来打开和关闭调试显示。 去尝试吧!

请注意,即使将“Main_Menu.tscn`”中的场景更改为另一个场景(如“Testing_Area.tscn``),调试显示仍然保持不变。 这是单例/自动加载中实例化/生成节点的美妙之处,并将它们作为子元素添加到单例/自动加载中。 作为单件/自动加载的子节点添加的任何节点将在游戏运行期间保持不变,而我们没有任何额外的工作!

添加暂停菜单

让我们添加一个暂停菜单,这样当我们按下 ui_cancel 动作时我们可以返回主菜单。

打开 Pause_Popup.tscn

Notice how the root node in Pause_Popup is a WindowDialog; WindowDialog inherits from Popup, which means WindowDialog can act like a popup.

Select Pause_Popup and scroll down all the way till you get to the Pause menu in the inspector. Notice how the pause mode is set to process instead of inherit like it is normally set by default. This makes it so it will continue to process even when the game is paused, which we need in order to interact with the UI elements.

Now that we've looked at how Pause_Popup.tscn is set up, let's write the code to make it work. Normally, we'd attach a script to the root node of the scene, Pause_Popup in this case, but since we'll need to receive a couple of signals in Globals.gd, we'll write all the code for the popup there.

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

const MAIN_MENU_PATH = "res://Main_Menu.tscn"
const POPUP_SCENE = preload("res://Pause_Popup.tscn")
var popup = null
  • MAIN_MENU_PATH:主菜单场景的路径。

  • POPUP_SCENE:我们之前看过的弹出场景。

  • popup:一个用于保存弹出场景的变量。

现在我们需要将 _process 添加到 Globals.gd 中,这样当按下 ui_cancel 动作时它就会响应。 将以下内容添加到``_process``:

func _process(delta):
    if Input.is_action_just_pressed("ui_cancel"):
        if popup == null:
            popup = POPUP_SCENE.instance()

            popup.get_node("Button_quit").connect("pressed", self, "popup_quit")
            popup.connect("popup_hide", self, "popup_closed")
            popup.get_node("Button_resume").connect("pressed", self, "popup_closed")

            canvas_layer.add_child(popup)
            popup.popup_centered()

            Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)

            get_tree().paused = true

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


Firstly, we check to see if the ui_cancel action is pressed. Then, we check to make sure Globals.gd does not already have a popup open by checking to see if popup is equal to null.

If Globals.gd do not have a pop-up open, we instance POPUP_SCENE and assign it to popup.

然后我们得到退出按钮并将它的 pressed 信号分配给 popup_quit ,我们将很快添加。

Next, we assign both the popup_hide signal from the WindowDialog and the pressed signal from the resume button to popup_closed, which we will be adding shortly.

Then, we add popup as a child of canvas_layer so it's drawn on top. We then tell popup to pop up at the center of the screen using popup_centered.

Next, we make sure the mouse mode is MOUSE_MODE_VISIBLE so the player can interact with the pop-up. If we did not do this, the player would not be able to interact with the pop up in any scene where the mouse mode is MOUSE_MODE_CAPTURED.

最后,我们暂停整个 SceneTree

注解

有关在Godot中暂停的更多信息,请参阅 暂停游戏


Now, we need to add the functions to which we've connected the signals. Let's add popup_closed first.

将以下内容添加到``Globals.gd``:

func popup_closed():
    get_tree().paused = false

    if popup != null:
        popup.queue_free()
        popup = null

popup_closed will resume the game and destroy the pop-up if there is one.

``popup_quit``类似,但我们也确保鼠标可见并将场景更改为标题屏幕。

将以下内容添加到``Globals.gd``:

func popup_quit():
    get_tree().paused = false

    Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)

    if popup != null:
        popup.queue_free()
        popup = null

    load_new_scene(MAIN_MENU_PATH)

popup_quit will resume the game, set the mouse mode to MOUSE_MODE_VISIBLE to ensure the mouse is visible in the main menu, destroy the pop-up if there is one, and change scenes to the main menu.


Before we're ready to test the pop-up, we should change one thing in Player.gd.

打开 Player.gd 并在 process_input 中,将捕获/释放光标的代码更改为以下内容:

代替:

# Capturing/Freeing cursor
if Input.is_action_just_pressed("ui_cancel"):
    if Input.get_mouse_mode() == Input.MOUSE_MODE_VISIBLE:
        Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
    else:
        Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)

您只会离开:

# Capturing/Freeing cursor
if Input.get_mouse_mode() == Input.MOUSE_MODE_VISIBLE:
    Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

Now, instead of capturing/freeing the mouse, we check whether the current mouse mode is MOUSE_MODE_VISIBLE. If it is, we set it back to MOUSE_MODE_CAPTURED.

Because the pop-up makes the mouse mode MOUSE_MODE_VISIBLE whenever you pause, we no longer have to worry about freeing and capturing the cursor in Player.gd.


Now the pause menu pop-up is finished. You can now pause at any point in the game and return to the main menu!

启动重生系统

由于游戏角色可以失去所有的健康,如果游戏角色死亡和重生,那将是理想的,所以让我们接下来添加!

Firstly, open up Player.tscn and expand HUD. Notice how there is a ColorRect called Death_Screen. When the player dies, we're going to make Death_Screen visible, and show them how long they have to wait before the player is able to respawn.

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

const RESPAWN_TIME = 4
var dead_time = 0
var is_dead = false

var globals
  • RESPAWN_TIME:重生的时间(以秒为单位)。

  • dead_time:一个跟踪游戏角色死亡时间的变量。

  • is_dead:一个跟踪游戏角色当前是否死亡的变量。

  • globals:一个变量来保存 Globals.gd 单例。


我们现在需要在 _ready 中添加几行,所以我们可以在 Player.gd 中使用 Globals.gd 。 将以下内容添加到 _ready:

globals = get_node("/root/Globals")
global_transform.origin = globals.get_respawn_position()

现在我们得到 Globals.gd 单例并将其分配给 globals。 我们还通过在游戏角色的全局中设置原点来设置游戏角色的全局位置 Transformglobals.get_respawn_position 返回的位置。

注解

别担心,我们将在下面添加“get_respawn_position`”!


Next, we need to make a few changes to _physics_process. Change _physics_process to the following:

func _physics_process(delta):

    if !is_dead:
        process_input(delta)
        process_view_input(delta)
        process_movement(delta)

    if (grabbed_object == null):
        process_changing_weapons(delta)
        process_reloading(delta)

    process_UI(delta)
    process_respawn(delta)

现在,当游戏角色死亡时,游戏角色将不会处理输入或移动输入。 我们现在也在调用 process_respawn

注解

The if !is_dead: expression is equivalent and works in the same way as the expression if is_dead == false:. And by removing the ! sign from the expression we obtain the opposite expression if is_dead == true:. It is just a shorter way of writing the same code functionality.

我们还没有制作 process_respawn ,所以让我们改变它。


让我们添加 process_respawn 。 将以下内容添加到``Player.gd``:

func process_respawn(delta):

    # If we've just died
    if health <= 0 and !is_dead:
        $Body_CollisionShape.disabled = true
        $Feet_CollisionShape.disabled = true

        changing_weapon = true
        changing_weapon_name = "UNARMED"

        $HUD/Death_Screen.visible = true

        $HUD/Panel.visible = false
        $HUD/Crosshair.visible = false

        dead_time = RESPAWN_TIME
        is_dead = true

        if grabbed_object != null:
            grabbed_object.mode = RigidBody.MODE_RIGID
            grabbed_object.apply_impulse(Vector3(0, 0, 0), -camera.global_transform.basis.z.normalized() * OBJECT_THROW_FORCE / 2)

            grabbed_object.collision_layer = 1
            grabbed_object.collision_mask = 1

            grabbed_object = null

    if is_dead:
        dead_time -= delta

        var dead_time_pretty = str(dead_time).left(3)
        $HUD/Death_Screen/Label.text = "You died\n" + dead_time_pretty + " seconds till respawn"

        if dead_time <= 0:
            global_transform.origin = globals.get_respawn_position()

            $Body_CollisionShape.disabled = false
            $Feet_CollisionShape.disabled = false

            $HUD/Death_Screen.visible = false

            $HUD/Panel.visible = true
            $HUD/Crosshair.visible = true

            for weapon in weapons:
                var weapon_node = weapons[weapon]
                if weapon_node != null:
                    weapon_node.reset_weapon()

            health = 100
            grenade_amounts = {"Grenade":2, "Sticky Grenade":2}
            current_grenade = "Grenade"

            is_dead = false

让我们来看看这个功能正在做什么。


首先,我们通过检查``health``是否等于或小于``0``并且``is_dead``是``false``来检查游戏角色是否刚刚死亡。

如果游戏角色刚刚去世,我们会禁用游戏角色的碰撞形状。 我们这样做是为了确保游戏角色不会用尸体挡住任何东西。

Next, we set changing_weapon to true and set changing_weapon_name to UNARMED. This is so, if the player is using a weapon, it is put away when they dies.

然后我们制作``Death_Screen`` ColorRect 可见,这样当游戏角色死亡时,游戏角色会得到漂亮的灰色覆盖。 然后我们制作UI的其余部分, Panel 和``Crosshair``节点,看不见。

Next, we set dead_time to RESPAWN_TIME so we can start counting down how long the player has been dead. We also set is_dead to true so we know the player has died.

If the player is holding an object when they died, we need to throw it. We first check whether the player is holding an object or not. If the player is holding a object, we throw it using the same code as the throwing code we added in 第5部分.

注解

表达式 You have died\n\n 组合是一个命令,用于在下面的新行上显示后面的文本。 当您用魔杖很好地将显示的文本分组为多行时,这总是很有用,因此它看起来更好,并且更容易被游戏游戏角色阅读。


然后我们检查游戏角色是否死了。 如果游戏角色死了,我们就从 dead_time 中删除 delta

然后我们创建一个名为 dead_time_pretty 的新变量,我们将 dead_time 转换为字符串,只使用从左边开始的前三个字符。 这为游戏角色提供了一个漂亮的字符串,显示游戏角色在游戏角色重生之前需要等待多长时间。

然后我们在``Death Screen``中更改 Label 来显示游戏角色离开的时间。

接下来我们检查游戏角色是否已经等待足够长时间并且可以重生。 我们通过检查 dead_time 是否为“0”或更少来做到这一点。

如果游戏角色等待足够长时间重生,我们将游戏角色的位置设置为“get_respawn_position”提供的新重生位置。

然后我们启用两个游戏角色的碰撞形状,以便游戏角色可以再次与环境发生碰撞。

接下来,我们使 Death_Screen 不可见,并使UI的其余部分, PanelCrosshair 节点再次可见。

然后我们通过每个武器并调用它的 reset_weapon 函数,我们将很快添加它。

然后我们将 health 重置为 100 ,将 grenade_amounts 重置为默认值,并将 current_grenade 改为 Grenade 。 这有效地将这些变量重置为其默认值。

最后,我们将 is_dead 设置为 false


在我们离开 Player.gd 之前,我们需要在 _input 中添加一个快速的东西。 在`_input``的开头添加以下内容:

if is_dead:
    return

现在当游戏角色死了,玩家无法用鼠标环顾四周。

完成重生系统

首先让我们打开 Weapon_Pistol.gd 脚本并添加 reset_weapon 函数。 添加以下内容:

func reset_weapon():
    ammo_in_weapon = 10
    spare_ammo = 20

现在当我们调用 reset_weapon 时,手枪中的弹药和备用弹药将重置为默认值。

现在让我们在 Weapon_Rifle.gd 中添加``reset_weapon``:

func reset_weapon():
    ammo_in_weapon = 50
    spare_ammo = 100

并将以下内容添加到``Weapon_Knife.gd``:

func reset_weapon():
    ammo_in_weapon = 1
    spare_ammo = 1

现在,当游戏角色死亡时,所有武器都会重置。


现在我们需要在 Globals.gd 中添加一些东西。 首先,添加以下类变量:

var respawn_points = null
  • respawn_points :一个变量,用于保存关卡中的所有重生点

因为我们每次都得到一个随机的衍生点,我们需要随机化这个数字生成器。在“_ready”中添加以下内容:

randomize()

randomize 将给我们一个新的随机种子,因此当我们使用任意一个随机函数时,我们得到一个(相对的)随机数字字符串。

现在让我们将 get_respawn_position 添加到``Globals.gd``:

func get_respawn_position():
    if respawn_points == null:
        return Vector3(0, 0, 0)
    else:
        var respawn_point = rand_range(0, respawn_points.size() - 1)
        return respawn_points[respawn_point].global_transform.origin

让我们回顾一下这个功能的作用。


首先,我们通过检查 respawn_points 是否为 null 来检查 Globals.gd 是否有任何 respawn_points

如果 respawn_points 是``null``,我们返回一个空位置 Vector 3 ,位置为``(0,0,0)``。

如果' respawn_points ' '不是' null ' ',那么我们就会得到一个介于' 0 ' '和' respawn_points ' ' '中的元素数量之间的随机数,减去' 1 ' ',因为大多数编程语言,包括' GDScript ' ' ',在访问列表中的元素时,都是从' 0 ' '开始计数的。

然后,我们在 respawn_points 的``respawn_point``位置返回 Spatial 节点的位置。


在我们完成 Globals.gd 之前。 我们需要将以下内容添加到 load_new_scene

respawn_points = null

我们将 respawn_points 设置为 null ,所以当/如果游戏角色达到没有重生点的等级时,我们不会在先前等级的重生点重生游戏角色。


现在我们需要的是一种设置重生点的方法。 打开 Ruins_Level.tscn 并选择 Spawn_Points 。 添加一个名为 Respawn_Point_Setter.gd 的新脚本,并将其附加到 Spawn_Points 。 将以下内容添加到``Respawn_Point_Setter.gd``:

extends Spatial

func _ready():
    var globals = get_node("/root/Globals")
    globals.respawn_points = get_children()

现在当一个带有 Respawn_Point_Setter.gd 的节点调用了它的 _ready 函数时,该节点的所有子节点都带有 Respawn_Point_Setter.gdSpawn_Points 在 `` Ruins_Level.tscn`` 将被添加到 Globals.gd 中的 respawn_points

警告

任何带有“Respawn_Point_Setter.gd`”的节点都必须位于 SceneTree <class_SceneTree>中的游戏角色上方,所以重新生成的点在游戏角色需要它们在游戏角色的 ``_ready` 函数之前设置。


现在当游戏角色死亡时,他们会在等待 4 秒后重生!

注解

除了 Ruins_Level.tscn 之外,还没有为任何级别设置生成点! 将生成点添加到“Space_Level.tscn”将留给读者练习。

编写一个我们可以随处使用的音响系统

最后,让我们制作一个音响系统,这样我们就可以在任何地方播放声音,而无需使用播放器。

首先,打开 SimpleAudioPlayer.gd 并将其更改为以下内容:

extends Spatial

var audio_node = null
var should_loop = false
var globals = null

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

    globals = get_node("/root/Globals")


func play_sound(audio_stream, position=null):
    if audio_stream == null:
        print ("No audio stream passed; cannot play sound")
        globals.created_audio.remove(globals.created_audio.find(self))
        queue_free()
        return

    audio_node.stream = audio_stream

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


func sound_finished():
    if should_loop:
        audio_node.play(0.0)
    else:
        globals.created_audio.remove(globals.created_audio.find(self))
        audio_node.stop()
        queue_free()

旧版本有一些变化,首先是我们不再将声音文件存储在 SimpleAudioPlayer.gd 中。 这对性能要好得多,因为我们在创建声音时不再加载每个音频片段,而是强制将音频流传递到“play_sound”。

另一个变化是我们有一个名为 should_loop 的新类变量。 我们不是仅在每次完成时销毁音频播放器,而是要检查并查看音频播放器是否设置为循环播放。 这使得我们可以像循环背景音乐那样使用音频,而不必在旧音频播放完成后用音乐生成新的音频播放器。

最后,不是在 Player.gd 中实例化/生成,而是在“Globals.gd”中生成音频播放器,这样我们就可以从任何场景创建声音。 现在音频播放器存储了 Globals.gd 单例,所以当音频播放器被销毁时,我们也可以从 Globals.gd 中的列表中删除它。

让我们回顾一下这些变化。


对于类变量,我们删除了所有的 audio_[insert name here] 变量,因为我们将从 Globals.gd 中传入这些变量。

我们还添加了两个新的类变量 should_loopglobals 。 我们将使用 should_loop 来判断音频播放器是否应该在声音结束时循环,而 globals 将保持 Globals.gd 单例。

在``_ready``里的唯一变化是现在音频播放器正在获得``Globals.gd``单例并将其分配给``globals``。

play_sound``现在需要传入一个名为``audio_stream``的音频流,而不是 ``sound_name 。 我们不是检查声音名称和设置音频播放器的流,而是检查以确保传入音频流。如果未传入音频流,我们打印错误消息,从列表中删除音频播放器 在 Globals.gd 单例中称为 created_audio ,然后释放音频播放器。

最后,在 sound_finished 中,我们首先检查音频播放器是否应该使用 should_loop 循环。 如果音频播放器应该循环,我们将从头开始再次播放声音,位置为“0.0”。 如果音频播放器不应该循环,我们从名为 created_audio 的``Globals.gd``单曲列表中删除音频播放器,然后释放音频播放器。


现在我们已完成对 SimpleAudioPlayer.gd 的更改,现在我们需要将注意力转向 Globals.gd 。 首先,添加以下类变量:

# All the audio files.

# You will need to provide your own sound files.
var audio_clips = {
    "Pistol_shot": null, #preload("res://path_to_your_audio_here!")
    "Rifle_shot": null, #preload("res://path_to_your_audio_here!")
    "Gun_cock": null, #preload("res://path_to_your_audio_here!")
}

const SIMPLE_AUDIO_PLAYER_SCENE = preload("res://Simple_Audio_Player.tscn")
var created_audio = []

让我们来看看这些全局变量。

  • audio_clips :一个 包含``Globals.gd`` 中可以播放的所有音频片段的字典。

  • SIMPLE_AUDIO_PLAYER_SCENE:简单的音频播放器场景。

  • created_audio:一个列表,用于保存所有已创建的简单的音频播放器 Globals.gd

注解

如果要添加其他音频,则需要将其添加到“audio_clips”。 本教程中未提供音频文件,因此您必须提供自己的音频文件。

我推荐的一个网站是**GameSounds.xyz**。 我正在使用2017年Sonniss'GDC游戏音频包中包含的Gamemaster音频枪声音包。我使用过的轨道(经过一些小编辑)如下:

  • gun_revolver_pistol_shot_04,

  • gun_semi_auto_rifle_cock_02,

  • gun_submachine_auto_shot_00_automatic_preview_01


现在我们需要在 Globals.gd 中添加一个名为 play_sound 的新函数:

func play_sound(sound_name, loop_sound=false, sound_position=null):
    if audio_clips.has(sound_name):
        var new_audio = SIMPLE_AUDIO_PLAYER_SCENE.instance()
        new_audio.should_loop = loop_sound

        add_child(new_audio)
        created_audio.append(new_audio)

        new_audio.play_sound(audio_clips[sound_name], sound_position)

    else:
        print ("ERROR: cannot play sound that does not exist in audio_clips!")

让我们回顾一下这个功能的作用。

首先,我们检查 Globals.gd 是否在 audio_clips 中有一个名为 sound_name 的音频剪辑。 如果没有,我们会打印一条错误消息。

如果 Globals.gd 有一个名为 sound_name 的音频剪辑,我们然后实例/生成一个新的 SIMPLE_AUDIO_PLAYER_SCENE 并将其分配给 new_audio

然后我们设置 should_loop ,并添加 new_audio 作为 Globals.gd 的子节点。

注解

请记住,我们必须小心地将节点添加到单个节点,因为在更改场景时这些节点不会被销毁。

我们将 "new_audio "添加到 "created_audio "列表中,以保存所有创建的音频。

然后我们调用 play_sound ,传入与 sound_name 相关的音频片段和声音位置。


在我们离开 Globals.gd 之前,我们需要在 load_new_scene 中添加几行代码,这样当播放器改变场景时,所有的音频都会被销毁。

将以下内容添加到``load_new_scene``:

for sound in created_audio:
    if (sound != null):
        sound.queue_free()
created_audio.clear()

现在,在 Globals.gd 改变场景之前,它会通过 created_sounds 中的每个简单音频播放器并释放/销毁它们。 一旦 Globals.gd 完成了 created_audio 中的所有声音,我们就会清除 created_audio ,这样它就不再拥有对任何(现已释放/毁坏)简单音频播放器的引用。


让我们改变 Player.gd 中的 create_sound 来使用这个新系统。 首先,从 Player.gd 的类变量中删除 simple_audio_player ,因为我们将不再直接在 Player.gd 中实例化/产生声音。

现在,将 create_sound 更改为以下内容:

func create_sound(sound_name, position=null):
    globals.play_sound(sound_name, false, position)

现在每当调用 create_sound 时,我们只需在 Globals.gd 中调用 play_sound ,传入所有收到的参数。


现在我们的FPS中的所有声音都可以在任何地方播放。 我们所要做的就是得到 Globals.gd 单例,并调用 play_sound ,传入我们想要播放的声音的名称,无论我们是否想要它循环,以及 播放声音。

例如,如果您想在手榴弹爆炸时发出爆炸声,您需要在 Globals.gdaudio_clips 中添加一个新的声音,得到 Globals.gd 单例, 然后您只需要在手榴弹 _ process 函数中添加类似 globals.play_sound(“explosion”,false,global_transform.origin) 的东西,就在手榴弹损坏其爆炸半径范围内的所有物体之后。

最后的笔记

../../../_images/FinishedTutorialPicture.png

现在您有一个完全工作的单人FPS!

在此之上,你已打下构建更复杂的FPS游戏的良好基础。

警告

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

您可以在这里下载整个教程的完成项目: Godot_FPS_Part_6.zip

注解

完成的项目源文件包含相同的代码,只是书写顺序稍有不同。因为本教程的撰写基于已完成的项目源文件。

完成的项目代码是按照创建功能的顺序编写的,不一定是理想的学习顺序。

除此之外,源代码完全相同,只是提供有用的评论,解释每个部分的作用。

小技巧

完整的项目源也托管在Github上: https://github.com/TwistedTwigleg/Godot_FPS_Tutorial

请注意,Github中的代码可能与文档中的教程同步也可能不同步

文档中的代码可能会随时更新至最新版。如果您不确定使用哪个,请使用文档中提供的项目,因为它们是由Godot社区负责维护的。

您可以在这里下载本教程中使用的所有 .blend 文件 : Godot_FPS_BlenderFiles.zip

启动资源中提供的所有资源(除非另有说明) 最初由TwistedTwigleg创建,由Godot社区进行更改/添加。 本教程提供的所有原始资源均在 MIT 许可下发布。

您可以随意使用这些资源! 所有原始资源均属于Godot社区,其他资源属于以下列出的资源:

天空盒由** StumpyStrust **创建,可以在OpenGameArt.org找到。 https://opengameart.org/content/space-skyboxes-0。 天空盒根据“CC0”许可证授权。

使用的字体是** Titillium-Regular **,并根据``SIL Open Font License,Version 1.1`许可。

使用此工具将天空盒转换为360 equirectangular图像:https://www.360toolkit.co/convert-cubemap-to-spherical-equirectangular.html

虽然没有提供声音,但您可以在https://gamesounds.xyz/找到许多游戏就绪声音

警告

OpenGameArt.org,360toolkit.co,Titillium-Regular,StumpyStrust和GameSounds.xyz的创建者都不参与本教程。