新增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: {
|
menu: {
|
||||||
panel: "面板"
|
panel: "面板",
|
||||||
|
develop: "开发",
|
||||||
|
create: "创建",
|
||||||
|
open: "打开"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -100,20 +100,12 @@
|
|||||||
"message": "open-panel"
|
"message": "open-panel"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"asset-menu": [
|
"assets": {
|
||||||
{
|
"menu": {
|
||||||
"path": "i18n:menu.create/ECS Framework",
|
"methods": "./dist/assets-menu.js",
|
||||||
"label": "创建行为树文件",
|
"assetMenu": "onAssetMenu"
|
||||||
"message": "create-behavior-tree-file",
|
|
||||||
"target": "folder"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "i18n:menu.open",
|
|
||||||
"label": "用行为树编辑器打开",
|
|
||||||
"message": "open-behavior-tree-file",
|
|
||||||
"target": [".bt.json", ".json"]
|
|
||||||
}
|
}
|
||||||
],
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"open-panel": {
|
"open-panel": {
|
||||||
"methods": [
|
"methods": [
|
||||||
@@ -195,15 +187,25 @@
|
|||||||
"create-behavior-tree-file"
|
"create-behavior-tree-file"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"open-behavior-tree-file": {
|
"load-behavior-tree-file": {
|
||||||
"methods": [
|
"methods": [
|
||||||
"open-behavior-tree-file"
|
"load-behavior-tree-file"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"create-behavior-tree-from-editor": {
|
"create-behavior-tree-from-editor": {
|
||||||
"methods": [
|
"methods": [
|
||||||
"create-behavior-tree-from-editor"
|
"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
|
// @ts-ignore
|
||||||
import packageJSON from '../package.json';
|
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 path from 'path';
|
||||||
import * as fs from 'fs';
|
import { AssetInfo } from '@cocos/creator-types/editor/packages/asset-db/@types/public';
|
||||||
import * as fsExtra from 'fs-extra';
|
|
||||||
import { readFileSync, outputFile } from 'fs-extra';
|
|
||||||
import { join } from 'path';
|
|
||||||
import { TemplateGenerator } from './TemplateGenerator';
|
|
||||||
import { CodeGenerator } from './CodeGenerator';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @en Registration method for the main process of Extension
|
* @en Registration method for the main process of Extension
|
||||||
* @zh 为扩展的主进程的注册方法
|
* @zh 为扩展的主进程的注册方法
|
||||||
*/
|
*/
|
||||||
export const methods: { [key: string]: (...any: any) => any } = {
|
export const methods: { [key: string]: (...any: any) => any } = {
|
||||||
|
// ================ 面板管理 ================
|
||||||
/**
|
/**
|
||||||
* @en A method that can be triggered by message
|
* 打开默认面板
|
||||||
* @zh 通过 message 触发的方法
|
|
||||||
*/
|
*/
|
||||||
openPanel() {
|
openPanel() {
|
||||||
Editor.Panel.open(packageJSON.name);
|
PanelHandler.openDefaultPanel();
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 安装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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开调试面板
|
* 打开调试面板
|
||||||
*/
|
*/
|
||||||
'open-debug'() {
|
'open-debug'() {
|
||||||
console.log('Opening ECS Framework debug panel...');
|
PanelHandler.openDebugPanel();
|
||||||
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编辑器。`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开代码生成器面板
|
* 打开代码生成器面板
|
||||||
*/
|
*/
|
||||||
'open-generator'() {
|
'open-generator'() {
|
||||||
console.log('Opening ECS Framework code generator panel...');
|
PanelHandler.openGeneratorPanel();
|
||||||
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编辑器。`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开行为树AI组件库面板
|
* 打开行为树面板
|
||||||
*/
|
*/
|
||||||
'open-behavior-tree'() {
|
'open-behavior-tree'() {
|
||||||
console.log('Opening Behavior Tree AI panel...');
|
PanelHandler.openBehaviorTreePanel();
|
||||||
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编辑器。`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ================ 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系统
|
* 安装行为树AI系统
|
||||||
*/
|
*/
|
||||||
async 'install-behavior-tree'() {
|
'install-behavior-tree'() {
|
||||||
console.log('Installing Behavior Tree AI system...');
|
BehaviorTreeHandler.install();
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新行为树AI系统
|
* 更新行为树AI系统
|
||||||
*/
|
*/
|
||||||
async 'update-behavior-tree'() {
|
'update-behavior-tree'() {
|
||||||
console.log('Updating Behavior Tree AI system...');
|
BehaviorTreeHandler.update();
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查行为树AI系统是否已安装
|
* 检查行为树AI是否已安装
|
||||||
*/
|
*/
|
||||||
async 'check-behavior-tree-installed'() {
|
'check-behavior-tree-installed'() {
|
||||||
const projectPath = Editor.Project.path;
|
return BehaviorTreeHandler.checkInstalled();
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开行为树文档
|
* 打开行为树文档
|
||||||
*/
|
*/
|
||||||
'open-behavior-tree-docs'() {
|
'open-behavior-tree-docs'() {
|
||||||
const url = 'https://github.com/esengine/BehaviourTree-ai/blob/master/ecs-integration/README.md';
|
BehaviorTreeHandler.openDocumentation();
|
||||||
|
|
||||||
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}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建行为树文件
|
* 创建行为树文件
|
||||||
*/
|
*/
|
||||||
async 'create-behavior-tree-file'(assetInfo: any) {
|
'create-behavior-tree-file'() {
|
||||||
console.log('Creating behavior tree file in folder:', assetInfo?.path);
|
BehaviorTreeHandler.createFile();
|
||||||
|
|
||||||
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)}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用行为树编辑器打开文件
|
* 加载行为树文件到编辑器
|
||||||
*/
|
*/
|
||||||
async 'open-behavior-tree-file'(assetInfo: any) {
|
async 'load-behavior-tree-file'(...args: any[]) {
|
||||||
console.log('Opening behavior tree file:', assetInfo);
|
const assetInfo = args.length >= 2 ? args[1] : args[0];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 直接从assetInfo获取文件系统路径
|
if (!assetInfo || (!assetInfo.file && !assetInfo.path)) {
|
||||||
const assetPath = assetInfo?.path;
|
throw new Error('无效的文件信息');
|
||||||
if (!assetPath) {
|
|
||||||
throw new Error('无效的文件路径');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换为文件系统路径
|
await Editor.Panel.open('cocos-ecs-extension.behavior-tree');
|
||||||
const projectPath = Editor.Project.path;
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
const relativePath = assetPath.replace('db://assets/', '');
|
|
||||||
const fsPath = path.join(projectPath, 'assets', relativePath);
|
|
||||||
|
|
||||||
console.log('File system path:', fsPath);
|
const result = await Editor.Message.request('cocos-ecs-extension', 'behavior-tree-panel-load-file', assetInfo);
|
||||||
|
|
||||||
// 检查文件是否存在
|
|
||||||
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}`);
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to open behavior tree file:', error);
|
|
||||||
Editor.Dialog.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 }) {
|
'create-behavior-tree-from-editor'(event: any, data: any) {
|
||||||
console.log('Creating behavior tree file from editor:', data.fileName);
|
BehaviorTreeHandler.createFromEditor(data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 覆盖现有行为树文件
|
||||||
|
*/
|
||||||
|
'overwrite-behavior-tree-file'(...args: any[]) {
|
||||||
|
const data = args.length >= 2 ? args[1] : args[0];
|
||||||
|
|
||||||
try {
|
if (data && data.filePath) {
|
||||||
const projectPath = Editor.Project.path;
|
BehaviorTreeHandler.overwriteFile(data);
|
||||||
const assetsPath = path.join(projectPath, 'assets');
|
} else {
|
||||||
|
throw new Error('文件路径不存在或数据无效');
|
||||||
// 确保文件名唯一
|
|
||||||
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)}`,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @en Method triggered when the extension is started
|
* @en Method triggered when the extension is started
|
||||||
* @zh 启动扩展时触发的方法
|
* @zh 启动扩展时触发的方法
|
||||||
*/
|
*/
|
||||||
export function load() {
|
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 卸载扩展时触发的方法
|
* @zh 卸载扩展时触发的方法
|
||||||
*/
|
*/
|
||||||
export function unload() {
|
export function unload() {
|
||||||
console.log('ECS Framework Extension unloaded');
|
console.log('[Cocos ECS Extension] 扩展已卸载');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,16 +74,6 @@ export function useBehaviorTreeEditor() {
|
|||||||
appState.isInstalling
|
appState.isInstalling
|
||||||
);
|
);
|
||||||
|
|
||||||
const fileOps = useFileOperations(
|
|
||||||
appState.treeNodes,
|
|
||||||
appState.selectedNodeId,
|
|
||||||
appState.connections,
|
|
||||||
appState.tempConnection,
|
|
||||||
appState.showExportModal,
|
|
||||||
codeGen,
|
|
||||||
() => connectionManager.updateConnections()
|
|
||||||
);
|
|
||||||
|
|
||||||
const connectionState = reactive({
|
const connectionState = reactive({
|
||||||
isConnecting: false,
|
isConnecting: false,
|
||||||
startNodeId: null as string | null,
|
startNodeId: null as string | null,
|
||||||
@@ -105,6 +95,16 @@ export function useBehaviorTreeEditor() {
|
|||||||
appState.zoomLevel
|
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(
|
const canvasManager = useCanvasManager(
|
||||||
appState.panX,
|
appState.panX,
|
||||||
appState.panY,
|
appState.panY,
|
||||||
@@ -182,11 +182,160 @@ export function useBehaviorTreeEditor() {
|
|||||||
installation.handleInstall();
|
installation.handleInstall();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 组件挂载时初始化连接
|
// 自动布局功能
|
||||||
onMounted(() => {
|
const autoLayout = () => {
|
||||||
// 延迟一下确保 DOM 已经渲染
|
if (appState.treeNodes.value.length === 0) {
|
||||||
nextTick(() => {
|
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();
|
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,
|
...canvasManager,
|
||||||
...nodeDisplay,
|
...nodeDisplay,
|
||||||
startNodeDrag,
|
startNodeDrag,
|
||||||
dragState
|
dragState,
|
||||||
|
autoLayout,
|
||||||
|
validateTree
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -268,13 +268,102 @@ export const config = behaviorTreeConfig;`;
|
|||||||
|
|
||||||
// 从配置创建行为树节点
|
// 从配置创建行为树节点
|
||||||
const createTreeFromConfig = (config: any): TreeNode[] => {
|
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 [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodes: TreeNode[] = [];
|
const nodes: TreeNode[] = [];
|
||||||
const processNode = (nodeConfig: any, parent?: TreeNode): 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) {
|
if (!template) {
|
||||||
throw new Error(`未知节点类型: ${nodeConfig.type}`);
|
throw new Error(`未知节点类型: ${nodeConfig.type}`);
|
||||||
}
|
}
|
||||||
@@ -287,25 +376,35 @@ export const config = behaviorTreeConfig;`;
|
|||||||
description: template.description,
|
description: template.description,
|
||||||
canHaveChildren: template.canHaveChildren,
|
canHaveChildren: template.canHaveChildren,
|
||||||
canHaveParent: template.canHaveParent,
|
canHaveParent: template.canHaveParent,
|
||||||
x: 400, // 默认在画布中心
|
x: 400,
|
||||||
y: 100, // 从顶部开始
|
y: 100,
|
||||||
properties: {},
|
properties: {},
|
||||||
children: [],
|
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]) => {
|
Object.entries(nodeConfig.properties).forEach(([key, propConfig]: [string, any]) => {
|
||||||
if (template.properties?.[key]) {
|
if (template.properties![key]) {
|
||||||
node.properties![key] = {
|
node.properties![key] = {
|
||||||
...template.properties[key],
|
...template.properties![key],
|
||||||
value: propConfig.value
|
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);
|
nodes.push(node);
|
||||||
|
|
||||||
// 处理子节点
|
// 处理子节点
|
||||||
@@ -323,6 +422,49 @@ export const config = behaviorTreeConfig;`;
|
|||||||
return nodes;
|
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
|
// 生成唯一节点ID
|
||||||
const generateNodeId = (): string => {
|
const generateNodeId = (): string => {
|
||||||
return 'node_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
return 'node_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ export function useConnectionManager(
|
|||||||
return getPortInfo(elementAtPoint as HTMLElement);
|
return getPortInfo(elementAtPoint as HTMLElement);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`[ConnectionManager] elementFromPoint 查询出错:`, error);
|
// 查询出错时静默处理
|
||||||
}
|
}
|
||||||
|
|
||||||
const allPorts = canvasAreaRef.value.querySelectorAll('.port');
|
const allPorts = canvasAreaRef.value.querySelectorAll('.port');
|
||||||
|
|||||||
@@ -1,26 +1,44 @@
|
|||||||
import { Ref, ref, watch } from 'vue';
|
import { Ref, ref, watch } from 'vue';
|
||||||
import { TreeNode, Connection } from '../types';
|
import { TreeNode, Connection } from '../types';
|
||||||
|
|
||||||
/**
|
interface FileOperationOptions {
|
||||||
* 文件操作管理
|
treeNodes: Ref<TreeNode[]>;
|
||||||
*/
|
selectedNodeId: Ref<string | null>;
|
||||||
export function useFileOperations(
|
connections: Ref<Connection[]>;
|
||||||
treeNodes: Ref<TreeNode[]>,
|
tempConnection: Ref<{ path: string }>;
|
||||||
selectedNodeId: Ref<string | null>,
|
showExportModal: Ref<boolean>;
|
||||||
connections: Ref<Connection[]>,
|
|
||||||
tempConnection: Ref<{ path: string }>,
|
|
||||||
showExportModal: Ref<boolean>,
|
|
||||||
codeGeneration?: {
|
codeGeneration?: {
|
||||||
createTreeFromConfig: (config: any) => TreeNode[];
|
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 hasUnsavedChanges = ref(false);
|
||||||
const lastSavedState = ref<string>('');
|
const lastSavedState = ref<string>('');
|
||||||
const currentFileName = ref('');
|
const currentFileName = ref('');
|
||||||
|
const currentFilePath = ref('');
|
||||||
// 监听树结构变化来更新未保存状态
|
|
||||||
const updateUnsavedStatus = () => {
|
const updateUnsavedStatus = () => {
|
||||||
const currentState = JSON.stringify({
|
const currentState = JSON.stringify({
|
||||||
nodes: treeNodes.value,
|
nodes: treeNodes.value,
|
||||||
@@ -28,11 +46,9 @@ export function useFileOperations(
|
|||||||
});
|
});
|
||||||
hasUnsavedChanges.value = currentState !== lastSavedState.value;
|
hasUnsavedChanges.value = currentState !== lastSavedState.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听变化
|
|
||||||
watch([treeNodes, connections], updateUnsavedStatus, { deep: true });
|
watch([treeNodes, connections], updateUnsavedStatus, { deep: true });
|
||||||
|
|
||||||
// 标记为已保存
|
|
||||||
const markAsSaved = () => {
|
const markAsSaved = () => {
|
||||||
const currentState = JSON.stringify({
|
const currentState = JSON.stringify({
|
||||||
nodes: treeNodes.value,
|
nodes: treeNodes.value,
|
||||||
@@ -41,38 +57,19 @@ export function useFileOperations(
|
|||||||
lastSavedState.value = currentState;
|
lastSavedState.value = currentState;
|
||||||
hasUnsavedChanges.value = false;
|
hasUnsavedChanges.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 检查是否需要保存的通用方法
|
const setCurrentFile = (fileName: string, filePath: string = '') => {
|
||||||
const checkUnsavedChanges = (): Promise<boolean> => {
|
currentFileName.value = fileName;
|
||||||
return new Promise((resolve) => {
|
currentFilePath.value = filePath;
|
||||||
if (!hasUnsavedChanges.value) {
|
markAsSaved();
|
||||||
resolve(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = confirm(
|
|
||||||
'当前行为树有未保存的更改,是否要保存?\n\n' +
|
|
||||||
'点击"确定"保存更改\n' +
|
|
||||||
'点击"取消"丢弃更改\n' +
|
|
||||||
'点击"X"取消操作'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
// 用户选择保存
|
|
||||||
saveBehaviorTree().then(() => {
|
|
||||||
resolve(true);
|
|
||||||
}).catch(() => {
|
|
||||||
resolve(false);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 用户选择丢弃更改
|
|
||||||
resolve(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 导出行为树数据
|
const clearCurrentFile = () => {
|
||||||
const exportBehaviorTreeData = () => {
|
currentFileName.value = '';
|
||||||
|
currentFilePath.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportBehaviorTreeData = (): FileData => {
|
||||||
return {
|
return {
|
||||||
nodes: treeNodes.value,
|
nodes: treeNodes.value,
|
||||||
connections: connections.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 newBehaviorTree = async () => {
|
||||||
const canProceed = await checkUnsavedChanges();
|
const canProceed = await checkUnsavedChanges();
|
||||||
if (canProceed) {
|
if (canProceed) {
|
||||||
treeNodes.value = [];
|
treeNodes.value = [];
|
||||||
selectedNodeId.value = null;
|
selectedNodeId.value = null;
|
||||||
connections.value = [];
|
connections.value = [];
|
||||||
tempConnection.value.path = '';
|
tempConnection.value.path = '';
|
||||||
currentFileName.value = '';
|
clearCurrentFile();
|
||||||
markAsSaved(); // 新建后标记为已保存状态
|
markAsSaved();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 保存行为树
|
|
||||||
const saveBehaviorTree = async (): Promise<boolean> => {
|
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 {
|
try {
|
||||||
const data = exportBehaviorTreeData();
|
const data = exportBehaviorTreeData();
|
||||||
const jsonString = JSON.stringify(data, null, 2);
|
const jsonString = JSON.stringify(data, null, 2);
|
||||||
console.log('数据准备完成,JSON长度:', jsonString.length);
|
|
||||||
|
|
||||||
// 使用 HTML input 替代 prompt(因为 prompt 在 Cocos Creator 扩展中不支持)
|
await sendToMain('overwrite-behavior-tree-file', {
|
||||||
const fileName = await getFileNameFromUser();
|
filePath: currentFilePath.value,
|
||||||
if (!fileName) {
|
content: jsonString
|
||||||
console.log('❌ 用户取消了保存操作');
|
});
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✓ 用户输入文件名:', fileName);
|
markAsSaved();
|
||||||
|
showMessage('保存成功!');
|
||||||
// 检测是否在Cocos Creator环境中
|
return true;
|
||||||
if (typeof Editor !== 'undefined' && typeof (window as any).sendToMain === 'function') {
|
} catch (error) {
|
||||||
console.log('✓ 使用Cocos Creator保存方式');
|
showMessage('保存失败: ' + error, 'error');
|
||||||
|
return false;
|
||||||
try {
|
}
|
||||||
(window as any).sendToMain('create-behavior-tree-from-editor', {
|
};
|
||||||
fileName: fileName + '.json',
|
|
||||||
content: jsonString,
|
const saveAsBehaviorTree = async (): Promise<boolean> => {
|
||||||
timestamp: new Date().toISOString()
|
try {
|
||||||
});
|
const data = exportBehaviorTreeData();
|
||||||
|
const jsonString = JSON.stringify(data, null, 2);
|
||||||
console.log('✓ 保存消息已发送到主进程');
|
|
||||||
|
const result = await Editor.Dialog.save({
|
||||||
// 更新当前文件名并标记为已保存
|
title: '保存行为树文件',
|
||||||
currentFileName.value = fileName;
|
filters: [
|
||||||
markAsSaved();
|
{ name: '行为树文件', extensions: ['bt.json', 'json'] },
|
||||||
|
{ name: '所有文件', extensions: ['*'] }
|
||||||
// 用户反馈
|
]
|
||||||
showMessage(`保存成功!文件名: ${fileName}.json`, 'success');
|
});
|
||||||
|
|
||||||
console.log('✅ 保存操作完成');
|
if (result.canceled || !result.filePath) {
|
||||||
return true;
|
return false;
|
||||||
} catch (sendError) {
|
}
|
||||||
console.error('❌ 发送消息时出错:', sendError);
|
|
||||||
showMessage('保存失败: ' + sendError, 'error');
|
const fs = require('fs-extra');
|
||||||
return false;
|
await fs.writeFile(result.filePath, jsonString);
|
||||||
}
|
|
||||||
} else {
|
const path = require('path');
|
||||||
console.log('✓ 使用浏览器下载保存方式');
|
const fileName = path.basename(result.filePath, path.extname(result.filePath));
|
||||||
|
setCurrentFile(fileName, result.filePath);
|
||||||
// 在浏览器环境中使用下载方式
|
showMessage(`保存成功!文件: ${result.filePath}`);
|
||||||
const blob = new Blob([jsonString], { type: 'application/json' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
return true;
|
||||||
const a = document.createElement('a');
|
} catch (error) {
|
||||||
a.href = url;
|
showMessage('另存为失败: ' + error, 'error');
|
||||||
a.download = `${fileName}.json`;
|
return false;
|
||||||
document.body.appendChild(a);
|
}
|
||||||
a.click();
|
};
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
const saveToFile = async (fileName: string, jsonString: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
// 标记为已保存
|
await sendToMain('create-behavior-tree-from-editor', {
|
||||||
currentFileName.value = fileName;
|
fileName: fileName + '.json',
|
||||||
markAsSaved();
|
content: jsonString
|
||||||
|
});
|
||||||
console.log('✅ 文件下载保存成功');
|
|
||||||
return true;
|
setCurrentFile(fileName, `assets/${fileName}.bt.json`);
|
||||||
}
|
showMessage(`保存成功!文件名: ${fileName}.json`);
|
||||||
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 保存过程中发生错误:', error);
|
|
||||||
showMessage('保存失败: ' + error, 'error');
|
showMessage('保存失败: ' + error, 'error');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 使用 HTML input 获取文件名(替代 prompt)
|
|
||||||
const getFileNameFromUser = (): Promise<string | null> => {
|
const getFileNameFromUser = (): Promise<string | null> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
// 创建模态对话框
|
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.style.cssText = `
|
overlay.style.cssText = `
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -216,11 +293,9 @@ export function useFileOperations(
|
|||||||
const saveBtn = dialog.querySelector('#save-btn') as HTMLButtonElement;
|
const saveBtn = dialog.querySelector('#save-btn') as HTMLButtonElement;
|
||||||
const cancelBtn = dialog.querySelector('#cancel-btn') as HTMLButtonElement;
|
const cancelBtn = dialog.querySelector('#cancel-btn') as HTMLButtonElement;
|
||||||
|
|
||||||
// 聚焦并选中文本
|
|
||||||
input.focus();
|
input.focus();
|
||||||
input.select();
|
input.select();
|
||||||
|
|
||||||
// 事件处理
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
document.body.removeChild(overlay);
|
document.body.removeChild(overlay);
|
||||||
};
|
};
|
||||||
@@ -236,7 +311,6 @@ export function useFileOperations(
|
|||||||
resolve(null);
|
resolve(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 回车键保存
|
|
||||||
input.onkeydown = (e) => {
|
input.onkeydown = (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
const fileName = input.value.trim();
|
const fileName = input.value.trim();
|
||||||
@@ -250,131 +324,108 @@ export function useFileOperations(
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 显示消息提示
|
const loadFileContent = (fileData: any, filePath: string = '') => {
|
||||||
const showMessage = (message: string, type: 'success' | 'error' = 'success') => {
|
try {
|
||||||
const toast = document.createElement('div');
|
if (!fileData) {
|
||||||
toast.style.cssText = `
|
return;
|
||||||
position: fixed;
|
}
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
let parsedData = fileData;
|
||||||
padding: 12px 20px;
|
|
||||||
background: ${type === 'success' ? '#4caf50' : '#f44336'};
|
if (fileData.rawContent) {
|
||||||
color: white;
|
try {
|
||||||
border-radius: 4px;
|
parsedData = JSON.parse(fileData.rawContent);
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
} catch (e) {
|
||||||
z-index: 10001;
|
parsedData = {
|
||||||
opacity: 0;
|
nodes: [],
|
||||||
transform: translateX(100%);
|
connections: []
|
||||||
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
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
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 () => {
|
const loadBehaviorTree = async () => {
|
||||||
@@ -397,20 +448,34 @@ export function useFileOperations(
|
|||||||
const newNodes = codeGeneration.createTreeFromConfig(config);
|
const newNodes = codeGeneration.createTreeFromConfig(config);
|
||||||
treeNodes.value = newNodes;
|
treeNodes.value = newNodes;
|
||||||
selectedNodeId.value = null;
|
selectedNodeId.value = null;
|
||||||
connections.value = [];
|
|
||||||
tempConnection.value.path = '';
|
if (config.connections && Array.isArray(config.connections)) {
|
||||||
markAsSaved(); // 加载后标记为已保存状态
|
connections.value = config.connections.map((conn: any) => ({
|
||||||
console.log('行为树配置加载成功');
|
id: conn.id,
|
||||||
if (updateConnections) {
|
sourceId: conn.sourceId,
|
||||||
updateConnections();
|
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 {
|
} else {
|
||||||
console.error('代码生成器未初始化');
|
showMessage('代码生成器未初始化', 'error');
|
||||||
alert('代码生成器未初始化');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载行为树配置失败:', error);
|
showMessage('配置文件格式错误', 'error');
|
||||||
alert('配置文件格式错误');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
@@ -423,37 +488,18 @@ export function useFileOperations(
|
|||||||
showExportModal.value = true;
|
showExportModal.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyToClipboard = () => {
|
|
||||||
// TODO: 实现复制到剪贴板功能
|
|
||||||
console.log('复制到剪贴板');
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveToFile = () => {
|
|
||||||
// TODO: 实现保存到文件功能
|
|
||||||
console.log('保存到文件');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 验证相关
|
|
||||||
const autoLayout = () => {
|
|
||||||
// TODO: 实现自动布局功能
|
|
||||||
console.log('自动布局');
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateTree = () => {
|
|
||||||
// TODO: 实现树验证功能
|
|
||||||
console.log('验证树结构');
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
newBehaviorTree,
|
newBehaviorTree,
|
||||||
saveBehaviorTree,
|
saveBehaviorTree,
|
||||||
|
saveAsBehaviorTree,
|
||||||
loadBehaviorTree,
|
loadBehaviorTree,
|
||||||
|
loadFileContent,
|
||||||
exportConfig,
|
exportConfig,
|
||||||
copyToClipboard,
|
|
||||||
saveToFile,
|
|
||||||
autoLayout,
|
|
||||||
validateTree,
|
|
||||||
hasUnsavedChanges,
|
hasUnsavedChanges,
|
||||||
markAsSaved
|
markAsSaved,
|
||||||
|
setCurrentFile,
|
||||||
|
clearCurrentFile,
|
||||||
|
currentFileName,
|
||||||
|
currentFilePath
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,6 @@ export function useInstallation(
|
|||||||
isInstalled.value = result.installed;
|
isInstalled.value = result.installed;
|
||||||
version.value = result.version;
|
version.value = result.version;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('检查安装状态失败:', error);
|
|
||||||
isInstalled.value = false;
|
isInstalled.value = false;
|
||||||
version.value = null;
|
version.value = null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -34,7 +33,7 @@ export function useInstallation(
|
|||||||
await installBehaviorTreeAI(Editor.Project.path);
|
await installBehaviorTreeAI(Editor.Project.path);
|
||||||
await checkInstallStatus();
|
await checkInstallStatus();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('安装失败:', error);
|
// 安装失败时静默处理
|
||||||
} finally {
|
} finally {
|
||||||
isInstalling.value = false;
|
isInstalling.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export function useNodeOperations(
|
|||||||
selectedNodeId.value = newNode.id;
|
selectedNodeId.value = newNode.id;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('节点创建失败:', error);
|
// 节点创建失败时静默处理
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -123,44 +123,18 @@ export function useNodeOperations(
|
|||||||
|
|
||||||
// 节点属性更新
|
// 节点属性更新
|
||||||
const updateNodeProperty = (path: string, value: any) => {
|
const updateNodeProperty = (path: string, value: any) => {
|
||||||
console.log('updateNodeProperty called:', path, value);
|
|
||||||
const node = selectedNodeId.value ? getNodeByIdLocal(selectedNodeId.value) : null;
|
const node = selectedNodeId.value ? getNodeByIdLocal(selectedNodeId.value) : null;
|
||||||
if (!node) {
|
if (!node) return;
|
||||||
console.log('No selected node found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Current node before update:', JSON.stringify(node, null, 2));
|
|
||||||
|
|
||||||
// 使用通用方法更新属性
|
// 使用通用方法更新属性
|
||||||
setNestedProperty(node, path, value);
|
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);
|
const nodeIndex = treeNodes.value.findIndex(n => n.id === node.id);
|
||||||
if (nodeIndex > -1) {
|
if (nodeIndex > -1) {
|
||||||
// 创建新的节点数组,确保Vue能检测到变化
|
|
||||||
const newNodes = [...treeNodes.value];
|
const newNodes = [...treeNodes.value];
|
||||||
newNodes[nodeIndex] = { ...node }; // 创建节点副本确保响应式更新
|
newNodes[nodeIndex] = { ...node };
|
||||||
treeNodes.value = newNodes;
|
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 { join } from 'path';
|
||||||
import { createApp, App, defineComponent } from 'vue';
|
import { createApp, App, defineComponent } from 'vue';
|
||||||
import { useBehaviorTreeEditor } from './composables/useBehaviorTreeEditor';
|
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: {
|
let pendingFileData: any = null;
|
||||||
show() { },
|
// Vue应用是否已挂载完成
|
||||||
hide() { },
|
let vueAppMounted: boolean = false;
|
||||||
},
|
// 存储面板实例,用于访问面板的DOM元素
|
||||||
|
let currentPanelInstance: any = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 面板定义
|
||||||
|
*/
|
||||||
|
const panelDefinition = {
|
||||||
|
/**
|
||||||
|
* 面板模板
|
||||||
|
*/
|
||||||
template: readFileSync(join(__dirname, '../../../static/template/behavior-tree/index.html'), 'utf-8'),
|
template: readFileSync(join(__dirname, '../../../static/template/behavior-tree/index.html'), 'utf-8'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 面板样式
|
||||||
|
*/
|
||||||
style: readFileSync(join(__dirname, '../../../static/style/behavior-tree/index.css'), 'utf-8'),
|
style: readFileSync(join(__dirname, '../../../static/style/behavior-tree/index.css'), 'utf-8'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择器
|
||||||
|
*/
|
||||||
$: {
|
$: {
|
||||||
app: '#behavior-tree-app',
|
app: '#behavior-tree-app',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 面板方法 - 用于处理来自扩展主进程的消息
|
||||||
|
*/
|
||||||
methods: {
|
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) {
|
if (this.$.app) {
|
||||||
console.log('Loading behavior tree file:', fileData);
|
try {
|
||||||
|
const BehaviorTreeEditor = defineComponent({
|
||||||
// 通知编辑器组件加载文件
|
setup() {
|
||||||
if (this.$.app) {
|
const editor = useBehaviorTreeEditor();
|
||||||
const event = new CustomEvent('load-behavior-tree-file', {
|
return editor;
|
||||||
detail: fileData
|
},
|
||||||
|
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() {
|
close() {
|
||||||
const app = panelDataMap.get(this);
|
try {
|
||||||
if (app) {
|
const app = panelDataMap.get(this);
|
||||||
app.unmount();
|
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'))
|
packageExists: fs.existsSync(path.join(projectPath, 'package.json'))
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('检查行为树安装状态失败:', error);
|
|
||||||
return {
|
return {
|
||||||
installed: false,
|
installed: false,
|
||||||
version: null,
|
version: null,
|
||||||
@@ -81,9 +80,8 @@ export async function installBehaviorTreeAI(projectPath: string): Promise<void>
|
|||||||
throw new Error('安装请求失败,未收到主进程响应');
|
throw new Error('安装请求失败,未收到主进程响应');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('行为树AI系统安装完成');
|
// 安装完成
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('行为树AI系统安装失败:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,9 +100,8 @@ export async function updateBehaviorTreeAI(projectPath: string): Promise<void> {
|
|||||||
throw new Error('更新请求失败,未收到主进程响应');
|
throw new Error('更新请求失败,未收到主进程响应');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('行为树AI系统更新完成');
|
// 更新完成
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('行为树AI系统更新失败:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,6 +33,13 @@
|
|||||||
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
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 {
|
.unsaved-indicator {
|
||||||
color: #ff6b6b;
|
color: #ff6b6b;
|
||||||
animation: pulse-unsaved 2s infinite;
|
animation: pulse-unsaved 2s infinite;
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
<!-- 头部工具栏 -->
|
<!-- 头部工具栏 -->
|
||||||
<div class="header-toolbar">
|
<div class="header-toolbar">
|
||||||
<div class="toolbar-left">
|
<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">
|
<div class="toolbar-buttons">
|
||||||
<button class="tool-btn" @click="newBehaviorTree" title="新建行为树">
|
<button class="tool-btn" @click="newBehaviorTree" title="新建行为树">
|
||||||
<span>📄</span> 新建
|
<span>📄</span> 新建
|
||||||
</button>
|
</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 ? ' *' : '' }}
|
<span>💾</span> 保存{{ hasUnsavedChanges ? ' *' : '' }}
|
||||||
</button>
|
</button>
|
||||||
|
<button class="tool-btn" @click="saveAsBehaviorTree" title="另存为新文件">
|
||||||
|
<span>💾</span> 另存为
|
||||||
|
</button>
|
||||||
<button class="tool-btn" @click="loadBehaviorTree" title="加载行为树">
|
<button class="tool-btn" @click="loadBehaviorTree" title="加载行为树">
|
||||||
<span>📂</span> 加载
|
<span>📂</span> 加载
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"__version__": "1.0.6"
|
"__version__": "1.0.6",
|
||||||
|
"custom_joint_texture_layouts": []
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user