主题
对话系统
对话系统管理游戏中的 NPC 对话、剧情演出和玩家选择。基于树形节点结构,支持顺序对话、随机对话、分支选择、自定义动作和等待。对话运行在客户端,服务端仅接收选择通知。
核心类型
| 类型 | 说明 |
|---|---|
GameDataConversation | 对话数编基类(抽象),所有对话节点的公共接口 |
GameDataConversationLine | 单行对话数编(文本、语音、动画) |
GameDataConversationSet | 对话序列/随机选择数编 |
GameDataConversationChoiceGroup | 玩家选择组数编 |
GameDataConversationChoiceItem | 单个选项数编 |
GameDataConversationWait | 等待节点数编 |
GameDataConversationCustomAction | 自定义逻辑节点数编 |
Conversation | 对话运行时基类(仅客户端) |
IConversationUI | 对话 UI 接口,游戏代码实现此接口提供视觉表现 |
ConversationParamShared | 对话共享状态和历史记录 |
节点继承关系
GameDataConversation (abstract)
├── GameDataConversationLine 单行对话
├── GameDataConversationSet 序列/随机
├── GameDataConversationChoiceGroup 选择分支
├── GameDataConversationWait 等待
└── GameDataConversationCustomAction 自定义逻辑
Conversation (abstract, CLIENT only)
├── ConversationLine
├── ConversationSet
├── ConversationChoiceGroup
├── ConversationWait
└── ConversationCustomAction数编配置
GameDataConversationLine — 单行对话
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
Character | IGameLink<GameDataCharacter>? | null | 说话角色(null 表示旁白) |
Text | LocalizedString? | null | 本地化文本,支持 {0} 占位符 |
TextParameters | List<Func<IExecutionContext, object>>? | null | 动态文本参数 |
Voice | IGameLink<GameDataActorSound>? | null | 语音音效 |
SpeakerAnimation | IGameLink<GameDataAnimation>? | null | 说话时的角色动画 |
Camera | IGameLink<GameDataCamera>? | null | 切换相机 |
CameraFollowCharacter | bool | false | 相机跟随角色单位 |
Duration | TimeSpan? | null | 显示时长(null 自动估算) |
WaitForConfirmation | bool | false | 需要玩家点击确认 |
AllowSkip | bool | false | 允许跳过 |
PortraitSide | PortraitSide | Left | 头像显示位置 |
DisplayMode | ConversationDisplayMode | UI | 显示方式 |
Validators 属性(继承自基类)可以设置条件验证。
GameDataConversationSet — 对话序列
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
Lines | List<IGameLink<GameDataConversation>?>? | null | 子节点列表(支持任意对话类型嵌套) |
ExecutionMode | ConversationExecutionMode? | Sequential | 执行模式 |
LineDisplayMode | ConversationDisplayMode? | null | 覆盖所有子节点的显示模式 |
执行模式枚举:
| 值 | 说明 |
|---|---|
Sequential | 按顺序执行所有子节点 |
RandomOne | 从通过验证的子节点中随机选一个执行 |
GameDataConversationChoiceGroup — 选择组
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
Choices | List<IGameLink<GameDataConversationChoiceItem>?> | [] | 选项列表 |
Timeout | TimeSpan? | null | 选择时间限制(游戏时间) |
DefaultChoiceOnTimeout | int? | null | 超时自动选择的选项索引 |
PromptText | LocalizedString? | null | 选项上方的提示文本 |
PromptCharacter | IGameLink<GameDataCharacter>? | null | 提示说话角色 |
PromptVoice | IGameLink<GameDataActorSound>? | null | 提示语音 |
PromptSpeakerAnimation | IGameLink<GameDataAnimation>? | null | 提示角色动画 |
Camera | IGameLink<GameDataCamera>? | null | 切换相机 |
DisplayMode | ConversationDisplayMode | UI | 显示方式 |
GameDataConversationChoiceItem — 单个选项
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
Text | LocalizedString? | null | 选项显示文本 |
Validators | Validator? | null | 选项启用条件 |
NextConversation | IGameLink<GameDataConversation>? | null | 选择后的后续对话 |
ServerEffect | IGameLink<GameDataEffect>? | null | 选择后在服务端执行的效果 |
NotifyServer | bool | true | 是否通知服务端 |
Icon | Icon? | null | 选项图标 |
> ServerEffect 是在服务端执行选择后逻辑的推荐方式。也可以在服务端订阅 EventPlayerConversationChoiceSelected 事件。
GameDataConversationWait — 等待
| 属性 | 类型 | 说明 |
|---|---|---|
Duration | TimeSpan | 等待时长(游戏时间,受暂停和时间缩放影响) |
GameDataConversationCustomAction — 自定义逻辑
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
Func | Func<IExecutionContext, ValueTask<bool>> | 返回 true | 自定义异步函数,返回 true 继续、false 中止 |
NextConversation | IGameLink<GameDataConversation>? | null | Func 返回 true 时的后续对话 |
显示模式枚举
| 值 | 说明 |
|---|---|
ConversationDisplayMode.UI | 专用对话 UI 面板(默认) |
ConversationDisplayMode.OnUnit | 单位头顶气泡(需要角色关联游戏内单位) |
运行时执行
启动对话
csharp
#if CLIENT
// 方式一:扩展方法(推荐)
await myConversationLink.Data!.StartConversation();
// 方式二:手动创建
var conversation = myConversationLink.Data!.CreateConversation(context);
if (conversation != null)
{
await conversation.StartAsync();
}
#endif执行流程
StartAsync()
↓
验证 Validators
↓ (根节点) 发布 EventConversationStarted
↓
Execute() ← 各子类实现
↓
├── ConversationLine: 准备文本/角色/语音 → ui.ShowLineAsync()
├── ConversationSet: 遍历/随机选择子节点 → 递归执行
├── ConversationChoiceGroup: 构建选项 → ui.ShowChoicesAsync() → 执行后续对话
├── ConversationWait: Game.Delay(duration)
└── ConversationCustomAction: 执行 Func → 条件继续
↓
↓ (根节点) 恢复默认相机
↓ (根节点) 发布 EventConversationCompleted共享状态(ConversationParamShared)
根节点创建共享状态,所有子节点继承同一实例,提供对话历史:
csharp
#if CLIENT
// 在 IConversationUI 实现中可以访问历史
// conversation.Shared.ConversationHistory ← 所有已执行的对话和选择
// 历史记录类型
// ConversationLineHistoryEntry — 对话行(含 LineInfo)
// ConversationChoiceHistoryEntry — 选择(含所有选项和玩家选择索引)
// 便捷查询
var lines = conversation.Shared.ConversationHistory.GetLines();
var choices = conversation.Shared.ConversationHistory.GetChoices();
var characterLines = conversation.Shared.ConversationHistory.GetLinesByCharacter("角色名");
#endif自动时长估算
当 Duration 为 null 时,ConversationDurationEstimator 根据文本内容自动计算:
- 基础时间:1000ms
- 每字符:50ms(约 180-240 字/分钟的中文阅读速度)
- 标点停顿:句末 400ms,句中 200-300ms,省略号 500ms
- 范围:1500ms ~ 15000ms
IConversationUI 接口
游戏代码必须实现此接口并注册,对话系统才能显示内容:
csharp
#if CLIENT
public class MyDialogUI : IConversationUI
{
public async Task ShowLineAsync(ConversationLineInfo lineInfo, ConversationLine? line, CancellationToken ct)
{
// 显示对话文本、角色名、头像
// lineInfo.Text — 文本内容
// lineInfo.CharacterName — 角色名(null 为旁白)
// lineInfo.Portrait — 头像纹理
// lineInfo.DisplayMode — UI 面板或头顶气泡
// lineInfo.Duration — 显示时长(null 表示等待确认)
// lineInfo.WaitForConfirmation — 是否需要点击确认
if (lineInfo.Duration.HasValue)
{
await Game.Delay(lineInfo.Duration.Value);
}
else
{
// 等待玩家点击确认
var tcs = new TaskCompletionSource();
// 设置确认按钮回调...
await tcs.Task;
}
}
public async Task<int> ShowChoicesAsync(
List<ConversationChoiceInfo> choices,
ConversationChoicePromptInfo? promptInfo,
ConversationChoiceGroup? choiceGroup,
CancellationToken ct)
{
// 显示选项列表
// choices[i].Text — 选项文本
// choices[i].IsEnabled — 是否可选
// promptInfo?.PromptText — 提示文本
// 返回玩家选择的索引(0-based)
var tcs = new TaskCompletionSource<int>();
// 设置选项按钮回调,点击时 tcs.SetResult(index)...
return await tcs.Task;
}
public void Hide() { /* 隐藏对话 UI */ }
public void Clear() { /* 清空内容 */ }
}
// 注册
ConversationUI.Register(new MyDialogUI());
#endif事件
客户端事件
| 事件 | 说明 | 属性 |
|---|---|---|
EventConversationStarted | 根对话开始 | Conversation |
EventConversationCompleted | 根对话结束 | Conversation |
EventConversationChoiceSelected | 玩家选择了一个选项 | Conversation, ChoiceIndex, ChoiceLink |
服务端事件
| 事件 | 说明 | 属性 |
|---|---|---|
EventPlayerConversationChoiceSelected | 收到玩家的选择通知 | Player, ChoiceLink |
仅当选项的 NotifyServer = true 或配置了 ServerEffect 时才会发送到服务端。
csharp
#if SERVER
Trigger<EventPlayerConversationChoiceSelected>.Subscribe(Game.Instance, async (sender, e) =>
{
Game.Logger.LogInformation("玩家 {Player} 选择了: {Choice}",
e.Player, e.ChoiceLink.FriendlyName);
return true;
});
#endif完整示例
简单线性对话
通过数编配置一个 ConversationSet,包含多个 ConversationLine,然后启动:
csharp
#if CLIENT
// myDialogLink 指向一个 GameDataConversationSet
await myDialogLink.Data!.StartConversation();
#endif带选择的对话
ConversationSet (Sequential)
├── ConversationLine "欢迎来到村庄!"
├── ConversationChoiceGroup
│ ├── ChoiceItem "接受任务" → NextConversation: 任务对话
│ ├── ChoiceItem "询问消息" → NextConversation: 消息对话
│ └── ChoiceItem "离开" → (无后续)
└── ConversationLine "再见!"服务端响应选择
csharp
#if SERVER
// 方式一:在选项上配置 ServerEffect(推荐)
// 在数编中设置 ChoiceItem.ServerEffect = 某个效果
// 方式二:订阅事件
Trigger<EventPlayerConversationChoiceSelected>.Subscribe(Game.Instance, async (sender, e) =>
{
if (e.ChoiceLink.Equals(myAcceptQuestChoiceLink))
{
// 处理接受任务的逻辑
}
return true;
});
#endif