diff --git a/packages/ui/src/UIRuntimeModule.ts b/packages/ui/src/UIRuntimeModule.ts index 9297b0d3..17fd4b9f 100644 --- a/packages/ui/src/UIRuntimeModule.ts +++ b/packages/ui/src/UIRuntimeModule.ts @@ -1,5 +1,4 @@ -import type { IScene } from '@esengine/ecs-framework'; -import { ComponentRegistry } from '@esengine/ecs-framework'; +import type { IScene, IComponentRegistry } from '@esengine/ecs-framework'; import type { IRuntimeModule, IRuntimePlugin, ModuleManifest, SystemContext } from '@esengine/engine-core'; import { EngineBridgeToken } from '@esengine/ecs-engine-bindgen'; @@ -14,10 +13,14 @@ import { UISliderComponent, UIScrollViewComponent } from './components'; +import { TextBlinkComponent } from './components/TextBlinkComponent'; +import { SceneLoadTriggerComponent } from './components/SceneLoadTriggerComponent'; import { UILayoutSystem } from './systems/UILayoutSystem'; import { UIInputSystem } from './systems/UIInputSystem'; import { UIAnimationSystem } from './systems/UIAnimationSystem'; import { UIRenderDataProvider } from './systems/UIRenderDataProvider'; +import { TextBlinkSystem } from './systems/TextBlinkSystem'; +import { SceneLoadTriggerSystem } from './systems/SceneLoadTriggerSystem'; import { UIRenderBeginSystem, UIRectRenderSystem, @@ -43,7 +46,7 @@ export { } from './tokens'; class UIRuntimeModule implements IRuntimeModule { - registerComponents(registry: typeof ComponentRegistry): void { + registerComponents(registry: IComponentRegistry): void { registry.register(UITransformComponent); registry.register(UIRenderComponent); registry.register(UIInteractableComponent); @@ -53,6 +56,8 @@ class UIRuntimeModule implements IRuntimeModule { registry.register(UIProgressBarComponent); registry.register(UISliderComponent); registry.register(UIScrollViewComponent); + registry.register(TextBlinkComponent); + registry.register(SceneLoadTriggerComponent); } createSystems(scene: IScene, context: SystemContext): void { @@ -65,6 +70,14 @@ class UIRuntimeModule implements IRuntimeModule { const animationSystem = new UIAnimationSystem(); scene.addSystem(animationSystem); + // 文本闪烁系统 | Text blink system + const textBlinkSystem = new TextBlinkSystem(); + scene.addSystem(textBlinkSystem); + + // 场景加载触发系统 | Scene load trigger system + const sceneLoadTriggerSystem = new SceneLoadTriggerSystem(); + scene.addSystem(sceneLoadTriggerSystem); + const renderBeginSystem = new UIRenderBeginSystem(); scene.addSystem(renderBeginSystem); diff --git a/packages/ui/src/components/SceneLoadTriggerComponent.ts b/packages/ui/src/components/SceneLoadTriggerComponent.ts new file mode 100644 index 00000000..309a798e --- /dev/null +++ b/packages/ui/src/components/SceneLoadTriggerComponent.ts @@ -0,0 +1,61 @@ +/** + * 场景加载触发组件 + * Scene Load Trigger Component + * + * 配合 UIInteractable 使用,点击时自动加载指定场景。 + * Works with UIInteractable to automatically load scene on click. + */ + +import { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework'; + +/** + * 场景加载触发组件 + * Scene Load Trigger Component + * + * 添加到带有 UIInteractable 的实体上,点击时会加载 targetScene 指定的场景。 + * Add to entity with UIInteractable, loads targetScene on click. + * + * @example + * ```json + * { + * "type": "SceneLoadTrigger", + * "data": { + * "targetScene": "GameScene", + * "enabled": true + * } + * } + * ``` + */ +@ECSComponent('SceneLoadTrigger') +@Serializable({ version: 1, typeId: 'SceneLoadTrigger' }) +export class SceneLoadTriggerComponent extends Component { + /** + * 目标场景名称 + * Target scene name to load on click + */ + @Serialize() + @Property({ type: 'string', label: 'Target Scene' }) + public targetScene: string = ''; + + /** + * 是否启用 + * Whether the trigger is enabled + */ + @Serialize() + @Property({ type: 'boolean', label: 'Enabled' }) + public enabled: boolean = true; + + /** + * 点击后是否禁用(防止重复点击) + * Disable after click (prevent double clicks) + */ + @Serialize() + @Property({ type: 'boolean', label: 'Disable On Click' }) + public disableOnClick: boolean = true; + + /** + * 内部标记:回调是否已绑定 + * Internal flag: whether callback is bound + */ + public _callbackBound: boolean = false; +} diff --git a/packages/ui/src/components/TextBlinkComponent.ts b/packages/ui/src/components/TextBlinkComponent.ts new file mode 100644 index 00000000..bc8179ef --- /dev/null +++ b/packages/ui/src/components/TextBlinkComponent.ts @@ -0,0 +1,101 @@ +/** + * 文本闪烁组件 + * Text Blink Component + * + * 让文本产生闪烁效果,类似 Unity 的 Animation 实现 + * Creates a blinking effect for text, similar to Unity's Animation implementation + */ + +import { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework'; + +/** + * 文本闪烁组件 + * Text Blink Component + */ +@ECSComponent('TextBlink') +@Serializable({ version: 1, typeId: 'TextBlink' }) +export class TextBlinkComponent extends Component { + /** + * 闪烁速度(周期/秒) + * Blink speed (cycles per second) + */ + @Serialize() + @Property({ type: 'number', label: 'Speed', min: 0.1, max: 10, step: 0.1 }) + public speed: number = 1.5; + + /** + * 最小透明度 + * Minimum alpha + */ + @Serialize() + @Property({ type: 'number', label: 'Min Alpha', min: 0, max: 1, step: 0.05 }) + public minAlpha: number = 0.3; + + /** + * 最大透明度 + * Maximum alpha + */ + @Serialize() + @Property({ type: 'number', label: 'Max Alpha', min: 0, max: 1, step: 0.05 }) + public maxAlpha: number = 1.0; + + /** + * 是否启用闪烁 + * Whether blinking is enabled + */ + @Serialize() + @Property({ type: 'boolean', label: 'Enabled' }) + public blinkEnabled: boolean = true; + + // ============= 运行时状态(不序列化)| Runtime state (not serialized) ============= + + /** 当前时间 | Current time */ + private _time: number = 0; + + /** + * 获取当前时间 + * Get current time + */ + public get time(): number { + return this._time; + } + + /** + * 更新时间 + * Update time + */ + public addTime(deltaTime: number): void { + this._time += deltaTime; + } + + /** + * 计算当前 alpha 值 + * Calculate current alpha value + * + * 使用正弦波实现平滑的闪烁效果 + * Uses sine wave for smooth blinking effect + */ + public calculateAlpha(): number { + if (!this.blinkEnabled) { + return this.maxAlpha; + } + + // 使用正弦波:sin 从 -1 到 1,映射到 minAlpha 到 maxAlpha + // Using sine wave: sin from -1 to 1, mapped to minAlpha to maxAlpha + const t = Math.sin(this._time * this.speed * Math.PI * 2); + const normalized = (t + 1) / 2; // 0 到 1 + return this.minAlpha + normalized * (this.maxAlpha - this.minAlpha); + } + + /** + * 重置状态 + * Reset state + */ + public reset(): void { + this._time = 0; + } + + override onRemovedFromEntity(): void { + this.reset(); + } +} diff --git a/packages/ui/src/components/UIRenderComponent.ts b/packages/ui/src/components/UIRenderComponent.ts index e9273c7c..3f3e1a2c 100644 --- a/packages/ui/src/components/UIRenderComponent.ts +++ b/packages/ui/src/components/UIRenderComponent.ts @@ -120,9 +120,36 @@ export class UIRenderComponent extends Component { /** * 九宫格边距 [top, right, bottom, left] * Nine-patch margins + * + * Defines the non-stretchable borders for nine-patch rendering. + * 定义九宫格渲染时不可拉伸的边框区域。 */ + @Serialize() + @Property({ type: 'vector4', label: 'Nine-Patch Margins' }) public ninePatchMargins: [number, number, number, number] = [0, 0, 0, 0]; + /** + * 源纹理宽度(像素) + * Source texture width in pixels + * + * Required for nine-patch UV calculations. + * 九宫格 UV 计算所需。 + */ + @Serialize() + @Property({ type: 'number', label: 'Texture Width', min: 1 }) + public textureWidth: number = 0; + + /** + * 源纹理高度(像素) + * Source texture height in pixels + * + * Required for nine-patch UV calculations. + * 九宫格 UV 计算所需。 + */ + @Serialize() + @Property({ type: 'number', label: 'Texture Height', min: 1 }) + public textureHeight: number = 0; + // ===== 边框 Border ===== /** diff --git a/packages/ui/src/components/UITransformComponent.ts b/packages/ui/src/components/UITransformComponent.ts index 78d719dc..31d30fd3 100644 --- a/packages/ui/src/components/UITransformComponent.ts +++ b/packages/ui/src/components/UITransformComponent.ts @@ -275,6 +275,15 @@ export class UITransformComponent extends Component implements ISortable { */ public worldScaleY: number = 1; + /** + * 计算后的世界层内顺序(考虑父元素和层级深度) + * Computed world order in layer (considering parent and hierarchy depth) + * + * 子元素总是渲染在父元素之上:worldOrderInLayer = parentWorldOrder + depth * 1000 + localOrder + * Children always render on top of parents: worldOrderInLayer = parentWorldOrder + depth * 1000 + localOrder + */ + public worldOrderInLayer: number = 0; + /** * 本地到世界的 2D 变换矩阵(只读,由 UILayoutSystem 计算) * Local to world 2D transformation matrix (readonly, computed by UILayoutSystem) diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 48cdcd5a..54cbb4c5 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -88,6 +88,9 @@ export { type UIFontWeight } from './components/UITextComponent'; +export { TextBlinkComponent } from './components/TextBlinkComponent'; +export { SceneLoadTriggerComponent } from './components/SceneLoadTriggerComponent'; + export { UILayoutComponent, UILayoutType, @@ -124,6 +127,8 @@ export { UILayoutSystem } from './systems/UILayoutSystem'; export { UIInputSystem, type UIInputEvent } from './systems/UIInputSystem'; export { UIAnimationSystem, UIEasing, type EasingFunction, type EasingName } from './systems/UIAnimationSystem'; export { UIRenderDataProvider, type IUIRenderDataProvider } from './systems/UIRenderDataProvider'; +export { TextBlinkSystem } from './systems/TextBlinkSystem'; +export { SceneLoadTriggerSystem } from './systems/SceneLoadTriggerSystem'; // Systems - Render (ECS-compliant render systems) export { diff --git a/packages/ui/src/systems/SceneLoadTriggerSystem.ts b/packages/ui/src/systems/SceneLoadTriggerSystem.ts new file mode 100644 index 00000000..c712c77d --- /dev/null +++ b/packages/ui/src/systems/SceneLoadTriggerSystem.ts @@ -0,0 +1,162 @@ +/** + * 场景加载触发系统 + * Scene Load Trigger System + * + * 处理 SceneLoadTriggerComponent,绑定 UIInteractable 点击事件到场景加载。 + * Processes SceneLoadTriggerComponent, binds UIInteractable click to scene loading. + */ + +import { Entity, EntitySystem, Matcher, ECSSystem, Core } from '@esengine/ecs-framework'; +import { SceneLoadTriggerComponent } from '../components/SceneLoadTriggerComponent'; +import { UIInteractableComponent } from '../components/UIInteractableComponent'; + +/** + * 场景加载函数类型(与 RuntimeSceneManager.loadScene 兼容) + * Scene load function type (compatible with RuntimeSceneManager.loadScene) + */ +type SceneLoadFunction = (sceneName: string) => Promise; + +/** + * 场景管理器接口(最小化,避免循环依赖) + * Scene manager interface (minimal, avoids circular dependency) + * + * 包含 IService 的 dispose 方法以兼容 ServiceContainer。 + * Includes IService's dispose method for ServiceContainer compatibility. + */ +interface ISceneManager { + loadScene(sceneName: string): Promise; + dispose(): void; +} + +/** + * 全局场景管理器服务键 + * Global scene manager service key + * + * 使用 Symbol.for 确保与 BrowserRuntime 中注册的键一致。 + * Uses Symbol.for to match the key registered in BrowserRuntime. + */ +const GlobalSceneManagerKey = Symbol.for('@esengine/service:runtimeSceneManager'); + +/** + * 场景加载触发系统 + * Scene Load Trigger System + * + * 自动将 SceneLoadTriggerComponent 的配置连接到 UIInteractable 的点击事件。 + * Automatically connects SceneLoadTriggerComponent config to UIInteractable click events. + */ +@ECSSystem('SceneLoadTrigger') +export class SceneLoadTriggerSystem extends EntitySystem { + private _sceneLoader: SceneLoadFunction | null = null; + + constructor() { + super(Matcher.empty().all(SceneLoadTriggerComponent, UIInteractableComponent)); + } + + /** + * 设置场景加载函数 + * Set scene load function + * + * 可以直接设置函数,或者系统会尝试从服务注册表获取 RuntimeSceneManager。 + * Can set function directly, or system will try to get RuntimeSceneManager from service registry. + */ + public setSceneLoader(loader: SceneLoadFunction): void { + this._sceneLoader = loader; + } + + protected override process(entities: readonly Entity[]): void { + // 如果没有设置场景加载器,尝试从服务注册表获取 + // If no scene loader set, try to get from service registry + if (!this._sceneLoader) { + this._tryGetSceneManager(); + } + + for (const entity of entities) { + const trigger = entity.getComponent(SceneLoadTriggerComponent); + const interactable = entity.getComponent(UIInteractableComponent); + + if (!trigger || !interactable) continue; + if (!trigger.enabled || !trigger.targetScene) continue; + + // 只绑定一次回调 + // Only bind callback once + if (trigger._callbackBound) continue; + + this._bindClickHandler(entity, trigger, interactable); + } + } + + /** + * 尝试从全局服务获取场景管理器 + * Try to get scene manager from global services + */ + private _tryGetSceneManager(): void { + try { + // 从 Core.services 获取场景管理器 + // Get scene manager from Core.services + // RuntimeSceneManager 实现了 IService 接口 + // RuntimeSceneManager implements IService interface + const sceneManager = Core.services.tryResolve(GlobalSceneManagerKey); + if (sceneManager?.loadScene) { + this._sceneLoader = (sceneName: string) => sceneManager.loadScene(sceneName); + } + } catch (e) { + // 忽略错误,保持 _sceneLoader 为 null + // Ignore error, keep _sceneLoader as null + } + } + + /** + * 绑定点击处理器 + * Bind click handler + */ + private _bindClickHandler( + entity: Entity, + trigger: SceneLoadTriggerComponent, + interactable: UIInteractableComponent + ): void { + const targetScene = trigger.targetScene; + + // 保存原有的 onClick(如果有) + // Save original onClick (if any) + const originalOnClick = interactable.onClick; + + interactable.onClick = () => { + // 调用原有回调 + // Call original callback + originalOnClick?.(); + + // 检查是否启用 + // Check if enabled + if (!trigger.enabled) return; + + // 禁用(防止重复点击) + // Disable (prevent double clicks) + if (trigger.disableOnClick) { + trigger.enabled = false; + } + + // 尝试获取场景加载器(可能在回调绑定后才注册) + // Try to get scene loader (may be registered after callback binding) + if (!this._sceneLoader) { + this._tryGetSceneManager(); + } + + // 加载场景 + // Load scene + if (this._sceneLoader) { + this._sceneLoader(targetScene).catch((error) => { + console.error(`[SceneLoadTriggerSystem] Failed to load scene "${targetScene}":`, error); + // 恢复启用状态 + // Restore enabled state + if (trigger.disableOnClick) { + trigger.enabled = true; + } + }); + } + // 静默处理:编辑器预览模式下场景切换不可用 + // Silent handling: scene switching not available in editor preview mode + }; + + trigger._callbackBound = true; + } +} diff --git a/packages/ui/src/systems/TextBlinkSystem.ts b/packages/ui/src/systems/TextBlinkSystem.ts new file mode 100644 index 00000000..e3316f5c --- /dev/null +++ b/packages/ui/src/systems/TextBlinkSystem.ts @@ -0,0 +1,37 @@ +/** + * 文本闪烁系统 - 实现 UI 元素的透明度脉冲动画 + * + * Text Blink System - Implements alpha pulse animation for UI elements + */ + +import { Entity, EntitySystem, Matcher, Time } from '@esengine/ecs-framework'; +import { TextBlinkComponent } from '../components/TextBlinkComponent'; +import { UITransformComponent } from '../components/UITransformComponent'; + +/** + * 处理 TextBlinkComponent,驱动 UI 元素的透明度动画。 + * 常用于 "TAP TO START" 等需要吸引注意力的文本效果。 + * + * Processes TextBlinkComponent to drive UI element alpha animation. + * Commonly used for attention-grabbing text effects like "TAP TO START". + */ +export class TextBlinkSystem extends EntitySystem { + constructor() { + super(Matcher.empty().all(TextBlinkComponent, UITransformComponent)); + } + + protected override process(entities: readonly Entity[]): void { + const deltaTime = Time.deltaTime; + + for (const entity of entities) { + if (!entity.enabled) continue; + + const blink = entity.getComponent(TextBlinkComponent); + const uiTransform = entity.getComponent(UITransformComponent); + if (!blink || !uiTransform) continue; + + blink.addTime(deltaTime); + uiTransform.alpha = blink.calculateAlpha(); + } + } +} diff --git a/packages/ui/src/systems/UILayoutSystem.ts b/packages/ui/src/systems/UILayoutSystem.ts index 3b94cb39..6af85862 100644 --- a/packages/ui/src/systems/UILayoutSystem.ts +++ b/packages/ui/src/systems/UILayoutSystem.ts @@ -96,7 +96,7 @@ export class UILayoutSystem extends EntitySystem { const identityMatrix: Matrix2D = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 }; for (const entity of rootEntities) { - this.layoutEntity(entity, parentX, parentY, this.canvasWidth, this.canvasHeight, 1, identityMatrix); + this.layoutEntity(entity, parentX, parentY, this.canvasWidth, this.canvasHeight, 1, identityMatrix, true, 0); } } @@ -112,7 +112,8 @@ export class UILayoutSystem extends EntitySystem { parentHeight: number, parentAlpha: number, parentMatrix: Matrix2D, - parentVisible: boolean = true + parentVisible: boolean = true, + depth: number = 0 ): void { const transform = entity.getComponent(UITransformComponent); if (!transform) return; @@ -199,6 +200,12 @@ export class UILayoutSystem extends EntitySystem { // Calculate world visibility (if parent is invisible, children are also invisible) transform.worldVisible = parentVisible && transform.visible; + // 计算世界层内顺序(子元素总是渲染在父元素之上) + // Calculate world order in layer (children always render on top of parents) + // 公式:depth * 1000 + localOrderInLayer + // Formula: depth * 1000 + localOrderInLayer + transform.worldOrderInLayer = depth * 1000 + transform.orderInLayer; + // 使用矩阵乘法计算世界变换 this.updateWorldMatrix(transform, parentMatrix); @@ -215,7 +222,7 @@ export class UILayoutSystem extends EntitySystem { // 检查是否有布局组件 const layout = entity.getComponent(UILayoutComponent); if (layout && layout.type !== UILayoutType.None) { - this.layoutChildren(layout, transform, children); + this.layoutChildren(layout, transform, children, depth + 1); } else { // 无布局组件,直接递归处理子元素 for (const child of children) { @@ -227,7 +234,8 @@ export class UILayoutSystem extends EntitySystem { height, transform.worldAlpha, transform.localToWorldMatrix, - transform.worldVisible + transform.worldVisible, + depth + 1 ); } } @@ -240,7 +248,8 @@ export class UILayoutSystem extends EntitySystem { private layoutChildren( layout: UILayoutComponent, parentTransform: UITransformComponent, - children: Entity[] + children: Entity[], + depth: number ): void { const contentStartX = parentTransform.worldX + layout.paddingLeft; // Y-up 系统:worldY 是底部,顶部 = worldY + height @@ -252,13 +261,13 @@ export class UILayoutSystem extends EntitySystem { switch (layout.type) { case UILayoutType.Horizontal: - this.layoutHorizontal(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight); + this.layoutHorizontal(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight, depth); break; case UILayoutType.Vertical: - this.layoutVertical(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight); + this.layoutVertical(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight, depth); break; case UILayoutType.Grid: - this.layoutGrid(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight); + this.layoutGrid(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight, depth); break; default: // 默认按正常方式递归(传递顶部 Y) @@ -270,7 +279,9 @@ export class UILayoutSystem extends EntitySystem { parentTransform.computedWidth, parentTransform.computedHeight, parentTransform.worldAlpha, - parentTransform.localToWorldMatrix + parentTransform.localToWorldMatrix, + parentTransform.worldVisible, + depth ); } } @@ -287,7 +298,8 @@ export class UILayoutSystem extends EntitySystem { startX: number, startY: number, contentWidth: number, - contentHeight: number + contentHeight: number, + depth: number ): void { // 计算总子元素宽度 const childSizes = children.map(child => { @@ -366,12 +378,14 @@ export class UILayoutSystem extends EntitySystem { childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha; // 传播世界可见性 | Propagate world visibility childTransform.worldVisible = parentTransform.worldVisible && childTransform.visible; + // 计算世界层内顺序 | Calculate world order in layer + childTransform.worldOrderInLayer = depth * 1000 + childTransform.orderInLayer; // 使用矩阵乘法计算世界旋转和缩放 this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix); childTransform.layoutDirty = false; // 递归处理子元素的子元素 - this.processChildrenRecursive(child, childTransform); + this.processChildrenRecursive(child, childTransform, depth); offsetX += size.width + gap; } @@ -389,7 +403,8 @@ export class UILayoutSystem extends EntitySystem { startX: number, startY: number, contentWidth: number, - contentHeight: number + contentHeight: number, + depth: number ): void { // 计算总子元素高度 const childSizes = children.map(child => { @@ -466,11 +481,13 @@ export class UILayoutSystem extends EntitySystem { childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha; // 传播世界可见性 | Propagate world visibility childTransform.worldVisible = parentTransform.worldVisible && childTransform.visible; + // 计算世界层内顺序 | Calculate world order in layer + childTransform.worldOrderInLayer = depth * 1000 + childTransform.orderInLayer; // 使用矩阵乘法计算世界旋转和缩放 this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix); childTransform.layoutDirty = false; - this.processChildrenRecursive(child, childTransform); + this.processChildrenRecursive(child, childTransform, depth); // 移动到下一个元素的顶部位置(向下 = Y 减小) currentTopY -= size.height + gap; @@ -489,7 +506,8 @@ export class UILayoutSystem extends EntitySystem { startX: number, startY: number, contentWidth: number, - _contentHeight: number + _contentHeight: number, + depth: number ): void { const columns = layout.columns; const gapX = layout.getHorizontalGap(); @@ -524,11 +542,13 @@ export class UILayoutSystem extends EntitySystem { childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha; // 传播世界可见性 | Propagate world visibility childTransform.worldVisible = parentTransform.worldVisible && childTransform.visible; + // 计算世界层内顺序 | Calculate world order in layer + childTransform.worldOrderInLayer = depth * 1000 + childTransform.orderInLayer; // 使用矩阵乘法计算世界旋转和缩放 this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix); childTransform.layoutDirty = false; - this.processChildrenRecursive(child, childTransform); + this.processChildrenRecursive(child, childTransform, depth); } } @@ -565,7 +585,7 @@ export class UILayoutSystem extends EntitySystem { * 递归处理子元素 * Recursively process children */ - private processChildrenRecursive(entity: Entity, parentTransform: UITransformComponent): void { + private processChildrenRecursive(entity: Entity, parentTransform: UITransformComponent, depth: number): void { const children = this.getUIChildren(entity); if (children.length === 0) return; @@ -574,7 +594,7 @@ export class UILayoutSystem extends EntitySystem { const layout = entity.getComponent(UILayoutComponent); if (layout && layout.type !== UILayoutType.None) { - this.layoutChildren(layout, parentTransform, children); + this.layoutChildren(layout, parentTransform, children, depth + 1); } else { for (const child of children) { this.layoutEntity( @@ -585,7 +605,8 @@ export class UILayoutSystem extends EntitySystem { parentTransform.computedHeight, parentTransform.worldAlpha, parentTransform.localToWorldMatrix, - parentTransform.worldVisible + parentTransform.worldVisible, + depth + 1 ); } } diff --git a/packages/ui/src/systems/render/UIButtonRenderSystem.ts b/packages/ui/src/systems/render/UIButtonRenderSystem.ts index ce85e2b5..05b511f9 100644 --- a/packages/ui/src/systems/render/UIButtonRenderSystem.ts +++ b/packages/ui/src/systems/render/UIButtonRenderSystem.ts @@ -58,9 +58,9 @@ export class UIButtonRenderSystem extends EntitySystem { const width = (transform.computedWidth ?? transform.width) * scaleX; const height = (transform.computedHeight ?? transform.height) * scaleY; const alpha = transform.worldAlpha ?? transform.alpha; - // 使用排序层和层内顺序 | Use sorting layer and order in layer + // 使用排序层和世界层内顺序 | Use sorting layer and world order in layer const sortingLayer = transform.sortingLayer; - const orderInLayer = transform.orderInLayer; + const orderInLayer = transform.worldOrderInLayer; // 使用 transform 的 pivot 作为旋转/缩放中心 const pivotX = transform.pivotX; const pivotY = transform.pivotY; diff --git a/packages/ui/src/systems/render/UIProgressBarRenderSystem.ts b/packages/ui/src/systems/render/UIProgressBarRenderSystem.ts index 0dbad655..45ee5b2f 100644 --- a/packages/ui/src/systems/render/UIProgressBarRenderSystem.ts +++ b/packages/ui/src/systems/render/UIProgressBarRenderSystem.ts @@ -55,9 +55,9 @@ export class UIProgressBarRenderSystem extends EntitySystem { const width = (transform.computedWidth ?? transform.width) * scaleX; const height = (transform.computedHeight ?? transform.height) * scaleY; const alpha = transform.worldAlpha ?? transform.alpha; - // 使用排序层和层内顺序 | Use sorting layer and order in layer + // 使用排序层和世界层内顺序 | Use sorting layer and world order in layer const sortingLayer = transform.sortingLayer; - const orderInLayer = transform.orderInLayer; + const orderInLayer = transform.worldOrderInLayer; // 使用 transform 的 pivot 作为旋转/缩放中心 const pivotX = transform.pivotX; const pivotY = transform.pivotY; diff --git a/packages/ui/src/systems/render/UIRectRenderSystem.ts b/packages/ui/src/systems/render/UIRectRenderSystem.ts index 7f2868e3..e52c30c6 100644 --- a/packages/ui/src/systems/render/UIRectRenderSystem.ts +++ b/packages/ui/src/systems/render/UIRectRenderSystem.ts @@ -10,7 +10,7 @@ import { EntitySystem, Matcher, Entity, ECSSystem } from '@esengine/ecs-framework'; import { UITransformComponent } from '../../components/UITransformComponent'; -import { UIRenderComponent } from '../../components/UIRenderComponent'; +import { UIRenderComponent, UIRenderType } from '../../components/UIRenderComponent'; import { UIButtonComponent } from '../../components/widgets/UIButtonComponent'; import { UIProgressBarComponent } from '../../components/widgets/UIProgressBarComponent'; import { UISliderComponent } from '../../components/widgets/UISliderComponent'; @@ -68,9 +68,11 @@ export class UIRectRenderSystem extends EntitySystem { const alpha = transform.worldAlpha ?? transform.alpha; // 使用世界旋转(考虑父级旋转) const rotation = transform.worldRotation ?? transform.rotation; - // 使用排序层和层内顺序 | Use sorting layer and order in layer + // 使用排序层和世界层内顺序 | Use sorting layer and world order in layer const sortingLayer = transform.sortingLayer; - const orderInLayer = transform.orderInLayer; + // worldOrderInLayer 考虑了父子层级关系,确保子元素渲染在父元素之上 + // worldOrderInLayer considers parent-child hierarchy, ensuring children render on top of parents + const orderInLayer = transform.worldOrderInLayer; // 使用 transform 的 pivot 作为旋转/缩放中心 const pivotX = transform.pivotX; const pivotY = transform.pivotY; @@ -107,24 +109,56 @@ export class UIRectRenderSystem extends EntitySystem { const textureGuid = typeof render.textureGuid === 'string' ? render.textureGuid : undefined; const textureId = typeof render.textureGuid === 'number' ? render.textureGuid : undefined; - collector.addRect( - renderX, renderY, - width, height, - render.textureTint, - alpha, - sortingLayer, - orderInLayer, - { - rotation, - pivotX, - pivotY, - textureId, - textureGuid, - uv: render.textureUV - ? [render.textureUV.u0, render.textureUV.v0, render.textureUV.u1, render.textureUV.v1] - : undefined - } - ); + + // Handle nine-patch rendering + // 处理九宫格渲染 + if (render.type === UIRenderType.NinePatch && + render.textureWidth > 0 && + render.textureHeight > 0) { + // addNinePatch expects top-left corner coordinates + // Y-up coordinate system: top = bottom + height + // addNinePatch 期望左上角坐标 + // Y轴向上坐标系:顶部 = 底部 + 高度 + const topLeftX = x; + const topLeftY = y + height; + collector.addNinePatch( + topLeftX, topLeftY, + width, height, + render.ninePatchMargins, + render.textureWidth, + render.textureHeight, + render.textureTint, + alpha, + sortingLayer, + orderInLayer, + { + rotation, + textureId, + textureGuid + } + ); + } else { + // Standard image rendering + // 标准图像渲染 + collector.addRect( + renderX, renderY, + width, height, + render.textureTint, + alpha, + sortingLayer, + orderInLayer, + { + rotation, + pivotX, + pivotY, + textureId, + textureGuid, + uv: render.textureUV + ? [render.textureUV.u0, render.textureUV.v0, render.textureUV.u1, render.textureUV.v1] + : undefined + } + ); + } } // Render background color if fill is enabled // 如果启用填充,渲染背景颜色 diff --git a/packages/ui/src/systems/render/UIRenderCollector.ts b/packages/ui/src/systems/render/UIRenderCollector.ts index 9183aa8b..bad23ce5 100644 --- a/packages/ui/src/systems/render/UIRenderCollector.ts +++ b/packages/ui/src/systems/render/UIRenderCollector.ts @@ -56,7 +56,7 @@ export interface UIRenderPrimitive { rotation: number; /** Pivot/Origin X (0-1, 0=left, 0.5=center, 1=right) | 锚点 X (0-1, 0=左, 0.5=中心, 1=右) */ pivotX: number; - /** Pivot/Origin Y (0-1, 0=top, 0.5=center, 1=bottom) | 锚点 Y (0-1, 0=上, 0.5=中心, 1=下) */ + /** Pivot/Origin Y (0-1, 0=bottom, 0.5=center, 1=top) in Y-up system | 锚点 Y (0-1, 0=下, 0.5=中心, 1=上) Y轴向上坐标系 */ pivotY: number; /** Packed color (0xAABBGGRR) | 打包颜色 */ color: number; @@ -171,6 +171,136 @@ export class UIRenderCollector { this.cache = null; } + /** + * Add a nine-patch (9-slice) primitive + * 添加九宫格原语 + * + * Nine-patch divides the texture into 9 regions: + * - Corners: Keep original size + * - Edges: Stretch in one direction + * - Center: Stretches in both directions + * + * 九宫格将纹理分为 9 个区域: + * - 角落:保持原始尺寸 + * - 边缘:单向拉伸 + * - 中心:双向拉伸 + * + * @param x - X position | X 坐标 + * @param y - Y position | Y 坐标 + * @param width - Target width | 目标宽度 + * @param height - Target height | 目标高度 + * @param margins - Nine-patch margins [top, right, bottom, left] | 九宫格边距 + * @param textureWidth - Source texture width | 源纹理宽度 + * @param textureHeight - Source texture height | 源纹理高度 + * @param color - Tint color | 着色颜色 + * @param alpha - Alpha value | 透明度 + * @param sortingLayer - Sorting layer | 排序层 + * @param orderInLayer - Order in layer | 层内顺序 + * @param options - Additional options | 额外选项 + */ + addNinePatch( + x: number, + y: number, + width: number, + height: number, + margins: [number, number, number, number], + textureWidth: number, + textureHeight: number, + color: number, + alpha: number, + sortingLayer: string, + orderInLayer: number, + options?: { + rotation?: number; + textureId?: number; + textureGuid?: string; + } + ): void { + const [marginTop, marginRight, marginBottom, marginLeft] = margins; + + // Ensure minimum size to avoid negative dimensions + // 确保最小尺寸以避免负尺寸 + const minWidth = marginLeft + marginRight; + const minHeight = marginTop + marginBottom; + const targetWidth = Math.max(width, minWidth); + const targetHeight = Math.max(height, minHeight); + + // Calculate center dimensions + // 计算中心区域尺寸 + const centerWidth = targetWidth - marginLeft - marginRight; + const centerHeight = targetHeight - marginTop - marginBottom; + + // Source texture UV boundaries (normalized) + // 源纹理 UV 边界(归一化) + const uvLeft = marginLeft / textureWidth; + const uvRight = (textureWidth - marginRight) / textureWidth; + const uvTop = marginTop / textureHeight; + const uvBottom = (textureHeight - marginBottom) / textureHeight; + + // Common options for all patches + // 所有 patch 的公共选项 + // Note: pivotY=1 means position is top-left corner (Y-up coordinate system) + // 注意:pivotY=1 表示位置是左上角(Y轴向上坐标系) + const baseOptions = { + rotation: options?.rotation ?? 0, + pivotX: 0, + pivotY: 1, + textureId: options?.textureId, + textureGuid: options?.textureGuid + }; + + // Helper to add a patch with specific UVs + // 辅助函数:添加具有特定 UV 的 patch + const addPatch = ( + px: number, + py: number, + pw: number, + ph: number, + u0: number, + v0: number, + u1: number, + v1: number + ) => { + if (pw <= 0 || ph <= 0) return; + this.addRect(px, py, pw, ph, color, alpha, sortingLayer, orderInLayer, { + ...baseOptions, + uv: [u0, v0, u1, v1] + }); + }; + + // Y-up coordinate system: y decreases as we go down + // Y轴向上坐标系:向下移动时 y 减小 + // (x, y) is top-left corner, patches extend downward (negative y direction) + // (x, y) 是左上角,patch 向下延伸(y 减小方向) + + // Top-left corner | 左上角 + addPatch(x, y, marginLeft, marginTop, 0, 0, uvLeft, uvTop); + + // Top edge | 顶边 + addPatch(x + marginLeft, y, centerWidth, marginTop, uvLeft, 0, uvRight, uvTop); + + // Top-right corner | 右上角 + addPatch(x + marginLeft + centerWidth, y, marginRight, marginTop, uvRight, 0, 1, uvTop); + + // Left edge | 左边 (move down = subtract y) + addPatch(x, y - marginTop, marginLeft, centerHeight, 0, uvTop, uvLeft, uvBottom); + + // Center | 中心 + addPatch(x + marginLeft, y - marginTop, centerWidth, centerHeight, uvLeft, uvTop, uvRight, uvBottom); + + // Right edge | 右边 + addPatch(x + marginLeft + centerWidth, y - marginTop, marginRight, centerHeight, uvRight, uvTop, 1, uvBottom); + + // Bottom-left corner | 左下角 + addPatch(x, y - marginTop - centerHeight, marginLeft, marginBottom, 0, uvBottom, uvLeft, 1); + + // Bottom edge | 底边 + addPatch(x + marginLeft, y - marginTop - centerHeight, centerWidth, marginBottom, uvLeft, uvBottom, uvRight, 1); + + // Bottom-right corner | 右下角 + addPatch(x + marginLeft + centerWidth, y - marginTop - centerHeight, marginRight, marginBottom, uvRight, uvBottom, 1, 1); + } + /** * Get render data * 获取渲染数据 diff --git a/packages/ui/src/systems/render/UIScrollViewRenderSystem.ts b/packages/ui/src/systems/render/UIScrollViewRenderSystem.ts index db26c170..b28085dc 100644 --- a/packages/ui/src/systems/render/UIScrollViewRenderSystem.ts +++ b/packages/ui/src/systems/render/UIScrollViewRenderSystem.ts @@ -56,9 +56,9 @@ export class UIScrollViewRenderSystem extends EntitySystem { const width = (transform.computedWidth ?? transform.width) * scaleX; const height = (transform.computedHeight ?? transform.height) * scaleY; const alpha = transform.worldAlpha ?? transform.alpha; - // 使用排序层和层内顺序 | Use sorting layer and order in layer + // 使用排序层和世界层内顺序 | Use sorting layer and world order in layer const sortingLayer = transform.sortingLayer; - const orderInLayer = transform.orderInLayer; + const orderInLayer = transform.worldOrderInLayer; // 使用 transform 的 pivot 计算位置 const pivotX = transform.pivotX; const pivotY = transform.pivotY; diff --git a/packages/ui/src/systems/render/UISliderRenderSystem.ts b/packages/ui/src/systems/render/UISliderRenderSystem.ts index 09f93ed7..8dd2704d 100644 --- a/packages/ui/src/systems/render/UISliderRenderSystem.ts +++ b/packages/ui/src/systems/render/UISliderRenderSystem.ts @@ -55,9 +55,9 @@ export class UISliderRenderSystem extends EntitySystem { const width = (transform.computedWidth ?? transform.width) * scaleX; const height = (transform.computedHeight ?? transform.height) * scaleY; const alpha = transform.worldAlpha ?? transform.alpha; - // 使用排序层和层内顺序 | Use sorting layer and order in layer + // 使用排序层和世界层内顺序 | Use sorting layer and world order in layer const sortingLayer = transform.sortingLayer; - const orderInLayer = transform.orderInLayer; + const orderInLayer = transform.worldOrderInLayer; // 使用 transform 的 pivot 计算中心位置 const pivotX = transform.pivotX; const pivotY = transform.pivotY; diff --git a/packages/ui/src/systems/render/UITextRenderSystem.ts b/packages/ui/src/systems/render/UITextRenderSystem.ts index ce4fe8f5..77171b31 100644 --- a/packages/ui/src/systems/render/UITextRenderSystem.ts +++ b/packages/ui/src/systems/render/UITextRenderSystem.ts @@ -112,9 +112,9 @@ export class UITextRenderSystem extends EntitySystem { const width = (transform.computedWidth ?? transform.width) * scaleX; const height = (transform.computedHeight ?? transform.height) * scaleY; const alpha = transform.worldAlpha ?? transform.alpha; - // 使用排序层和层内顺序 | Use sorting layer and order in layer + // 使用排序层和世界层内顺序 | Use sorting layer and world order in layer const sortingLayer = transform.sortingLayer; - const orderInLayer = transform.orderInLayer; + const orderInLayer = transform.worldOrderInLayer; // 使用 transform 的 pivot 作为旋转/缩放中心 const pivotX = transform.pivotX; const pivotY = transform.pivotY;