在上篇教程中,我们一起编写了一个UI管理器脚本来管理整个游戏的UI
在这篇教程中我们将开始真正编写Galgame的核心功能——对话框
跨语言脚本的功能说明
在上篇教程中我遗漏了Godot中跨语言脚本的功能的说明,所以在此说明一下
Godot支持将C#脚本挂载为单例(自动加载),使得GDS能够访问其属性。然而,静态变量和某些复杂的C#类型(如委托)是GDS无法访问的。
例如,下面这个C#的方法,在下文中我们将要编写的脚本中的方法,虽然被挂载为单例,但在GDS中无法访问:
public void 开始对话(Node 根节点, string 文件全名, float 文字速度 = 0.05f, float 对话框透明度 = 1.0f, bool 自动显示 = false, bool 对话结束后关闭对话框 = true, Action<object[]> 对话结束后回调函数 = null, float 回调函数等待时间 = 3.0f, object[] 回调函数参数 = null)
因为Godot 对某些复杂的 C# 类型(如委托)支持有限,也就导致 Godot 无法正确解析方法
为了在GDS中访问该方法,只需删除与回调函数有关的参数:
public void 开始对话(Node 根节点, string 文件全名, float 文字速度 = 0.05f, float 对话框透明度 = 1.0f, bool 自动显示 = false, bool 对话结束后关闭对话框 = true)
尽管如此,我还是不推荐使用跨语言脚本。跨语言维护成本高,且需要考虑当前编写的脚本在另一种语言中是否能够调用其方法或属性,这将带来不必要的时间成本。
如果项目同时使用GDS和C#,我推荐选择一种语言来编写一个功能模块,避免跨语言访问属性。
整理思路
创建脚本的过程不再赘述,首先我们开始整理对话系统的思路。
需要实现的功能
在设计对话系统时,我们需要考虑以下几个关键功能:
-
对话文本文件的读取:支持从外部文件中读取对话文本,便于内容的管理和更新。
-
文本占位符替换: 在对话文本中替换占位符,例如替换金钱和天数等动态内容。
-
对话流控制:
- 支持逐字显示对话内容。
- 允许自动播放对话或手动切换到下一行。
- 检测对话是否完成,并在结束后执行相应逻辑(如回调)。
-
用户交互:
- 监听用户输入,以控制对话的继续或结束。
- 可以通过点击切换下一行对话或隐藏对话框。
-
对话相关设置:允许对话框设置透明度,文字显示速度
实现思路
-
对话文本文件的读取:
- 使用
FileAccess
类打开并读取指定路径的对话文本文件,逐行解析CSV格式的数据,并填充到一个字符串数组中,以便后续调用和管理。
- 使用
-
文本占位符替换:
- 创建一个
替换对话文本占位符
方法,通过正则表达式或简单的字符串替换逻辑,替换对话文本中的动态内容(如金钱和天数)。
- 创建一个
-
对话流控制:
- 实现逐字显示对话内容的逻辑。使用
Timer
控件来控制文本逐字显示的速度,并在文本显示完成后,判断是否需要自动播放或等待用户输入。 - 在
显示下一行对话内容
方法中,确定当前对话是否结束,并在结束后调用相应的回调函数以执行特定逻辑。
- 实现逐字显示对话内容的逻辑。使用
-
用户交互:
- 监听用户的输入事件,使用
_Input
方法来捕捉用户的点击或按键输入,以决定是否切换到下一行对话或隐藏对话框。 - 提供用户通过点击切换下一行对话的功能,使对话体验更加流畅。
- 监听用户的输入事件,使用
-
对话相关设置:
- 在
开始对话
方法中,允许设置对话框的透明度和文字显示速度,以便根据游戏的需要进行动态调整。
- 在
编写脚本
准备阶段
在开始编写实际逻辑之前,我们需要声明一些变量用以实现文件读取功能
GDS
extends Node
#==================================================
# 对话系统数据
#==================================================
var dialogue_data = [] ## 储存从 CSV 文件中读取的对话内容
C#
using Godot;
using System;
using System.Threading.Tasks;
namespace 对话系统工具集
{
public class 对话系统基类
{
public static string[][] 对话数据;
}
}
在这里,我们声明了一个数组来存储对话数据
C#使用的是二维数组,因为我们的对话要至少包含角色名和对话内容字段
在C#中,为了方便管理和引用,我们使用了命名空间
该命名空间总共会包含两个类
我们目前编写的是实现的对话框基本功能的代码,属于对话系统基类
这个C#脚本不会被挂载为单例,也不会使用Godot的节点功能,所以不用继承自Godot的任何一个类
因此,该脚本的命名不必遵守Godot的规范,即文件名为类名
因为该脚本包含了命名空间以及多个类,Godot无法进行解析
文件读取
在对话系统中,我们将使用CSV文件存储我们的对话内容
CSV文件
对话数据将存储在一个CSV文件中,每一行代表一个对话,其中第一列为角色名,第二列为对话内容。
"角色名","第一行对话文本内容" "","第二行对话文本内容" "","文本对话内容和角色名可以是空的" "只要保证遵循引号包裹字段并用逗号分割的格式即可",""
GDS
extends Node
#==================================================
# 对话系统数据
#==================================================
var dialogue_data = [] ## 储存从 CSV 文件中读取的对话内容
#==================================================
# 从CSV文件加载对话内容
#==================================================
func load_dialogue_from_csv(file_name: String):
var file = null
# 根据当前语言打开指定路径文件,以只读模式读取文件内容
file = FileAccess.open("res://dialogue/"+ GlobalVariables.current_language + "/" + file_name, FileAccess.READ)
# 如果文件成功打开
if file:
dialogue_data.clear() # 清除之前的对话数据,防止重复添加旧对话数据
file.seek(0) # 将文件指针移至文件开头,从头开始读取
# 循环读取CSV文件的每一行
while not file.eof_reached():
var line = file.get_csv_line() # 获取CSV第一行,返回数组,每个元素是该行的字段
if line.size() >= 2: # 检查行是否至少有两个字段(角色名和对话)
dialogue_data.append(line) # 将字段内容存入 dialogue_data
file.close() # 关闭文件
代码解释
在GDScript版本中,首先使用FileAccess.open
函数打开指定路径下的文件,并根据全局语言变量来确定具体路径。打开文件时指定为只读模式(FileAccess.READ
)
检查文件是否成功打开,如果成功,清空之前的dialogue_data
数组,防止累积数据。同时将文件指针移至文件开头
进入循环,使用get_csv_line()
逐行读取文件的内容,直到到达文件末尾。每行读取的内容返回一个包含每个字段的数组
检查每行是否至少包含两个字段(角色名和对话内容),若满足条件,将该行数据添加到dialogue_data
数组中
文件读取完成后关闭文件,释放资源
C#
using Godot;
using System;
using System.Threading.Tasks;
namespace 对话系统工具集
{
public class 对话系统基类
{
public static string[][] 对话数据;
// 读取指定的对话文本文件,并解析成字符串数组
public static void 读取对话文本(string 文件全名)
{
var 对话文本文件 = FileAccess.Open($"res://dialogue/{全局变量.当前语言}/{文件全名}", FileAccess.ModeFlags.Read);
if (对话文本文件 != null)
{
对话数据 = new string[][] { }; // 初始化对话数据数组
对话文本文件.Seek(0); // 重置文件指针到开头
// 逐行读取CSV文件内容
while (!对话文本文件.EofReached())
{
var 读取到的行 = 对话文本文件.GetCsvLine();
// 确保读取到的行有足够的数据
if (读取到的行.Length >= 2)
{
// 动态扩展对话数据数组
Array.Resize(ref 对话数据, 对话数据.Length + 1);
对话数据[对话数据.Length - 1] = 读取到的行;
}
}
对话文本文件.Close(); // 关闭文件
}
}
}
}
代码解释
在C#版本中,同样使用FileAccess.Open
函数打开指定路径的文件,这个路径基于当前的语言设置,从全局变量获取。文件打开方式是只读模式(FileAccess.ModeFlags.Read
)
检查文件是否成功打开,如果成功,初始化一个二维数组对话数据
用于存储后续从文件中读取的内容。此处二维数组的结构是基于角色名和对话内容两列
将文件指针移到文件的起始位置,确保从文件的头开始读取内容
进入循环,使用GetCsvLine
逐行读取文件,直到到达文件末尾(EofReached()
为true
)。GetCsvLine
会返回一行内容,解析后变成一个包含CSV每个字段的数组
检查每行是否至少包含两个字段,即角色名和对话文本。若满足条件,动态扩展二维数组的大小,并将读取到的行存入对话数据
数组
文件读取完成后关闭文件,确保释放文件资源
此处的基于语言全局变量获取对话文件是用以适配多语言的,文件路径的结构树应该是这样的:
res:// └── dialogue/ ├── zh/ │ └── your_dialogue.csv ├── en/ │ └── your_dialogue.csv └── ja/ └── your_dialogue.csv
每个语言对应的文件夹下都存放着同名的对应语言的对话文本
如果你不需要多语言适配只需要将文件路径更改为这样
"res://dialogue/" + file_name "res://dialogue/{文件全名}"
此时你的对话文件路径结构树应该是:
res:// └── dialogue/ └── your_dialogue.csv
文本占位符替换
为了灵活的显示对话内容,我们可以使用占位符,将对应占位符替换为对应的变量
GDS
extends Node
#==================================================
# 对话系统数据
#==================================================
var dialogue_data = [] ## 储存从 CSV 文件中读取的对话内容
#==================================================
# 从CSV文件加载对话内容
#==================================================
func load_dialogue_from_csv(file_name: String):
var file = null
# 根据当前语言打开指定路径文件,以只读模式读取文件内容
file = FileAccess.open("res://dialogue/"+ GlobalVariables.current_language + "/" + file_name, FileAccess.READ)
# 如果文件成功打开
if file:
dialogue_data.clear() # 清除之前的对话数据,防止重复添加旧对话数据
file.seek(0) # 将文件指针移至文件开头,从头开始读取
# 循环读取CSV文件的每一行
while not file.eof_reached():
var line = file.get_csv_line() # 获取CSV第一行,返回数组,每个元素是该行的字段
if line.size() >= 2: # 检查行是否至少有两个字段(角色名和对话)
dialogue_data.append(line) # 将字段内容存入 dialogue_data
file.close() # 关闭文件
#==================================================
# 替换变量占位符
#==================================================
func replace_placeholders(text: String) -> String:
# 替换占位符为相应的变量值
text = text.replace("%gold%", str(GlobalVariables.gold))
text = text.replace("%day%", str(GlobalVariables.day))
return text
代码解释
replace_placeholders
函数接收一个字符串参数 text
,该字符串包含可能包含占位符的文本。
函数的第一步是使用 replace()
方法,将字符串中的 "%gold%"
替换为 GlobalVariables.gold
的实际值(通过将 GlobalVariables.gold
转换为字符串)
接着,函数将 "%day%"
占位符替换为 GlobalVariables.day
的实际值
最后,将替换后的字符串返回。
C#
using Godot;
using System;
using System.Threading.Tasks;
namespace 对话系统工具集
{
public class 对话系统基类
{
public static string[][] 对话数据;
// 读取指定的对话文本文件,并解析成字符串数组
public static void 读取对话文本(string 文件全名)
{
var 对话文本文件 = FileAccess.Open($"res://dialogue/{全局变量.当前语言}/{文件全名}", FileAccess.ModeFlags.Read);
if (对话文本文件 != null)
{
对话数据 = new string[][] { }; // 初始化对话数据数组
对话文本文件.Seek(0); // 重置文件指针到开头
// 逐行读取CSV文件内容
while (!对话文本文件.EofReached())
{
var 读取到的行 = 对话文本文件.GetCsvLine();
// 确保读取到的行有足够的数据
if (读取到的行.Length >= 2)
{
// 动态扩展对话数据数组
Array.Resize(ref 对话数据, 对话数据.Length + 1);
对话数据[对话数据.Length - 1] = 读取到的行;
}
}
对话文本文件.Close(); // 关闭文件
}
}
// 替换对话文本中的占位符
public static string 替换对话文本占位符(string 对话文本)
{
对话文本 = 对话文本.Replace("%gold%", 全局变量.金钱.ToString()); // 替换金钱占位符
对话文本 = 对话文本.Replace("%day%", 全局变量.天数.ToString()); // 替换天数占位符
return 对话文本;
}
}
}
代码解释
替换对话文本占位符
是一个静态方法,接收一个字符串参数 对话文本
,表示包含占位符的对话内容。
使用 Replace()
方法,将字符串中的 "%gold%"
占位符替换为 全局变量.金钱
的实际值,并通过 ToString()
方法将金钱数值转换为字符串。
同样,使用 Replace()
将 "%day%"
占位符替换为 全局变量.天数
的实际值,格式为字符串。
最后,返回替换后的 对话文本
。
逐字显示
在实现完对话系统的两个基本功能后,我们需要开始编写逐字显示的逻辑
在此之前,我们需要新增一些变量来获取数据
GDS
extends Node
#==================================================
# 对话系统数据
#==================================================
var dialogue_data = [] ## 储存从 CSV 文件中读取的对话内容
var current_line: int = 0 ## 当前正在显示的对话行数
var current_text: String = "" ## 当前行的完整对话文本
var displayed_text: String = "" ## 当前逐字显示的文本,用于更新显示部分
var char_index: int = 0 ## 当前正在显示的字符索引,逐步递增
var text_speed: float = 0.05 ## 逐字显示速度/字符间间隔时间(单位:秒)
var is_dialogue_active: bool = false ## 对话启动状态
var is_dialogue_finished: bool = true ## 对话内容完成状态
## 对话框实例
var dialogue_box = null
## 角色名节点Label
var name_label = null
## 对话文本节点Label
var dialogue_label = null
## 倒计时节点Timer
var dialogue_timer = null
#==================================================
# 从CSV文件加载对话内容
#==================================================
func load_dialogue_from_csv(file_name: String):
var file = null
# 根据当前语言打开指定路径文件,以只读模式读取文件内容
file = FileAccess.open("res://dialogue/"+ GlobalVariables.current_language + "/" + file_name, FileAccess.READ)
# 如果文件成功打开
if file:
dialogue_data.clear() # 清除之前的对话数据,防止重复添加旧对话数据
file.seek(0) # 将文件指针移至文件开头,从头开始读取
# 循环读取CSV文件的每一行
while not file.eof_reached():
var line = file.get_csv_line() # 获取CSV第一行,返回数组,每个元素是该行的字段
if line.size() >= 2: # 检查行是否至少有两个字段(角色名和对话)
dialogue_data.append(line) # 将字段内容存入 dialogue_data
file.close() # 关闭文件
#==================================================
# 替换变量占位符
#==================================================
func replace_placeholders(text: String) -> String:
# 替换占位符为相应的变量值
text = text.replace("%gold%", str(GlobalVariables.gold))
text = text.replace("%day%", str(GlobalVariables.day))
return text
C#
using Godot;
using System;
using System.Threading.Tasks;
namespace 对话系统工具集
{
public class 对话系统基类
{
public static string[][] 对话数据;
public int 当前行索引 = 0; // 从0开始代表第一行
public bool 对话已激活 = false;
public bool 对话已完成 = true;
// 读取指定的对话文本文件,并解析成字符串数组
public static void 读取对话文本(string 文件全名)
{
var 对话文本文件 = FileAccess.Open($"res://dialogue/{全局变量.当前语言}/{文件全名}", FileAccess.ModeFlags.Read);
if (对话文本文件 != null)
{
对话数据 = new string[][] { }; // 初始化对话数据数组
对话文本文件.Seek(0); // 重置文件指针到开头
// 逐行读取CSV文件内容
while (!对话文本文件.EofReached())
{
var 读取到的行 = 对话文本文件.GetCsvLine();
// 确保读取到的行有足够的数据
if (读取到的行.Length >= 2)
{
// 动态扩展对话数据数组
Array.Resize(ref 对话数据, 对话数据.Length + 1);
对话数据[对话数据.Length - 1] = 读取到的行;
}
}
对话文本文件.Close(); // 关闭文件
}
}
// 替换对话文本中的占位符
public static string 替换对话文本占位符(string 对话文本)
{
对话文本 = 对话文本.Replace("%gold%", 全局变量.金钱.ToString()); // 替换金钱占位符
对话文本 = 对话文本.Replace("%day%", 全局变量.天数.ToString()); // 替换天数占位符
return 对话文本;
}
}
public class 逐字显示对话内容 : 对话系统基类
{
public string 当前需要显示文本 = ""; // 当前需要显示的文本
public string 显示的文本 = ""; // 已显示的文本
public int 字符索引 = 0; // 当前字符索引
public bool 自动显示 = false; // 是否自动显示
public Control 对话框; // 对话框的UI控件
public Label 角色名; // 角色名的Label
public Label 对话内容; // 对话内容的Label
public Timer 对话框计时器; // 对话框的计时器
public float 文本显示速度 = 0.05f; // 文本显示速度
}
}
在这里,C#脚本新创建了一个继承自
对话系统基类
的逐字显示对话内容
类,用来编写逐字显示的逻辑
在完成准备阶段后我们就可以开始编写逐字显示对话的逻辑了
显示当前对话
GDS
extends Node
#==================================================
# 对话系统数据
#==================================================
var dialogue_data = [] ## 储存从 CSV 文件中读取的对话内容
var current_line: int = 0 ## 当前正在显示的对话行数
var current_text: String = "" ## 当前行的完整对话文本
var displayed_text: String = "" ## 当前逐字显示的文本,用于更新显示部分
var char_index: int = 0 ## 当前正在显示的字符索引,逐步递增
var text_speed: float = 0.05 ## 逐字显示速度/字符间间隔时间(单位:秒)
var is_dialogue_active: bool = false ## 对话启动状态
var is_dialogue_finished: bool = true ## 对话内容完成状态
## 对话框实例
var dialogue_box = null
## 角色名节点Label
var name_label = null
## 对话文本节点Label
var dialogue_label = null
## 倒计时节点Timer
var dialogue_timer = null
#==================================================
# 从CSV文件加载对话内容
#==================================================
func load_dialogue_from_csv(file_name: String):
var file = null
# 根据当前语言打开指定路径文件,以只读模式读取文件内容
file = FileAccess.open("res://dialogue/"+ GlobalVariables.current_language + "/" + file_name, FileAccess.READ)
# 如果文件成功打开
if file:
dialogue_data.clear() # 清除之前的对话数据,防止重复添加旧对话数据
file.seek(0) # 将文件指针移至文件开头,从头开始读取
# 循环读取CSV文件的每一行
while not file.eof_reached():
var line = file.get_csv_line() # 获取CSV第一行,返回数组,每个元素是该行的字段
if line.size() >= 2: # 检查行是否至少有两个字段(角色名和对话)
dialogue_data.append(line) # 将字段内容存入 dialogue_data
file.close() # 关闭文件
#==================================================
# 替换变量占位符
#==================================================
func replace_placeholders(text: String) -> String:
# 替换占位符为相应的变量值
text = text.replace("%gold%", str(GlobalVariables.gold))
text = text.replace("%day%", str(GlobalVariables.day))
return text
#==================================================
# 开始逐字显示当前对话
#==================================================
func show_current_dialogue():
# 对话启动状态设置为true
is_dialogue_active = true
# 如果当前行数小于对话数据的总行数
if current_line < dialogue_data.size():
# 读取当前行内容(line是一个数组)
var line = dialogue_data[current_line]
name_label.text = line[0]
current_text = replace_placeholders(line[1]) # 完整的对话文本
displayed_text = "" # 初始化已显示的文本为空
char_index = 0 # 初始化字符索引
dialogue_label.text = "" # 清空对话框中的文本
## 启动逐字显示的定时器
dialogue_timer.start(text_speed)
# 逐字显示逻辑在定时器超时回调中处理
_on_DialogueTimer_timeout() # 立即调用,开始显示第一个字符
代码解释
首先将 is_dialogue_active
设为 true
,表示对话系统处于激活状态
接着判断当前对话的行数 current_line
是否小于总对话数据的大小 dialogue_data.size()
如果条件满足,读取当前行的数据 line
,这是一个数组,其中第一个元素为角色名,第二个元素为对话内容
角色名被设置到 name_label.text
中,而对话内容经过 replace_placeholders()
(文本占位符替换) 处理后,存储到 current_text
中
displayed_text
和 char_index
被初始化为空和0,准备开始逐字显示文本
清空对话框的现有内容,接着启动 dialogue_timer
,通过定时器控制逐字显示的速度
最后,立即调用 _on_DialogueTimer_timeout()
,开始显示第一个字符
C#
using Godot;
using System;
using System.Threading.Tasks;
namespace 对话系统工具集
{
public class 对话系统基类
{
public static string[][] 对话数据;
public int 当前行索引 = 0; // 从0开始代表第一行
public bool 对话已激活 = false;
public bool 对话已完成 = true;
// 读取指定的对话文本文件,并解析成字符串数组
public static void 读取对话文本(string 文件全名)
{
var 对话文本文件 = FileAccess.Open($"res://dialogue/{全局变量.当前语言}/{文件全名}", FileAccess.ModeFlags.Read);
if (对话文本文件 != null)
{
对话数据 = new string[][] { }; // 初始化对话数据数组
对话文本文件.Seek(0); // 重置文件指针到开头
// 逐行读取CSV文件内容
while (!对话文本文件.EofReached())
{
var 读取到的行 = 对话文本文件.GetCsvLine();
// 确保读取到的行有足够的数据
if (读取到的行.Length >= 2)
{
// 动态扩展对话数据数组
Array.Resize(ref 对话数据, 对话数据.Length + 1);
对话数据[对话数据.Length - 1] = 读取到的行;
}
}
对话文本文件.Close(); // 关闭文件
}
}
// 替换对话文本中的占位符
public static string 替换对话文本占位符(string 对话文本)
{
对话文本 = 对话文本.Replace("%gold%", 全局变量.金钱.ToString()); // 替换金钱占位符
对话文本 = 对话文本.Replace("%day%", 全局变量.天数.ToString()); // 替换天数占位符
return 对话文本;
}
}
public class 逐字显示对话内容 : 对话系统基类
{
public string 当前需要显示文本 = ""; // 当前需要显示的文本
public string 显示的文本 = ""; // 已显示的文本
public int 字符索引 = 0; // 当前字符索引
public bool 自动显示 = false; // 是否自动显示
public Control 对话框; // 对话框的UI控件
public Label 角色名; // 角色名的Label
public Label 对话内容; // 对话内容的Label
public Timer 对话框计时器; // 对话框的计时器
public float 文本显示速度 = 0.05f; // 文本显示速度
// 显示当前对话内容
public async void 显示当前对话()
{
对话已激活 = true; // 标记对话已激活
// 如果当前行索引在有效范围内
if (当前行索引 < 对话数据.Length)
{
string[] 当前行数据 = 对话数据[当前行索引]; // 获取当前行的数据
角色名.Text = 当前行数据[0]; // 设置角色名
当前需要显示文本 = 替换对话文本占位符(当前行数据[1]); // 替换对话文本中的占位符
显示的文本 = ""; // 初始化显示的文本
字符索引 = 0; // 重置字符索引
对话内容.Text = ""; // 清空对话内容
对话框计时器.Start(文本显示速度); // 启动计时器
await 计时器结束后回调(); // 等待计时器结束后的回调
}
}
}
}
代码解释
首先将 对话已激活
设为 true
,表示对话系统已经激活
接着判断 当前行索引
是否小于 对话数据.Length
,确保当前行在有效范围内
如果满足条件,读取当前行的数据 当前行数据
,这是一个包含角色名和对话内容的字符串数组
将角色名设置到 角色名.Text
中,并通过 替换对话文本占位符()
处理对话文本中的占位符
初始化 显示的文本
和 字符索引
,为逐字显示文本做好准备
清空对话框中的文本,启动 对话框计时器
,并控制逐字显示的速度
最后,使用 await
等待定时器结束后的回调逻辑
计时器结束后回调
也就是当前字符显示完成后显示下一个字符的回调函数,
在此之前,我们需要新增几个变量
GDS
var allow_nextline: bool = true ## 允许进行下一段对话
var auto_display: bool = false ## 自动显示所有文本
var auto_close: bool = true ## 对话结束再次点击关闭对话框
C#
public class 逐字显示对话内容 : 对话系统基类
{
public bool 自动显示 = false; // 是否自动显示
public bool 允许进入下一行对话 = true; // 是否允许进入下一行对话
}
public class 对话系统基类
{
public bool 对话结束后再次点击关闭对话框 = true;
}
现在,我们就可以编写计时器的回调逻辑了
GDS
extends Node
#==================================================
# 对话系统数据
#==================================================
var dialogue_data = [] ## 储存从 CSV 文件中读取的对话内容
var current_line: int = 0 ## 当前正在显示的对话行数
var current_text: String = "" ## 当前行的完整对话文本
var displayed_text: String = "" ## 当前逐字显示的文本,用于更新显示部分
var char_index: int = 0 ## 当前正在显示的字符索引,逐步递增
var text_speed: float = 0.05 ## 逐字显示速度/字符间间隔时间(单位:秒)
var is_dialogue_active: bool = false ## 对话启动状态
var is_dialogue_finished: bool = true ## 对话内容完成状态
## 对话框实例
var dialogue_box = null
## 角色名节点Label
var name_label = null
## 对话文本节点Label
var dialogue_label = null
## 倒计时节点Timer
var dialogue_timer = null
var allow_nextline: bool = true ## 允许进行下一段对话
var auto_display: bool = false ## 自动显示所有文本
var auto_close: bool = true ## 对话结束再次点击自动关闭对话框
#==================================================
# 从CSV文件加载对话内容
#==================================================
func load_dialogue_from_csv(file_name: String):
var file = null
# 根据当前语言打开指定路径文件,以只读模式读取文件内容
file = FileAccess.open("res://dialogue/"+ GlobalVariables.current_language + "/" + file_name, FileAccess.READ)
# 如果文件成功打开
if file:
dialogue_data.clear() # 清除之前的对话数据,防止重复添加旧对话数据
file.seek(0) # 将文件指针移至文件开头,从头开始读取
# 循环读取CSV文件的每一行
while not file.eof_reached():
var line = file.get_csv_line() # 获取CSV第一行,返回数组,每个元素是该行的字段
if line.size() >= 2: # 检查行是否至少有两个字段(角色名和对话)
dialogue_data.append(line) # 将字段内容存入 dialogue_data
file.close() # 关闭文件
#==================================================
# 替换变量占位符
#==================================================
func replace_placeholders(text: String) -> String:
# 替换占位符为相应的变量值
text = text.replace("%gold%", str(GlobalVariables.gold))
text = text.replace("%day%", str(GlobalVariables.day))
return text
#==================================================
# 开始逐字显示当前对话
#==================================================
func show_current_dialogue():
# 对话启动状态设置为true
is_dialogue_active = true
# 如果当前行数小于对话数据的总行数
if current_line < dialogue_data.size():
# 读取当前行内容(line是一个数组)
var line = dialogue_data[current_line]
name_label.text = line[0]
current_text = replace_placeholders(line[1]) # 完整的对话文本
displayed_text = "" # 初始化已显示的文本为空
char_index = 0 # 初始化字符索引
dialogue_label.text = "" # 清空对话框中的文本
## 启动逐字显示的定时器
dialogue_timer.start(text_speed)
# 逐字显示逻辑在定时器超时回调中处理
_on_DialogueTimer_timeout() # 立即调用,开始显示第一个字符
#==================================================
# 逐字显示逻辑
#==================================================
func _on_DialogueTimer_timeout():
# 检查是否还有剩余字符未显示
if char_index < current_text.length():
displayed_text += current_text[char_index] # 添加当前字符到已显示文本
dialogue_label.text = displayed_text # 更新对话框中的文本
char_index += 1 # 增加字符索引
dialogue_timer.start(text_speed)
else:
# 当前行对话已显示完成
dialogue_timer.stop()
if auto_display:
# 停止定时器,当前对话显示完成
# 如果是自动显示则显示下一行文本
await get_tree().create_timer(3).timeout # 等待3秒
next_dialogue()
# 如果禁用了监听且对话完成则直接进入对话结算,需外部手动关闭对话框
if !allow_nextline and current_line == dialogue_data.size() - 1:
auto_close = false
_on_dialogue_end()
代码解释
当计时器触发超时事件时,检查是否还有未显示的字符。
如果字符索引小于当前文本的长度,则表示还有字符未显示,程序会将当前字符添加到已显示的文本中,并更新对话框中的文本内容
然后字符索引加一,并重新启动计时器,继续逐字显示
如果所有字符已经显示完毕,计时器将停止
如果启用了自动显示 (auto_display
为 true
),在等待3秒后会自动显示下一行对话
另外,如果当前行是最后一行并且禁用了进入下一行的选项(allow_nextline
),会触发对话结束的处理逻辑,停止自动关闭对话框
C#
using Godot;
using System;
using System.Threading.Tasks;
namespace 对话系统工具集
{
public class 对话系统基类
{
public static string[][] 对话数据;
public int 当前行索引 = 0; // 从0开始代表第一行
public bool 对话已激活 = false;
public bool 对话已完成 = true;
public bool 自动显示 = false; // 是否自动显示
public bool 允许进入下一行对话 = true; // 是否允许进入下一行对话
// 读取指定的对话文本文件,并解析成字符串数组
public static void 读取对话文本(string 文件全名)
{
var 对话文本文件 = FileAccess.Open($"res://dialogue/{全局变量.当前语言}/{文件全名}", FileAccess.ModeFlags.Read);
if (对话文本文件 != null)
{
对话数据 = new string[][] { }; // 初始化对话数据数组
对话文本文件.Seek(0); // 重置文件指针到开头
// 逐行读取CSV文件内容
while (!对话文本文件.EofReached())
{
var 读取到的行 = 对话文本文件.GetCsvLine();
// 确保读取到的行有足够的数据
if (读取到的行.Length >= 2)
{
// 动态扩展对话数据数组
Array.Resize(ref 对话数据, 对话数据.Length + 1);
对话数据[对话数据.Length - 1] = 读取到的行;
}
}
对话文本文件.Close(); // 关闭文件
}
}
// 替换对话文本中的占位符
public static string 替换对话文本占位符(string 对话文本)
{
对话文本 = 对话文本.Replace("%gold%", 全局变量.金钱.ToString()); // 替换金钱占位符
对话文本 = 对话文本.Replace("%day%", 全局变量.天数.ToString()); // 替换天数占位符
return 对话文本;
}
}
public class 逐字显示对话内容 : 对话系统基类
{
public string 当前需要显示文本 = ""; // 当前需要显示的文本
public string 显示的文本 = ""; // 已显示的文本
public int 字符索引 = 0; // 当前字符索引
public bool 自动显示 = false; // 是否自动显示
public Control 对话框; // 对话框的UI控件
public Label 角色名; // 角色名的Label
public Label 对话内容; // 对话内容的Label
public Timer 对话框计时器; // 对话框的计时器
public float 文本显示速度 = 0.05f; // 文本显示速度
public bool 对话结束后再次点击关闭对话框 = true;
// 显示当前对话内容
public async void 显示当前对话()
{
对话已激活 = true; // 标记对话已激活
// 如果当前行索引在有效范围内
if (当前行索引 < 对话数据.Length)
{
string[] 当前行数据 = 对话数据[当前行索引]; // 获取当前行的数据
角色名.Text = 当前行数据[0]; // 设置角色名
当前需要显示文本 = 替换对话文本占位符(当前行数据[1]); // 替换对话文本中的占位符
显示的文本 = ""; // 初始化显示的文本
字符索引 = 0; // 重置字符索引
对话内容.Text = ""; // 清空对话内容
对话框计时器.Start(文本显示速度); // 启动计时器
await 计时器结束后回调(); // 等待计时器结束后的回调
}
}
// 计时器结束后的回调
public async Task 计时器结束后回调()
{
if (字符索引 < 当前需要显示文本.Length) // 如果还有字符未显示
{
显示的文本 += 当前需要显示文本[字符索引]; // 显示下一个字符
对话内容.Text = 显示的文本; // 更新对话内容的显示
字符索引++; // 增加字符索引
对话框计时器.Start(文本显示速度); // 启动计时器
}
else
{
对话框计时器.Stop(); // 停止计时器
if (自动显示) // 如果是自动显示模式
{
await Task.Delay(3000); // 等待3秒后自动显示下一行
显示下一行对话内容(); // 显示下一行对话内容
}
if (!允许进入下一行对话 && 当前行索引 == 对话数据.Length - 1) // 如果不允许进入下一行且为最后一行
{
对话结束后再次点击关闭对话框 = false; // 禁止再次点击关闭对话框
await 对话结束后逻辑(); // 执行对话结束后的逻辑
}
}
}
}
}
代码解释
与 GDScript 版本类似,当字符索引小于当前文本的长度时,表示还有字符未显示,因此添加下一个字符并更新对话内容的显示。
字符索引递增后,重新启动计时器,逐字显示文本。
如果所有字符都显示完毕,停止计时器。
如果启用了自动显示模式(自动显示
),则等待3秒后自动显示下一行。
当不允许进入下一行并且当前行索引是最后一行时,触发对话结束后的逻辑并禁止再次点击关闭对话框。
显示下一行内容
紧接着,当当前行文本显示完毕后,我们需要编写显示下一行的文本的逻辑
GDS
extends Node
#==================================================
# 对话系统数据
#==================================================
var dialogue_data = [] ## 储存从 CSV 文件中读取的对话内容
var current_line: int = 0 ## 当前正在显示的对话行数
var current_text: String = "" ## 当前行的完整对话文本
var displayed_text: String = "" ## 当前逐字显示的文本,用于更新显示部分
var char_index: int = 0 ## 当前正在显示的字符索引,逐步递增
var text_speed: float = 0.05 ## 逐字显示速度/字符间间隔时间(单位:秒)
var is_dialogue_active: bool = false ## 对话启动状态
var is_dialogue_finished: bool = true ## 对话内容完成状态
## 对话框实例
var dialogue_box = null
## 角色名节点Label
var name_label = null
## 对话文本节点Label
var dialogue_label = null
## 倒计时节点Timer
var dialogue_timer = null
var allow_nextline: bool = true ## 允许进行下一段对话
var auto_display: bool = false ## 自动显示所有文本
var auto_close: bool = true ## 对话结束再次点击自动关闭对话框
#==================================================
# 从CSV文件加载对话内容
#==================================================
func load_dialogue_from_csv(file_name: String):
var file = null
# 根据当前语言打开指定路径文件,以只读模式读取文件内容
file = FileAccess.open("res://dialogue/"+ GlobalVariables.current_language + "/" + file_name, FileAccess.READ)
# 如果文件成功打开
if file:
dialogue_data.clear() # 清除之前的对话数据,防止重复添加旧对话数据
file.seek(0) # 将文件指针移至文件开头,从头开始读取
# 循环读取CSV文件的每一行
while not file.eof_reached():
var line = file.get_csv_line() # 获取CSV第一行,返回数组,每个元素是该行的字段
if line.size() >= 2: # 检查行是否至少有两个字段(角色名和对话)
dialogue_data.append(line) # 将字段内容存入 dialogue_data
file.close() # 关闭文件
#==================================================
# 替换变量占位符
#==================================================
func replace_placeholders(text: String) -> String:
# 替换占位符为相应的变量值
text = text.replace("%gold%", str(GlobalVariables.gold))
text = text.replace("%day%", str(GlobalVariables.day))
return text
#==================================================
# 开始逐字显示当前对话
#==================================================
func show_current_dialogue():
# 对话启动状态设置为true
is_dialogue_active = true
# 如果当前行数小于对话数据的总行数
if current_line < dialogue_data.size():
# 读取当前行内容(line是一个数组)
var line = dialogue_data[current_line]
name_label.text = line[0]
current_text = replace_placeholders(line[1]) # 完整的对话文本
displayed_text = "" # 初始化已显示的文本为空
char_index = 0 # 初始化字符索引
dialogue_label.text = "" # 清空对话框中的文本
## 启动逐字显示的定时器
dialogue_timer.start(text_speed)
# 逐字显示逻辑在定时器超时回调中处理
_on_DialogueTimer_timeout() # 立即调用,开始显示第一个字符
#==================================================
# 逐字显示逻辑
#==================================================
func _on_DialogueTimer_timeout():
# 检查是否还有剩余字符未显示
if char_index < current_text.length():
displayed_text += current_text[char_index] # 添加当前字符到已显示文本
dialogue_label.text = displayed_text # 更新对话框中的文本
char_index += 1 # 增加字符索引
dialogue_timer.start(text_speed)
else:
# 当前行对话已显示完成
dialogue_timer.stop()
if auto_display:
# 停止定时器,当前对话显示完成
# 如果是自动显示则显示下一行文本
await get_tree().create_timer(3).timeout # 等待3秒
next_dialogue()
# 如果禁用了监听且对话完成则直接进入对话结算,需外部手动关闭对话框
if !allow_nextline and current_line == dialogue_data.size() - 1:
auto_close = false
_on_dialogue_end()
# ==================================================
# 跳到下一行对话
# ==================================================
func next_dialogue():
if char_index >= current_text.length():
if current_line < dialogue_data.size() - 1:
current_line += 1
show_current_dialogue()
else:
# 如果到末尾则触发回调函数
_on_dialogue_end()
代码解释
首先检查当前字符索引是否已经达到文本末尾,确保当前行的文本已完全显示。
如果文本显示完毕,并且当前行不是最后一行,则将当前行索引加一,调用 show_current_dialogue()
显示下一行的内容。
如果已经是最后一行,则调用 _on_dialogue_end()
,触发对话结束后的回调逻辑。
C#
using Godot;
using System;
using System.Threading.Tasks;
namespace 对话系统工具集
{
public class 对话系统基类
{
public static string[][] 对话数据;
public int 当前行索引 = 0; // 从0开始代表第一行
public bool 对话已激活 = false;
public bool 对话已完成 = true;
public bool 自动显示 = false; // 是否自动显示
public bool 允许进入下一行对话 = true; // 是否允许进入下一行对话
// 读取指定的对话文本文件,并解析成字符串数组
public static void 读取对话文本(string 文件全名)
{
var 对话文本文件 = FileAccess.Open($"res://dialogue/{全局变量.当前语言}/{文件全名}", FileAccess.ModeFlags.Read);
if (对话文本文件 != null)
{
对话数据 = new string[][] { }; // 初始化对话数据数组
对话文本文件.Seek(0); // 重置文件指针到开头
// 逐行读取CSV文件内容
while (!对话文本文件.EofReached())
{
var 读取到的行 = 对话文本文件.GetCsvLine();
// 确保读取到的行有足够的数据
if (读取到的行.Length >= 2)
{
// 动态扩展对话数据数组
Array.Resize(ref 对话数据, 对话数据.Length + 1);
对话数据[对话数据.Length - 1] = 读取到的行;
}
}
对话文本文件.Close(); // 关闭文件
}
}
// 替换对话文本中的占位符
public static string 替换对话文本占位符(string 对话文本)
{
对话文本 = 对话文本.Replace("%gold%", 全局变量.金钱.ToString()); // 替换金钱占位符
对话文本 = 对话文本.Replace("%day%", 全局变量.天数.ToString()); // 替换天数占位符
return 对话文本;
}
}
public class 逐字显示对话内容 : 对话系统基类
{
public string 当前需要显示文本 = ""; // 当前需要显示的文本
public string 显示的文本 = ""; // 已显示的文本
public int 字符索引 = 0; // 当前字符索引
public bool 自动显示 = false; // 是否自动显示
public Control 对话框; // 对话框的UI控件
public Label 角色名; // 角色名的Label
public Label 对话内容; // 对话内容的Label
public Timer 对话框计时器; // 对话框的计时器
public float 文本显示速度 = 0.05f; // 文本显示速度
public bool 对话结束后再次点击关闭对话框 = true;
// 显示当前对话内容
public async void 显示当前对话()
{
对话已激活 = true; // 标记对话已激活
// 如果当前行索引在有效范围内
if (当前行索引 < 对话数据.Length)
{
string[] 当前行数据 = 对话数据[当前行索引]; // 获取当前行的数据
角色名.Text = 当前行数据[0]; // 设置角色名
当前需要显示文本 = 替换对话文本占位符(当前行数据[1]); // 替换对话文本中的占位符
显示的文本 = ""; // 初始化显示的文本
字符索引 = 0; // 重置字符索引
对话内容.Text = ""; // 清空对话内容
对话框计时器.Start(文本显示速度); // 启动计时器
await 计时器结束后回调(); // 等待计时器结束后的回调
}
}
// 计时器结束后的回调
public async Task 计时器结束后回调()
{
if (字符索引 < 当前需要显示文本.Length) // 如果还有字符未显示
{
显示的文本 += 当前需要显示文本[字符索引]; // 显示下一个字符
对话内容.Text = 显示的文本; // 更新对话内容的显示
字符索引++; // 增加字符索引
对话框计时器.Start(文本显示速度); // 启动计时器
}
else
{
对话框计时器.Stop(); // 停止计时器
if (自动显示) // 如果是自动显示模式
{
await Task.Delay(3000); // 等待3秒后自动显示下一行
显示下一行对话内容(); // 显示下一行对话内容
}
if (!允许进入下一行对话 && 当前行索引 == 对话数据.Length - 1) // 如果不允许进入下一行且为最后一行
{
对话结束后再次点击关闭对话框 = false; // 禁止再次点击关闭对话框
await 对话结束后逻辑(); // 执行对话结束后的逻辑
}
}
}
// 显示下一行对话内容
public async void 显示下一行对话内容()
{
if (字符索引 >= 当前需要显示文本.Length) // 如果当前字符索引已到达文本末尾
{
if (当前行索引 < 对话数据.Length - 1) // 如果还有下一行对话
{
当前行索引++; // 进入下一行
显示当前对话(); // 显示当前对话
}
else
{
await 对话结束后逻辑(); // 否则执行对话结束后的逻辑
}
}
}
}
}
代码解释
同样,首先检查当前字符索引是否到达了文本的末尾,确保文本已完全显示。
如果当前行索引还没有到达对话数据的最后一行,则将行索引加一,调用 显示当前对话()
来显示下一行。
如果已经是最后一行,则调用 对话结束后逻辑()
,进行对话结束后的处理。
对话结束后的逻辑
当对话结束后,我们需要一些逻辑来重置对话状态,关闭对话框,以及触发为每个对话实例指定的回调函数
首先我们需要再添加一些变量
GDS
var on_dialogue_end = null ## 对话结束的回调函数
var on_dialogue_end_waittime:float = 3 ## 回调函数执行延迟时间(秒)
var on_dialogue_end_params: Array = [] ## 回调函数的参数(支持多个)
C#
public class 对话系统基类
{
public Action<object[]> 对话结束后回调;
public float 对话结束后回调等待时间 = 3.0f;
public object[] 对话结束后回调函数参数;
}
然后开始编写逻辑
GDS
extends Node
#==================================================
# 对话系统数据
#==================================================
var dialogue_data = [] ## 储存从 CSV 文件中读取的对话内容
var current_line: int = 0 ## 当前正在显示的对话行数
var current_text: String = "" ## 当前行的完整对话文本
var displayed_text: String = "" ## 当前逐字显示的文本,用于更新显示部分
var char_index: int = 0 ## 当前正在显示的字符索引,逐步递增
var text_speed: float = 0.05 ## 逐字显示速度/字符间间隔时间(单位:秒)
var is_dialogue_active: bool = false ## 对话启动状态
var is_dialogue_finished: bool = true ## 对话内容完成状态
## 对话框实例
var dialogue_box = null
## 角色名节点Label
var name_label = null
## 对话文本节点Label
var dialogue_label = null
## 倒计时节点Timer
var dialogue_timer = null
var allow_nextline: bool = true ## 允许进行下一段对话
var auto_display: bool = false ## 自动显示所有文本
var auto_close: bool = true ## 对话结束再次点击自动关闭对话框
#==================================================
# 从CSV文件加载对话内容
#==================================================
func load_dialogue_from_csv(file_name: String):
var file = null
# 根据当前语言打开指定路径文件,以只读模式读取文件内容
file = FileAccess.open("res://dialogue/"+ GlobalVariables.current_language + "/" + file_name, FileAccess.READ)
# 如果文件成功打开
if file:
dialogue_data.clear() # 清除之前的对话数据,防止重复添加旧对话数据
file.seek(0) # 将文件指针移至文件开头,从头开始读取
# 循环读取CSV文件的每一行
while not file.eof_reached():
var line = file.get_csv_line() # 获取CSV第一行,返回数组,每个元素是该行的字段
if line.size() >= 2: # 检查行是否至少有两个字段(角色名和对话)
dialogue_data.append(line) # 将字段内容存入 dialogue_data
file.close() # 关闭文件
#==================================================
# 替换变量占位符
#==================================================
func replace_placeholders(text: String) -> String:
# 替换占位符为相应的变量值
text = text.replace("%gold%", str(GlobalVariables.gold))
text = text.replace("%day%", str(GlobalVariables.day))
return text
#==================================================
# 开始逐字显示当前对话
#==================================================
func show_current_dialogue():
# 对话启动状态设置为true
is_dialogue_active = true
# 如果当前行数小于对话数据的总行数
if current_line < dialogue_data.size():
# 读取当前行内容(line是一个数组)
var line = dialogue_data[current_line]
name_label.text = line[0]
current_text = replace_placeholders(line[1]) # 完整的对话文本
displayed_text = "" # 初始化已显示的文本为空
char_index = 0 # 初始化字符索引
dialogue_label.text = "" # 清空对话框中的文本
## 启动逐字显示的定时器
dialogue_timer.start(text_speed)
# 逐字显示逻辑在定时器超时回调中处理
_on_DialogueTimer_timeout() # 立即调用,开始显示第一个字符
#==================================================
# 逐字显示逻辑
#==================================================
func _on_DialogueTimer_timeout():
# 检查是否还有剩余字符未显示
if char_index < current_text.length():
displayed_text += current_text[char_index] # 添加当前字符到已显示文本
dialogue_label.text = displayed_text # 更新对话框中的文本
char_index += 1 # 增加字符索引
dialogue_timer.start(text_speed)
else:
# 当前行对话已显示完成
dialogue_timer.stop()
if auto_display:
# 停止定时器,当前对话显示完成
# 如果是自动显示则显示下一行文本
await get_tree().create_timer(3).timeout # 等待3秒
next_dialogue()
# 如果禁用了监听且对话完成则直接进入对话结算,需外部手动关闭对话框
if !allow_nextline and current_line == dialogue_data.size() - 1:
auto_close = false
_on_dialogue_end()
# ==================================================
# 跳到下一行对话
# ==================================================
func next_dialogue():
if char_index >= current_text.length():
if current_line < dialogue_data.size() - 1:
current_line += 1
show_current_dialogue()
else:
# 如果到末尾则触发回调函数
_on_dialogue_end()
# ==================================================
# 对话结束后执行的逻辑
# ==================================================
func _on_dialogue_end():
is_dialogue_finished = true
if auto_close:
UiManager.close_ui("Dialogue")
is_dialogue_active = false
if on_dialogue_end != null:
await get_tree().create_timer(on_dialogue_end_waittime).timeout # 等待
# 调用回调函数
on_dialogue_end.callv(on_dialogue_end_params)
代码解释
首先将 is_dialogue_finished
标记为 true
,表示对话已完成。
如果 auto_close
为 true
,则调用 UiManager.close_ui("Dialogue")
关闭对话框,并将 is_dialogue_active
设置为 false
,标记对话不再激活。
如果 on_dialogue_end
回调不为 null
,则等待指定的时间 on_dialogue_end_waittime
,然后调用回调函数 on_dialogue_end.callv(on_dialogue_end_params)
,并传递相应的参数。
C#
using Godot;
using System;
using System.Threading.Tasks;
namespace 对话系统工具集
{
public class 对话系统基类
{
public static string[][] 对话数据;
public int 当前行索引 = 0; // 从0开始代表第一行
public bool 对话已激活 = false;
public bool 对话已完成 = true;
public bool 自动显示 = false; // 是否自动显示
public bool 允许进入下一行对话 = true; // 是否允许进入下一行对话
public Action<object[]> 对话结束后回调;
public float 对话结束后回调等待时间 = 3.0f;
public object[] 对话结束后回调函数参数;
// 读取指定的对话文本文件,并解析成字符串数组
public static void 读取对话文本(string 文件全名)
{
var 对话文本文件 = FileAccess.Open($"res://dialogue/{全局变量.当前语言}/{文件全名}", FileAccess.ModeFlags.Read);
if (对话文本文件 != null)
{
对话数据 = new string[][] { }; // 初始化对话数据数组
对话文本文件.Seek(0); // 重置文件指针到开头
// 逐行读取CSV文件内容
while (!对话文本文件.EofReached())
{
var 读取到的行 = 对话文本文件.GetCsvLine();
// 确保读取到的行有足够的数据
if (读取到的行.Length >= 2)
{
// 动态扩展对话数据数组
Array.Resize(ref 对话数据, 对话数据.Length + 1);
对话数据[对话数据.Length - 1] = 读取到的行;
}
}
对话文本文件.Close(); // 关闭文件
}
}
// 替换对话文本中的占位符
public static string 替换对话文本占位符(string 对话文本)
{
对话文本 = 对话文本.Replace("%gold%", 全局变量.金钱.ToString()); // 替换金钱占位符
对话文本 = 对话文本.Replace("%day%", 全局变量.天数.ToString()); // 替换天数占位符
return 对话文本;
}
// 处理对话结束后的逻辑
public async Task 对话结束后逻辑()
{
对话已完成 = true; // 标记对话已完成
if (对话结束后再次点击关闭对话框)
{
// 关闭对话框的UI
UI管理器.关闭指定UI("对话框");
对话已激活 = false; // 标记对话未激活
}
if (对话结束后回调 != null)
{
// 等待设定的时间后执行回调
await Task.Delay((int)(对话结束后回调等待时间 * 1000));
对话结束后回调.Invoke(对话结束后回调函数参数); // 调用回调函数
}
}
}
public class 逐字显示对话内容 : 对话系统基类
{
public string 当前需要显示文本 = ""; // 当前需要显示的文本
public string 显示的文本 = ""; // 已显示的文本
public int 字符索引 = 0; // 当前字符索引
public bool 自动显示 = false; // 是否自动显示
public Control 对话框; // 对话框的UI控件
public Label 角色名; // 角色名的Label
public Label 对话内容; // 对话内容的Label
public Timer 对话框计时器; // 对话框的计时器
public float 文本显示速度 = 0.05f; // 文本显示速度
public bool 对话结束后再次点击关闭对话框 = true;
// 显示当前对话内容
public async void 显示当前对话()
{
对话已激活 = true; // 标记对话已激活
// 如果当前行索引在有效范围内
if (当前行索引 < 对话数据.Length)
{
string[] 当前行数据 = 对话数据[当前行索引]; // 获取当前行的数据
角色名.Text = 当前行数据[0]; // 设置角色名
当前需要显示文本 = 替换对话文本占位符(当前行数据[1]); // 替换对话文本中的占位符
显示的文本 = ""; // 初始化显示的文本
字符索引 = 0; // 重置字符索引
对话内容.Text = ""; // 清空对话内容
对话框计时器.Start(文本显示速度); // 启动计时器
await 计时器结束后回调(); // 等待计时器结束后的回调
}
}
// 计时器结束后的回调
public async Task 计时器结束后回调()
{
if (字符索引 < 当前需要显示文本.Length) // 如果还有字符未显示
{
显示的文本 += 当前需要显示文本[字符索引]; // 显示下一个字符
对话内容.Text = 显示的文本; // 更新对话内容的显示
字符索引++; // 增加字符索引
对话框计时器.Start(文本显示速度); // 启动计时器
}
else
{
对话框计时器.Stop(); // 停止计时器
if (自动显示) // 如果是自动显示模式
{
await Task.Delay(3000); // 等待3秒后自动显示下一行
显示下一行对话内容(); // 显示下一行对话内容
}
if (!允许进入下一行对话 && 当前行索引 == 对话数据.Length - 1) // 如果不允许进入下一行且为最后一行
{
对话结束后再次点击关闭对话框 = false; // 禁止再次点击关闭对话框
await 对话结束后逻辑(); // 执行对话结束后的逻辑
}
}
}
// 显示下一行对话内容
public async void 显示下一行对话内容()
{
if (字符索引 >= 当前需要显示文本.Length) // 如果当前字符索引已到达文本末尾
{
if (当前行索引 < 对话数据.Length - 1) // 如果还有下一行对话
{
当前行索引++; // 进入下一行
显示当前对话(); // 显示当前对话
}
else
{
await 对话结束后逻辑(); // 否则执行对话结束后的逻辑
}
}
}
}
}
代码解释
首先将 对话已完成
标记为 true
,表示对话已完成。
如果 对话结束后再次点击关闭对话框
为 true
,则关闭对话框的 UI,调用 UI管理器.关闭指定UI("对话框")
,并将 对话已激活
设置为 false
,表示对话框不再激活。
如果 对话结束后回调
不为 null
,则等待设定的时间 对话结束后回调等待时间
,然后调用 对话结束后回调.Invoke(对话结束后回调函数参数)
执行回调函数并传递相应参数。
监听输入
在编写完逐字显示的逻辑后,我们需要监听玩家的输入来实现显示下一行对话以及隐藏对话框
输入映射
在编写监听时,我们必须在编辑器中设定我们的输入映射
在编辑器左上角找到项目 -> 项目设置 -> 输入映射
在这里我们可以创建和编辑内置映射,在接下来的脚本中我们使用了内置映射,所以需要将界面右上角的显示内置动作给打开
打开之后第一个就是我们需要修改的内置映射ui_accept
这是UI接受事件的映射,我们点击右边的加号来添加一个鼠标左键的映射
随后在上方的添加新动作输入框中输入hidden_dialogue_box字段来添加一个新的动作
还是同样的方法,点击右边的加号,添加一个鼠标右键的映射,或任何你想要用来在对话中切换对话框显示状态的按键
在此之前,我们还需要新增几个变量,来控制监听,以便拓展功能
GDS
var allow_show_all: bool = true ## 允许立即显示所有文本
var allow_hidden: bool = true ## 允许隐藏对话框
C#
public class 逐字显示对话内容 : 对话系统基类
{
public bool 允许立即显示完当前对话 = true; // 是否允许隐藏对话框
public bool 允许隐藏对话框 = true; // 是否允许隐藏对话框
}
切换下一行对话
我这里将切换下一行对话和立即显示完当前对话写在一起,因为都是来自同一个按键事件
并且实现了galgame都有的功能,即如果当前是自动模式则取消自动模式
GDS
extends Node
#==================================================
# 对话系统数据
#==================================================
var dialogue_data = [] ## 储存从 CSV 文件中读取的对话内容
var current_line: int = 0 ## 当前正在显示的对话行数
var current_text: String = "" ## 当前行的完整对话文本
var displayed_text: String = "" ## 当前逐字显示的文本,用于更新显示部分
var char_index: int = 0 ## 当前正在显示的字符索引,逐步递增
var text_speed: float = 0.05 ## 逐字显示速度/字符间间隔时间(单位:秒)
var is_dialogue_active: bool = false ## 对话启动状态
var is_dialogue_finished: bool = true ## 对话内容完成状态
## 对话框实例
var dialogue_box = null
## 角色名节点Label
var name_label = null
## 对话文本节点Label
var dialogue_label = null
## 倒计时节点Timer
var dialogue_timer = null
var allow_nextline: bool = true ## 允许进行下一段对话
var auto_display: bool = false ## 自动显示所有文本
var auto_close: bool = true ## 对话结束再次点击自动关闭对话框
var allow_show_all: bool = true ## 允许立即显示所有文本
var allow_hidden: bool = true ## 允许隐藏对话框
#==================================================
# 从CSV文件加载对话内容
#==================================================
func load_dialogue_from_csv(file_name: String):
var file = null
# 根据当前语言打开指定路径文件,以只读模式读取文件内容
file = FileAccess.open("res://dialogue/"+ GlobalVariables.current_language + "/" + file_name, FileAccess.READ)
# 如果文件成功打开
if file:
dialogue_data.clear() # 清除之前的对话数据,防止重复添加旧对话数据
file.seek(0) # 将文件指针移至文件开头,从头开始读取
# 循环读取CSV文件的每一行
while not file.eof_reached():
var line = file.get_csv_line() # 获取CSV第一行,返回数组,每个元素是该行的字段
if line.size() >= 2: # 检查行是否至少有两个字段(角色名和对话)
dialogue_data.append(line) # 将字段内容存入 dialogue_data
file.close() # 关闭文件
#==================================================
# 替换变量占位符
#==================================================
func replace_placeholders(text: String) -> String:
# 替换占位符为相应的变量值
text = text.replace("%gold%", str(GlobalVariables.gold))
text = text.replace("%day%", str(GlobalVariables.day))
return text
#==================================================
# 开始逐字显示当前对话
#==================================================
func show_current_dialogue():
# 对话启动状态设置为true
is_dialogue_active = true
# 如果当前行数小于对话数据的总行数
if current_line < dialogue_data.size():
# 读取当前行内容(line是一个数组)
var line = dialogue_data[current_line]
name_label.text = line[0]
current_text = replace_placeholders(line[1]) # 完整的对话文本
displayed_text = "" # 初始化已显示的文本为空
char_index = 0 # 初始化字符索引
dialogue_label.text = "" # 清空对话框中的文本
## 启动逐字显示的定时器
dialogue_timer.start(text_speed)
# 逐字显示逻辑在定时器超时回调中处理
_on_DialogueTimer_timeout() # 立即调用,开始显示第一个字符
#==================================================
# 逐字显示逻辑
#==================================================
func _on_DialogueTimer_timeout():
# 检查是否还有剩余字符未显示
if char_index < current_text.length():
displayed_text += current_text[char_index] # 添加当前字符到已显示文本
dialogue_label.text = displayed_text # 更新对话框中的文本
char_index += 1 # 增加字符索引
dialogue_timer.start(text_speed)
else:
# 当前行对话已显示完成
dialogue_timer.stop()
if auto_display:
# 停止定时器,当前对话显示完成
# 如果是自动显示则显示下一行文本
await get_tree().create_timer(3).timeout # 等待3秒
next_dialogue()
# 如果禁用了监听且对话完成则直接进入对话结算,需外部手动关闭对话框
if !allow_nextline and current_line == dialogue_data.size() - 1:
auto_close = false
_on_dialogue_end()
# ==================================================
# 跳到下一行对话
# ==================================================
func next_dialogue():
if char_index >= current_text.length():
if current_line < dialogue_data.size() - 1:
current_line += 1
show_current_dialogue()
else:
# 如果到末尾则触发回调函数
_on_dialogue_end()
# ==================================================
# 对话结束后执行的逻辑
# ==================================================
func _on_dialogue_end():
is_dialogue_finished = true
if auto_close:
UiManager.close_ui("Dialogue")
is_dialogue_active = false
if on_dialogue_end != null:
await get_tree().create_timer(on_dialogue_end_waittime).timeout # 等待
# 调用回调函数
on_dialogue_end.callv(on_dialogue_end_params)
# ==================================================
# 捕捉输入
# ==================================================
func _input(event):
if !is_dialogue_finished and event.is_action_pressed("ui_accept"): # 按下确认键时
if allow_nextline and dialogue_timer.is_stopped() and char_index >= current_text.length() and dialogue_box.visible:
next_dialogue() # 切换到下一段对话
elif allow_show_all and not dialogue_timer.is_stopped():
# 立即显示全部文本
dialogue_timer.stop()
dialogue_label.text = current_text
char_index = current_text.length()
auto_display = false
代码解释
首先,函数 _input(event)
用于处理输入事件
如果对话尚未结束且玩家按下确认键(ui_accept
),将会进入后续逻辑
接着,判断是否允许切换到下一段对话、对话定时器是否停止、当前文本是否已显示完、以及对话框是否可见
如果这些条件都满足,调用 next_dialogue()
函数,切换到下一段对话
如果条件不满足,检查是否允许立即显示当前对话的完整文本,并且对话定时器未停止
如果是这种情况,首先停止定时器,然后将当前对话的完整文本显示在对话框中,并更新字符索引以反映文本的完整显示
最后,将 auto_display
设置为 false
,禁用自动显示功能
using Godot;
using System;
using System.Threading.Tasks;
namespace 对话系统工具集
{
public class 对话系统基类
{
public static string[][] 对话数据;
public int 当前行索引 = 0; // 从0开始代表第一行
public bool 对话已激活 = false;
public bool 对话已完成 = true;
public bool 自动显示 = false; // 是否自动显示
public bool 允许进入下一行对话 = true; // 是否允许进入下一行对话
// 读取指定的对话文本文件,并解析成字符串数组
public static void 读取对话文本(string 文件全名)
{
var 对话文本文件 = FileAccess.Open($"res://dialogue/{全局变量.当前语言}/{文件全名}", FileAccess.ModeFlags.Read);
if (对话文本文件 != null)
{
对话数据 = new string[][] { }; // 初始化对话数据数组
对话文本文件.Seek(0); // 重置文件指针到开头
// 逐行读取CSV文件内容
while (!对话文本文件.EofReached())
{
var 读取到的行 = 对话文本文件.GetCsvLine();
// 确保读取到的行有足够的数据
if (读取到的行.Length >= 2)
{
// 动态扩展对话数据数组
Array.Resize(ref 对话数据, 对话数据.Length + 1);
对话数据[对话数据.Length - 1] = 读取到的行;
}
}
对话文本文件.Close(); // 关闭文件
}
}
// 替换对话文本中的占位符
public static string 替换对话文本占位符(string 对话文本)
{
对话文本 = 对话文本.Replace("%gold%", 全局变量.金钱.ToString()); // 替换金钱占位符
对话文本 = 对话文本.Replace("%day%", 全局变量.天数.ToString()); // 替换天数占位符
return 对话文本;
}
// 处理对话结束后的逻辑
public async Task 对话结束后逻辑()
{
对话已完成 = true; // 标记对话已完成
if (对话结束后再次点击关闭对话框)
{
// 关闭对话框的UI
UI管理器.关闭指定UI("对话框");
对话已激活 = false; // 标记对话未激活
}
if (对话结束后回调 != null)
{
// 等待设定的时间后执行回调
await Task.Delay((int)(对话结束后回调等待时间 * 1000));
对话结束后回调.Invoke(对话结束后回调函数参数); // 调用回调函数
}
}
}
public class 逐字显示对话内容 : 对话系统基类
{
public string 当前需要显示文本 = ""; // 当前需要显示的文本
public string 显示的文本 = ""; // 已显示的文本
public int 字符索引 = 0; // 当前字符索引
public bool 自动显示 = false; // 是否自动显示
public Control 对话框; // 对话框的UI控件
public Label 角色名; // 角色名的Label
public Label 对话内容; // 对话内容的Label
public Timer 对话框计时器; // 对话框的计时器
public float 文本显示速度 = 0.05f; // 文本显示速度
public bool 对话结束后再次点击关闭对话框 = true;
public bool 允许立即显示完当前对话 = true; // 是否允许隐藏对话框
public bool 允许隐藏对话框 = true; // 是否允许隐藏对话框
// 显示当前对话内容
public async void 显示当前对话()
{
对话已激活 = true; // 标记对话已激活
// 如果当前行索引在有效范围内
if (当前行索引 < 对话数据.Length)
{
string[] 当前行数据 = 对话数据[当前行索引]; // 获取当前行的数据
角色名.Text = 当前行数据[0]; // 设置角色名
当前需要显示文本 = 替换对话文本占位符(当前行数据[1]); // 替换对话文本中的占位符
显示的文本 = ""; // 初始化显示的文本
字符索引 = 0; // 重置字符索引
对话内容.Text = ""; // 清空对话内容
对话框计时器.Start(文本显示速度); // 启动计时器
await 计时器结束后回调(); // 等待计时器结束后的回调
}
}
// 计时器结束后的回调
public async Task 计时器结束后回调()
{
if (字符索引 < 当前需要显示文本.Length) // 如果还有字符未显示
{
显示的文本 += 当前需要显示文本[字符索引]; // 显示下一个字符
对话内容.Text = 显示的文本; // 更新对话内容的显示
字符索引++; // 增加字符索引
对话框计时器.Start(文本显示速度); // 启动计时器
}
else
{
对话框计时器.Stop(); // 停止计时器
if (自动显示) // 如果是自动显示模式
{
await Task.Delay(3000); // 等待3秒后自动显示下一行
显示下一行对话内容(); // 显示下一行对话内容
}
if (!允许进入下一行对话 && 当前行索引 == 对话数据.Length - 1) // 如果不允许进入下一行且为最后一行
{
对话结束后再次点击关闭对话框 = false; // 禁止再次点击关闭对话框
await 对话结束后逻辑(); // 执行对话结束后的逻辑
}
}
}
// 显示下一行对话内容
public async void 显示下一行对话内容()
{
if (字符索引 >= 当前需要显示文本.Length) // 如果当前字符索引已到达文本末尾
{
if (当前行索引 < 对话数据.Length - 1) // 如果还有下一行对话
{
当前行索引++; // 进入下一行
显示当前对话(); // 显示当前对话
}
else
{
await 对话结束后逻辑(); // 否则执行对话结束后的逻辑
}
}
}
// 监听切换下一行对话的输入
public void 监听切换下一行对话(InputEvent @event)
{
if (@event.IsActionPressed("ui_accept")) // 如果接受输入被按下
{
if (允许进入下一行对话 && 对话框计时器.IsStopped() && 字符索引 >= 当前需要显示文本.Length && 对话框.Visible) // 如果计时器停止并且文本显示完成且对话框可见
{
显示下一行对话内容(); // 显示下一行对话内容
}
else if(允许立即显示完当前对话 && !对话框计时器.IsStopped()){
对话框计时器.Stop(); // 停止计时器
对话内容.Text = 当前需要显示文本; // 显示完整的当前文本
字符索引 = 当前需要显示文本.Length; // 更新字符索引
自动显示 = false; // 禁止自动显示
}
}
}
}
}
代码解释
监听切换下一行对话
方法用于处理切换对话的输入事件
首先,检查接受输入是否被按下(ui_accept
)
如果被按下,接着判断允许进入下一行对话的条件,包括对话框计时器是否停止、字符索引是否已达到文本的末尾以及对话框是否可见
如果条件满足,调用 显示下一行对话内容()
方法,进行下一行对话的显示
如果条件不满足,则检查是否允许立即显示当前对话的完整文本,并且对话框计时器处于运行状态
在满足条件时,停止计时器,更新对话内容为完整的当前文本,并更新字符索引,以便反映文本的完整显示
最后,将 自动显示
设置为 false
,禁止自动显示功能。
切换对话框可见性
还是galgame的通用功能,一般是右键切换对话框可见性
GDS
extends Node
#==================================================
# 对话系统数据
#==================================================
var dialogue_data = [] ## 储存从 CSV 文件中读取的对话内容
var current_line: int = 0 ## 当前正在显示的对话行数
var current_text: String = "" ## 当前行的完整对话文本
var displayed_text: String = "" ## 当前逐字显示的文本,用于更新显示部分
var char_index: int = 0 ## 当前正在显示的字符索引,逐步递增
var text_speed: float = 0.05 ## 逐字显示速度/字符间间隔时间(单位:秒)
var is_dialogue_active: bool = false ## 对话启动状态
var is_dialogue_finished: bool = true ## 对话内容完成状态
## 对话框实例
var dialogue_box = null
## 角色名节点Label
var name_label = null
## 对话文本节点Label
var dialogue_label = null
## 倒计时节点Timer
var dialogue_timer = null
var allow_nextline: bool = true ## 允许进行下一段对话
var auto_display: bool = false ## 自动显示所有文本
var auto_close: bool = true ## 对话结束再次点击自动关闭对话框
var allow_show_all: bool = true ## 允许立即显示所有文本
var allow_hidden: bool = true ## 允许隐藏对话框
#==================================================
# 从CSV文件加载对话内容
#==================================================
func load_dialogue_from_csv(file_name: String):
var file = null
# 根据当前语言打开指定路径文件,以只读模式读取文件内容
file = FileAccess.open("res://dialogue/"+ GlobalVariables.current_language + "/" + file_name, FileAccess.READ)
# 如果文件成功打开
if file:
dialogue_data.clear() # 清除之前的对话数据,防止重复添加旧对话数据
file.seek(0) # 将文件指针移至文件开头,从头开始读取
# 循环读取CSV文件的每一行
while not file.eof_reached():
var line = file.get_csv_line() # 获取CSV第一行,返回数组,每个元素是该行的字段
if line.size() >= 2: # 检查行是否至少有两个字段(角色名和对话)
dialogue_data.append(line) # 将字段内容存入 dialogue_data
file.close() # 关闭文件
#==================================================
# 替换变量占位符
#==================================================
func replace_placeholders(text: String) -> String:
# 替换占位符为相应的变量值
text = text.replace("%gold%", str(GlobalVariables.gold))
text = text.replace("%day%", str(GlobalVariables.day))
return text
#==================================================
# 开始逐字显示当前对话
#==================================================
func show_current_dialogue():
# 对话启动状态设置为true
is_dialogue_active = true
# 如果当前行数小于对话数据的总行数
if current_line < dialogue_data.size():
# 读取当前行内容(line是一个数组)
var line = dialogue_data[current_line]
name_label.text = line[0]
current_text = replace_placeholders(line[1]) # 完整的对话文本
displayed_text = "" # 初始化已显示的文本为空
char_index = 0 # 初始化字符索引
dialogue_label.text = "" # 清空对话框中的文本
## 启动逐字显示的定时器
dialogue_timer.start(text_speed)
# 逐字显示逻辑在定时器超时回调中处理
_on_DialogueTimer_timeout() # 立即调用,开始显示第一个字符
#==================================================
# 逐字显示逻辑
#==================================================
func _on_DialogueTimer_timeout():
# 检查是否还有剩余字符未显示
if char_index < current_text.length():
displayed_text += current_text[char_index] # 添加当前字符到已显示文本
dialogue_label.text = displayed_text # 更新对话框中的文本
char_index += 1 # 增加字符索引
dialogue_timer.start(text_speed)
else:
# 当前行对话已显示完成
dialogue_timer.stop()
if auto_display:
# 停止定时器,当前对话显示完成
# 如果是自动显示则显示下一行文本
await get_tree().create_timer(3).timeout # 等待3秒
next_dialogue()
# 如果禁用了监听且对话完成则直接进入对话结算,需外部手动关闭对话框
if !allow_nextline and current_line == dialogue_data.size() - 1:
auto_close = false
_on_dialogue_end()
# ==================================================
# 跳到下一行对话
# ==================================================
func next_dialogue():
if char_index >= current_text.length():
if current_line < dialogue_data.size() - 1:
current_line += 1
show_current_dialogue()
else:
# 如果到末尾则触发回调函数
_on_dialogue_end()
# ==================================================
# 对话结束后执行的逻辑
# ==================================================
func _on_dialogue_end():
is_dialogue_finished = true
if auto_close:
UiManager.close_ui("Dialogue")
is_dialogue_active = false
if on_dialogue_end != null:
await get_tree().create_timer(on_dialogue_end_waittime).timeout # 等待
# 调用回调函数
on_dialogue_end.callv(on_dialogue_end_params)
# ==================================================
# 捕捉输入
# ==================================================
func _input(event):
if !is_dialogue_finished and event.is_action_pressed("ui_accept"): # 按下确认键时
if allow_nextline and dialogue_timer.is_stopped() and char_index >= current_text.length() and dialogue_box.visible:
next_dialogue() # 切换到下一段对话
elif allow_show_all and not dialogue_timer.is_stopped():
# 立即显示全部文本
dialogue_timer.stop()
dialogue_label.text = current_text
char_index = current_text.length()
auto_display = false
if allow_hidden and is_dialogue_active and event.is_action_pressed("hidden_dialogue_box"):
# 按下右键关闭或开启对话框
dialogue_box.visible = !dialogue_box.visible # 切换可见性
if dialogue_box.visible:
dialogue_timer.start(text_speed) # 如果对话框可见,开始计时
else:
dialogue_timer.stop() # 如果对话框不可见,停止计时
代码解释
在_input(event)
函数中新增了检查是否允许隐藏对话框的操作,以及对话是否处于活动状态,且玩家按下了隐藏对话框的按键(hidden_dialogue_box
)
如果条件满足,切换对话框的可见性。如果对话框现在可见,则开始计时器以继续文本显示;如果对话框现在隐藏,则停止计时器,暂停文本显示。
using Godot;
using System;
using System.Threading.Tasks;
namespace 对话系统工具集
{
public class 对话系统基类
{
public static string[][] 对话数据;
public int 当前行索引 = 0; // 从0开始代表第一行
public bool 对话已激活 = false;
public bool 对话已完成 = true;
public bool 自动显示 = false; // 是否自动显示
public bool 允许进入下一行对话 = true; // 是否允许进入下一行对话
// 读取指定的对话文本文件,并解析成字符串数组
public static void 读取对话文本(string 文件全名)
{
var 对话文本文件 = FileAccess.Open($"res://dialogue/{全局变量.当前语言}/{文件全名}", FileAccess.ModeFlags.Read);
if (对话文本文件 != null)
{
对话数据 = new string[][] { }; // 初始化对话数据数组
对话文本文件.Seek(0); // 重置文件指针到开头
// 逐行读取CSV文件内容
while (!对话文本文件.EofReached())
{
var 读取到的行 = 对话文本文件.GetCsvLine();
// 确保读取到的行有足够的数据
if (读取到的行.Length >= 2)
{
// 动态扩展对话数据数组
Array.Resize(ref 对话数据, 对话数据.Length + 1);
对话数据[对话数据.Length - 1] = 读取到的行;
}
}
对话文本文件.Close(); // 关闭文件
}
}
// 替换对话文本中的占位符
public static string 替换对话文本占位符(string 对话文本)
{
对话文本 = 对话文本.Replace("%gold%", 全局变量.金钱.ToString()); // 替换金钱占位符
对话文本 = 对话文本.Replace("%day%", 全局变量.天数.ToString()); // 替换天数占位符
return 对话文本;
}
// 处理对话结束后的逻辑
public async Task 对话结束后逻辑()
{
对话已完成 = true; // 标记对话已完成
if (对话结束后再次点击关闭对话框)
{
// 关闭对话框的UI
UI管理器.关闭指定UI("对话框");
对话已激活 = false; // 标记对话未激活
}
if (对话结束后回调 != null)
{
// 等待设定的时间后执行回调
await Task.Delay((int)(对话结束后回调等待时间 * 1000));
对话结束后回调.Invoke(对话结束后回调函数参数); // 调用回调函数
}
}
}
public class 逐字显示对话内容 : 对话系统基类
{
public string 当前需要显示文本 = ""; // 当前需要显示的文本
public string 显示的文本 = ""; // 已显示的文本
public int 字符索引 = 0; // 当前字符索引
public bool 自动显示 = false; // 是否自动显示
public Control 对话框; // 对话框的UI控件
public Label 角色名; // 角色名的Label
public Label 对话内容; // 对话内容的Label
public Timer 对话框计时器; // 对话框的计时器
public float 文本显示速度 = 0.05f; // 文本显示速度
public bool 对话结束后再次点击关闭对话框 = true;
public bool 允许立即显示完当前对话 = true; // 是否允许隐藏对话框
public bool 允许隐藏对话框 = true; // 是否允许隐藏对话框
// 显示当前对话内容
public async void 显示当前对话()
{
对话已激活 = true; // 标记对话已激活
// 如果当前行索引在有效范围内
if (当前行索引 < 对话数据.Length)
{
string[] 当前行数据 = 对话数据[当前行索引]; // 获取当前行的数据
角色名.Text = 当前行数据[0]; // 设置角色名
当前需要显示文本 = 替换对话文本占位符(当前行数据[1]); // 替换对话文本中的占位符
显示的文本 = ""; // 初始化显示的文本
字符索引 = 0; // 重置字符索引
对话内容.Text = ""; // 清空对话内容
对话框计时器.Start(文本显示速度); // 启动计时器
await 计时器结束后回调(); // 等待计时器结束后的回调
}
}
// 计时器结束后的回调
public async Task 计时器结束后回调()
{
if (字符索引 < 当前需要显示文本.Length) // 如果还有字符未显示
{
显示的文本 += 当前需要显示文本[字符索引]; // 显示下一个字符
对话内容.Text = 显示的文本; // 更新对话内容的显示
字符索引++; // 增加字符索引
对话框计时器.Start(文本显示速度); // 启动计时器
}
else
{
对话框计时器.Stop(); // 停止计时器
if (自动显示) // 如果是自动显示模式
{
await Task.Delay(3000); // 等待3秒后自动显示下一行
显示下一行对话内容(); // 显示下一行对话内容
}
if (!允许进入下一行对话 && 当前行索引 == 对话数据.Length - 1) // 如果不允许进入下一行且为最后一行
{
对话结束后再次点击关闭对话框 = false; // 禁止再次点击关闭对话框
await 对话结束后逻辑(); // 执行对话结束后的逻辑
}
}
}
// 显示下一行对话内容
public async void 显示下一行对话内容()
{
if (字符索引 >= 当前需要显示文本.Length) // 如果当前字符索引已到达文本末尾
{
if (当前行索引 < 对话数据.Length - 1) // 如果还有下一行对话
{
当前行索引++; // 进入下一行
显示当前对话(); // 显示当前对话
}
else
{
await 对话结束后逻辑(); // 否则执行对话结束后的逻辑
}
}
}
// 监听切换下一行对话的输入
public void 监听切换下一行对话(InputEvent @event)
{
if (@event.IsActionPressed("ui_accept")) // 如果接受输入被按下
{
if (允许进入下一行对话 && 对话框计时器.IsStopped() && 字符索引 >= 当前需要显示文本.Length && 对话框.Visible) // 如果计时器停止并且文本显示完成且对话框可见
{
显示下一行对话内容(); // 显示下一行对话内容
}
else if(允许立即显示完当前对话 && !对话框计时器.IsStopped()){
对话框计时器.Stop(); // 停止计时器
对话内容.Text = 当前需要显示文本; // 显示完整的当前文本
字符索引 = 当前需要显示文本.Length; // 更新字符索引
自动显示 = false; // 禁止自动显示
}
}
}
// 监听隐藏对话框的输入
public void 监听隐藏对话框()
{
对话框.Visible = !对话框.Visible; // 切换对话框的可见性
if (对话框.Visible) // 如果对话框现在可见
{
对话框计时器.Start(文本显示速度); // 启动计时器以继续显示文本
}
else // 如果对话框现在隐藏
{
对话框计时器.Stop(); // 停止计时器,暂停文本显示
}
}
}
}
代码解释
监听隐藏对话框
方法用于处理隐藏对话框的输入事件
首先,切换对话框的可见性。 如果对话框现在可见,则启动计时器,以继续文本显示;如果对话框现在隐藏,则停止计时器,暂停文本显示。
基类脚本拓展
在实现对话框核心功能后,我们可以对基类脚本进行拓展以实现更多功能
GDS
# ================================================== # 检测当前对话行数 # ================================================== func check_current_line(line_index:int) -> bool: while is_dialogue_active: await get_tree().create_timer(0.01).timeout if current_line == line_index: return true return false
代码解释
check_current_line(line_index:int) -> bool
函数用于检查当前对话行索引是否达到指定值在函数内部,使用
while
循环来持续检查对话是否处于活动状态每次循环调用
await get_tree().create_timer(0.01).timeout
以每 10 毫秒暂停一次,确保检查不会阻塞主线程如果
current_line
等于传入的line_index
,则返回true
,表示当前行索引匹配如果循环结束而没有找到匹配的行索引,则返回
false
,表示未匹配C#
public class 对话系统基类 { // 检测当前对话行索引是否达到指定值 public async Task<bool> 检测当前对话行索引(int 行索引) { while (对话已激活) { await Task.Delay(10); // 每10毫秒检查一次 if (当前行索引 == 行索引) // 如果当前行索引匹配 { return true; // 返回成功 } } return false; // 返回失败 } }
代码解释
检测当前对话行索引(int 行索引)
方法用于检查当前对话行索引是否达到指定值同样地,在
while
循环中持续检查对话已激活
的状态每次循环调用
await Task.Delay(10)
,以每 10 毫秒检查一次如果
当前行索引
等于传入的行索引
,则返回true
,表示匹配成功如果循环结束而没有找到匹配的行索引,则返回
false
,表示匹配失败
编写启动函数
在编写完所有对话系统的逻辑后,我们需要一个启动函数来启动对话
而我们也可以通过这个启动函数来设置对话框的状态
GDS
extends Node
#==================================================
# 对话系统数据
#==================================================
var dialogue_data = [] ## 储存从 CSV 文件中读取的对话内容
var current_line: int = 0 ## 当前正在显示的对话行数
var current_text: String = "" ## 当前行的完整对话文本
var displayed_text: String = "" ## 当前逐字显示的文本,用于更新显示部分
var char_index: int = 0 ## 当前正在显示的字符索引,逐步递增
var text_speed: float = 0.05 ## 逐字显示速度/字符间间隔时间(单位:秒)
var is_dialogue_active: bool = false ## 对话启动状态
var is_dialogue_finished: bool = true ## 对话内容完成状态
## 对话框实例
var dialogue_box = null
## 角色名节点Label
var name_label = null
## 对话文本节点Label
var dialogue_label = null
## 倒计时节点Timer
var dialogue_timer = null
var allow_nextline: bool = true ## 允许进行下一段对话
var auto_display: bool = false ## 自动显示所有文本
var auto_close: bool = true ## 对话结束再次点击自动关闭对话框
var allow_show_all: bool = true ## 允许立即显示所有文本
var allow_hidden: bool = true ## 允许隐藏对话框
#==================================================
# 从CSV文件加载对话内容
#==================================================
func load_dialogue_from_csv(file_name: String):
var file = null
# 根据当前语言打开指定路径文件,以只读模式读取文件内容
file = FileAccess.open("res://dialogue/"+ GlobalVariables.current_language + "/" + file_name, FileAccess.READ)
# 如果文件成功打开
if file:
dialogue_data.clear() # 清除之前的对话数据,防止重复添加旧对话数据
file.seek(0) # 将文件指针移至文件开头,从头开始读取
# 循环读取CSV文件的每一行
while not file.eof_reached():
var line = file.get_csv_line() # 获取CSV第一行,返回数组,每个元素是该行的字段
if line.size() >= 2: # 检查行是否至少有两个字段(角色名和对话)
dialogue_data.append(line) # 将字段内容存入 dialogue_data
file.close() # 关闭文件
#==================================================
# 替换变量占位符
#==================================================
func replace_placeholders(text: String) -> String:
# 替换占位符为相应的变量值
text = text.replace("%gold%", str(GlobalVariables.gold))
text = text.replace("%day%", str(GlobalVariables.day))
return text
#==================================================
# 开始逐字显示当前对话
#==================================================
func show_current_dialogue():
# 对话启动状态设置为true
is_dialogue_active = true
# 如果当前行数小于对话数据的总行数
if current_line < dialogue_data.size():
# 读取当前行内容(line是一个数组)
var line = dialogue_data[current_line]
name_label.text = line[0]
current_text = replace_placeholders(line[1]) # 完整的对话文本
displayed_text = "" # 初始化已显示的文本为空
char_index = 0 # 初始化字符索引
dialogue_label.text = "" # 清空对话框中的文本
## 启动逐字显示的定时器
dialogue_timer.start(text_speed)
# 逐字显示逻辑在定时器超时回调中处理
_on_DialogueTimer_timeout() # 立即调用,开始显示第一个字符
#==================================================
# 逐字显示逻辑
#==================================================
func _on_DialogueTimer_timeout():
# 检查是否还有剩余字符未显示
if char_index < current_text.length():
displayed_text += current_text[char_index] # 添加当前字符到已显示文本
dialogue_label.text = displayed_text # 更新对话框中的文本
char_index += 1 # 增加字符索引
dialogue_timer.start(text_speed)
else:
# 当前行对话已显示完成
dialogue_timer.stop()
if auto_display:
# 停止定时器,当前对话显示完成
# 如果是自动显示则显示下一行文本
await get_tree().create_timer(3).timeout # 等待3秒
next_dialogue()
# 如果禁用了监听且对话完成则直接进入对话结算,需外部手动关闭对话框
if !allow_nextline and current_line == dialogue_data.size() - 1:
auto_close = false
_on_dialogue_end()
# ==================================================
# 跳到下一行对话
# ==================================================
func next_dialogue():
if char_index >= current_text.length():
if current_line < dialogue_data.size() - 1:
current_line += 1
show_current_dialogue()
else:
# 如果到末尾则触发回调函数
_on_dialogue_end()
# ==================================================
# 对话结束后执行的逻辑
# ==================================================
func _on_dialogue_end():
is_dialogue_finished = true
if auto_close:
UiManager.close_ui("Dialogue")
is_dialogue_active = false
if on_dialogue_end != null:
await get_tree().create_timer(on_dialogue_end_waittime).timeout # 等待
# 调用回调函数
on_dialogue_end.callv(on_dialogue_end_params)
# ==================================================
# 捕捉输入
# ==================================================
func _input(event):
if !is_dialogue_finished and event.is_action_pressed("ui_accept"): # 按下确认键时
if allow_nextline and dialogue_timer.is_stopped() and char_index >= current_text.length() and dialogue_box.visible:
next_dialogue() # 切换到下一段对话
elif allow_show_all and not dialogue_timer.is_stopped():
# 立即显示全部文本
dialogue_timer.stop()
dialogue_label.text = current_text
char_index = current_text.length()
auto_display = false
if allow_hidden and is_dialogue_active and event.is_action_pressed("hidden_dialogue_box"):
# 按下右键关闭或开启对话框
dialogue_box.visible = !dialogue_box.visible # 切换可见性
if dialogue_box.visible:
dialogue_timer.start(text_speed) # 如果对话框可见,开始计时
else:
dialogue_timer.stop() # 如果对话框不可见,停止计时
# ==================================================
# 显示对话内容
# ==================================================
func start_dialogue(root_node: Node, file_name: String, speed: float = 0.05, opacity: float = 1.00, auto: bool = false, close: bool = true, end_callback: Callable = Callable(), waittime: float = 3,callback_params: Array = []):
# 实例化对话框
UiManager.creat_ui("Dialogue",root_node)
dialogue_box = UiManager.get_current_ui()
# 获取对话框子节点
name_label = dialogue_box.get_node("Name")
dialogue_label = dialogue_box.get_node("Dialogue")
dialogue_timer = dialogue_box.get_node("DialogueTimer")
# 将对话内容显示完成状态设置为false
is_dialogue_finished = false
# 设置透明度
dialogue_box.self_modulate.a = opacity
# 设置对话速度
text_speed = speed
# 设置文字显示模式
auto_display = auto
auto_close = close
# 设置回调函数并保存参数数组
on_dialogue_end_waittime = waittime
on_dialogue_end = end_callback
on_dialogue_end_params = callback_params
# 加载对话文本数据
load_dialogue_from_csv(file_name)
current_line = 0 # 初始化为第一行对话
## 连接信号
dialogue_timer.connect("timeout", Callable(self, "_on_DialogueTimer_timeout"))
show_current_dialogue() # 显示对话
代码解释
实例化对话框的 UI,使用 UiManager.creat_ui("Dialogue", root_node)
创建对话框并将其添加到指定的根节点
通过 UiManager.get_current_ui()
获取当前对话框的引用,并分别获取其子节点,包括角色名标签、对话内容标签和对话计时器
设置一个布尔值 is_dialogue_finished
,用于表示对话是否完成,初始值为 false
通过 dialogue_box.self_modulate.a = opacity
设置对话框的透明度
设置对话文本的显示速度,使用参数 speed
设置自动显示和自动关闭的模式,使用参数 auto
和 close
准备好结束对话时的回调函数和相关参数
使用 load_dialogue_from_csv(file_name)
加载对话文本数据,并将当前行索引初始化为 0
连接计时器的超时信号到 _on_DialogueTimer_timeout
方法,以便在计时器超时时处理相关逻辑
调用 show_current_dialogue()
方法显示当前的对话内容
C#
using Godot;
using System;
using 对话系统工具集;
public partial class 显示对话文本 : Node
{
// 创建一个逐字显示对话内容的实例
public static 逐字显示对话内容 逐字显示 = new 逐字显示对话内容();
// 开始对话的方法,接受各种参数进行初始化
public void 开始对话(Node 根节点, string 文件全名, float 文字速度 = 0.05f, float 对话框透明度 = 1.0f, bool 自动显示 = false, bool 对话结束后关闭对话框 = true, Action<object[]> 对话结束后回调函数 = null, float 回调函数等待时间 = 3.0f, object[] 回调函数参数 = null)
{
UI管理器.创建UI("对话框", 根节点); // 创建对话框的UI
逐字显示.对话框 = UI管理器.获取当前UI(); // 获取当前创建的对话框
逐字显示.角色名 = 逐字显示.对话框.GetNode<Label>("Name"); // 获取角色名的Label
逐字显示.对话内容 = 逐字显示.对话框.GetNode<Label>("Dialogue"); // 获取对话内容的Label
逐字显示.对话框计时器 = 逐字显示.对话框.GetNode<Timer>("DialogueTimer"); // 获取计时器
逐字显示.对话已完成 = false; // 标记对话尚未完成
逐字显示.对话框.SelfModulate = new Color(1, 1, 1, 对话框透明度); // 设置对话框透明度
逐字显示.文本显示速度 = 文字速度; // 设置文本显示速度
逐字显示.自动显示 = 自动显示; // 设置是否自动显示下一行对话
逐字显示.对话结束后再次点击关闭对话框 = 对话结束后关闭对话框; // 设置对话结束后是否允许再次关闭对话框
逐字显示.对话结束后回调等待时间 = 回调函数等待时间; // 设置对话结束后回调函数的等待时间
逐字显示.对话结束后回调 = 对话结束后回调函数; // 设置对话结束后的回调函数
逐字显示.对话结束后回调函数参数 = 回调函数参数; // 设置回调函数的参数
对话系统基类.读取对话文本(文件全名); // 读取对话文本
逐字显示.当前行索引 = 0; // 初始化当前行索引为0
逐字显示.对话框计时器.Timeout += 计时器超时; // 绑定计时器超时事件
逐字显示.显示当前对话(); // 显示当前对话内容
}
// 计时器超时的方法
private async void 计时器超时()
{
await 逐字显示.计时器结束后回调(); // 调用逐字显示的计时器结束后回调
}
}
代码解释
通过 public static 逐字显示对话内容 逐字显示 = new 逐字显示对话内容();
声明一个静态字段,实例化 逐字显示对话内容
类。
开始对话
方法接收多个参数进行初始化
使用 UI管理器.创建UI("对话框", 根节点)
创建对话框并将其添加到指定的根节点
通过 逐字显示.对话框 = UI管理器.获取当前UI()
获取当前创建的对话框
分别获取角色名、对话内容和对话框计时器的引用
设置对话的完成的标记为 false
,通过 逐字显示.对话框.SelfModulate = new Color(1, 1, 1, 对话框透明度)
设置对话框的透明度
设置文本显示速度、是否自动显示下一行对话、对话结束后关闭对话框的选项等
准备好结束对话时的回调函数和相关参数
通过 对话系统基类.读取对话文本(文件全名)
读取对话文本,初始化当前行索引为 0
绑定计时器的超时事件到 计时器超时
方法,显示当前的对话内容
定义一个私有异步方法 计时器超时
,在计时器超时后调用 逐字显示
的回调方法。
至此,我们的GDS的对话系统的的脚本就完全编写完毕了
这里的C#代码是写在一个新的脚本中,该脚本引用了我们刚刚编写好的
对话系统工具集
命名空间,并继承自Node
节点以便作为单例存在至于为什么要作为单例存在,那是因为在Godot中想要响应按键事件,脚本就必须挂载在节点上,而将脚本设为单例其就可以作为一个全局的节点存在
连接按键事件
由于我们的C#脚本没有连接按键事件,所以就算在对话系统工具集
命名空间中定义了按键事件也无法响应
为了响应按键事件,我们需要将脚本挂载到节点上或设置为单例,所以我们才需要新建一个脚本
我们需要将连接按键事件函数写进我们新创建的脚本中
C#
using Godot;
using System;
using 对话系统工具集;
public partial class 显示对话文本 : Node
{
// 创建一个逐字显示对话内容的实例
public static 逐字显示对话内容 逐字显示 = new 逐字显示对话内容();
// 开始对话的方法,接受各种参数进行初始化
public void 开始对话(Node 根节点, string 文件全名, float 文字速度 = 0.05f, float 对话框透明度 = 1.0f, bool 自动显示 = false, bool 对话结束后关闭对话框 = true, Action<object[]> 对话结束后回调函数 = null, float 回调函数等待时间 = 3.0f, object[] 回调函数参数 = null)
{
UI管理器.创建UI("对话框", 根节点); // 创建对话框的UI
逐字显示.对话框 = UI管理器.获取当前UI(); // 获取当前创建的对话框
逐字显示.角色名 = 逐字显示.对话框.GetNode<Label>("Name"); // 获取角色名的Label
逐字显示.对话内容 = 逐字显示.对话框.GetNode<Label>("Dialogue"); // 获取对话内容的Label
逐字显示.对话框计时器 = 逐字显示.对话框.GetNode<Timer>("DialogueTimer"); // 获取计时器
逐字显示.对话已完成 = false; // 标记对话尚未完成
逐字显示.对话框.SelfModulate = new Color(1, 1, 1, 对话框透明度); // 设置对话框透明度
逐字显示.文本显示速度 = 文字速度; // 设置文本显示速度
逐字显示.自动显示 = 自动显示; // 设置是否自动显示下一行对话
逐字显示.对话结束后再次点击关闭对话框 = 对话结束后关闭对话框; // 设置对话结束后是否允许再次关闭对话框
逐字显示.对话结束后回调等待时间 = 回调函数等待时间; // 设置对话结束后回调函数的等待时间
逐字显示.对话结束后回调 = 对话结束后回调函数; // 设置对话结束后的回调函数
逐字显示.对话结束后回调函数参数 = 回调函数参数; // 设置回调函数的参数
对话系统基类.读取对话文本(文件全名); // 读取对话文本
逐字显示.当前行索引 = 0; // 初始化当前行索引为0
逐字显示.对话框计时器.Timeout += 计时器超时; // 绑定计时器超时事件
逐字显示.显示当前对话(); // 显示当前对话内容
}
// 计时器超时的方法
private async void 计时器超时()
{
await 逐字显示.计时器结束后回调(); // 调用逐字显示的计时器结束后回调
}
// 处理输入事件的方法
public override void _Input(InputEvent @event)
{
// 如果允许进入下一行对话且对话未完成
if (!逐字显示.对话已完成)
{
逐字显示.监听切换下一行对话(@event); // 监听切换下一行对话的输入
}
// 如果允许隐藏对话框且对话已激活,并且检测到隐藏对话框的输入
if (逐字显示.允许隐藏对话框 && 逐字显示.对话已激活 && @event.IsActionPressed("hidden_dialogue_box"))
{
逐字显示.监听隐藏对话框(); // 切换对话框的可见性
}
}
}
代码解释
这个新增的方法重写了 Godot 的 _Input
方法,用于处理输入事件
首先,检查对话是否已经完成。如果对话未完成,调用 逐字显示
的 监听切换下一行对话
方法,以响应输入事件(如按下按钮)来切换到下一行对话
接着,检查是否允许隐藏对话框,并且对话框已激活,且检测到用户的输入事件对应于隐藏对话框的操作。如果满足这些条件,调用 逐字显示
的 监听隐藏对话框
方法来切换对话框的可见性
结束
以上就是本篇教程的所有内容了,我们完成了对话系统的实现
我将会在下篇,也就是最后一篇教程中讲解,如何使用这个对话系统