version:1.0.1

This commit is contained in:
sli97 2023-09-26 22:16:42 +08:00
parent 47104caa6e
commit 2ee8352e9d
12 changed files with 552 additions and 332 deletions

18
CHANGELOG.md Normal file
View File

@ -0,0 +1,18 @@
# Changelog
# 1.0.1 / 2023-09-26
**调整**
- 行为树编辑器面板默认在`Cocos`主编辑器内打开(场景 Panel 旁边),更方便用户操作。
- 切换`层级管理器`节点时,会自动切换为当前节点的行为树,如果当前节点没有行为树,则显示 mask 提示用户`添加组件`并`创建json`。
- 修改`BehaviorEditor`组件按钮文案为`Create / Editor`。
- 在已经打开行为树编辑器的情况下,再次点击`BehaviorEditor`组件的`Create / Editor`可以起到重启插件的效果在更新了外部资源JSON组件
- 画布大小从`2400 1600`下降为` 2000 1400`。
- 禁止设置非组合节点`Composite`的`Abort Type`属性。
- 左下角增加`Root`按钮回到根节点处
- 样式修改
# 1.0.0 / 2023-09-21
**首次发布**

View File

@ -3,8 +3,8 @@
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.ready = exports.update = exports.$ = exports.template = void 0; exports.ready = exports.update = exports.$ = exports.template = void 0;
exports.template = ` exports.template = `
<div style="display:flex;justify-content:center;align-items:center;margin-top:10px;padding-right:8px;"> <div style="display:flex;justify-content:center;align-items:center;margin-top:10px;">
<ui-button class="editor" style="height:24px;padding:0 16px;">Edit Tree</ui-prop> <ui-button class="editor" style="height:24px;padding:0 16px;">Create / Edit</ui-prop>
</div> </div>
`; `;
@ -64,15 +64,17 @@ function ready() {
}); });
if (success) { if (success) {
console.log(`JSON文件挂载成功`); console.log(`JSON文件挂载成功`);
Editor.Message.request("behavior-eden", "open-panel");
} }
else { else {
console.warn("JSON文件挂载失败"); console.warn("JSON文件挂载失败");
return;
} }
} }
else {
// 打开插件面板 // 打开插件面板
Editor.Message.request("behavior-eden", "open-panel"); const success = await Editor.Message.request("behavior-eden", "open-panel");
// success为false可能是已经打开了通知面板刷新
if (!success) {
await Editor.Message.request("behavior-eden", "refresh-panel");
} }
}); });
} }

9
dist/main.js vendored
View File

