refactor: reorganize package structure and decouple framework packages (#338)
* refactor: reorganize package structure and decouple framework packages ## Package Structure Reorganization - Reorganized 55 packages into categorized subdirectories: - packages/framework/ - Generic framework (Laya/Cocos compatible) - packages/engine/ - ESEngine core modules - packages/rendering/ - Rendering modules (WASM dependent) - packages/physics/ - Physics modules - packages/streaming/ - World streaming - packages/network-ext/ - Network extensions - packages/editor/ - Editor framework and plugins - packages/rust/ - Rust WASM engine - packages/tools/ - Build tools and SDK ## Framework Package Decoupling - Decoupled behavior-tree and blueprint packages from ESEngine dependencies - Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent) - ESEngine-specific code moved to esengine/ subpath exports - Framework packages now usable with Cocos/Laya without ESEngine ## CI Configuration - Updated CI to only type-check and lint framework packages - Added type-check:framework and lint:framework scripts ## Breaking Changes - Package import paths changed due to directory reorganization - ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine') * fix: update es-engine file path after directory reorganization * docs: update README to focus on framework over engine * ci: only build framework packages, remove Rust/WASM dependencies * fix: remove esengine subpath from behavior-tree and blueprint builds ESEngine integration code will only be available in full engine builds. Framework packages are now purely engine-agnostic. * fix: move network-protocols to framework, build both in CI * fix: update workflow paths from packages/core to packages/framework/core * fix: exclude esengine folder from type-check in behavior-tree and blueprint * fix: update network tsconfig references to new paths * fix: add test:ci:framework to only test framework packages in CI * fix: only build core and math npm packages in CI * fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
52
packages/rendering/tilemap/module.json
Normal file
52
packages/rendering/tilemap/module.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"id": "tilemap",
|
||||
"name": "@esengine/tilemap",
|
||||
"globalKey": "tilemap",
|
||||
"displayName": "Tilemap 2D",
|
||||
"description": "2D tilemap rendering and editing | 2D 瓦片地图渲染和编辑",
|
||||
"version": "1.0.0",
|
||||
"category": "Rendering",
|
||||
"icon": "Grid3X3",
|
||||
"tags": [
|
||||
"2d",
|
||||
"tilemap",
|
||||
"tiled"
|
||||
],
|
||||
"isCore": false,
|
||||
"defaultEnabled": false,
|
||||
"isEngineModule": true,
|
||||
"canContainContent": true,
|
||||
"platforms": [
|
||||
"web",
|
||||
"desktop"
|
||||
],
|
||||
"dependencies": [
|
||||
"core",
|
||||
"math",
|
||||
"sprite",
|
||||
"asset-system"
|
||||
],
|
||||
"externalDependencies": [
|
||||
"@esengine/physics-rapier2d"
|
||||
],
|
||||
"exports": {
|
||||
"components": [
|
||||
"TilemapComponent"
|
||||
],
|
||||
"systems": [
|
||||
"TilemapRenderingSystem"
|
||||
],
|
||||
"loaders": [
|
||||
"TilemapLoader",
|
||||
"TilesetLoader"
|
||||
]
|
||||
},
|
||||
"assetExtensions": {
|
||||
".tilemap.json": "tilemap",
|
||||
".tileset.json": "tileset"
|
||||
},
|
||||
"editorPackage": "@esengine/tilemap-editor",
|
||||
"requiresWasm": false,
|
||||
"outputPath": "dist/index.js",
|
||||
"pluginExport": "TilemapPlugin"
|
||||
}
|
||||
60
packages/rendering/tilemap/package.json
Normal file
60
packages/rendering/tilemap/package.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "@esengine/tilemap",
|
||||
"version": "1.0.0",
|
||||
"description": "Tilemap system for ECS Framework - supports Tiled editor import",
|
||||
"esengine": {
|
||||
"plugin": true,
|
||||
"pluginExport": "TilemapPlugin",
|
||||
"category": "tilemap"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"tilemap",
|
||||
"tiled",
|
||||
"game-engine",
|
||||
"2d",
|
||||
"typescript"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf dist tsconfig.tsbuildinfo",
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"rebuild": "npm run clean && npm run build"
|
||||
},
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/ecs-framework-math": "workspace:*",
|
||||
"@esengine/asset-system": "workspace:*",
|
||||
"@esengine/engine-core": "workspace:*",
|
||||
"@esengine/ecs-engine-bindgen": "workspace:*",
|
||||
"@esengine/physics-rapier2d": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"rimraf": "^5.0.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/esengine/esengine.git",
|
||||
"directory": "packages/tilemap"
|
||||
}
|
||||
}
|
||||
41
packages/rendering/tilemap/plugin.json
Normal file
41
packages/rendering/tilemap/plugin.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"id": "@esengine/tilemap",
|
||||
"name": "Tilemap System",
|
||||
"version": "1.0.0",
|
||||
"description": "瓦片地图系统,支持 Tiled 格式导入和高效渲染 | Tilemap system with Tiled format import and efficient rendering",
|
||||
"author": "ESEngine Team",
|
||||
"license": "MIT",
|
||||
"category": "rendering",
|
||||
"tags": ["tilemap", "tiled", "2d", "rendering"],
|
||||
"icon": "Grid3X3",
|
||||
"enabledByDefault": true,
|
||||
"canContainContent": true,
|
||||
"isEnginePlugin": true,
|
||||
"isCore": false,
|
||||
"modules": [
|
||||
{
|
||||
"name": "TilemapRuntime",
|
||||
"type": "runtime",
|
||||
"loadingPhase": "default",
|
||||
"entry": "./src/index.ts",
|
||||
"components": ["TilemapComponent"],
|
||||
"systems": ["TilemapRenderingSystem"]
|
||||
},
|
||||
{
|
||||
"name": "TilemapEditor",
|
||||
"type": "editor",
|
||||
"loadingPhase": "default",
|
||||
"entry": "./src/editor/index.ts",
|
||||
"panels": ["tilemap-editor"],
|
||||
"inspectors": ["TilemapInspectorProvider"],
|
||||
"gizmoProviders": ["TilemapGizmoProvider"]
|
||||
}
|
||||
],
|
||||
"dependencies": [
|
||||
{
|
||||
"id": "@esengine/core",
|
||||
"version": "^1.0.0"
|
||||
}
|
||||
],
|
||||
"platforms": ["web", "desktop"]
|
||||
}
|
||||
207
packages/rendering/tilemap/src/TilemapAnimationSystem.ts
Normal file
207
packages/rendering/tilemap/src/TilemapAnimationSystem.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* Tilemap Animation System
|
||||
* 瓦片地图动画系统
|
||||
*
|
||||
* Manages tile animation playback for all animated tiles in tilesets.
|
||||
* 管理图块集中所有动画瓦片的动画播放。
|
||||
*/
|
||||
|
||||
import type { ITilesetData, ITileMetadata } from './TilemapComponent';
|
||||
|
||||
/**
|
||||
* Animation state for a single animated tile
|
||||
* 单个动画瓦片的动画状态
|
||||
*/
|
||||
interface TileAnimationState {
|
||||
/** Current frame index | 当前帧索引 */
|
||||
currentFrame: number;
|
||||
/** Elapsed time since last frame change (ms) | 自上次帧变化以来的时间(毫秒) */
|
||||
elapsedTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tilemap Animation System
|
||||
* 瓦片地图动画系统
|
||||
*/
|
||||
export class TilemapAnimationSystem {
|
||||
/** Animation states keyed by "tilesetIndex:tileId" | 按"图块集索引:瓦片ID"索引的动画状态 */
|
||||
private animationStates: Map<string, TileAnimationState> = new Map();
|
||||
|
||||
/** Cached animated tile metadata for quick lookup | 缓存的动画瓦片元数据用于快速查找 */
|
||||
private animatedTiles: Map<string, ITileMetadata> = new Map();
|
||||
|
||||
/** Whether animations are playing | 动画是否正在播放 */
|
||||
private _isPlaying: boolean = true;
|
||||
|
||||
/**
|
||||
* Register a tileset's animated tiles
|
||||
* 注册图块集的动画瓦片
|
||||
*/
|
||||
registerTileset(tilesetIndex: number, tileset: ITilesetData): void {
|
||||
if (!tileset.tiles) return;
|
||||
|
||||
for (const tile of tileset.tiles) {
|
||||
if (tile.animation && tile.animation.frames.length > 0) {
|
||||
const key = `${tilesetIndex}:${tile.id}`;
|
||||
this.animatedTiles.set(key, tile);
|
||||
this.animationStates.set(key, {
|
||||
currentFrame: 0,
|
||||
elapsedTime: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a tileset
|
||||
* 注销图块集
|
||||
*/
|
||||
unregisterTileset(tilesetIndex: number): void {
|
||||
const keysToRemove: string[] = [];
|
||||
for (const key of this.animationStates.keys()) {
|
||||
if (key.startsWith(`${tilesetIndex}:`)) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
for (const key of keysToRemove) {
|
||||
this.animationStates.delete(key);
|
||||
this.animatedTiles.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all animation states
|
||||
* 清除所有动画状态
|
||||
*/
|
||||
clear(): void {
|
||||
this.animationStates.clear();
|
||||
this.animatedTiles.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all animations
|
||||
* 更新所有动画
|
||||
* @param deltaTime Time since last update in milliseconds | 自上次更新以来的时间(毫秒)
|
||||
*/
|
||||
update(deltaTime: number): void {
|
||||
if (!this._isPlaying) return;
|
||||
|
||||
for (const [key, state] of this.animationStates) {
|
||||
const tile = this.animatedTiles.get(key);
|
||||
if (!tile?.animation) continue;
|
||||
|
||||
const frames = tile.animation.frames;
|
||||
const currentFrame = frames[state.currentFrame];
|
||||
if (!currentFrame) continue;
|
||||
|
||||
state.elapsedTime += deltaTime;
|
||||
|
||||
// Advance frames while elapsed time exceeds frame duration
|
||||
while (state.elapsedTime >= currentFrame.duration) {
|
||||
state.elapsedTime -= currentFrame.duration;
|
||||
state.currentFrame = (state.currentFrame + 1) % frames.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current display tile ID for an animated tile
|
||||
* 获取动画瓦片的当前显示瓦片ID
|
||||
* @param tilesetIndex Tileset index | 图块集索引
|
||||
* @param tileId Original tile ID | 原始瓦片ID
|
||||
* @returns Current frame's tile ID, or original if not animated | 当前帧的瓦片ID,如果不是动画则返回原始ID
|
||||
*/
|
||||
getCurrentTileId(tilesetIndex: number, tileId: number): number {
|
||||
const key = `${tilesetIndex}:${tileId}`;
|
||||
const tile = this.animatedTiles.get(key);
|
||||
const state = this.animationStates.get(key);
|
||||
|
||||
if (!tile?.animation || !state) {
|
||||
return tileId;
|
||||
}
|
||||
|
||||
const frame = tile.animation.frames[state.currentFrame];
|
||||
return frame?.tileId ?? tileId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tile has animation
|
||||
* 检查瓦片是否有动画
|
||||
*/
|
||||
hasAnimation(tilesetIndex: number, tileId: number): boolean {
|
||||
const key = `${tilesetIndex}:${tileId}`;
|
||||
return this.animatedTiles.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get animation metadata for a tile
|
||||
* 获取瓦片的动画元数据
|
||||
*/
|
||||
getAnimation(tilesetIndex: number, tileId: number): ITileMetadata | undefined {
|
||||
const key = `${tilesetIndex}:${tileId}`;
|
||||
return this.animatedTiles.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset animation to first frame
|
||||
* 重置动画到第一帧
|
||||
*/
|
||||
resetAnimation(tilesetIndex: number, tileId: number): void {
|
||||
const key = `${tilesetIndex}:${tileId}`;
|
||||
const state = this.animationStates.get(key);
|
||||
if (state) {
|
||||
state.currentFrame = 0;
|
||||
state.elapsedTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all animations to first frame
|
||||
* 重置所有动画到第一帧
|
||||
*/
|
||||
resetAll(): void {
|
||||
for (const state of this.animationStates.values()) {
|
||||
state.currentFrame = 0;
|
||||
state.elapsedTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play/pause animations
|
||||
* 播放/暂停动画
|
||||
*/
|
||||
get isPlaying(): boolean {
|
||||
return this._isPlaying;
|
||||
}
|
||||
|
||||
set isPlaying(value: boolean) {
|
||||
this._isPlaying = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle play/pause
|
||||
* 切换播放/暂停
|
||||
*/
|
||||
togglePlayback(): boolean {
|
||||
this._isPlaying = !this._isPlaying;
|
||||
return this._isPlaying;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all animated tile IDs for a tileset
|
||||
* 获取图块集的所有动画瓦片ID
|
||||
*/
|
||||
getAnimatedTileIds(tilesetIndex: number): number[] {
|
||||
const ids: number[] = [];
|
||||
for (const key of this.animatedTiles.keys()) {
|
||||
if (key.startsWith(`${tilesetIndex}:`)) {
|
||||
const tileId = parseInt(key.split(':')[1], 10);
|
||||
ids.push(tileId);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
|
||||
/** Global animation system instance | 全局动画系统实例 */
|
||||
export const tilemapAnimationSystem = new TilemapAnimationSystem();
|
||||
1257
packages/rendering/tilemap/src/TilemapComponent.ts
Normal file
1257
packages/rendering/tilemap/src/TilemapComponent.ts
Normal file
File diff suppressed because it is too large
Load Diff
94
packages/rendering/tilemap/src/TilemapRuntimeModule.ts
Normal file
94
packages/rendering/tilemap/src/TilemapRuntimeModule.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { IScene, IComponentRegistry } from '@esengine/ecs-framework';
|
||||
import type { IRuntimeModule, IRuntimePlugin, ModuleManifest, SystemContext } from '@esengine/engine-core';
|
||||
import { AssetManagerToken } from '@esengine/asset-system';
|
||||
import { RenderSystemToken } from '@esengine/ecs-engine-bindgen';
|
||||
import { Physics2DWorldToken } from '@esengine/physics-rapier2d';
|
||||
|
||||
import { TilemapComponent } from './TilemapComponent';
|
||||
import { TilemapRenderingSystem } from './systems/TilemapRenderingSystem';
|
||||
import { TilemapCollider2DComponent } from './physics/TilemapCollider2DComponent';
|
||||
import { TilemapPhysicsSystem } from './physics/TilemapPhysicsSystem';
|
||||
import { TilemapLoader } from './loaders/TilemapLoader';
|
||||
import { TilemapAssetType } from './constants';
|
||||
import {
|
||||
TilemapSystemToken,
|
||||
TilemapPhysicsSystemToken
|
||||
} from './tokens';
|
||||
|
||||
// 重新导出 tokens | Re-export tokens
|
||||
export {
|
||||
TilemapSystemToken,
|
||||
TilemapPhysicsSystemToken
|
||||
} from './tokens';
|
||||
|
||||
class TilemapRuntimeModule implements IRuntimeModule {
|
||||
private _tilemapPhysicsSystem: TilemapPhysicsSystem | null = null;
|
||||
private _loaderRegistered = false;
|
||||
|
||||
registerComponents(registry: IComponentRegistry): void {
|
||||
registry.register(TilemapComponent);
|
||||
registry.register(TilemapCollider2DComponent);
|
||||
}
|
||||
|
||||
createSystems(scene: IScene, context: SystemContext): void {
|
||||
// 从服务注册表获取依赖 | Get dependencies from service registry
|
||||
const assetManager = context.services.get(AssetManagerToken);
|
||||
const renderSystem = context.services.get(RenderSystemToken);
|
||||
|
||||
if (!this._loaderRegistered && assetManager) {
|
||||
assetManager.registerLoader(TilemapAssetType, new TilemapLoader());
|
||||
this._loaderRegistered = true;
|
||||
}
|
||||
|
||||
const tilemapSystem = new TilemapRenderingSystem();
|
||||
scene.addSystem(tilemapSystem);
|
||||
|
||||
if (renderSystem) {
|
||||
renderSystem.addRenderDataProvider(tilemapSystem);
|
||||
}
|
||||
|
||||
this._tilemapPhysicsSystem = new TilemapPhysicsSystem();
|
||||
scene.addSystem(this._tilemapPhysicsSystem);
|
||||
|
||||
// 注册服务到服务注册表 | Register services to service registry
|
||||
context.services.register(TilemapSystemToken, tilemapSystem);
|
||||
context.services.register(TilemapPhysicsSystemToken, this._tilemapPhysicsSystem);
|
||||
}
|
||||
|
||||
onSystemsCreated(_scene: IScene, context: SystemContext): void {
|
||||
// 从服务注册表获取物理世界 | Get physics world from service registry
|
||||
const physics2DWorld = context.services.get(Physics2DWorldToken);
|
||||
|
||||
if (this._tilemapPhysicsSystem && physics2DWorld) {
|
||||
this._tilemapPhysicsSystem.setPhysicsWorld(physics2DWorld);
|
||||
}
|
||||
}
|
||||
|
||||
get tilemapPhysicsSystem(): TilemapPhysicsSystem | null {
|
||||
return this._tilemapPhysicsSystem;
|
||||
}
|
||||
}
|
||||
|
||||
const manifest: ModuleManifest = {
|
||||
id: 'tilemap',
|
||||
name: '@esengine/tilemap',
|
||||
displayName: 'Tilemap 2D',
|
||||
version: '1.0.0',
|
||||
description: 'Tilemap system with Tiled editor support',
|
||||
category: 'Rendering',
|
||||
icon: 'Grid3X3',
|
||||
isCore: false,
|
||||
defaultEnabled: false,
|
||||
isEngineModule: true,
|
||||
canContainContent: true,
|
||||
dependencies: ['core', 'math', 'sprite', 'asset-system'],
|
||||
exports: { components: ['TilemapComponent', 'TilemapCollider2DComponent'] },
|
||||
editorPackage: '@esengine/tilemap-editor'
|
||||
};
|
||||
|
||||
export const TilemapPlugin: IRuntimePlugin = {
|
||||
manifest,
|
||||
runtimeModule: new TilemapRuntimeModule()
|
||||
};
|
||||
|
||||
export { TilemapRuntimeModule };
|
||||
9
packages/rendering/tilemap/src/constants.ts
Normal file
9
packages/rendering/tilemap/src/constants.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Tilemap Constants
|
||||
* 瓦片地图常量
|
||||
*/
|
||||
|
||||
// Asset type constants for tilemap
|
||||
// 瓦片地图资产类型常量
|
||||
export const TilemapAssetType = 'tilemap' as const;
|
||||
export const TilesetAssetType = 'tileset' as const;
|
||||
44
packages/rendering/tilemap/src/index.ts
Normal file
44
packages/rendering/tilemap/src/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Tilemap System for ECS Framework
|
||||
* ECS框架的瓦片地图系统
|
||||
*/
|
||||
|
||||
// Constants
|
||||
export { TilemapAssetType, TilesetAssetType } from './constants';
|
||||
|
||||
// Component
|
||||
export { TilemapComponent } from './TilemapComponent';
|
||||
export type { ITilemapData, ITilesetData, ITileMetadata, ITileAnimation, ITileAnimationFrame } from './TilemapComponent';
|
||||
export type { ResizeAnchor } from './TilemapComponent';
|
||||
|
||||
// Animation System
|
||||
export { TilemapAnimationSystem, tilemapAnimationSystem } from './TilemapAnimationSystem';
|
||||
|
||||
// Systems
|
||||
export { TilemapRenderingSystem } from './systems/TilemapRenderingSystem';
|
||||
export type { TilemapRenderData, ViewportBounds } from './systems/TilemapRenderingSystem';
|
||||
|
||||
// Physics
|
||||
export { TilemapCollider2DComponent, TilemapColliderMode } from './physics/TilemapCollider2DComponent';
|
||||
export type { CollisionRect } from './physics/TilemapCollider2DComponent';
|
||||
export { TilemapPhysicsSystem } from './physics/TilemapPhysicsSystem';
|
||||
export type { IPhysicsWorld, IPhysics2DSystem } from './physics/TilemapPhysicsSystem';
|
||||
|
||||
// Loaders
|
||||
export { TilemapLoader } from './loaders/TilemapLoader';
|
||||
export type { ITilemapAsset } from './loaders/TilemapLoader';
|
||||
export { TilesetLoader } from './loaders/TilesetLoader';
|
||||
export type { ITilesetAsset } from './loaders/TilesetLoader';
|
||||
|
||||
// Tiled converter
|
||||
export { TiledConverter } from './loaders/TiledConverter';
|
||||
export type { ITiledMap, ITiledConversionResult } from './loaders/TiledConverter';
|
||||
|
||||
// Runtime module and plugin
|
||||
export { TilemapRuntimeModule, TilemapPlugin } from './TilemapRuntimeModule';
|
||||
|
||||
// Service tokens | 服务令牌
|
||||
export {
|
||||
TilemapSystemToken,
|
||||
TilemapPhysicsSystemToken
|
||||
} from './tokens';
|
||||
332
packages/rendering/tilemap/src/loaders/TiledConverter.ts
Normal file
332
packages/rendering/tilemap/src/loaders/TiledConverter.ts
Normal file
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* Tiled Map Editor format converter
|
||||
* Tiled 地图编辑器格式转换器
|
||||
*
|
||||
* Converts Tiled JSON export format to our internal tilemap/tileset format.
|
||||
* 将 Tiled JSON 导出格式转换为内部 tilemap/tileset 格式。
|
||||
*/
|
||||
|
||||
import { ITilemapAsset } from './TilemapLoader';
|
||||
import { ITilesetAsset } from './TilesetLoader';
|
||||
|
||||
/**
|
||||
* Tiled map JSON format (exported from Tiled)
|
||||
* Tiled 地图 JSON 格式
|
||||
*/
|
||||
export interface ITiledMap {
|
||||
/** Map width in tiles */
|
||||
width: number;
|
||||
/** Map height in tiles */
|
||||
height: number;
|
||||
/** Tile width in pixels */
|
||||
tilewidth: number;
|
||||
/** Tile height in pixels */
|
||||
tileheight: number;
|
||||
/** Map orientation (orthogonal, isometric, etc.) */
|
||||
orientation: string;
|
||||
/** Render order */
|
||||
renderorder: string;
|
||||
/** Layers array */
|
||||
layers: ITiledLayer[];
|
||||
/** Tilesets array */
|
||||
tilesets: ITiledTileset[];
|
||||
/** Custom properties */
|
||||
properties?: ITiledProperty[];
|
||||
/** Tiled version */
|
||||
tiledversion?: string;
|
||||
/** Map version */
|
||||
version?: string | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tiled layer format
|
||||
* Tiled 图层格式
|
||||
*/
|
||||
export interface ITiledLayer {
|
||||
/** Layer name */
|
||||
name: string;
|
||||
/** Layer type (tilelayer, objectgroup, imagelayer, group) */
|
||||
type: string;
|
||||
/** Layer ID */
|
||||
id: number;
|
||||
/** Layer width in tiles */
|
||||
width?: number;
|
||||
/** Layer height in tiles */
|
||||
height?: number;
|
||||
/** Tile data (for tilelayer) */
|
||||
data?: number[];
|
||||
/** Layer visibility */
|
||||
visible: boolean;
|
||||
/** Layer opacity (0-1) */
|
||||
opacity: number;
|
||||
/** Layer X offset */
|
||||
x: number;
|
||||
/** Layer Y offset */
|
||||
y: number;
|
||||
/** Objects (for objectgroup) */
|
||||
objects?: ITiledObject[];
|
||||
/** Custom properties */
|
||||
properties?: ITiledProperty[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tiled object format
|
||||
* Tiled 对象格式
|
||||
*/
|
||||
export interface ITiledObject {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
rotation: number;
|
||||
visible: boolean;
|
||||
properties?: ITiledProperty[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tiled tileset format
|
||||
* Tiled 瓦片集格式
|
||||
*/
|
||||
export interface ITiledTileset {
|
||||
/** First GID (global tile ID) */
|
||||
firstgid: number;
|
||||
/** Tileset name */
|
||||
name: string;
|
||||
/** Image path */
|
||||
image: string;
|
||||
/** Image width */
|
||||
imagewidth: number;
|
||||
/** Image height */
|
||||
imageheight: number;
|
||||
/** Tile width */
|
||||
tilewidth: number;
|
||||
/** Tile height */
|
||||
tileheight: number;
|
||||
/** Tile count */
|
||||
tilecount: number;
|
||||
/** Columns */
|
||||
columns: number;
|
||||
/** Margin */
|
||||
margin: number;
|
||||
/** Spacing */
|
||||
spacing: number;
|
||||
/** Tile properties/metadata */
|
||||
tiles?: ITiledTile[];
|
||||
/** External tileset source (if embedded) */
|
||||
source?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tiled tile metadata
|
||||
* Tiled 瓦片元数据
|
||||
*/
|
||||
export interface ITiledTile {
|
||||
id: number;
|
||||
type?: string;
|
||||
properties?: ITiledProperty[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tiled property format
|
||||
* Tiled 属性格式
|
||||
*/
|
||||
export interface ITiledProperty {
|
||||
name: string;
|
||||
type: string;
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conversion result
|
||||
* 转换结果
|
||||
*/
|
||||
export interface ITiledConversionResult {
|
||||
/** Converted tilemap */
|
||||
tilemap: ITilemapAsset;
|
||||
/** Converted tilesets */
|
||||
tilesets: ITilesetAsset[];
|
||||
/** Object layers (if any) */
|
||||
objects: Array<{
|
||||
name: string;
|
||||
objects: ITiledObject[];
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tiled format converter
|
||||
* Tiled 格式转换器
|
||||
*/
|
||||
export class TiledConverter {
|
||||
/**
|
||||
* Convert Tiled JSON map to internal format
|
||||
* 将 Tiled JSON 地图转换为内部格式
|
||||
*/
|
||||
static convert(tiledMap: ITiledMap, mapName: string = 'tilemap'): ITiledConversionResult {
|
||||
// Convert tilesets
|
||||
const tilesets = tiledMap.tilesets.map(ts => this.convertTileset(ts));
|
||||
|
||||
// Find the first tile layer for main data
|
||||
const tileLayer = tiledMap.layers.find(l => l.type === 'tilelayer');
|
||||
|
||||
// Convert tile data (Tiled uses global IDs, we need to adjust)
|
||||
let data: number[] = [];
|
||||
if (tileLayer && tileLayer.data) {
|
||||
data = this.convertTileData(tileLayer.data, tiledMap.tilesets);
|
||||
}
|
||||
|
||||
// Extract collision layer if exists
|
||||
const collisionLayer = tiledMap.layers.find(
|
||||
l => l.type === 'tilelayer' &&
|
||||
(l.name.toLowerCase().includes('collision') || l.name.toLowerCase().includes('solid'))
|
||||
);
|
||||
const collisionData = collisionLayer?.data
|
||||
? this.convertTileData(collisionLayer.data, tiledMap.tilesets)
|
||||
: undefined;
|
||||
|
||||
// Collect all layers
|
||||
const layers = tiledMap.layers
|
||||
.filter(l => l.type === 'tilelayer')
|
||||
.map(l => ({
|
||||
name: l.name,
|
||||
visible: l.visible,
|
||||
opacity: l.opacity,
|
||||
data: l.data ? this.convertTileData(l.data, tiledMap.tilesets) : undefined
|
||||
}));
|
||||
|
||||
// Collect object layers
|
||||
const objects = tiledMap.layers
|
||||
.filter(l => l.type === 'objectgroup')
|
||||
.map(l => ({
|
||||
name: l.name,
|
||||
objects: l.objects || []
|
||||
}));
|
||||
|
||||
// Convert properties
|
||||
const properties: Record<string, unknown> = {};
|
||||
if (tiledMap.properties) {
|
||||
for (const prop of tiledMap.properties) {
|
||||
properties[prop.name] = prop.value;
|
||||
}
|
||||
}
|
||||
|
||||
const tilemap: ITilemapAsset = {
|
||||
name: mapName,
|
||||
version: 1,
|
||||
width: tiledMap.width,
|
||||
height: tiledMap.height,
|
||||
tileWidth: tiledMap.tilewidth,
|
||||
tileHeight: tiledMap.tileheight,
|
||||
tileset: tilesets.length > 0 ? tilesets[0].name : '',
|
||||
data,
|
||||
layers: layers.length > 1 ? layers : undefined,
|
||||
collisionData,
|
||||
properties
|
||||
};
|
||||
|
||||
return {
|
||||
tilemap,
|
||||
tilesets,
|
||||
objects
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Tiled tileset to internal format
|
||||
* 将 Tiled 瓦片集转换为内部格式
|
||||
*/
|
||||
private static convertTileset(tiledTileset: ITiledTileset): ITilesetAsset {
|
||||
const tiles = tiledTileset.tiles?.map(t => ({
|
||||
id: t.id,
|
||||
type: t.type,
|
||||
properties: t.properties?.reduce((acc, p) => {
|
||||
acc[p.name] = p.value;
|
||||
return acc;
|
||||
}, {} as Record<string, unknown>)
|
||||
}));
|
||||
|
||||
return {
|
||||
name: tiledTileset.name,
|
||||
version: 1,
|
||||
image: tiledTileset.image,
|
||||
imageWidth: tiledTileset.imagewidth,
|
||||
imageHeight: tiledTileset.imageheight,
|
||||
tileWidth: tiledTileset.tilewidth,
|
||||
tileHeight: tiledTileset.tileheight,
|
||||
tileCount: tiledTileset.tilecount,
|
||||
columns: tiledTileset.columns,
|
||||
rows: Math.ceil(tiledTileset.tilecount / tiledTileset.columns),
|
||||
margin: tiledTileset.margin || 0,
|
||||
spacing: tiledTileset.spacing || 0,
|
||||
tiles
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Tiled tile data (global IDs to local IDs)
|
||||
* 转换 Tiled 瓦片数据(全局ID到本地ID)
|
||||
*
|
||||
* Tiled uses global tile IDs where each tileset has a firstgid.
|
||||
* We convert to local IDs starting from 1 (0 = empty).
|
||||
*/
|
||||
private static convertTileData(data: number[], tilesets: ITiledTileset[]): number[] {
|
||||
if (tilesets.length === 0) return data;
|
||||
|
||||
// For single tileset, simple conversion
|
||||
if (tilesets.length === 1) {
|
||||
const firstgid = tilesets[0].firstgid;
|
||||
return data.map(gid => {
|
||||
if (gid === 0) return 0;
|
||||
// Clear flip flags (high bits in Tiled)
|
||||
const tileId = gid & 0x1FFFFFFF;
|
||||
return tileId - firstgid + 1;
|
||||
});
|
||||
}
|
||||
|
||||
// For multiple tilesets, find which tileset each tile belongs to
|
||||
return data.map(gid => {
|
||||
if (gid === 0) return 0;
|
||||
|
||||
const tileId = gid & 0x1FFFFFFF;
|
||||
|
||||
// Find the tileset this tile belongs to
|
||||
let tileset: ITiledTileset | null = null;
|
||||
for (let i = tilesets.length - 1; i >= 0; i--) {
|
||||
if (tileId >= tilesets[i].firstgid) {
|
||||
tileset = tilesets[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!tileset) return 0;
|
||||
|
||||
return tileId - tileset.firstgid + 1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Tiled JSON string
|
||||
* 解析 Tiled JSON 字符串
|
||||
*/
|
||||
static parse(jsonString: string): ITiledMap {
|
||||
return JSON.parse(jsonString) as ITiledMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert and stringify to internal format
|
||||
* 转换并序列化为内部格式
|
||||
*/
|
||||
static convertToJson(tiledMap: ITiledMap, mapName?: string): {
|
||||
tilemapJson: string;
|
||||
tilesetJsons: string[];
|
||||
} {
|
||||
const result = this.convert(tiledMap, mapName);
|
||||
|
||||
return {
|
||||
tilemapJson: JSON.stringify(result.tilemap, null, 2),
|
||||
tilesetJsons: result.tilesets.map(ts => JSON.stringify(ts, null, 2))
|
||||
};
|
||||
}
|
||||
}
|
||||
93
packages/rendering/tilemap/src/loaders/TilemapLoader.ts
Normal file
93
packages/rendering/tilemap/src/loaders/TilemapLoader.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Tilemap asset loader
|
||||
* 瓦片地图资产加载器
|
||||
*/
|
||||
|
||||
import {
|
||||
IAssetLoader,
|
||||
IAssetContent,
|
||||
IAssetParseContext,
|
||||
AssetContentType
|
||||
} from '@esengine/asset-system';
|
||||
import { TilemapAssetType } from '../constants';
|
||||
|
||||
/**
|
||||
* Tilemap data interface
|
||||
* 瓦片地图数据接口
|
||||
*/
|
||||
export interface ITilemapAsset {
|
||||
/** 名称 */
|
||||
name: string;
|
||||
/** 版本 */
|
||||
version: number;
|
||||
/** 宽度(瓦片数) */
|
||||
width: number;
|
||||
/** 高度(瓦片数) */
|
||||
height: number;
|
||||
/** 瓦片宽度(像素) */
|
||||
tileWidth: number;
|
||||
/** 瓦片高度(像素) */
|
||||
tileHeight: number;
|
||||
/** 瓦片集资源GUID */
|
||||
tileset: string;
|
||||
/** 瓦片数据(行主序,0表示空) */
|
||||
data: number[];
|
||||
/** 图层(可选) */
|
||||
layers?: Array<{
|
||||
name: string;
|
||||
visible: boolean;
|
||||
opacity: number;
|
||||
data?: number[];
|
||||
/** 材质路径 */
|
||||
materialPath?: string;
|
||||
}>;
|
||||
/** 碰撞数据(可选) */
|
||||
collisionData?: number[];
|
||||
/** 自定义属性 */
|
||||
properties?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tilemap loader implementation
|
||||
* 瓦片地图加载器实现
|
||||
*/
|
||||
export class TilemapLoader implements IAssetLoader<ITilemapAsset> {
|
||||
readonly supportedType = TilemapAssetType;
|
||||
readonly supportedExtensions = ['.tilemap.json', '.tilemap'];
|
||||
readonly contentType: AssetContentType = 'text';
|
||||
|
||||
/**
|
||||
* Parse tilemap asset from text content
|
||||
* 从文本内容解析瓦片地图资产
|
||||
*/
|
||||
async parse(content: IAssetContent, _context: IAssetParseContext): Promise<ITilemapAsset> {
|
||||
if (!content.text) {
|
||||
throw new Error('Tilemap content is empty');
|
||||
}
|
||||
|
||||
const jsonData = JSON.parse(content.text) as ITilemapAsset;
|
||||
|
||||
// 验证必要字段
|
||||
// Validate required fields
|
||||
if (!jsonData.width || !jsonData.height || !jsonData.data) {
|
||||
throw new Error('Invalid tilemap format: missing required fields');
|
||||
}
|
||||
|
||||
return jsonData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose loaded asset
|
||||
* 释放已加载的资产
|
||||
*/
|
||||
dispose(asset: ITilemapAsset): void {
|
||||
// 清理瓦片数据 | Clean up tile data
|
||||
asset.data.length = 0;
|
||||
if (asset.layers) {
|
||||
asset.layers.length = 0;
|
||||
}
|
||||
if (asset.collisionData) {
|
||||
asset.collisionData.length = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
102
packages/rendering/tilemap/src/loaders/TilesetLoader.ts
Normal file
102
packages/rendering/tilemap/src/loaders/TilesetLoader.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Tileset asset loader
|
||||
* 瓦片集资产加载器
|
||||
*/
|
||||
|
||||
import {
|
||||
IAssetLoader,
|
||||
IAssetContent,
|
||||
IAssetParseContext,
|
||||
AssetContentType
|
||||
} from '@esengine/asset-system';
|
||||
import { TilesetAssetType } from '../constants';
|
||||
|
||||
/**
|
||||
* Tileset data interface
|
||||
* 瓦片集数据接口
|
||||
*/
|
||||
export interface ITilesetAsset {
|
||||
/** 名称 */
|
||||
name: string;
|
||||
/** 版本 */
|
||||
version: number;
|
||||
/** 纹理图像资源GUID或路径 */
|
||||
image: string;
|
||||
/** 图像宽度(像素) */
|
||||
imageWidth: number;
|
||||
/** 图像高度(像素) */
|
||||
imageHeight: number;
|
||||
/** 瓦片宽度(像素) */
|
||||
tileWidth: number;
|
||||
/** 瓦片高度(像素) */
|
||||
tileHeight: number;
|
||||
/** 瓦片总数 */
|
||||
tileCount: number;
|
||||
/** 列数 */
|
||||
columns: number;
|
||||
/** 行数 */
|
||||
rows: number;
|
||||
/** 边距(像素) */
|
||||
margin?: number;
|
||||
/** 间距(像素) */
|
||||
spacing?: number;
|
||||
/** 每个瓦片的元数据 */
|
||||
tiles?: Array<{
|
||||
id: number;
|
||||
type?: string;
|
||||
properties?: Record<string, unknown>;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tileset loader implementation
|
||||
* 瓦片集加载器实现
|
||||
*/
|
||||
export class TilesetLoader implements IAssetLoader<ITilesetAsset> {
|
||||
readonly supportedType = TilesetAssetType;
|
||||
readonly supportedExtensions = ['.tileset.json', '.tileset'];
|
||||
readonly contentType: AssetContentType = 'text';
|
||||
|
||||
/**
|
||||
* Parse tileset asset from text content
|
||||
* 从文本内容解析瓦片集资产
|
||||
*/
|
||||
async parse(content: IAssetContent, _context: IAssetParseContext): Promise<ITilesetAsset> {
|
||||
if (!content.text) {
|
||||
throw new Error('Tileset content is empty');
|
||||
}
|
||||
|
||||
const jsonData = JSON.parse(content.text) as ITilesetAsset;
|
||||
|
||||
// 验证必要字段
|
||||
// Validate required fields
|
||||
if (!jsonData.tileWidth || !jsonData.tileHeight || !jsonData.image) {
|
||||
throw new Error('Invalid tileset format: missing required fields');
|
||||
}
|
||||
|
||||
// 计算派生字段(如果未提供)
|
||||
// Calculate derived fields if not provided
|
||||
if (!jsonData.columns && jsonData.imageWidth) {
|
||||
jsonData.columns = Math.floor(jsonData.imageWidth / jsonData.tileWidth);
|
||||
}
|
||||
if (!jsonData.rows && jsonData.imageHeight) {
|
||||
jsonData.rows = Math.floor(jsonData.imageHeight / jsonData.tileHeight);
|
||||
}
|
||||
if (!jsonData.tileCount && jsonData.columns && jsonData.rows) {
|
||||
jsonData.tileCount = jsonData.columns * jsonData.rows;
|
||||
}
|
||||
|
||||
return jsonData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose loaded asset
|
||||
* 释放已加载的资产
|
||||
*/
|
||||
dispose(asset: ITilesetAsset): void {
|
||||
// 清理瓦片元数据 | Clean up tile metadata
|
||||
if (asset.tiles) {
|
||||
asset.tiles.length = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* TilemapCollider2D Component
|
||||
* Tilemap 碰撞体组件
|
||||
*
|
||||
* 将 TilemapComponent 的碰撞数据转换为物理碰撞体。
|
||||
* 使用优化算法合并相邻碰撞格子,减少碰撞体数量。
|
||||
*/
|
||||
|
||||
import { Component, Property, Serialize, ECSComponent, Serializable } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 碰撞体生成模式
|
||||
*/
|
||||
export enum TilemapColliderMode {
|
||||
/** 每个碰撞格子单独创建碰撞体 */
|
||||
PerTile = 0,
|
||||
/** 合并相邻碰撞格子为更大的矩形 */
|
||||
Merged = 1,
|
||||
/** 只生成边缘碰撞体(优化性能) */
|
||||
EdgeOnly = 2,
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并后的碰撞矩形
|
||||
*/
|
||||
export interface CollisionRect {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* TilemapCollider2D 组件
|
||||
*
|
||||
* 自动从同一实体的 TilemapComponent 读取碰撞数据,
|
||||
* 并生成对应的物理碰撞体。
|
||||
*/
|
||||
@ECSComponent('TilemapCollider2D')
|
||||
@Serializable({ version: 1, typeId: 'TilemapCollider2D' })
|
||||
export class TilemapCollider2DComponent extends Component {
|
||||
/**
|
||||
* 碰撞体生成模式
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'enum',
|
||||
label: 'Collider Mode',
|
||||
options: [
|
||||
{ label: 'Per Tile', value: TilemapColliderMode.PerTile },
|
||||
{ label: 'Merged', value: TilemapColliderMode.Merged },
|
||||
{ label: 'Edge Only', value: TilemapColliderMode.EdgeOnly },
|
||||
],
|
||||
})
|
||||
public colliderMode: TilemapColliderMode = TilemapColliderMode.Merged;
|
||||
|
||||
/**
|
||||
* 碰撞层(该碰撞体所在的层)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'collisionLayer', label: 'Collision Layer' })
|
||||
public collisionLayer: number = 1; // Default layer
|
||||
|
||||
/**
|
||||
* 碰撞掩码(该碰撞体可以与哪些层碰撞)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'collisionMask', label: 'Collision Mask' })
|
||||
public collisionMask: number = 0xFFFF; // All layers
|
||||
|
||||
/**
|
||||
* 摩擦系数
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Friction', min: 0, max: 1, step: 0.01 })
|
||||
public friction: number = 0.5;
|
||||
|
||||
/**
|
||||
* 弹性系数
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Restitution', min: 0, max: 1, step: 0.01 })
|
||||
public restitution: number = 0;
|
||||
|
||||
/**
|
||||
* 是否为触发器
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Is Trigger' })
|
||||
public isTrigger: boolean = false;
|
||||
|
||||
/**
|
||||
* 生成的碰撞矩形缓存
|
||||
* @internal
|
||||
*/
|
||||
public _collisionRects: CollisionRect[] = [];
|
||||
|
||||
/**
|
||||
* 碰撞体句柄列表
|
||||
* @internal
|
||||
*/
|
||||
public _colliderHandles: number[] = [];
|
||||
|
||||
/**
|
||||
* 是否需要重建碰撞体
|
||||
* @internal
|
||||
*/
|
||||
public _needsRebuild: boolean = true;
|
||||
|
||||
/**
|
||||
* 碰撞数据版本(用于检测变化)
|
||||
* @internal
|
||||
*/
|
||||
public _lastCollisionVersion: number = -1;
|
||||
|
||||
/**
|
||||
* 从碰撞数据生成碰撞矩形
|
||||
* @param collisionData 碰撞数据数组
|
||||
* @param width 地图宽度(格子数)
|
||||
* @param height 地图高度(格子数)
|
||||
* @param tileWidth 格子宽度(像素)
|
||||
* @param tileHeight 格子高度(像素)
|
||||
*/
|
||||
public generateCollisionRects(
|
||||
collisionData: Uint32Array,
|
||||
width: number,
|
||||
height: number,
|
||||
tileWidth: number,
|
||||
tileHeight: number
|
||||
): CollisionRect[] {
|
||||
if (collisionData.length === 0) {
|
||||
this._collisionRects = [];
|
||||
return [];
|
||||
}
|
||||
|
||||
switch (this.colliderMode) {
|
||||
case TilemapColliderMode.PerTile:
|
||||
this._collisionRects = this._generatePerTileRects(
|
||||
collisionData, width, height, tileWidth, tileHeight
|
||||
);
|
||||
break;
|
||||
case TilemapColliderMode.Merged:
|
||||
this._collisionRects = this._generateMergedRects(
|
||||
collisionData, width, height, tileWidth, tileHeight
|
||||
);
|
||||
break;
|
||||
case TilemapColliderMode.EdgeOnly:
|
||||
// Edge-only 模式暂时使用合并模式
|
||||
this._collisionRects = this._generateMergedRects(
|
||||
collisionData, width, height, tileWidth, tileHeight
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
this._needsRebuild = true;
|
||||
return this._collisionRects;
|
||||
}
|
||||
|
||||
/**
|
||||
* 每个格子单独生成矩形
|
||||
*/
|
||||
private _generatePerTileRects(
|
||||
collisionData: Uint32Array,
|
||||
width: number,
|
||||
height: number,
|
||||
tileWidth: number,
|
||||
tileHeight: number
|
||||
): CollisionRect[] {
|
||||
const rects: CollisionRect[] = [];
|
||||
|
||||
for (let row = 0; row < height; row++) {
|
||||
for (let col = 0; col < width; col++) {
|
||||
if (collisionData[row * width + col] > 0) {
|
||||
rects.push({
|
||||
x: col * tileWidth,
|
||||
y: row * tileHeight,
|
||||
width: tileWidth,
|
||||
height: tileHeight,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rects;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并相邻格子生成更大的矩形(贪心算法)
|
||||
*
|
||||
* 使用行优先扫描,合并水平相邻的碰撞格子,
|
||||
* 然后尝试垂直合并相同宽度的矩形。
|
||||
*/
|
||||
private _generateMergedRects(
|
||||
collisionData: Uint32Array,
|
||||
width: number,
|
||||
height: number,
|
||||
tileWidth: number,
|
||||
tileHeight: number
|
||||
): CollisionRect[] {
|
||||
// 创建已处理标记数组
|
||||
const processed = new Array(width * height).fill(false);
|
||||
const rects: CollisionRect[] = [];
|
||||
|
||||
for (let row = 0; row < height; row++) {
|
||||
for (let col = 0; col < width; col++) {
|
||||
const index = row * width + col;
|
||||
|
||||
// 跳过已处理或无碰撞的格子
|
||||
if (processed[index] || collisionData[index] === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 找到水平方向最大范围
|
||||
let endCol = col;
|
||||
while (
|
||||
endCol < width &&
|
||||
collisionData[row * width + endCol] > 0 &&
|
||||
!processed[row * width + endCol]
|
||||
) {
|
||||
endCol++;
|
||||
}
|
||||
const rectWidth = endCol - col;
|
||||
|
||||
// 找到垂直方向最大范围(保持相同宽度)
|
||||
let endRow = row;
|
||||
let canExtend = true;
|
||||
while (canExtend && endRow < height) {
|
||||
// 检查这一行是否都有碰撞且未处理
|
||||
for (let c = col; c < endCol; c++) {
|
||||
const idx = endRow * width + c;
|
||||
if (collisionData[idx] === 0 || processed[idx]) {
|
||||
canExtend = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (canExtend) {
|
||||
endRow++;
|
||||
}
|
||||
}
|
||||
const rectHeight = endRow - row;
|
||||
|
||||
// 标记所有包含的格子为已处理
|
||||
for (let r = row; r < endRow; r++) {
|
||||
for (let c = col; c < endCol; c++) {
|
||||
processed[r * width + c] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加合并后的矩形
|
||||
rects.push({
|
||||
x: col * tileWidth,
|
||||
y: row * tileHeight,
|
||||
width: rectWidth * tileWidth,
|
||||
height: rectHeight * tileHeight,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return rects;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取碰撞矩形数量
|
||||
*/
|
||||
public getCollisionRectCount(): number {
|
||||
return this._collisionRects.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记需要重建
|
||||
*/
|
||||
public markNeedsRebuild(): void {
|
||||
this._needsRebuild = true;
|
||||
}
|
||||
|
||||
public override onRemovedFromEntity(): void {
|
||||
this._collisionRects = [];
|
||||
this._colliderHandles = [];
|
||||
}
|
||||
}
|
||||
173
packages/rendering/tilemap/src/physics/TilemapPhysicsSystem.ts
Normal file
173
packages/rendering/tilemap/src/physics/TilemapPhysicsSystem.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* TilemapPhysicsSystem
|
||||
* Tilemap 物理系统
|
||||
*
|
||||
* 负责将 TilemapComponent 的碰撞数据同步到物理世界。
|
||||
* 需要与 Physics2DSystem 配合使用。
|
||||
*/
|
||||
|
||||
import { EntitySystem, Matcher, ECSSystem, type Entity, type Scene } from '@esengine/ecs-framework';
|
||||
import { TransformComponent } from '@esengine/engine-core';
|
||||
import { TilemapComponent } from '../TilemapComponent';
|
||||
import { TilemapCollider2DComponent, type CollisionRect } from './TilemapCollider2DComponent';
|
||||
import type { IPhysics2DWorld } from '@esengine/physics-rapier2d';
|
||||
|
||||
// 重新导出类型以保持向后兼容 | Re-export types for backward compatibility
|
||||
export type IPhysicsWorld = IPhysics2DWorld;
|
||||
|
||||
/**
|
||||
* 物理系统接口
|
||||
*/
|
||||
export interface IPhysics2DSystem {
|
||||
world: IPhysicsWorld;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tilemap 物理系统
|
||||
*
|
||||
* 监听带有 TilemapComponent 和 TilemapCollider2DComponent 的实体,
|
||||
* 自动将碰撞数据转换为物理碰撞体。
|
||||
*/
|
||||
@ECSSystem('TilemapPhysics', { updateOrder: 50 })
|
||||
export class TilemapPhysicsSystem extends EntitySystem {
|
||||
private _physicsWorld: IPhysicsWorld | null = null;
|
||||
private _pendingEntities: Entity[] = [];
|
||||
|
||||
constructor() {
|
||||
super(Matcher.empty().all(TilemapComponent, TilemapCollider2DComponent));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置物理世界引用
|
||||
*/
|
||||
public setPhysicsWorld(world: IPhysicsWorld): void {
|
||||
this._physicsWorld = world;
|
||||
|
||||
// 处理待处理的实体
|
||||
for (const entity of this._pendingEntities) {
|
||||
this._createColliders(entity);
|
||||
}
|
||||
this._pendingEntities = [];
|
||||
}
|
||||
|
||||
protected override onAdded(entity: Entity): void {
|
||||
if (!this._physicsWorld) {
|
||||
this._pendingEntities.push(entity);
|
||||
return;
|
||||
}
|
||||
this._createColliders(entity);
|
||||
}
|
||||
|
||||
protected override onRemoved(entity: Entity): void {
|
||||
this._removeColliders(entity);
|
||||
|
||||
const idx = this._pendingEntities.indexOf(entity);
|
||||
if (idx >= 0) {
|
||||
this._pendingEntities.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
if (!this._physicsWorld) return;
|
||||
|
||||
for (const entity of entities) {
|
||||
const tilemap = entity.getComponent(TilemapComponent);
|
||||
const collider = entity.getComponent(TilemapCollider2DComponent);
|
||||
|
||||
if (!tilemap || !collider) continue;
|
||||
|
||||
// 检查碰撞数据是否变化
|
||||
const currentVersion = tilemap.renderDirty ? Date.now() : collider._lastCollisionVersion;
|
||||
if (collider._needsRebuild || currentVersion !== collider._lastCollisionVersion) {
|
||||
this._rebuildColliders(entity);
|
||||
collider._lastCollisionVersion = currentVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建碰撞体
|
||||
*/
|
||||
private _createColliders(entity: Entity): void {
|
||||
const tilemap = entity.getComponent(TilemapComponent);
|
||||
const collider = entity.getComponent(TilemapCollider2DComponent);
|
||||
const transform = entity.getComponent(TransformComponent);
|
||||
|
||||
if (!tilemap || !collider || !this._physicsWorld) return;
|
||||
|
||||
// 生成碰撞矩形
|
||||
const collisionData = tilemap.collisionData;
|
||||
collider.generateCollisionRects(
|
||||
collisionData,
|
||||
tilemap.width,
|
||||
tilemap.height,
|
||||
tilemap.tileWidth,
|
||||
tilemap.tileHeight
|
||||
);
|
||||
|
||||
// 获取实体位置偏移
|
||||
const offsetX = transform?.position.x ?? 0;
|
||||
const offsetY = transform?.position.y ?? 0;
|
||||
|
||||
// 计算地图总高度(像素),用于 Y 轴翻转
|
||||
// Calculate total map height (pixels) for Y-axis flip
|
||||
const mapPixelHeight = tilemap.height * tilemap.tileHeight;
|
||||
|
||||
// 为每个碰撞矩形创建物理碰撞体
|
||||
for (const rect of collider._collisionRects) {
|
||||
// Y 轴翻转:rect.y 是从顶部计算的,需要翻转到底部
|
||||
// Y-axis flip: rect.y is calculated from top, needs flip to bottom
|
||||
const flippedY = mapPixelHeight - rect.y - rect.height;
|
||||
|
||||
const handle = this._physicsWorld.createStaticCollider(
|
||||
entity.id,
|
||||
{
|
||||
x: offsetX + rect.x + rect.width / 2,
|
||||
y: offsetY + flippedY + rect.height / 2,
|
||||
},
|
||||
{
|
||||
x: rect.width / 2,
|
||||
y: rect.height / 2,
|
||||
},
|
||||
collider.collisionLayer,
|
||||
collider.collisionMask,
|
||||
collider.friction,
|
||||
collider.restitution,
|
||||
collider.isTrigger
|
||||
);
|
||||
|
||||
if (handle !== null) {
|
||||
collider._colliderHandles.push(handle);
|
||||
}
|
||||
}
|
||||
|
||||
collider._needsRebuild = false;
|
||||
this.logger.debug(`Created ${collider._colliderHandles.length} colliders for tilemap entity ${entity.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除碰撞体
|
||||
*/
|
||||
private _removeColliders(entity: Entity): void {
|
||||
const collider = entity.getComponent(TilemapCollider2DComponent);
|
||||
if (!collider || !this._physicsWorld) return;
|
||||
|
||||
for (const handle of collider._colliderHandles) {
|
||||
this._physicsWorld.removeCollider(handle);
|
||||
}
|
||||
collider._colliderHandles = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 重建碰撞体
|
||||
*/
|
||||
private _rebuildColliders(entity: Entity): void {
|
||||
this._removeColliders(entity);
|
||||
this._createColliders(entity);
|
||||
}
|
||||
|
||||
protected override onDestroy(): void {
|
||||
this._physicsWorld = null;
|
||||
this._pendingEntities = [];
|
||||
}
|
||||
}
|
||||
435
packages/rendering/tilemap/src/systems/TilemapRenderingSystem.ts
Normal file
435
packages/rendering/tilemap/src/systems/TilemapRenderingSystem.ts
Normal file
@@ -0,0 +1,435 @@
|
||||
import { EntitySystem, Matcher, ECSSystem, Entity } from '@esengine/ecs-framework';
|
||||
import { SortingLayers, 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';
|
||||
|
||||
/** 度转弧度常量 | Degrees to radians constant */
|
||||
const DEG_TO_RAD = Math.PI / 180;
|
||||
|
||||
/**
|
||||
* 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 layer name
|
||||
*/
|
||||
sortingLayer: string;
|
||||
/**
|
||||
* 层内排序顺序
|
||||
* Order within the sorting layer
|
||||
*/
|
||||
orderInLayer: number;
|
||||
/** Sorting order for rendering (deprecated, use sortingLayer + orderInLayer) | 渲染排序顺序(已弃用,使用 sortingLayer + orderInLayer) */
|
||||
sortingOrder: number;
|
||||
/** Texture path for loading | 纹理路径 */
|
||||
texturePath?: string;
|
||||
/** Material ID for this layer (0 = default) | 此图层的材质ID(0 = 默认) */
|
||||
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.sortingLayer, tilemap.orderInLayer, 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.sortingLayer, tilemap.orderInLayer, 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 rotationRad = transform.rotation.z * DEG_TO_RAD;
|
||||
const cos = Math.cos(rotationRad);
|
||||
const sin = Math.sin(rotationRad);
|
||||
|
||||
// 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 (clockwise positive)
|
||||
// 应用旋转变换(顺时针为正)
|
||||
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,
|
||||
sortingLayer: tilemap.sortingLayer,
|
||||
orderInLayer: tilemap.orderInLayer + layerIndex,
|
||||
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 rotationRad = transform.rotation.z * DEG_TO_RAD;
|
||||
const cos = Math.cos(rotationRad);
|
||||
const sin = Math.sin(rotationRad);
|
||||
|
||||
// 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 (clockwise positive)
|
||||
// 应用旋转变换(顺时针为正)
|
||||
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,
|
||||
sortingLayer: string,
|
||||
orderInLayer: 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,
|
||||
sortingLayer,
|
||||
orderInLayer: orderInLayer + layerIndex,
|
||||
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 = [];
|
||||
}
|
||||
}
|
||||
27
packages/rendering/tilemap/src/tokens.ts
Normal file
27
packages/rendering/tilemap/src/tokens.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Tilemap 模块服务令牌
|
||||
* Tilemap module service tokens
|
||||
*
|
||||
* 定义 tilemap 模块导出的服务令牌。
|
||||
* Defines service tokens exported by tilemap module.
|
||||
*/
|
||||
|
||||
import { createServiceToken } from '@esengine/ecs-framework';
|
||||
import type { TilemapRenderingSystem } from './systems/TilemapRenderingSystem';
|
||||
import type { TilemapPhysicsSystem } from './physics/TilemapPhysicsSystem';
|
||||
|
||||
// ============================================================================
|
||||
// Tilemap 模块导出的令牌 | Tokens exported by Tilemap module
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Tilemap 渲染系统令牌
|
||||
* Tilemap rendering system token
|
||||
*/
|
||||
export const TilemapSystemToken = createServiceToken<TilemapRenderingSystem>('tilemapSystem');
|
||||
|
||||
/**
|
||||
* Tilemap 物理系统令牌
|
||||
* Tilemap physics system token
|
||||
*/
|
||||
export const TilemapPhysicsSystemToken = createServiceToken<TilemapPhysicsSystem>('tilemapPhysicsSystem');
|
||||
23
packages/rendering/tilemap/tsconfig.build.json
Normal file
23
packages/rendering/tilemap/tsconfig.build.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"jsx": "react-jsx",
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
68
packages/rendering/tilemap/tsconfig.json
Normal file
68
packages/rendering/tilemap/tsconfig.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": false,
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM"
|
||||
],
|
||||
"outDir": "./bin",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"composite": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"removeComments": false,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"importHelpers": true,
|
||||
"downlevelIteration": true,
|
||||
"isolatedModules": false,
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"plugin.json"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"bin",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../framework/core"
|
||||
},
|
||||
{
|
||||
"path": "../../engine/engine-core"
|
||||
},
|
||||
{
|
||||
"path": "../../engine/ecs-engine-bindgen"
|
||||
},
|
||||
{
|
||||
"path": "../../editor/editor-core"
|
||||
},
|
||||
{
|
||||
"path": "../../engine/asset-system"
|
||||
}
|
||||
]
|
||||
}
|
||||
7
packages/rendering/tilemap/tsup.config.ts
Normal file
7
packages/rendering/tilemap/tsup.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { runtimeOnlyPreset } from '../../tools/build-config/src/presets/plugin-tsup';
|
||||
|
||||
export default defineConfig({
|
||||
...runtimeOnlyPreset(),
|
||||
tsconfig: 'tsconfig.build.json'
|
||||
});
|
||||
Reference in New Issue
Block a user