11. API 参考
公共类型命名
Sus* 是产品 API(在 using Sharq.Core 之后调用的 Runtime 与面向买家的 Editor 类型)。 Sharq* 是 .sharq 编译器与流水线(解析器、解释器、导入器)。产品接口使用 ISus*。无前缀的公共类型是封闭的祖父级集合(响应式原语、host/layer 名词,以及这些 API 的配套类型)。不要新增无前缀公共类型;新产品类型用 Sus* / ISus*,新编译器类型用 Sharq*。
SusApp — fluent bootstrap
官方文档化的应用入口点。它是 SusBootstrap 之上的一个轻量 fluent 构建器, 保证初始化顺序:panel TSS → EventSystem → 令牌级联 → OverlayHost → world panel → 字体 → custom styles → configure → mount → 主题(最后)。
public sealed class SusApp
{
public VisualElement Root { get; }
public UIDocument Document { get; }
public SusWorldSpacePanel WorldPanel { get; } // after Run/Mount; null if UseWorldSpace(false)
// Create
public static SusApp Create(UIDocument document); // ApplyDefaultTSS + root
public static SusApp Create(VisualElement root); // advanced — no TSS
// Fluent config (all return this)
public SusApp UseTheme(SusTheme theme); // default Dark; applied last
public SusApp UseTokenCascade(bool enabled = true); // L1–L5 + OverlayHost (default on)
public SusApp UseWorldSpace(bool enabled = true); // SusWorldSpacePanel + Default (default on)
public SusApp UseCustomStyles(params string[] resourcePaths); // after cascade
public SusApp UseFonts(SusFontAsset fontAsset); // writes no style; warns unless exported to USS
public SusApp UseIcons(params ISusIconProvider[] providers);
public SusApp UseIcons(SusIconSetAsset iconSet);
public SusApp UseLogLevel(SusLogLevel level); // process gate; call before Run/Mount
public SusApp Configure(Action<VisualElement> configure); // before theme; router/manual UI
// Finalize
public VisualElement Run(); // no root component
public T Mount<T>() where T : SusComponent, new(); // Mount + Finalize
}Finalize 顺序(Run / Mount)
- 图标(
UseIcons→SusIconRegistry.RegisterProvider) - 令牌级联(
LoadTokenCascade:_palette→_font→_theme→design-tokens→_icon→ 扩展项 + OverlayHost) - World-space 面板(
EnsureWorldSpacePanel,仅当处于 Play Mode 且启用了UseWorldSpace时) - 字体(
UseFonts— 不影响样式;字体取自_font.uss,见 Design tokens §2) - Custom styles(在 root 与 OverlayHost 上的
UseCustomStyles) - Configure 回调
- 挂载根组件(如果调用了
Mount<T>) - 主题最后应用(
SusThemeService.Instance.SetTheme(root, theme))
UseLogLevel 不属于 finalize — 它会立即设置 SusLog.Level(在 Run / Mount 之前调用是安全的)。
SusApp.Create(uiDocument)
.UseTheme(SusTheme.Dark)
.UseLogLevel(SusLogLevel.Verbose) // optional — diagnostics / audits
.UseCustomStyles("SusRuntime/demo-tokens")
.UseWorldSpace(true)
.Configure(root => BuildManualUi(root))
.Mount<HomeScreen>();
// Router apps (sus-router extension):
SusApp.Create(uiDocument)
.UseTheme(SusTheme.Dark)
.UseRouter(router, r => r.Register("/", typeof(HomeScreen)), initialPath: "/")
.Run();SusLog
Sharq.Core 中的进程级 gated logger。代理到 UnityEngine.Debug,以便 Editor Console 与游戏内 开发控制台 继续接收消息。不是 SusConsoleService 使用的环形缓冲 SusLogEntry 类型 — 该 struct 只存储被拦截的 Unity 日志,供叠加 UI 使用。
public enum SusLogLevel
{
Error = 0,
Warn = 1, // default buyer level
Info = 2,
Verbose = 3, // audits, probes, bootstrap traces
}
public static class SusLog
{
public static SusLogLevel Level { get; set; } // minimum emitted; default Warn
public static bool IsEnabled(SusLogLevel level);
public static bool IsVerbose { get; } // IsEnabled(Verbose)
public static void Error(string message);
public static void Error(string message, Object context);
public static void Warn(string message);
public static void Warn(string message, Object context);
public static void Info(string message);
public static void Verbose(string message);
public static void Diagnostic(string message); // same gate as Verbose
}默认: Warn(Error + 关键 Warn)。诊断在提升级别之前保持静默。
如何启用 Verbose
| 来源 | 效果 |
|---|---|
SusApp.UseLogLevel(SusLogLevel.Verbose) / SusLog.Level = … | 代码覆盖;在 Run / Mount 之前调用 |
Assets/sus.config.json → "logLevel": "Verbose" | 首次访问 SusLog 时读取(见 10-configuration) |
脚本定义 SUS_VERBOSE_LOGS | 初始化时把下限抬到 Verbose;配置与 UseLogLevel 不能再降低 |
优先级:define 下限 → 最后一次 UseLogLevel / SusLog.Level → 配置中的 logLevel → 默认 Warn。
对昂贵的转储先做门控:if (SusLog.IsVerbose) SusLog.Verbose(...)。
SusComponent — 基类
public abstract partial class SusComponent : VisualElement
{
// ─── Creating reactive properties ───
protected Prop<T> P<T>(T initial = default);
protected Computed<T> C<T>(Func<T> fn);
protected WatchHandle Watch<T>(Prop<T> source, Action<T, T> callback);
protected WatchHandle WatchEffect(Action fn);
// ─── Life cycle ───
protected virtual void Created(); // constructor
protected virtual void BeforeMounted(); // between Created() and Build()
protected virtual void Mounted(); // ONCE per instance, after Build() - deferred
protected virtual void Attached(); // EVERY attach to a panel (incl. the first)
protected virtual void Updated(); // every frame
protected virtual void BeforeUnmounted(); // BEFORE removing from the panel
protected virtual void Detached(); // EVERY detach, right before Unmounted()
protected virtual void Unmounted(); // AFTER detach, on EVERY detach
// ─── Generation ───
protected abstract void Build(); // generated by the compiler
// ─── Provide / Inject ───
// overwrite:false (default) fires OnDuplicateProvide if the key already exists, then still writes
protected void Provide<T>(string key, T value, bool overwrite = false);
protected T Inject<T>(string key);
protected bool TryInject<T>(string key, out T value);
protected bool HasInjection(string key);
// ─── Events ───
protected void Emit<T>(string eventName, T data);
public void On(string eventName, Delegate handler);
public void Off(string eventName, Delegate handler);
// ─── Slots ───
protected void RegisterSlotContent(string name, VisualElement content,
Func<Dictionary<string, object>, VisualElement> builder); // called from the generated Build()
protected void BuildSlot(string name, Func<VisualElement, VisualElement> wrapper,
VisualElement container);
protected VisualElement GetSlotContainer(string name);
public VisualElement Slot(string name); // runtime access after Build()
}生命周期(顺序)
Constructor:
Created() → BeforeMounted() → Build() → LoadCompanionStyleSheets()
Deferred(schedule.Execute, next frame):
Mounted()
OnAttachToPanel (every attach, including the first):
ScheduleReactiveUpdates() → Updated() ~60 FPS → Attached()
OnDetachFromPanel (every detach):
BeforeUnmounted() → _updateItem.Pause() → DisposeAllBindings()* → Detached() → Unmounted()
(* skipped while IsRelocating)生命周期不对称:attach 一次,detach 每次都会发生
Mounted() 不是 Unmounted() 的镜像:
| 钩子 | 触发频率 |
|---|---|
Mounted() | 每个实例一次 —— 首次 attach 之后的那一帧。之后重新 attach 时不再调用。 |
Unmounted() | 每次 detach 都会调用,包括之后的每一次;甚至在没有先前 Mounted() 的 detach 时也会调用(在创建的同一帧内被销毁)。 |
Attached() / Detached() | 每次 attach / detach 都会调用,二者彼此对称(在仍处于挂载状态时被销毁的实例,收到的 Detached() 会少一次)。Attached() 在 attach 所在的那一帧运行,而非延迟运行。 |
由此产生的后果:
- 当
IsRelocating为 true 时(同一面板内的 detach/attach,例如浮层传送),DisposeAllBindings()会被跳过,因此在Created()中只注册一次的Watch()/WatchEffect()会继续工作。在非重定位情况下,真正的 detach 会释放它们。 - 在
Mounted()中设置、在Unmounted()中拆除的监听器,在其后的第一次 detach 之后将永久失效。请在Created()中为组件自身或子元素注册(没有任何东西会拆除它们),而对于必须在重新挂载、传送或重新打开后依然存活的外部来源(模型、服务、同级组件)的订阅,请使用Attached()/Detached()。 Detached()没有IsRelocating豁免:即使是同一面板内的传送,它也会运行,因此它总是与下一次Attached()配对。- 不要假设
Mounted()与Unmounted()是一一对应的。
protected override void Attached() => _model.Changed += OnModelChanged;
protected override void Detached() => _model.Changed -= OnModelChanged;
Updated()仅在元素挂载到面板上时运行(OnAttachToPanelHandler)。从构造函数/deferred 调用ScheduleReactiveUpdates的做法已被移除——挂载之前schedule可能不会触发。
自省(DescribeProps / DescribeAllowed / DescribeEvents)
组件在实例上、无需调用方知道其具体类型就能给出的三种只读自我描述。 storybook 的控件面板正是由它们构建的——其中没有任何专属于该面板的内容, 因此任何东西(保存预设的 UI、调试检查器)都可以调用同一套 API:
public partial class SusComponent
{
public IReadOnlyList<SusPropInfo> DescribeProps();
public IReadOnlyDictionary<string, SusAllowedInfo> DescribeAllowed();
public IReadOnlyList<SusEventInfo> DescribeEvents();
public IDisposable SubscribeEvent(string eventName, Action<object> handler); // null if unknown
public bool IsDependencySatisfied(SusPropInfo prop);
public string DescribeDependency(SusPropInfo prop); // "ContentMode = icon" | null
}DescribeProps()遍历每个公共Prop<T>/ReadonlyProp<T>字段:名称、值类型、 当前值(读取时不注册响应式依赖)、分组、声明的范围/依赖, 以及它是否曾被读取或观察过。DescribeAllowed()返回通过UseAllowed注册的 prop 的合法值集合—— 取值、fallback、别名——这样调用方无需知道组件使用的具体SusOptionSets就能构建选择器。DescribeEvents()列出公共On*委托字段(名称、总线名、payload 类型);SubscribeEvent以通用方式订阅其中之一,并对参数装箱。
两个特性让数值范围和 prop 依赖变得显式,而不是靠猜:
[SusRange(0, 5, 0.5)] public Prop<float> Rating = new(0);
[SusRange(0, 100, "%")] public Prop<int> Progress = new(0);
[SusDependsOn(nameof(ContentMode), "icon")] public Prop<string> Icon = new("");[SusRange(min, max, step)](或 [SusRange(min, max, unit)])为 Prop<int>/Prop<float> 设定边界; 没有它时,使用方回退到 0…100。[SusDependsOn(prop, value = null)]——可重复,条件之间为“与”—— 声明某个 prop 只有在同一组件的另一个 prop 取给定值时才有意义; IsDependencySatisfied / DescribeDependency 让调用方可以禁用该控件并说明原因, 而不是渲染一个默默不起作用的控件。
Bind 辅助方法(响应式)
所有绑定都通过 ReactiveEffect 工作——自动订阅 Prop<T> / Computed<T>,并在任意来源变化时更新。 每个辅助方法都返回一个 WatchHandle(在 detach 时会被追踪并释放)。
// v-if: add/remove from DOM (reactive)
protected WatchHandle BindVisibility(VisualElement el, Func<bool> getter);
// v-show: toggle display (reactive)
protected WatchHandle BindShow(VisualElement el, Func<bool> getter);
// :text: bind string to Label (reactive)
protected WatchHandle BindText(Label label, Func<string> getter);
// :class: switch CSS class by condition (reactive)
protected WatchHandle BindClass(VisualElement el, string className, Func<bool> getter);
// v-for (generic, key-based diff, reactive)
protected WatchHandle BindList<T>(VisualElement container,
Func<IEnumerable<T>> source,
Func<T, int, VisualElement> itemBuilder,
Func<T, object> keySelector = null);
// v-for (generic, IEnumerable - typed item access, reactive)
protected WatchHandle BindListFor<T>(VisualElement container,
IEnumerable<T> source,
Func<T, int, VisualElement> itemBuilder,
Func<T, object> keySelector = null);v-model(双向绑定)
// Real BindModel overloads (SusComponent.Bind.cs):
BindModel(myTextField, NameProp); // TextField ↔ Prop<string>
BindModel(mySlider, VolumeProp); // Slider ↔ Prop<float>
BindModel(myToggle, MuteProp); // Toggle ↔ Prop<bool>
BindModel(myDropdown, ModeProp); // DropdownField ↔ Prop<string>组件之间的 Props
// Reactive bind on child Prop<T> (:prop="expr" in .sharq)
// case-insensitive, ReactiveEffect, auto-cleanup. Non-generic: getter returns object.
protected void BindChildProp(VisualElement child, string propName, Func<object> getter);
// Direct bind to a Prop<T> instance (old way, PascalCase-sensitive)
protected void BindProperty<T>(Prop<T> target, Func<T> getter);
// Literal prop (prop="value" in .sharq)
// case-insensitive, mutates .Value (does not replace Prop<T>)
internal static void SetChildProp(VisualElement el, string propName, object value);
// Scalar conversion: string→bool/int/float/enum/string
private static object ConvertScalar(object value, Type targetType);BindList(有状态的重新排序)
// Insert instead of Remove+Add - focus/scroll/input is not lost
// All three options (BindList<T>, BindListFor, BindListFor<T>) have been fixed.SusBootstrap
public static class SusBootstrap
{
// Mounts component T into the container.
// Loads the design-token cascade in order:
// _palette → _font → _theme → design-tokens → _icon → extras + OverlayHost
// (_global comes from SusDefault.tss / ApplyDefaultTSS — not this cascade.)
// When called for the first time, automatically creates an EventSystem (no InputModule).
public static T Mount<T>(VisualElement container) where T : SusComponent, new();
public static T Mount<T>(UIDocument uiDocument) where T : SusComponent, new();
// Cascade only (no component) — used by SusApp and manual UI.
public static void LoadTokenCascade(VisualElement container);
// Returns or creates the OverlayHost as the last child of the container.
public static OverlayHost GetOrCreateOverlay(VisualElement container);
// Finds or creates SusWorldSpacePanel + wires WorldSpaceService.Default
// (SusApp calls this automatically unless UseWorldSpace(false)).
public static SusWorldSpacePanel EnsureWorldSpacePanel(
Camera camera = null, OverlayHost overlayHost = null);
// Panel TSS (_palette + _font + _global via SusDefault.tss).
public static void ApplyDefaultTSS(UIDocument document);
}SusMotion.Reduce — 减少动效
SusMotion.Reduce 是一个应用级的 Prop<bool>,对应“减少动效”(reduce motion)无障碍设置。 它默认关闭,为 false 时每个 SusMotion.Play 的行为与以前完全相同。
// Settings screen: the player asked for less motion.
SusMotion.Reduce.Value = true;标志开启期间:
- 有限 的
SusMotionplay 会立即写入结束值,并同步调用onComplete, 没有 tick,也没有延迟; - 带有 无限 组(
Repeat <= 0)的 play 不会启动,目标保持不变; - 把标志从
false切换为true会停止所有正在运行的无限 play, 并应用其恢复模式。已在运行的有限 play 会继续播放到结束。
拥有自己循环的组件(SusMotion 之外的定时 ticker)应当监听该标志并 暂停,就像监听其他任何 Prop<T> 一样:
SusMotion.Reduce.Changed += (_, reduce) => SetPaused(reduce);
SetPaused(SusMotion.Reduce.Peek());在元素挂载期间订阅,并在 detach 时取消订阅:该标志的生命周期长于任何面板。 USS 过渡不受影响;该标志覆盖持续性动画和脚本驱动的动画。
替代关系(v1 → v2)
| 旧(v1) | 新(v2) |
|---|---|
sus(UPM com.sus.sfc) | com.sharq-it.sus.core |
sharq-ui-system(SusCompiler.exe、LibSassHost) | SharqFileImporter(AssetPostprocessor) |
ElementBase —— 反射式 | SusComponent : VisualElement |
compiled ui/ —— 手动与自动混合 | generated/ —— 仅自动生成,已 .gitignored |
在生态系统中的位置
sus-core (this package)
├── sus-router — navigation (Push/Replace/Back, screens)
└── your Unity project — consumer app