新增cocos右键打开和保存行为树功能
This commit is contained in:
@@ -1,63 +0,0 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node_b0bpuk8ei",
|
||||
"type": "Sequence",
|
||||
"name": "序列器",
|
||||
"icon": "→",
|
||||
"description": "按顺序执行子节点,任一失败则整体失败",
|
||||
"x": 207.39999389648438,
|
||||
"y": 145.59999084472656,
|
||||
"children": [
|
||||
"node_pgmfxi7ho"
|
||||
],
|
||||
"properties": {
|
||||
"abortType": {
|
||||
"name": "中止类型",
|
||||
"type": "select",
|
||||
"value": "None",
|
||||
"description": "决定节点在何种情况下会被中止",
|
||||
"options": [
|
||||
"None",
|
||||
"LowerPriority",
|
||||
"Self",
|
||||
"Both"
|
||||
],
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"canHaveChildren": true,
|
||||
"canHaveParent": true,
|
||||
"hasError": false
|
||||
},
|
||||
{
|
||||
"id": "node_pgmfxi7ho",
|
||||
"type": "Inverter",
|
||||
"name": "反转器",
|
||||
"icon": "⚡",
|
||||
"description": "反转子节点的执行结果",
|
||||
"x": 163.39999389648438,
|
||||
"y": 436.59999084472656,
|
||||
"children": [],
|
||||
"properties": {},
|
||||
"canHaveChildren": true,
|
||||
"canHaveParent": true,
|
||||
"hasError": false,
|
||||
"parent": "node_b0bpuk8ei"
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"id": "node_b0bpuk8ei-node_pgmfxi7ho",
|
||||
"sourceId": "node_b0bpuk8ei",
|
||||
"targetId": "node_pgmfxi7ho",
|
||||
"path": "M 307.3999938964844 265.59999084472656 C 307.3999938964844 351.09999084472656 263.3999938964844 351.09999084472656 263.3999938964844 436.59999084472656",
|
||||
"active": false
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"name": "untitled",
|
||||
"created": "2025-06-17T14:52:33.885Z",
|
||||
"version": "1.0"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"ver": "2.0.1",
|
||||
"importer": "json",
|
||||
"imported": true,
|
||||
"uuid": "cb66452d-5cad-46a9-96f9-b62831e0edc3",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1 +1,15 @@
|
||||
"use strict";module.exports={open_panel:"Default Panel",send_to_panel:"Send message to Default Panel",description:"Professional ECS Framework Development Assistant: One-click install @esengine/ecs-framework, intelligent code generator for components and systems, project template generation, real-time status monitoring and version management. Features welcome panel, debug panel and code generator to make ECS development in Cocos Creator more efficient and convenient."};
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
description: "Professional ECS Framework Development Assistant: One-click installation of @esengine/ecs-framework, intelligent code generator for quick creation of components and systems, project template generation, real-time status detection and version management. Provides welcome panel, debug panel, code generator and behavior tree AI component library to make ECS development in Cocos Creator more efficient and convenient.",
|
||||
|
||||
open_panel: "Default Panel",
|
||||
send_to_panel: "Send message to panel",
|
||||
|
||||
menu: {
|
||||
panel: "Panel",
|
||||
develop: "Develop",
|
||||
create: "Create",
|
||||
open: "Open"
|
||||
}
|
||||
};
|
||||
@@ -10,6 +10,9 @@ module.exports = {
|
||||
|
||||
// 菜单相关
|
||||
menu: {
|
||||
panel: "面板"
|
||||
panel: "面板",
|
||||
develop: "开发",
|
||||
create: "创建",
|
||||
open: "打开"
|
||||
}
|
||||
};
|
||||
@@ -100,20 +100,12 @@
|
||||
"message": "open-panel"
|
||||
}
|
||||
],
|
||||
"asset-menu": [
|
||||
{
|
||||
"path": "i18n:menu.create/ECS Framework",
|
||||
"label": "创建行为树文件",
|
||||
"message": "create-behavior-tree-file",
|
||||
"target": "folder"
|
||||
},
|
||||
{
|
||||
"path": "i18n:menu.open",
|
||||
"label": "用行为树编辑器打开",
|
||||
"message": "open-behavior-tree-file",
|
||||
"target": [".bt.json", ".json"]
|
||||
"assets": {
|
||||
"menu": {
|
||||
"methods": "./dist/assets-menu.js",
|
||||
"assetMenu": "onAssetMenu"
|
||||
}
|
||||
],
|
||||
},
|
||||
"messages": {
|
||||
"open-panel": {
|
||||
"methods": [
|
||||
@@ -195,15 +187,25 @@
|
||||
"create-behavior-tree-file"
|
||||
]
|
||||
},
|
||||
"open-behavior-tree-file": {
|
||||
"load-behavior-tree-file": {
|
||||
"methods": [
|
||||
"open-behavior-tree-file"
|
||||
"load-behavior-tree-file"
|
||||
]
|
||||
},
|
||||
"create-behavior-tree-from-editor": {
|
||||
"methods": [
|
||||
"create-behavior-tree-from-editor"
|
||||
]
|
||||
},
|
||||
"overwrite-behavior-tree-file": {
|
||||
"methods": [
|
||||
"overwrite-behavior-tree-file"
|
||||
]
|
||||
},
|
||||
"behavior-tree-panel-load-file": {
|
||||
"methods": [
|
||||
"behavior-tree.loadBehaviorTreeFile"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
export function onAssetMenu(assetInfo: any) {
|
||||
console.log('[AssetMenu] onAssetMenu 被调用,资源信息:', assetInfo);
|
||||
console.log('[AssetMenu] assetInfo 完整结构:', JSON.stringify(assetInfo, null, 2));
|
||||
|
||||
const menuItems = [];
|
||||
|
||||
// 检查是否为行为树文件
|
||||
const isTargetFile = (assetInfo && assetInfo.name && assetInfo.name.endsWith('.bt.json')) ||
|
||||
(assetInfo && assetInfo.file && assetInfo.file.endsWith('.bt.json'));
|
||||
|
||||
if (isTargetFile) {
|
||||
console.log('[AssetMenu] 发现 .bt.json 文件,添加菜单项');
|
||||
menuItems.push({
|
||||
label: '用行为树编辑器打开',
|
||||
click() {
|
||||
console.log('[AssetMenu] 菜单项被点击,文件信息:', assetInfo);
|
||||
|
||||
// 直接调用主进程的方法,不需要复杂的序列化
|
||||
try {
|
||||
Editor.Message.send('cocos-ecs-extension', 'load-behavior-tree-file', assetInfo);
|
||||
console.log('[AssetMenu] 消息发送成功');
|
||||
} catch (error) {
|
||||
console.error('[AssetMenu] 消息发送失败:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 在目录中添加创建选项
|
||||
if (assetInfo && assetInfo.isDirectory) {
|
||||
menuItems.push({
|
||||
label: '创建行为树文件',
|
||||
click() {
|
||||
console.log('[AssetMenu] 在目录中创建行为树文件:', assetInfo);
|
||||
try {
|
||||
Editor.Message.send('cocos-ecs-extension', 'create-behavior-tree-file');
|
||||
} catch (error) {
|
||||
console.error('[AssetMenu] 创建消息发送失败:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[AssetMenu] 返回菜单项数量:', menuItems.length);
|
||||
return menuItems;
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
import { exec } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as fsExtra from 'fs-extra';
|
||||
|
||||
/**
|
||||
* 行为树相关的处理器
|
||||
*/
|
||||
export class BehaviorTreeHandler {
|
||||
/**
|
||||
* 安装行为树AI系统
|
||||
*/
|
||||
static async install(): Promise<boolean> {
|
||||
const projectPath = Editor.Project.path;
|
||||
const command = 'npm install @esengine/ai';
|
||||
|
||||
console.log(`Installing Behavior Tree AI to project: ${projectPath}`);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
exec(command, { cwd: projectPath }, (error, stdout, stderr) => {
|
||||
console.log('Install stdout:', stdout);
|
||||
if (stderr) console.log('Install stderr:', stderr);
|
||||
|
||||
if (error) {
|
||||
console.error('Behavior Tree AI installation failed:', error);
|
||||
resolve(false);
|
||||
} else {
|
||||
console.log('Behavior Tree AI installation completed successfully');
|
||||
|
||||
// 验证安装是否成功
|
||||
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ai');
|
||||
const installSuccess = fs.existsSync(nodeModulesPath);
|
||||
|
||||
if (installSuccess) {
|
||||
console.log('Behavior Tree AI installed successfully');
|
||||
resolve(true);
|
||||
} else {
|
||||
console.warn('Behavior Tree AI directory not found after install');
|
||||
resolve(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新行为树AI系统
|
||||
*/
|
||||
static async update(): Promise<boolean> {
|
||||
const projectPath = Editor.Project.path;
|
||||
const command = 'npm update @esengine/ai';
|
||||
|
||||
console.log(`Updating Behavior Tree AI in project: ${projectPath}`);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
exec(command, { cwd: projectPath }, (error, stdout, stderr) => {
|
||||
console.log('Update stdout:', stdout);
|
||||
if (stderr) console.log('Update stderr:', stderr);
|
||||
|
||||
if (error) {
|
||||
console.error('Behavior Tree AI update failed:', error);
|
||||
resolve(false);
|
||||
} else {
|
||||
console.log('Behavior Tree AI update completed successfully');
|
||||
|
||||
// 验证更新是否成功
|
||||
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ai');
|
||||
const updateSuccess = fs.existsSync(nodeModulesPath);
|
||||
|
||||
if (updateSuccess) {
|
||||
console.log('Behavior Tree AI updated successfully');
|
||||
resolve(true);
|
||||
} else {
|
||||
console.warn('Behavior Tree AI directory not found after update');
|
||||
resolve(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查行为树AI是否已安装
|
||||
*/
|
||||
static checkInstalled(): boolean {
|
||||
try {
|
||||
const projectPath = Editor.Project.path;
|
||||
const packageJsonPath = path.join(projectPath, 'package.json');
|
||||
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
||||
|
||||
return '@esengine/ai' in dependencies;
|
||||
} catch (error) {
|
||||
console.error('Error checking Behavior Tree AI installation:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开行为树文档
|
||||
*/
|
||||
static openDocumentation(): void {
|
||||
const url = 'https://github.com/esengine/ai/blob/master/README.md';
|
||||
|
||||
try {
|
||||
const { shell } = require('electron');
|
||||
shell.openExternal(url);
|
||||
console.log('Behavior Tree documentation opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to open Behavior Tree documentation:', error);
|
||||
Editor.Dialog.info('打开行为树文档', {
|
||||
detail: `请手动访问以下链接查看文档:\n\n${url}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建行为树文件
|
||||
*/
|
||||
static async createFile(assetInfo?: any): Promise<void> {
|
||||
try {
|
||||
const projectPath = Editor.Project.path;
|
||||
const assetsPath = path.join(projectPath, 'assets');
|
||||
|
||||
// 生成唯一文件名
|
||||
let fileName = 'NewBehaviorTree';
|
||||
let counter = 1;
|
||||
let filePath = path.join(assetsPath, `${fileName}.bt.json`);
|
||||
|
||||
while (fs.existsSync(filePath)) {
|
||||
fileName = `NewBehaviorTree_${counter}`;
|
||||
filePath = path.join(assetsPath, `${fileName}.bt.json`);
|
||||
counter++;
|
||||
}
|
||||
|
||||
// 创建默认的行为树配置
|
||||
const defaultConfig = {
|
||||
version: "1.0.0",
|
||||
type: "behavior-tree",
|
||||
metadata: {
|
||||
createdAt: new Date().toISOString(),
|
||||
nodeCount: 1
|
||||
},
|
||||
tree: {
|
||||
id: "root",
|
||||
type: "sequence",
|
||||
namespace: "behaviourTree/composites",
|
||||
properties: {},
|
||||
children: []
|
||||
}
|
||||
};
|
||||
|
||||
// 写入文件
|
||||
await fsExtra.writeFile(filePath, JSON.stringify(defaultConfig, null, 2));
|
||||
|
||||
// 刷新资源管理器 - 使用正确的资源路径
|
||||
const relativeAssetPath = path.relative(projectPath, filePath).replace(/\\/g, '/');
|
||||
const dbAssetPath = 'db://' + relativeAssetPath;
|
||||
await Editor.Message.request('asset-db', 'refresh-asset', dbAssetPath);
|
||||
|
||||
console.log(`Behavior tree file created: ${filePath}`);
|
||||
|
||||
Editor.Dialog.info('创建成功', {
|
||||
detail: `行为树文件 "${fileName}.bt.json" 已创建完成!\n\n文件位置:assets/${fileName}.bt.json\n\n您可以右键点击文件选择"用行为树编辑器打开"来编辑它。`,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to create behavior tree file:', error);
|
||||
Editor.Dialog.error('创建失败', {
|
||||
detail: `创建行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开行为树文件
|
||||
*/
|
||||
static async openFile(assetInfo: any): Promise<void> {
|
||||
try {
|
||||
if (!assetInfo || !assetInfo.file) {
|
||||
throw new Error('无效的文件信息');
|
||||
}
|
||||
|
||||
const filePath = assetInfo.file;
|
||||
const fileData = await this.loadFileData(filePath);
|
||||
await this.openPanel();
|
||||
await this.sendDataToPanel(fileData);
|
||||
|
||||
} catch (error) {
|
||||
Editor.Dialog.error('打开失败', {
|
||||
detail: `打开行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取并解析文件数据
|
||||
*/
|
||||
private static async loadFileData(filePath: string): Promise<any> {
|
||||
try {
|
||||
let assetPath = filePath;
|
||||
|
||||
if (path.isAbsolute(filePath)) {
|
||||
const projectPath = Editor.Project.path;
|
||||
if (filePath.startsWith(projectPath)) {
|
||||
assetPath = path.relative(projectPath, filePath);
|
||||
assetPath = assetPath.replace(/\\/g, '/');
|
||||
}
|
||||
}
|
||||
|
||||
if (!assetPath.startsWith('db://')) {
|
||||
assetPath = 'db://' + assetPath;
|
||||
}
|
||||
|
||||
try {
|
||||
const assetInfo = await Editor.Message.request('asset-db', 'query-asset-info', assetPath);
|
||||
|
||||
if (assetInfo && assetInfo.source) {
|
||||
const content = await fsExtra.readFile(assetInfo.source, 'utf8');
|
||||
let fileContent: any;
|
||||
|
||||
try {
|
||||
fileContent = JSON.parse(content);
|
||||
} catch (parseError) {
|
||||
fileContent = {
|
||||
version: "1.0.0",
|
||||
type: "behavior-tree",
|
||||
rawContent: content
|
||||
};
|
||||
}
|
||||
|
||||
const fileData = {
|
||||
...fileContent,
|
||||
_fileInfo: {
|
||||
fileName: path.basename(assetInfo.source, path.extname(assetInfo.source)),
|
||||
filePath: assetInfo.source,
|
||||
assetPath: assetPath
|
||||
}
|
||||
};
|
||||
|
||||
return fileData;
|
||||
}
|
||||
} catch (assetError) {
|
||||
// 资源系统读取失败,尝试直接文件读取
|
||||
}
|
||||
|
||||
const actualFilePath = path.isAbsolute(filePath) ? filePath : path.join(Editor.Project.path, filePath);
|
||||
|
||||
if (!fs.existsSync(actualFilePath)) {
|
||||
throw new Error(`文件不存在: ${actualFilePath}`);
|
||||
}
|
||||
|
||||
const content = await fsExtra.readFile(actualFilePath, 'utf8');
|
||||
let fileContent: any;
|
||||
|
||||
try {
|
||||
fileContent = JSON.parse(content);
|
||||
} catch (parseError) {
|
||||
fileContent = {
|
||||
version: "1.0.0",
|
||||
type: "behavior-tree",
|
||||
rawContent: content
|
||||
};
|
||||
}
|
||||
|
||||
const fileData = {
|
||||
...fileContent,
|
||||
_fileInfo: {
|
||||
fileName: path.basename(actualFilePath, path.extname(actualFilePath)),
|
||||
filePath: actualFilePath
|
||||
}
|
||||
};
|
||||
|
||||
return fileData;
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`文件读取失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开行为树面板
|
||||
*/
|
||||
private static async openPanel(): Promise<void> {
|
||||
await Editor.Panel.open('cocos-ecs-extension.behavior-tree');
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送数据到面板
|
||||
*/
|
||||
private static async sendDataToPanel(fileData: any): Promise<void> {
|
||||
try {
|
||||
const result = await Editor.Message.request('cocos-ecs-extension.behavior-tree', 'loadBehaviorTreeFile', fileData);
|
||||
} catch (error) {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
Editor.Message.send('cocos-ecs-extension.behavior-tree', 'loadBehaviorTreeFile', fileData);
|
||||
} catch (delayError) {
|
||||
// 静默失败
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从编辑器创建行为树文件
|
||||
*/
|
||||
static async createFromEditor(data: { fileName: string, content: string }): Promise<void> {
|
||||
try {
|
||||
const projectPath = Editor.Project.path;
|
||||
const assetsPath = path.join(projectPath, 'assets');
|
||||
|
||||
let fileName = data.fileName;
|
||||
let counter = 1;
|
||||
let filePath = path.join(assetsPath, `${fileName}.bt.json`);
|
||||
|
||||
while (fs.existsSync(filePath)) {
|
||||
fileName = `${data.fileName}_${counter}`;
|
||||
filePath = path.join(assetsPath, `${fileName}.bt.json`);
|
||||
counter++;
|
||||
}
|
||||
|
||||
await fsExtra.writeFile(filePath, data.content);
|
||||
|
||||
const relativeAssetPath = path.relative(projectPath, filePath).replace(/\\/g, '/');
|
||||
const dbAssetPath = 'db://' + relativeAssetPath;
|
||||
await Editor.Message.request('asset-db', 'refresh-asset', dbAssetPath);
|
||||
|
||||
Editor.Dialog.info('保存成功', {
|
||||
detail: `行为树文件 "${fileName}.bt.json" 已保存到 assets 目录中!`,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
Editor.Dialog.error('保存失败', {
|
||||
detail: `保存行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 覆盖现有行为树文件
|
||||
*/
|
||||
static async overwriteFile(data: { filePath: string, content: string }): Promise<void> {
|
||||
try {
|
||||
await fsExtra.writeFile(data.filePath, data.content);
|
||||
|
||||
const projectPath = Editor.Project.path;
|
||||
const relativeAssetPath = path.relative(projectPath, data.filePath).replace(/\\/g, '/');
|
||||
const dbAssetPath = 'db://' + relativeAssetPath;
|
||||
await Editor.Message.request('asset-db', 'refresh-asset', dbAssetPath);
|
||||
|
||||
const fileName = path.basename(data.filePath, path.extname(data.filePath));
|
||||
Editor.Dialog.info('覆盖成功', {
|
||||
detail: `行为树文件 "${fileName}.bt.json" 已更新!`,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
Editor.Dialog.error('覆盖失败', {
|
||||
detail: `覆盖行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
import { exec } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { TemplateGenerator } from '../TemplateGenerator';
|
||||
|
||||
/**
|
||||
* ECS框架相关的处理器
|
||||
*/
|
||||
export class EcsFrameworkHandler {
|
||||
/**
|
||||
* 安装ECS Framework
|
||||
*/
|
||||
static async install(): Promise<void> {
|
||||
const projectPath = Editor.Project.path;
|
||||
const command = 'npm install @esengine/ecs-framework';
|
||||
|
||||
console.log(`Installing ECS Framework to project: ${projectPath}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, { cwd: projectPath }, (error, stdout, stderr) => {
|
||||
console.log('Install stdout:', stdout);
|
||||
if (stderr) console.log('Install stderr:', stderr);
|
||||
|
||||
if (error) {
|
||||
console.error('Installation failed:', error);
|
||||
reject(error);
|
||||
} else {
|
||||
console.log('Installation completed successfully');
|
||||
|
||||
// 验证安装是否成功
|
||||
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ecs-framework');
|
||||
const installSuccess = fs.existsSync(nodeModulesPath);
|
||||
|
||||
if (installSuccess) {
|
||||
console.log('ECS Framework installed successfully');
|
||||
resolve();
|
||||
} else {
|
||||
console.warn('ECS Framework directory not found after install');
|
||||
reject(new Error('安装验证失败'));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新ECS Framework
|
||||
*/
|
||||
static async update(targetVersion?: string): Promise<void> {
|
||||
const projectPath = Editor.Project.path;
|
||||
const version = targetVersion ? `@${targetVersion}` : '@latest';
|
||||
const command = `npm install @esengine/ecs-framework${version}`;
|
||||
|
||||
console.log(`Updating ECS Framework to ${version} in project: ${projectPath}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, { cwd: projectPath }, (error, stdout, stderr) => {
|
||||
console.log('Update stdout:', stdout);
|
||||
if (stderr) console.log('Update stderr:', stderr);
|
||||
|
||||
if (error) {
|
||||
console.error('Update failed:', error);
|
||||
reject(error);
|
||||
} else {
|
||||
console.log('Update completed successfully');
|
||||
|
||||
// 验证更新是否成功
|
||||
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ecs-framework');
|
||||
const updateSuccess = fs.existsSync(nodeModulesPath);
|
||||
|
||||
if (updateSuccess) {
|
||||
console.log(`ECS Framework updated successfully to ${version}`);
|
||||
resolve();
|
||||
} else {
|
||||
console.warn('ECS Framework directory not found after update');
|
||||
reject(new Error('更新验证失败'));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载ECS Framework
|
||||
*/
|
||||
static async uninstall(): Promise<void> {
|
||||
const projectPath = Editor.Project.path;
|
||||
const command = 'npm uninstall @esengine/ecs-framework';
|
||||
|
||||
console.log(`Uninstalling ECS Framework from project: ${projectPath}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, { cwd: projectPath }, (error, stdout, stderr) => {
|
||||
console.log('Uninstall stdout:', stdout);
|
||||
if (stderr) console.log('Uninstall stderr:', stderr);
|
||||
|
||||
if (error) {
|
||||
console.error('Uninstall failed:', error);
|
||||
reject(error);
|
||||
} else {
|
||||
console.log('Uninstall completed successfully');
|
||||
|
||||
// 检查是否真的卸载了
|
||||
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ecs-framework');
|
||||
const stillExists = fs.existsSync(nodeModulesPath);
|
||||
|
||||
if (stillExists) {
|
||||
console.warn('ECS Framework directory still exists after uninstall');
|
||||
reject(new Error('卸载验证失败'));
|
||||
} else {
|
||||
console.log('ECS Framework uninstalled successfully');
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开文档
|
||||
*/
|
||||
static openDocumentation(): void {
|
||||
const url = 'https://github.com/esengine/ecs-framework/blob/master/README.md';
|
||||
|
||||
try {
|
||||
// 使用Electron的shell模块打开外部链接
|
||||
const { shell } = require('electron');
|
||||
shell.openExternal(url);
|
||||
console.log('Documentation link opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to open documentation:', error);
|
||||
Editor.Dialog.info('打开文档', {
|
||||
detail: `请手动访问以下链接查看文档:\n\n${url}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建ECS模板
|
||||
*/
|
||||
static createTemplate(): void {
|
||||
const projectPath = Editor.Project.path;
|
||||
console.log(`Creating ECS template in project: ${projectPath}`);
|
||||
|
||||
try {
|
||||
const templateGenerator = new TemplateGenerator(projectPath);
|
||||
|
||||
// 检查是否已存在模板
|
||||
if (templateGenerator.checkTemplateExists()) {
|
||||
const existingFiles = templateGenerator.getExistingFiles();
|
||||
const fileList = existingFiles.length > 0 ? existingFiles.join('\n• ') : '未检测到具体文件';
|
||||
|
||||
Editor.Dialog.warn('模板已存在', {
|
||||
detail: `检测到已存在ECS模板,包含以下文件:\n\n• ${fileList}\n\n是否要覆盖现有模板?`,
|
||||
buttons: ['覆盖', '取消'],
|
||||
}).then((result: any) => {
|
||||
if (result.response === 0) {
|
||||
// 用户选择覆盖
|
||||
console.log('User chose to overwrite existing template');
|
||||
templateGenerator.removeExistingTemplate();
|
||||
templateGenerator.createTemplate();
|
||||
this.showTemplateCreatedDialog();
|
||||
} else {
|
||||
console.log('User cancelled template creation');
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建新模板
|
||||
templateGenerator.createTemplate();
|
||||
console.log('ECS template created successfully');
|
||||
this.showTemplateCreatedDialog();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to create ECS template:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
Editor.Dialog.error('模板创建失败', {
|
||||
detail: `创建ECS模板时发生错误:\n\n${errorMessage}\n\n请检查项目权限和目录结构。`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示模板创建成功的对话框
|
||||
*/
|
||||
private static showTemplateCreatedDialog(): void {
|
||||
Editor.Dialog.info('模板创建成功', {
|
||||
detail: '✅ ECS项目模板已创建完成!\n\n已为您的Cocos Creator项目生成了完整的ECS架构模板,包括:\n\n' +
|
||||
'• 位置、速度、Cocos节点组件\n' +
|
||||
'• 移动系统和节点同步系统\n' +
|
||||
'• 实体工厂和场景管理器\n' +
|
||||
'• ECS管理器组件(可直接添加到节点)\n' +
|
||||
'• 完整的使用文档\n\n' +
|
||||
'请刷新资源管理器查看新创建的文件。',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开GitHub仓库
|
||||
*/
|
||||
static openGitHub(): void {
|
||||
const url = 'https://github.com/esengine/ecs-framework';
|
||||
|
||||
try {
|
||||
const { shell } = require('electron');
|
||||
shell.openExternal(url);
|
||||
console.log('GitHub repository opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to open GitHub repository:', error);
|
||||
Editor.Dialog.info('打开GitHub', {
|
||||
detail: `请手动访问以下链接:\n\n${url}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开QQ群
|
||||
*/
|
||||
static openQQGroup(): void {
|
||||
const url = 'https://qm.qq.com/cgi-bin/qm/qr?k=your-qq-group-key';
|
||||
|
||||
try {
|
||||
const { shell } = require('electron');
|
||||
shell.openExternal(url);
|
||||
console.log('QQ group opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to open QQ group:', error);
|
||||
Editor.Dialog.info('QQ群', {
|
||||
detail: '请手动搜索QQ群号或访问相关链接加入讨论群。',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 面板管理相关的处理器
|
||||
*/
|
||||
export class PanelHandler {
|
||||
/**
|
||||
* 打开默认面板
|
||||
*/
|
||||
static openDefaultPanel(): void {
|
||||
try {
|
||||
Editor.Panel.open('cocos-ecs-extension');
|
||||
console.log('Default panel opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to open default panel:', error);
|
||||
Editor.Dialog.error('打开面板失败', {
|
||||
detail: `无法打开面板:\n\n${error}\n\n请尝试重启Cocos Creator编辑器。`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开调试面板
|
||||
*/
|
||||
static openDebugPanel(): void {
|
||||
try {
|
||||
Editor.Panel.open('cocos-ecs-extension.debug');
|
||||
console.log('Debug panel opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to open debug panel:', error);
|
||||
Editor.Dialog.error('打开调试面板失败', {
|
||||
detail: `无法打开调试面板:\n\n${error}\n\n请尝试重启Cocos Creator编辑器。`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开代码生成器面板
|
||||
*/
|
||||
static openGeneratorPanel(): void {
|
||||
try {
|
||||
Editor.Panel.open('cocos-ecs-extension.generator');
|
||||
console.log('Generator panel opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to open generator panel:', error);
|
||||
Editor.Dialog.error('打开代码生成器失败', {
|
||||
detail: `无法打开代码生成器面板:\n\n${error}\n\n请尝试重启Cocos Creator编辑器。`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开行为树面板
|
||||
*/
|
||||
static openBehaviorTreePanel(): void {
|
||||
try {
|
||||
Editor.Panel.open('cocos-ecs-extension.behavior-tree');
|
||||
console.log('Behavior Tree panel opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to open behavior tree panel:', error);
|
||||
Editor.Dialog.error('打开行为树面板失败', {
|
||||
detail: `无法打开行为树AI组件库面板:\n\n${error}\n\n请尝试重启Cocos Creator编辑器。`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { EcsFrameworkHandler } from './EcsFrameworkHandler';
|
||||
export { BehaviorTreeHandler } from './BehaviorTreeHandler';
|
||||
export { PanelHandler } from './PanelHandler';
|
||||
@@ -1,655 +1,149 @@
|
||||
// @ts-ignore
|
||||
import packageJSON from '../package.json';
|
||||
import { exec, spawn } from 'child_process';
|
||||
import { EcsFrameworkHandler, BehaviorTreeHandler, PanelHandler } from './handlers';
|
||||
import { readJSON } from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as fsExtra from 'fs-extra';
|
||||
import { readFileSync, outputFile } from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import { TemplateGenerator } from './TemplateGenerator';
|
||||
import { CodeGenerator } from './CodeGenerator';
|
||||
import { AssetInfo } from '@cocos/creator-types/editor/packages/asset-db/@types/public';
|
||||
|
||||
/**
|
||||
* @en Registration method for the main process of Extension
|
||||
* @zh 为扩展的主进程的注册方法
|
||||
*/
|
||||
export const methods: { [key: string]: (...any: any) => any } = {
|
||||
// ================ 面板管理 ================
|
||||
/**
|
||||
* @en A method that can be triggered by message
|
||||
* @zh 通过 message 触发的方法
|
||||
* 打开默认面板
|
||||
*/
|
||||
openPanel() {
|
||||
Editor.Panel.open(packageJSON.name);
|
||||
},
|
||||
|
||||
/**
|
||||
* 安装ECS Framework
|
||||
*/
|
||||
'install-ecs-framework'() {
|
||||
const projectPath = Editor.Project.path;
|
||||
const command = 'npm install @esengine/ecs-framework';
|
||||
|
||||
console.log(`Installing ECS Framework to project: ${projectPath}`);
|
||||
console.log(`Command: ${command}`);
|
||||
|
||||
exec(command, { cwd: projectPath }, (error, stdout, stderr) => {
|
||||
console.log('Install stdout:', stdout);
|
||||
console.log('Install stderr:', stderr);
|
||||
|
||||
if (error) {
|
||||
console.error('Installation failed:', error);
|
||||
} else {
|
||||
console.log('Installation completed successfully');
|
||||
|
||||
// 验证安装是否成功
|
||||
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ecs-framework');
|
||||
const installSuccess = require('fs').existsSync(nodeModulesPath);
|
||||
|
||||
if (installSuccess) {
|
||||
console.log('ECS Framework installed successfully');
|
||||
} else {
|
||||
console.warn('ECS Framework directory not found after install');
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新ECS Framework
|
||||
*/
|
||||
'update-ecs-framework'(targetVersion?: string) {
|
||||
const projectPath = Editor.Project.path;
|
||||
const version = targetVersion ? `@${targetVersion}` : '@latest';
|
||||
const command = `npm install @esengine/ecs-framework${version}`;
|
||||
|
||||
console.log(`Updating ECS Framework to ${version} in project: ${projectPath}`);
|
||||
console.log(`Command: ${command}`);
|
||||
|
||||
exec(command, { cwd: projectPath }, (error, stdout, stderr) => {
|
||||
console.log('Update stdout:', stdout);
|
||||
console.log('Update stderr:', stderr);
|
||||
|
||||
if (error) {
|
||||
console.error('Update failed:', error);
|
||||
} else {
|
||||
console.log('Update completed successfully');
|
||||
|
||||
// 验证更新是否成功
|
||||
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ecs-framework');
|
||||
const updateSuccess = require('fs').existsSync(nodeModulesPath);
|
||||
|
||||
if (updateSuccess) {
|
||||
console.log(`ECS Framework updated successfully to ${version}`);
|
||||
} else {
|
||||
console.warn('ECS Framework directory not found after update');
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 卸载ECS Framework
|
||||
*/
|
||||
'uninstall-ecs-framework'() {
|
||||
const projectPath = Editor.Project.path;
|
||||
const command = 'npm uninstall @esengine/ecs-framework';
|
||||
|
||||
console.log(`Uninstalling ECS Framework from project: ${projectPath}`);
|
||||
console.log(`Command: ${command}`);
|
||||
|
||||
exec(command, { cwd: projectPath }, (error, stdout, stderr) => {
|
||||
console.log('Uninstall stdout:', stdout);
|
||||
console.log('Uninstall stderr:', stderr);
|
||||
|
||||
if (error) {
|
||||
console.error('Uninstall failed:', error);
|
||||
} else {
|
||||
console.log('Uninstall completed successfully');
|
||||
|
||||
// 检查是否真的卸载了
|
||||
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ecs-framework');
|
||||
const stillExists = require('fs').existsSync(nodeModulesPath);
|
||||
|
||||
if (stillExists) {
|
||||
console.warn('ECS Framework directory still exists after uninstall');
|
||||
} else {
|
||||
console.log('ECS Framework uninstalled successfully');
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开文档
|
||||
*/
|
||||
'open-documentation'() {
|
||||
const url = 'https://github.com/esengine/ecs-framework/blob/master/README.md';
|
||||
|
||||
try {
|
||||
// 使用Electron的shell模块打开外部链接(推荐方法)
|
||||
const { shell } = require('electron');
|
||||
shell.openExternal(url);
|
||||
console.log('Documentation link opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to open documentation with shell.openExternal, trying exec:', error);
|
||||
|
||||
// 备用方法:使用系统命令
|
||||
exec(`start "" "${url}"`, (execError) => {
|
||||
if (execError) {
|
||||
console.error('Failed to open documentation with exec:', execError);
|
||||
Editor.Dialog.info('打开文档', {
|
||||
detail: `请手动访问以下链接查看文档:\n\n${url}`,
|
||||
});
|
||||
} else {
|
||||
console.log('Documentation link opened successfully with exec');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建ECS模板
|
||||
*/
|
||||
'create-ecs-template'() {
|
||||
const projectPath = Editor.Project.path;
|
||||
console.log(`Creating ECS template in project: ${projectPath}`);
|
||||
|
||||
try {
|
||||
const templateGenerator = new TemplateGenerator(projectPath);
|
||||
|
||||
// 检查是否已存在模板
|
||||
if (templateGenerator.checkTemplateExists()) {
|
||||
const existingFiles = templateGenerator.getExistingFiles();
|
||||
const fileList = existingFiles.length > 0 ? existingFiles.join('\n• ') : '未检测到具体文件';
|
||||
|
||||
Editor.Dialog.warn('模板已存在', {
|
||||
detail: `检测到已存在ECS模板,包含以下文件:\n\n• ${fileList}\n\n是否要覆盖现有模板?`,
|
||||
buttons: ['覆盖', '取消'],
|
||||
}).then((result: any) => {
|
||||
if (result.response === 0) {
|
||||
// 用户选择覆盖
|
||||
console.log('User chose to overwrite existing template');
|
||||
templateGenerator.removeExistingTemplate();
|
||||
templateGenerator.createTemplate();
|
||||
|
||||
Editor.Dialog.info('模板创建成功', {
|
||||
detail: '✅ ECS项目模板已覆盖并重新创建完成!\n\n已为您的Cocos Creator项目生成了完整的ECS架构模板,包括:\n\n' +
|
||||
'• 位置、速度、Cocos节点组件\n' +
|
||||
'• 移动系统和节点同步系统\n' +
|
||||
'• 实体工厂和场景管理器\n' +
|
||||
'• ECS管理器组件(可直接添加到节点)\n' +
|
||||
'• 完整的使用文档\n\n' +
|
||||
'请刷新资源管理器查看新创建的文件。',
|
||||
});
|
||||
} else {
|
||||
console.log('User cancelled template creation');
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建新模板
|
||||
templateGenerator.createTemplate();
|
||||
|
||||
console.log('ECS template created successfully');
|
||||
|
||||
Editor.Dialog.info('模板创建成功', {
|
||||
detail: '✅ ECS项目模板已创建完成!\n\n已为您的Cocos Creator项目生成了完整的ECS架构模板,包括:\n\n' +
|
||||
'• 位置、速度、Cocos节点组件\n' +
|
||||
'• 移动系统和节点同步系统\n' +
|
||||
'• 实体工厂和场景管理器\n' +
|
||||
'• ECS管理器组件(可直接添加到节点)\n' +
|
||||
'• 完整的使用文档\n\n' +
|
||||
'请刷新资源管理器查看新创建的文件。',
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to create ECS template:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
Editor.Dialog.error('模板创建失败', {
|
||||
detail: `创建ECS模板时发生错误:\n\n${errorMessage}\n\n请检查项目权限和目录结构。`,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开GitHub仓库
|
||||
*/
|
||||
'open-github'() {
|
||||
const url = 'https://github.com/esengine/ecs-framework';
|
||||
|
||||
try {
|
||||
// 使用Electron的shell模块打开外部链接(推荐方法)
|
||||
const { shell } = require('electron');
|
||||
shell.openExternal(url);
|
||||
console.log('GitHub link opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to open GitHub with shell.openExternal, trying exec:', error);
|
||||
|
||||
// 备用方法:使用系统命令
|
||||
exec(`start "" "${url}"`, (execError) => {
|
||||
if (execError) {
|
||||
console.error('Failed to open GitHub with exec:', execError);
|
||||
Editor.Dialog.info('打开GitHub', {
|
||||
detail: `请手动访问以下链接:\n\n${url}`,
|
||||
});
|
||||
} else {
|
||||
console.log('GitHub link opened successfully with exec');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开QQ群
|
||||
*/
|
||||
'open-qq-group'() {
|
||||
const url = 'https://qm.qq.com/cgi-bin/qm/qr?k=1DMoPJEsY5xUpTAcmjIHK8whgHJHYQTL&authKey=%2FklVb3S0Momc1q1J%2FWHncuwMVHGrDbwV1Y6gAfa5e%2FgHCvyYUL2gpA6hSOU%2BVSa5&noverify=0&group_code=481923584';
|
||||
|
||||
try {
|
||||
// 使用Electron的shell模块打开外部链接(推荐方法)
|
||||
const { shell } = require('electron');
|
||||
shell.openExternal(url);
|
||||
console.log('QQ group link opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to open QQ group with shell.openExternal, trying exec:', error);
|
||||
|
||||
// 备用方法:使用系统命令
|
||||
exec(`start "" "${url}"`, (execError) => {
|
||||
if (execError) {
|
||||
console.error('Failed to open QQ group with exec:', execError);
|
||||
Editor.Dialog.info('加入QQ群', {
|
||||
detail: `请手动访问以下链接加入QQ群:\n\n${url}\n\n或手动搜索QQ群号:481923584`,
|
||||
});
|
||||
} else {
|
||||
console.log('QQ group link opened successfully with exec');
|
||||
}
|
||||
});
|
||||
}
|
||||
PanelHandler.openDefaultPanel();
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开调试面板
|
||||
*/
|
||||
'open-debug'() {
|
||||
console.log('Opening ECS Framework debug panel...');
|
||||
try {
|
||||
// 正确的打开特定面板的方法
|
||||
Editor.Panel.open(packageJSON.name + '.debug');
|
||||
console.log('Debug panel opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to open debug panel:', error);
|
||||
Editor.Dialog.error('打开调试面板失败', {
|
||||
detail: `无法打开调试面板:\n\n${error}\n\n请尝试重启Cocos Creator编辑器。`,
|
||||
});
|
||||
}
|
||||
PanelHandler.openDebugPanel();
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开代码生成器面板
|
||||
*/
|
||||
'open-generator'() {
|
||||
console.log('Opening ECS Framework code generator panel...');
|
||||
try {
|
||||
// 正确的打开特定面板的方法
|
||||
Editor.Panel.open(packageJSON.name + '.generator');
|
||||
console.log('Generator panel opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to open generator panel:', error);
|
||||
Editor.Dialog.error('打开代码生成器失败', {
|
||||
detail: `无法打开代码生成器面板:\n\n${error}\n\n请尝试重启Cocos Creator编辑器。`,
|
||||
});
|
||||
}
|
||||
PanelHandler.openGeneratorPanel();
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开行为树AI组件库面板
|
||||
* 打开行为树面板
|
||||
*/
|
||||
'open-behavior-tree'() {
|
||||
console.log('Opening Behavior Tree AI panel...');
|
||||
try {
|
||||
Editor.Panel.open(packageJSON.name + '.behavior-tree');
|
||||
console.log('Behavior Tree panel opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to open behavior tree panel:', error);
|
||||
Editor.Dialog.error('打开行为树面板失败', {
|
||||
detail: `无法打开行为树AI组件库面板:\n\n${error}\n\n请尝试重启Cocos Creator编辑器。`,
|
||||
});
|
||||
}
|
||||
PanelHandler.openBehaviorTreePanel();
|
||||
},
|
||||
|
||||
// ================ ECS框架管理 ================
|
||||
/**
|
||||
* 安装ECS Framework
|
||||
*/
|
||||
'install-ecs-framework'() {
|
||||
EcsFrameworkHandler.install();
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新ECS Framework
|
||||
*/
|
||||
'update-ecs-framework'() {
|
||||
EcsFrameworkHandler.update();
|
||||
},
|
||||
|
||||
/**
|
||||
* 卸载ECS Framework
|
||||
*/
|
||||
'uninstall-ecs-framework'() {
|
||||
EcsFrameworkHandler.uninstall();
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开文档
|
||||
*/
|
||||
'open-documentation'() {
|
||||
EcsFrameworkHandler.openDocumentation();
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建ECS模板
|
||||
*/
|
||||
'create-ecs-template'() {
|
||||
EcsFrameworkHandler.createTemplate();
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开GitHub仓库
|
||||
*/
|
||||
'open-github'() {
|
||||
EcsFrameworkHandler.openGitHub();
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开QQ群
|
||||
*/
|
||||
'open-qq-group'() {
|
||||
EcsFrameworkHandler.openQQGroup();
|
||||
},
|
||||
|
||||
// ================ 行为树管理 ================
|
||||
/**
|
||||
* 安装行为树AI系统
|
||||
*/
|
||||
async 'install-behavior-tree'() {
|
||||
console.log('Installing Behavior Tree AI system...');
|
||||
const projectPath = Editor.Project.path;
|
||||
|
||||
try {
|
||||
// 检查项目路径是否有效
|
||||
if (!projectPath || !fs.existsSync(projectPath)) {
|
||||
throw new Error('无效的项目路径');
|
||||
}
|
||||
|
||||
const packageJsonPath = path.join(projectPath, 'package.json');
|
||||
|
||||
// 检查package.json是否存在
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
throw new Error('项目根目录未找到package.json文件');
|
||||
}
|
||||
|
||||
console.log('Installing @esengine/ai package...');
|
||||
|
||||
// 执行npm安装
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const cmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||
const npmProcess = spawn(cmd, ['install', '@esengine/ai'], {
|
||||
cwd: projectPath,
|
||||
stdio: 'pipe',
|
||||
shell: true
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
npmProcess.stdout?.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
npmProcess.stderr?.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
npmProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
console.log('NPM install completed successfully');
|
||||
console.log('STDOUT:', stdout);
|
||||
resolve();
|
||||
} else {
|
||||
console.error('NPM install failed with code:', code);
|
||||
console.error('STDERR:', stderr);
|
||||
reject(new Error(`NPM安装失败 (退出码: ${code})\n\n${stderr || stdout}`));
|
||||
}
|
||||
});
|
||||
|
||||
npmProcess.on('error', (error) => {
|
||||
console.error('NPM process error:', error);
|
||||
reject(new Error(`NPM进程错误: ${error.message}`));
|
||||
});
|
||||
});
|
||||
|
||||
// 复制行为树相关文件到项目中
|
||||
const sourceDir = path.join(__dirname, '../../../thirdparty/BehaviourTree-ai');
|
||||
const targetDir = path.join(projectPath, 'assets/scripts/AI');
|
||||
|
||||
if (fs.existsSync(sourceDir)) {
|
||||
console.log('Copying behavior tree files...');
|
||||
await fsExtra.ensureDir(targetDir);
|
||||
|
||||
// 创建示例文件
|
||||
const exampleCode = `import { Scene, Entity, Component } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreeSystem, BehaviorTreeFactory, TaskStatus } from '@esengine/ai/ecs-integration';
|
||||
|
||||
/**
|
||||
* 示例AI组件
|
||||
*/
|
||||
export class AIExampleComponent extends Component {
|
||||
// 在场景中添加行为树系统
|
||||
static setupBehaviorTreeSystem(scene: Scene) {
|
||||
const behaviorTreeSystem = new BehaviorTreeSystem();
|
||||
scene.addEntityProcessor(behaviorTreeSystem);
|
||||
return behaviorTreeSystem;
|
||||
}
|
||||
|
||||
// 为实体添加简单AI行为
|
||||
static addSimpleAI(entity: Entity) {
|
||||
BehaviorTreeFactory.addBehaviorTreeToEntity(
|
||||
entity,
|
||||
(builder) => builder
|
||||
.selector()
|
||||
.action((entity) => {
|
||||
console.log("AI正在巡逻...");
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
.action((entity) => {
|
||||
console.log("AI正在警戒...");
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
.endComposite(),
|
||||
{ debugMode: true }
|
||||
);
|
||||
}
|
||||
}`;
|
||||
|
||||
const examplePath = path.join(targetDir, 'AIExample.ts');
|
||||
await fsExtra.writeFile(examplePath, exampleCode);
|
||||
console.log('Example file created successfully');
|
||||
}
|
||||
|
||||
console.log('Behavior Tree AI system installed successfully');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to install Behavior Tree AI system:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`行为树AI系统安装失败:\n\n${errorMessage}`);
|
||||
}
|
||||
'install-behavior-tree'() {
|
||||
BehaviorTreeHandler.install();
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新行为树AI系统
|
||||
*/
|
||||
async 'update-behavior-tree'() {
|
||||
console.log('Updating Behavior Tree AI system...');
|
||||
const projectPath = Editor.Project.path;
|
||||
|
||||
try {
|
||||
// 检查是否已安装
|
||||
const packageJsonPath = path.join(projectPath, 'package.json');
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
throw new Error('项目根目录未找到package.json文件');
|
||||
}
|
||||
|
||||
const packageJson = await fsExtra.readJson(packageJsonPath);
|
||||
const dependencies = packageJson.dependencies || {};
|
||||
|
||||
if (!dependencies['@esengine/ai']) {
|
||||
throw new Error('尚未安装行为树AI系统,请先进行安装');
|
||||
}
|
||||
|
||||
console.log('Checking for updates...');
|
||||
|
||||
// 执行npm更新
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const cmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||
const npmProcess = spawn(cmd, ['update', '@esengine/ai'], {
|
||||
cwd: projectPath,
|
||||
stdio: 'pipe',
|
||||
shell: true
|
||||
});
|
||||
|
||||
npmProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
console.log('Update completed successfully');
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`更新失败 (退出码: ${code})`));
|
||||
}
|
||||
});
|
||||
|
||||
npmProcess.on('error', (error) => {
|
||||
reject(new Error(`更新进程错误: ${error.message}`));
|
||||
});
|
||||
});
|
||||
|
||||
console.log('Behavior Tree AI system updated successfully');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to update Behavior Tree AI system:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`行为树AI系统更新失败:\n\n${errorMessage}`);
|
||||
}
|
||||
'update-behavior-tree'() {
|
||||
BehaviorTreeHandler.update();
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查行为树AI系统是否已安装
|
||||
* 检查行为树AI是否已安装
|
||||
*/
|
||||
async 'check-behavior-tree-installed'() {
|
||||
const projectPath = Editor.Project.path;
|
||||
|
||||
try {
|
||||
const packageJsonPath = path.join(projectPath, 'package.json');
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const packageJson = await fsExtra.readJson(packageJsonPath);
|
||||
const dependencies = packageJson.dependencies || {};
|
||||
|
||||
return !!dependencies['@esengine/ai'];
|
||||
} catch (error) {
|
||||
console.error('Failed to check installation status:', error);
|
||||
return false;
|
||||
}
|
||||
'check-behavior-tree-installed'() {
|
||||
return BehaviorTreeHandler.checkInstalled();
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开行为树文档
|
||||
*/
|
||||
'open-behavior-tree-docs'() {
|
||||
const url = 'https://github.com/esengine/BehaviourTree-ai/blob/master/ecs-integration/README.md';
|
||||
|
||||
try {
|
||||
const { shell } = require('electron');
|
||||
shell.openExternal(url);
|
||||
console.log('Behavior Tree documentation opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to open documentation:', error);
|
||||
Editor.Dialog.info('打开文档', {
|
||||
detail: `请手动访问以下链接查看行为树文档:\n\n${url}`,
|
||||
});
|
||||
}
|
||||
BehaviorTreeHandler.openDocumentation();
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建行为树文件
|
||||
*/
|
||||
async 'create-behavior-tree-file'(assetInfo: any) {
|
||||
console.log('Creating behavior tree file in folder:', assetInfo?.path);
|
||||
|
||||
try {
|
||||
// 获取项目assets目录
|
||||
const projectPath = Editor.Project.path;
|
||||
const assetsPath = path.join(projectPath, 'assets');
|
||||
|
||||
// 生成唯一文件名
|
||||
let fileName = 'NewBehaviorTree';
|
||||
let counter = 1;
|
||||
let filePath = path.join(assetsPath, `${fileName}.bt.json`);
|
||||
|
||||
while (fs.existsSync(filePath)) {
|
||||
fileName = `NewBehaviorTree_${counter}`;
|
||||
filePath = path.join(assetsPath, `${fileName}.bt.json`);
|
||||
counter++;
|
||||
}
|
||||
|
||||
// 创建默认的行为树配置
|
||||
const defaultConfig = {
|
||||
version: "1.0.0",
|
||||
type: "behavior-tree",
|
||||
metadata: {
|
||||
createdAt: new Date().toISOString(),
|
||||
nodeCount: 1
|
||||
},
|
||||
tree: {
|
||||
id: "root",
|
||||
type: "sequence",
|
||||
namespace: "behaviourTree/composites",
|
||||
properties: {},
|
||||
children: []
|
||||
}
|
||||
};
|
||||
|
||||
// 写入文件
|
||||
await fsExtra.writeFile(filePath, JSON.stringify(defaultConfig, null, 2));
|
||||
|
||||
// 刷新资源管理器
|
||||
await Editor.Message.request('asset-db', 'refresh-asset', 'db://assets');
|
||||
|
||||
console.log(`Behavior tree file created: ${filePath}`);
|
||||
|
||||
Editor.Dialog.info('创建成功', {
|
||||
detail: `行为树文件 "${fileName}.bt.json" 已创建完成!\n\n文件位置:assets/${fileName}.bt.json\n\n您可以右键点击文件选择"用行为树编辑器打开"来编辑它。`,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to create behavior tree file:', error);
|
||||
Editor.Dialog.error('创建失败', {
|
||||
detail: `创建行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`,
|
||||
});
|
||||
}
|
||||
'create-behavior-tree-file'() {
|
||||
BehaviorTreeHandler.createFile();
|
||||
},
|
||||
|
||||
/**
|
||||
* 用行为树编辑器打开文件
|
||||
* 加载行为树文件到编辑器
|
||||
*/
|
||||
async 'open-behavior-tree-file'(assetInfo: any) {
|
||||
console.log('Opening behavior tree file:', assetInfo);
|
||||
async 'load-behavior-tree-file'(...args: any[]) {
|
||||
const assetInfo = args.length >= 2 ? args[1] : args[0];
|
||||
|
||||
try {
|
||||
// 直接从assetInfo获取文件系统路径
|
||||
const assetPath = assetInfo?.path;
|
||||
if (!assetPath) {
|
||||
throw new Error('无效的文件路径');
|
||||
if (!assetInfo || (!assetInfo.file && !assetInfo.path)) {
|
||||
throw new Error('无效的文件信息');
|
||||
}
|
||||
|
||||
// 转换为文件系统路径
|
||||
const projectPath = Editor.Project.path;
|
||||
const relativePath = assetPath.replace('db://assets/', '');
|
||||
const fsPath = path.join(projectPath, 'assets', relativePath);
|
||||
await Editor.Panel.open('cocos-ecs-extension.behavior-tree');
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
console.log('File system path:', fsPath);
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(fsPath)) {
|
||||
throw new Error('文件不存在');
|
||||
}
|
||||
|
||||
// 检查文件是否为JSON格式
|
||||
let fileContent: any;
|
||||
try {
|
||||
const content = await fsExtra.readFile(fsPath, 'utf8');
|
||||
fileContent = JSON.parse(content);
|
||||
} catch (parseError) {
|
||||
throw new Error('文件不是有效的JSON格式');
|
||||
}
|
||||
|
||||
// 验证是否为行为树文件
|
||||
if (fileContent.type !== 'behavior-tree' && !fileContent.tree) {
|
||||
const confirm = await new Promise<boolean>((resolve) => {
|
||||
Editor.Dialog.warn('文件格式提醒', {
|
||||
detail: '此文件可能不是标准的行为树配置文件,仍要打开吗?',
|
||||
buttons: ['打开', '取消'],
|
||||
}).then((result: any) => {
|
||||
resolve(result.response === 0);
|
||||
});
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 打开行为树编辑器面板
|
||||
Editor.Panel.open('cocos-ecs-extension.behavior-tree');
|
||||
|
||||
console.log(`Behavior tree file opened in editor: ${fsPath}`);
|
||||
const result = await Editor.Message.request('cocos-ecs-extension', 'behavior-tree-panel-load-file', assetInfo);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to open behavior tree file:', error);
|
||||
Editor.Dialog.error('打开失败', {
|
||||
detail: `打开行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`,
|
||||
detail: `打开行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -657,57 +151,38 @@ export class AIExampleComponent extends Component {
|
||||
/**
|
||||
* 从编辑器创建行为树文件
|
||||
*/
|
||||
async 'create-behavior-tree-from-editor'(data: { fileName: string, content: string }) {
|
||||
console.log('Creating behavior tree file from editor:', data.fileName);
|
||||
'create-behavior-tree-from-editor'(event: any, data: any) {
|
||||
BehaviorTreeHandler.createFromEditor(data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 覆盖现有行为树文件
|
||||
*/
|
||||
'overwrite-behavior-tree-file'(...args: any[]) {
|
||||
const data = args.length >= 2 ? args[1] : args[0];
|
||||
|
||||
try {
|
||||
const projectPath = Editor.Project.path;
|
||||
const assetsPath = path.join(projectPath, 'assets');
|
||||
|
||||
// 确保文件名唯一
|
||||
let fileName = data.fileName;
|
||||
let counter = 1;
|
||||
let filePath = path.join(assetsPath, `${fileName}.bt.json`);
|
||||
|
||||
while (fs.existsSync(filePath)) {
|
||||
fileName = `${data.fileName}_${counter}`;
|
||||
filePath = path.join(assetsPath, `${fileName}.bt.json`);
|
||||
counter++;
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
await fsExtra.writeFile(filePath, data.content);
|
||||
|
||||
// 刷新资源管理器
|
||||
await Editor.Message.request('asset-db', 'refresh-asset', 'db://assets');
|
||||
|
||||
console.log(`Behavior tree file created from editor: ${filePath}`);
|
||||
|
||||
Editor.Dialog.info('保存成功', {
|
||||
detail: `行为树文件 "${fileName}.bt.json" 已保存到 assets 目录中!`,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to create behavior tree file from editor:', error);
|
||||
Editor.Dialog.error('保存失败', {
|
||||
detail: `保存行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`,
|
||||
});
|
||||
if (data && data.filePath) {
|
||||
BehaviorTreeHandler.overwriteFile(data);
|
||||
} else {
|
||||
throw new Error('文件路径不存在或数据无效');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @en Method triggered when the extension is started
|
||||
* @zh 启动扩展时触发的方法
|
||||
*/
|
||||
export function load() {
|
||||
console.log('ECS Framework Extension loaded');
|
||||
console.log('[Cocos ECS Extension] 扩展已加载');
|
||||
}
|
||||
|
||||
/**
|
||||
* @en Method triggered when uninstalling the extension
|
||||
* @en Method triggered when the extension is uninstalled
|
||||
* @zh 卸载扩展时触发的方法
|
||||
*/
|
||||
export function unload() {
|
||||
console.log('ECS Framework Extension unloaded');
|
||||
console.log('[Cocos ECS Extension] 扩展已卸载');
|
||||
}
|
||||
|
||||
@@ -74,16 +74,6 @@ export function useBehaviorTreeEditor() {
|
||||
appState.isInstalling
|
||||
);
|
||||
|
||||
const fileOps = useFileOperations(
|
||||
appState.treeNodes,
|
||||
appState.selectedNodeId,
|
||||
appState.connections,
|
||||
appState.tempConnection,
|
||||
appState.showExportModal,
|
||||
codeGen,
|
||||
() => connectionManager.updateConnections()
|
||||
);
|
||||
|
||||
const connectionState = reactive({
|
||||
isConnecting: false,
|
||||
startNodeId: null as string | null,
|
||||
@@ -105,6 +95,16 @@ export function useBehaviorTreeEditor() {
|
||||
appState.zoomLevel
|
||||
);
|
||||
|
||||
const fileOps = useFileOperations({
|
||||
treeNodes: appState.treeNodes,
|
||||
selectedNodeId: appState.selectedNodeId,
|
||||
connections: appState.connections,
|
||||
tempConnection: appState.tempConnection,
|
||||
showExportModal: appState.showExportModal,
|
||||
codeGeneration: codeGen,
|
||||
updateConnections: connectionManager.updateConnections
|
||||
});
|
||||
|
||||
const canvasManager = useCanvasManager(
|
||||
appState.panX,
|
||||
appState.panY,
|
||||
@@ -182,11 +182,160 @@ export function useBehaviorTreeEditor() {
|
||||
installation.handleInstall();
|
||||
};
|
||||
|
||||
// 组件挂载时初始化连接
|
||||
onMounted(() => {
|
||||
// 延迟一下确保 DOM 已经渲染
|
||||
nextTick(() => {
|
||||
// 自动布局功能
|
||||
const autoLayout = () => {
|
||||
if (appState.treeNodes.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rootNode = appState.treeNodes.value.find(node =>
|
||||
!appState.treeNodes.value.some(otherNode =>
|
||||
otherNode.children?.includes(node.id)
|
||||
)
|
||||
);
|
||||
|
||||
if (!rootNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const levelNodes: { [level: number]: any[] } = {};
|
||||
const visited = new Set<string>();
|
||||
|
||||
const queue = [{ node: rootNode, level: 0 }];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { node, level } = queue.shift()!;
|
||||
|
||||
if (visited.has(node.id)) continue;
|
||||
visited.add(node.id);
|
||||
|
||||
if (!levelNodes[level]) {
|
||||
levelNodes[level] = [];
|
||||
}
|
||||
levelNodes[level].push(node);
|
||||
|
||||
if (node.children && Array.isArray(node.children)) {
|
||||
node.children.forEach((childId: string) => {
|
||||
const childNode = appState.treeNodes.value.find(n => n.id === childId);
|
||||
if (childNode && !visited.has(childId)) {
|
||||
queue.push({ node: childNode, level: level + 1 });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const nodeWidth = 200;
|
||||
const nodeHeight = 150;
|
||||
const startX = 400;
|
||||
const startY = 100;
|
||||
|
||||
Object.keys(levelNodes).forEach(levelStr => {
|
||||
const level = parseInt(levelStr);
|
||||
const nodes = levelNodes[level];
|
||||
const totalWidth = (nodes.length - 1) * nodeWidth;
|
||||
const offsetX = -totalWidth / 2;
|
||||
|
||||
nodes.forEach((node, index) => {
|
||||
node.x = startX + offsetX + index * nodeWidth;
|
||||
node.y = startY + level * nodeHeight;
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
connectionManager.updateConnections();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// 验证树结构
|
||||
const validateTree = () => {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
const rootNodes = appState.treeNodes.value.filter(node =>
|
||||
!appState.treeNodes.value.some(otherNode =>
|
||||
otherNode.children?.includes(node.id)
|
||||
)
|
||||
);
|
||||
|
||||
if (rootNodes.length === 0) {
|
||||
errors.push('没有找到根节点');
|
||||
} else if (rootNodes.length > 1) {
|
||||
warnings.push(`找到多个根节点: ${rootNodes.map(n => n.name).join(', ')}`);
|
||||
}
|
||||
|
||||
appState.treeNodes.value.forEach(node => {
|
||||
const hasParent = appState.treeNodes.value.some(otherNode =>
|
||||
otherNode.children?.includes(node.id)
|
||||
);
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
|
||||
if (!hasParent && !hasChildren && appState.treeNodes.value.length > 1) {
|
||||
warnings.push(`节点 "${node.name}" 是孤立节点`);
|
||||
}
|
||||
});
|
||||
|
||||
appState.connections.value.forEach(conn => {
|
||||
const sourceNode = appState.treeNodes.value.find(n => n.id === conn.sourceId);
|
||||
const targetNode = appState.treeNodes.value.find(n => n.id === conn.targetId);
|
||||
|
||||
if (!sourceNode) {
|
||||
errors.push(`连接 ${conn.id} 的源节点不存在`);
|
||||
}
|
||||
if (!targetNode) {
|
||||
errors.push(`连接 ${conn.id} 的目标节点不存在`);
|
||||
}
|
||||
});
|
||||
|
||||
let message = '树结构验证完成!\n\n';
|
||||
|
||||
if (errors.length > 0) {
|
||||
message += `❌ 错误 (${errors.length}):\n${errors.map(e => `• ${e}`).join('\n')}\n\n`;
|
||||
}
|
||||
|
||||
if (warnings.length > 0) {
|
||||
message += `⚠️ 警告 (${warnings.length}):\n${warnings.map(w => `• ${w}`).join('\n')}\n\n`;
|
||||
}
|
||||
|
||||
if (errors.length === 0 && warnings.length === 0) {
|
||||
message += '✅ 没有发现问题!';
|
||||
}
|
||||
|
||||
alert(message);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const appContainer = document.querySelector('#behavior-tree-app');
|
||||
if (appContainer) {
|
||||
(appContainer as any).loadFileContent = fileOps.loadFileContent;
|
||||
(appContainer as any).showError = (errorMessage: string) => {
|
||||
alert('文件加载失败: ' + errorMessage);
|
||||
};
|
||||
}
|
||||
|
||||
const handleLoadBehaviorTreeFile = (event: CustomEvent) => {
|
||||
fileOps.loadFileContent(event.detail);
|
||||
};
|
||||
|
||||
const handleFileLoadError = (event: CustomEvent) => {
|
||||
console.error('[BehaviorTreeEditor] DOM事件错误:', event.detail);
|
||||
alert('文件加载失败: ' + event.detail.error);
|
||||
};
|
||||
|
||||
document.addEventListener('load-behavior-tree-file', handleLoadBehaviorTreeFile as EventListener);
|
||||
document.addEventListener('file-load-error', handleFileLoadError as EventListener);
|
||||
|
||||
console.log('[BehaviorTreeEditor] 事件系统准备完成(直接方法调用 + DOM事件备用)');
|
||||
|
||||
onUnmounted(() => {
|
||||
console.log('[BehaviorTreeEditor] 清理事件监听器');
|
||||
document.removeEventListener('load-behavior-tree-file', handleLoadBehaviorTreeFile as EventListener);
|
||||
document.removeEventListener('file-load-error', handleFileLoadError as EventListener);
|
||||
|
||||
// 清理暴露的方法
|
||||
if (appContainer) {
|
||||
delete (appContainer as any).loadFileContent;
|
||||
delete (appContainer as any).showError;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -210,6 +359,8 @@ export function useBehaviorTreeEditor() {
|
||||
...canvasManager,
|
||||
...nodeDisplay,
|
||||
startNodeDrag,
|
||||
dragState
|
||||
dragState,
|
||||
autoLayout,
|
||||
validateTree
|
||||
};
|
||||
}
|
||||
@@ -268,13 +268,102 @@ export const config = behaviorTreeConfig;`;
|
||||
|
||||
// 从配置创建行为树节点
|
||||
const createTreeFromConfig = (config: any): TreeNode[] => {
|
||||
if (!config || !config.tree) {
|
||||
console.log('createTreeFromConfig被调用,接收到的配置:', config);
|
||||
console.log('nodeTemplates当前数量:', nodeTemplates.value.length);
|
||||
|
||||
// 处理两种不同的文件格式
|
||||
if (config.nodes && Array.isArray(config.nodes)) {
|
||||
console.log('使用nodes格式处理,节点数量:', config.nodes.length);
|
||||
const result = createTreeFromNodesFormat(config);
|
||||
console.log('nodes格式处理结果:', result);
|
||||
return result;
|
||||
} else if (config.tree) {
|
||||
console.log('使用tree格式处理');
|
||||
const result = createTreeFromTreeFormat(config);
|
||||
console.log('tree格式处理结果:', result);
|
||||
return result;
|
||||
} else {
|
||||
console.log('配置格式不匹配,返回空数组');
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 处理新格式(nodes数组格式)
|
||||
const createTreeFromNodesFormat = (config: any): TreeNode[] => {
|
||||
console.log('createTreeFromNodesFormat开始处理');
|
||||
|
||||
if (!config.nodes || !Array.isArray(config.nodes)) {
|
||||
console.log('nodes数据无效');
|
||||
return [];
|
||||
}
|
||||
|
||||
const nodes: TreeNode[] = [];
|
||||
|
||||
config.nodes.forEach((nodeConfig: any, index: number) => {
|
||||
console.log(`处理第${index + 1}个节点:`, nodeConfig);
|
||||
|
||||
const template = findTemplateByType(nodeConfig.type);
|
||||
console.log(`为节点类型 "${nodeConfig.type}" 找到的模板:`, template);
|
||||
|
||||
if (!template) {
|
||||
console.warn(`未找到节点类型 "${nodeConfig.type}" 的模板`);
|
||||
return;
|
||||
}
|
||||
|
||||
const node: TreeNode = {
|
||||
id: nodeConfig.id || generateNodeId(),
|
||||
type: template.type,
|
||||
name: nodeConfig.name || template.name,
|
||||
icon: nodeConfig.icon || template.icon,
|
||||
description: nodeConfig.description || template.description,
|
||||
canHaveChildren: template.canHaveChildren,
|
||||
canHaveParent: template.canHaveParent,
|
||||
x: nodeConfig.x || 400,
|
||||
y: nodeConfig.y || 100,
|
||||
properties: {},
|
||||
children: nodeConfig.children || [],
|
||||
parent: nodeConfig.parent,
|
||||
hasError: false
|
||||
};
|
||||
|
||||
// 恢复属性
|
||||
if (nodeConfig.properties && template.properties) {
|
||||
Object.entries(nodeConfig.properties).forEach(([key, propConfig]: [string, any]) => {
|
||||
if (template.properties![key]) {
|
||||
node.properties![key] = {
|
||||
...template.properties![key],
|
||||
value: propConfig.value !== undefined ? propConfig.value : template.properties![key].value
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 确保所有模板属性都有默认值
|
||||
if (template.properties) {
|
||||
Object.entries(template.properties).forEach(([key, propDef]) => {
|
||||
if (!node.properties![key]) {
|
||||
node.properties![key] = { ...propDef };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`创建的节点:`, node);
|
||||
nodes.push(node);
|
||||
});
|
||||
|
||||
console.log(`createTreeFromNodesFormat完成,总共创建了${nodes.length}个节点`);
|
||||
return nodes;
|
||||
};
|
||||
|
||||
// 处理旧格式(tree对象格式)
|
||||
const createTreeFromTreeFormat = (config: any): TreeNode[] => {
|
||||
if (!config.tree) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const nodes: TreeNode[] = [];
|
||||
const processNode = (nodeConfig: any, parent?: TreeNode): TreeNode => {
|
||||
const template = nodeTemplates.value.find(t => t.className === nodeConfig.type);
|
||||
const template = findTemplateByType(nodeConfig.type);
|
||||
if (!template) {
|
||||
throw new Error(`未知节点类型: ${nodeConfig.type}`);
|
||||
}
|
||||
@@ -287,25 +376,35 @@ export const config = behaviorTreeConfig;`;
|
||||
description: template.description,
|
||||
canHaveChildren: template.canHaveChildren,
|
||||
canHaveParent: template.canHaveParent,
|
||||
x: 400, // 默认在画布中心
|
||||
y: 100, // 从顶部开始
|
||||
x: 400,
|
||||
y: 100,
|
||||
properties: {},
|
||||
children: [],
|
||||
parent: parent?.id // 设置父节点ID
|
||||
parent: parent?.id,
|
||||
hasError: false
|
||||
};
|
||||
|
||||
// 恢复属性
|
||||
if (nodeConfig.properties) {
|
||||
if (nodeConfig.properties && template.properties) {
|
||||
Object.entries(nodeConfig.properties).forEach(([key, propConfig]: [string, any]) => {
|
||||
if (template.properties?.[key]) {
|
||||
if (template.properties![key]) {
|
||||
node.properties![key] = {
|
||||
...template.properties[key],
|
||||
value: propConfig.value
|
||||
...template.properties![key],
|
||||
value: propConfig.value !== undefined ? propConfig.value : template.properties![key].value
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 确保所有模板属性都有默认值
|
||||
if (template.properties) {
|
||||
Object.entries(template.properties).forEach(([key, propDef]) => {
|
||||
if (!node.properties![key]) {
|
||||
node.properties![key] = { ...propDef };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
nodes.push(node);
|
||||
|
||||
// 处理子节点
|
||||
@@ -323,6 +422,49 @@ export const config = behaviorTreeConfig;`;
|
||||
return nodes;
|
||||
};
|
||||
|
||||
// 通过类型名查找模板(支持多种匹配方式)
|
||||
const findTemplateByType = (typeName: string): NodeTemplate | undefined => {
|
||||
// 直接匹配 type 字段
|
||||
let template = nodeTemplates.value.find(t => t.type === typeName);
|
||||
if (template) return template;
|
||||
|
||||
// 匹配 className 字段
|
||||
template = nodeTemplates.value.find(t => t.className === typeName);
|
||||
if (template) return template;
|
||||
|
||||
// 大小写不敏感匹配 type
|
||||
template = nodeTemplates.value.find(t => t.type.toLowerCase() === typeName.toLowerCase());
|
||||
if (template) return template;
|
||||
|
||||
// 大小写不敏感匹配 className
|
||||
template = nodeTemplates.value.find(t => t.className && t.className.toLowerCase() === typeName.toLowerCase());
|
||||
if (template) return template;
|
||||
|
||||
// 特殊映射处理
|
||||
const typeMapping: Record<string, string> = {
|
||||
'Sequence': 'sequence',
|
||||
'Selector': 'selector',
|
||||
'Parallel': 'parallel',
|
||||
'Inverter': 'inverter',
|
||||
'Repeater': 'repeater',
|
||||
'AlwaysSucceed': 'always-succeed',
|
||||
'AlwaysFail': 'always-fail',
|
||||
'UntilSuccess': 'until-success',
|
||||
'UntilFail': 'until-fail',
|
||||
'ExecuteAction': 'execute-action',
|
||||
'LogAction': 'log-action',
|
||||
'WaitAction': 'wait-action'
|
||||
};
|
||||
|
||||
const mappedType = typeMapping[typeName];
|
||||
if (mappedType) {
|
||||
template = nodeTemplates.value.find(t => t.type === mappedType);
|
||||
if (template) return template;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// 生成唯一节点ID
|
||||
const generateNodeId = (): string => {
|
||||
return 'node_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
|
||||
@@ -291,7 +291,7 @@ export function useConnectionManager(
|
||||
return getPortInfo(elementAtPoint as HTMLElement);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`[ConnectionManager] elementFromPoint 查询出错:`, error);
|
||||
// 查询出错时静默处理
|
||||
}
|
||||
|
||||
const allPorts = canvasAreaRef.value.querySelectorAll('.port');
|
||||
|
||||
@@ -1,26 +1,44 @@
|
||||
import { Ref, ref, watch } from 'vue';
|
||||
import { TreeNode, Connection } from '../types';
|
||||
|
||||
/**
|
||||
* 文件操作管理
|
||||
*/
|
||||
export function useFileOperations(
|
||||
treeNodes: Ref<TreeNode[]>,
|
||||
selectedNodeId: Ref<string | null>,
|
||||
connections: Ref<Connection[]>,
|
||||
tempConnection: Ref<{ path: string }>,
|
||||
showExportModal: Ref<boolean>,
|
||||
interface FileOperationOptions {
|
||||
treeNodes: Ref<TreeNode[]>;
|
||||
selectedNodeId: Ref<string | null>;
|
||||
connections: Ref<Connection[]>;
|
||||
tempConnection: Ref<{ path: string }>;
|
||||
showExportModal: Ref<boolean>;
|
||||
codeGeneration?: {
|
||||
createTreeFromConfig: (config: any) => TreeNode[];
|
||||
},
|
||||
updateConnections?: () => void
|
||||
) {
|
||||
// 跟踪未保存状态
|
||||
};
|
||||
updateConnections?: () => void;
|
||||
}
|
||||
|
||||
interface FileData {
|
||||
nodes: TreeNode[];
|
||||
connections: Connection[];
|
||||
metadata: {
|
||||
name: string;
|
||||
created: string;
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function useFileOperations(options: FileOperationOptions) {
|
||||
const {
|
||||
treeNodes,
|
||||
selectedNodeId,
|
||||
connections,
|
||||
tempConnection,
|
||||
showExportModal,
|
||||
codeGeneration,
|
||||
updateConnections
|
||||
} = options;
|
||||
|
||||
const hasUnsavedChanges = ref(false);
|
||||
const lastSavedState = ref<string>('');
|
||||
const currentFileName = ref('');
|
||||
|
||||
// 监听树结构变化来更新未保存状态
|
||||
const currentFilePath = ref('');
|
||||
|
||||
const updateUnsavedStatus = () => {
|
||||
const currentState = JSON.stringify({
|
||||
nodes: treeNodes.value,
|
||||
@@ -28,11 +46,9 @@ export function useFileOperations(
|
||||
});
|
||||
hasUnsavedChanges.value = currentState !== lastSavedState.value;
|
||||
};
|
||||
|
||||
// 监听变化
|
||||
|
||||
watch([treeNodes, connections], updateUnsavedStatus, { deep: true });
|
||||
|
||||
// 标记为已保存
|
||||
|
||||
const markAsSaved = () => {
|
||||
const currentState = JSON.stringify({
|
||||
nodes: treeNodes.value,
|
||||
@@ -41,38 +57,19 @@ export function useFileOperations(
|
||||
lastSavedState.value = currentState;
|
||||
hasUnsavedChanges.value = false;
|
||||
};
|
||||
|
||||
// 检查是否需要保存的通用方法
|
||||
const checkUnsavedChanges = (): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
if (!hasUnsavedChanges.value) {
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = confirm(
|
||||
'当前行为树有未保存的更改,是否要保存?\n\n' +
|
||||
'点击"确定"保存更改\n' +
|
||||
'点击"取消"丢弃更改\n' +
|
||||
'点击"X"取消操作'
|
||||
);
|
||||
|
||||
if (result) {
|
||||
// 用户选择保存
|
||||
saveBehaviorTree().then(() => {
|
||||
resolve(true);
|
||||
}).catch(() => {
|
||||
resolve(false);
|
||||
});
|
||||
} else {
|
||||
// 用户选择丢弃更改
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
|
||||
const setCurrentFile = (fileName: string, filePath: string = '') => {
|
||||
currentFileName.value = fileName;
|
||||
currentFilePath.value = filePath;
|
||||
markAsSaved();
|
||||
};
|
||||
|
||||
// 导出行为树数据
|
||||
const exportBehaviorTreeData = () => {
|
||||
|
||||
const clearCurrentFile = () => {
|
||||
currentFileName.value = '';
|
||||
currentFilePath.value = '';
|
||||
};
|
||||
|
||||
const exportBehaviorTreeData = (): FileData => {
|
||||
return {
|
||||
nodes: treeNodes.value,
|
||||
connections: connections.value,
|
||||
@@ -83,97 +80,177 @@ export function useFileOperations(
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// 工具栏操作
|
||||
|
||||
const showMessage = (message: string, type: 'success' | 'error' = 'success') => {
|
||||
const toast = document.createElement('div');
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 12px 20px;
|
||||
background: ${type === 'success' ? '#4caf50' : '#f44336'};
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
z-index: 10001;
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
transition: all 0.3s ease;
|
||||
`;
|
||||
toast.textContent = message;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '1';
|
||||
toast.style.transform = 'translateX(0)';
|
||||
}, 10);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '0';
|
||||
toast.style.transform = 'translateX(100%)';
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(toast)) {
|
||||
document.body.removeChild(toast);
|
||||
}
|
||||
}, 300);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const sendToMain = (message: string, data: any): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
Editor.Message.request('cocos-ecs-extension', message, data)
|
||||
.then((result) => {
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const checkUnsavedChanges = (): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
if (!hasUnsavedChanges.value) {
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = confirm(
|
||||
'当前行为树有未保存的更改,是否要保存?\n\n' +
|
||||
'点击"确定"保存更改\n' +
|
||||
'点击"取消"丢弃更改'
|
||||
);
|
||||
|
||||
if (result) {
|
||||
saveBehaviorTree().then(() => {
|
||||
resolve(true);
|
||||
}).catch(() => {
|
||||
resolve(false);
|
||||
});
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const newBehaviorTree = async () => {
|
||||
const canProceed = await checkUnsavedChanges();
|
||||
if (canProceed) {
|
||||
treeNodes.value = [];
|
||||
selectedNodeId.value = null;
|
||||
connections.value = [];
|
||||
tempConnection.value.path = '';
|
||||
currentFileName.value = '';
|
||||
markAsSaved(); // 新建后标记为已保存状态
|
||||
treeNodes.value = [];
|
||||
selectedNodeId.value = null;
|
||||
connections.value = [];
|
||||
tempConnection.value.path = '';
|
||||
clearCurrentFile();
|
||||
markAsSaved();
|
||||
}
|
||||
};
|
||||
|
||||
// 保存行为树
|
||||
const saveBehaviorTree = async (): Promise<boolean> => {
|
||||
console.log('=== 开始保存行为树 ===');
|
||||
if (currentFilePath.value) {
|
||||
return await saveToCurrentFile();
|
||||
} else {
|
||||
return await saveAsBehaviorTree();
|
||||
}
|
||||
};
|
||||
|
||||
const saveToCurrentFile = async (): Promise<boolean> => {
|
||||
if (!currentFilePath.value) {
|
||||
return await saveAsBehaviorTree();
|
||||
}
|
||||
|
||||
try {
|
||||
const data = exportBehaviorTreeData();
|
||||
const jsonString = JSON.stringify(data, null, 2);
|
||||
console.log('数据准备完成,JSON长度:', jsonString.length);
|
||||
|
||||
// 使用 HTML input 替代 prompt(因为 prompt 在 Cocos Creator 扩展中不支持)
|
||||
const fileName = await getFileNameFromUser();
|
||||
if (!fileName) {
|
||||
console.log('❌ 用户取消了保存操作');
|
||||
return false;
|
||||
}
|
||||
await sendToMain('overwrite-behavior-tree-file', {
|
||||
filePath: currentFilePath.value,
|
||||
content: jsonString
|
||||
});
|
||||
|
||||
console.log('✓ 用户输入文件名:', fileName);
|
||||
|
||||
// 检测是否在Cocos Creator环境中
|
||||
if (typeof Editor !== 'undefined' && typeof (window as any).sendToMain === 'function') {
|
||||
console.log('✓ 使用Cocos Creator保存方式');
|
||||
|
||||
try {
|
||||
(window as any).sendToMain('create-behavior-tree-from-editor', {
|
||||
fileName: fileName + '.json',
|
||||
content: jsonString,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
console.log('✓ 保存消息已发送到主进程');
|
||||
|
||||
// 更新当前文件名并标记为已保存
|
||||
currentFileName.value = fileName;
|
||||
markAsSaved();
|
||||
|
||||
// 用户反馈
|
||||
showMessage(`保存成功!文件名: ${fileName}.json`, 'success');
|
||||
|
||||
console.log('✅ 保存操作完成');
|
||||
return true;
|
||||
} catch (sendError) {
|
||||
console.error('❌ 发送消息时出错:', sendError);
|
||||
showMessage('保存失败: ' + sendError, 'error');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
console.log('✓ 使用浏览器下载保存方式');
|
||||
|
||||
// 在浏览器环境中使用下载方式
|
||||
const blob = new Blob([jsonString], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${fileName}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
// 标记为已保存
|
||||
currentFileName.value = fileName;
|
||||
markAsSaved();
|
||||
|
||||
console.log('✅ 文件下载保存成功');
|
||||
return true;
|
||||
}
|
||||
markAsSaved();
|
||||
showMessage('保存成功!');
|
||||
return true;
|
||||
} catch (error) {
|
||||
showMessage('保存失败: ' + error, 'error');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveAsBehaviorTree = async (): Promise<boolean> => {
|
||||
try {
|
||||
const data = exportBehaviorTreeData();
|
||||
const jsonString = JSON.stringify(data, null, 2);
|
||||
|
||||
const result = await Editor.Dialog.save({
|
||||
title: '保存行为树文件',
|
||||
filters: [
|
||||
{ name: '行为树文件', extensions: ['bt.json', 'json'] },
|
||||
{ name: '所有文件', extensions: ['*'] }
|
||||
]
|
||||
});
|
||||
|
||||
if (result.canceled || !result.filePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fs = require('fs-extra');
|
||||
await fs.writeFile(result.filePath, jsonString);
|
||||
|
||||
const path = require('path');
|
||||
const fileName = path.basename(result.filePath, path.extname(result.filePath));
|
||||
setCurrentFile(fileName, result.filePath);
|
||||
showMessage(`保存成功!文件: ${result.filePath}`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
showMessage('另存为失败: ' + error, 'error');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveToFile = async (fileName: string, jsonString: string): Promise<boolean> => {
|
||||
try {
|
||||
await sendToMain('create-behavior-tree-from-editor', {
|
||||
fileName: fileName + '.json',
|
||||
content: jsonString
|
||||
});
|
||||
|
||||
setCurrentFile(fileName, `assets/${fileName}.bt.json`);
|
||||
showMessage(`保存成功!文件名: ${fileName}.json`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ 保存过程中发生错误:', error);
|
||||
showMessage('保存失败: ' + error, 'error');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 使用 HTML input 获取文件名(替代 prompt)
|
||||
const getFileNameFromUser = (): Promise<string | null> => {
|
||||
return new Promise((resolve) => {
|
||||
// 创建模态对话框
|
||||
const overlay = document.createElement('div');
|
||||
overlay.style.cssText = `
|
||||
position: fixed;
|
||||
@@ -216,11 +293,9 @@ export function useFileOperations(
|
||||
const saveBtn = dialog.querySelector('#save-btn') as HTMLButtonElement;
|
||||
const cancelBtn = dialog.querySelector('#cancel-btn') as HTMLButtonElement;
|
||||
|
||||
// 聚焦并选中文本
|
||||
input.focus();
|
||||
input.select();
|
||||
|
||||
// 事件处理
|
||||
const cleanup = () => {
|
||||
document.body.removeChild(overlay);
|
||||
};
|
||||
@@ -236,7 +311,6 @@ export function useFileOperations(
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
// 回车键保存
|
||||
input.onkeydown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const fileName = input.value.trim();
|
||||
@@ -250,131 +324,108 @@ export function useFileOperations(
|
||||
});
|
||||
};
|
||||
|
||||
// 显示消息提示
|
||||
const showMessage = (message: string, type: 'success' | 'error' = 'success') => {
|
||||
const toast = document.createElement('div');
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 12px 20px;
|
||||
background: ${type === 'success' ? '#4caf50' : '#f44336'};
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
z-index: 10001;
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
transition: all 0.3s ease;
|
||||
`;
|
||||
toast.textContent = message;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// 动画显示
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '1';
|
||||
toast.style.transform = 'translateX(0)';
|
||||
}, 10);
|
||||
|
||||
// 3秒后自动消失
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '0';
|
||||
toast.style.transform = 'translateX(100%)';
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(toast)) {
|
||||
document.body.removeChild(toast);
|
||||
}
|
||||
}, 300);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
// 生成当前行为树的配置
|
||||
const generateCurrentConfig = () => {
|
||||
if (treeNodes.value.length === 0) return null;
|
||||
|
||||
const rootNode = treeNodes.value.find(node =>
|
||||
!treeNodes.value.some(otherNode =>
|
||||
otherNode.children?.includes(node.id)
|
||||
)
|
||||
);
|
||||
|
||||
if (!rootNode) return null;
|
||||
|
||||
return {
|
||||
version: "1.0.0",
|
||||
type: "behavior-tree",
|
||||
metadata: {
|
||||
createdAt: new Date().toISOString(),
|
||||
nodeCount: treeNodes.value.length
|
||||
},
|
||||
tree: generateNodeConfig(rootNode)
|
||||
};
|
||||
};
|
||||
|
||||
// 简化的节点配置生成(用于文件保存)
|
||||
const generateNodeConfig = (node: TreeNode): any => {
|
||||
const config: any = {
|
||||
id: node.id,
|
||||
type: node.type,
|
||||
namespace: getNodeNamespace(node.type),
|
||||
properties: {}
|
||||
};
|
||||
|
||||
// 处理节点属性
|
||||
if (node.properties) {
|
||||
Object.entries(node.properties).forEach(([key, prop]) => {
|
||||
if (prop.value !== undefined && prop.value !== '') {
|
||||
config.properties[key] = {
|
||||
type: prop.type,
|
||||
value: prop.value
|
||||
const loadFileContent = (fileData: any, filePath: string = '') => {
|
||||
try {
|
||||
if (!fileData) {
|
||||
return;
|
||||
}
|
||||
|
||||
let parsedData = fileData;
|
||||
|
||||
if (fileData.rawContent) {
|
||||
try {
|
||||
parsedData = JSON.parse(fileData.rawContent);
|
||||
} catch (e) {
|
||||
parsedData = {
|
||||
nodes: [],
|
||||
connections: []
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (parsedData.nodes && Array.isArray(parsedData.nodes)) {
|
||||
treeNodes.value = parsedData.nodes.map((node: any) => ({
|
||||
...node,
|
||||
x: node.x || 0,
|
||||
y: node.y || 0,
|
||||
children: node.children || [],
|
||||
properties: node.properties || {},
|
||||
canHaveChildren: node.canHaveChildren !== false,
|
||||
canHaveParent: node.canHaveParent !== false,
|
||||
hasError: node.hasError || false
|
||||
}));
|
||||
} else if (parsedData.tree) {
|
||||
const treeNode = parsedData.tree;
|
||||
const nodes = [treeNode];
|
||||
|
||||
const extractNodes = (node: any): any[] => {
|
||||
const allNodes = [node];
|
||||
if (node.children && Array.isArray(node.children)) {
|
||||
node.children.forEach((child: any) => {
|
||||
if (typeof child === 'object') {
|
||||
allNodes.push(...extractNodes(child));
|
||||
}
|
||||
});
|
||||
}
|
||||
return allNodes;
|
||||
};
|
||||
|
||||
const allNodes = extractNodes(treeNode);
|
||||
treeNodes.value = allNodes.map((node: any, index: number) => ({
|
||||
...node,
|
||||
x: node.x || (300 + index * 150),
|
||||
y: node.y || (100 + Math.floor(index / 3) * 200),
|
||||
children: Array.isArray(node.children)
|
||||
? node.children.filter((child: any) => typeof child === 'string')
|
||||
: [],
|
||||
properties: node.properties || {},
|
||||
canHaveChildren: true,
|
||||
canHaveParent: node.id !== 'root',
|
||||
hasError: false
|
||||
}));
|
||||
} else {
|
||||
treeNodes.value = [];
|
||||
}
|
||||
|
||||
if (parsedData.connections && Array.isArray(parsedData.connections)) {
|
||||
connections.value = parsedData.connections.map((conn: any) => ({
|
||||
id: conn.id || Math.random().toString(36).substr(2, 9),
|
||||
sourceId: conn.sourceId,
|
||||
targetId: conn.targetId,
|
||||
path: conn.path || '',
|
||||
active: conn.active || false
|
||||
}));
|
||||
} else {
|
||||
connections.value = [];
|
||||
}
|
||||
|
||||
if (fileData._fileInfo) {
|
||||
const fileName = fileData._fileInfo.fileName || 'untitled';
|
||||
const fullPath = fileData._fileInfo.filePath || filePath;
|
||||
setCurrentFile(fileName, fullPath);
|
||||
} else if (parsedData.metadata?.name) {
|
||||
setCurrentFile(parsedData.metadata.name, filePath);
|
||||
} else {
|
||||
setCurrentFile('untitled', filePath);
|
||||
}
|
||||
|
||||
selectedNodeId.value = null;
|
||||
tempConnection.value.path = '';
|
||||
|
||||
if (updateConnections) {
|
||||
setTimeout(() => {
|
||||
updateConnections();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('文件加载失败:', error);
|
||||
showMessage('文件加载失败: ' + error, 'error');
|
||||
treeNodes.value = [];
|
||||
connections.value = [];
|
||||
selectedNodeId.value = null;
|
||||
setCurrentFile('untitled', '');
|
||||
}
|
||||
|
||||
// 处理子节点
|
||||
if (node.children && node.children.length > 0) {
|
||||
config.children = node.children
|
||||
.map(childId => treeNodes.value.find(n => n.id === childId))
|
||||
.filter(Boolean)
|
||||
.map(child => generateNodeConfig(child!));
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
// 获取节点命名空间
|
||||
const getNodeNamespace = (nodeType: string): string => {
|
||||
// ECS节点
|
||||
if (['has-component', 'add-component', 'remove-component', 'modify-component',
|
||||
'has-tag', 'is-active', 'wait-time', 'destroy-entity'].includes(nodeType)) {
|
||||
return 'ecs-integration/behaviors';
|
||||
}
|
||||
|
||||
// 复合节点
|
||||
if (['sequence', 'selector', 'parallel', 'parallel-selector',
|
||||
'random-selector', 'random-sequence'].includes(nodeType)) {
|
||||
return 'behaviourTree/composites';
|
||||
}
|
||||
|
||||
// 装饰器
|
||||
if (['repeater', 'inverter', 'always-fail', 'always-succeed',
|
||||
'until-fail', 'until-success'].includes(nodeType)) {
|
||||
return 'behaviourTree/decorators';
|
||||
}
|
||||
|
||||
// 动作节点
|
||||
if (['execute-action', 'log-action', 'wait-action'].includes(nodeType)) {
|
||||
return 'behaviourTree/actions';
|
||||
}
|
||||
|
||||
// 条件节点
|
||||
if (['execute-conditional'].includes(nodeType)) {
|
||||
return 'behaviourTree/conditionals';
|
||||
}
|
||||
|
||||
return 'behaviourTree';
|
||||
};
|
||||
|
||||
const loadBehaviorTree = async () => {
|
||||
@@ -397,20 +448,34 @@ export function useFileOperations(
|
||||
const newNodes = codeGeneration.createTreeFromConfig(config);
|
||||
treeNodes.value = newNodes;
|
||||
selectedNodeId.value = null;
|
||||
connections.value = [];
|
||||
tempConnection.value.path = '';
|
||||
markAsSaved(); // 加载后标记为已保存状态
|
||||
console.log('行为树配置加载成功');
|
||||
if (updateConnections) {
|
||||
updateConnections();
|
||||
|
||||
if (config.connections && Array.isArray(config.connections)) {
|
||||
connections.value = config.connections.map((conn: any) => ({
|
||||
id: conn.id,
|
||||
sourceId: conn.sourceId,
|
||||
targetId: conn.targetId,
|
||||
path: conn.path || '',
|
||||
active: conn.active || false
|
||||
}));
|
||||
} else {
|
||||
connections.value = [];
|
||||
}
|
||||
|
||||
tempConnection.value.path = '';
|
||||
|
||||
const fileName = file.name.replace(/\.(json|bt)$/, '');
|
||||
setCurrentFile(fileName, '');
|
||||
|
||||
setTimeout(() => {
|
||||
if (updateConnections) {
|
||||
updateConnections();
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
console.error('代码生成器未初始化');
|
||||
alert('代码生成器未初始化');
|
||||
showMessage('代码生成器未初始化', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载行为树配置失败:', error);
|
||||
alert('配置文件格式错误');
|
||||
showMessage('配置文件格式错误', 'error');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
@@ -423,37 +488,18 @@ export function useFileOperations(
|
||||
showExportModal.value = true;
|
||||
};
|
||||
|
||||
const copyToClipboard = () => {
|
||||
// TODO: 实现复制到剪贴板功能
|
||||
console.log('复制到剪贴板');
|
||||
};
|
||||
|
||||
const saveToFile = () => {
|
||||
// TODO: 实现保存到文件功能
|
||||
console.log('保存到文件');
|
||||
};
|
||||
|
||||
// 验证相关
|
||||
const autoLayout = () => {
|
||||
// TODO: 实现自动布局功能
|
||||
console.log('自动布局');
|
||||
};
|
||||
|
||||
const validateTree = () => {
|
||||
// TODO: 实现树验证功能
|
||||
console.log('验证树结构');
|
||||
};
|
||||
|
||||
return {
|
||||
newBehaviorTree,
|
||||
saveBehaviorTree,
|
||||
saveAsBehaviorTree,
|
||||
loadBehaviorTree,
|
||||
loadFileContent,
|
||||
exportConfig,
|
||||
copyToClipboard,
|
||||
saveToFile,
|
||||
autoLayout,
|
||||
validateTree,
|
||||
hasUnsavedChanges,
|
||||
markAsSaved
|
||||
markAsSaved,
|
||||
setCurrentFile,
|
||||
clearCurrentFile,
|
||||
currentFileName,
|
||||
currentFilePath
|
||||
};
|
||||
}
|
||||
@@ -19,7 +19,6 @@ export function useInstallation(
|
||||
isInstalled.value = result.installed;
|
||||
version.value = result.version;
|
||||
} catch (error) {
|
||||
console.error('检查安装状态失败:', error);
|
||||
isInstalled.value = false;
|
||||
version.value = null;
|
||||
} finally {
|
||||
@@ -34,7 +33,7 @@ export function useInstallation(
|
||||
await installBehaviorTreeAI(Editor.Project.path);
|
||||
await checkInstallStatus();
|
||||
} catch (error) {
|
||||
console.error('安装失败:', error);
|
||||
// 安装失败时静默处理
|
||||
} finally {
|
||||
isInstalling.value = false;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export function useNodeOperations(
|
||||
selectedNodeId.value = newNode.id;
|
||||
|
||||
} catch (error) {
|
||||
console.error('节点创建失败:', error);
|
||||
// 节点创建失败时静默处理
|
||||
}
|
||||
};
|
||||
|
||||
@@ -123,44 +123,18 @@ export function useNodeOperations(
|
||||
|
||||
// 节点属性更新
|
||||
const updateNodeProperty = (path: string, value: any) => {
|
||||
console.log('updateNodeProperty called:', path, value);
|
||||
const node = selectedNodeId.value ? getNodeByIdLocal(selectedNodeId.value) : null;
|
||||
if (!node) {
|
||||
console.log('No selected node found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Current node before update:', JSON.stringify(node, null, 2));
|
||||
if (!node) return;
|
||||
|
||||
// 使用通用方法更新属性
|
||||
setNestedProperty(node, path, value);
|
||||
|
||||
console.log(`Updated property ${path} to:`, value);
|
||||
console.log('Updated node after change:', JSON.stringify(node, null, 2));
|
||||
|
||||
// 强制触发响应式更新 - 创建新数组来强制Vue检测变化
|
||||
// 强制触发响应式更新
|
||||
const nodeIndex = treeNodes.value.findIndex(n => n.id === node.id);
|
||||
if (nodeIndex > -1) {
|
||||
// 创建新的节点数组,确保Vue能检测到变化
|
||||
const newNodes = [...treeNodes.value];
|
||||
newNodes[nodeIndex] = { ...node }; // 创建节点副本确保响应式更新
|
||||
newNodes[nodeIndex] = { ...node };
|
||||
treeNodes.value = newNodes;
|
||||
|
||||
console.log('Triggered reactive update - replaced array');
|
||||
|
||||
// 验证更新是否成功
|
||||
nextTick(() => {
|
||||
const verifyNode = treeNodes.value.find(n => n.id === node.id);
|
||||
console.log('Verification - node after update:', JSON.stringify(verifyNode, null, 2));
|
||||
|
||||
// 验证属性值
|
||||
const pathParts = path.split('.');
|
||||
let checkValue: any = verifyNode;
|
||||
for (const part of pathParts) {
|
||||
checkValue = checkValue?.[part];
|
||||
}
|
||||
console.log(`Verification - final value at ${path}:`, checkValue);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,75 +2,189 @@ import { readFileSync } from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import { createApp, App, defineComponent } from 'vue';
|
||||
import { useBehaviorTreeEditor } from './composables/useBehaviorTreeEditor';
|
||||
import { EventManager } from './utils/EventManager';
|
||||
|
||||
const panelDataMap = new WeakMap<any, App>();
|
||||
// Vue应用实例
|
||||
let panelDataMap = new WeakMap<any, any>();
|
||||
|
||||
module.exports = Editor.Panel.define({
|
||||
listeners: {
|
||||
show() { },
|
||||
hide() { },
|
||||
},
|
||||
// 待处理的文件队列
|
||||
let pendingFileData: any = null;
|
||||
// Vue应用是否已挂载完成
|
||||
let vueAppMounted: boolean = false;
|
||||
// 存储面板实例,用于访问面板的DOM元素
|
||||
let currentPanelInstance: any = null;
|
||||
|
||||
/**
|
||||
* 面板定义
|
||||
*/
|
||||
const panelDefinition = {
|
||||
/**
|
||||
* 面板模板
|
||||
*/
|
||||
template: readFileSync(join(__dirname, '../../../static/template/behavior-tree/index.html'), 'utf-8'),
|
||||
|
||||
/**
|
||||
* 面板样式
|
||||
*/
|
||||
style: readFileSync(join(__dirname, '../../../static/style/behavior-tree/index.css'), 'utf-8'),
|
||||
|
||||
/**
|
||||
* 选择器
|
||||
*/
|
||||
$: {
|
||||
app: '#behavior-tree-app',
|
||||
},
|
||||
|
||||
/**
|
||||
* 面板方法 - 用于处理来自扩展主进程的消息
|
||||
*/
|
||||
methods: {
|
||||
sendToMain(message: string, ...args: any[]) {
|
||||
Editor.Message.send('cocos-ecs-extension', message, ...args);
|
||||
/**
|
||||
* 加载行为树文件
|
||||
*/
|
||||
async loadBehaviorTreeFile(assetInfo: any) {
|
||||
try {
|
||||
const filePath = assetInfo?.file || assetInfo?.path;
|
||||
if (!filePath) {
|
||||
throw new Error('无法获取文件路径');
|
||||
}
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`文件不存在: ${filePath}`);
|
||||
}
|
||||
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
let fileContent: any;
|
||||
|
||||
try {
|
||||
fileContent = JSON.parse(content);
|
||||
} catch (parseError) {
|
||||
fileContent = {
|
||||
version: "1.0.0",
|
||||
type: "behavior-tree",
|
||||
tree: { id: "root", type: "sequence", children: [] }
|
||||
};
|
||||
}
|
||||
|
||||
const fileInfo = {
|
||||
...fileContent,
|
||||
_fileInfo: {
|
||||
fileName: path.basename(filePath, path.extname(filePath)),
|
||||
filePath: filePath
|
||||
}
|
||||
};
|
||||
|
||||
const notifyVueComponent = () => {
|
||||
const appContainer = currentPanelInstance?.$.app;
|
||||
|
||||
if (appContainer && vueAppMounted) {
|
||||
if (typeof (appContainer as any).loadFileContent === 'function') {
|
||||
(appContainer as any).loadFileContent(fileInfo);
|
||||
} else {
|
||||
const event = new CustomEvent('load-behavior-tree-file', { detail: fileInfo });
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
} else {
|
||||
pendingFileData = fileInfo;
|
||||
}
|
||||
};
|
||||
|
||||
notifyVueComponent();
|
||||
|
||||
if (pendingFileData) {
|
||||
setTimeout(() => {
|
||||
if (pendingFileData) {
|
||||
notifyVueComponent();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
return { success: true, message: '文件加载成功' };
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const event = new CustomEvent('file-load-error', { detail: { error: errorMessage } });
|
||||
document.dispatchEvent(event);
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 面板准备完成时调用
|
||||
*/
|
||||
ready() {
|
||||
currentPanelInstance = this;
|
||||
|
||||
loadBehaviorTreeFile(fileData: any) {
|
||||
console.log('Loading behavior tree file:', fileData);
|
||||
|
||||
// 通知编辑器组件加载文件
|
||||
if (this.$.app) {
|
||||
const event = new CustomEvent('load-behavior-tree-file', {
|
||||
detail: fileData
|
||||
if (this.$.app) {
|
||||
try {
|
||||
const BehaviorTreeEditor = defineComponent({
|
||||
setup() {
|
||||
const editor = useBehaviorTreeEditor();
|
||||
return editor;
|
||||
},
|
||||
template: readFileSync(join(__dirname, '../../../static/template/behavior-tree/BehaviorTreeEditor.html'), 'utf-8')
|
||||
});
|
||||
this.$.app.dispatchEvent(event);
|
||||
|
||||
const app = createApp(BehaviorTreeEditor);
|
||||
|
||||
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('ui-');
|
||||
|
||||
app.config.errorHandler = (err, instance, info) => {
|
||||
console.error('[BehaviorTreePanel] Vue错误:', err, info);
|
||||
};
|
||||
|
||||
app.component('tree-node-item', defineComponent({
|
||||
props: ['node', 'level', 'getNodeByIdLocal'],
|
||||
emits: ['node-select'],
|
||||
template: `
|
||||
<div class="tree-node-item"
|
||||
:class="'level-' + level"
|
||||
@click="$emit('node-select', node)">
|
||||
<span class="node-icon">{{ node.icon || '●' }}</span>
|
||||
<span class="node-name">{{ node.name || node.type }}</span>
|
||||
<span class="node-type">{{ node.type }}</span>
|
||||
</div>
|
||||
`
|
||||
}));
|
||||
|
||||
app.mount(this.$.app);
|
||||
panelDataMap.set(this, app);
|
||||
vueAppMounted = true;
|
||||
|
||||
if (pendingFileData) {
|
||||
const event = new CustomEvent('load-behavior-tree-file', { detail: pendingFileData });
|
||||
document.dispatchEvent(event);
|
||||
pendingFileData = null;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[BehaviorTreePanel] 初始化失败:', error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
ready() {
|
||||
if (this.$.app) {
|
||||
const app = createApp({});
|
||||
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('ui-');
|
||||
|
||||
// 暴露发送消息到主进程的方法
|
||||
(window as any).sendToMain = this.sendToMain.bind(this);
|
||||
|
||||
// 树节点组件
|
||||
app.component('tree-node-item', defineComponent({
|
||||
props: ['node', 'level', 'getNodeByIdLocal'],
|
||||
emits: ['node-select'],
|
||||
template: readFileSync(join(__dirname, '../../../static/template/behavior-tree/TreeNodeItem.html'), 'utf-8')
|
||||
}));
|
||||
|
||||
// 行为树编辑器组件
|
||||
app.component('BehaviorTreeEditor', defineComponent({
|
||||
setup() {
|
||||
const editor = useBehaviorTreeEditor();
|
||||
return editor;
|
||||
},
|
||||
template: readFileSync(join(__dirname, '../../../static/template/behavior-tree/BehaviorTreeEditor.html'), 'utf-8')
|
||||
}));
|
||||
|
||||
app.mount(this.$.app);
|
||||
panelDataMap.set(this, app);
|
||||
}
|
||||
},
|
||||
|
||||
beforeClose() { },
|
||||
|
||||
/**
|
||||
* 面板关闭时调用
|
||||
*/
|
||||
close() {
|
||||
const app = panelDataMap.get(this);
|
||||
if (app) {
|
||||
app.unmount();
|
||||
try {
|
||||
const app = panelDataMap.get(this);
|
||||
if (app) {
|
||||
app.unmount();
|
||||
panelDataMap.delete(this);
|
||||
}
|
||||
|
||||
EventManager.getInstance().cleanup();
|
||||
|
||||
} catch (error) {
|
||||
console.error('[BehaviorTreePanel] 清理资源时发生错误:', error);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 导出面板定义 - 使用Editor.Panel.define()包装
|
||||
module.exports = Editor.Panel.define(panelDefinition);
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 事件管理器 - 统一处理面板的事件通信
|
||||
*/
|
||||
export class EventManager {
|
||||
private static instance: EventManager;
|
||||
private eventListeners: Map<string, EventListener[]> = new Map();
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): EventManager {
|
||||
if (!EventManager.instance) {
|
||||
EventManager.instance = new EventManager();
|
||||
}
|
||||
return EventManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加事件监听器
|
||||
*/
|
||||
addEventListener(eventType: string, listener: EventListener): void {
|
||||
if (!this.eventListeners.has(eventType)) {
|
||||
this.eventListeners.set(eventType, []);
|
||||
}
|
||||
|
||||
const listeners = this.eventListeners.get(eventType)!;
|
||||
listeners.push(listener);
|
||||
|
||||
// 添加到DOM
|
||||
document.addEventListener(eventType, listener);
|
||||
|
||||
console.log(`[EventManager] 添加事件监听器: ${eventType}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件监听器
|
||||
*/
|
||||
removeEventListener(eventType: string, listener: EventListener): void {
|
||||
const listeners = this.eventListeners.get(eventType);
|
||||
if (listeners) {
|
||||
const index = listeners.indexOf(listener);
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1);
|
||||
document.removeEventListener(eventType, listener);
|
||||
console.log(`[EventManager] 移除事件监听器: ${eventType}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除特定类型的所有监听器
|
||||
*/
|
||||
removeAllListeners(eventType: string): void {
|
||||
const listeners = this.eventListeners.get(eventType);
|
||||
if (listeners) {
|
||||
listeners.forEach(listener => {
|
||||
document.removeEventListener(eventType, listener);
|
||||
});
|
||||
this.eventListeners.delete(eventType);
|
||||
console.log(`[EventManager] 移除所有 ${eventType} 事件监听器`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有事件监听器
|
||||
*/
|
||||
cleanup(): void {
|
||||
this.eventListeners.forEach((listeners, eventType) => {
|
||||
listeners.forEach(listener => {
|
||||
document.removeEventListener(eventType, listener);
|
||||
});
|
||||
});
|
||||
this.eventListeners.clear();
|
||||
console.log('[EventManager] 清理所有事件监听器');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息到主进程
|
||||
*/
|
||||
static sendToMain(message: string, ...args: any[]): void {
|
||||
try {
|
||||
if (typeof (window as any).sendToMain === 'function') {
|
||||
(window as any).sendToMain(message, ...args);
|
||||
console.log(`[EventManager] 发送消息到主进程: ${message}`, args);
|
||||
} else {
|
||||
console.error('[EventManager] sendToMain 方法不可用');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[EventManager] 发送消息失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发自定义事件
|
||||
*/
|
||||
static dispatch(eventType: string, detail?: any): void {
|
||||
try {
|
||||
const event = new CustomEvent(eventType, { detail });
|
||||
document.dispatchEvent(event);
|
||||
console.log(`[EventManager] 触发事件: ${eventType}`, detail);
|
||||
} catch (error) {
|
||||
console.error(`[EventManager] 触发事件失败: ${eventType}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,6 @@ export async function checkBehaviorTreeInstalled(projectPath: string): Promise<I
|
||||
packageExists: fs.existsSync(path.join(projectPath, 'package.json'))
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('检查行为树安装状态失败:', error);
|
||||
return {
|
||||
installed: false,
|
||||
version: null,
|
||||
@@ -81,9 +80,8 @@ export async function installBehaviorTreeAI(projectPath: string): Promise<void>
|
||||
throw new Error('安装请求失败,未收到主进程响应');
|
||||
}
|
||||
|
||||
console.log('行为树AI系统安装完成');
|
||||
// 安装完成
|
||||
} catch (error) {
|
||||
console.error('行为树AI系统安装失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -102,9 +100,8 @@ export async function updateBehaviorTreeAI(projectPath: string): Promise<void> {
|
||||
throw new Error('更新请求失败,未收到主进程响应');
|
||||
}
|
||||
|
||||
console.log('行为树AI系统更新完成');
|
||||
// 更新完成
|
||||
} catch (error) {
|
||||
console.error('行为树AI系统更新失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,13 @@
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.current-file {
|
||||
color: #a0aec0;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.unsaved-indicator {
|
||||
color: #ff6b6b;
|
||||
animation: pulse-unsaved 2s infinite;
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
<!-- 头部工具栏 -->
|
||||
<div class="header-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<h2>🌳 行为树可视化编辑器 <span v-if="hasUnsavedChanges" class="unsaved-indicator">●</span></h2>
|
||||
<h2>🌳 行为树可视化编辑器
|
||||
<span v-if="currentFileName" class="current-file">- {{ currentFileName }}</span>
|
||||
<span v-if="hasUnsavedChanges" class="unsaved-indicator">●</span>
|
||||
</h2>
|
||||
<div class="toolbar-buttons">
|
||||
<button class="tool-btn" @click="newBehaviorTree" title="新建行为树">
|
||||
<span>📄</span> 新建
|
||||
</button>
|
||||
<button class="tool-btn" :class="{ 'has-changes': hasUnsavedChanges }" @click="saveBehaviorTree" title="保存行为树">
|
||||
<button class="tool-btn" :class="{ 'has-changes': hasUnsavedChanges }" @click="saveBehaviorTree" :title="currentFilePath ? '保存到: ' + currentFilePath : (currentFileName ? '另存为 ' + currentFileName : '保存行为树')">
|
||||
<span>💾</span> 保存{{ hasUnsavedChanges ? ' *' : '' }}
|
||||
</button>
|
||||
<button class="tool-btn" @click="saveAsBehaviorTree" title="另存为新文件">
|
||||
<span>💾</span> 另存为
|
||||
</button>
|
||||
<button class="tool-btn" @click="loadBehaviorTree" title="加载行为树">
|
||||
<span>📂</span> 加载
|
||||
</button>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"__version__": "1.0.6"
|
||||
"__version__": "1.0.6",
|
||||
"custom_joint_texture_layouts": []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user