feat: 添加跨平台运行时、资产系统和UI适配功能 (#256)

* feat(platform-common): 添加WASM加载器和环境检测API

* feat(rapier2d): 新增Rapier2D WASM绑定包

* feat(physics-rapier2d): 添加跨平台WASM加载器

* feat(asset-system): 添加运行时资产目录和bundle格式

* feat(asset-system-editor): 新增编辑器资产管理包

* feat(editor-core): 添加构建系统和模块管理

* feat(editor-app): 重构浏览器预览使用import maps

* feat(platform-web): 添加BrowserRuntime和资产读取

* feat(engine): 添加材质系统和着色器管理

* feat(material): 新增材质系统和着色器编辑器

* feat(tilemap): 增强tilemap编辑器和动画系统

* feat(modules): 添加module.json配置

* feat(core): 添加module.json和类型定义更新

* chore: 更新依赖和构建配置

* refactor(plugins): 更新插件模板使用ModuleManifest

* chore: 添加第三方依赖库

* chore: 移除BehaviourTree-ai和ecs-astar子模块

* docs: 更新README和文档主题样式

* fix: 修复Rust文档测试和添加rapier2d WASM绑定

* fix(tilemap-editor): 修复画布高DPI屏幕分辨率适配问题

* feat(ui): 添加UI屏幕适配系统(CanvasScaler/SafeArea)

* fix(ecs-engine-bindgen): 添加缺失的ecs-framework-math依赖

* fix: 添加缺失的包依赖修复CI构建

* fix: 修复CodeQL检测到的代码问题

* fix: 修复构建错误和缺失依赖

* fix: 修复类型检查错误

* fix(material-system): 修复tsconfig配置支持TypeScript项目引用

* fix(editor-core): 修复Rollup构建配置添加tauri external

* fix: 修复CodeQL检测到的代码问题

* fix: 修复CodeQL检测到的代码问题
This commit is contained in:
YHH
2025-12-03 22:15:22 +08:00
committed by GitHub
parent caf7622aa0
commit 63f006ab62
496 changed files with 77601 additions and 4067 deletions

View File

@@ -1,20 +1,34 @@
import { EntitySystem, Matcher, ECSSystem, Entity } from '@esengine/ecs-framework';
import { TransformComponent } from '@esengine/engine-core';
import { Color } from '@esengine/ecs-framework-math';
import type { IRenderDataProvider } from '@esengine/ecs-engine-bindgen';
import { TilemapComponent } from '../TilemapComponent';
import { TilemapComponent, type ITilemapLayerData } from '../TilemapComponent';
/**
* Tilemap render data for a single tilemap
* Tilemap render data for a single tilemap layer
* 单个瓦片地图图层的渲染数据
*/
export interface TilemapRenderData {
/** Entity ID | 实体ID */
entityId: number;
/** Layer index within the tilemap | 图层在瓦片地图中的索引 */
layerIndex: number;
/** Transform data [x, y, rotation, scaleX, scaleY, originX, originY] per tile | 每个瓦片的变换数据 */
transforms: Float32Array;
/** Texture IDs per tile | 每个瓦片的纹理ID */
textureIds: Uint32Array;
/** UV coordinates [u0, v0, u1, v1] per tile | 每个瓦片的UV坐标 */
uvs: Float32Array;
/** Packed colors (ARGB) per tile | 每个瓦片的打包颜色 */
colors: Uint32Array;
/** Number of tiles in this layer | 此图层的瓦片数量 */
tileCount: number;
/** Sorting order for rendering | 渲染排序顺序 */
sortingOrder: number;
/** Texture path for loading | 纹理路径 */
texturePath?: string;
/** Material ID for this layer (0 = default) | 此图层的材质ID0 = 默认) */
materialId: number;
}
/**
@@ -28,22 +42,40 @@ export interface ViewportBounds {
}
/**
* 瓦片地图渲染系统 - 准备瓦片地图渲染数据
* Cache key for layer render data
* 图层渲染数据的缓存键
*/
type LayerCacheKey = `${number}_${number}`;
/**
* 瓦片地图渲染系统 - 准备瓦片地图渲染数据(按图层)
* Tilemap rendering system - Prepares tilemap render data (per layer)
*/
@ECSSystem('TilemapRendering', { updateOrder: 40 })
export class TilemapRenderingSystem extends EntitySystem implements IRenderDataProvider {
private _renderDataCache: Map<number, TilemapRenderData> = new Map();
/** Cache for layer render data: key = "entityId_layerIndex" | 图层渲染数据缓存 */
private _layerRenderDataCache: Map<LayerCacheKey, TilemapRenderData> = new Map();
/** Current frame render data | 当前帧渲染数据 */
private _currentFrameData: TilemapRenderData[] = [];
/** Viewport bounds for culling | 视口边界用于剔除 */
private _viewportBounds: ViewportBounds | null = null;
constructor() {
super(Matcher.empty().all(TilemapComponent, TransformComponent));
}
/**
* Set viewport bounds for tile culling
* 设置视口边界用于瓦片剔除
*/
setViewportBounds(bounds: ViewportBounds): void {
this._viewportBounds = bounds;
}
/**
* Get render data for current frame
* 获取当前帧的渲染数据
*/
getRenderData(): readonly TilemapRenderData[] {
return this._currentFrameData;
}
@@ -61,74 +93,95 @@ export class TilemapRenderingSystem extends EntitySystem implements IRenderDataP
if (!tilemap || !transform || !tilemap.visible) continue;
let renderData = this._renderDataCache.get(entity.id);
// Process each layer separately
// 分别处理每个图层
const layers = tilemap.layers;
for (let layerIndex = 0; layerIndex < layers.length; layerIndex++) {
const layer = layers[layerIndex];
if (!layer.visible) continue;
if (!renderData || tilemap.renderDirty) {
renderData = this.buildRenderData(entity.id, tilemap, transform);
this._renderDataCache.set(entity.id, renderData);
tilemap.renderDirty = false;
} else {
this.updateTransforms(renderData, tilemap, transform);
const cacheKey: LayerCacheKey = `${entity.id}_${layerIndex}`;
let renderData = this._layerRenderDataCache.get(cacheKey);
if (!renderData || tilemap.renderDirty) {
renderData = this.buildLayerRenderData(entity.id, layerIndex, tilemap, transform, layer);
this._layerRenderDataCache.set(cacheKey, renderData);
} else {
this.updateLayerTransforms(renderData, layerIndex, tilemap, transform, layer);
}
if (renderData.tileCount > 0) {
this._currentFrameData.push(renderData);
}
}
this._currentFrameData.push(renderData);
// Clear dirty flag after processing all layers
// 处理完所有图层后清除脏标志
tilemap.renderDirty = false;
}
// Sort by sorting order (lower values render first)
// 按排序顺序排序(较小值先渲染)
this._currentFrameData.sort((a, b) => a.sortingOrder - b.sortingOrder);
}
private buildRenderData(
/**
* Build render data for a single layer
* 为单个图层构建渲染数据
*/
private buildLayerRenderData(
entityId: number,
layerIndex: number,
tilemap: TilemapComponent,
transform: TransformComponent
transform: TransformComponent,
layer: ITilemapLayerData
): TilemapRenderData {
const mergedData = tilemap.getMergedTileData();
const layerData = tilemap.getLayerData(layerIndex);
if (!layerData) {
return this.createEmptyRenderData(entityId, layerIndex, tilemap.sortingOrder, layer.materialId);
}
const width = tilemap.width;
const height = tilemap.height;
const tileWidth = tilemap.tileWidth;
const tileHeight = tilemap.tileHeight;
// Calculate visible tile range
// 计算可见瓦片范围
let startCol = 0,
endCol = width;
let startRow = 0,
endRow = height;
const { startCol, endCol, startRow, endRow } = this.calculateVisibleRange(
width, height, tileWidth, tileHeight, transform
);
if (this._viewportBounds) {
const bounds = this._viewportBounds;
const mapX = transform.position.x;
const mapY = transform.position.y;
startCol = Math.max(0, Math.floor((bounds.left - mapX) / tileWidth));
endCol = Math.min(width, Math.ceil((bounds.right - mapX) / tileWidth));
startRow = Math.max(0, Math.floor((bounds.bottom - mapY) / tileHeight));
endRow = Math.min(height, Math.ceil((bounds.top - mapY) / tileHeight));
}
// 计算非空瓦片数量
// Count non-empty tiles in this layer
// 计算此图层的非空瓦片数量
let tileCount = 0;
for (let row = startRow; row < endRow; row++) {
for (let col = startCol; col < endCol; col++) {
if (mergedData[row * width + col] > 0) tileCount++;
if (layerData[row * width + col] > 0) tileCount++;
}
}
if (tileCount === 0) {
return this.createEmptyRenderData(entityId, layerIndex, tilemap.sortingOrder, layer.materialId);
}
const transforms = new Float32Array(tileCount * 7);
const textureIds = new Uint32Array(tileCount);
const uvs = new Float32Array(tileCount * 4);
const colors = new Uint32Array(tileCount);
const colorValue = this.parseColor(tilemap.color, tilemap.alpha);
// Calculate color with layer opacity
// 计算带有图层透明度的颜色
const effectiveAlpha = tilemap.alpha * layer.opacity;
const colorValue = Color.packHexAlpha(tilemap.color, effectiveAlpha);
// 计算旋转参数
// Calculate rotation parameters
// Note: transform.rotation.z is already in radians (set by Viewport gizmo)
// 注意transform.rotation.z 已经是弧度(由 Viewport gizmo 设置)
// 计算旋转参数
const cos = Math.cos(transform.rotation.z);
const sin = Math.sin(transform.rotation.z);
// Tilemap 旋转中心点(左下角为原点,中心在 width/2, height/2 处)
// Tilemap rotation pivot (origin at bottom-left, center at width/2, height/2)
// Tilemap rotation pivot
// Tilemap 旋转中心点
const pivotX = transform.position.x + (width * tileWidth * transform.scale.x) / 2;
const pivotY = transform.position.y + (height * tileHeight * transform.scale.y) / 2;
@@ -137,45 +190,43 @@ export class TilemapRenderingSystem extends EntitySystem implements IRenderDataP
for (let row = startRow; row < endRow; row++) {
for (let col = startCol; col < endCol; col++) {
const gid = mergedData[row * width + col];
const gid = layerData[row * width + col];
if (gid <= 0) continue;
// Find corresponding tileset
// 查找对应的 tileset
const tilesetInfo = tilemap.getTilesetForGid(gid);
if (!tilesetInfo) {
continue;
}
if (!tilesetInfo) continue;
const { index: tilesetIndex, localId } = tilesetInfo;
// Get texture path
// 获取纹理路径
if (!texturePath && tilemap.tilesets[tilesetIndex]) {
texturePath = tilemap.tilesets[tilesetIndex].source;
}
// 计算瓦片的本地位置(相对于 tilemap 中心)
// Calculate tile local position (relative to tilemap center)
// 计算瓦片的本地位置(相对于 tilemap 中心)
const localX = transform.position.x + col * tileWidth * transform.scale.x + (tileWidth * transform.scale.x) / 2 - pivotX;
const localY = transform.position.y + (height - 1 - row) * tileHeight * transform.scale.y + (tileHeight * transform.scale.y) / 2 - pivotY;
// 应用旋转变换
// Apply rotation transform
// 应用旋转变换
const rotatedX = localX * cos - localY * sin + pivotX;
const rotatedY = localX * sin + localY * cos + pivotY;
// Transform: [x, y, rotation, scaleX, scaleY, originX, originY]
// Each tile rotates the same angle as the tilemap, so the whole map rotates as a unit
// 每个 tile 旋转与 tilemap 相同的角度,这样整个地图作为一个整体旋转
const tOffset = idx * 7;
transforms[tOffset] = rotatedX;
transforms[tOffset + 1] = rotatedY;
transforms[tOffset + 2] = transform.rotation.z; // Each tile rotates with tilemap
transforms[tOffset + 2] = transform.rotation.z;
transforms[tOffset + 3] = tileWidth * transform.scale.x;
transforms[tOffset + 4] = tileHeight * transform.scale.y;
transforms[tOffset + 5] = 0.5;
transforms[tOffset + 6] = 0.5;
// Texture ID (使用 tileset 的 textureId)
// Texture ID
textureIds[idx] = tilemap.tilesets[tilesetIndex]?.textureId || 0;
// UV coordinates
@@ -200,33 +251,106 @@ export class TilemapRenderingSystem extends EntitySystem implements IRenderDataP
return {
entityId,
layerIndex,
transforms,
textureIds,
uvs,
colors,
tileCount,
sortingOrder: tilemap.sortingOrder,
texturePath
sortingOrder: tilemap.sortingOrder + layerIndex * 0.001,
texturePath,
materialId: layer.materialId ?? 0
};
}
private updateTransforms(
/**
* Update transforms for a layer (when only position/rotation/scale changed)
* 更新图层的变换(当只有位置/旋转/缩放改变时)
*/
private updateLayerTransforms(
renderData: TilemapRenderData,
layerIndex: number,
tilemap: TilemapComponent,
transform: TransformComponent
transform: TransformComponent,
layer: ITilemapLayerData
): void {
const mergedData = tilemap.getMergedTileData();
const layerData = tilemap.getLayerData(layerIndex);
if (!layerData) return;
const width = tilemap.width;
const height = tilemap.height;
const tileWidth = tilemap.tileWidth;
const tileHeight = tilemap.tileHeight;
// 计算可见瓦片范围(与 buildRenderData 保持一致)
// Calculate visible tile range (consistent with buildRenderData)
let startCol = 0,
endCol = width;
let startRow = 0,
endRow = height;
// Calculate visible tile range
// 计算可见瓦片范围
const { startCol, endCol, startRow, endRow } = this.calculateVisibleRange(
width, height, tileWidth, tileHeight, transform
);
// Calculate rotation parameters
// 计算旋转参数
const cos = Math.cos(transform.rotation.z);
const sin = Math.sin(transform.rotation.z);
// Tilemap rotation pivot
// Tilemap 旋转中心点
const pivotX = transform.position.x + (width * tileWidth * transform.scale.x) / 2;
const pivotY = transform.position.y + (height * tileHeight * transform.scale.y) / 2;
let idx = 0;
for (let row = startRow; row < endRow; row++) {
for (let col = startCol; col < endCol; col++) {
if (layerData[row * width + col] <= 0) continue;
// Calculate tile local position
// 计算瓦片本地位置
const localX = transform.position.x + col * tileWidth * transform.scale.x + (tileWidth * transform.scale.x) / 2 - pivotX;
const localY = transform.position.y + (height - 1 - row) * tileHeight * transform.scale.y + (tileHeight * transform.scale.y) / 2 - pivotY;
// Apply rotation transform
// 应用旋转变换
const rotatedX = localX * cos - localY * sin + pivotX;
const rotatedY = localX * sin + localY * cos + pivotY;
const tOffset = idx * 7;
renderData.transforms[tOffset] = rotatedX;
renderData.transforms[tOffset + 1] = rotatedY;
renderData.transforms[tOffset + 2] = transform.rotation.z;
renderData.transforms[tOffset + 3] = tileWidth * transform.scale.x;
renderData.transforms[tOffset + 4] = tileHeight * transform.scale.y;
idx++;
}
}
// Update color (alpha or layer opacity may have changed)
// 更新颜色alpha 或图层透明度可能已更改)
const effectiveAlpha = tilemap.alpha * layer.opacity;
const colorValue = Color.packHexAlpha(tilemap.color, effectiveAlpha);
for (let i = 0; i < renderData.colors.length; i++) {
renderData.colors[i] = colorValue;
}
// Update sorting order and material ID
// 更新排序顺序和材质ID
renderData.sortingOrder = tilemap.sortingOrder + layerIndex * 0.001;
renderData.materialId = layer.materialId ?? 0;
}
/**
* Calculate visible tile range based on viewport bounds
* 根据视口边界计算可见瓦片范围
*/
private calculateVisibleRange(
width: number,
height: number,
tileWidth: number,
tileHeight: number,
transform: TransformComponent
): { startCol: number; endCol: number; startRow: number; endRow: number } {
let startCol = 0, endCol = width;
let startRow = 0, endRow = height;
if (this._viewportBounds) {
const bounds = this._viewportBounds;
@@ -239,83 +363,52 @@ export class TilemapRenderingSystem extends EntitySystem implements IRenderDataP
endRow = Math.min(height, Math.ceil((bounds.top - mapY) / tileHeight));
}
// 计算旋转参数
// Calculate rotation parameters
// Note: transform.rotation.z is already in radians (set by Viewport gizmo)
// 注意transform.rotation.z 已经是弧度(由 Viewport gizmo 设置)
const cos = Math.cos(transform.rotation.z);
const sin = Math.sin(transform.rotation.z);
// Tilemap 旋转中心点
// Tilemap rotation pivot
const pivotX = transform.position.x + (width * tileWidth * transform.scale.x) / 2;
const pivotY = transform.position.y + (height * tileHeight * transform.scale.y) / 2;
let idx = 0;
for (let row = startRow; row < endRow; row++) {
for (let col = startCol; col < endCol; col++) {
if (mergedData[row * width + col] <= 0) continue;
// 计算瓦片的本地位置(相对于 tilemap 中心)
// Calculate tile local position (relative to tilemap center)
const localX = transform.position.x + col * tileWidth * transform.scale.x + (tileWidth * transform.scale.x) / 2 - pivotX;
const localY = transform.position.y + (height - 1 - row) * tileHeight * transform.scale.y + (tileHeight * transform.scale.y) / 2 - pivotY;
// 应用旋转变换
// Apply rotation transform
const rotatedX = localX * cos - localY * sin + pivotX;
const rotatedY = localX * sin + localY * cos + pivotY;
// Each tile rotates the same angle as the tilemap
// 每个 tile 旋转与 tilemap 相同的角度
const tOffset = idx * 7;
renderData.transforms[tOffset] = rotatedX;
renderData.transforms[tOffset + 1] = rotatedY;
renderData.transforms[tOffset + 2] = transform.rotation.z;
renderData.transforms[tOffset + 3] = tileWidth * transform.scale.x;
renderData.transforms[tOffset + 4] = tileHeight * transform.scale.y;
idx++;
}
}
// Update color (alpha may have changed)
// 更新颜色alpha 可能已更改)
const colorValue = this.parseColor(tilemap.color, tilemap.alpha);
for (let i = 0; i < renderData.colors.length; i++) {
renderData.colors[i] = colorValue;
}
renderData.sortingOrder = tilemap.sortingOrder;
return { startCol, endCol, startRow, endRow };
}
private parseColor(hex: string, alpha: number): number {
const colorHex = hex.replace('#', '');
let r = 255,
g = 255,
b = 255;
if (colorHex.length === 6) {
r = parseInt(colorHex.substring(0, 2), 16);
g = parseInt(colorHex.substring(2, 4), 16);
b = parseInt(colorHex.substring(4, 6), 16);
} else if (colorHex.length === 3) {
r = parseInt(colorHex[0] + colorHex[0], 16);
g = parseInt(colorHex[1] + colorHex[1], 16);
b = parseInt(colorHex[2] + colorHex[2], 16);
}
const a = Math.round(alpha * 255);
return (a << 24) | (b << 16) | (g << 8) | r;
/**
* Create empty render data
* 创建空的渲染数据
*/
private createEmptyRenderData(
entityId: number,
layerIndex: number,
sortingOrder: number,
materialId?: number
): TilemapRenderData {
return {
entityId,
layerIndex,
transforms: new Float32Array(0),
textureIds: new Uint32Array(0),
uvs: new Float32Array(0),
colors: new Uint32Array(0),
tileCount: 0,
sortingOrder: sortingOrder + layerIndex * 0.001,
materialId: materialId ?? 0
};
}
protected override onRemoved(entity: Entity): void {
this._renderDataCache.delete(entity.id);
// Remove all cached layer data for this entity
// 移除此实体的所有缓存图层数据
const keysToDelete: LayerCacheKey[] = [];
for (const key of this._layerRenderDataCache.keys()) {
if (key.startsWith(`${entity.id}_`)) {
keysToDelete.push(key);
}
}
for (const key of keysToDelete) {
this._layerRenderDataCache.delete(key);
}
}
/**
* Clear all cached render data
* 清除所有缓存的渲染数据
*/
clearCache(): void {
this._renderDataCache.clear();
this._layerRenderDataCache.clear();
this._currentFrameData = [];
}
}