载入中...
搜索中...
未找到
声明式 UI模块

声明式 UI模块

脚本入口: eve.UI()

构建并挂载保留式控件树,通过稳定 ID 消费点击和更改事件。

基本用法

ui.beginBuild();
ui.beginWindow("HUD", "root");
ui.text("Ready", "status");
ui.button("Start", "start");
ui.end();
ui.mountBuildAs("hud");

对象关系与调用时机

UI 管理多个命名 Host;Host 保存控件树;稳定 ID 标识控件;UIComponent.build() 可封装可复用树。事件返回完整 host/id 路径,修改接口在当前 selected host 中查找。

自定义组件

脚本组件拥有实例级 props 与 state 表;setProps(table) / setState(table) 会合并 变更并标记脏状态。已挂载的根组件会进入帧调度队列,在下一次渲染前通过稳定 ID 自动协调旧树;同一帧的多次变更只重建一次,通常不需要手动调用 updateIfDirty()。 持久化的子组件实例可用 renderChild(child, props) 嵌入,子组件 markDirty() 会自动向 父组件传播。首次挂载和后续重建分别调用 onMount()、onUpdated();显式 unmount() 会递归卸载子组件并调用 onUnmount(),同时隐藏对应 Host。

renderChild(child, props, replaceProps=false) 默认与 setProps() 一样合并属性;第三个 参数为 true 时先清空旧属性表再应用新属性。updateIfDirty() 仍可用于测试或确实需要 同步刷新的工具代码。

组件可用 bindClick(id, fn) / bindChange(id, fn) 声明事件处理器;同一组件内相同 类型与 ID 的绑定会被替换而不是累加,组件卸载时自动注销。动态列表使用 renderKeyed(key, factory, props):稳定 key 会复用原组件实例,移除 key 会触发子组件 onUnmount() 并释放其事件处理器。

componentOnClick、componentOnChange 与 componentClearHandlers 是 UIComponent 实现事件所有权的内部桥接函数;游戏脚本应使用上述 bindClick / bindChange,不要直接调用这些桥接函数。

布局

beginRow() / beginColumn() 支持 setItemFlexGrow()、setItemFlexShrink()、 setItemFlexBasis()、setItemAlignSelf() 与 setFlexWrap()。独立的横纵间距使用 setLayoutGaps(columnGap, rowGap);setLayoutOverflow("visible" | "clip" | "scroll") 控制超出内容的显示方式。beginGrid(columns, id, columnGap, rowGap) 创建等宽列网格, setItemGridColumnSpan() 设置最后一个子项的跨列数,setItemAspectRatio() 可让 Flex 或 Grid 根据宽高比推导另一轴尺寸。

saveTreeJson() 写出 schema: "eve.ui.tree" 与当前 version: 4。加载器把无版本文档 视为版本 1,接受版本 1–4,拒绝未知 schema 和未来版本;未知字段会被忽略,新增字段 均有向后兼容默认值。

class Label extends eve.UIComponent {
function build() { ui().text(props.text, "label") }
}
class Counter extends eve.UIComponent {
label = null
constructor(u) {
base.constructor(u, { title = "Counter" })
state.value <- 0
label = Label(u)
}
function build() {
local u = ui()
u.beginWindow(props.title, "root")
renderChild(label, { text = "Value " + state.value })
u.button("+1", "increment")
u.end()
}
}

.9.png 九宫格资源

支持未经 Android 编译的标准 .9.png:顶边和左边各一段连续黑色像素定义可拉伸区, 底边和右边可选黑线定义内容区。加载时会移除四周 1px 标记框并缓存纹理。

ui.beginBuild()
ui.beginWindow("Nine patch", "root")
if (ui.beginNinePatch("assets/panel.9.png", "panel", 320, 0)) {
ui.text("内容会自动使用 .9.png 声明的 padding", "content")
ui.end()
}
ui.ninePatch("assets/button.9.png", "preview", 180, 48)
ui.end()
ui.mountBuildAs("nine-patch-demo")

ninePatch(path,id,w,h) 创建叶子图片;beginNinePatch(path,id,w,h) 创建可容纳子控件的 面板;setImageNinePatchFile(id,path) 可替换已挂载图片。当前解析器有意只接受单段拉伸 区(与引擎单中心九宫格模型一致),多个不连续拉伸段会返回 false 并输出诊断。

目标导向指南

创建可交互 HUD

beginBuild() 后用 Window/Flex/Group/List 组织控件,给每个交互控件稳定 ID,完成后 mountBuildAs("hud")。更新阶段循环 consumeClick() / consumeChange(),渲染阶段调用 beginFrameAndRender()。

使用弹性布局(Flex)

