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:
233
packages/tilemap-editor/src/TilemapEditorPlugin.ts
Normal file
233
packages/tilemap-editor/src/TilemapEditorPlugin.ts
Normal 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();
|
||||
Reference in New Issue
Block a user