14 · 自定义对话框模板
引擎内置三个模板(bottom/center/fullscreen),但你可以创建完全自定义的对话框。本章讲解如何实现。
架构回顾
灵泛引擎的对话框模板系统采用核心逻辑共享 + 容器控件自由的设计:
DialogEngine(纯逻辑) ← 共享:打字机/内联标记/NVL/标签
↑ 委托
模板控件(独立 Control) ← 自由:布局/外观/交互- DialogEngine——提取了打字机推进、内联标记解析、NVL 累积、
{w}/{p}/{fast}标签处理等纯逻辑 - 模板控件——实现
IDialogBox接口的独立UserControl,自定义容器结构,委托DialogEngine处理核心逻辑
IDialogBox 接口
csharp
public interface IDialogBox
{
bool IsComplete { get; } // 打字机是否已完成
bool IsPausedByTag { get; } // 是否被 {w}/{p} 暂停
void SetText(string text, string? speaker = null); // 设置文本
void Advance(double deltaSeconds); // 每帧推进打字机
void SkipToEnd(); // 跳到末尾
void Hide(); // 隐藏
void ResetNvlState(); // 重置 NVL 状态
Control AsControl(); // 获取 Avalonia 控件
}DialogEngine 核心
DialogEngine 提供:
| 方法 | 说明 |
|---|---|
SetText(string) | 设置文本(ADV),重置打字机 |
SetNvlText(string) → int | 设置 NVL 文本,返回旧文本长度 |
Advance(double) → string? | 推进打字机,返回 raw 文本 |
SkipToEnd() → string | 跳到末尾,返回完整文本 |
ResumeFromPause() | 解除 {w}/{p} 暂停 |
ResetNvlState() | 重置 NVL 状态 |
ApplyInlineMarkup(Inlines, raw) | 静态方法:解析内联标记 |
创建自定义模板
1. 实现 IDialogBox
csharp
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Media;
using LingFanEngine.Abstractions;
using LingFanEngine.Abstractions.Interfaces.Core;
using LingFanEngine.Views;
public class MyDialogBox : UserControl, IDialogBox
{
private readonly DialogEngine _engine;
private readonly TextBlock _contentText;
private readonly Border _root;
private readonly IStateContainer _state;
public bool IsComplete => _engine.IsComplete;
public bool IsPausedByTag => _engine.IsPausedByTag;
public MyDialogBox(IStateContainer state)
{
_state = state;
_engine = new DialogEngine(state);
// 完全自定义容器结构
_contentText = new TextBlock
{
FontSize = 20,
TextWrapping = TextWrapping.Wrap,
Foreground = Brushes.White,
Margin = new Thickness(20)
};
_root = new Border
{
Background = new SolidColorBrush(Color.FromArgb(180, 20, 20, 30)),
CornerRadius = new CornerRadius(8),
Child = _contentText,
IsVisible = false
};
Content = _root;
// 点击交互
_root.PointerPressed += (_, _) =>
{
if (!_root.IsVisible) return;
if (IsPausedByTag && !IsComplete) { _engine.ResumeFromPause(); return; }
if (!IsComplete) { SkipToEnd(); }
else
{
_state.Set(StateKeys.Dialog.Complete, true);
_state.Set(StateKeys.Dialog.WaitingSayComplete, true);
}
};
}
public void SetText(string text, string? speaker = null)
{
_engine.SetText(text);
DialogEngine.ApplyInlineMarkup(_contentText.Inlines!, "");
_root.IsVisible = true;
}
public void Advance(double deltaSeconds)
{
if (!_root.IsVisible || IsComplete) return;
var raw = _engine.Advance(deltaSeconds);
if (raw != null)
DialogEngine.ApplyInlineMarkup(_contentText.Inlines!, raw);
}
public void SkipToEnd()
{
var full = _engine.SkipToEnd();
DialogEngine.ApplyInlineMarkup(_contentText.Inlines!, full);
}
public void Hide() => _root.IsVisible = false;
public void ResetNvlState() => _engine.ResetNvlState();
public Control AsControl() => this;
}2. 实现工厂
csharp
public class MyDialogBoxFactory : IDialogBoxFactory
{
public IDialogBox Create(IStateContainer state) => new MyDialogBox(state);
}3. 注册模板
csharp
public static IServiceCollection AddDialogTemplates(this IServiceCollection services)
{
services.AddSingleton(sp =>
{
var registry = sp.GetRequiredService<IDialogTemplateRegistry>();
registry.Register("bottom", new DefaultDialogBoxFactory());
registry.Register("center", new CenterBubbleDialogBoxFactory());
registry.Register("fullscreen", new FullScreenNvlDialogBoxFactory());
registry.Register("my_style", new MyDialogBoxFactory()); // ← 你的模板
registry.SetDefault("bottom");
return registry;
});
return services;
}4. 在 DSL 中使用
dsl
say "这句用我的自定义模板。" speaker="旁白" template="my_style"点击交互模式
模板的 PointerPressed 处理三种情况:
csharp
_root.PointerPressed += (_, _) =>
{
if (!_root.IsVisible) return;
// 1. {w}/{p} 暂停后点击 → 继续打字机(不跳到末尾)
if (IsPausedByTag && !IsComplete)
{
_engine.ResumeFromPause();
return;
}
// 2. 打字机未完成 → 跳到末尾
if (!IsComplete)
{
SkipToEnd();
}
// 3. 已完成 → 标记完成,触发下一句
else
{
_state.Set(StateKeys.Dialog.Complete, true);
_state.Set(StateKeys.Dialog.WaitingSayComplete, true);
}
};ResumeFromPause vs SkipToEnd
{w}/{p}暂停后点击 →ResumeFromPause()(继续逐字)- 打字机进行中点击 →
SkipToEnd()(跳到末尾)
不要混淆!
NVL 支持
如果模板要支持 NVL 模式,在 SetText 中检测:
csharp
public void SetText(string text, string? speaker = null)
{
var nvlActive = _state.Get<bool>(StateKeys.Nvl.Active);
if (nvlActive)
{
var oldLen = _engine.SetNvlText(text);
if (oldLen > 0)
DialogEngine.ApplyInlineMarkup(_contentText.Inlines!, text[..oldLen]);
else
DialogEngine.ApplyInlineMarkup(_contentText.Inlines!, "");
}
else
{
_engine.SetText(text);
DialogEngine.ApplyInlineMarkup(_contentText.Inlines!, "");
}
_root.IsVisible = true;
}自定义自由度
模板控件的结构完全自由,不受预设参数限制:
| 可自定义 | 示例 |
|---|---|
| 容器结构 | Border/Grid/StackPanel 任意布局 |
| 容器位置 | 底部/中央/顶部/全屏 |
| 容器外观 | 背景色/圆角/边框/阴影/边距 |
| 侧脸图 | 位置(左/右/无)、宽度 |
| 说话者 | 位置(上/下/内联/隐藏)、字号 |
| 文本 | 字号、颜色、对齐、换行 |
| 点击交互 | 推进/跳过/完成的指针事件 |
| 背景图 | 背景图加载(可选) |
参考实现
引擎内置三个模板的源码可供参考:
| 模板 | 源码位置 |
|---|---|
bottom | src/EngineCore/LingFanEngine/Views/DialogBox.cs |
center | src/Demo/LingFanEngine.Entry/UI/Dialogs/CenterBubbleDialogBox.cs |
fullscreen | src/Demo/LingFanEngine.Entry/UI/Dialogs/FullScreenNvlDialogBox.cs |
动手练习
创建一个带说话者头像的右侧对话框:
csharp
public class RightSideDialogBox : UserControl, IDialogBox
{
private readonly DialogEngine _engine;
private readonly TextBlock _speakerText;
private readonly TextBlock _contentText;
private readonly Border _root;
private readonly IStateContainer _state;
public bool IsComplete => _engine.IsComplete;
public bool IsPausedByTag => _engine.IsPausedByTag;
public RightSideDialogBox(IStateContainer state)
{
_state = state;
_engine = new DialogEngine(state);
_speakerText = new TextBlock
{
FontSize = 16, FontWeight = FontWeight.Bold,
Foreground = new SolidColorBrush(Color.Parse("#FFD700")),
Margin = new Thickness(10, 5)
};
_contentText = new TextBlock
{
FontSize = 18, TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(10)
};
var stack = new StackPanel();
stack.Children.Add(_speakerText);
stack.Children.Add(_contentText);
_root = new Border
{
Background = new SolidColorBrush(Color.FromArgb(200, 0, 0, 0)),
CornerRadius = new CornerRadius(12, 0, 0, 12),
Child = stack,
IsVisible = false,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Bottom,
MaxWidth = 500
};
Content = _root;
_root.PointerPressed += (_, _) =>
{
if (!_root.IsVisible) return;
if (IsPausedByTag && !IsComplete) { _engine.ResumeFromPause(); return; }
if (!IsComplete) SkipToEnd();
else { _state.Set(StateKeys.Dialog.Complete, true); _state.Set(StateKeys.Dialog.WaitingSayComplete, true); }
};
}
public void SetText(string text, string? speaker = null)
{
_engine.SetText(text);
_speakerText.Text = speaker ?? "";
_speakerText.IsVisible = !string.IsNullOrEmpty(speaker);
DialogEngine.ApplyInlineMarkup(_contentText.Inlines!, "");
_root.IsVisible = true;
}
public void Advance(double deltaSeconds)
{
if (!_root.IsVisible || IsComplete) return;
var raw = _engine.Advance(deltaSeconds);
if (raw != null) DialogEngine.ApplyInlineMarkup(_contentText.Inlines!, raw);
}
public void SkipToEnd()
{
var full = _engine.SkipToEnd();
DialogEngine.ApplyInlineMarkup(_contentText.Inlines!, full);
}
public void Hide() => _root.IsVisible = false;
public void ResetNvlState() => _engine.ResetNvlState();
public Control AsControl() => this;
}下一步
自定义模板掌握了!下一章学习加密与发布——把游戏安全地发布出去。