@ -11,8 +11,13 @@ const package_json_1 = __importDefault(require("../package.json"));
* @zh 为扩展的主进程的注册方法 * @zh 为扩展的主进程的注册方法
*/ */
exports.methods = { exports.methods = {
openPanel() { async openPanel() {
Editor.Panel.open(package_json_1.default.name); return await Editor.Panel.openBeside("scene", package_json_1.default.name);
},
async refreshPanel() {
await Editor.Panel.close(package_json_1.default.name);
await new Promise((rs) => setTimeout(rs, 300));
await Editor.Panel.openBeside("scene", package_json_1.default.name);
}, },
}; };
/** /**

View File

@ -10,8 +10,8 @@ const node_1 = require("../../runtime/node");
const decorator_1 = require("../../runtime/core/decorator"); const decorator_1 = require("../../runtime/core/decorator");
const utils_1 = require("../../runtime/core/utils"); const utils_1 = require("../../runtime/core/utils");
// 画布宽高 // 画布宽高
const CANVAS_WIDTH = 2400; const CANVAS_WIDTH = 2000;
const CANVAS_HEIGHT = 1600; const CANVAS_HEIGHT = 1400;
// 节点box属性 // 节点box属性
const BOX_WIDTH = 100; const BOX_WIDTH = 100;
// const BOX_HEIGHT = 64; //height通过getBoxHeight方法动态计算 // const BOX_HEIGHT = 64; //height通过getBoxHeight方法动态计算
@ -141,6 +141,21 @@ const ParentNodes = [...decorator_1.nodeClsMap.values()]
return false; return false;
}) })
.map((cls) => cls.name); .map((cls) => cls.name);
/***
* 获取所有Composite类型的节点
*/
const CompositeNodes = [...decorator_1.nodeClsMap.values()]
.filter((cls) => {
let parentCls = getParentCls(cls);
while (parentCls) {
if (parentCls.name === node_1.Composite.name) {
return true;
}
parentCls = getParentCls(parentCls);
}
return false;
})
.map((cls) => cls.name);
const activeCursor = () => { const activeCursor = () => {
stage && (stage.container().style.cursor = "pointer"); stage && (stage.container().style.cursor = "pointer");
}; };
@ -151,7 +166,7 @@ const component = lib_1.Vue.extend({
template: lib_1.fs.readFileSync(path_1.default.join(__dirname, "../../../src/panels/static/template/vue/app.html"), "utf-8"), template: lib_1.fs.readFileSync(path_1.default.join(__dirname, "../../../src/panels/static/template/vue/app.html"), "utf-8"),
$: { $: {
tree: "#tree", tree: "#tree",
left: "#left", scroll: "#scroll",
}, },
filters: { filters: {
toUpperCase(value) { toUpperCase(value) {
@ -189,6 +204,7 @@ const component = lib_1.Vue.extend({
*/ */
nodeCompMethodInfo: {}, nodeCompMethodInfo: {},
logs: [], logs: [],
maskText: "",
}; };
}, },
computed: { computed: {
@ -253,12 +269,143 @@ const component = lib_1.Vue.extend({
}, },
}, },
async mounted() { async mounted() {
this.initCanvas(); this.init();
await this.getAssets();
await this.selectCurrentAsset();
this.$refs.left.scroll((CANVAS_WIDTH - this.$refs.left.getBoundingClientRect().width) / 2, ROOT_Y - 50);
}, },
methods: { methods: {
/***
* 初始化相关
*/
async init() {
// 初始化画布
this.initCanvas();
// 根据当前选中节点初始化行为树
await this.initSelection();
this.backRoot();
},
initCanvas() {
// 初始化舞台
stage = new lib_1.Konva.Stage({
container: this.$refs.tree,
width: CANVAS_WIDTH,
height: CANVAS_HEIGHT,
});
// 基础层目前只有一层每层都会生成一个canvas
baseLayer = new lib_1.Konva.Layer();
// 背景组
bgGroup = this.generateBg();
baseLayer.add(bgGroup);
// 静态箭头层
staticArrowGroup = new lib_1.Konva.Group();
baseLayer.add(staticArrowGroup);
// 动态箭头层(用户手动拉出来的箭头)
dynamicArrowGroup = this.generateDynamicArrowGroup();
baseLayer.add(dynamicArrowGroup);
// Root组
rootGroup = this.generateRoot();
baseLayer.add(rootGroup);
// 节点层
nodeGroup = new lib_1.Konva.Group();
baseLayer.add(nodeGroup);
// 框选层
selectionGroup = this.generatesSelectionGroup();
baseLayer.add(selectionGroup);
// 层添加到舞台
stage.add(baseLayer);
},
// 获取json文件
async initAssets() {
const behaviorTreeComponentUuids = [];
const dfs = (node) => {
if (!node) {
return;
}
for (const comp of node.components) {
if (comp.type === BTreeCompName) {
behaviorTreeComponentUuids.push(comp.value);
break;
}
}
for (const item of node.children) {
dfs(item);
}
};
// 获取场景节点树
const sceneNode = await Editor.Message.request("scene", "query-node-tree");
// 收集场景上所有BehaviorTree组件uuid
dfs(sceneNode);
// 收集BehaviorTree组件上的json路径
const rawUrls = await Promise.all(behaviorTreeComponentUuids.map((uuid) => Editor.Message.request("scene", "execute-component-method", {
uuid: uuid,
name: "getAssetUrl",
})));
// 过滤空的并去重
const urls = [...new Set(rawUrls.filter(Boolean))];
// 根据url获取所有json文件信息
const assets = await Promise.all(urls.map((url) => Editor.Message.request("asset-db", "query-asset-info", url)));
this.assets = assets.map(({ name, source, file }) => ({ name, url: source, file: file, content: "" }));
},
async initSelection() {
// 找到当前选中的节点
const node = await Editor.Message.request("scene", "query-node", Editor.Selection.getSelected("node"));
// 未选中节点或者选中的是场景节点(场景节点不能添加组件)
if (!(node === null || node === void 0 ? void 0 : node.__comps__)) {
this.maskText = "请选中一个非场景根节点非空名称节点来开始行为树制作";
return;
}
// 找到BehaviorTree组件
const index = node.__comps__.findIndex((v) => v.type === BTreeCompName);
if (index === -1) {
this.maskText = "要制作行为树,需要先为当前节点添加行为树组件";
return;
}
const comp = node.__comps__[index];
// 调用BehaviorTree组件上的方法
const url = await Editor.Message.request("scene", "execute-component-method", {
uuid: comp.value.uuid.value,
name: "getAssetUrl",
});
// JSON文件不存在
if (!url) {
this.maskText = "当前行为树组件缺少JSON资源点击BehaviorEditor组件按钮即可创建";
return;
}
this.maskText = "";
// 选中此文件设置好currentAsset才能把组件nodes数据同步到JSON
this.handleSelectAsset(url);
},
async handleSelectAsset(url) {
var _a;
// 实时获取当前场景所有behaviorTree组件上的json文件
await this.initAssets();
const json = this.assets.find((e) => e.url === url);
if (!json) {
this.showWarn("JSON文件不存在");
return;
}
if (((_a = this.currentAsset) === null || _a === void 0 ? void 0 : _a.url) === (json === null || json === void 0 ? void 0 : json.url)) {
return;
}
this.currentAsset = json;
try {
const content = await lib_1.fs.readJSONSync(json.file);
this.currentAsset.content = content;
this.render();
}
catch (e) {
if (e instanceof SyntaxError) {
// JSON文件语法异常的情况下初始化内容
this.showWarn("JSON文件内容初始化成功");
const content = { nodes: [] };
await lib_1.fs.writeJSONSync(json.file, content);
this.currentAsset.content = content;
this.render();
}
else {
// 输出其他异常
this.showWarn(e);
}
}
},
handlePanelChange(value) { handlePanelChange(value) {
this.currentPanel = value; this.currentPanel = value;
}, },
@ -275,7 +422,7 @@ const component = lib_1.Vue.extend({
this.render(); this.render();
} }
}, },
async handleEventNodeChange(lifeCycle, uuid = "") { async handleEventNodeChange(lifeCycle, uuid = "", shouldSave = true) {
if (!this.currentNode) { if (!this.currentNode) {
this.showWarn("当前节点不存在"); this.showWarn("当前节点不存在");
return; return;
@ -287,9 +434,13 @@ const component = lib_1.Vue.extend({
else { else {
this.currentNode.event[lifeCycle].comp = ""; this.currentNode.event[lifeCycle].comp = "";
this.currentNode.event[lifeCycle].method = ""; this.currentNode.event[lifeCycle].method = "";
this.currentNode.event[lifeCycle].data = ""; // 事件参数就不手动清空了
// this.currentNode.event[lifeCycle].data = "";
} }
// 点击canvas某个节点的时候会触发handleEventNodeChange获取场景节点数据此时不保存数据
if (shouldSave) {
await this.saveAsset(); await this.saveAsset();
}
}, },
async getNodeCompMethods(lifeCycle, uuid) { async getNodeCompMethods(lifeCycle, uuid) {
const [nodeInfo, compMethodInfo] = await Promise.all([ const [nodeInfo, compMethodInfo] = await Promise.all([
@ -331,132 +482,16 @@ const component = lib_1.Vue.extend({
this.currentNode.event[lifeCycle].data = data; this.currentNode.event[lifeCycle].data = data;
await this.saveAsset(); await this.saveAsset();
}, },
async selectCurrentAsset() {
// 找到点击组件的节点
const node = await Editor.Message.request("scene", "query-node", Editor.Selection.getSelected("node"));
if (!node) {
this.showWarn(`未选中节点`);
return;
}
// 找到BehaviorTree组件
const index = node.__comps__.findIndex((v) => v.type === BTreeCompName);
if (index === -1) {
this.showWarn(`节点未挂载【${BTreeCompName}】组件`);
return;
}
const comp = node.__comps__[index];
// 调用BehaviorTree组件上的方法
const url = await Editor.Message.request("scene", "execute-component-method", {
uuid: comp.value.uuid.value,
name: "getAssetUrl",
});
// JSON文件不存在
if (!url) {
this.showWarn(`${BTreeCompName}】组件未指定JSON文件`);
return;
}
// 选中此文件设置好currentAsset才能把组件nodes数据同步到JSON
this.handleSelectAsset(url);
},
async handleSelectAsset(url) {
const json = this.assets.find((e) => e.url === url);
if (!json) {
this.showWarn("JSON文件不存在");
return;
}
this.currentAsset = json;
try {
const content = await lib_1.fs.readJSONSync(json.file);
this.currentAsset.content = content;
this.render();
}
catch (e) {
if (e instanceof SyntaxError) {
// JSON文件语法异常的情况下初始化内容
this.showWarn("JSON文件内容初始化成功");
const content = { nodes: [] };
await lib_1.fs.writeJSONSync(json.file, content);
this.currentAsset.content = content;
this.render();
}
else {
// 输出其他异常
this.showWarn(e);
}
}
},
// 全部所有behaviorTree组件上使用到的json文件
async getAssets() {
const behaviorTreeComponentUuids = [];
const dfs = (node) => {
if (!node) {
return;
}
for (const comp of node.components) {
if (comp.type === BTreeCompName) {
behaviorTreeComponentUuids.push(comp.value);
break;
}
}
for (const item of node.children) {
dfs(item);
}
};
// 获取场景节点树
const sceneNode = await Editor.Message.request("scene", "query-node-tree");
// 收集场景上所有BehaviorTree组件uuid
dfs(sceneNode);
// 收集BehaviorTree组件上的json路径
const rawUrls = await Promise.all(behaviorTreeComponentUuids.map((uuid) => Editor.Message.request("scene", "execute-component-method", {
uuid: uuid,
name: "getAssetUrl",
})));
// 过滤空的并去重
const urls = [...new Set(rawUrls.filter(Boolean))];
// 根据url获取所有json文件信息
const assets = await Promise.all(urls.map((url) => Editor.Message.request("asset-db", "query-asset-info", url)));
this.assets = assets.map(({ name, source, file }) => ({ name, url: source, file: file, content: "" }));
},
async saveAsset() { async saveAsset() {
var _a; var _a;
const url = (_a = this.currentAsset) === null || _a === void 0 ? void 0 : _a.url; const file = (_a = this.currentAsset) === null || _a === void 0 ? void 0 : _a.file;
if (!url) { if (!file) {
this.showWarn("数据存储失败未指定JSON"); this.showWarn("数据存储失败未指定JSON");
return; return;
} }
const content = JSON.stringify(this.currentAsset.content, null, 2); const content = this.currentAsset.content;
// 修改json文件 // 修改json文件
await Editor.Message.request("asset-db", "save-asset", url, content); lib_1.fs.writeJSONSync(file, content);
},
initCanvas() {
// 初始化舞台
stage = new lib_1.Konva.Stage({
container: this.$refs.tree,
width: CANVAS_WIDTH,
height: CANVAS_HEIGHT,
});
// 基础层目前只有一层每层都会生成一个canvas
baseLayer = new lib_1.Konva.Layer();
// 背景组
bgGroup = this.generateBg();
baseLayer.add(bgGroup);
// 静态箭头层
staticArrowGroup = new lib_1.Konva.Group();
baseLayer.add(staticArrowGroup);
// 动态箭头层(用户手动拉出来的箭头)
dynamicArrowGroup = this.generateDynamicArrowGroup();
baseLayer.add(dynamicArrowGroup);
// Root组
rootGroup = this.generateRoot();
baseLayer.add(rootGroup);
// 节点层
nodeGroup = new lib_1.Konva.Group();
baseLayer.add(nodeGroup);
// 框选层
selectionGroup = this.generatesSelectionGroup();
baseLayer.add(selectionGroup);
// 层添加到舞台
stage.add(baseLayer);
}, },
/*** /***
* 渲染画布 * 渲染画布
@ -568,7 +603,7 @@ const component = lib_1.Vue.extend({
type, type,
abortType: this.AbortType.None, abortType: this.AbortType.None,
x: (CANVAS_WIDTH - BOX_WIDTH) / 2, x: (CANVAS_WIDTH - BOX_WIDTH) / 2,
y: CANVAS_HEIGHT / 4, y: ROOT_Y + 100,
isRoot: false, isRoot: false,
children: [], children: [],
event: { event: {
@ -828,7 +863,7 @@ const component = lib_1.Vue.extend({
if (e.evt.buttons === 4) { if (e.evt.buttons === 4) {
const movementX = e.evt.movementX; const movementX = e.evt.movementX;
const movementY = e.evt.movementY; const movementY = e.evt.movementY;
this.$refs.left.scrollBy(-movementX, -movementY); this.$refs.scroll.scrollBy(-movementX, -movementY);
return; return;
} }
if (!selection.visible()) { if (!selection.visible()) {
@ -981,11 +1016,11 @@ const component = lib_1.Vue.extend({
selectedBoxes = []; selectedBoxes = [];
this.currentNode = node; this.currentNode = node;
this.handlePanelChange(1); this.handlePanelChange(1);
this.render();
// 获取节点事件相关信息 // 获取节点事件相关信息
this.handleEventNodeChange(this.onStart, this.currentNode.event[this.onStart].node); this.handleEventNodeChange(this.onStart, this.currentNode.event[this.onStart].node, false);
this.handleEventNodeChange(this.onUpdate, this.currentNode.event[this.onUpdate].node); this.handleEventNodeChange(this.onUpdate, this.currentNode.event[this.onUpdate].node, false);
this.handleEventNodeChange(this.onEnd, this.currentNode.event[this.onEnd].node); this.handleEventNodeChange(this.onEnd, this.currentNode.event[this.onEnd].node, false);
this.render();
}); });
return wrapper; return wrapper;
}, },
@ -1247,6 +1282,12 @@ const component = lib_1.Vue.extend({
} }
return BOX_HEIGHT; return BOX_HEIGHT;
}, },
isComposite(node) {
if (!node) {
return false;
}
return CompositeNodes.includes(node.type);
},
showWarn(text) { showWarn(text) {
let time = getTime(); let time = getTime();
const content = `${time}${text}`; const content = `${time}${text}`;
@ -1256,6 +1297,11 @@ const component = lib_1.Vue.extend({
this.logs.push({ id: uuid(), content }); this.logs.push({ id: uuid(), content });
console.warn(content); console.warn(content);
}, },
// 视角回到root节点
backRoot() {
var _a, _b;
(_b = (_a = this.$refs) === null || _a === void 0 ? void 0 : _a.scroll) === null || _b === void 0 ? void 0 : _b.scroll((CANVAS_WIDTH - this.$refs.scroll.getBoundingClientRect().width) / 2, ROOT_Y - 50);
},
}, },
}); });
const panelDataMap = new WeakMap(); const panelDataMap = new WeakMap();
@ -1281,7 +1327,14 @@ module.exports = Editor.Panel.define({
data() { data() {
return {}; return {};
}, },
methods: {}, methods: {
initSelection() {
const vm = panelDataMap.get(this);
if (vm) {
vm.initSelection();
}
},
},
ready() { ready() {
if (this.$.app) { if (this.$.app) {
const vm = new component(); const vm = new component();

View File

@ -22,5 +22,5 @@ class Blackboard {
this.map.clear(); this.map.clear();
} }
} }
exports.Blackboard = Blackboard;
Blackboard.map = new Map(); Blackboard.map = new Map();
exports.Blackboard = Blackboard;

View File

@ -37,9 +37,14 @@
"openPanel" "openPanel"
] ]
}, },
"send-to-panel": { "refresh-panel": {
"methods": [ "methods": [
"default.hello" "refreshPanel"
]
},
"selection:select": {
"methods": [
"default.initSelection"
] ]
} }
}, },

View File

@ -3,8 +3,8 @@
type Selector<$> = { $: Record<keyof $, any | null> }; type Selector<$> = { $: Record<keyof $, any | null> };
export const template = ` export const template = `
<div style="display:flex;justify-content:center;align-items:center;margin-top:10px;padding-right:8px;"> <div style="display:flex;justify-content:center;align-items:center;margin-top:10px;">
<ui-button class="editor" style="height:24px;padding:0 16px;">Edit Tree</ui-prop> <ui-button class="editor" style="height:24px;padding:0 16px;">Create / Edit</ui-prop>
</div> </div>
`; `;
@ -69,13 +69,16 @@ export function ready(this: Selector<typeof $> & any) {
}); });
if (success) { if (success) {
console.log(`JSON文件挂载成功`); console.log(`JSON文件挂载成功`);
Editor.Message.request("behavior-eden", "open-panel");
} else { } else {
console.warn("JSON文件挂载失败"); console.warn("JSON文件挂载失败");
return;
}
} }
} else {
// 打开插件面板 // 打开插件面板
Editor.Message.request("behavior-eden", "open-panel"); const success = await Editor.Message.request("behavior-eden", "open-panel");
// success为false可能是已经打开了通知面板刷新
if (!success) {
await Editor.Message.request("behavior-eden", "refresh-panel");
} }
}); });
} }

View File

@ -6,8 +6,13 @@ import packageJSON from "../package.json";
* @zh * @zh
*/ */
export const methods: { [key: string]: (...any: any) => any } = { export const methods: { [key: string]: (...any: any) => any } = {
openPanel() { async openPanel() {
Editor.Panel.open(packageJSON.name); return await Editor.Panel.openBeside("scene", packageJSON.name);
},
async refreshPanel() {
await Editor.Panel.close(packageJSON.name);
await new Promise((rs) => setTimeout(rs, 300));
await Editor.Panel.openBeside("scene", packageJSON.name);
}, },
}; };

View File

@ -6,8 +6,8 @@ import { nodeClsMap } from "../../runtime/core/decorator";
import { buildTree, preOrder } from "../../runtime/core/utils"; import { buildTree, preOrder } from "../../runtime/core/utils";
// 画布宽高 // 画布宽高
const CANVAS_WIDTH = 2400; const CANVAS_WIDTH = 2000;
const CANVAS_HEIGHT = 1600; const CANVAS_HEIGHT = 1400;
// 节点box属性 // 节点box属性
const BOX_WIDTH = 100; const BOX_WIDTH = 100;
// const BOX_HEIGHT = 64; //height通过getBoxHeight方法动态计算 // const BOX_HEIGHT = 64; //height通过getBoxHeight方法动态计算
@ -149,6 +149,24 @@ const ParentNodes = [...nodeClsMap.values()]
}) })
.map((cls) => cls.name); .map((cls) => cls.name);
/***
* Composite类型的节点
*/
const CompositeNodes = [...nodeClsMap.values()]
.filter((cls) => {
let parentCls = getParentCls(cls);
while (parentCls) {
if (parentCls.name === Composite.name) {
return true;
}
parentCls = getParentCls(parentCls);
}
return false;
})
.map((cls) => cls.name);
const activeCursor = () => { const activeCursor = () => {
stage && (stage.container().style.cursor = "pointer"); stage && (stage.container().style.cursor = "pointer");
}; };
@ -160,7 +178,7 @@ const component = Vue.extend({
template: fs.readFileSync(path.join(__dirname, "../../../src/panels/static/template/vue/app.html"), "utf-8"), template: fs.readFileSync(path.join(__dirname, "../../../src/panels/static/template/vue/app.html"), "utf-8"),
$: { $: {
tree: "#tree", tree: "#tree",
left: "#left", scroll: "#scroll",
}, },
filters: { filters: {
toUpperCase(value) { toUpperCase(value) {
@ -199,6 +217,7 @@ const component = Vue.extend({
*/ */
nodeCompMethodInfo: {}, nodeCompMethodInfo: {},
logs: [], logs: [],
maskText: "",
}; };
}, },
computed: { computed: {
@ -267,12 +286,165 @@ const component = Vue.extend({
}, },
}, },
async mounted() { async mounted() {
this.initCanvas(); this.init();
await this.getAssets();
await this.selectCurrentAsset();
this.$refs.left.scroll((CANVAS_WIDTH - this.$refs.left.getBoundingClientRect().width) / 2, ROOT_Y - 50);
}, },
methods: { methods: {
/***
*
*/
async init() {
// 初始化画布
this.initCanvas();
// 根据当前选中节点初始化行为树
await this.initSelection();
this.backRoot();
},
initCanvas() {
// 初始化舞台
stage = new Konva.Stage({
container: this.$refs.tree,
width: CANVAS_WIDTH,
height: CANVAS_HEIGHT,
});
// 基础层目前只有一层每层都会生成一个canvas
baseLayer = new Konva.Layer();
// 背景组
bgGroup = this.generateBg();
baseLayer.add(bgGroup);
// 静态箭头层
staticArrowGroup = new Konva.Group();
baseLayer.add(staticArrowGroup);
// 动态箭头层(用户手动拉出来的箭头)
dynamicArrowGroup = this.generateDynamicArrowGroup();
baseLayer.add(dynamicArrowGroup);
// Root组
rootGroup = this.generateRoot();
baseLayer.add(rootGroup);
// 节点层
nodeGroup = new Konva.Group();
baseLayer.add(nodeGroup);
// 框选层
selectionGroup = this.generatesSelectionGroup();
baseLayer.add(selectionGroup);
// 层添加到舞台
stage.add(baseLayer);
},
// 获取json文件
async initAssets() {
const behaviorTreeComponentUuids = [];
const dfs = (node) => {
if (!node) {
return;
}
for (const comp of node.components) {
if (comp.type === BTreeCompName) {
behaviorTreeComponentUuids.push(comp.value);
break;
}
}
for (const item of node.children) {
dfs(item);
}
};
// 获取场景节点树
const sceneNode = await Editor.Message.request("scene", "query-node-tree");
// 收集场景上所有BehaviorTree组件uuid
dfs(sceneNode);
// 收集BehaviorTree组件上的json路径
const rawUrls = await Promise.all(
behaviorTreeComponentUuids.map((uuid) =>
Editor.Message.request("scene", "execute-component-method", {
uuid: uuid,
name: "getAssetUrl",
})
)
);
// 过滤空的并去重
const urls = [...new Set(rawUrls.filter(Boolean))];
// 根据url获取所有json文件信息
const assets = await Promise.all(urls.map((url) => Editor.Message.request("asset-db", "query-asset-info", url)));
this.assets = assets.map(({ name, source, file }) => ({ name, url: source, file: file, content: "" }));
},
async initSelection() {
// 找到当前选中的节点
const node = await Editor.Message.request("scene", "query-node", Editor.Selection.getSelected("node"));
// 未选中节点或者选中的是场景节点(场景节点不能添加组件)
if (!node?.__comps__) {
this.maskText = "请选中一个非场景根节点非空名称节点来开始行为树制作";
return;
}
// 找到BehaviorTree组件
const index = node.__comps__.findIndex((v: any) => v.type === BTreeCompName);
if (index === -1) {
this.maskText = "要制作行为树,需要先为当前节点添加行为树组件";
return;
}
const comp = node.__comps__[index];
// 调用BehaviorTree组件上的方法
const url = await Editor.Message.request("scene", "execute-component-method", {
uuid: comp.value.uuid.value,
name: "getAssetUrl",
});
// JSON文件不存在
if (!url) {
this.maskText = "当前行为树组件缺少JSON资源点击BehaviorEditor组件按钮即可创建";
return;
}
this.maskText = "";
// 选中此文件设置好currentAsset才能把组件nodes数据同步到JSON
this.handleSelectAsset(url);
},
async handleSelectAsset(url) {
// 实时获取当前场景所有behaviorTree组件上的json文件
await this.initAssets();
const json = this.assets.find((e) => e.url === url);
if (!json) {
this.showWarn("JSON文件不存在");
return;
}
if (this.currentAsset?.url === json?.url) {
return;
}
this.currentAsset = json;
try {
const content = await fs.readJSONSync(json.file);
this.currentAsset.content = content;
this.render();
} catch (e) {
if (e instanceof SyntaxError) {
// JSON文件语法异常的情况下初始化内容
this.showWarn("JSON文件内容初始化成功");
const content = { nodes: [] };
await fs.writeJSONSync(json.file, content);
this.currentAsset.content = content;
this.render();
} else {
// 输出其他异常
this.showWarn(e);
}
}
},
handlePanelChange(value) { handlePanelChange(value) {
this.currentPanel = value; this.currentPanel = value;
}, },
@ -289,7 +461,7 @@ const component = Vue.extend({
this.render(); this.render();
} }
}, },
async handleEventNodeChange(lifeCycle, uuid = "") { async handleEventNodeChange(lifeCycle, uuid = "", shouldSave = true) {
if (!this.currentNode) { if (!this.currentNode) {
this.showWarn("当前节点不存在"); this.showWarn("当前节点不存在");
return; return;
@ -301,9 +473,13 @@ const component = Vue.extend({
} else { } else {
this.currentNode.event[lifeCycle].comp = ""; this.currentNode.event[lifeCycle].comp = "";
this.currentNode.event[lifeCycle].method = ""; this.currentNode.event[lifeCycle].method = "";
this.currentNode.event[lifeCycle].data = ""; // 事件参数就不手动清空了
// this.currentNode.event[lifeCycle].data = "";
} }
// 点击canvas某个节点的时候会触发handleEventNodeChange获取场景节点数据此时不保存数据
if (shouldSave) {
await this.saveAsset(); await this.saveAsset();
}
}, },
async getNodeCompMethods(lifeCycle, uuid) { async getNodeCompMethods(lifeCycle, uuid) {
const [nodeInfo, compMethodInfo] = await Promise.all([ const [nodeInfo, compMethodInfo] = await Promise.all([
@ -348,151 +524,18 @@ const component = Vue.extend({
this.currentNode.event[lifeCycle].data = data; this.currentNode.event[lifeCycle].data = data;
await this.saveAsset(); await this.saveAsset();
}, },
async selectCurrentAsset() {
// 找到点击组件的节点
const node = await Editor.Message.request("scene", "query-node", Editor.Selection.getSelected("node"));
if (!node) {
this.showWarn(`未选中节点`);
return;
}
// 找到BehaviorTree组件
const index = node.__comps__.findIndex((v: any) => v.type === BTreeCompName);
if (index === -1) {
this.showWarn(`节点未挂载【${BTreeCompName}】组件`);
return;
}
const comp = node.__comps__[index];
// 调用BehaviorTree组件上的方法
const url = await Editor.Message.request("scene", "execute-component-method", {
uuid: comp.value.uuid.value,
name: "getAssetUrl",
});
// JSON文件不存在
if (!url) {
this.showWarn(`${BTreeCompName}】组件未指定JSON文件`);
return;
}
// 选中此文件设置好currentAsset才能把组件nodes数据同步到JSON
this.handleSelectAsset(url);
},
async handleSelectAsset(url) {
const json = this.assets.find((e) => e.url === url);
if (!json) {
this.showWarn("JSON文件不存在");
return;
}
this.currentAsset = json;
try {
const content = await fs.readJSONSync(json.file);
this.currentAsset.content = content;
this.render();
} catch (e) {
if (e instanceof SyntaxError) {
// JSON文件语法异常的情况下初始化内容
this.showWarn("JSON文件内容初始化成功");
const content = { nodes: [] };
await fs.writeJSONSync(json.file, content);
this.currentAsset.content = content;
this.render();
} else {
// 输出其他异常
this.showWarn(e);
}
}
},
// 全部所有behaviorTree组件上使用到的json文件
async getAssets() {
const behaviorTreeComponentUuids = [];
const dfs = (node) => {
if (!node) {
return;
}
for (const comp of node.components) {
if (comp.type === BTreeCompName) {
behaviorTreeComponentUuids.push(comp.value);
break;
}
}
for (const item of node.children) {
dfs(item);
}
};
// 获取场景节点树
const sceneNode = await Editor.Message.request("scene", "query-node-tree");
// 收集场景上所有BehaviorTree组件uuid
dfs(sceneNode);
// 收集BehaviorTree组件上的json路径
const rawUrls = await Promise.all(
behaviorTreeComponentUuids.map((uuid) =>
Editor.Message.request("scene", "execute-component-method", {
uuid: uuid,
name: "getAssetUrl",
})
)
);
// 过滤空的并去重
const urls = [...new Set(rawUrls.filter(Boolean))];
// 根据url获取所有json文件信息
const assets = await Promise.all(urls.map((url) => Editor.Message.request("asset-db", "query-asset-info", url)));
this.assets = assets.map(({ name, source, file }) => ({ name, url: source, file: file, content: "" }));
},
async saveAsset() { async saveAsset() {
const url = this.currentAsset?.url; const file = this.currentAsset?.file;
if (!url) { if (!file) {
this.showWarn("数据存储失败未指定JSON"); this.showWarn("数据存储失败未指定JSON");
return; return;
} }
const content = JSON.stringify(this.currentAsset.content, null, 2); const content = this.currentAsset.content;
// 修改json文件 // 修改json文件
await Editor.Message.request("asset-db", "save-asset", url, content); fs.writeJSONSync(file, content);
}, },
initCanvas() {
// 初始化舞台
stage = new Konva.Stage({
container: this.$refs.tree,
width: CANVAS_WIDTH,
height: CANVAS_HEIGHT,
});
// 基础层目前只有一层每层都会生成一个canvas
baseLayer = new Konva.Layer();
// 背景组
bgGroup = this.generateBg();
baseLayer.add(bgGroup);
// 静态箭头层
staticArrowGroup = new Konva.Group();
baseLayer.add(staticArrowGroup);
// 动态箭头层(用户手动拉出来的箭头)
dynamicArrowGroup = this.generateDynamicArrowGroup();
baseLayer.add(dynamicArrowGroup);
// Root组
rootGroup = this.generateRoot();
baseLayer.add(rootGroup);
// 节点层
nodeGroup = new Konva.Group();
baseLayer.add(nodeGroup);
// 框选层
selectionGroup = this.generatesSelectionGroup();
baseLayer.add(selectionGroup);
// 层添加到舞台
stage.add(baseLayer);
},
/*** /***
* *
* renderNode * renderNode
@ -616,7 +659,7 @@ const component = Vue.extend({
type, type,
abortType: this.AbortType.None, abortType: this.AbortType.None,
x: (CANVAS_WIDTH - BOX_WIDTH) / 2, x: (CANVAS_WIDTH - BOX_WIDTH) / 2,
y: CANVAS_HEIGHT / 4, y: ROOT_Y + 100,
isRoot: false, isRoot: false,
children: [], children: [],
event: { event: {
@ -895,7 +938,7 @@ const component = Vue.extend({
if (e.evt.buttons === 4) { if (e.evt.buttons === 4) {
const movementX = e.evt.movementX; const movementX = e.evt.movementX;
const movementY = e.evt.movementY; const movementY = e.evt.movementY;
this.$refs.left.scrollBy(-movementX, -movementY); this.$refs.scroll.scrollBy(-movementX, -movementY);
return; return;
} }
if (!selection.visible()) { if (!selection.visible()) {
@ -1063,11 +1106,11 @@ const component = Vue.extend({
selectedBoxes = []; selectedBoxes = [];
this.currentNode = node; this.currentNode = node;
this.handlePanelChange(1); this.handlePanelChange(1);
this.render();
// 获取节点事件相关信息 // 获取节点事件相关信息
this.handleEventNodeChange(this.onStart, this.currentNode.event[this.onStart].node); this.handleEventNodeChange(this.onStart, this.currentNode.event[this.onStart].node, false);
this.handleEventNodeChange(this.onUpdate, this.currentNode.event[this.onUpdate].node); this.handleEventNodeChange(this.onUpdate, this.currentNode.event[this.onUpdate].node, false);
this.handleEventNodeChange(this.onEnd, this.currentNode.event[this.onEnd].node); this.handleEventNodeChange(this.onEnd, this.currentNode.event[this.onEnd].node, false);
this.render();
}); });
return wrapper; return wrapper;
@ -1349,6 +1392,13 @@ const component = Vue.extend({
return BOX_HEIGHT; return BOX_HEIGHT;
}, },
isComposite(node) {
if (!node) {
return false;
}
return CompositeNodes.includes(node.type);
},
showWarn(text) { showWarn(text) {
let time = getTime(); let time = getTime();
const content = `${time}${text}`; const content = `${time}${text}`;
@ -1358,6 +1408,10 @@ const component = Vue.extend({
this.logs.push({ id: uuid(), content }); this.logs.push({ id: uuid(), content });
console.warn(content); console.warn(content);
}, },
// 视角回到root节点
backRoot() {
this.$refs?.scroll?.scroll((CANVAS_WIDTH - this.$refs.scroll.getBoundingClientRect().width) / 2, ROOT_Y - 50);
},
}, },
}); });
const panelDataMap = new WeakMap() as WeakMap<object, InstanceType<typeof component>>; const panelDataMap = new WeakMap() as WeakMap<object, InstanceType<typeof component>>;
@ -1383,7 +1437,14 @@ module.exports = Editor.Panel.define({
data() { data() {
return {}; return {};
}, },
methods: {}, methods: {
initSelection() {
const vm = panelDataMap.get(this);
if (vm) {
vm.initSelection();
}
},
},
ready() { ready() {
if (this.$.app) { if (this.$.app) {
const vm = new component(); const vm = new component();

View File

@ -1,7 +1,8 @@
.wrapper { .wrapper {
display: flex; display: flex;
background-color: #2b2b2b; background-color: #2b2b2b;
max-height: calc(100vh - 26px); width: 100%;
height: 100%;
font-size: 12px; font-size: 12px;
color: #cccccc; color: #cccccc;
font-family: BlinkMacSystemFont, "PingFang SC", system-ui, -apple-system, Helvetica Neue, Helvetica, sans-serif; font-family: BlinkMacSystemFont, "PingFang SC", system-ui, -apple-system, Helvetica Neue, Helvetica, sans-serif;
@ -15,12 +16,24 @@ ui-prop[no-label] {
padding: 0px 4px 0 4px; padding: 0px 4px 0 4px;
} }
.node-type {
padding-right: 8px;
}
.life-cycle-input {
margin-top: 4px;
}
.right { .right {
width: 280px; width: 260px;
border-left: 1px solid #050505; border-left: 2px solid #050505;
flex-shrink: 0; flex-shrink: 0;
} }
.right > div {
padding-right: 2px;
}
.node-panel { .node-panel {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -29,25 +42,41 @@ ui-prop[no-label] {
.left { .left {
position: relative; position: relative;
overflow: scroll; width: calc(100% - 260px);
display: flex;
}
.scroll {
overflow: auto;
} }
.fix-left { .fix-left {
position: fixed; position: absolute;
left: 13px; left: 14px;
top: 34px; top: 14px;
z-index: 1; z-index: 1;
display: flex; display: flex;
} }
.fix-right { .fix-right {
position: fixed; position: absolute;
right: 300px; right: 14px;
top: 34px; top: 14px;
z-index: 1; z-index: 1;
display: flex; display: flex;
} }
.fix-bottom {
position: absolute;
left: 14px;
bottom: 24px;
z-index: 1;
}
.fix-bottom ui-button {
padding: 0 12px;
}
.asset { .asset {
} }
@ -55,7 +84,7 @@ ui-prop[no-label] {
color: #e8b116; color: #e8b116;
min-width: 200px; min-width: 200px;
max-width: 500px; max-width: 500px;
font-weight: bold; /* font-weight: bold; */
} }
.log div { .log div {
@ -64,6 +93,10 @@ ui-prop[no-label] {
white-space: nowrap; white-space: nowrap;
} }
.log div:first-child {
padding-top: 8px;
}
.flex { .flex {
display: flex; display: flex;
} }
@ -102,10 +135,22 @@ ui-prop[no-label] {
margin-top: 4px; margin-top: 4px;
} }
.mt-12 {
margin-top: 12px;
}
.mr-4 {
margin-right: 4px;
}
.mr-6 { .mr-6 {
margin-right: 6px; margin-right: 6px;
} }
.mr-8 {
margin-right: 8px;
}
.grow { .grow {
flex-grow: 1; flex-grow: 1;
} }
@ -113,3 +158,16 @@ ui-prop[no-label] {
.flex-1 { .flex-1 {
flex: 1 1 0%; flex: 1 1 0%;
} }
.mask {
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
z-index: 999;
background-color: rgba(2, 2, 2, 0.6);
display: flex;
justify-content: center;
align-items: center;
}

View File

@ -1,3 +1,3 @@
<div> <div style="width: 100%; height: 100%" id="wrapper">
<div id="app"></div> <div id="app"></div>
</div> </div>

View File

@ -1,5 +1,9 @@
<div class="wrapper"> <div class="wrapper">
<div class="left" ref="left"> <div class="mask" v-if="maskText">{{maskText}}</div>
<div class="left">
<div class="scroll" ref="scroll">
<div id="tree" ref="tree" />
</div>
<div class="fix-left"> <div class="fix-left">
<div class="log"> <div class="log">
<ui-section header="Logs" class="config" expand> <ui-section header="Logs" class="config" expand>
@ -14,7 +18,9 @@
</ui-select> </ui-select>
</div> </div>
</div> </div>
<div id="tree" ref="tree" /> <div class="fix-bottom" @click="backRoot">
<ui-button>Root</ui-button>
</div>
</div> </div>
<div class="right"> <div class="right">
@ -24,7 +30,7 @@
<div v-if="currentPanel === 0" class="node-panel"> <div v-if="currentPanel === 0" class="node-panel">
<div v-for="category of nodeCategory" class="node-type"> <div v-for="category of nodeCategory" class="node-type">
<ui-section :header="category.id" :expand="category.expand"> <ui-section :header="category.id" :expand="category.expand">
<ui-button class="mt-4 mr-6" v-for="item of category.items" :key="item.type" @click="addNode(item.type)" <ui-button class="mt-4" v-for="item of category.items" :key="item.type" @click="addNode(item.type)"
>{{ item.type }}</ui-button >{{ item.type }}</ui-button
> >
</ui-section> </ui-section>
@ -53,6 +59,7 @@
<ui-label slot="label">Abort Type</ui-label> <ui-label slot="label">Abort Type</ui-label>
<ui-select <ui-select
slot="content" slot="content"
:disabled="!isComposite(currentNode)"
:value="currentNode.abortType" :value="currentNode.abortType"
@change="handleAbortTypeChange($event.target.value)" @change="handleAbortTypeChange($event.target.value)"
> >
@ -93,9 +100,10 @@
</ui-select> </ui-select>
</div> </div>
</ui-prop> </ui-prop>
<ui-prop message="EventData"> <ui-prop no-label>
<ui-input <ui-input
slot="content" slot="content"
class="life-cycle-input"
:value="currentNode.event[onStart].data" :value="currentNode.event[onStart].data"
@input="handleEventDataChange(onStart, $event.target.value)" @input="handleEventDataChange(onStart, $event.target.value)"
></ui-input> ></ui-input>
@ -107,7 +115,7 @@
<ui-label slot="label">{{onUpdate | toUpperCase}}</ui-label> <ui-label slot="label">{{onUpdate | toUpperCase}}</ui-label>
</ui-prop> </ui-prop>
<ui-prop no-label> <ui-prop no-label>
<div slot="content" class="flex items-center"> <div slot="content" class="flex items-center gap-4">
<ui-node <ui-node
class="flex-1" class="flex-1"
droppable="cc.Node" droppable="cc.Node"
@ -134,9 +142,10 @@
</ui-select> </ui-select>
</div> </div>
</ui-prop> </ui-prop>
<ui-prop message="EventData"> <ui-prop no-label>
<ui-input <ui-input
slot="content" slot="content"
class="life-cycle-input"
:value="currentNode.event[onUpdate].data" :value="currentNode.event[onUpdate].data"
@input="handleEventDataChange(onUpdate, $event.target.value)" @input="handleEventDataChange(onUpdate, $event.target.value)"
></ui-input> ></ui-input>
@ -148,7 +157,7 @@
<ui-label slot="label">{{onEnd | toUpperCase}}</ui-label> <ui-label slot="label">{{onEnd | toUpperCase}}</ui-label>
</ui-prop> </ui-prop>
<ui-prop no-label> <ui-prop no-label>
<div slot="content" class="flex items-center"> <div slot="content" class="flex items-center gap-4">
<ui-node <ui-node
class="flex-1" class="flex-1"
droppable="cc.Node" droppable="cc.Node"
@ -175,9 +184,10 @@
</ui-select> </ui-select>
</div> </div>
</ui-prop> </ui-prop>
<ui-prop message="EventData"> <ui-prop no-label>
<ui-input <ui-input
slot="content" slot="content"
class="life-cycle-input"
:value="currentNode.event[onEnd].data" :value="currentNode.event[onEnd].data"
@input="handleEventDataChange(onEnd, $event.target.value)" @input="handleEventDataChange(onEnd, $event.target.value)"
></ui-input> ></ui-input>
@ -186,7 +196,7 @@
</div> </div>
</div> </div>
</div> </div>
<div v-else class="flex justify-center mt-4">请选择节点</div> <div v-else class="flex justify-center mt-12">Please select a node</div>
</div> </div>
</div> </div>
</div> </div>