322 lines
12 KiB
TypeScript
322 lines
12 KiB
TypeScript
|
|
import { EntitySystem, Matcher, ECSSystem, Entity } from '@esengine/ecs-framework';
|
|||
|
|
import { TransformComponent } from '@esengine/ecs-components';
|
|||
|
|
import type { IRenderDataProvider } from '@esengine/ecs-engine-bindgen';
|
|||
|
|
import { TilemapComponent } from '../TilemapComponent';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Tilemap render data for a single tilemap
|
|||
|
|
*/
|
|||
|
|
export interface TilemapRenderData {
|
|||
|
|
entityId: number;
|
|||
|
|
transforms: Float32Array;
|
|||
|
|
textureIds: Uint32Array;
|
|||
|
|
uvs: Float32Array;
|
|||
|
|
colors: Uint32Array;
|
|||
|
|
tileCount: number;
|
|||
|
|
sortingOrder: number;
|
|||
|
|
texturePath?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 视口边界
|
|||
|
|
*/
|
|||
|
|
export interface ViewportBounds {
|
|||
|
|
left: number;
|
|||
|
|
right: number;
|
|||
|
|
top: number;
|
|||
|
|
bottom: number;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 瓦片地图渲染系统 - 准备瓦片地图渲染数据
|
|||
|
|
*/
|
|||
|
|
@ECSSystem('TilemapRendering', { updateOrder: 40 })
|
|||
|
|
export class TilemapRenderingSystem extends EntitySystem implements IRenderDataProvider {
|
|||
|
|
private _renderDataCache: Map<number, TilemapRenderData> = new Map();
|
|||
|
|
private _currentFrameData: TilemapRenderData[] = [];
|
|||
|
|
private _viewportBounds: ViewportBounds | null = null;
|
|||
|
|
|
|||
|
|
constructor() {
|
|||
|
|
super(Matcher.empty().all(TilemapComponent, TransformComponent));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setViewportBounds(bounds: ViewportBounds): void {
|
|||
|
|
this._viewportBounds = bounds;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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;
|
|||
|
|
|
|||
|
|
let renderData = this._renderDataCache.get(entity.id);
|
|||
|
|
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this._currentFrameData.push(renderData);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this._currentFrameData.sort((a, b) => a.sortingOrder - b.sortingOrder);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private buildRenderData(
|
|||
|
|
entityId: number,
|
|||
|
|
tilemap: TilemapComponent,
|
|||
|
|
transform: TransformComponent
|
|||
|
|
): TilemapRenderData {
|
|||
|
|
const mergedData = tilemap.getMergedTileData();
|
|||
|
|
const width = tilemap.width;
|
|||
|
|
const height = tilemap.height;
|
|||
|
|
const tileWidth = tilemap.tileWidth;
|
|||
|
|
const tileHeight = tilemap.tileHeight;
|
|||
|
|
|
|||
|
|
// 计算可见瓦片范围
|
|||
|
|
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));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 计算非空瓦片数量
|
|||
|
|
let tileCount = 0;
|
|||
|
|
for (let row = startRow; row < endRow; row++) {
|
|||
|
|
for (let col = startCol; col < endCol; col++) {
|
|||
|
|
if (mergedData[row * width + col] > 0) tileCount++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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 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)
|
|||
|
|
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 = mergedData[row * width + col];
|
|||
|
|
if (gid <= 0) continue;
|
|||
|
|
|
|||
|
|
// 查找对应的 tileset
|
|||
|
|
const tilesetInfo = tilemap.getTilesetForGid(gid);
|
|||
|
|
if (!tilesetInfo) {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const { index: tilesetIndex, localId } = tilesetInfo;
|
|||
|
|
|
|||
|
|
// 获取纹理路径
|
|||
|
|
if (!texturePath && tilemap.tilesets[tilesetIndex]) {
|
|||
|
|
texturePath = tilemap.tilesets[tilesetIndex].source;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 计算瓦片的本地位置(相对于 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;
|
|||
|
|
|
|||
|
|
// 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 + 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)
|
|||
|
|
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,
|
|||
|
|
transforms,
|
|||
|
|
textureIds,
|
|||
|
|
uvs,
|
|||
|
|
colors,
|
|||
|
|
tileCount,
|
|||
|
|
sortingOrder: tilemap.sortingOrder,
|
|||
|
|
texturePath
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private updateTransforms(
|
|||
|
|
renderData: TilemapRenderData,
|
|||
|
|
tilemap: TilemapComponent,
|
|||
|
|
transform: TransformComponent
|
|||
|
|
): void {
|
|||
|
|
const mergedData = tilemap.getMergedTileData();
|
|||
|
|
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;
|
|||
|
|
|
|||
|
|
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));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 计算旋转参数
|
|||
|
|
// 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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
protected override onRemoved(entity: Entity): void {
|
|||
|
|
this._renderDataCache.delete(entity.id);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
clearCache(): void {
|
|||
|
|
this._renderDataCache.clear();
|
|||
|
|
this._currentFrameData = [];
|
|||
|
|
}
|
|||
|
|
}
|