基于Godot游戏引擎实现Galgame核心功能Step2

Programming LanguagesOther游戏制作galgame制作GodotC#游戏引擎
15-10-2024 - 06:54
15-10-2024 - 06:54

上个话题我们了解了如何使用 Godot 搭建 Galgame 中最基础的对话框
在这篇话题中,我们将了解如何使用GDScript以及 C# 实现游戏中通用的UI管理器

创建脚本

我们需要创建一个脚本用来编写UI管理器的代码
首先创建一个Scripts文件夹用以存放我们项目中的脚本
随后在该文件夹下创建脚本,并选择你所需要的语言

由于该脚本不需要挂载在任何节点,其模板选择Object: Empty就可以了

image.png

在Godot中,一个脚本就是一个类,如果你创建的是 C# 脚本,那么其脚本中的类名必须和文件名一致
如果你是使用的中文类名,则文件名也必须为中文

在创建完脚本后,我们打开创建的脚本并开始编写代码
打开文件后,不同语言的脚本默认模板是不同的:

GDS

extends Node

C#

using Godot;
using System;

public partial class UI管理器 : Node
{

}

上文说过了,脚本文件名必须和类名一致,GDScript和C#都支持使用中文作为属性名
但如果你的脚本需要挂载为全局脚本的话就不要使用中文文件名和属性名,Godot引擎无法识别
这里C#使用中文属性名的好处在于,直观,易懂,但前提是你是独自开发或你的团队中没有非中文母语者,以及严格遵循命名规范的程序员

