跳转到内容

9. 编译与生成

A .sharq single-file component next to the plain C# class SharqFileImporter generates from it on save

流水线

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_fontdesign-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 放在一起)中声明要生成什么:

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.CompileDirectorysus-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 Componentssus.config.jsonSharqDirectorySharqFileImporter
在包内保存 .sharq(watcher)+ 域重载时的新鲜度检查 + 菜单所有带 sharq.gen.json 的包SusPackage*(见上文)
CLI手动运行 / CI不依赖 Unity 的 Assets/(只有 .g.csSharqBootstrap

包的使用方不能依赖运行时的 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.stylethis.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 的 StyleSheetImporterImplSusUssFromString)。在没有 Editor factory 的独立播放器上, USS 解析不可用;模板热重载仍然可以工作。
  • Editor 推送:RemoteHotReloadPushService(通过 EditorPrefs 的 Sharq.RemoteHotReload.Enabled = 0 可以禁用)。

模板(E3)

  • 只编辑 <template>SharqCompileEvents.OnTemplateChangedTemplateHotReloadService / SharqTemplateInterpreter.TryApply
  • 远程:ui.hotreload.template / client.hotreload_template

模板解释器支持矩阵

特性状态行为
$MainElement / 根属性 → thisBuildMethodGenerator 一致: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 状态被重置:

  1. Edit → Preferences → General → Script Changes While Playing = Recompile And Continue Playing(否则 Play 模式会停止)。没有这个设置, Unity 会在每次脚本变化时停止 Play 模式——下面的快照机制也就没有意义了。
  2. HotReloadStatePreserveService 会在重载之前为 UIDocumentEditorWindow 的 Sus 树拍摄 SusComponentSnapshot 快照,并在重载之后通过 delayCall 恢复它们。
  3. 基础类型加上 enum 类型的 Prop<T> 值会被序列化;其他类型会被忽略。
  4. Assets/sus.config.json 中的选项:"HotReloadStatePreserve": true(默认值)。 设为 false 可以关闭它。

"比赛中途绝不重新编译"这条规则的优先级仍然高于实时 PvP 会话——E2b 只是一个 Editor 层面的便利功能,不能替代这条规则。