第6部分
部分概述
在这部分,我们将添加一个主菜单和暂停菜单,为玩家添加一个重生系统,并改变和移动声音系统,以便可以从任何脚本中使用它.
这是FPS教程的最后一部分;结束后,你将有一个坚实的基础,用Godot建立惊人的FPS游戏!
注解
假设您已经完成了 第5部分,才会进入本部分教程.从 第5部分 中完成的项目将是第六部分的起始项目
让我们开始吧!
添加主菜单
首先,打开 Main_Menu.tscn
,看看场景是如何设置的.
主菜单分为三个不同的面板,每个面板代表主菜单的不同”屏幕”.
注解
``Background_Animation``节点就是这样,菜单的背景比纯色更有趣. 这是一个环顾天空盒的相机,没什么特别的.
随意展开所有的节点,看看它们是如何设置的.记住,当你完成后,只保留 Start_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
首先,我们得到所有的 Panel 节点,并将它们分配到合适的变量中.
接下来,我们将所有的按钮 pressed
信号连接到各自的 [panel_name_here]_button_pressed
函数.
然后,我们将鼠标模式设置为 MOUSE_MODE_VISIBLE
,以保证每当玩家回到这个场景时,鼠标都是可见的.
然后我们得到一个叫 Globals
的单例.然后设置 HSlider 节点的值,使它们的值与单例中的鼠标和手柄的灵敏度一致.
注解
我们还没有制作 Globals
单例,所以不用担心! 我们很快就会成功!
在 start_menu_button_pressed
中,我们检查哪个按钮被按下.
根据按下的按钮,我们要么更改当前可见的面板,退出应用程序,要么打开Godot网站.
在 level_select_menu_button_pressed
中,我们检查按下了哪个按钮.
如果按下”后退”按钮,我们会更改当前可见的面板以返回主菜单.
如果有一个场景变化按钮被按下,首先调用 set_mouse_and_joypad_sensitivity
,这样单例( Globals.gd
)就有了 HSlider 节点的值.然后,用它的 load_new_scene
函数告诉单例改变节点,并传入玩家选择的场景的文件路径.
注解
不要担心单例,我们很快到了!
在 options_menu_button_pressed
中,我们检查按下了哪个按钮.
如果按下”后退”按钮,我们会更改当前可见的面板以返回主菜单.
如果按下 fullscreen
按钮,我们就会将 OS 的全屏模式设置为其当前值的翻转版本.
如果 vsync
按钮被按下,我们根据Vsync检查按钮的状态来设置 OS 的Vsync.
最后,让我们来看看 set_mouse_and_joypad_sensitivity
.
首先,我们得到 Globals
单例,并将其分配给一个局部变量.
然后我们将 mouse_sensitivity
和``joypad_sensitivity``变量设置为它们各自的值 HSlider 节点对应物.
使 Globals
单例
现在,为了让所有这些都能正常工作,我们需要创建 Globals
单例.在 Script
选项卡中创建一个新的脚本,并将其命名为 Globals.gd
.
注解
要制作 Globals
单例,请转到编辑器中的 Script
选项卡,然后单击 New
并出现一个 Create Script
框,除了 Path
您需要插入脚本名称 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)
正如你所看到的,它是相当小而简单的.随着这部分的进展,我们会不断给 Globals.gd
添加更复杂的逻辑,但现在,它所做的就是持有两个类变量,以及抽象定义我们如何改变场景.
mouse_sensitivity
:我们鼠标的当前灵敏度,所以我们可以在Player.gd
中加载它.joypad_sensitivity
:我们游戏手柄的当前灵敏度,所以我们可以在Player.gd
中加载它.
现在,我们将使用 Globals.gd
来实现跨场景携带变量的功能.因为鼠标和手柄的灵敏度都存储在 Globals.gd
中,我们在一个场景中做的任何改变(比如在 Options_Menu
中)都会影响玩家的灵敏度.
我们在 load_new_scene
中所做的就是调用 SceneTree 的 change_scene
函数,传入 load_new_scene
中给出的场景路径.
这就是现在 Globals.gd
所需要的所有代码!在我们测试主菜单之前,首先需要将 Globals.gd
设置为自动加载脚本.
打开``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
中设置正确文件的路径! 否则,您将无法从级别选择菜单/屏幕更改场景.
添加调试菜单
现在,让我们添加一个简单的调试场景,这样就可以在游戏中跟踪FPS(每秒帧数)等内容.打开 Debug_Display.tscn
.
你可以看到它是一个 Panel,位置在屏幕的右上角.它有三个 Labels,一个是显示游戏运行的FPS,一个是显示游戏在什么操作系统上运行,还有一个标签是显示游戏在哪个Godot版本上运行.
让我们添加填充这些代码所需的代码 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())
我们来看看这个脚本的功能.
在 _ready
中,我们使用 get_name
函数将 OS_Label
的文本设置为 OS 提供的名称.这将返回Godot被编译的操作系统或操作系统的名称.例如,当你运行Windows时,它将返回 Windows
,而当你运行Linux时,它将返回 X11
.
然后,我们将 Engine_Label
的文本设置为 Engine.get_version_info
提供的版本信息. Engine.get_version_info
会返回一个字典,里面有关于当前运行的Godot版本的有用信息.我们只关心字符串的版本,至少对这个标签来说是这样,所以我们得到这个字符串,并将其赋值为 Engine_Label
中的 text
.参见 Engine 了解更多关于 get_version_info
返回值的信息.
在 _process
中,我们将 FPS_Label
的文本设置为 Engine.get_frames_per_second
,但由于 get_frames_per_second
返回的是一个整数,所以我们必须先用 str
将其转变成一个字符串,然后才能将其添加到 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)
这将会在单例中调用一个新的函数 set_debug_display
,所以接下来让我们添加这个函数吧!
打开 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)
现在在 _ready
中,我们创建一个新的画布层,将其分配给 canvas_layer
,并将其添加为子节点.因为 Globals.gd
是一个自动加载单例,所以当游戏启动时,Godot会做一个 Node ,它将有 Globals.gd
附在上面.由于Godot制作了一个 Node ,我们可以像对待其他节点一样对待 Globals.gd
来添加或删除子节点.
我们添加一个 CanvasLayer 的原因是为了在 Globals.gd
中实例/spawn的所有GUI和UI节点总是绘制在其他所有东西之上.
当将节点添加到单例自动加载时,你必须小心不要丢失对任何子节点的引用.这是因为当你改变活动场景时,节点不会被释放和销毁,这意味着如果你在实例化和生成很多节点而没有释放它们,可能会遇到内存问题.
现在我们需要将 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
)改变场景时,调试显示也会保持.这就是在单例/自动加载中实例化/spawning节点并将它们作为子节点添加到单例/自动加载的好处.只要游戏还在运行,任何作为单例/自动加载的子节点都会一直存在,而不需要我们做任何额外的工作!
添加暂停菜单
让我们添加一个暂停菜单,这样当我们按下 ui_cancel
动作时我们可以返回主菜单.
打开 Pause_Popup.tscn
.
注意 Pause_Popup
中的根节点是一个 WindowDialog ; WindowDialog 继承自 Popup ,这意味着 WindowDialog 可以像弹出窗口一样运行.
选择 Pause_Popup
,然后一直向下滚动,直到在检查器中找到 Pause
菜单.请注意,暂停模式被设置为 process
,而不是像通常默认设置的 inherit
.这使得它即使在游戏暂停时也会继续处理,我们需要这样做才能与UI元素进行交互.
现在我们已经了解 Pause_Popup.tscn
是如何设置的,让我们编写代码来使它工作.通常,我们会在场景的根节点上附加一个脚本,在本例中是 Pause_Popup
,但由于需要在 Globals.gd
中接收一些信号,我们将在那里写弹出窗口的所有代码.
打开 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
让我们回顾一下这里发生的事情.
首先,我们检查 ui_cancel
动作是否被按下.然后,我们通过检查 popup
是否等于 null
来确保 Globals.gd
没有打开 popup
.
如果 Globals.gd
没有弹出窗口,我们实例化 POPUP_SCENE
并将其分配给 popup
.
然后我们得到退出按钮并将它的 pressed
信号分配给 popup_quit
,我们将很快添加.
接下来,我们将 WindowDialog 的 popup_hide
信号和resume按钮的 pressed
信号都分配给 popup_closed
,将很快添加.
然后,我们添加 popup
作为 canvas_layer
的子级,这样它就会被绘制在上面.然后用 popup_centered
通知 popup
在屏幕中心弹出.
接下来,我们确保鼠标模式为 MOUSE_MODE_VISIBLE
,这样玩家就可以与弹出窗口进行交互.如果我们不这样做,玩家将无法在任何鼠标模式为 MOUSE_MODE_CAPTURED
的场景中与弹出窗口交互.
最后,我们暂停整个 SceneTree.
注解
有关在Godot中暂停的更多信息,请参阅 暂停游戏
现在,我们需要添加连接信号的函数.先添加 popup_closed
.
将以下内容添加到``Globals.gd``:
func popup_closed():
get_tree().paused = false
if popup != null:
popup.queue_free()
popup = null
popup_closed
将恢复游戏,并在有弹出窗口的情况下销毁.
``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
将恢复游戏,将鼠标模式设置为 MOUSE_MODE_VISIBLE
以确保鼠标在主菜单中可见,如果有弹出窗口,则销毁弹出窗口,并将场景改为主菜单.
在我们准备测试弹出窗口之前,应该在 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)
现在,我们不是捕捉或释放鼠标,而是检查当前的鼠标模式是否为 MOUSE_MODE_VISIBLE
.如果是,就把它设置为 MOUSE_MODE_CAPTURED
.
因为每当你暂停时,弹出的窗口会使鼠标模式为 MOUSE_MODE_VISIBLE
,所以我们不再担心在 Player.gd
中释放和捕捉光标.
现在弹出的暂停菜单已经完成.现在你可以在游戏中的任何一点暂停并返回主菜单!
启动重生系统
由于游戏角色可以失去所有的健康,如果游戏角色死亡和重生,那将是理想的,所以让我们接下来添加!
首先,打开 Player.tscn
,展开 HUD
.注意有一个 ColorRect 叫 Death_Screen
.当玩家死亡时,我们将使 Death_Screen
可见,并向他们展示在玩家能够重生之前他们需要等待多长时间.
打开 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
!
接下来,我们需要对 _physics_process
进行一些修改.将 _physics_process
改为:
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
.
注解
if !is_dead:
表达式与 if is_dead == false:
表达式是等价的,工作方式相同.而把表达式中的 !
号去掉,我们就可以得到相反的表达式 if is_dead == true:
.这只是用一种更短的方式来写同样的代码功能.
我们还没有制作 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``来检查游戏角色是否刚刚死亡.
如果游戏角色刚刚去世,我们会禁用游戏角色的碰撞形状. 我们这样做是为了确保游戏角色不会用尸体挡住任何东西.
接下来,我们将 changing_weapon
设置为 true
,并将 changing_weapon_name
设置为 UNARMED
.这样一来,如果玩家在使用武器,当他们死亡时,武器就会被收起来.
然后我们制作``Death_Screen`` ColorRect 可见,这样当游戏角色死亡时,游戏角色会得到漂亮的灰色覆盖. 然后我们制作UI的其余部分, Panel
和``Crosshair``节点,看不见.
接下来,我们把 dead_time
设置为 RESPAWN_TIME
,这样就可以开始倒数玩家已经死亡多久.还将 is_dead
设置为 true
,这样就知道玩家已经死亡.
如果玩家死亡的时候手里持有一个东西,我们就需要扔掉它.我们首先检查玩家是否持有一个物体.如果玩家持有一个物体,使用与 第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的其余部分, Panel
和 Crosshair
节点再次可见.
然后我们通过每个武器并调用它的 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.gd
, Spawn_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
中实例化/生成,而是在 Player.gd
中生成音频播放器,这样我们就可以从任何场景创建声音. 现在音频播放器存储了 Globals.gd
单例,所以当音频播放器被销毁时,我们也可以从 Globals.gd
中的列表中删除它.
让我们回顾一下这些变化.
对于类变量,我们删除了所有的 audio_[insert name here]
变量,因为我们将从 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 = []
让我们来看看这些全局变量.
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.gd
的 audio_clips
中添加一个新的声音,得到 Globals.gd
单例, 然后您只需要在手榴弹 _ process
函数中添加类似 globals.play_sound("explosion",false,global_transform.origin)
的东西,就在手榴弹损坏其爆炸半径范围内的所有物体之后.
最后的笔记
现在您有一个完全工作的单人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的创建者都不参与本教程.