第6部分¶
Part overview¶
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!

注解
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
让我们开始吧!
使 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
中所做的就是调用 SceneTree 的 change_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
选项卡。

然后通过单击旁边的按钮(`
)选择``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
中设置正确文件的路径! 否则,您将无法从级别选择菜单/屏幕更改场景。
启动重生系统¶
由于游戏角色可以失去所有的健康,如果游戏角色死亡和重生,那将是理想的,所以让我们接下来添加!
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
。 我们还通过在游戏角色的全局中设置原点来设置游戏角色的全局位置 Transform 到 globals.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
组合是一个命令,用于在下面的新行上显示后面的文本。 当您用魔杖很好地将显示的文本分组为多行时,这总是很有用,因此它看起来更好,并且更容易被游戏游戏角色阅读。
Then we check whether the player is dead. If so, we then remove delta
from dead_time
.
然后我们创建一个名为 dead_time_pretty
的新变量,我们将 dead_time
转换为字符串,只使用从左边开始的前三个字符。 这为游戏角色提供了一个漂亮的字符串,显示游戏角色在游戏角色重生之前需要等待多长时间。
然后我们在``Death Screen``中更改 Label 来显示游戏角色离开的时间。
接下来我们检查游戏角色是否已经等待足够长时间并且可以重生。 我们通过检查 dead_time
是否为“0”或更少来做到这一点。
如果游戏角色等待足够长时间重生,我们将游戏角色的位置设置为“get_respawn_position”提供的新重生位置。
然后我们启用两个游戏角色的碰撞形状,以便游戏角色可以再次与环境发生碰撞。
Next, we make the Death_Screen
invisible and make the rest of the UI, the Panel
and Crosshair
nodes, visible again.
然后我们通过每个武器并调用它的 reset_weapon
函数,我们将很快添加它。
Then, we reset health
to 100
, grenade_amounts
to its default values, and change current_grenade
to Grenade
.
This effectively resets these variables to their default values.
最后,我们将 is_dead
设置为 false
。
在我们离开 Player.gd
之前,我们需要在 _input
中添加一个快速的东西。 在`_input``的开头添加以下内容:
if is_dead:
return
Now, when the player is dead, they cannot look around with the mouse.
完成重生系统¶
Firstly, let's open Weapon_Pistol.gd
and add the reset_weapon
function. Add the following:
func reset_weapon():
ammo_in_weapon = 10
spare_ammo = 20
Now, when we call reset_weapon
, the ammo in the pistol and the ammo in the spares will be reset to their default values.
现在让我们在 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
Now all the weapons will reset when the player dies.
Now we need to add a few things to Globals.gd
. Firstly, add the following class variable:
var respawn_points = null
respawn_points
: A variable to hold all the respawn points in a level
因为我们每次都得到一个随机的衍生点,我们需要随机化这个数字生成器。在“_ready”中添加以下内容:
randomize()
randomize
will get us a new random seed so we get a (relatively) random string of numbers when we use any of the random functions.
现在让我们将 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
让我们回顾一下这个功能的作用。
Firstly, we check if Globals.gd
has any respawn_points
by checking whether respawn_points
is null
or not.
如果 respawn_points
是``null``,我们返回一个空位置 Vector 3 ,位置为``(0,0,0)``。
如果' respawn_points ' '不是' null ' ',那么我们就会得到一个介于' 0 ' '和' respawn_points ' ' '中的元素数量之间的随机数,减去' 1 ' ',因为大多数编程语言,包括' GDScript ' ' ',在访问列表中的元素时,都是从' 0 ' '开始计数的。
然后,我们在 respawn_points
的``respawn_point``位置返回 Spatial 节点的位置。
Before we are done with Globals.gd
, we need to add the following to 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()
Now, when a node with Respawn_Point_Setter.gd
has its _ready
function called, all the children
nodes of the node with Respawn_Point_Setter.gd
, Spawn_Points
in the case of Ruins_Level.tscn
, will be added
to respawn_points
in Globals.gd
.
警告
任何带有“Respawn_Point_Setter.gd`”的节点都必须位于 SceneTree <class_SceneTree>中的游戏角色上方,所以重新生成的点在游戏角色需要它们在游戏角色的 ``_ready` 函数之前设置。
Now, when the player dies, they will respawn after waiting 4
seconds!
注解
除了 Ruins_Level.tscn
之外,还没有为任何级别设置生成点! 将生成点添加到“Space_Level.tscn”将留给读者练习。
编写一个我们可以随处使用的音响系统¶
Finally, let's make a sound system so we can play sounds from anywhere, without having to use the player.
Firstly, open up SimpleAudioPlayer.gd
and change it to the following:
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
中的列表中删除它。
让我们回顾一下这些变化。
For the class variables, we removed all the audio_[insert name here]
variables since we will instead have these passed in from Globals.gd
.
我们还添加了两个新的类变量 should_loop
和 globals
。 我们将使用 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 = []
Let's go over these global variables.
audio_clips
: A dictionary holding all the audio clipsGlobals.gd
can play.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!")
让我们回顾一下这个功能的作用。
Firstly, we check whether Globals.gd
has an audio clip with the name sound_name
in audio_clips
. If it does not, we print an error message.
如果 Globals.gd
有一个名为 sound_name
的音频剪辑,我们然后实例/生成一个新的 SIMPLE_AUDIO_PLAYER_SCENE
并将其分配给 new_audio
。
然后我们设置 should_loop
,并添加 new_audio
作为 Globals.gd
的子节点。
注解
请记住,我们必须小心地将节点添加到单个节点,因为在更改场景时这些节点不会被销毁。
We add the new_audio
into the created_audio
list to hold all created audios.
然后我们调用 play_sound
,传入与 sound_name
相关的音频片段和声音位置。
Before we leave Globals.gd
, we need to add a few lines of code to load_new_scene
so when the player changes scenes, all the audio is destroyed.
将以下内容添加到``load_new_scene``:
for sound in created_audio:
if (sound != null):
sound.queue_free()
created_audio.clear()
Now, before Globals.gd
changes scenes, it goes through each simple audio player in created_sounds
and frees/destroys them. Once Globals.gd
has gone through
all the sounds in created_audio
, we clear created_audio
so it no longer holds any references to any (now freed/destroyed) simple audio players.
Let's change create_sound
in Player.gd
to use this new system. First, remove simple_audio_player
from Player.gd
's class variables since we will
no longer be directly instancing/spawning sounds in Player.gd
.
现在,将 create_sound
更改为以下内容:
func create_sound(sound_name, position=null):
globals.play_sound(sound_name, false, position)
Now, whenever create_sound
is called, we simply call play_sound
in Globals.gd
, passing in all the arguments received.
Now all the sounds in our FPS can be played from anywhere. All we have to do is get the Globals.gd
singleton, and call play_sound
, pass in the name of the sound
we want to play, whether we want it to loop or not, and the position from which to play the sound.
For example, if you want to play an explosion sound when the grenade explodes you'd need to add a new sound to audio_clips
in Globals.gd
,
get the Globals.gd
singleton, and then you just need to add something like
globals.play_sound("explosion", false, global_transform.origin)
in the grenades
_process
function, right after the grenade damages all the bodies within its blast radius.
最后的笔记¶

现在您有一个完全工作的单人FPS!
在此之上,你已打下构建更复杂的FPS游戏的良好基础。
警告
如果您迷路了,请务必再次阅读代码!
您可以在这里下载整个教程的完成项目: Godot_FPS_Part_6.zip
注解
完成的项目源文件包含相同的代码,只是书写顺序稍有不同。因为本教程的撰写基于已完成的项目源文件。
完成的项目代码是按照创建功能的顺序编写的,不一定是理想的学习顺序。
除此之外,源代码完全相同,只是提供有用的评论,解释每个部分的作用。
小技巧
The finished project source is hosted on GitHub as well: https://github.com/TwistedTwigleg/Godot_FPS_Tutorial
Please note that the code in GitHub may or may not be in sync with the tutorial in the documentation.
文档中的代码可能会随时更新至最新版。如果您不确定使用哪个,请使用文档中提供的项目,因为它们是由Godot社区负责维护的。
You can download all the .blend
files used in this tutorial here: 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的创建者都不参与本教程。**