[Godot] 菜单 UI Scene 切换方法

503 阅读2分钟

这个场景管理器主要是接受资源路径的输入,进行场景的加载

可以通过设置加载模式来决定,在加载某一类场景时是否保留同类场景

还可以将被卸载的场景入栈,在需要的时候出栈

extends Node

class_name SceneManager

@export var scene_root : Node

class ScenesStackItem:
	var scene_name : String
	var group_name : String
	
	func _init(_scene_name : String, _group_name : String):
		scene_name = _scene_name
		group_name = _group_name
	
var scenes_stack : Array[ScenesStackItem]

enum LoadSceneMode{
	Single,
	Additive
}
	
func load_scene_with_relative_path(load_scene_name : String, group_name : String, load_scene_mode : LoadSceneMode):
	if scene_root == null:
		print("Have not assigned Scene Root!")
		return
	
	if load_scene_mode == LoadSceneMode.Single:
		var old_scenes = get_tree().get_nodes_in_group(group_name)
		for old_scene in old_scenes:
			old_scene.queue_free()
	
	_load_scene_with_relative_path(load_scene_name, group_name)
	
func _load_scene_with_relative_path(load_scene_name : String, group_name : String):
	var dir = DirAccess.open("res://scenes")
	var load_scene_path : String = "res://scenes/" + load_scene_name + ".tscn"
	
	if dir.file_exists(load_scene_name + ".tscn"):
		var scene = load(load_scene_path).instantiate()
		scene_root.add_child(scene)
		scene.add_to_group(group_name)
	else:
		print("File < " + load_scene_path + " > does not exist!")

func push_scene_with_relative_path(scene_name : String, group_name : String):
	scenes_stack.push_back(ScenesStackItem.new(scene_name, group_name))

func pop_scene_with_relative_path(load_scene_mode : LoadSceneMode):
	var item : ScenesStackItem = scenes_stack.pop_back()
	load_scene_with_relative_path(item.scene_name, item.group_name, load_scene_mode)

func clear_scenes_stack():
	scenes_stack.clear()

但是之后在试用的时候,发现一旦我修改了场景文件夹的组织方式,那么所有已经设置好的场景路径名称全部要修改一遍,这实在是太蠢了

于是我就改成了使用 PackedScene

extends Node

class_name SceneManager

@export var scene_root : Node

class ScenesStackItem:
	var packed_scene : PackedScene
	var group_name : String
	
	func _init(_packed_scene : PackedScene, _group_name : String):
		packed_scene = _packed_scene
		group_name = _group_name
	
var scenes_stack : Array[ScenesStackItem]

enum LoadSceneMode{
	Single,
	Additive
}
	
func load_packed_scene(packed_scene : PackedScene, group_name : String, load_scene_mode : LoadSceneMode):
	if scene_root == null:
		print("Have not assigned Scene Root!")
		return
	
	if load_scene_mode == LoadSceneMode.Single:
		var old_scenes = get_tree().get_nodes_in_group(group_name)
		for old_scene in old_scenes:
			old_scene.queue_free()
	
	_load_packed_scene(packed_scene, group_name)
	
func _load_packed_scene(packed_scene : PackedScene, group_name : String):
	if packed_scene != null:
		var scene = packed_scene.instantiate()
		scene_root.add_child(scene)
		scene.add_to_group(group_name)
	else:
		print("Packed Scene is null!")

func push_packed_scene(packed_scene : PackedScene, group_name : String):
	scenes_stack.push_back(ScenesStackItem.new(packed_scene, group_name))

func pop_packed_scene(load_scene_mode : LoadSceneMode):
	var item : ScenesStackItem = scenes_stack.pop_back()
	load_packed_scene(item.PackedScene, item.group_name, load_scene_mode)

func clear_scenes_stack():
	scenes_stack.clear()

一个使用示例是加载场景按钮:

extends CustomButton

class_name LoadSceneButton

@export var load_packed_scene : Array[PackedScene]
@export var group_names : Array[String]
@export var load_scene_modes : Array[SceneManager.LoadSceneMode]

func _ready():
	connect("button_down", Callable(self, "_load_scene"))

func _load_scene():
	# load scene
	for i in range(load_packed_scene.size()):
		Globals.scenes().load_packed_scene(load_packed_scene[i], group_names[i], load_scene_modes[i])

但是当我写到场景堆栈的时候,假设我需要在离开这个场景的时候把这个场景加入堆栈,那么这个时候 godot 就会提示我这是循环依赖

extends Control

class_name Menu

# if you need push current scene
@export var push_curr_scene_when_exit : bool = false
@export var push_packed_scene : PackedScene
@export var push_group_name : String

@export var clear_stack_when_enter: bool = false

func _enter_tree():
	if clear_stack_when_enter:
		Globals.scenes().clear_scenes_stack()
		
func _exit_tree():
	if push_curr_scene_when_exit:
		Globals.scenes().push_packed_scene(push_packed_scene, push_group_name)

因此我觉得我似乎陷入了牛角尖……其实完全不用这么多

我或许不能强求所有场景都是用一个统一的函数来加载,我完全可以把某些场景捆绑在一起,或者在游戏开始时就生成

比如暂停菜单和设置菜单这种场景,可以在游戏开始时就生成并隐藏,在整个游戏过程中不销毁

比如某个关卡的 ui 和 world 节点我完全可以绑在一起

又比如从主菜单到选关菜单,虽然进入关卡后应该销毁,但是刚进入游戏时也可以都一起生成,进入关卡之后一起销毁

从主菜单切换到选关菜单就不用什么复杂的函数了,就是隐藏主菜单,显示选关菜单

因此我现在是在游戏开始时就载入所有菜单,然后通过切换可见性来切换菜单。将功能分散到一个一个按钮里面,例如:

back_to_previous_button.gd

extends CustomButton

@export var menu_root : Control

# may be override by sub class
func _ready():
	super._ready()
	connect("custom_button_down", Callable(self, "_return_button_down"))
	
func _return_button_down():
	menu_root.visible = false
	Globals.scenes().pop_scene()

close_menu_button.gd

extends CustomButton

@export var menu_root : Control

# may be override by sub class
func _ready():
	super._ready()
	connect("custom_button_down", Callable(self, "_close_menu_button_down"))
	
func _close_menu_button_down():
	menu_root.visible = false

open_menu_button.gd

extends CustomButton

@export var menu_root : Control
@export var open_menu_root : Control

# may be override by sub class
func _ready():
	super._ready()
	connect("custom_button_down", Callable(self, "_open_menu_button_down"))
	
func _open_menu_button_down():
	menu_root.visible = false
	open_menu_root.visible = true
	Globals.scenes().push_scene(menu_root)