如果是严格遵循命名规范的话,
C#的命名规范为帕斯卡命名(Pascal Case),也称大驼峰命名,即PascalCase,单词间首字母大写
GDS使用蛇形命名(snake_case),即snake_case,单词间使用下划线进行分割.
由于两种语言都支持Unicode 字符,所以使用中文属性名也能过编译
(使用中文属性名的优缺点我上文已经说过了,在之后的教程中GDS将使用英文属性名,C#将使用中文属性名)

整理思路

在创建完脚本之后,我们就可以正式开始编写UI管理器的代码了
在此之前,我们需要梳理UI管理器的实现逻辑
就通用的UI管理器而言,我们需要两个主要功能

"开启UI" 以及 "关闭UI"

那么我们该如何在Godot中实现这两个功能呢 ?

实现思路

在Godot中,如果你的UI是一个独立的场景(就像我们在上个教程中创建的对话框场景),那么,我们则需要将该场景实例化为当前场景的子节点来显示它,当需要关闭这个UI时,只需要将其从父节点移除就行了
如果你的UI是当前场景中的子节点,我们开启关闭UI只需要更改其可见性就可以了
当UI是当前场景中的子节点时你可以使用以下代码来关闭或开启UI

GDS

yourUI.visible = false # 关闭UI  
yourUI.visible = true # 开启UI  

C#

你的UI节点.visible = false // 关闭UI  
你的UI节点.visible = true // 开启UI  

本教程中的UI皆是封装好的场景,这样的UI一般是通用的UI,而特殊的UI才需要作为某一场景的子节点而非单独的场景

代码编写

准备操作

创建注册表

在编写开启UI的函数之前,我们需要创建一个注册表,来储存所有我们需要管理的UI场景
我们一般使用字典来存储注册表之类的数据
所以,我们需要创建一个字典用以注册需要管理的UI场景

GDS

extends Node  
## 用于存储所有 UI 的字典
var ui_scenes = {}  

C#

using Godot;
using System.Collections.Generic;  
  
public partial class UI管理器 : Node
{
    public static Dictionary<string, PackedScene> UI场景注册表 = new Dictionary<string, PackedScene>();
}

在GDS中直接使用花括号来声明一个字典,而C#中则必须要指定类型,因为GDS是动态语言,C#是静态语言

在GDS中,双井号为文档注释,当你在代码编辑器中引用时就会看到其注释内容,以便得知其具体功能,在脚本中添加文档注释的属性也可以在Godot编辑器的离线文档中看到

声明状态变量

在声明完成注册表后,我们还需要声明一个变量来储存当前的UI实例,以及用以储存所有已实例化的UI的一个字典
除此之外,我们还需要一个变量来记录当前的UI名称以及UI的开启状态

记录UI的开启状态的变量需要根据你注册表的UI数量来分别为每个UI声明一个

GDS

extends Node  
## 用于存储所有 UI 的字典
var ui_scenes = {}  
## 当前UI
var current_ui = null  
## 用于存储实例化的 UI
var instantiated_ui = {}
## 当前UI名称
var current_ui_name = ""
## UI开启状态
var Dialogue = false

C#

using Godot;
using System.Collections.Generic;  
  
public partial class UI管理器 : Node
{
    public static Dictionary<string, PackedScene> UI场景注册表 = new Dictionary<string, PackedScene>();
    public static Control 当前UI = null;
    public static Dictionary<string, Control> 已实例化的UI = new Dictionary<string, Control>();
    public static string 当前UI的名称 = "";
    public static bool 对话框UI场景状态 = false;
}

在C#中将属性声明为静态属性(static)是为了方便在其他脚本中调用,且UI管理器属于一个工具类,其属性应该是唯一的,不需要创建实例

初始化注册表

最后,再将我们需要的UI场景注册进注册表就可以了

GDS

extends Node  
## 用于存储所有 UI 的字典
var ui_scenes = {}  
## 当前UI
var current_ui = null  
## 用于存储实例化的 UI
var instantiated_ui = {}
## 当前UI名称
var current_ui_name = ""
## UI开启状态
var Dialogue = false

func _ready():
	# 预加载所有需要管理的 UI 场景
	ui_scenes["Dialogue"] = ResourceLoader.load("res://scenes/DialogueBox.tscn")

C#

using Godot;
using System.Collections.Generic;  
  
public partial class UI管理器 : Node
{
    public static Dictionary<string, PackedScene> UI场景注册表 = new Dictionary<string, PackedScene>();
    public static Control 当前UI = null;
    public static Dictionary<string, Control> 已实例化的UI = new Dictionary<string, Control>();
    public static string 当前UI的名称 = "";
    public static bool 对话框UI场景状态 = false;

    public static void 初始化()
	{
		UI场景注册表["对话框"] = ResourceLoader.Load<PackedScene>("res://scenes/DialogueBox.tscn");
	}
}

这里使用ResourceLoader类的Load方法,将一个给定路径的文件缓存进内存里
也就是赋值给对应的变量,以便我们在之后的代码中访问,这里我们加载的是场景文件
当它缓存在内存中赋值给变量时,其类型会转变为PackedScene
也就是一个序列化场景
这个方法常用于在Godot里预加载资源

这里在GDS中使用\_ready方法将UI注册表进行初始化,因为该脚本是一个全局脚本,需要在编辑器中设置为自动加载(Auto Load),以便其他脚本访问其属性,而C#则不需要,工具类将其属性定义为静态就能直接在其他C#脚本中访问了
Godot支持跨语言访问脚本属性,如果你想了解如何在GDS中调用C#的方法以及获取变量值,我会在下文说明

尽管Godot支持GDS和C#间跨语言访问属性,但无论如何,在C#中声明的静态变量在GDS中都无法直接访问,但你可以通过使用在C#脚本中定义的方法来访问该静态变量,不然的话就会:

Invalid access to property or key '当前UI的名称' on a base object of type 'Node (UI管理器.cs)'.

编写开启UI方法

开启UI的实现逻辑在上文已经提到了,我们需要实例化UI场景添加到当前节点即可

GDS

extends Node  
## 用于存储所有 UI 的字典
var ui_scenes = {}  
## 当前UI
var current_ui = null  
## 用于存储实例化的 UI
var instantiated_ui = {}
## 当前UI名称
var current_ui_name = ""
## UI开启状态
var Dialogue = false

func _ready():
	# 预加载所有需要管理的 UI 场景
	ui_scenes["Dialogue"] = ResourceLoader.load("res://scenes/DialogueBox.tscn")

## 创建UI
## [param ui_name] UI字典中注册的字符串,即要显示的UI[br]
## [param root_node] 要显示的UI节点位置,通常为当前场景根节点[br]
func create_ui(ui_name: String, root_node: Node):
	if ui_scenes.has(ui_name):
		current_ui_name = ui_name
		current_ui = ui_scenes[ui_name].instantiate()  # 实例化新 UI
		root_node.add_child(current_ui)  # 将其添加到指定根节点
		current_ui.show()  # 显示 UI
		instantiated_ui[ui_name] = current_ui  # 存储实例化的 UI
		self.set(ui_name,true)

C#

using Godot;
using System.Collections.Generic;  
  
public partial class UI管理器 : Node
{
    public static Dictionary<string, PackedScene> UI场景注册表 = new Dictionary<string, PackedScene>();
    public static Control 当前UI = null;
    public static Dictionary<string, Control> 已实例化的UI = new Dictionary<string, Control>();
    public static string 当前UI的名称 = "";
    public static bool 对话框UI场景状态 = false;

    public static void 初始化()
    {
		UI场景注册表["对话框"] = ResourceLoader.Load<PackedScene>("res://scenes/DialogueBox.tscn");
    }

    public static void 创建UI(string UI名称, Node 根节点)
    {
	if (UI场景注册表.ContainsKey(UI名称))
	{
		当前UI的名称 = UI名称;
		当前UI = (Control)UI场景注册表[UI名称].Instantiate(); // 实例化新的UI
		根节点.AddChild(当前UI); // 将其添加到指定根节点
		当前UI.Show(); // 设置为可见
		已实例化的UI[UI名称] = 当前UI; // 储存实例化的UI
		设置UI状态(UI名称, true);
	}
    }
}

在这里,我们创建了一个create_ui/创建UI的方法来开启我们的UI,并声明了两个参数,分别用来传入要开启的UI名称以及需要添加到的父节点

首先我们需要判断开启的UI是否存在于注册表中,以免出现找不到资源的意外情况

这里使用Dictionary类的has(t)/ContainsKey()方法来检测传入的参数,即ui_name/UI名称是否为该字典的键值

如果该字典存在指定的键值的话则返回true

当UI注册表中存在指定的UI名称时,则将当前的UI名称设定为指定的UI的名称

随后使用instantiate()/Instantiate()方法实例化UI注册表中对应的UI

然后使用add_child()/AddChile()方法将实例化的UI添加为指定节点的子节点

当当前UI被添加到指定节点下后,再用show()/Show()方法将该UI可见性设定为true

并且将当前UI实例存储到已实例化的UI的字典中

最后将其状态标记为开启

GDS中使用self.set()方法将该UI对应的状态标记变量设置为true

C#中由于创建UI()是一个静态函数,不能使用this关键字,所以这里又声明了一个静态函数用以设置UI状态

    public static void 设置UI状态(string UI名称, bool 状态)
	{
		switch (UI名称)
		{
			case "对话框":
				对话框UI场景状态 = 状态;
				break;
		}
	}

这里采用switch case语句来批量设置UI状态

编写关闭UI方法

我们需要编写两个方法以满足基本需求

"关闭指定UI" 以及 "关闭当前UI

关闭指定UI

关闭指定UI的逻辑自然和创建UI相反,我们需要从父节点移除该UI节点

可以通过以下代码来实现

GDS

extends Node  
## 用于存储所有 UI 的字典
var ui_scenes = {}  
## 当前UI
var current_ui = null  
## 用于存储实例化的 UI
var instantiated_ui = {}
## 当前UI名称
var current_ui_name = ""
## UI开启状态
var Dialogue = false

func _ready():
	# 预加载所有需要管理的 UI 场景
	ui_scenes["Dialogue"] = ResourceLoader.load("res://scenes/DialogueBox.tscn")

## 创建UI
## [param ui_name] UI字典中注册的字符串,即要显示的UI[br]
## [param root_node] 要显示的UI节点位置,通常为当前场景根节点[br]
func create_ui(ui_name: String, root_node: Node):
	if ui_scenes.has(ui_name):
		current_ui_name = ui_name
		current_ui = ui_scenes[ui_name].instantiate()  # 实例化新 UI
		root_node.add_child(current_ui)  # 将其添加到指定根节点
		current_ui.show()  # 显示 UI
		instantiated_ui[ui_name] = current_ui  # 存储实例化的 UI
		self.set(ui_name,true)

## 关闭指定UI
func close_ui(ui_name: String):
	if instantiated_ui.has(ui_name):
		var ui_instance = instantiated_ui[ui_name]
		ui_instance.get_parent().remove_child(ui_instance)  # 从父节点中移除
		instantiated_ui.erase(ui_name)  # 从字典中移除
		if current_ui_name == ui_name:
			current_ui_name = ""  # 如果关闭的是当前 UI,重置当前 UI 名称
		self.set(ui_name,false)

C#

using Godot;
using System.Collections.Generic;  
  
public partial class UI管理器 : Node
{
    public static Dictionary<string, PackedScene> UI场景注册表 = new Dictionary<string, PackedScene>();
    public static Control 当前UI = null;
    public static Dictionary<string, Control> 已实例化的UI = new Dictionary<string, Control>();
    public static string 当前UI的名称 = "";
    public static bool 对话框UI场景状态 = false;

    public static void 初始化()
    {
		UI场景注册表["对话框"] = ResourceLoader.Load<PackedScene>("res://scenes/DialogueBox.tscn");
    }

    public static void 创建UI(string UI名称, Node 根节点)
    {
	if (UI场景注册表.ContainsKey(UI名称))
	{
		当前UI的名称 = UI名称;
		当前UI = (Control)UI场景注册表[UI名称].Instantiate(); // 实例化新的UI
		根节点.AddChild(当前UI); // 将其添加到指定根节点
		当前UI.Show(); // 设置为可见
		已实例化的UI[UI名称] = 当前UI; // 储存实例化的UI
		设置UI状态(UI名称, true);
	}
    }

    public static void 关闭指定UI(string UI名称)
    {
        if (已实例化的UI.ContainsKey(UI名称))
        {
	    Node UI实例 = 已实例化的UI[UI名称];
	    UI实例.GetParent().RemoveChild(UI实例); // 从父节点移除
	    已实例化的UI.Remove(UI名称); // 从字典中移除
            设置UI状态(UI名称, false);
            if (当前UI的名称 == UI名称)
	    {
	        当前UI的名称 = ""; // 如果关闭的是当前UI,重置当前UI名称
	    }
	}
    }

    public static void 设置UI状态(string UI名称, bool 状态)
    {
	switch (UI名称)
	{
	    case "对话框":
	        对话框UI场景状态 = 状态;
		break;
	}
    }
}

在这里,我们创建了一个名为close_ui/关闭指定UI,并声明了一个参数,用以传入需要关闭的UI

当调用该函数时首先会检测已实例化的UI字典中是否存在该UI

如果该UI存在于字典当中,则获取该UI的实例,使用get_parent()/GetParent()方法来获取其父节点

随后对其父节点使用remove_child()/RemoveChild()方法来将其从父节点移除,即关闭指定的UI

然后再使用erase()/Remove()方法将其从已实例化的UI字典中移除

并将UI的状态标记为关闭,如果关闭的UI是当前UI则把UI的名称重置

使用remove_child()/RemoveChild()方法并不会将其删除,也就是从内存中释放,因为UI是需要重复使用的

关闭当前UI

要关闭当前UI其逻辑和关闭指定UI是相同的,我们只需要调整部分代码即可

GDS

extends Node  
## 用于存储所有 UI 的字典
var ui_scenes = {}  
## 当前UI
var current_ui = null  
## 用于存储实例化的 UI
var instantiated_ui = {}
## 当前UI名称
var current_ui_name = ""
## UI开启状态
var Dialogue = false

func _ready():
	# 预加载所有需要管理的 UI 场景
	ui_scenes["Dialogue"] = ResourceLoader.load("res://scenes/DialogueBox.tscn")

## 创建UI
## [param ui_name] UI字典中注册的字符串,即要显示的UI[br]
## [param root_node] 要显示的UI节点位置,通常为当前场景根节点[br]
func create_ui(ui_name: String, root_node: Node):
	if ui_scenes.has(ui_name):
		current_ui_name = ui_name
		current_ui = ui_scenes[ui_name].instantiate()  # 实例化新 UI
		root_node.add_child(current_ui)  # 将其添加到指定根节点
		current_ui.show()  # 显示 UI
		instantiated_ui[ui_name] = current_ui  # 存储实例化的 UI
		self.set(ui_name,true)

## 关闭指定UI
func close_ui(ui_name: String):
	if instantiated_ui.has(ui_name):
		var ui_instance = instantiated_ui[ui_name]
		ui_instance.get_parent().remove_child(ui_instance)  # 从父节点中移除
		instantiated_ui.erase(ui_name)  # 从字典中移除
		if current_ui_name == ui_name:
			current_ui_name = ""  # 如果关闭的是当前 UI,重置当前 UI 名称
		self.set(ui_name,false)
## 关闭当前 UI
func close_current_ui():
	if current_ui:
		current_ui.get_parent().remove_child(current_ui)
		self.set(current_ui_name,false)
		current_ui = null  # 重置当前 UI 为 null
		current_ui_name = ""

C#

using Godot;
using System.Collections.Generic;  
  
public partial class UI管理器 : Node
{
    public static Dictionary<string, PackedScene> UI场景注册表 = new Dictionary<string, PackedScene>();
    public static Control 当前UI = null;
    public static Dictionary<string, Control> 已实例化的UI = new Dictionary<string, Control>();
    public static string 当前UI的名称 = "";
    public static bool 对话框UI场景状态 = false;

    public static void 初始化()
    {
		UI场景注册表["对话框"] = ResourceLoader.Load<PackedScene>("res://scenes/DialogueBox.tscn");
    }

    public static void 创建UI(string UI名称, Node 根节点)
    {
	if (UI场景注册表.ContainsKey(UI名称))
	{
		当前UI的名称 = UI名称;
		当前UI = (Control)UI场景注册表[UI名称].Instantiate(); // 实例化新的UI
		根节点.AddChild(当前UI); // 将其添加到指定根节点
		当前UI.Show(); // 设置为可见
		已实例化的UI[UI名称] = 当前UI; // 储存实例化的UI
		设置UI状态(UI名称, true);
	}
    }

    public static void 关闭指定UI(string UI名称)
    {
        if (已实例化的UI.ContainsKey(UI名称))
        {
	    Node UI实例 = 已实例化的UI[UI名称];
	    UI实例.GetParent().RemoveChild(UI实例); // 从父节点移除
	    已实例化的UI.Remove(UI名称); // 从字典中移除
            设置UI状态(UI名称, false);
            if (当前UI的名称 == UI名称)
	    {
	        当前UI的名称 = ""; // 如果关闭的是当前UI,重置当前UI名称
	    }
	}
    }

    public static void 关闭当前UI()
    {
	if (当前UI != null)
	{
	    当前UI.GetParent().RemoveChild(当前UI); // 从父节点中移除
            设置UI状态(当前UI的名称, false);
	    当前UI = null; // 重置当前UI为null
	    当前UI的名称 = "";
	}
    }

    public static void 设置UI状态(string UI名称, bool 状态)
    {
	switch (UI名称)
	{
	    case "对话框":
	        对话框UI场景状态 = 状态;
		break;
	}
    }

}

在这里,我们创建了一个close_current_ui()/关闭当前UI()的方法

当调用该方法时,会检测当前是否存在UI

如果当前存在UI则获取其父节点并将其从父节点移除

随后将其开启状态标记为关闭

并重置当前UI实例和当前UI的名称

扩展脚本

如果你还有其他需要的话,可以对脚本进行拓展

创建UI时覆盖当前UI

你可以在create_ui()/创建UI()方法中新增一个参数用以判断是否需要覆盖当前UI作为一个可选参数

如果当前存在UI且需要覆盖UI则直接调用close_current_ui()/关闭当前UI()方法来关闭当前UI实现覆盖

GDS

## 创建UI
## [param ui_name] UI字典中注册的字符串,即要显示的UI[br]
## [param root_node] 要显示的UI节点位置,通常为当前场景根节点[br]
## [param over](可选) 是否清除其他UI
func create_ui(ui_name: String, root_node: Node, over: bool = false):
	if current_ui and over:
		close_current_ui()  # 隐藏当前 UI
	if ui_scenes.has(ui_name):
		current_ui_name = ui_name
		current_ui = ui_scenes[ui_name].instantiate()  # 实例化新 UI
		root_node.add_child(current_ui)  # 将其添加到指定根节点
		current_ui.show()  # 显示 UI
		instantiated_ui[ui_name] = current_ui  # 存储实例化的 UI
		self.set(ui_name,true)

C#

	public static void 创建UI(string UI名称, Node 根节点, bool 覆盖当前UI = false)
	{
		if (当前UI != null && 覆盖当前UI)
		{
                        关闭当前UI();
		}

		if (UI场景注册表.ContainsKey(UI名称))
		{
			当前UI的名称 = UI名称;
			当前UI = (Control)UI场景注册表[UI名称].Instantiate(); // 实例化新的UI
			根节点.AddChild(当前UI); // 将其添加到指定根节点
			当前UI.Show(); // 设置为可见
			已实例化的UI[UI名称] = 当前UI; // 储存实例化的UI
			设置UI状态(UI名称, true);
		}
	}

获取当前UI

有时候你需要在外部脚本获取当前UI的实例,但却无法直接访问UI管理器脚本的UI实例变量时

你可以创建一个带有返回值的方法,当外部脚本调用时直接返回当前实例

GDS

## 获取当前UI
func get_current_ui():
	return current_ui

C#

    public static Control 获取当前UI()
	{
		return 当前UI;
	}

你可以创建多个方法或整合为一个方法来返回你需要获取的变量值,这样获取变量的方法适用于所有脚本

结束

以上就是关于如何编写UI管理器的全部内容了

下一篇教程将开始正式编写对话系统的代码

Topic status:Normal
115
kohaku