MIT 许可证 NuGet

首页 / 文档 / 应用与面板

应用、面板与帧循环

BaseApp 派生自 Panel。你的应用 就是 面板树的根, 这就是为什么 AddPanel 和 AddControl 可以直接在它上面工作,也是为什么没有另一个 需要学习的场景图类型。

事情发生的顺序

阶段在这里做什么是安全的
你的构造函数标题、分辨率、相机、StorageService.DirectoryBase,以及每个 RenderQuality.Default* 值。GPU 还不存在。
Init()由平台运行器在设备和交换链存在后调用。只有当你需要包住初始化时才重写它。
Create()构建面板树、加载字体、注册 Compute 特效。先调用 base.Create()。
Update(time)每帧一次。推进你自己的状态;管线读取结果。
Resize() / ResizeContent()在窗口或方向变化时调用。在这里重新摆放你的 2D 布局。
Draw()由引擎持有。你通常不重写它。

Create 被声明为 async void,这不寻常但是有意为之:它让你可以 await 字体或设置读取,而首批帧 已经在呈现。管线中没有任何东西等待它,所以晚三帧加入的控件 就只是晚三帧出现。

面板

面板拥有控件和其他面板。没有布局系统,没有停靠,也没有 约束求解器 — 面板是放置相关事物的位置,也是放置驱动它们的代码的位置。

成员说明
Controls、Panels普通列表。你可以读取和重排它们;使用 Add/Remove 方法来变更。
AddControl添加一个叶子控件并排队它的加载。如果它已存在则返回 false。
AddPanel添加一个子面板。它 不会 排队加载 — 有真实加载工作的面板需要显式排队,见下文。
RemoveControl / RemovePanel分离 并 释放子项。如果你想分离并保留它,就自己从列表中移除它。
Layer、Order父级内的绘制顺序。平局时回退到插入索引,因此排序是稳定的全序。
Alpha沿子树向下相乘。Alpha = 0 会把整个分支从每个通道中剪掉 — 隐藏一个屏幕最廉价的方式。
SetMode(string)为带变体的面板提供的一个字符串钩子,这样你不需要为每个状态建子类。
OnClose在面板关闭自身时调用;对话框用它告诉持有者它已完成。
IsDisposed由 Dispose 设置。加载队列会检查它,所以加载中途释放是安全的。
绘制会递归,更新不会。

Panel.Draw 在每个通道中替你遍历它的控件和子面板, 并且是排序后的。Panel 有意 不 重写 Update:你在你自己的 Update 中, 按你想要的顺序更新你拥有的子项。这就是为什么 更新顺序是你可以推理的东西 — 也是为什么一个你忘记更新的控件 仍然会绘制,处于它最后的状态。

唯一的 Update 签名

每个控件和面板都有相同的形态。先调用 base.Update,然后驱动你拥有的一切。可选 参数让父级可以只覆盖一帧的摆放,而不改动子项的字段。

public override bool Update(float time, float? alpha = null,
    float? posX = null, float? posY = null, float? posZ = null,
    float? width = null, float? height = null, float? depth = null)
{
    var result = base.Update(time, alpha: alpha,
        posX: posX, posY: posY, posZ: posZ,
        width: width, height: height, depth: depth);

    // time is the delta in seconds since the previous frame.
    angle += time * 0.5f;

    robot.Rotation = angle;
    robot.Update(time);

    return result;
}
要点为什么重要
time 是增量自上一帧以来的秒数,不是绝对时钟。如果你需要相位,BaseApp.Time 是累计总量。
返回 true表示这一帧已被消费 — 一次点击已被处理。交互式面板用它来阻止传播。
没有 Draw 要写更新状态就是全部契约。应用代码中没有绘制调用、没有命令列表、没有材质绑定。
输入在这里到达OnClick 和 OnTouch 从 BaseControl.Update 内部触发,这就是为什么从未被更新的控件也永远不会可点击。
总是按名称传这些参数

基类声明把参数排序为 posX, posY, posZ, width, height, depth,但几种控件 类型把它们的重写声明为 posX, posY, width, height, posZ, depth。这些类型全都是 float?,所以编译器不会报错,而每个重写 都按名称转发给它的基类,这保证了行为正确 — 只对具名调用者而言。 一个越过 alpha 之后的位置调用,可能会悄悄把你的 Z 放进 Width。请传具名参数,或者干脆省略参数, 在调用 Update(time) 之前直接给属性赋值。

加载是一个队列,Ready 是标志

没有任何东西同步加载。AddControl 通过 BaseApp.RequestLoad 把子项放进一个全局队列;队列带并发上限地调用 Load(),成功后把 Ready = true 置位并打上 LoadComplete 标记。

  • 加载失败不会被重试。 Load 返回 false 会让控件保持未就绪,且什么都不抛出。一个看不见的模型通常意味着资产路径错误。
  • Ready 因类型而异。 对叶子控件来说,它意味着 GPU 资源存在且可绘制;对面板来说,它只意味着面板自身的加载已完成,而子项还在它身后陆续就绪。
  • 加载期间释放是安全的。 队列在前后都会检查 IsDisposed,并丢弃结果而不是复活一个已死的控件。
  • 排队会去重。 对同一个可加载项请求两次是无操作,返回 false。
  • Web 构建按帧预算支出。 浏览器没有同步文件访问,所以加载被跨帧计量而不是阻塞 — 这就是大场景在那里渐进淡入的原因。
// Wait for something before using it, without blocking the frame:
if (robot.Ready)
    robot.Update(time);

// A panel that does real loading work of its own opts in by declaring
// the interface; the base class already provides every member.
internal class Rocks : Panel, ILoadable
{
    public override async Task<bool> Load() { /* extract in the background */ return true; }
}

// AddPanel does not queue, so queue it yourself:
AddPanel(rocks);
DeviceServices.BaseApp?.RequestLoad(rocks);

这个切分是有意的:ILoadable 是加载队列 认得的,IControl 是渲染通道认得的。 面板可加载但不可绘制;精灵两者都是。因为面板只有在确有工作要做时才声明 ILoadable,类型系统 防止你意外地把容器排队。