Files
esengine/packages/tilemap/src/systems/TilemapRenderingSystem.ts
yhh ad96edfad0 fix: 恢复 @esengine/ecs-framework 包名
上一个提交错误地将 npm 包名也改了,这里恢复正确的包名。
只更新 GitHub 仓库 URL,不改变 npm 包名。
2025-12-08 21:26:35 +08:00

415 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, type ITilemapLayerData } from '../TilemapComponent';
/**
* 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;
}
/**
* 视口边界
*/
export interface ViewportBounds {
left: number;
right: number;
top: number;
bottom: number;
}
/**
* 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 {
/** 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;
}
protected override onBegin(): void {
this._currentFrameData = [];
}
protected override process(entities: readonly Entity[]): void {
for (const entity of entities) {
if (!entity.enabled) continue;
const tilemap = entity.getComponent(TilemapComponent) as TilemapComponent | null;
const transform = entity.getComponent(TransformComponent) as TransformComponent | null;
if (!tilemap || !transform || !tilemap.visible) continue;
// Process each layer separately
// 分别处理每个图层
const layers = tilemap.layers;
for (let layerIndex = 0; layerIndex < layers.length; layerIndex++) {
const layer = layers[layerIndex];
if (!layer.visible) continue;
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);
}
}
// 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);
}
/**
* Build render data for a single layer
* 为单个图层构建渲染数据
*/
private buildLayerRenderData(
entityId: number,
layerIndex: number,
tilemap: TilemapComponent,
transform: TransformComponent,
layer: ITilemapLayerData
): TilemapRenderData {
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
// 计算可见瓦片范围
const { startCol, endCol, startRow, endRow } = this.calculateVisibleRange(
width, height, tileWidth, tileHeight, transform
);
// 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 (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);
// Calculate color with layer opacity
// 计算带有图层透明度的颜色
const effectiveAlpha = tilemap.alpha * layer.opacity;
const colorValue = Color.packHexAlpha(tilemap.color, effectiveAlpha);
// 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;
let texturePath: string | undefined;
for (let row = startRow; row < endRow; row++) {
for (let col = startCol; col < endCol; col++) {
const gid = layerData[row * width + col];
if (gid <= 0) continue;
// Find corresponding tileset
// 查找对应的 tileset
const tilesetInfo = tilemap.getTilesetForGid(gid);
if (!tilesetInfo) continue;
const { index: tilesetIndex, localId } = tilesetInfo;
// Get texture path
// 获取纹理路径
if (!texturePath && tilemap.tilesets[tilesetIndex]) {
texturePath = tilemap.tilesets[tilesetIndex].source;
}
// 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]
const tOffset = idx * 7;
transforms[tOffset] = rotatedX;
transforms[tOffset + 1] = rotatedY;
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
textureIds[idx] = tilemap.tilesets[tilesetIndex]?.textureId || 0;
// UV coordinates
const tileUV = tilemap.getTileUV(tilesetIndex, localId);
const uvOffset = idx * 4;
if (tileUV) {
uvs[uvOffset] = tileUV[0];
uvs[uvOffset + 1] = tileUV[1];
uvs[uvOffset + 2] = tileUV[2];
uvs[uvOffset + 3] = tileUV[3];
} else {
uvs[uvOffset] = 0;
uvs[uvOffset + 1] = 0;
uvs[uvOffset + 2] = 1;
uvs[uvOffset + 3] = 1;
}
colors[idx] = colorValue;
idx++;
}
}
return {
entityId,
layerIndex,
transforms,
textureIds,
uvs,
colors,
tileCount,
sortingOrder: tilemap.sortingOrder + layerIndex * 0.001,
texturePath,
materialId: layer.materialId ?? 0
};
}
/**
* Update transforms for a layer (when only position/rotation/scale changed)
* 更新图层的变换(当只有位置/旋转/缩放改变时)
*/
private updateLayerTransforms(
renderData: TilemapRenderData,
layerIndex: number,
tilemap: TilemapComponent,
transform: TransformComponent,
layer: ITilemapLayerData
): void {
const layerData = tilemap.getLayerData(layerIndex);
if (!layerData) return;
const width = tilemap.width;
const height = tilemap.height;
const tileWidth = tilemap.tileWidth;
const tileHeight = tilemap.tileHeight;
// 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;
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));
}
return { startCol, endCol, startRow, endRow };
}
/**
* 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 {
// 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._layerRenderDataCache.clear();
this._currentFrameData = [];
}
}