From fc042bb7d9ca662aaa6770e9eaeeceb8efd86fd6 Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Tue, 28 Oct 2025 17:19:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(editor):=20=E6=B7=BB=E5=8A=A0=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E5=A4=9A=E8=AF=AD=E8=A8=80=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BehaviorTreeAssetSerializer.ts | 13 +- packages/editor-app/src/App.tsx | 35 ++++ .../components/BehaviorTreeNodePalette.tsx | 69 +++++++- .../src/components/PluginGeneratorWindow.tsx | 21 ++- .../src/components/PluginManagerWindow.tsx | 164 ++++++++++++++---- .../editor-app/src/services/PluginLoader.ts | 12 +- packages/editor-app/vite.config.ts | 153 ++++++++++++++-- .../src/Plugins/EditorPluginManager.ts | 22 ++- .../editor-core/src/Plugins/IEditorPlugin.ts | 10 ++ 9 files changed, 441 insertions(+), 58 deletions(-) diff --git a/packages/behavior-tree/src/Serialization/BehaviorTreeAssetSerializer.ts b/packages/behavior-tree/src/Serialization/BehaviorTreeAssetSerializer.ts index 9f007216..4a999e7b 100644 --- a/packages/behavior-tree/src/Serialization/BehaviorTreeAssetSerializer.ts +++ b/packages/behavior-tree/src/Serialization/BehaviorTreeAssetSerializer.ts @@ -1,5 +1,4 @@ -import { encode, decode } from '@msgpack/msgpack'; -import { createLogger } from '@esengine/ecs-framework'; +import { createLogger, BinarySerializer } from '@esengine/ecs-framework'; import type { BehaviorTreeAsset } from './BehaviorTreeAsset'; import { BehaviorTreeAssetValidator } from './BehaviorTreeAsset'; import { EditorFormatConverter, type EditorFormat } from './EditorFormatConverter'; @@ -49,7 +48,7 @@ export interface DeserializationOptions { /** * 行为树资产序列化器 * - * 支持JSON和二进制(MessagePack)两种格式 + * 支持JSON和二进制两种格式 */ export class BehaviorTreeAssetSerializer { /** @@ -110,11 +109,11 @@ export class BehaviorTreeAssetSerializer { } /** - * 序列化为二进制格式(MessagePack) + * 序列化为二进制格式 */ private static serializeToBinary(asset: BehaviorTreeAsset): Uint8Array { try { - const binary = encode(asset); + const binary = BinarySerializer.encode(asset); logger.info(`已序列化为二进制: ${binary.length} 字节`); return binary; } catch (error) { @@ -208,7 +207,7 @@ export class BehaviorTreeAssetSerializer { */ private static deserializeFromBinary(binary: Uint8Array): BehaviorTreeAsset { try { - const asset = decode(binary) as BehaviorTreeAsset; + const asset = BinarySerializer.decode(binary) as BehaviorTreeAsset; logger.info(`已从二进制反序列化: ${asset.nodes.length} 个节点`); return asset; } catch (error) { @@ -251,7 +250,7 @@ export class BehaviorTreeAssetSerializer { if (format === 'json') { asset = JSON.parse(data as string); } else { - asset = decode(data as Uint8Array) as BehaviorTreeAsset; + asset = BinarySerializer.decode(data as Uint8Array) as BehaviorTreeAsset; } const size = typeof data === 'string' ? data.length : data.length; diff --git a/packages/editor-app/src/App.tsx b/packages/editor-app/src/App.tsx index e683c5f3..27a26607 100644 --- a/packages/editor-app/src/App.tsx +++ b/packages/editor-app/src/App.tsx @@ -513,6 +513,21 @@ function App() { const handleLocaleChange = () => { const newLocale = locale === 'en' ? 'zh' : 'en'; changeLocale(newLocale); + + // 通知所有已加载的插件更新语言 + if (pluginManager) { + const allPlugins = pluginManager.getAllEditorPlugins(); + allPlugins.forEach(plugin => { + if (plugin.setLocale) { + plugin.setLocale(newLocale); + } + }); + + // 通过 MessageHub 通知需要重新获取节点模板 + if (messageHub) { + messageHub.publish('locale:changed', { locale: newLocale }); + } + } }; const handleToggleDevtools = async () => { @@ -716,6 +731,21 @@ function App() { setShowPluginManager(false)} + locale={locale} + onOpen={() => { + // 同步所有插件的语言状态 + const allPlugins = pluginManager.getAllEditorPlugins(); + allPlugins.forEach(plugin => { + if (plugin.setLocale) { + plugin.setLocale(locale); + } + }); + }} + onRefresh={async () => { + if (currentProjectPath && pluginManager) { + await pluginLoaderRef.current.loadProjectPlugins(currentProjectPath, pluginManager); + } + }} /> )} @@ -752,6 +782,11 @@ function App() { onClose={() => setShowPluginGenerator(false)} projectPath={currentProjectPath} locale={locale} + onSuccess={async () => { + if (currentProjectPath && pluginManager) { + await pluginLoaderRef.current.loadProjectPlugins(currentProjectPath, pluginManager); + } + }} /> )} diff --git a/packages/editor-app/src/components/BehaviorTreeNodePalette.tsx b/packages/editor-app/src/components/BehaviorTreeNodePalette.tsx index 01b06b19..10eca25a 100644 --- a/packages/editor-app/src/components/BehaviorTreeNodePalette.tsx +++ b/packages/editor-app/src/components/BehaviorTreeNodePalette.tsx @@ -1,5 +1,7 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { NodeTemplates, NodeTemplate } from '@esengine/behavior-tree'; +import { Core } from '@esengine/ecs-framework'; +import { EditorPluginManager, MessageHub } from '@esengine/editor-core'; import { NodeIcon } from './NodeIcon'; interface BehaviorTreeNodePaletteProps { @@ -15,7 +17,70 @@ export const BehaviorTreeNodePalette: React.FC = ( onNodeSelect }) => { const [selectedCategory, setSelectedCategory] = useState('all'); - const allTemplates = NodeTemplates.getAllTemplates(); + const [allTemplates, setAllTemplates] = useState([]); + + // 获取所有节点模板(包括插件提供的) + const loadAllTemplates = () => { + console.log('[BehaviorTreeNodePalette] 开始加载节点模板'); + try { + const pluginManager = Core.services.resolve(EditorPluginManager); + const allPlugins = pluginManager.getAllEditorPlugins(); + console.log('[BehaviorTreeNodePalette] 找到插件数量:', allPlugins.length); + + // 合并所有插件的节点模板 + const templates: NodeTemplate[] = []; + for (const plugin of allPlugins) { + if (plugin.getNodeTemplates) { + console.log('[BehaviorTreeNodePalette] 从插件获取模板:', plugin.name); + const pluginTemplates = plugin.getNodeTemplates(); + console.log('[BehaviorTreeNodePalette] 插件提供的模板数量:', pluginTemplates.length); + if (pluginTemplates.length > 0) { + console.log('[BehaviorTreeNodePalette] 第一个模板:', pluginTemplates[0].displayName); + } + templates.push(...pluginTemplates); + } + } + + // 如果没有插件提供模板,回退到装饰器注册的模板 + if (templates.length === 0) { + console.log('[BehaviorTreeNodePalette] 没有插件提供模板,使用默认模板'); + templates.push(...NodeTemplates.getAllTemplates()); + } + + console.log('[BehaviorTreeNodePalette] 总共加载了', templates.length, '个模板'); + setAllTemplates(templates); + } catch (error) { + console.error('[BehaviorTreeNodePalette] 加载模板失败:', error); + // 如果无法访问插件管理器,使用默认模板 + setAllTemplates(NodeTemplates.getAllTemplates()); + } + }; + + // 初始加载 + useEffect(() => { + loadAllTemplates(); + }, []); + + // 监听语言变化事件 + useEffect(() => { + try { + const messageHub = Core.services.resolve(MessageHub); + console.log('[BehaviorTreeNodePalette] 订阅 locale:changed 事件'); + const unsubscribe = messageHub.subscribe('locale:changed', (data: any) => { + console.log('[BehaviorTreeNodePalette] 收到 locale:changed 事件:', data); + // 语言变化时重新加载模板 + loadAllTemplates(); + }); + + return () => { + console.log('[BehaviorTreeNodePalette] 取消订阅 locale:changed 事件'); + unsubscribe(); + }; + } catch (error) { + console.error('[BehaviorTreeNodePalette] 订阅事件失败:', error); + // 如果无法访问 MessageHub,忽略 + } + }, []); // 按类别分组(排除根节点类别) const categories = ['all', ...new Set(allTemplates diff --git a/packages/editor-app/src/components/PluginGeneratorWindow.tsx b/packages/editor-app/src/components/PluginGeneratorWindow.tsx index 4af99c25..a0eb39e7 100644 --- a/packages/editor-app/src/components/PluginGeneratorWindow.tsx +++ b/packages/editor-app/src/components/PluginGeneratorWindow.tsx @@ -7,9 +7,10 @@ interface PluginGeneratorWindowProps { onClose: () => void; projectPath: string | null; locale: string; + onSuccess?: () => Promise; } -export function PluginGeneratorWindow({ onClose, projectPath, locale }: PluginGeneratorWindowProps) { +export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess }: PluginGeneratorWindowProps) { const [pluginName, setPluginName] = useState(''); const [pluginVersion, setPluginVersion] = useState('1.0.0'); const [outputPath, setOutputPath] = useState(projectPath ? `${projectPath}/plugins` : ''); @@ -107,10 +108,22 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale }: PluginGe }); if (!response.ok) { - throw new Error('Failed to generate plugin'); + const errorData = await response.json(); + throw new Error(errorData.error || 'Failed to generate plugin'); } + const result = await response.json(); + alert(t('success')); + + if (result.path) { + await TauriAPI.showInFolder(result.path); + } + + if (onSuccess) { + await onSuccess(); + } + onClose(); } catch (error) { console.error('Failed to generate plugin:', error); @@ -121,8 +134,8 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale }: PluginGe }; return ( -
-
e.stopPropagation()}> +
+

{t('title')}

@@ -116,11 +194,11 @@ export function PluginManagerWindow({ pluginManager, onClose }: PluginManagerWin const CategoryIcon = (LucideIcons as any)[categoryIcons[plugin.category]]; return CategoryIcon ? : null; })()} - {categoryNames[plugin.category]} + {getCategoryName(plugin.category)} {plugin.installedAt && ( - Installed: {new Date(plugin.installedAt).toLocaleDateString()} + {t('installed')}: {new Date(plugin.installedAt).toLocaleDateString()} )}
@@ -146,17 +224,17 @@ export function PluginManagerWindow({ pluginManager, onClose }: PluginManagerWin
{plugin.enabled ? ( - Enabled + {t('enabled')} ) : ( - Disabled + {t('disabled')} )}
); @@ -168,9 +246,9 @@ export function PluginManagerWindow({ pluginManager, onClose }: PluginManagerWin
-

Plugin Manager

+

{t('title')}

-
@@ -181,7 +259,7 @@ export function PluginManagerWindow({ pluginManager, onClose }: PluginManagerWin setFilter(e.target.value)} /> @@ -191,25 +269,49 @@ export function PluginManagerWindow({ pluginManager, onClose }: PluginManagerWin
- {enabledCount} enabled + {enabledCount} {t('enabled')} - {disabledCount} disabled + {disabledCount} {t('disabled')}
+ {onRefresh && ( + + )}
@@ -221,7 +323,7 @@ export function PluginManagerWindow({ pluginManager, onClose }: PluginManagerWin {plugins.length === 0 ? (
-

No plugins installed

+

{t('noPlugins')}

) : (
@@ -244,7 +346,7 @@ export function PluginManagerWindow({ pluginManager, onClose }: PluginManagerWin return CategoryIcon ? : null; })()} - {categoryNames[cat]} + {getCategoryName(cat)} {categoryPlugins.length} diff --git a/packages/editor-app/src/services/PluginLoader.ts b/packages/editor-app/src/services/PluginLoader.ts index c338cc78..4391957a 100644 --- a/packages/editor-app/src/services/PluginLoader.ts +++ b/packages/editor-app/src/services/PluginLoader.ts @@ -59,8 +59,12 @@ export class PluginLoader { const packageJson: PluginPackageJson = JSON.parse(packageJsonContent); if (this.loadedPluginNames.has(packageJson.name)) { - console.log(`[PluginLoader] Plugin ${packageJson.name} already loaded`); - return; + try { + await pluginManager.uninstallEditor(packageJson.name); + this.loadedPluginNames.delete(packageJson.name); + } catch (error) { + console.error(`[PluginLoader] Failed to uninstall existing plugin ${packageJson.name}:`, error); + } } let entryPoint = 'src/index.ts'; @@ -89,7 +93,9 @@ export class PluginLoader { // 移除开头的 ./ entryPoint = entryPoint.replace(/^\.\//, ''); - const moduleUrl = `/@user-project/plugins/${pluginDirName}/${entryPoint}`; + // 添加时间戳参数强制重新加载模块(避免缓存) + const timestamp = Date.now(); + const moduleUrl = `/@user-project/plugins/${pluginDirName}/${entryPoint}?t=${timestamp}`; console.log(`[PluginLoader] Loading plugin from: ${moduleUrl}`); diff --git a/packages/editor-app/vite.config.ts b/packages/editor-app/vite.config.ts index 7179aa61..06a6dc08 100644 --- a/packages/editor-app/vite.config.ts +++ b/packages/editor-app/vite.config.ts @@ -7,6 +7,7 @@ const host = process.env.TAURI_DEV_HOST; const userProjectPathMap = new Map(); const editorPackageMapping = new Map(); +const editorPackageVersions = new Map(); function loadEditorPackages() { const packagesDir = path.resolve(__dirname, '..'); @@ -32,6 +33,9 @@ function loadEditorPackages() { editorPackageMapping.set(packageJson.name, entryPath); } } + if (packageJson.version) { + editorPackageVersions.set(packageJson.name, packageJson.version); + } } } catch (e) { console.error(`[Vite] Failed to read package.json for ${dir}:`, e); @@ -174,6 +178,10 @@ const userProjectPlugin = () => ({ fs.mkdirSync(path.join(pluginPath, 'src', 'nodes'), { recursive: true }); } + const coreVersion = editorPackageVersions.get('@esengine/ecs-framework') || '2.2.8'; + const editorCoreVersion = editorPackageVersions.get('@esengine/editor-core') || '1.0.0'; + const behaviorTreeVersion = editorPackageVersions.get('@esengine/behavior-tree') || '1.0.0'; + const packageJson = { name: pluginName, version: pluginVersion, @@ -196,11 +204,11 @@ const userProjectPlugin = () => ({ watch: 'tsc --watch' }, peerDependencies: { - '@esengine/ecs-framework': '^2.2.8', - '@esengine/editor-core': '^1.0.0' + '@esengine/ecs-framework': `^${coreVersion}`, + '@esengine/editor-core': `^${editorCoreVersion}` }, dependencies: { - '@esengine/behavior-tree': '^1.0.0' + '@esengine/behavior-tree': `^${behaviorTreeVersion}` }, devDependencies: { 'typescript': '^5.8.3' @@ -258,13 +266,24 @@ import { EditorPluginCategory } from '@esengine/editor-core'; import type { Core, ServiceContainer } from '@esengine/ecs-framework'; import { getRegisteredNodeTemplates } from '@esengine/behavior-tree'; import type { NodeTemplate } from '@esengine/behavior-tree'; +import { t, setLocale } from './locales'; export class ${pluginName.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('')}Plugin implements IEditorPlugin { readonly name = '${pluginName}'; readonly version = '${pluginVersion}'; - readonly displayName = '${pluginName}'; readonly category = EditorPluginCategory.Tool; - readonly description = 'Behavior tree plugin for ${pluginName}'; + + get displayName(): string { + return t('plugin.name'); + } + + get description(): string { + return t('plugin.description'); + } + + setLocale(locale: string): void { + setLocale(locale); + } async install(core: Core, services: ServiceContainer): Promise { console.log('[${pluginName}] Plugin installed'); @@ -275,7 +294,17 @@ export class ${pluginName.split('-').map(s => s.charAt(0).toUpperCase() + s.slic } getNodeTemplates(): NodeTemplate[] { - return getRegisteredNodeTemplates(); + const templates = getRegisteredNodeTemplates(); + return templates.map(template => ({ + ...template, + displayName: t(template.displayName), + description: t(template.description), + properties: template.properties?.map(prop => ({ + ...prop, + label: t(prop.label), + description: prop.description ? t(prop.description) : undefined + })) + })); } } @@ -283,25 +312,61 @@ export const ${pluginName.replace(/-/g, '')}Plugin = new ${pluginName.split('-') `; fs.writeFileSync(path.join(pluginPath, 'src', 'plugin.ts'), pluginTs); + const localesDir = path.join(pluginPath, 'src', 'locales'); + if (!fs.existsSync(localesDir)) { + fs.mkdirSync(localesDir, { recursive: true }); + } + + const localesIndexTs = `export const translations = { + zh: { + 'plugin.name': '${pluginName}', + 'plugin.description': '${pluginName} 行为树插件', + 'ExampleAction.name': '示例动作', + 'ExampleAction.description': '这是一个示例动作节点', + 'ExampleAction.message.label': '消息内容', + 'ExampleAction.message.description': '要打印的消息' + }, + en: { + 'plugin.name': '${pluginName}', + 'plugin.description': 'Behavior tree plugin for ${pluginName}', + 'ExampleAction.name': 'Example Action', + 'ExampleAction.description': 'This is an example action node', + 'ExampleAction.message.label': 'Message', + 'ExampleAction.message.description': 'The message to print' + } +}; + +let currentLocale = 'zh'; + +export function setLocale(locale: string) { + currentLocale = locale; +} + +export function t(key: string): string { + return translations[currentLocale]?.[key] || translations['en']?.[key] || key; +} +`; + fs.writeFileSync(path.join(localesDir, 'index.ts'), localesIndexTs); + if (includeExample) { const exampleActionTs = `import { Component, Entity, ECSComponent, Serialize } from '@esengine/ecs-framework'; import { BehaviorNode, BehaviorProperty, NodeType, TaskStatus, BlackboardComponent } from '@esengine/behavior-tree'; @ECSComponent('ExampleAction') @BehaviorNode({ - displayName: '示例动作', + displayName: 'ExampleAction.name', category: '自定义', type: NodeType.Action, icon: 'Star', - description: '这是一个示例动作节点', + description: 'ExampleAction.description', color: '#FF9800' }) export class ExampleAction extends Component { @Serialize() @BehaviorProperty({ - label: '消息内容', + label: 'ExampleAction.message.label', type: 'string', - description: '要打印的消息' + description: 'ExampleAction.message.description' }) message: string = 'Hello from example action!'; @@ -340,6 +405,74 @@ import { EditorPluginManager } from '@esengine/editor-core'; const pluginManager = Core.services.resolve(EditorPluginManager); await pluginManager.installEditor(${pluginName.replace(/-/g, '')}Plugin); \`\`\` + +## 多语言支持 + +本插件内置中英文多语言支持,使用简单的 i18n key 方式。 + +### 设置语言 + +\`\`\`typescript +// 设置为中文 +${pluginName.replace(/-/g, '')}Plugin.setLocale('zh'); + +// 设置为英文 +${pluginName.replace(/-/g, '')}Plugin.setLocale('en'); +\`\`\` + +### 翻译文件结构 + +在 \`src/locales/index.ts\` 中,使用扁平化的 key-value 结构: + +\`\`\`typescript +export const translations = { + zh: { + 'plugin.name': '插件名称', + 'plugin.description': '插件描述', + 'NodeName.name': '节点显示名', + 'NodeName.description': '节点描述', + 'NodeName.property.label': '属性标签', + 'NodeName.property.description': '属性描述' + }, + en: { + 'plugin.name': 'Plugin Name', + // ... + } +}; +\`\`\` + +### 在代码中使用 + +\`\`\`typescript +import { t } from '../locales'; + +@BehaviorNode({ + displayName: t('YourNode.name'), + description: t('YourNode.description') +}) +export class YourNode extends Component { + @BehaviorProperty({ + label: t('YourNode.propertyName.label') + }) + propertyName: string = ''; +} +\`\`\` + +## 目录结构 + +\`\`\` +${pluginName}/ +├── src/ +│ ├── locales/ # 多语言文件 +│ │ └── index.ts +│ ├── nodes/ # 行为树节点 +│ │ └── ExampleAction.ts +│ ├── plugin.ts # 插件主类 +│ └── index.ts # 导出入口 +├── package.json +├── tsconfig.json +└── README.md +\`\`\` `; fs.writeFileSync(path.join(pluginPath, 'README.md'), readme); diff --git a/packages/editor-core/src/Plugins/EditorPluginManager.ts b/packages/editor-core/src/Plugins/EditorPluginManager.ts index c51081c4..7c765c11 100644 --- a/packages/editor-core/src/Plugins/EditorPluginManager.ts +++ b/packages/editor-core/src/Plugins/EditorPluginManager.ts @@ -179,9 +179,29 @@ export class EditorPluginManager extends PluginManager { /** * 获取所有插件元数据 + * + * 实时从插件实例获取 displayName 和 description,以支持多语言切换 */ public getAllPluginMetadata(): IEditorPluginMetadata[] { - return Array.from(this.pluginMetadata.values()); + const metadataList: IEditorPluginMetadata[] = []; + + for (const [name, metadata] of this.pluginMetadata.entries()) { + const plugin = this.editorPlugins.get(name); + + // 如果插件实例存在,使用实时的 displayName 和 description + if (plugin) { + metadataList.push({ + ...metadata, + displayName: plugin.displayName, + description: plugin.description + }); + } else { + // 回退到缓存的元数据 + metadataList.push(metadata); + } + } + + return metadataList; } /** diff --git a/packages/editor-core/src/Plugins/IEditorPlugin.ts b/packages/editor-core/src/Plugins/IEditorPlugin.ts index 9c0ed58a..c3e00e73 100644 --- a/packages/editor-core/src/Plugins/IEditorPlugin.ts +++ b/packages/editor-core/src/Plugins/IEditorPlugin.ts @@ -121,6 +121,16 @@ export interface IEditorPlugin extends IPlugin { * 文件保存后回调 */ onAfterSave?(filePath: string): void | Promise; + + /** + * 设置插件语言 + */ + setLocale?(locale: string): void; + + /** + * 获取行为树节点模板 + */ + getNodeTemplates?(): any[]; } /**