新增cocos右键打开和保存行为树功能

This commit is contained in:
YHH
2025-06-18 15:20:07 +08:00
parent 06ea01e928
commit 96f651b7ca
23 changed files with 1789 additions and 1112 deletions

View File

@@ -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"
}
}

View File

@@ -1,11 +0,0 @@
{
"ver": "2.0.1",
"importer": "json",
"imported": true,
"uuid": "cb66452d-5cad-46a9-96f9-b62831e0edc3",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

View File

@@ -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"
}
};

View File

@@ -10,6 +10,9 @@ module.exports = {
// 菜单相关
menu: {
panel: "面板"
panel: "面板",
develop: "开发",
create: "创建",
open: "打开"
}
};

View File

@@ -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"
]
}
}
}

View File

@@ -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;
}

View File

@@ -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)}`,
});
}
}
}

View File

@@ -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群号或访问相关链接加入讨论群。',
});
}
}
}

View File

@@ -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编辑器。`,
});
}
}
}

View File

@@ -0,0 +1,3 @@
export { EcsFrameworkHandler } from './EcsFrameworkHandler';
export { BehaviorTreeHandler } from './BehaviorTreeHandler';
export { PanelHandler } from './PanelHandler';

View File

@@ -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] 扩展已卸载');
}

View File

@@ -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
};
}

View File

@@ -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);

View File

@@ -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');

View File

@@ -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
};
}

View File

@@ -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;
}

View File

@@ -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);
});
}
};

View File

@@ -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);

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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>

View File

@@ -1,3 +1,4 @@
{
"__version__": "1.0.6"
"__version__": "1.0.6",
"custom_joint_texture_layouts": []
}