Feature/tilemap editor (#237)

* feat: 添加 Tilemap 编辑器插件和组件生命周期支持

* feat(editor-core): 添加声明式插件注册 API

* feat(editor-core): 改进tiledmap结构合并tileset进tiledmapeditor

* feat: 添加 editor-runtime SDK 和插件系统改进

* fix(ci): 修复SceneResourceManager里变量未使用问题
This commit is contained in:
YHH
2025-11-25 22:23:19 +08:00
committed by GitHub
parent 551ca7805d
commit 3fb6f919f8
166 changed files with 54691 additions and 8674 deletions

View File

@@ -0,0 +1,233 @@
/**
* Tilemap Editor Plugin
*/
import React from 'react';
import { Grid3X3 } from 'lucide-react';
import type { ServiceContainer, Entity } from '@esengine/ecs-framework';
import { Core } from '@esengine/ecs-framework';
import type {
IEditorPlugin,
PanelDescriptor,
EntityCreationTemplate,
ComponentAction,
} from '@esengine/editor-core';
import { EditorPluginCategory, PanelPosition, InspectorRegistry, EntityStoreService, MessageHub, ComponentRegistry, IDialogService, IFileSystemService } from '@esengine/editor-core';
import type { IDialog, IFileSystem } from '@esengine/editor-core';
import { Edit3 } from 'lucide-react';
import { TilemapComponent } from '@esengine/tilemap';
import { TransformComponent } from '@esengine/ecs-components';
import { useTilemapEditorStore } from './stores/TilemapEditorStore';
import { TilemapEditorPanel } from './components/panels/TilemapEditorPanel';
import { TilemapInspectorProvider } from './providers/TilemapInspectorProvider';
import { registerTilemapGizmo } from './gizmos/TilemapGizmo';
export class TilemapEditorPlugin implements IEditorPlugin {
readonly name = '@esengine/tilemap-editor';
readonly version = '1.0.0';
readonly category = EditorPluginCategory.Tool;
private unsubscribers: Array<() => void> = [];
get displayName(): string {
return 'Tilemap Editor';
}
get description(): string {
return 'Visual tilemap editing tools for creating tile-based game levels';
}
registerPanels(): PanelDescriptor[] {
return [
{
id: 'tilemap-editor',
title: 'Tilemap Editor',
position: PanelPosition.Center,
component: TilemapEditorPanel,
isDynamic: true,
closable: true,
order: 50,
},
];
}
async install(_core: Core, services: ServiceContainer): Promise<void> {
// Register inspector provider
const inspectorRegistry = services.resolve(InspectorRegistry);
if (inspectorRegistry) {
inspectorRegistry.register(new TilemapInspectorProvider());
}
// Register TilemapComponent to component registry for add component menu
const componentRegistry = services.resolve(ComponentRegistry);
if (componentRegistry) {
componentRegistry.register({
name: 'Tilemap',
type: TilemapComponent,
category: 'components.category.tilemap',
description: 'Tilemap component for tile-based levels'
});
}
// Subscribe to tilemap:create-asset message
const messageHub = services.resolve(MessageHub);
if (messageHub) {
const unsubscribe = messageHub.subscribe('tilemap:create-asset', async (payload: {
entityId?: string;
onChange?: (value: string | null) => void;
}) => {
await this.handleCreateTilemapAsset(services, payload);
});
this.unsubscribers.push(unsubscribe);
}
// Register Tilemap gizmo support
// 注册 Tilemap gizmo 支持
registerTilemapGizmo();
console.log('[TilemapEditorPlugin] Installed');
}
private async handleCreateTilemapAsset(
_services: ServiceContainer,
payload: { entityId?: string; onChange?: (value: string | null) => void }
): Promise<void> {
const dialog = Core.services.tryResolve(IDialogService) as IDialog | null;
const fileSystem = Core.services.tryResolve(IFileSystemService) as IFileSystem | null;
const messageHub = Core.services.tryResolve(MessageHub);
if (!dialog || !fileSystem) {
console.error('[TilemapEditorPlugin] Dialog or FileSystem service not available');
return;
}
// Show save dialog
const filePath = await dialog.saveDialog({
title: '创建 Tilemap 资产',
filters: [{ name: 'Tilemap', extensions: ['tilemap.json'] }],
defaultPath: 'new-tilemap.tilemap.json'
});
if (!filePath) {
return;
}
// Create default tilemap data
const defaultTilemapData = {
width: 20,
height: 15,
tileWidth: 16,
tileHeight: 16,
layers: [
{
name: 'Layer 1',
visible: true,
opacity: 1,
data: new Array(20 * 15).fill(0)
}
],
tilesets: []
};
// Write file
await fileSystem.writeFile(filePath, JSON.stringify(defaultTilemapData, null, 2));
// Update component property via onChange callback
if (payload.onChange) {
payload.onChange(filePath);
}
// Open tilemap editor panels
if (messageHub && payload.entityId) {
useTilemapEditorStore.getState().setEntityId(payload.entityId);
messageHub.publish('dynamic-panel:open', { panelId: 'tilemap-editor', title: 'Tilemap Editor' });
messageHub.publish('dynamic-panel:open', { panelId: 'tileset-panel', title: 'Tileset' });
}
console.log('[TilemapEditorPlugin] Created tilemap asset:', filePath);
}
async uninstall(): Promise<void> {
// Cleanup subscriptions
this.unsubscribers.forEach(unsub => unsub());
this.unsubscribers = [];
console.log('[TilemapEditorPlugin] Uninstalled');
}
registerComponentActions(): ComponentAction[] {
return [
{
id: 'tilemap-edit',
componentName: 'Tilemap',
label: '编辑 Tilemap',
icon: React.createElement(Edit3, { size: 14 }),
order: 0,
execute: (_component: unknown, entity: Entity) => {
const messageHub = Core.services.resolve(MessageHub);
if (messageHub) {
const entityIdStr = String(entity.id);
useTilemapEditorStore.getState().setEntityId(entityIdStr);
messageHub.publish('dynamic-panel:open', { panelId: 'tilemap-editor', title: 'Tilemap Editor' });
}
}
}
];
}
registerEntityCreationTemplates(): EntityCreationTemplate[] {
return [
{
id: 'create-tilemap-entity',
label: '创建 Tilemap',
icon: React.createElement(Grid3X3, { size: 12 }),
order: 100,
create: (_parentEntityId?: number): number => {
const scene = Core.scene;
if (!scene) {
throw new Error('Scene not available');
}
const entityStore = Core.services.resolve(EntityStoreService);
const messageHub = Core.services.resolve(MessageHub);
if (!entityStore || !messageHub) {
throw new Error('EntityStoreService or MessageHub not available');
}
// Count existing tilemap entities
const tilemapCount = entityStore.getAllEntities()
.filter((e: Entity) => e.name.startsWith('Tilemap ')).length;
const entityName = `Tilemap ${tilemapCount + 1}`;
// Create entity via scene
const entity = scene.createEntity(entityName);
// Add TransformComponent (required for rendering)
entity.addComponent(new TransformComponent());
// Add TilemapComponent with default settings
const tilemapComponent = new TilemapComponent();
tilemapComponent.tileWidth = 16;
tilemapComponent.tileHeight = 16;
tilemapComponent.initializeEmpty(20, 15);
entity.addComponent(tilemapComponent);
// Register with entity store
entityStore.addEntity(entity);
// Notify
messageHub.publish('entity:added', { entity });
messageHub.publish('scene:modified', {});
// Select the new entity
entityStore.selectEntity(entity);
return entity.id;
}
}
];
}
}
export const tilemapEditorPlugin = new TilemapEditorPlugin();