常见问题解答
在使用 Mortar 的过程中,你可能会遇到一些疑问。这里整理了最常见的问题和解答。
基础概念
Mortar 和其他对话系统有什么区别?
最大的区别是“内容与逻辑分离“:
- 传统系统:
"你好<sound=hi.wav>,欢迎<color=red>光临</color>!" - Mortar:文本是纯文本,事件单独写,通过位置关联
这样做的好处:
- 写手可以专心写故事,不用管技术标记
- 程序员可以灵活控制事件,不会破坏文本
- 文本内容容易翻译和修改
为什么要用字符位置来触发事件?
字符位置让你能精确控制事件触发时机:
text: "轰隆隆!一道闪电划过天空。"
with events: [
0, shake_screen() // 在"轰"字时屏幕震动
3, flash_effect() // 在"!"时闪光效果
4, play_thunder() // 在"一"字时雷声
]
这对于:
- 打字机效果(逐字显示)
- 语音同步
- 音效配合
都特别有用!
我可以不用事件,只写对话吗?
当然可以! 事件是可选的:
node SimpleDialogue {
text: "你好!"
text: "欢迎来玩!"
choice: [
"谢谢" -> Thanks,
"拜拜" -> return
]
}
这样写完全合法,适合简单场景。
语法相关
分号和逗号必须写吗?
大部分情况下可以省略! Mortar 语法很宽松:
// 这三种写法都可以
text: "你好"
text: "你好",
text: "你好";
with events: [
0, sound_a()
1, sound_b()
]
with events: [
0, sound_a(),
1, sound_b(),
]
with events: [
0, sound_a();
1, sound_b();
]
但建议保持一致,选一种风格坚持下去。
字符串必须用双引号吗?
单引号和双引号都可以:
text: "双引号字符串"
text: '单引号字符串'
node 和 nd 有什么区别?
完全一样! nd 只是 node 的简写:
node OpeningScene { }
nd 开场 { } // 完全相同
类似的简写还有:
fn=functionBool=Boolean
怎么写注释?
用 // 写单行注释,用 /* */ 写多行注释:
// 这是单行注释
/*
这是
多行
注释
*/
node Example {
text: "对话内容" // 也可以写在行尾
}
节点与跳转
节点名字有什么要求?
技术上可以使用:
- 英文字母、数字、下划线
- 但不能以数字开头
但我们强烈推荐使用大驼峰命名法(PascalCase):
// ✅ 推荐的命名(大驼峰)
node OpeningScene { }
node ForestEntrance { }
node BossDialogue { }
node Chapter1Start { }
// ⚠️ 不推荐但能用
node opening_scene { } // 蛇形是函数的风格
node forest_1 { } // 可以,但不如 Forest1
// ❌ 不好的命名
node 开场 { } // 避免使用 非 ASCII 文本
node 1node { } // 不能以数字开头
node node-1 { } // 不能用短横线
为什么推荐大驼峰?
- 与主流编程语言的类型命名一致
- 清晰易读,便于识别
- 避免跨平台编码问题
- 团队协作更规范
可以跳转到不存在的节点吗?
不行! 编译器会检查所有的跳转:
node A {
choice: [
"去B" -> B, // ✅ B存在,可以
"去C" -> C // ❌ C不存在,报错
]
}
node B { }
怎么结束对话?
有三种方式:
- return - 结束当前节点(如果有后续节点,会继续)
- 没有后续跳转 - 对话自然结束
- 跳转到特殊节点 - 可以做一个专门的“结束“节点
// 方式1:使用return
node A {
choice: [
"结束" -> return
]
}
// 方式2:自然结束
node B {
text: "再见!"
// 没有跳转,对话结束
}
// 方式3:结束节点
node C {
choice: [
"结束" -> EndingNode
]
}
node EndingNode {
text: "谢谢游玩!"
}
选择系统
选项可以嵌套吗?
可以! 而且可以嵌套任意层:
choice: [
"吃什么?" -> [
"中餐" -> [
"米饭" -> End1,
"面条" -> End2
],
"西餐" -> [
"牛排" -> End3,
"意面" -> End4
]
]
]
when 条件怎么写?
有两种写法:
choice: [
// 链式写法
("选项A").when(has_key) -> A,
// 函数式写法
"选项B" when has_key -> B
]
条件函数必须返回 Bool 类型:
fn has_key() -> Bool
如果所有选项的条件都不满足怎么办?
这是游戏逻辑需要处理的问题。Mortar 只负责编译,不管运行时逻辑。
建议:
- 至少留一个没有条件的“默认选项“
- 在游戏里检查是否有可用选项
事件系统
事件的数字可以是小数吗?
可以! 小数特别适合语音同步:
text: "这段话配了语音。"
with events: [
0.0, start_voice()
1.5, highlight_word() // 1.5秒时
3.2, another_effect() // 3.2秒时
]
多个事件可以在同一个位置吗?
可以! 而且会按顺序执行:
with events: [
0, effect_a()
0, effect_b() // 同样在位置0
0, effect_c() // 也在位置0
]
游戏运行时会按顺序调用这三个函数。
事件函数必须声明吗?
是的! 所有用到的函数都要声明:
node A {
with events: [
0, my_function() // 使用了函数
]
}
// 必须声明
fn my_function()
不声明会编译报错。
函数相关
函数声明只是占位符吗?
是的! 函数的实际实现在你的游戏代码里:
// Mortar 文件里只需要声明
fn play_sound(file: String)
// 真正的实现在你的游戏代码(比如C#/C++/Rust等)
// 例如在Unity中:
// public void play_sound(string file) {
// AudioSource.PlayClipAtPoint(file);
// }
Mortar 只负责:
- 检查函数名是否正确
- 检查参数类型是否匹配
- 生成JSON让游戏知道该调用什么
支持哪些参数类型?
目前支持这些基本类型:
String- 字符串Bool/Boolean- 布尔值(真/假)Number- 数字(整数或小数)
fn example_func(
name: String,
age: Number,
is_active: Bool
) -> String
函数可以没有参数吗?
可以!
fn simple_function()
fn another() -> String
函数可以有多个参数吗?
可以! 用逗号分隔:
fn complex_function(
param1: String,
param2: Number,
param3: Bool
) -> Bool
函数名有什么命名规范?
强烈推荐使用蛇形命名法(snake_case):
// ✅ 推荐的命名(蛇形)
fn play_sound(file_name: String)
fn get_player_name() -> String
fn check_inventory() -> Bool
fn calculate_damage(base: Number, modifier: Number) -> Number
// ⚠️ 不推荐
fn playSound() { } // 驼峰是其他语言的风格
fn PlaySound() { } // 大驼峰是节点的风格
fn 播放声音() { } // 避免使用 非 ASCII 文本
参数名也要用蛇形命名:
fn load_scene(scene_name: String, fade_time: Number) // ✅
fn load_scene(SceneName: String, fadeTime: Number) // ❌
字符串插值
什么是字符串插值?
在字符串里嵌入变量或函数调用:
text: $"你好,{get_name()}!你有{get_score()}分。"
注意字符串前的 $ 符号!
插值必须是函数吗?
目前 Mortar 的插值主要用于函数调用。插值里的内容会被替换成函数的返回值。
不用 $ 会怎样?
没有 $ 就是普通字符串,{} 会被当作普通字符:
text: "你好,{name}!" // 就是显示 "你好,{name}!"
text: $"你好,{name}!" // name会被替换成实际值
编译与输出
编译后的文件是什么格式?
JSON 格式,默认是压缩的(没有空格和换行):
mortar hello.mortar # 生成压缩JSON
mortar hello.mortar --pretty # 生成格式化JSON
怎么指定输出文件名?
用 -o 参数:
mortar input.mortar -o output.json
不指定的话,默认是 input.mortared
JSON 结构是怎样的?
大致结构:
{
"nodes": {
""NodeName"": {
"texts": [...],
"events": [...],
"choices": [...]
}
},
"functions": [...]
}
详细结构看 JSON 输出说明
编译错误怎么看?
Mortar 的错误信息很友好,会指出:
- 错误位置(行号、列号)
- 错误原因
- 相关的代码片段
Error: Undefined node 'Unknown'
┌─ hello.mortar:5:20
│
5 │ choice: ["去" -> Unknown]
│ ^^^^^^^ 这个节点不存在
│
项目实践
多人协作怎么办?
建议:
- 使用 Git 管理 Mortar 文件
- 按功能模块划分文件,减少冲突
- 制定命名规范
- 写清楚注释
怎么和游戏引擎配合?
基本流程:
- 写好 Mortar 文件
- 编译成 JSON
- 在游戏里读取 JSON
- 实现对应的函数
- 按照 JSON 指示执行
详见 接入你的游戏
适合什么类型的游戏?
特别适合:
- RPG 对话系统
- 视觉小说
- 文字冒险游戏
- 互动故事
基本上任何需要“结构化对话“的游戏都适合!
可以用在非游戏项目吗?
当然! 任何需要结构化文本和事件的场景都可以:
- 教育软件
- 聊天机器人
- 交互式演示
- 多媒体展示
进阶话题
支持变量吗?
目前不支持内置变量系统,但你可以:
- 在游戏代码里维护变量
- 通过函数调用来读写变量
// Mortar 文件
fn get_player_hp() -> Number
fn set_player_hp(hp: Number)
// 游戏代码里实现这些函数
支持表达式吗?
目前不支持复杂表达式,但可以通过函数实现:
// 不支持:
choice: [
"选项" when hp > 50 && has_key -> Next
]
// 可以这样:
choice: [
"选项" when can_proceed() -> Next
]
fn can_proceed() -> Bool // 在游戏里实现逻辑
本功能即将支持。
怎么做本地化(多语言)?
本功能即将支持。
支持模块化吗?
目前每个 .mortar 文件是独立的,不能互相引用。
建议:
- 把相关对话写在同一个文件
- 或者在游戏里加载多个 JSON 文件并整合
本功能即将支持。
故障排查
编译时报“语法错误“怎么办?
- 仔细看错误信息指出的位置
- 检查是否漏了括号、引号
- 检查关键字拼写是否正确
- 确保“NodeName“、函数名有效
“未定义的节点“错误?
检查:
- 跳转目标的节点是否存在
- “NodeName“大小写是否一致(区分大小写!)
- 是否有拼写错误
“类型不匹配“错误?
检查:
- 函数声明的参数类型
- 调用时传入的参数是否匹配
- 返回类型是否正确
生成的 JSON 游戏读取不了?
- 确保JSON格式正确(用
--pretty检查) - 检查游戏代码的解析逻辑
- 查看是否有编码问题(使用UTF-8)
还有问题?
- 📖 查看 示例代码
- 💬 到 GitHub Discussions 提问
- 🐛 在 GitHub Issues 报告 bug
- 📚 阅读 参考资料
我们很乐意帮助你!🎉