beginRow / beginColumn / beginFlex("row"|"column", id, gap) 自动排列子控件,无需手写 sameLine。spacer() 吸收剩余空间;setItemFlexGrow / setItemSize 作用在刚添加的子项上;setFlexAlign / setFlexJustify 配置当前 Flex 容器。

ui.beginRow("toolbar", 8.0);
ui.setFlexJustify("space-between");
ui.iconButton("save", "Save", "save");
ui.spacer("sp");
ui.button("Quit", "quit");
ui.end();

桌面框架可用 beginToolbar、beginToolbox、beginSidebar、beginStatusBar 和 beginSplitPane("row"|"column", ratio, id) 直接组合。SplitPane 必须包含两个直接子项, 拖拽分隔条会产生普通 value change;比例可通过 getValue / setValue 读取和恢复。 独立面板可用 setHostMovable(true) / setHostResizable(true) 允许用户调整;ImGui 会按 稳定 Host ID 自动写入和恢复 ini 布局。无标题 Overlay 的不透明度由 setHostOverlayAlpha(0..1) 控制。

ui.beginToolbar("toolbar");
ui.iconButton("save", "", "save");
ui.spacer("toolbar-fill");
ui.badge("Ready", "ready");
ui.end();
ui.beginSplitPane("row", 0.25, "workspace");
ui.beginSidebar("left", 240.0);
ui.searchField("Search tools", "", "tool-search");
ui.beginToolbox("tools", 40.0, 3);
ui.iconButton("pointer", "", "select");
ui.iconButton("move", "", "move");
ui.iconButton("paint-brush", "", "paint");
ui.end();
ui.end();
ui.beginCard("content");
ui.sectionHeader("Inspector", "inspector");
ui.end();
ui.end();

使用内置编辑器图标

icon(name, id) 显示语义图标,iconButton(name, label, id) 创建图标按钮。图标字体随 引擎构建和 SDK 安装,不依赖游戏工作目录。名称不区分大小写,空格和下划线会转成 连字符;常用名称包括 search、settings、save、undo、redo、folder-open、 pointer、move、paint-brush、database、layers、play 和 warning。

ui.beginRow("tools", 4.0);
ui.iconButton("pointer", "", "select");
ui.setItemSelected(true); // 当前工具使用主题激活色
ui.iconButton("move", "", "move");
ui.iconButton("paint-brush", "Paint", "paint");
ui.end();

setItemSelected(true) 为刚加入的普通按钮或图标按钮设置选中外观;工具切换后用既有的 setChecked(id, selected) 更新已挂载控件。选中态只是 UI 投影,工具的权威状态仍由 EditorToolbar 或具体编辑器 session 持有。

组合桌面编辑器控件

Editor 常用控件已内置:searchField、switch、badge、colorPalette、sectionHeader、beginCard、 beginMenuBar / beginMenu / menuItem。setItemTooltip 为刚添加的控件设置悬停说明。 菜单栏应作为 Window 的直接子项;所有可交互控件都应使用稳定 ID。

ui.beginMenuBar("main-menu");
ui.beginMenu("File", "file");
ui.menuItem("Save", "Ctrl+S", "save");
ui.end();
ui.end();
ui.searchField("Search assets", "", "asset-search");
ui.setItemTooltip("Filter project assets");
ui.sectionHeader("Properties", "properties");
ui.beginCard("selection-card");
local addSwitch = ui["switch"].bindenv(ui); // `switch` 是 Squirrel 关键字
addSwitch("Visible", true, "visible");
ui.badge("Modified", "state");
ui.colorPalette("Accent", 0.18, 0.42, 0.86, 1.0, "accent");
ui.end();

