Skip to content

对话系统 ​

对话系统管理游戏中的 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 — 单行对话 ​

属性类型默认值说明
CharacterIGameLink<GameDataCharacter>?null说话角色(null 表示旁白)
TextLocalizedString?null本地化文本,支持 {0} 占位符
TextParametersList<Func<IExecutionContext, object>>?null动态文本参数
VoiceIGameLink<GameDataActorSound>?null语音音效
SpeakerAnimationIGameLink<GameDataAnimation>?null说话时的角色动画
CameraIGameLink<GameDataCamera>?null切换相机
CameraFollowCharacterboolfalse相机跟随角色单位
DurationTimeSpan?null显示时长(null 自动估算)
WaitForConfirmationboolfalse需要玩家点击确认
AllowSkipboolfalse允许跳过
PortraitSidePortraitSideLeft头像显示位置
DisplayModeConversationDisplayModeUI显示方式

Validators 属性(继承自基类)可以设置条件验证。

GameDataConversationSet — 对话序列 ​

属性类型默认值说明
LinesList<IGameLink<GameDataConversation>?>?null子节点列表(支持任意对话类型嵌套)
ExecutionModeConversationExecutionMode?Sequential执行模式
LineDisplayModeConversationDisplayMode?null覆盖所有子节点的显示模式

执行模式枚举:

值说明
Sequential按顺序执行所有子节点
RandomOne从通过验证的子节点中随机选一个执行

GameDataConversationChoiceGroup — 选择组 ​

属性类型默认值说明
ChoicesList<IGameLink<GameDataConversationChoiceItem>?>[]选项列表
TimeoutTimeSpan?null选择时间限制(游戏时间)
DefaultChoiceOnTimeoutint?null超时自动选择的选项索引
PromptTextLocalizedString?null选项上方的提示文本
PromptCharacterIGameLink<GameDataCharacter>?null提示说话角色
PromptVoiceIGameLink<GameDataActorSound>?null提示语音
PromptSpeakerAnimationIGameLink<GameDataAnimation>?null提示角色动画
CameraIGameLink<GameDataCamera>?null切换相机
DisplayModeConversationDisplayModeUI显示方式

GameDataConversationChoiceItem — 单个选项 ​

属性类型默认值说明
TextLocalizedString?null选项显示文本
ValidatorsValidator?null选项启用条件
NextConversationIGameLink<GameDataConversation>?null选择后的后续对话
ServerEffectIGameLink<GameDataEffect>?null选择后在服务端执行的效果
NotifyServerbooltrue是否通知服务端
IconIcon?null选项图标

> ServerEffect 是在服务端执行选择后逻辑的推荐方式。也可以在服务端订阅 EventPlayerConversationChoiceSelected 事件。

GameDataConversationWait — 等待 ​

属性类型说明
DurationTimeSpan等待时长(游戏时间,受暂停和时间缩放影响)

GameDataConversationCustomAction — 自定义逻辑 ​

属性类型默认值说明
FuncFunc<IExecutionContext, ValueTask<bool>>返回 true自定义异步函数,返回 true 继续、false 中止
NextConversationIGameLink<GameDataConversation>?nullFunc 返回 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

相关文档 ​