feat(ui): 添加场景切换和文本闪烁组件

新增组件:
- SceneLoadTriggerComponent: 场景切换触发器
- TextBlinkComponent: 文本闪烁效果

新增系统:
- SceneLoadTriggerSystem: 处理场景切换逻辑
- TextBlinkSystem: 处理文本闪烁动画

其他改进:
- UIRuntimeModule 适配新组件注册接口
- UI 渲染系统优化
This commit is contained in:
yhh
2025-12-16 11:25:49 +08:00
parent 75be905f14
commit 7814b97ace
16 changed files with 653 additions and 53 deletions

View File

@@ -1,5 +1,4 @@
import type { IScene } from '@esengine/ecs-framework'; import type { IScene, IComponentRegistry } from '@esengine/ecs-framework';
import { ComponentRegistry } from '@esengine/ecs-framework';
import type { IRuntimeModule, IRuntimePlugin, ModuleManifest, SystemContext } from '@esengine/engine-core'; import type { IRuntimeModule, IRuntimePlugin, ModuleManifest, SystemContext } from '@esengine/engine-core';
import { EngineBridgeToken } from '@esengine/ecs-engine-bindgen'; import { EngineBridgeToken } from '@esengine/ecs-engine-bindgen';
@@ -14,10 +13,14 @@ import {
UISliderComponent, UISliderComponent,
UIScrollViewComponent UIScrollViewComponent
} from './components'; } from './components';
import { TextBlinkComponent } from './components/TextBlinkComponent';
import { SceneLoadTriggerComponent } from './components/SceneLoadTriggerComponent';
import { UILayoutSystem } from './systems/UILayoutSystem'; import { UILayoutSystem } from './systems/UILayoutSystem';
import { UIInputSystem } from './systems/UIInputSystem'; import { UIInputSystem } from './systems/UIInputSystem';
import { UIAnimationSystem } from './systems/UIAnimationSystem'; import { UIAnimationSystem } from './systems/UIAnimationSystem';
import { UIRenderDataProvider } from './systems/UIRenderDataProvider'; import { UIRenderDataProvider } from './systems/UIRenderDataProvider';
import { TextBlinkSystem } from './systems/TextBlinkSystem';
import { SceneLoadTriggerSystem } from './systems/SceneLoadTriggerSystem';
import { import {
UIRenderBeginSystem, UIRenderBeginSystem,
UIRectRenderSystem, UIRectRenderSystem,
@@ -43,7 +46,7 @@ export {
} from './tokens'; } from './tokens';
class UIRuntimeModule implements IRuntimeModule { class UIRuntimeModule implements IRuntimeModule {
registerComponents(registry: typeof ComponentRegistry): void { registerComponents(registry: IComponentRegistry): void {
registry.register(UITransformComponent); registry.register(UITransformComponent);
registry.register(UIRenderComponent); registry.register(UIRenderComponent);
registry.register(UIInteractableComponent); registry.register(UIInteractableComponent);
@@ -53,6 +56,8 @@ class UIRuntimeModule implements IRuntimeModule {
registry.register(UIProgressBarComponent); registry.register(UIProgressBarComponent);
registry.register(UISliderComponent); registry.register(UISliderComponent);
registry.register(UIScrollViewComponent); registry.register(UIScrollViewComponent);
registry.register(TextBlinkComponent);
registry.register(SceneLoadTriggerComponent);
} }
createSystems(scene: IScene, context: SystemContext): void { createSystems(scene: IScene, context: SystemContext): void {
@@ -65,6 +70,14 @@ class UIRuntimeModule implements IRuntimeModule {
const animationSystem = new UIAnimationSystem(); const animationSystem = new UIAnimationSystem();
scene.addSystem(animationSystem); 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(); const renderBeginSystem = new UIRenderBeginSystem();
scene.addSystem(renderBeginSystem); scene.addSystem(renderBeginSystem);

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -120,9 +120,36 @@ export class UIRenderComponent extends Component {
/** /**
* 九宫格边距 [top, right, bottom, left] * 九宫格边距 [top, right, bottom, left]
* Nine-patch margins * 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]; 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 ===== // ===== 边框 Border =====
/** /**

View File

@@ -275,6 +275,15 @@ export class UITransformComponent extends Component implements ISortable {
*/ */
public worldScaleY: number = 1; 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 计算) * 本地到世界的 2D 变换矩阵(只读,由 UILayoutSystem 计算)
* Local to world 2D transformation matrix (readonly, computed by UILayoutSystem) * Local to world 2D transformation matrix (readonly, computed by UILayoutSystem)

View File

@@ -88,6 +88,9 @@ export {
type UIFontWeight type UIFontWeight
} from './components/UITextComponent'; } from './components/UITextComponent';
export { TextBlinkComponent } from './components/TextBlinkComponent';
export { SceneLoadTriggerComponent } from './components/SceneLoadTriggerComponent';
export { export {
UILayoutComponent, UILayoutComponent,
UILayoutType, UILayoutType,
@@ -124,6 +127,8 @@ export { UILayoutSystem } from './systems/UILayoutSystem';
export { UIInputSystem, type UIInputEvent } from './systems/UIInputSystem'; export { UIInputSystem, type UIInputEvent } from './systems/UIInputSystem';
export { UIAnimationSystem, UIEasing, type EasingFunction, type EasingName } from './systems/UIAnimationSystem'; export { UIAnimationSystem, UIEasing, type EasingFunction, type EasingName } from './systems/UIAnimationSystem';
export { UIRenderDataProvider, type IUIRenderDataProvider } from './systems/UIRenderDataProvider'; export { UIRenderDataProvider, type IUIRenderDataProvider } from './systems/UIRenderDataProvider';
export { TextBlinkSystem } from './systems/TextBlinkSystem';
export { SceneLoadTriggerSystem } from './systems/SceneLoadTriggerSystem';
// Systems - Render (ECS-compliant render systems) // Systems - Render (ECS-compliant render systems)
export { export {

View File

@@ -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<void>;
/**
* 场景管理器接口(最小化,避免循环依赖)
* Scene manager interface (minimal, avoids circular dependency)
*
* 包含 IService 的 dispose 方法以兼容 ServiceContainer。
* Includes IService's dispose method for ServiceContainer compatibility.
*/
interface ISceneManager {
loadScene(sceneName: string): Promise<void>;
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<ISceneManager>(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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -96,7 +96,7 @@ export class UILayoutSystem extends EntitySystem {
const identityMatrix: Matrix2D = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 }; const identityMatrix: Matrix2D = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
for (const entity of rootEntities) { 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, parentHeight: number,
parentAlpha: number, parentAlpha: number,
parentMatrix: Matrix2D, parentMatrix: Matrix2D,
parentVisible: boolean = true parentVisible: boolean = true,
depth: number = 0
): void { ): void {
const transform = entity.getComponent(UITransformComponent); const transform = entity.getComponent(UITransformComponent);
if (!transform) return; if (!transform) return;
@@ -199,6 +200,12 @@ export class UILayoutSystem extends EntitySystem {
// Calculate world visibility (if parent is invisible, children are also invisible) // Calculate world visibility (if parent is invisible, children are also invisible)
transform.worldVisible = parentVisible && transform.visible; 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); this.updateWorldMatrix(transform, parentMatrix);
@@ -215,7 +222,7 @@ export class UILayoutSystem extends EntitySystem {
// 检查是否有布局组件 // 检查是否有布局组件
const layout = entity.getComponent(UILayoutComponent); const layout = entity.getComponent(UILayoutComponent);
if (layout && layout.type !== UILayoutType.None) { if (layout && layout.type !== UILayoutType.None) {
this.layoutChildren(layout, transform, children); this.layoutChildren(layout, transform, children, depth + 1);
} else { } else {
// 无布局组件,直接递归处理子元素 // 无布局组件,直接递归处理子元素
for (const child of children) { for (const child of children) {
@@ -227,7 +234,8 @@ export class UILayoutSystem extends EntitySystem {
height, height,
transform.worldAlpha, transform.worldAlpha,
transform.localToWorldMatrix, transform.localToWorldMatrix,
transform.worldVisible transform.worldVisible,
depth + 1
); );
} }
} }
@@ -240,7 +248,8 @@ export class UILayoutSystem extends EntitySystem {
private layoutChildren( private layoutChildren(
layout: UILayoutComponent, layout: UILayoutComponent,
parentTransform: UITransformComponent, parentTransform: UITransformComponent,
children: Entity[] children: Entity[],
depth: number
): void { ): void {
const contentStartX = parentTransform.worldX + layout.paddingLeft; const contentStartX = parentTransform.worldX + layout.paddingLeft;
// Y-up 系统worldY 是底部,顶部 = worldY + height // Y-up 系统worldY 是底部,顶部 = worldY + height
@@ -252,13 +261,13 @@ export class UILayoutSystem extends EntitySystem {
switch (layout.type) { switch (layout.type) {
case UILayoutType.Horizontal: case UILayoutType.Horizontal:
this.layoutHorizontal(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight); this.layoutHorizontal(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight, depth);
break; break;
case UILayoutType.Vertical: case UILayoutType.Vertical:
this.layoutVertical(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight); this.layoutVertical(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight, depth);
break; break;
case UILayoutType.Grid: case UILayoutType.Grid:
this.layoutGrid(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight); this.layoutGrid(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight, depth);
break; break;
default: default:
// 默认按正常方式递归(传递顶部 Y // 默认按正常方式递归(传递顶部 Y
@@ -270,7 +279,9 @@ export class UILayoutSystem extends EntitySystem {
parentTransform.computedWidth, parentTransform.computedWidth,
parentTransform.computedHeight, parentTransform.computedHeight,
parentTransform.worldAlpha, parentTransform.worldAlpha,
parentTransform.localToWorldMatrix parentTransform.localToWorldMatrix,
parentTransform.worldVisible,
depth
); );
} }
} }
@@ -287,7 +298,8 @@ export class UILayoutSystem extends EntitySystem {
startX: number, startX: number,
startY: number, startY: number,
contentWidth: number, contentWidth: number,
contentHeight: number contentHeight: number,
depth: number
): void { ): void {
// 计算总子元素宽度 // 计算总子元素宽度
const childSizes = children.map(child => { const childSizes = children.map(child => {
@@ -366,12 +378,14 @@ export class UILayoutSystem extends EntitySystem {
childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha; childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha;
// 传播世界可见性 | Propagate world visibility // 传播世界可见性 | Propagate world visibility
childTransform.worldVisible = parentTransform.worldVisible && childTransform.visible; childTransform.worldVisible = parentTransform.worldVisible && childTransform.visible;
// 计算世界层内顺序 | Calculate world order in layer
childTransform.worldOrderInLayer = depth * 1000 + childTransform.orderInLayer;
// 使用矩阵乘法计算世界旋转和缩放 // 使用矩阵乘法计算世界旋转和缩放
this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix); this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix);
childTransform.layoutDirty = false; childTransform.layoutDirty = false;
// 递归处理子元素的子元素 // 递归处理子元素的子元素
this.processChildrenRecursive(child, childTransform); this.processChildrenRecursive(child, childTransform, depth);
offsetX += size.width + gap; offsetX += size.width + gap;
} }
@@ -389,7 +403,8 @@ export class UILayoutSystem extends EntitySystem {
startX: number, startX: number,
startY: number, startY: number,
contentWidth: number, contentWidth: number,
contentHeight: number contentHeight: number,
depth: number
): void { ): void {
// 计算总子元素高度 // 计算总子元素高度
const childSizes = children.map(child => { const childSizes = children.map(child => {
@@ -466,11 +481,13 @@ export class UILayoutSystem extends EntitySystem {
childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha; childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha;
// 传播世界可见性 | Propagate world visibility // 传播世界可见性 | Propagate world visibility
childTransform.worldVisible = parentTransform.worldVisible && childTransform.visible; childTransform.worldVisible = parentTransform.worldVisible && childTransform.visible;
// 计算世界层内顺序 | Calculate world order in layer
childTransform.worldOrderInLayer = depth * 1000 + childTransform.orderInLayer;
// 使用矩阵乘法计算世界旋转和缩放 // 使用矩阵乘法计算世界旋转和缩放
this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix); this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix);
childTransform.layoutDirty = false; childTransform.layoutDirty = false;
this.processChildrenRecursive(child, childTransform); this.processChildrenRecursive(child, childTransform, depth);
// 移动到下一个元素的顶部位置(向下 = Y 减小) // 移动到下一个元素的顶部位置(向下 = Y 减小)
currentTopY -= size.height + gap; currentTopY -= size.height + gap;
@@ -489,7 +506,8 @@ export class UILayoutSystem extends EntitySystem {
startX: number, startX: number,
startY: number, startY: number,
contentWidth: number, contentWidth: number,
_contentHeight: number _contentHeight: number,
depth: number
): void { ): void {
const columns = layout.columns; const columns = layout.columns;
const gapX = layout.getHorizontalGap(); const gapX = layout.getHorizontalGap();
@@ -524,11 +542,13 @@ export class UILayoutSystem extends EntitySystem {
childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha; childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha;
// 传播世界可见性 | Propagate world visibility // 传播世界可见性 | Propagate world visibility
childTransform.worldVisible = parentTransform.worldVisible && childTransform.visible; childTransform.worldVisible = parentTransform.worldVisible && childTransform.visible;
// 计算世界层内顺序 | Calculate world order in layer
childTransform.worldOrderInLayer = depth * 1000 + childTransform.orderInLayer;
// 使用矩阵乘法计算世界旋转和缩放 // 使用矩阵乘法计算世界旋转和缩放
this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix); this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix);
childTransform.layoutDirty = false; childTransform.layoutDirty = false;
this.processChildrenRecursive(child, childTransform); this.processChildrenRecursive(child, childTransform, depth);
} }
} }
@@ -565,7 +585,7 @@ export class UILayoutSystem extends EntitySystem {
* 递归处理子元素 * 递归处理子元素
* Recursively process children * Recursively process children
*/ */
private processChildrenRecursive(entity: Entity, parentTransform: UITransformComponent): void { private processChildrenRecursive(entity: Entity, parentTransform: UITransformComponent, depth: number): void {
const children = this.getUIChildren(entity); const children = this.getUIChildren(entity);
if (children.length === 0) return; if (children.length === 0) return;
@@ -574,7 +594,7 @@ export class UILayoutSystem extends EntitySystem {
const layout = entity.getComponent(UILayoutComponent); const layout = entity.getComponent(UILayoutComponent);
if (layout && layout.type !== UILayoutType.None) { if (layout && layout.type !== UILayoutType.None) {
this.layoutChildren(layout, parentTransform, children); this.layoutChildren(layout, parentTransform, children, depth + 1);
} else { } else {
for (const child of children) { for (const child of children) {
this.layoutEntity( this.layoutEntity(
@@ -585,7 +605,8 @@ export class UILayoutSystem extends EntitySystem {
parentTransform.computedHeight, parentTransform.computedHeight,
parentTransform.worldAlpha, parentTransform.worldAlpha,
parentTransform.localToWorldMatrix, parentTransform.localToWorldMatrix,
parentTransform.worldVisible parentTransform.worldVisible,
depth + 1
); );
} }
} }

View File

@@ -58,9 +58,9 @@ export class UIButtonRenderSystem extends EntitySystem {
const width = (transform.computedWidth ?? transform.width) * scaleX; const width = (transform.computedWidth ?? transform.width) * scaleX;
const height = (transform.computedHeight ?? transform.height) * scaleY; const height = (transform.computedHeight ?? transform.height) * scaleY;
const alpha = transform.worldAlpha ?? transform.alpha; 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 sortingLayer = transform.sortingLayer;
const orderInLayer = transform.orderInLayer; const orderInLayer = transform.worldOrderInLayer;
// 使用 transform 的 pivot 作为旋转/缩放中心 // 使用 transform 的 pivot 作为旋转/缩放中心
const pivotX = transform.pivotX; const pivotX = transform.pivotX;
const pivotY = transform.pivotY; const pivotY = transform.pivotY;

View File

@@ -55,9 +55,9 @@ export class UIProgressBarRenderSystem extends EntitySystem {
const width = (transform.computedWidth ?? transform.width) * scaleX; const width = (transform.computedWidth ?? transform.width) * scaleX;
const height = (transform.computedHeight ?? transform.height) * scaleY; const height = (transform.computedHeight ?? transform.height) * scaleY;
const alpha = transform.worldAlpha ?? transform.alpha; 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 sortingLayer = transform.sortingLayer;
const orderInLayer = transform.orderInLayer; const orderInLayer = transform.worldOrderInLayer;
// 使用 transform 的 pivot 作为旋转/缩放中心 // 使用 transform 的 pivot 作为旋转/缩放中心
const pivotX = transform.pivotX; const pivotX = transform.pivotX;
const pivotY = transform.pivotY; const pivotY = transform.pivotY;

View File

@@ -10,7 +10,7 @@
import { EntitySystem, Matcher, Entity, ECSSystem } from '@esengine/ecs-framework'; import { EntitySystem, Matcher, Entity, ECSSystem } from '@esengine/ecs-framework';
import { UITransformComponent } from '../../components/UITransformComponent'; import { UITransformComponent } from '../../components/UITransformComponent';
import { UIRenderComponent } from '../../components/UIRenderComponent'; import { UIRenderComponent, UIRenderType } from '../../components/UIRenderComponent';
import { UIButtonComponent } from '../../components/widgets/UIButtonComponent'; import { UIButtonComponent } from '../../components/widgets/UIButtonComponent';
import { UIProgressBarComponent } from '../../components/widgets/UIProgressBarComponent'; import { UIProgressBarComponent } from '../../components/widgets/UIProgressBarComponent';
import { UISliderComponent } from '../../components/widgets/UISliderComponent'; import { UISliderComponent } from '../../components/widgets/UISliderComponent';
@@ -68,9 +68,11 @@ export class UIRectRenderSystem extends EntitySystem {
const alpha = transform.worldAlpha ?? transform.alpha; const alpha = transform.worldAlpha ?? transform.alpha;
// 使用世界旋转(考虑父级旋转) // 使用世界旋转(考虑父级旋转)
const rotation = transform.worldRotation ?? transform.rotation; 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 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 作为旋转/缩放中心 // 使用 transform 的 pivot 作为旋转/缩放中心
const pivotX = transform.pivotX; const pivotX = transform.pivotX;
const pivotY = transform.pivotY; const pivotY = transform.pivotY;
@@ -107,6 +109,37 @@ export class UIRectRenderSystem extends EntitySystem {
const textureGuid = typeof render.textureGuid === 'string' ? render.textureGuid : undefined; const textureGuid = typeof render.textureGuid === 'string' ? render.textureGuid : undefined;
const textureId = typeof render.textureGuid === 'number' ? render.textureGuid : undefined; const textureId = typeof render.textureGuid === 'number' ? render.textureGuid : 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( collector.addRect(
renderX, renderY, renderX, renderY,
width, height, width, height,
@@ -126,6 +159,7 @@ export class UIRectRenderSystem extends EntitySystem {
} }
); );
} }
}
// Render background color if fill is enabled // Render background color if fill is enabled
// 如果启用填充,渲染背景颜色 // 如果启用填充,渲染背景颜色
else if (render.fillBackground && render.backgroundAlpha > 0) { else if (render.fillBackground && render.backgroundAlpha > 0) {

View File

@@ -56,7 +56,7 @@ export interface UIRenderPrimitive {
rotation: number; rotation: number;
/** Pivot/Origin X (0-1, 0=left, 0.5=center, 1=right) | 锚点 X (0-1, 0=左, 0.5=中心, 1=右) */ /** Pivot/Origin X (0-1, 0=left, 0.5=center, 1=right) | 锚点 X (0-1, 0=左, 0.5=中心, 1=右) */
pivotX: number; 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; pivotY: number;
/** Packed color (0xAABBGGRR) | 打包颜色 */ /** Packed color (0xAABBGGRR) | 打包颜色 */
color: number; color: number;
@@ -171,6 +171,136 @@ export class UIRenderCollector {
this.cache = null; 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 * Get render data
* 获取渲染数据 * 获取渲染数据

View File

@@ -56,9 +56,9 @@ export class UIScrollViewRenderSystem extends EntitySystem {
const width = (transform.computedWidth ?? transform.width) * scaleX; const width = (transform.computedWidth ?? transform.width) * scaleX;
const height = (transform.computedHeight ?? transform.height) * scaleY; const height = (transform.computedHeight ?? transform.height) * scaleY;
const alpha = transform.worldAlpha ?? transform.alpha; 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 sortingLayer = transform.sortingLayer;
const orderInLayer = transform.orderInLayer; const orderInLayer = transform.worldOrderInLayer;
// 使用 transform 的 pivot 计算位置 // 使用 transform 的 pivot 计算位置
const pivotX = transform.pivotX; const pivotX = transform.pivotX;
const pivotY = transform.pivotY; const pivotY = transform.pivotY;

View File

@@ -55,9 +55,9 @@ export class UISliderRenderSystem extends EntitySystem {
const width = (transform.computedWidth ?? transform.width) * scaleX; const width = (transform.computedWidth ?? transform.width) * scaleX;
const height = (transform.computedHeight ?? transform.height) * scaleY; const height = (transform.computedHeight ?? transform.height) * scaleY;
const alpha = transform.worldAlpha ?? transform.alpha; 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 sortingLayer = transform.sortingLayer;
const orderInLayer = transform.orderInLayer; const orderInLayer = transform.worldOrderInLayer;
// 使用 transform 的 pivot 计算中心位置 // 使用 transform 的 pivot 计算中心位置
const pivotX = transform.pivotX; const pivotX = transform.pivotX;
const pivotY = transform.pivotY; const pivotY = transform.pivotY;

View File

@@ -112,9 +112,9 @@ export class UITextRenderSystem extends EntitySystem {
const width = (transform.computedWidth ?? transform.width) * scaleX; const width = (transform.computedWidth ?? transform.width) * scaleX;
const height = (transform.computedHeight ?? transform.height) * scaleY; const height = (transform.computedHeight ?? transform.height) * scaleY;
const alpha = transform.worldAlpha ?? transform.alpha; 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 sortingLayer = transform.sortingLayer;
const orderInLayer = transform.orderInLayer; const orderInLayer = transform.worldOrderInLayer;
// 使用 transform 的 pivot 作为旋转/缩放中心 // 使用 transform 的 pivot 作为旋转/缩放中心
const pivotX = transform.pivotX; const pivotX = transform.pivotX;
const pivotY = transform.pivotY; const pivotY = transform.pivotY;