9. 编译与生成

流水线
Saving .sharq to Assets/
│
▼ AssetPostprocessor.OnPostprocessAllAssets
├── SharqFileImporter.ProcessSharq()
│ ├── SharqFileParser → SharqFileModel (template, script, style)
│ ├── SharqSectionCache → incremental regeneration
│ ├── SharqValidator → diagnostics (warnings)
│ ├── BuildMethodGenerator → .g.cs
│ ├── ScopedCssGenerator → _scoped.g.uss
│ └── StyleParser → .g.uss (global)
│
▼ Result:
Assets/SusUI/Generated/ComponentName.g.cs
Assets/SusUI/Generated/ComponentName_scoped.g.uss
Assets/SusUI/Generated/ComponentName.g.uss (global)增量编译
- 内容哈希(Content-hash):如果
.sharq没有变化——跳过 - 段级(Section-level):如果只有
<style>变化——只重新生成.uss(无需 C# 域重载即可热重载) - SharqAutoRegenerate:在域重载时检查新鲜度
SharqValidator —— 诊断
| 检查项 | 级别 |
|---|---|
v-for 缺少 :key(当 StrictVForKey: true 时) | Warning |
<script> 中未使用的字段 | Info |
生成的文件
Assets/SusUI/
├── Component.sharq ← source (you can edit here, in any subfolder)
├── Resources/SusRuntime/ ← author's runtime resources (in git): Icons/, Fonts/, theme-overrides
└── Generated/ ← auto-generated (.gitignored)
├── Component.g.cs
├── Component_scoped.g.uss
├── Component.g.uss
├── Component_static.g.uss
├── Component.sections.json
├── Component.sharq.hash
├── .gitignore
└── Resources/SusRuntime/ ← synced USS components for runtime (auto)基础主题/图标/字体(
_theme、_icon、_font、design-tokens)随包本身一起发布:sus-core/Runtime/Resources/SusRuntime/。SusRuntime是一个虚拟的 Resources 命名空间 (Resources.Load("SusRuntime/..")),并不是"编译器输出";Unity 会把每个Resources/SusRuntime/文件夹(包内和项目内)合并到同一个路径下。
重要: 始终编辑 .sharq,绝不要编辑 Generated/*——生成的文件会被覆盖。
清理: 删除一个
.sharq文件会自动删除全部 6 个生成文件、它的.meta,以及任何 Resources 副本。重命名会删除旧的产物并创建新的。
运行时加载 USS
组件在构建版本中通过 Resources/SusRuntime/ 加载样式。Editor 有一个回退机制——如果在 Resources 中找不到文件,会通过 AssetDatabase 直接从 Generated/ 加载。
批量流水线(面向库包)
除了项目范围的导入器(Assets/SusUI)之外,还有一条声明式的批量流水线—— 供那些直接在包内随包发布已生成的 .cs/.uss 的库使用(可选的下游 UI 包)。
包会在描述文件 sharq.gen.json(与 package.json 放在一起)中声明要生成什么:
{
"displayName": "My UI Package",
"sources": ["Components"],
"generated": "Runtime/Generated",
"resources": "Runtime/Resources/SusRuntime",
"watch": true,
"namespace": "MyCompany.UI"
}可选的 namespace:一个用点分隔的 C# 标识符,会包裹该包每一个生成的 .g.cs 类型。留空 / 省略 → 全局命名空间(legacy)。下游 UI 包应该把它设为自己的包根命名空间, 这样短类型名就不会在多个包之间冲突。
可选的 usings:一个字符串数组,列出额外的 C# 命名空间,会作为 using 指令写进 每一个生成的 .g.cs 文件(当从另一个 UI 包组合类型时用得上)。
基础设施位于 sus-core/Editor/Packaging/:
SusPackageRegistry—— 为所有已解析的包查找描述文件;只考虑可变包 (file:链接 / 内嵌包)——registry / git 包已经随包附带了现成的产物。SusPackageGenerator—— 菜单Sharq/Generate All Packages(一次性生成所有包) 和窗口Sharq/Generate Package…(逐个生成)。底层原理:SharqBatchCompiler.CompileDirectory(sus-core/Editor/AssetPipeline/),使用与导入器相同的BuildMethodGenerator。SusPackageAutoCompile—— 为每个带watch: true的包的源目录建立一个 FileSystemWatcher(Assets/之外的包对 AssetPostprocessor 不可见):保存.sharq→ 自动重新生成。在 Play 模式下: 仅样式 / 仅模板的变化会使用SharqBatchMode.HotReloadSafe(USS + 解释器,不需要.g.cs/ 域重载);<script>的变化会推迟到退出 Play 模式后, 再执行一次完整的 Generate。在 Edit 模式下,始终是完整的包级 Generate。- 结果会提交到包里(不同于会被
.gitignore的项目范围Generated/)。
三条生成流水线
| 流水线 | 触发方式 | 范围 | 执行者 |
|---|---|---|---|
| 项目 | 在 Assets/ 中保存 .sharq(AssetPostprocessor)+ Window → SUS → Sharq → Regenerate All Prototype Components | sus.config.json → SharqDirectory | SharqFileImporter |
| 包 | 在包内保存 .sharq(watcher)+ 域重载时的新鲜度检查 + 菜单 | 所有带 sharq.gen.json 的包 | SusPackage*(见上文) |
| CLI | 手动运行 / CI | 不依赖 Unity 的 Assets/(只有 .g.cs) | SharqBootstrap |
包的使用方不能依赖运行时的 Roslyn 源生成器——所以
.cs会被提前生成,并像普通脚本 一样随包发布。
生成器如何把作者写的 .sharq 翻译成 C#
BuildMethodGenerator 把 DSL 语法转换成合法的 C#:
在 .sharq 中 | 在 .g.cs 中 |
|---|---|
[CreateProperty(default:0)] public Prop<int> Damage = new(0); | [CreateProperty](DSL 参数 default:/validate: 会被丢弃)加上支撑字段 public Prop<int> Damage = new(0);,以及配套属性 public int damage { get => Damage.Value; set => Damage.Value = value; } |
存在 [CreateProperty] | 自动添加 using Unity.Properties; |
表达式中 Vue 风格的引号:v-if="Mode.Value != 'delete'",!= '' | C# 引号:Mode.Value != "delete",!= "" |
$using System.Linq; | using System.Linq; |
using System; 和 using System.Collections.Generic; 总是会被添加。
<script> 块的约定
SusComponent : VisualElement—— 根就是组件本身:使用this.Q<…>()、this.style、this.schedule(而不是Element.…)。- 基础生命周期钩子:
Created()、BeforeMounted()、Mounted()、Updated()、BeforeUnmounted()、Unmounted()(都是protected override void)。没有Init()方法——元素初始化(this.Q<…>()、Watch(...))应该放在Created()里。
热重载(Editor + DEVELOPMENT_BUILD)
USS(E1 / E4)
- 只编辑
<style>→ 重新生成.g.uss,无需域重载 →UssHotReloadService在存活的组件上重新加载配套样式表(Editor Play 中 <1 秒)。 - 远程(E4):
SusRuntimeHotReload.ApplyUss+ Runtime MCP 的ui.hotreload.uss/ Session MCP 的client.hotreload_uss。从文本生成 StyleSheet 走 Editor 的StyleSheetImporterImpl(SusUssFromString)。在没有 Editor factory 的独立播放器上, USS 解析不可用;模板热重载仍然可以工作。 - Editor 推送:
RemoteHotReloadPushService(通过 EditorPrefs 的Sharq.RemoteHotReload.Enabled= 0 可以禁用)。
模板(E3)
- 只编辑
<template>→SharqCompileEvents.OnTemplateChanged→TemplateHotReloadService/SharqTemplateInterpreter.TryApply。 - 远程:
ui.hotreload.template/client.hotreload_template。
模板解释器支持矩阵
| 特性 | 状态 | 行为 |
|---|---|---|
$MainElement / 根属性 → this | ✅ | 与 BuildMethodGenerator 一致:class / :class / name 应用到宿主组件,子元素放在内部 |
class、:class(对象)、name、:text | ✅ | |
v-if / v-show(字面量、!、==/!=、||/&&、Prop.Value) | ✅ | 复杂 C#(string.IsNullOrEmpty、方法调用)→ 回退 |
Prop="lit" / :Prop="expr" | ✅ | 简单字符串/数字/布尔值/枚举 |
<slot> / 命名插槽 | ✅ | 通过 GetSlotContainer / BuildSlot |
@click / @* 事件 | ⚠️ 跳过 | 打印日志 [SharqInterp] Skipping unsupported event…;应用树时不带事件处理器 |
v-if 上的 transition= | ⚠️ 忽略 | 该属性会被跳过;动画只有在完整重新编译后才会出现 |
v-for | ❌ 回退 | TryApply → false,需要域重载 / 完整 Build |
| 任意嵌套的 C# 表达式 | ❌ 回退 |
经验法则:简单的下游 UI 模板(Divider / Badge / 简化版 Alert)能得到 TryApply == true;任何明显复杂的东西(v-for、复杂表达式)都会可预期地 带着警告回退。
保留状态的域重载(E2b)
编辑 <script> 时,Unity 会执行域重载。为了防止 UI 状态被重置:
- Edit → Preferences → General → Script Changes While Playing = Recompile And Continue Playing(否则 Play 模式会停止)。没有这个设置, Unity 会在每次脚本变化时停止 Play 模式——下面的快照机制也就没有意义了。
HotReloadStatePreserveService会在重载之前为 UIDocument 和 EditorWindow 的 Sus 树拍摄SusComponentSnapshot快照,并在重载之后通过delayCall恢复它们。- 基础类型加上 enum 类型的
Prop<T>值会被序列化;其他类型会被忽略。 Assets/sus.config.json中的选项:"HotReloadStatePreserve": true(默认值)。 设为false可以关闭它。
"比赛中途绝不重新编译"这条规则的优先级仍然高于实时 PvP 会话——E2b 只是一个 Editor 层面的便利功能,不能替代这条规则。