colorPalette(label, r, g, b, a, id) 显示颜色编辑器与色板网格。当前颜色存在节点 tint 上,用 getColorR/G/B/A / setColor 读写;拖动或点选色块走普通 consumeChange()。 setValueText(id, "#ff0000;#00ff00") 可换成自定义色板(#rrggbb / #rrggbbaa 或 r,g,b[,a], 分号或换行分隔);留空则使用内置 16 色。

用 Theme 统一布局

Theme 除颜色和 ImGui 基础几何外,还提供类似 CSS design tokens 的语义布局默认值: toolbarHeight、statusBarHeight、sidebarWidth、toolboxCellSize、 splitterSize、minPaneSize、panelPadding、cardPadding、barPadding、 sectionSpacingY、searchMinWidth 和 searchIconGap。暗色与亮色主题共享这些布局 参数,因此切换配色不会改变界面结构。

Toolbar、Sidebar、Toolbox、Card、StatusBar、SplitPane 和 SearchField 在没有显式尺寸时自动继承 Theme。局部差异继续通过 setItemSize、setItemPadding、setItemMargin、setItemMinSize 等接口覆盖, 其优先级高于 Theme 默认值。SplitPane 的直接子节点总是填满获分配的 pane,内部控件 再依据可用宽度响应式重排;Toolbox 的列数是上限,空间不足时会自动减少列数。

更新而不重建整个 UI

文本变化用 setText(id, value),进度用 setValue(),显示隐藏用 setVisible(), 启用状态用 setEnabled();结构变化才重新 build 并 remountBuildAs()。多宿主时先 select(host) 再按局部 ID 操作。

配置焦点、输入策略与无障碍语义

构建树时,setItemFocusMode("none"|"click"|"all")、 setItemMouseFilter("stop"|"pass"|"ignore") 与 setItemTabIndex(index) 作用于刚添加的 控件。setItemFocusOrder(previous, next) 配置顺序导航, setItemFocusNeighbors(left, right, up, down) 配置方向导航;空字符串表示继续使用稳定的 tabIndex/树顺序。setItemEnabled(false) 会同时禁止交互与焦点。

指针点击在保留树中按 MouseFilter 路由:stop 在当前控件处理后截断,pass 处理后继续 向祖先冒泡,ignore 不处理但允许继续向祖先传递。轮询队列仍只记录原始目标一次,祖先 通过各自的 onClick 处理器观察同一事件,避免脚本收到重复目标。

setItemAccessibility(role, name, description) 为控件附加语义角色、可读名称和说明。 角色可用 button、checkbox、combobox、textbox、slider、menuitem、 progressbar、image、heading、status、region 或 generic。

挂载后可用 requestFocus(id) 请求焦点,或用 moveFocus("next"|"previous"|"left"|"right"|"up"|"down") 导航; getFocusedId() 返回当前焦点的稳定 ID。键盘 Tab、Shift+Tab 和方向键会走同一套显式邻居 与顺序规则,游戏手柄或自定义输入层可直接调用 moveFocus()。

ui.searchField("Search", "", "search");
ui.setItemTabIndex(0);
ui.setItemFocusNeighbors("", "save", "", "");
ui.setItemAccessibility("textbox", "Search scene", "Filter scene nodes");
ui.iconButton("save", "", "save");
ui.setItemTabIndex(1);
ui.setItemFocusOrder("search", "");
ui.setItemAccessibility("button", "Save scene", "");

切换统一主题

内置 dark / light 预设共享圆角、边框、间距与字体缩放,只切换配色。推荐 setTheme("dark") / setTheme("light"),用 getTheme() 读取当前名;也可用 setThemeDark() / setThemeLight()。DPI 用 setScale(),主题几何会按比例缩放。

需要在同一界面中区分编辑器面板、游戏 HUD 或嵌入工具时,在添加容器后调用 setItemTheme("dark"|"light"|"inherit")。主题覆盖作用于该容器及其整个子树,嵌套容器 可以再次覆盖;离开子树后自动恢复外层主题。该设置随 UI JSON 资产保存和加载。 对于刚通过 beginWindow、beginCard、beginGroup 等打开且仍在构建的当前容器,使用 setThemeScope();这样根 Window 也能拥有独立于全局预设的主题。

可复用的局部视觉样式使用命名 StyleClass。先用 defineStyleClass(name, parent) 声明类, 再用 setStyleClassColor(name, "text"|"background"|"border"|"accent", r, g, b, a) 和 setStyleClassMetric(name, "padding"|"rounding"|"alpha", x, y) 配置稀疏 token。三个修改 接口返回稳定状态字符串(如 applied、unknown-parent、inheritance-cycle),不会静默 接受无效定义。setItemStyleClass() 设置最近控件,setStyleScope() 设置当前容器并通过 ImGui 样式栈自然作用于子树;子节点只覆盖自己声明的 token。命名继承在建树或 patch 时 解析为节点快照,因此逐帧渲染不做字符串注册表查询。类名随 version 4 JSON 保存;注册表 由应用启动脚本重新声明,加载旧版资产时缺失字段按无样式处理,未知字段仍忽略。

保留树和 MCP EditorHost 的 Button、Checkbox、单值 Slider、Combo 与文本输入共享同一组 原生控件原语,因此尺寸、禁用态和编辑结果语义一致。文本输入持有 UTF-8 std::string 并在 ImGui 请求扩容时增长,不再使用 1 KiB/512 字节固定缓冲;SDL/ImGui 后端继续负责平台 IME。

轻量动效由 UiTweenDriver 驱动,在 beginFrameAndRender() / tickTweens(nowMs) 中推进:

  • animateHostPos(x, y, durationMs, ease = "smoothstep", delayMs = 0) — 宿主窗口位置
  • animateHostSize(w, h, durationMs, ease, delayMs) — 宿主显式尺寸
  • animateHostOverlayAlpha(alpha, durationMs, ease, delayMs) — 叠加层背景透明度
  • animateItemOpacity(id, opacity, durationMs, ease, delayMs) — 节点瞬态 opacity(不写 JSON / StyleClass)
  • animateItemPos(id, x, y, durationMs, ease, delayMs) — 绝对定位节点偏移(自动 absolute)

durationMs <= 0 且无 delay 时立即跳到终点;再次对同一属性调用会替换未完成的补间。 cancelHostTweens() / cancelItemTweens(id) / cancelAllTweens() 取消选中宿主上的待定补间。 缓动名与 Motion/Math 对齐(smoothstep 为默认,另有 linear、Quad/Cubic/Sine/Expo、Back/Elastic/Bounce); 未知名称按 linear 处理。

需要 loops / Sequence / Punch,或与 SimulationStep 同拍时,用 eve.Animation() 的 Motion 经 UI 侧 sink 推送写回(animation 模块启用时编译 UiMotionSinks):

UiHostPosSink posSink(hostHandle);
UiNodeOpacitySink opacitySink(hostHandle, "panel");
anim->motionVec2({0,0}, {120,60}, 0.35f).ease("outQuad").bind(posSink);
anim->motion(0.f, 1.f, 0.25f).ease("outCubic").bind(opacitySink);
anim->advance(step); // 与游戏时钟同泵
float step
Definition TreeMesh.cpp:314

无 animation 的裁剪构建仍保留上方 animate* 便利路径;sink 桥接 TU 会被排除。

getLayoutDiagnostics() 输出各节点测量尺寸和横纵溢出, getAccessibilitySnapshot() 输出语义节点的 role/name/description、enabled 与 focused 状态, 供调试器和自动化读取而不触碰 ImGui 内部对象。

常见问题

  • 每帧重新 mount,丢失输入焦点与控件状态。
  • 多个控件使用同一 ID。
  • 未循环消费全部 click/change,队列在后续帧才清空。

API 快查

下列方法名来自当前 Squirrel 绑定;同一模块创建的辅助对象(例如 World、Body、Source)的方法也列在这里。

  • beginBuild()、beginCard()、beginChild()、beginCollapsing()、beginColumn()、beginFlex()、beginFrameAndRender()、beginGroup()、beginList()、beginMenu()、beginMenuBar()、beginNinePatch()、beginRow()、beginSidebar()、beginSplitPane()、beginStatusBar()、beginScrollList()、beginToolbar()、beginToolbox()、beginWindow()、bindOwner()
  • animateHostPos()、animateHostSize()、animateHostOverlayAlpha()、animateItemOpacity()、animateItemPos()、cancelHostTweens()、cancelItemTweens()、cancelAllTweens()、tickTweens()、getHostTweenCount()、getItemTweenCount()、badge()、button()、checkbox()、colorPalette()、combo()、consumeChange()、consumeClick()、consumeDrop()、dispatchEvents()、dragDropSupport()、end()、getChecked()、getColorA()、getColorB()、getColorG()、getColorR()、getDropOrigin()、getDropSource()、getDropText()、getDropType()、getName()
  • getFocusedId()、getScale()、getTheme()、getValue()、getValueText()、icon()、iconButton()、initBackend()、inputText()、isBackendReady()、listItem()、mountBuild()、moveFocus()
  • menuItem()、mountBuildAs()、mountSimple()、progress()、remountBuildAs()、sameLine()、searchField()、sectionHeader()、select()、separator()、setChecked()
  • requestFocus()、setEnabled()、setFlexAlign()、setFlexJustify()、setHostAnchor()、setHostLayer()、setHostModal()、setHostMovable()、setHostOverlay()、setHostOverlayAlpha()、setHostPercent()、setHostPos()、setHostResizable()、setHostSize()、setHostVisible()、setHostWorldAnchor()、clearHostWorldAnchor()、setHostWorldEdgePolicy()、setHostWorldDistanceScale()、setHostWorldOverlap()、getHostWorldState()、getHostWorldScreenX()、getHostWorldScreenY()、setImageCornerRadius()、setImageNinePatch()、setImageTint()、setImageUv()、setItemAbsolute()、setItemAccessibility()、setItemAlignSelf()、setItemDragSource()、setItemDropTarget()、setItemEnabled()、setItemSelected()、setItemFlexBasis()、setItemFlexGrow()、setItemFlexShrink()、setItemFocusMode()、setItemFocusNeighbors()、setItemFocusOrder()、setItemMargin()、setItemMaxSize()、setItemMinSize()、setItemMouseFilter()、setItemPadding()、setItemPercent()、setItemSize()、setItemTabIndex()、setItemTheme()、setItemTooltip()、setNavGamepad()、setNavKeyboard()、setScale()、setText()
  • setTextWrap()、setTheme()、setThemeDark()、setThemeLight()、setThemeScope()、defineStyleClass()、setStyleClassColor()、setStyleClassMetric()、clearStyleClasses()、setItemStyleClass()、setStyleScope()、setColor()、setValue()、setValueText()、setVisible()、slider()、spacer()、switch()、text()、textWrapped()、wantCaptureKeyboard()
  • wantCaptureMouse()、registerTexture()、unregisterTexture()、setImageTextureId()、setImageNinePatchFile()
  • image()、imageButton()、ninePatch()、onClick()、onChange()、saveTreeJson()、loadTreeJson()、getStats()、getLayoutDiagnostics()、getAccessibilitySnapshot()
  • viewport()、viewportCanvas()、viewportHovered()、viewportActive()、viewportMouseX()、viewportMouseY()、viewportDragDX()、viewportDragDY()、viewportWheel()
  • inspect()、inspectClose()、inspectRefresh()、inspectSelectClass()、inspectObject()、inspectSetPickHandler()、inspectPickScene()、inspectAddInstance()、propertySchema()
  • dbOpen()、dbClose()、dbRefresh()、dbSelectClass()、dbRegister()、dbCreateInstance()、dbUnregister()
  • editorOpen()、editorClose()、editorSelectPanel()、sceneOpen()、sceneClose()、sceneSelectNode()、sceneSetPickHandler()

propertySchema(instance) 返回脚本实例经反射派生的属性 schema 表(typeId / version / properties[],含 path、kind、displayName、readOnly、数值约束与 choices)。editor="color"|"vec2"|"vec3"|"vec4"|slider|combo 等 attribute 决定 kind; 可编辑面板请用 inspectObject(instance),不要把 schema 表当成第二权威状态。

引擎纹理控件

image() 和 imageButton() 可以显示任意引擎 Texture。先用 textureId = ui.registerTexture(texture) 获得后端中立句柄,再用 ui.setImageTextureId(id, textureId) 绑定到当前 Host 的控件。纹理不再使用时,先把相关 控件设为 0,再调用 ui.unregisterTexture(textureId);这样 Vulkan 与 WebGPU 后端都能 释放对应资源。UV、色调、九宫格与圆角仍通过 setImageUv/Tint/NinePatch/CornerRadius 组合。

桌面 Drag & Drop

Drag & Drop 仅在 Windows、macOS、Linux 桌面构建启用;Android、iOS 和 Web/WASM 的 dragDropSupport() 返回 unsupported-platform,不会伪装成功。setItemDragSource(type, text) 和 setItemDropTarget(type) 作用于刚添加且会产生交互 item 的控件;目标类型可用 * 接受全部内部 payload,file 接受操作系统文件拖入。

ui.button("Stone", "stone")
ui.setItemDragSource("asset", "textures/stone.png")
ui.viewport("scene", 640, 360)
ui.setItemDropTarget("asset")
local target = ui.consumeDrop()
if (target != "") {
local payloadType = ui.getDropType()
local payloadText = ui.getDropText()
local source = ui.getDropSource() // OS 文件为空
local origin = ui.getDropOrigin() // internal / os-file
}

事件保存 owning UTF-8 文本快照,源节点或目标节点在消费前销毁不会留下悬空引用。

内嵌渲染视口(Viewport)

ui.viewport(id, w, h) 声明一个内嵌渲染目标控件:它维护一个离屏 Canvas (尺寸跟随控件矩形),游戏在 eve_render 里先把 2D(gfx.setCanvas + 立即模式绘制) 或 3D(gfx.renderScene3DToCanvas(canvas, camera))渲染进去,然后 ui.beginFrameAndRender() 会把该 Canvas 纹理显示在控件中。视口交互输入 (悬停、按住、控件本地鼠标坐标、拖拽增量、滚轮)通过 viewportHovered/Active/MouseX/ MouseY/DragDX/DragDY/Wheel(id) 每帧读取。完整示例见 examples/terrain-editor(高度图地形 + orbit 相机 + 抬高/压低笔刷)。

3D 世界锚点(World Anchor)

世界锚点把整个 UI Host 投影到当前活动 Camera3D 的屏幕位置,适合单位名牌、交互提示、 任务标记和编辑器 3D gizmo 标签。先选择 Host,再调用 setHostWorldAnchor(x, y, z);clearHostWorldAnchor() 恢复普通屏幕布局。

setHostWorldEdgePolicy("hide"|"clamp") 控制目标离开视口后的行为; setHostWorldDistanceScale(enabled, referenceDistance, minScale, maxScale) 配置按相机距离缩放。 相机不可用或目标在相机背面时 Host 自动隐藏,不会沿用上一帧坐标。世界坐标由游戏场景 持有者负责更新;UI 只保存投影输入和逐帧派生的屏幕状态。当前版本不做深度遮挡判断, 需要被场景几何遮挡的面板应继续使用 3D mesh/material。

大量名牌同时出现时,可用 setHostWorldOverlap(true, priority, padding, maxDisplacement) 开启确定性的屏幕矩形避让。高优先级、近距离 Host 先占位,其余 Host 在限定距离内上下寻找空位;没有可用位置时状态变为 crowded 并暂停渲染。 getHostWorldState() 返回 visible、behind-camera、outside-viewport、no-camera、 crowded 或 disabled;最终屏幕坐标可通过 getHostWorldScreenX/Y() 查询。

ui.select("unit-nameplate")
ui.setHostWorldAnchor(unitX, unitY + 2.0, unitZ)
ui.setHostWorldEdgePolicy("clamp")
ui.setHostWorldDistanceScale(true, 10.0, 0.7, 1.2)
ui.setHostWorldOverlap(true, 10, 4.0, 96.0)

MCP EditorHost 脚本接口

eve mcp 会在根表建立 eve.host,供项目脚本创建和维护 AI 编辑器。它与 eve.UI() 的游戏内 retained UI 相互独立,但都运行在同一个 Squirrel VM 中。

  • 窗口与状态:status()、openWindow()、closeWindow()、windowState()。
  • View 与交互:applyEditor()、removeEditor()、setValue()、events()、 widgetRect()、capture()、save()。
  • ViewModel:registerVM()、unregisterVM()、runScript()。
  • 热更新:reloadResource(path) 手动重载项目内的 mcp.nut、mcp/*.nut、 editors/*.vm.nut 或 editors/*.editor.json;hotReloadStatus() 返回 watcher、 成功/失败计数与最近诊断。正常保存会由 MCP 主机自动触发,无需重启。

Canvas 高度驱动的父布局

ParentScalerSettings、ParentScalerInput、ParentScalerState、ParentScalerOutput 和 evaluateParentScaler(state,output,settings,input) 对应 Pcg ParentScaler。scaleWithCanvas=true 且存在 Canvas 和目标时,FullScreen 输出原始 canvasHeight;partScreen=true 时按 Pcg 公式将其钳制到 [0.1,maxHeight]。只有 Canvas 高度变化或 lastScaleHeight==0 时才令 applyHeight=true,调用者再把 height 应用到每个仍有效的目标 Rect,保留各自宽度。

脚本字段包括 scaleWithCanvas、partScreen、maxHeight、hasCanvas、targetCount、canvasHeight、 lastScaleHeight、applyHeight 和 height。状态与输出由调用者持有;零目标不会消耗高度变化,后续加入目标 仍能获得写入。无效目标由调用者跳过,负目标数或非有限高度在修改 state/output 前失败。

使用要点

  • 模块对象和它创建的资源对象应保存在全局或实体状态中,不要在每帧重复创建。
  • 带 update(dt) 的系统应在 eve_update 调用;绘制方法应在 eve_render 调用。
  • 参数约束、默认值和返回类型以对应模块头文件及 addFunc 绑定为准;本文 API 快查与当前源码同步生成。

源码: src/modules/ui/ 相关测试: 在 test/ 中搜索 ui。

PhotoMode 的 Photo 域由 PcgPhotoModePhotoAuthority 保存并统一发布。它覆盖场景身份、 截图分辨率、EXR/JPG/PNG/TGA 格式、加载保存设置、关闭时恢复,以及 FPS、准星和三分法 叠层开关。分辨率索引 0 使用当前 drawable 尺寸,1..9 精确映射 Pcg 的 640×480 到 7680×4320 档位;screenshotSize() 与 screenshotExtension() 供捕获和 UI 调用方读取。 authority 只保存稳定值,不持有 widget;实际叠层仍由 UIHost 使用现有 UI API 构建。

Pcg 截图按钮与颜色预览同步

AutoAssignTakePhotoEvent 的启动期按钮接线使用现有 ui.onClick(id, callback):回调中调用 gfx.saveFramePng(path);首次启用 readback 后若尚无已呈现帧会返回 false,下一帧再次调用。该组合对应 Unity Button.onClick -> ScreenShotter.TakeHiResShot,并允许调用者显式选择输出路径。

eve.PcgColorPreviewSync() 对应 ColorPreviewSync.OnEnable。调用 sync(r,g,b,a) 后,通过 getRed()、getGreen()、getBlue()、getAlpha() 取得按钮 highlighted color:纯黑源色映射到 (0.5,0.5,0.5,1),其余颜色严格乘以原脚本硬编码的 2.5。非有限输入返回结构化错误且保留原状态。

PcgDraggableWindow

eve.PcgDraggableWindow() 对应照片模式的 DraggableUIWindow。configure(x,y,width,height,screenWidth,screenHeight,canvasScale) 保存初始 anchored position;drag(deltaX,deltaY) 按 canvas scale 换算并复现 Pcg 的 X 负向、Y 正向边界钳制。pointerDown(middleButtonDown) 总是产生置顶请求,中键同时复位;用 consumeBringToFront() 消费请求,并通过 getX() / getY() 将结果交给 ui.setHostPos。

PcgPhotoModePanels

eve.PcgPhotoModePanels() 保留七个 Pcg 照片模式页签的固定编号:Camera=0、Unity=1、Terrain=2、Lighting=3、Water=4、PostFX=5、PhotoMode=6。select(panel) 原子关闭其余六个面板并选择目标;getSelected()、isActive(panel)、getRevision() 提供快照,consumeScrollReset() 通知调用者重建 scroll content 并把滚动条置顶。

eve.PcgPhotoModePanelButton() 通过 configure(nonSelectedRGBA,selectedRGBA) 保存两组颜色,setSelected() 同时控制按钮 normal/selected 色和面板布局开关。读取接口为 getNormalR()、getNormalG()、getNormalB()、getNormalA()、getHighlightR()、getLayoutEnabled()、getSelected();highlight 始终来自选中色,与 Pcg 注册逻辑一致。

PcgPhotoModeColorPicker

eve.PcgPhotoModeColorPicker() 实现照片模式颜色选择器。open(r,g,b,a,hdr)、setLast(...)、setFocusedName(name)、close() 管理打开、旧值和标题;setRed()、setGreen()、setBlue()、setHdr() 对应四条滑杆,reset() 恢复旧值。读取接口包括 getRed/Green/Blue/Alpha、getPreviewRed/Green/Blue、getHdrSwatch()、getHdrEnabled()、getOpen()、getFocusedName()、getChangeRevision() 和 consumeBringToFront()。HDR alpha 大于 1 或小于 0 时严格保留 Pcg 打开阶段和预览阶段的两次 RGB 乘法。 预览通道的完整方法名为 getPreviewRed()、getPreviewGreen()、getPreviewBlue()。

PcgScreenshotSavedNotice

eve.PcgScreenshotSavedNotice() 对应截图保存提示。configure(enabled,showSeconds) 设置开关和持续时间;截图成功后调用 request(path),在该帧渲染结束调用 endFrame(unscaledNow) 才显示,再用 tick(unscaledNow) 到期隐藏。getVisible()、getPathVisible()、getPath()、getPending() 返回 UI 快照。重复 request 会取消旧显示并从新的 end-of-frame 重新计时。

PcgTooltipManager

eve.PcgTooltipManager() 合并移植 Tooltip、TooltipManager、TooltipProfile 和 TooltipTrigger。configure(interactionMode,theme,delay,bottomOffset,topOffset,wrapLimit) 中 interactionMode 为 UI=0、SceneObjects=1、Both=2,theme 为 Light=0、Dark=1。addTooltip()、removeTooltip() 管理 profile;enter(source,id,now) 做 Pcg 的首个 substring 匹配,enterText(source,content,header,now) 对应 Trigger 自带文本,exit(source)、hide() 和 tick(now,mouseX,mouseY,screenWidth,screenHeight,anyInput) 管理延迟、位置与隐藏。

状态通过 getVisible()、getPending()、getHeader()、getText()、getHeaderVisible()、getWrapEnabled()、getPivotX()、getPivotY()、getBackgroundR()、getForeground()、getCount() 读取。

PcgControllerSelection

eve.PcgControllerSelection() 对应 UIControllerSelection。用 add(name,controllerType,widgetId) 按 Inspector 顺序登记说明项,refresh(currentController) 选择第一个类型匹配项并生成互斥可见快照;没有匹配时返回成功但 value=false,保留之前状态。通过 getCount()、getSelectedIndex()、getVisible(index)、getWidgetId(index) 读取,再用 ui.setVisible 应用到稳定 widget id。

PcgPhotoModeRuntimeUI

eve.PcgPhotoModeRuntimeUI() 对应 PhotoModeUtils 与 PhotoModeUIHelper 的运行时控件状态。configure(kind,name,value,current,min,max,imageFound) 的 kind 从 Field=0 到 Vector3=13。各 get*Visible() 返回原 prefab 的互斥可见快照,getInitialCallbackRevision() 记录配置后主动派发初值的次数。

PcgLoadingScreen

eve.PcgLoadingScreen() 接收 TerrainLoader 的四类加载事件。configure(fadeOutSpeed) 设置背景每秒 衰减的 alpha;onLoadProgressStarted() 显示画布并把背景和进度复位, onLoadProgressUpdated(progress) 更新进度且在值达到 1 时自动执行 onLoadProgressEnded()。 结束会隐藏进度条和文本并开始淡出,调用方每帧用 tick(deltaTime) 推进;alpha 到零后画布关闭。

超时诊断使用 beginTimeout() 开始事务,addMissingScene(terrainName,impostorName) 返回索引,随后用 addRegularReference(index,name) 和 addImpostorReference(index,name) 收集仍引用该场景的对象,最后 endTimeout() 原子发布与 Pcg 相同结构的诊断并开始淡出。getCanvasVisible、getProgressVisible、 getTextVisible、getFading、getProgress、getBackgroundAlpha 和 getTimeoutMessage 返回 UI 快照。

markSliderUsed()、setSliderValue()、applyFloatInput() 保留滑杆与文本框同步规则;updateWrap(text) 复现 68 字符 metrics 换行,包括无空格硬切时跳过切点字符的原始行为。

状态查询包括 getKind()、getName()、getValueText()、getValue()、getMinimum()、getMaximum()、 getWholeNumbers()、getUsingSlider()、getInputRefresh()、getLabelVisible()、 getSecondLabelVisible()、getSliderVisible()、getInputVisible()、getToggleVisible()、 getButtonVisible()、getDropdownVisible()、getImageVisible()、getHeaderVisible()、getColorVisible()、 getVector2Visible()、getVector3Visible() 和 getInitialCallbackRevision()。

PcgPhotoModeValues

eve.PcgPhotoModeValues() 完整保存 Pcg PhotoModeValues 的 102 个字段及原始默认值。字段名沿用 m_* 配置名;getFieldCount/getFieldName/getFieldType 提供稳定 schema,类型编号为 Bool=0、Int=1、Float=2、String=3、Color=4。使用对应 setBool/setInt/setFloat/setString/setColor 与 getBool/getInt/getFloat/getString;颜色先 selectColor(name),再读取 RGBA。resetDefaults() 原子恢复原始配置。

snapshotJson() 输出 eve.ui.pcg-photo-mode-values schema version 1,并包含全部 102 字段;restoreJson(json) 要求精确 schema、版本和字段集合,拒绝未知或缺失字段,解析到临时候选对象后一次性交换,因此任何错误都保留当前配置。

PcgPhotoModeSession

eve.PcgPhotoModeSession() 对应 PhotoMode.cs 的进入、加载和退出事务。begin(capturedJson,savedJson,loadSaved,savedEver,currentPipeline,savedPipeline,sceneName,lightingProfile) 保存进入前快照,仅在管线、场景和光照配置均匹配时加载保存值。getLoadDecision() 返回 Current=0、Saved=1、SaveCurrent=2。replaceWorkingJson() 原子更新运行值;end(resetOnDisable,applicationPlaying) 选择工作值或进入前快照作为 outputJson()。

退出后 getRemovePhotoCameraRequested() 和 getUnfreezePlayerRequested() 始终为真;只有两个退出参数均为真时 getRestoreRequested() 为真。调用者按这些请求恢复各权威模块并清理临时对象。

脚本方法:begin()、replaceWorkingJson()、end()、workingJson()、outputJson()、getActive()、getRestoreRequested()、getRemovePhotoCameraRequested()、getUnfreezePlayerRequested()、getLoadDecision()、getRevision()。

PcgPhotoModeRanges

eve.PcgPhotoModeRanges() 完整保存 PhotoModeMinAndMaxValues 的 57 组范围。getCount/getName 按原声明顺序枚举;select(name) 后用 getMinimum/getMaximum/getIntegral 读取。setRange() 原子替换自定义范围,整数范围要求整数端点;clamp(name,value) 返回钳制结果。m_densityVolumeFogDistance 的默认上限保留正无穷。

Pcg PhotoMode 跨域应用事务

PcgPhotoModeApplyPlan 比较两个完整 PcgPhotoModeValues 快照,按 Pcg 源字段声明顺序输出变更,并把字段路由到 Photo、System、Graphics、Camera、Streaming、Weather、Lighting、Water、PostFx、Terrain、Grass 或 Audio 权威域。executeRegistered() 通过 eve.photo-mode.apply capability 同步执行;未安装提供者会返回可观察的 Unsupported 错误。任一字段失败时,先前字段按逆序恢复,getRollbackComplete() 报告恢复是否完整。脚本可用 compileJson、命令枚举和执行状态来驱动照片模式。

脚本方法:compileJson、executeRegistered、getCommandCount、getCommandField、getCommandDomain、getAppliedCount、getRollbackComplete、getRevision。

照片模式注册执行器会把 m_globalVolume 通过现有 IAudioQuery 写入真实 Audio master volume。其他字段由实现 IPhotoModeFieldSink 的模块 listener 接收;每个字段必须恰有一个提供者,缺失或重复所有者都会使事务失败并触发逆序回滚。

Pcg Lighting profile 身份

PhotoMode 的 m_isUsingPcgLighting 与 m_selectedPcgLightingProfile 由 PcgPhotoModePhotoAuthority 作为持久化身份联合保存。lightingProfileMatches(currentProfile) 同时比较 profile 索引和 Pcg/custom 模式;PcgPhotoModeSession.begin() 也使用这两个条件与场景名、渲染管线共同决定是否允许加载旧设置。