Feature/runtime cdn and plugin loader (#240)

* feat(ui): 完善 UI 布局系统和编辑器可视化工具

* refactor: 移除 ModuleRegistry,统一使用 PluginManager 插件系统

* fix: 修复 CodeQL 警告并提升测试覆盖率

* refactor: 分离运行时入口点,解决 runtime bundle 包含 React 的问题

* fix(ci): 添加 editor-core 和 editor-runtime 到 CI 依赖构建步骤

* docs: 完善 ServiceContainer 文档,新增 Symbol.for 模式和 @InjectProperty 说明

* fix(ci): 修复 type-check 失败问题

* fix(ci): 修复类型检查失败问题

* fix(ci): 修复类型检查失败问题

* fix(ci): behavior-tree 构建添加 @tauri-apps 外部依赖

* fix(ci): behavior-tree 添加 @tauri-apps/plugin-fs 类型依赖

* fix(ci): platform-web 添加缺失的 behavior-tree 依赖

* fix(lint): 移除正则表达式中不必要的转义字符
This commit is contained in:
YHH
2025-11-27 20:42:46 +08:00
committed by GitHub
parent 71869b1a58
commit 107439d70c
367 changed files with 10661 additions and 12473 deletions

View File

@@ -36,7 +36,7 @@
],
"author": "ESEngine Team",
"license": "MIT",
"dependencies": {
"peerDependencies": {
"@esengine/ecs-framework": "workspace:*",
"@esengine/ecs-components": "workspace:*",
"@esengine/asset-system": "workspace:*"

View File

@@ -275,6 +275,18 @@ export class EngineBridge implements IEngineBridge {
}
}
/**
* Render sprites as overlay without clearing the screen.
* 渲染精灵作为叠加层,不清除屏幕。
*
* This is used for UI rendering on top of world content.
* 用于在世界内容上渲染 UI。
*/
renderOverlay(): void {
if (!this.initialized) return;
this.getEngine().renderOverlay();
}
/**
* Load a texture.
* 加载纹理。
@@ -622,6 +634,109 @@ export class EngineBridge implements IEngineBridge {
return this.getEngine().getViewportIds();
}
// ===== Screen Space Mode API =====
// ===== 屏幕空间模式 API =====
// Saved world space camera state
// 保存的世界空间相机状态
private savedWorldCamera: CameraConfig | null = null;
/**
* Push screen space rendering mode.
* 进入屏幕空间渲染模式。
*
* Saves the current world camera and switches to a fixed orthographic projection
* centered at (0, 0) with the specified canvas size.
* 保存当前世界相机并切换到以 (0, 0) 为中心的固定正交投影。
*
* @param canvasWidth - UI canvas width (design resolution) | UI 画布宽度(设计分辨率)
* @param canvasHeight - UI canvas height (design resolution) | UI 画布高度(设计分辨率)
*/
pushScreenSpaceMode(canvasWidth: number, canvasHeight: number): void {
if (!this.initialized) return;
// Save current world camera state
// 保存当前世界相机状态
this.savedWorldCamera = this.getCamera();
// Switch to screen space camera:
// - Position at origin (0, 0)
// - Zoom = 1 (1 pixel = 1 world unit)
// - No rotation
// 切换到屏幕空间相机:
// - 位置在原点 (0, 0)
// - 缩放 = 11 像素 = 1 世界单位)
// - 无旋转
//
// For screen space UI, we want the camera to show exactly canvasWidth x canvasHeight pixels
// centered at (0, 0). This means the visible area is:
// X: [-canvasWidth/2, canvasWidth/2]
// Y: [-canvasHeight/2, canvasHeight/2]
// 对于屏幕空间 UI我们希望相机精确显示 canvasWidth x canvasHeight 像素
// 以 (0, 0) 为中心。这意味着可见区域是:
// X: [-canvasWidth/2, canvasWidth/2]
// Y: [-canvasHeight/2, canvasHeight/2]
// Get current viewport size to calculate proper zoom
// 获取当前视口尺寸以计算正确的缩放
// Note: This assumes canvas.width/height match actual rendering size
// 注意:这假设 canvas.width/height 与实际渲染尺寸匹配
const canvas = document.getElementById(this.config.canvasId) as HTMLCanvasElement;
if (canvas) {
// Calculate zoom so that canvasWidth x canvasHeight fits exactly in the viewport
// 计算缩放使 canvasWidth x canvasHeight 正好适合视口
// zoom = viewport_size / world_visible_size
// For UI, we want 1 UI unit = 1 pixel on screen when canvas matches viewport
// 对于 UI当画布与视口匹配时我们希望 1 UI 单位 = 1 屏幕像素
const viewportWidth = canvas.width;
const viewportHeight = canvas.height;
// Calculate zoom based on the design canvas size vs actual viewport
// 根据设计画布尺寸与实际视口计算缩放
// This scales UI to fit the viewport while maintaining aspect ratio
const zoomX = viewportWidth / canvasWidth;
const zoomY = viewportHeight / canvasHeight;
// Use minimum to ensure entire canvas is visible (letterbox if needed)
// 使用最小值确保整个画布可见(如需要则显示黑边)
const zoom = Math.min(zoomX, zoomY);
this.setCamera({
x: 0,
y: 0,
zoom: zoom,
rotation: 0
});
} else {
// Fallback: use zoom = 1
// 回退:使用 zoom = 1
this.setCamera({
x: 0,
y: 0,
zoom: 1,
rotation: 0
});
}
}
/**
* Pop screen space rendering mode.
* 退出屏幕空间渲染模式。
*
* Restores the previously saved world camera.
* 恢复之前保存的世界相机。
*/
popScreenSpaceMode(): void {
if (!this.initialized) return;
// Restore world camera
// 恢复世界相机
if (this.savedWorldCamera) {
this.setCamera(this.savedWorldCamera);
this.savedWorldCamera = null;
}
}
/**
* Dispose the bridge and release resources.
* 销毁桥接并释放资源。

View File

@@ -8,6 +8,6 @@
export { EngineBridge, EngineBridgeConfig } from './core/EngineBridge';
export { RenderBatcher } from './core/RenderBatcher';
export { SpriteRenderHelper, ITransformComponent } from './core/SpriteRenderHelper';
export { EngineRenderSystem, type TransformComponentType, type IRenderDataProvider, type GizmoDataProviderFn, type HasGizmoProviderFn } from './systems/EngineRenderSystem';
export { EngineRenderSystem, type TransformComponentType, type IRenderDataProvider, type IUIRenderDataProvider, type GizmoDataProviderFn, type HasGizmoProviderFn, type ProviderRenderData } from './systems/EngineRenderSystem';
export { CameraSystem } from './systems/CameraSystem';
export * from './types';

View File

@@ -34,6 +34,22 @@ export interface IRenderDataProvider {
getRenderData(): readonly ProviderRenderData[];
}
/**
* Interface for UI render data providers
* UI 渲染数据提供者接口
*
* All UI is rendered in Screen Space with independent orthographic projection.
* 所有 UI 都在屏幕空间渲染,使用独立的正交投影。
*/
export interface IUIRenderDataProvider extends IRenderDataProvider {
/** Get UI render data | 获取 UI 渲染数据 */
getRenderData(): readonly ProviderRenderData[];
/** @deprecated Use getRenderData() instead */
getScreenSpaceRenderData?(): readonly ProviderRenderData[];
/** @deprecated World space UI is no longer supported */
getWorldSpaceRenderData?(): readonly ProviderRenderData[];
}
/**
* Internal gizmo color interface (duck-typed, compatible with editor-core GizmoColor)
* 内部 gizmo 颜色接口(鸭子类型,与 editor-core GizmoColor 兼容)
@@ -145,6 +161,22 @@ export class EngineRenderSystem extends EntitySystem {
private gizmoDataProvider: GizmoDataProviderFn | null = null;
private hasGizmoProvider: HasGizmoProviderFn | null = null;
// UI Canvas boundary settings
// UI 画布边界设置
private uiCanvasWidth: number = 0;
private uiCanvasHeight: number = 0;
private showUICanvasBoundary: boolean = true;
// UI render data provider (supports screen space and world space)
// UI 渲染数据提供者(支持屏幕空间和世界空间)
private uiRenderDataProvider: IUIRenderDataProvider | null = null;
// Preview mode flag: when true, UI uses screen space overlay projection
// when false (editor mode), UI renders in world space following editor camera
// 预览模式标志:为 true 时UI 使用屏幕空间叠加投影
// 为 false编辑器模式UI 在世界空间渲染,跟随编辑器相机
private previewMode: boolean = false;
/**
* Create a new engine render system.
* 创建新的引擎渲染系统。
@@ -190,6 +222,14 @@ export class EngineRenderSystem extends EntitySystem {
* Process all matched entities.
* 处理所有匹配的实体。
*
* Rendering is done in two passes:
* 1. World Pass: World sprites, tilemaps, gizmos (affected by world camera)
* 2. UI Pass: Screen space UI (independent orthographic projection, overlaid on world)
*
* 渲染分两个阶段进行:
* 1. 世界阶段:世界 Sprite、瓦片地图、Gizmo受世界相机影响
* 2. UI 阶段:屏幕空间 UI独立正交投影叠加在世界之上
*
* @param entities - Entities to process | 要处理的实体
*/
protected override process(entities: readonly Entity[]): void {
@@ -197,6 +237,11 @@ export class EngineRenderSystem extends EntitySystem {
// 清空并重用映射用于绘制gizmo
this.entityRenderMap.clear();
// ===== Pass 1: World Space Rendering =====
// ===== 阶段 1世界空间渲染 =====
// This includes world sprites, tilemaps, and world space UI
// 包括世界 Sprite、瓦片地图和世界空间 UI
// Collect all render items with sorting order
// 收集所有渲染项及其排序顺序
const renderItems: Array<{ sortingOrder: number; sprites: SpriteRenderData[] }> = [];
@@ -259,7 +304,6 @@ export class EngineRenderSystem extends EntitySystem {
}
// Collect render data from providers (e.g., tilemap)
// 收集来自提供者的渲染数据(如瓦片地图)
for (const provider of this.renderDataProviders) {
const renderDataList = provider.getRenderData();
for (const data of renderDataList) {
@@ -297,6 +341,18 @@ export class EngineRenderSystem extends EntitySystem {
}
}
// Collect UI render data if in editor mode (renders in world space)
// 如果在编辑器模式,收集 UI 渲染数据(在世界空间渲染)
if (!this.previewMode && this.uiRenderDataProvider) {
const uiRenderData = this.uiRenderDataProvider.getRenderData();
for (const data of uiRenderData) {
const uiSprites = this.convertProviderDataToSprites(data);
if (uiSprites.length > 0) {
renderItems.push({ sortingOrder: data.sortingOrder, sprites: uiSprites });
}
}
}
// Sort by sortingOrder (lower values render first, appear behind)
// 按 sortingOrder 排序(值越小越先渲染,显示在后面)
renderItems.sort((a, b) => a.sortingOrder - b.sortingOrder);
@@ -332,7 +388,127 @@ export class EngineRenderSystem extends EntitySystem {
this.drawCameraFrustums();
}
// Draw UI canvas boundary
// 绘制 UI 画布边界
if (this.showGizmos && this.showUICanvasBoundary && this.uiCanvasWidth > 0 && this.uiCanvasHeight > 0) {
this.drawUICanvasBoundary();
}
// ===== World Pass: Render world content =====
// ===== 世界阶段:渲染世界内容 =====
this.bridge.render();
// ===== Pass 2: Screen Space UI Rendering (Preview Mode Only) =====
// ===== 阶段 2屏幕空间 UI 渲染(仅预览模式)=====
// UI is rendered on top of world content with independent projection
// UI 使用独立投影渲染在世界内容之上
// Only in preview mode - in editor mode, UI is rendered in world space above
// 仅在预览模式 - 在编辑器模式UI 在上面的世界空间渲染
if (this.previewMode) {
this.renderScreenSpaceUI();
}
}
/**
* Render screen space UI with fixed orthographic projection.
* 使用固定正交投影渲染屏幕空间 UI。
*
* Screen space UI is rendered with an independent orthographic projection
* based on the UI canvas size, not affected by the world camera.
* 屏幕空间 UI 使用基于 UI 画布尺寸的独立正交投影渲染,不受世界相机影响。
*/
private renderScreenSpaceUI(): void {
if (!this.uiRenderDataProvider) {
return;
}
// Get all UI render data (now only screen space)
// 获取所有 UI 渲染数据(现在只有屏幕空间)
const uiRenderData = this.uiRenderDataProvider.getRenderData();
if (uiRenderData.length === 0) {
return;
}
// Switch to screen space projection
// 切换到屏幕空间投影
// Use UI canvas size for the orthographic projection
// 使用 UI 画布尺寸进行正交投影
const canvasWidth = this.uiCanvasWidth > 0 ? this.uiCanvasWidth : 1920;
const canvasHeight = this.uiCanvasHeight > 0 ? this.uiCanvasHeight : 1080;
// Save current camera state and switch to screen space mode
// 保存当前相机状态并切换到屏幕空间模式
this.bridge.pushScreenSpaceMode(canvasWidth, canvasHeight);
// Clear batcher for screen space content
this.batcher.clear();
// Collect screen space UI render items
const screenSpaceItems: Array<{ sortingOrder: number; sprites: SpriteRenderData[] }> = [];
for (const data of uiRenderData) {
const uiSprites = this.convertProviderDataToSprites(data);
if (uiSprites.length > 0) {
screenSpaceItems.push({ sortingOrder: data.sortingOrder, sprites: uiSprites });
}
}
// Sort by sortingOrder
screenSpaceItems.sort((a, b) => a.sortingOrder - b.sortingOrder);
// Submit screen space UI sprites
for (const item of screenSpaceItems) {
for (const sprite of item.sprites) {
this.batcher.addSprite(sprite);
}
}
if (!this.batcher.isEmpty) {
const sprites = this.batcher.getSprites();
this.bridge.submitSprites(sprites);
// Render overlay (without clearing screen)
// 渲染叠加层(不清屏)
this.bridge.renderOverlay();
}
// Restore world space camera
// 恢复世界空间相机
this.bridge.popScreenSpaceMode();
}
/**
* Convert provider render data to sprite render data array.
* 将提供者渲染数据转换为 Sprite 渲染数据数组。
*/
private convertProviderDataToSprites(data: ProviderRenderData): SpriteRenderData[] {
// Get texture ID - load from path if needed
let textureId = data.textureIds[0] || 0;
if (textureId === 0 && data.texturePath) {
textureId = this.bridge.getOrLoadTextureByPath(data.texturePath);
}
const sprites: SpriteRenderData[] = [];
for (let i = 0; i < data.tileCount; i++) {
const tOffset = i * 7;
const uvOffset = i * 4;
const renderData: SpriteRenderData = {
x: data.transforms[tOffset],
y: data.transforms[tOffset + 1],
rotation: data.transforms[tOffset + 2],
scaleX: data.transforms[tOffset + 3],
scaleY: data.transforms[tOffset + 4],
originX: data.transforms[tOffset + 5],
originY: data.transforms[tOffset + 6],
textureId,
uv: [data.uvs[uvOffset], data.uvs[uvOffset + 1], data.uvs[uvOffset + 2], data.uvs[uvOffset + 3]],
color: data.colors[i]
};
sprites.push(renderData);
}
return sprites;
}
/**
@@ -675,6 +851,78 @@ export class EngineRenderSystem extends EntitySystem {
}
}
/**
* Draw UI canvas boundary.
* 绘制 UI 画布边界。
*
* Shows the design resolution boundary of the UI canvas.
* 显示 UI 画布的设计分辨率边界。
*/
private drawUICanvasBoundary(): void {
const w = this.uiCanvasWidth;
const h = this.uiCanvasHeight;
// Canvas is centered at (0, 0) in Y-up coordinate system
// 画布以 (0, 0) 为中心Y 轴向上坐标系
// Bottom-left: (-w/2, -h/2), Top-right: (w/2, h/2)
// Draw the boundary as a rectangle
// 绘制边界矩形
// Using origin (0, 0) means position is bottom-left corner
// 使用 origin (0, 0) 表示位置是左下角
this.bridge.addGizmoRect(
-w / 2, // x: left edge
-h / 2, // y: bottom edge (in Y-up system)
w, // width
h, // height
0, // rotation
0, // originX: left
0, // originY: bottom
0.5, 0.8, 1.0, 0.6, // Light blue color for UI canvas boundary
false // Don't show transform handles
);
// Draw corner markers for better visibility
// 绘制角标记以提高可见性
const markerSize = 20;
const markerColor = { r: 0.5, g: 0.8, b: 1.0, a: 1.0 };
// Top-left corner marker (L shape)
const corners = [
// Top-left
{ x: -w / 2, y: h / 2 - markerSize, ex: -w / 2, ey: h / 2 },
{ x: -w / 2, y: h / 2, ex: -w / 2 + markerSize, ey: h / 2 },
// Top-right
{ x: w / 2 - markerSize, y: h / 2, ex: w / 2, ey: h / 2 },
{ x: w / 2, y: h / 2, ex: w / 2, ey: h / 2 - markerSize },
// Bottom-right
{ x: w / 2, y: -h / 2 + markerSize, ex: w / 2, ey: -h / 2 },
{ x: w / 2, y: -h / 2, ex: w / 2 - markerSize, ey: -h / 2 },
// Bottom-left
{ x: -w / 2 + markerSize, y: -h / 2, ex: -w / 2, ey: -h / 2 },
{ x: -w / 2, y: -h / 2, ex: -w / 2, ey: -h / 2 + markerSize },
];
for (const line of corners) {
const dx = line.ex - line.x;
const dy = line.ey - line.y;
const length = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
this.bridge.addGizmoRect(
(line.x + line.ex) / 2,
(line.y + line.ey) / 2,
length,
2, // Line thickness
angle,
0.5,
0.5,
markerColor.r, markerColor.g, markerColor.b, markerColor.a,
false
);
}
}
/**
* Set gizmo registry functions.
* 设置 gizmo 注册表函数。
@@ -711,6 +959,42 @@ export class EngineRenderSystem extends EntitySystem {
return this.showGizmos;
}
/**
* Set UI canvas size for boundary display.
* 设置 UI 画布尺寸以显示边界。
*
* @param width - Canvas width (design resolution) | 画布宽度(设计分辨率)
* @param height - Canvas height (design resolution) | 画布高度(设计分辨率)
*/
setUICanvasSize(width: number, height: number): void {
this.uiCanvasWidth = width;
this.uiCanvasHeight = height;
}
/**
* Get UI canvas size.
* 获取 UI 画布尺寸。
*/
getUICanvasSize(): { width: number; height: number } {
return { width: this.uiCanvasWidth, height: this.uiCanvasHeight };
}
/**
* Set UI canvas boundary visibility.
* 设置 UI 画布边界可见性。
*/
setShowUICanvasBoundary(show: boolean): void {
this.showUICanvasBoundary = show;
}
/**
* Get UI canvas boundary visibility.
* 获取 UI 画布边界可见性。
*/
getShowUICanvasBoundary(): boolean {
return this.showUICanvasBoundary;
}
/**
* Set selected entity IDs.
* 设置选中的实体ID。
@@ -798,6 +1082,51 @@ export class EngineRenderSystem extends EntitySystem {
}
}
/**
* Set the UI render data provider.
* 设置 UI 渲染数据提供者。
*
* The UI render data provider supports both screen space and world space UI.
* UI 渲染数据提供者支持屏幕空间和世界空间 UI。
*
* @param provider - UI render data provider | UI 渲染数据提供者
*/
setUIRenderDataProvider(provider: IUIRenderDataProvider | null): void {
this.uiRenderDataProvider = provider;
}
/**
* Get the UI render data provider.
* 获取 UI 渲染数据提供者。
*/
getUIRenderDataProvider(): IUIRenderDataProvider | null {
return this.uiRenderDataProvider;
}
/**
* Set preview mode.
* 设置预览模式。
*
* In preview mode (true): UI uses screen space overlay projection, independent of world camera.
* In editor mode (false): UI renders in world space, following the editor camera.
*
* 预览模式trueUI 使用屏幕空间叠加投影,独立于世界相机。
* 编辑器模式falseUI 在世界空间渲染,跟随编辑器相机。
*
* @param mode - True for preview mode, false for editor mode
*/
setPreviewMode(mode: boolean): void {
this.previewMode = mode;
}
/**
* Get preview mode.
* 获取预览模式。
*/
isPreviewMode(): boolean {
return this.previewMode;
}
/**
* Get the number of sprites rendered.
* 获取渲染的精灵数量。

View File

@@ -85,6 +85,14 @@ export class GameEngine {
* * `show_handles` - Whether to show transform handles | 是否显示变换手柄
*/
addGizmoRect(x: number, y: number, width: number, height: number, rotation: number, origin_x: number, origin_y: number, r: number, g: number, b: number, a: number, show_handles: boolean): void;
/**
* Render sprites as overlay (without clearing screen).
* 渲染精灵作为叠加层(不清除屏幕)。
*
* This is used for UI rendering on top of the world content.
* 用于在世界内容上渲染 UI。
*/
renderOverlay(): void;
/**
* Resize a specific viewport.
* 调整特定视口大小。
@@ -259,6 +267,7 @@ export interface InitOutput {
readonly gameengine_new: (a: number, b: number) => [number, number, number];
readonly gameengine_registerViewport: (a: number, b: number, c: number, d: number, e: number) => [number, number];
readonly gameengine_render: (a: number) => [number, number];
readonly gameengine_renderOverlay: (a: number) => [number, number];
readonly gameengine_renderToViewport: (a: number, b: number, c: number) => [number, number];
readonly gameengine_resize: (a: number, b: number, c: number) => void;
readonly gameengine_resizeViewport: (a: number, b: number, c: number, d: number, e: number) => void;