主题
GameSystemUI 对话控件
GameSystemUI.ConversationSystemUI 提供官方对话 UI 实现,主要用于接入 对话系统 的 IConversationUI,也可以单独使用 BubbleUI 做单位头顶气泡提示。
它是对话系统的客户端表现层,不负责创建对话数据、推进剧情状态或执行任务奖励。剧情流程、选项跳转、条件和奖励仍应放在 GameUI.ConversationSystem / GameDataConversation* 或业务逻辑里。
常用命名空间:
csharp
#if CLIENT
using GameCore.BaseType;
using GameCore.EntitySystem;
using GameSystemUI.ConversationSystemUI.Advanced;
using GameSystemUI.ConversationSystemUI.Data;
using GameUI.ConversationSystem;
using GameUI.ConversationSystem.Data;
using GameUI.ConversationSystem.Data.Enum;
using GameUI.Struct;
#endif推荐公开入口
| 控件 | 作用 | 推荐用法 |
|---|---|---|
ConversationPanel | 默认对话面板,实现 IConversationUI | 注册到 ConversationUI,作为对话系统默认 UI |
BubbleUI | 跟随单位显示的气泡 UI | 用于 NPC 闲聊、战斗短提示、环境反馈 |
ConversationPanelBase | 对话 UI 抽象基类 | 需要完全自定义对话 UI 时继承 |
TypewriterEffectBehavior | 给 Label 添加打字机效果 | 只在自定义控件中直接使用 |
ConversationChoiceItem 通常作为选择项模板的内部控件使用,普通项目不需要直接 new。选择项外观优先通过 GameDataControlConversationChoiceItem 模板改。
注册默认对话面板
如果你使用框架对话系统,最常见的接法是在客户端创建一个 ConversationPanel,添加到 UI 根节点,然后注册为当前 IConversationUI:
csharp
#if CLIENT
private static ConversationPanel? conversationPanel;
private static void CreateConversationUI()
{
conversationPanel = new ConversationPanel
{
EnableTypewriter = true,
TypewriterSpeed = 0.5f,
BubbleDirection = BubbleDirection.Right,
BackgroundVisible = false,
};
conversationPanel.AddToRoot();
ConversationUI.Register(conversationPanel);
}
private static void CleanupConversationUI()
{
ConversationUI.Unregister();
conversationPanel?.RemoveFromParent();
conversationPanel = null;
}
#endif注册后,通过 GameDataConversation.StartConversation(...) 启动的对话会调用 ConversationUI.Current,再由 ConversationPanel 显示台词、选项和气泡。
csharp
// 客户端示意:通常由交互、任务或触发器逻辑启动。
CmdResult result = await conversationData.StartConversation();显示模式
ConversationPanel 支持两种显示模式,模式来自 ConversationLineInfo.DisplayMode 或对应的对话数据。
| 模式 | 效果 | 适合场景 |
|---|---|---|
ConversationDisplayMode.UI | 在屏幕对话框中显示角色名、立绘、文本和选项 | 剧情、任务、关键选择 |
ConversationDisplayMode.OnUnit | 在角色绑定单位附近显示 BubbleUI | NPC 闲聊、战斗提示、环境氛围 |
OnUnit 模式要求对话角色能解析到有效的 InGameUnit。如果没有有效单位,默认面板会退回到 UI 对话框模式。
台词完成规则
ConversationPanelBase 已处理 AllowSkip、WaitForConfirmation 和 Duration 的组合:
| 配置 | 行为 |
|---|---|
AllowSkip = true | 玩家可立即点击完成,跳过按钮可见 |
AllowSkip = false, WaitForConfirmation = false, Duration = 3s | 3 秒后自动完成 |
AllowSkip = false, WaitForConfirmation = true, Duration = 3s | 3 秒后才允许点击确认 |
AllowSkip = false, WaitForConfirmation = true, Duration = null | UI 会一直等待外部逻辑完成,不适合普通台词 |
打字机效果播放中点击会先补全文本;文本补全后,只有 CanFinish = true 才会结束当前台词。
单独使用 BubbleUI
如果你只是想在单位头顶显示一句短提示,可以不启动完整对话系统,直接创建 BubbleUI:
csharp
#if CLIENT
private static BubbleUI? npcBubble;
private static void ShowNpcHint(Unit npcUnit)
{
npcBubble ??= new BubbleUI
{
Direction = BubbleDirection.Up,
EnableTypewriter = true,
TypewriterSpeed = 0.8f,
};
npcBubble.BindUnit = npcUnit;
npcBubble.ShowText("前面有危险。");
npcBubble.Show();
}
private static void HideNpcHint()
{
npcBubble?.Hide();
}
#endifBubbleUI 会每帧把绑定单位的绑点位置转换到 UI 坐标,并带有 1 像素级别的防抖动。绑定单位失效或转换失败时,它会停止思考并隐藏。
BubbleUI 布局模板
BubbleUI 的不同方向由 GameDataControlBubbleUI.DirectionLayouts 配置。默认数据已经提供 Right、Left、Up、Down 四组布局;项目需要换图、改偏移或换绑点时,新建模板即可。
csharp
using GameData;
using GameSystemUI.ConversationSystemUI.Data;
using GameUI.Control.Data;
using GameUI.Struct;
public static class MyConversationControls
{
public static readonly GameLink<GameDataControl, GameDataControlBubbleUI> Bubble =
new("MyBubbleUI"u8);
public static readonly GameLink<GameDataBubbleUILayout, GameDataBubbleUILayout> BubbleUp =
new("MyBubbleUIUp"u8);
}
private static void OnGameDataInitialization()
{
_ = new GameDataBubbleUILayout(MyConversationControls.BubbleUp)
{
BubbleImage = new Image("image/conversation/bubble_up.png"),
UIOffset = new UIPosition(0, -20),
OffsetZ = -50,
CharsPerLine = 16,
TextMargin = new Thickness(24, 18, 24, 28),
SlicedEdges = new Thickness(24, 18, 24, 28),
SocketName = "socket_blood_bar",
};
_ = new GameDataControlBubbleUI(MyConversationControls.Bubble)
{
DefaultDirection = BubbleDirection.Up,
DirectionLayouts = new()
{
[BubbleDirection.Up] = MyConversationControls.BubbleUp,
},
FontSize = 30,
};
}
#if CLIENT
var bubble = new BubbleUI(MyConversationControls.Bubble)
{
BindUnit = npcUnit,
};
bubble.ShowText("自定义气泡。");
bubble.Show();
#endif图片路径遵循 GameUI 资源规则。项目图片通常放在 ui/image/...,代码里从 image/... 开始写;内置资源使用 @gameui/...。详见 项目资源路径指南。
自定义对话 UI
默认面板不满足需求时,继承 ConversationPanelBase。基类已经封装异步等待、取消、计时和 CanFinish 状态,你只需要实现具体显示和在玩家确认时调用受保护方法。
csharp
#if CLIENT
public sealed class MyConversationPanel : ConversationPanelBase
{
public override void ShowLine(ConversationLineInfo lineInfo, GameDataConversationLine? lineData)
{
// 显示角色名、立绘和文本。
}
public override void ShowChoices(
List<ConversationChoiceInfo> choices,
ConversationChoicePromptInfo? promptInfo,
GameDataConversationChoiceGroup? choiceGroupData)
{
// 显示选项,并在点击某项时调用 CompleteChoiceSelection(index)。
}
public override void HideLine() { }
public override void HideChoices() { }
public override void Clear() { }
public override void Hide() => Visible = false;
protected override void OnCanFinishChanged(bool canFinish)
{
// 更新“可继续”提示。
}
}
#endif不要在 UI 层直接修改对话状态或发放奖励。IConversationUI 的职责是显示和返回玩家选择,业务状态应由对话节点、任务系统或服务端逻辑处理。
清理
模式切换、退出关卡或重新创建 HUD 时,应注销当前对话 UI 并移除控件:
csharp
#if CLIENT
private static void CleanupConversationUI()
{
ConversationUI.Unregister();
conversationPanel?.RemoveFromParent();
conversationPanel = null;
npcBubble?.Destroy();
npcBubble = null;
}
#endif如果使用自定义 ConversationPanelBase,注意在 DisposeManaged() 中取消事件订阅、停止计时器或打字机效果,避免旧面板继续响应输入。
常见问题
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 启动对话但没有 UI | 没有 ConversationUI.Register(...),或面板没有加入 UI 根节点 | 客户端初始化时创建 ConversationPanel 并注册 |
OnUnit 没有显示气泡 | 角色没有绑定有效 InGameUnit,或单位不在屏幕内 | 检查对话角色数据和单位生命周期 |
| 台词一直不能点掉 | AllowSkip = false 且 WaitForConfirmation = true,但没有 Duration | 普通台词给出 Duration,或改为允许跳过 |
| 选择项显示但点击无效 | 选择项 IsEnabled = false | 用禁用状态表达条件未满足;可用时再设为 true |
| 气泡位置不合适 | 绑点、UIOffset 或 OffsetZ 不符合模型 | 用 GameDataBubbleUILayout.SocketName、UIOffset、OffsetZ 调整 |