refactor: reorganize package structure and decouple framework packages (#338)
* refactor: reorganize package structure and decouple framework packages ## Package Structure Reorganization - Reorganized 55 packages into categorized subdirectories: - packages/framework/ - Generic framework (Laya/Cocos compatible) - packages/engine/ - ESEngine core modules - packages/rendering/ - Rendering modules (WASM dependent) - packages/physics/ - Physics modules - packages/streaming/ - World streaming - packages/network-ext/ - Network extensions - packages/editor/ - Editor framework and plugins - packages/rust/ - Rust WASM engine - packages/tools/ - Build tools and SDK ## Framework Package Decoupling - Decoupled behavior-tree and blueprint packages from ESEngine dependencies - Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent) - ESEngine-specific code moved to esengine/ subpath exports - Framework packages now usable with Cocos/Laya without ESEngine ## CI Configuration - Updated CI to only type-check and lint framework packages - Added type-check:framework and lint:framework scripts ## Breaking Changes - Package import paths changed due to directory reorganization - ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine') * fix: update es-engine file path after directory reorganization * docs: update README to focus on framework over engine * ci: only build framework packages, remove Rust/WASM dependencies * fix: remove esengine subpath from behavior-tree and blueprint builds ESEngine integration code will only be available in full engine builds. Framework packages are now purely engine-agnostic. * fix: move network-protocols to framework, build both in CI * fix: update workflow paths from packages/core to packages/framework/core * fix: exclude esengine folder from type-check in behavior-tree and blueprint * fix: update network tsconfig references to new paths * fix: add test:ci:framework to only test framework packages in CI * fix: only build core and math npm packages in CI * fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
129
packages/framework/behavior-tree/build-rollup.cjs
Normal file
129
packages/framework/behavior-tree/build-rollup.cjs
Normal file
@@ -0,0 +1,129 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
console.log('🚀 使用 Rollup 构建npm包...');
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// 清理旧的dist目录
|
||||
if (fs.existsSync('./dist')) {
|
||||
console.log('🧹 清理旧的构建文件...');
|
||||
execSync('rimraf ./dist', { stdio: 'inherit' });
|
||||
}
|
||||
|
||||
// 执行Rollup构建
|
||||
console.log('📦 执行 Rollup 构建...');
|
||||
execSync('npx rollup -c rollup.config.cjs', { stdio: 'inherit' });
|
||||
|
||||
// 生成package.json
|
||||
console.log('📋 生成 package.json...');
|
||||
generatePackageJson();
|
||||
|
||||
// 复制其他文件
|
||||
console.log('📁 复制必要文件...');
|
||||
copyFiles();
|
||||
|
||||
// 输出构建结果
|
||||
showBuildResults();
|
||||
|
||||
console.log('✅ 构建完成!');
|
||||
console.log('\n🚀 发布命令:');
|
||||
console.log('cd dist && npm publish');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 构建失败:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function generatePackageJson() {
|
||||
const sourcePackage = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
||||
|
||||
const distPackage = {
|
||||
name: sourcePackage.name,
|
||||
version: sourcePackage.version,
|
||||
description: sourcePackage.description,
|
||||
main: 'index.cjs',
|
||||
module: 'index.mjs',
|
||||
unpkg: 'index.umd.js',
|
||||
types: 'index.d.ts',
|
||||
exports: {
|
||||
'.': {
|
||||
import: './index.mjs',
|
||||
require: './index.cjs',
|
||||
types: './index.d.ts'
|
||||
}
|
||||
},
|
||||
files: [
|
||||
'index.mjs',
|
||||
'index.mjs.map',
|
||||
'index.cjs',
|
||||
'index.cjs.map',
|
||||
'index.umd.js',
|
||||
'index.umd.js.map',
|
||||
'index.es5.js',
|
||||
'index.es5.js.map',
|
||||
'index.d.ts'
|
||||
],
|
||||
keywords: [
|
||||
'ecs',
|
||||
'behavior-tree',
|
||||
'ai',
|
||||
'game-ai',
|
||||
'entity-component-system',
|
||||
'typescript',
|
||||
'cocos-creator',
|
||||
'laya',
|
||||
'rollup'
|
||||
],
|
||||
author: sourcePackage.author,
|
||||
license: sourcePackage.license,
|
||||
repository: sourcePackage.repository,
|
||||
bugs: sourcePackage.bugs,
|
||||
homepage: sourcePackage.homepage,
|
||||
peerDependencies: sourcePackage.peerDependencies,
|
||||
publishConfig: sourcePackage.publishConfig,
|
||||
engines: {
|
||||
node: '>=16.0.0'
|
||||
},
|
||||
sideEffects: false
|
||||
};
|
||||
|
||||
fs.writeFileSync('./dist/package.json', JSON.stringify(distPackage, null, 2));
|
||||
}
|
||||
|
||||
function copyFiles() {
|
||||
const filesToCopy = [
|
||||
// 如果有 README 或其他文件需要复制,可以在这里添加
|
||||
];
|
||||
|
||||
filesToCopy.forEach(({ src, dest }) => {
|
||||
if (fs.existsSync(src)) {
|
||||
fs.copyFileSync(src, dest);
|
||||
console.log(` ✓ 复制: ${path.basename(dest)}`);
|
||||
} else {
|
||||
console.log(` ⚠️ 文件不存在: ${src}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (filesToCopy.length === 0) {
|
||||
console.log(' ℹ️ 没有需要复制的文件');
|
||||
}
|
||||
}
|
||||
|
||||
function showBuildResults() {
|
||||
const distDir = './dist';
|
||||
const files = ['index.mjs', 'index.cjs', 'index.umd.js', 'index.es5.js', 'index.d.ts'];
|
||||
|
||||
console.log('\n📊 构建结果:');
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(distDir, file);
|
||||
if (fs.existsSync(filePath)) {
|
||||
const size = fs.statSync(filePath).size;
|
||||
console.log(` ${file}: ${(size / 1024).toFixed(1)}KB`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
16
packages/framework/behavior-tree/jest.config.cjs
Normal file
16
packages/framework/behavior-tree/jest.config.cjs
Normal file
@@ -0,0 +1,16 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/src'],
|
||||
testMatch: ['**/*.test.ts'],
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.ts',
|
||||
'!src/**/*.d.ts',
|
||||
'!src/index.ts'
|
||||
],
|
||||
coverageDirectory: 'coverage',
|
||||
verbose: true,
|
||||
testTimeout: 10000,
|
||||
passWithNoTests: true
|
||||
};
|
||||
52
packages/framework/behavior-tree/module.json
Normal file
52
packages/framework/behavior-tree/module.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"id": "behavior-tree",
|
||||
"name": "@esengine/behavior-tree",
|
||||
"globalKey": "behaviorTree",
|
||||
"displayName": "Behavior Tree",
|
||||
"description": "AI behavior tree system | AI 行为树系统",
|
||||
"version": "1.0.0",
|
||||
"category": "AI",
|
||||
"icon": "GitBranch",
|
||||
"tags": [
|
||||
"ai",
|
||||
"behavior",
|
||||
"tree"
|
||||
],
|
||||
"isCore": false,
|
||||
"defaultEnabled": false,
|
||||
"isEngineModule": true,
|
||||
"canContainContent": true,
|
||||
"platforms": [
|
||||
"web",
|
||||
"desktop"
|
||||
],
|
||||
"dependencies": [
|
||||
"core"
|
||||
],
|
||||
"exports": {
|
||||
"components": [
|
||||
"BehaviorTreeComponent"
|
||||
],
|
||||
"systems": [
|
||||
"BehaviorTreeSystem"
|
||||
],
|
||||
"loaders": [
|
||||
"BehaviorTreeLoader"
|
||||
],
|
||||
"other": [
|
||||
"BehaviorTree",
|
||||
"BTNode",
|
||||
"Selector",
|
||||
"Sequence",
|
||||
"Condition",
|
||||
"Action"
|
||||
]
|
||||
},
|
||||
"assetExtensions": {
|
||||
".btree": "behavior-tree"
|
||||
},
|
||||
"editorPackage": "@esengine/behavior-tree-editor",
|
||||
"requiresWasm": false,
|
||||
"outputPath": "dist/index.js",
|
||||
"pluginExport": "BehaviorTreePlugin"
|
||||
}
|
||||
62
packages/framework/behavior-tree/package.json
Normal file
62
packages/framework/behavior-tree/package.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "@esengine/behavior-tree",
|
||||
"version": "1.0.1",
|
||||
"description": "ECS-based AI behavior tree system - works with any ECS framework (ESEngine, Cocos, Laya, etc.)",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"behavior-tree",
|
||||
"ai",
|
||||
"game-ai",
|
||||
"entity-component-system",
|
||||
"cocos",
|
||||
"laya",
|
||||
"esengine"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf dist tsconfig.tsbuildinfo",
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^20.19.17",
|
||||
"jest": "^29.7.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"ts-jest": "^29.4.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/esengine/esengine.git",
|
||||
"directory": "packages/framework/behavior-tree"
|
||||
}
|
||||
}
|
||||
30
packages/framework/behavior-tree/plugin.json
Normal file
30
packages/framework/behavior-tree/plugin.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"id": "@esengine/behavior-tree",
|
||||
"name": "Behavior Tree System",
|
||||
"version": "1.0.0",
|
||||
"description": "AI behavior tree system with visual editor and runtime execution",
|
||||
"category": "ai",
|
||||
"loadingPhase": "default",
|
||||
"enabledByDefault": true,
|
||||
"canContainContent": false,
|
||||
"isEnginePlugin": false,
|
||||
"modules": [
|
||||
{
|
||||
"name": "BehaviorTreeRuntime",
|
||||
"type": "runtime",
|
||||
"entry": "./src/index.ts"
|
||||
},
|
||||
{
|
||||
"name": "BehaviorTreeEditor",
|
||||
"type": "editor",
|
||||
"entry": "./src/editor/index.ts"
|
||||
}
|
||||
],
|
||||
"dependencies": [
|
||||
{
|
||||
"id": "@esengine/core",
|
||||
"version": ">=1.0.0"
|
||||
}
|
||||
],
|
||||
"icon": "GitBranch"
|
||||
}
|
||||
4312
packages/framework/behavior-tree/pnpm-lock.yaml
generated
Normal file
4312
packages/framework/behavior-tree/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
186
packages/framework/behavior-tree/rollup.config.cjs
Normal file
186
packages/framework/behavior-tree/rollup.config.cjs
Normal file
@@ -0,0 +1,186 @@
|
||||
const resolve = require('@rollup/plugin-node-resolve');
|
||||
const commonjs = require('@rollup/plugin-commonjs');
|
||||
const terser = require('@rollup/plugin-terser');
|
||||
const babel = require('@rollup/plugin-babel');
|
||||
const dts = require('rollup-plugin-dts').default;
|
||||
const { readFileSync } = require('fs');
|
||||
|
||||
const pkg = JSON.parse(readFileSync('./package.json', 'utf8'));
|
||||
|
||||
const banner = `/**
|
||||
* @esengine/behavior-tree v${pkg.version}
|
||||
* 完全ECS化的行为树系统
|
||||
*
|
||||
* @author ${pkg.author}
|
||||
* @license ${pkg.license}
|
||||
*/`;
|
||||
|
||||
const external = ['@esengine/ecs-framework'];
|
||||
|
||||
const commonPlugins = [
|
||||
resolve({
|
||||
browser: true,
|
||||
preferBuiltins: false
|
||||
}),
|
||||
commonjs({
|
||||
include: /node_modules/
|
||||
}),
|
||||
babel({
|
||||
babelHelpers: 'bundled',
|
||||
exclude: 'node_modules/**',
|
||||
extensions: ['.js', '.ts']
|
||||
})
|
||||
];
|
||||
|
||||
module.exports = [
|
||||
// ES模块构建
|
||||
{
|
||||
input: 'bin/index.js',
|
||||
output: {
|
||||
file: 'dist/index.mjs',
|
||||
format: 'es',
|
||||
banner,
|
||||
sourcemap: true,
|
||||
exports: 'named'
|
||||
},
|
||||
plugins: [
|
||||
...commonPlugins,
|
||||
terser({
|
||||
format: {
|
||||
comments: /^!/
|
||||
}
|
||||
})
|
||||
],
|
||||
external,
|
||||
treeshake: {
|
||||
moduleSideEffects: false,
|
||||
propertyReadSideEffects: false,
|
||||
unknownGlobalSideEffects: false
|
||||
}
|
||||
},
|
||||
|
||||
// CommonJS构建
|
||||
{
|
||||
input: 'bin/index.js',
|
||||
output: {
|
||||
file: 'dist/index.cjs',
|
||||
format: 'cjs',
|
||||
banner,
|
||||
sourcemap: true,
|
||||
exports: 'named'
|
||||
},
|
||||
plugins: [
|
||||
...commonPlugins,
|
||||
terser({
|
||||
format: {
|
||||
comments: /^!/
|
||||
}
|
||||
})
|
||||
],
|
||||
external,
|
||||
treeshake: {
|
||||
moduleSideEffects: false
|
||||
}
|
||||
},
|
||||
|
||||
// UMD构建
|
||||
{
|
||||
input: 'bin/index.js',
|
||||
output: {
|
||||
file: 'dist/index.umd.js',
|
||||
format: 'umd',
|
||||
name: 'BehaviorTree',
|
||||
banner,
|
||||
sourcemap: true,
|
||||
exports: 'named',
|
||||
globals: {
|
||||
'@esengine/ecs-framework': 'ECS'
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
...commonPlugins,
|
||||
terser({
|
||||
format: {
|
||||
comments: /^!/
|
||||
}
|
||||
})
|
||||
],
|
||||
external,
|
||||
treeshake: {
|
||||
moduleSideEffects: false
|
||||
}
|
||||
},
|
||||
|
||||
// ES5兼容构建
|
||||
{
|
||||
input: 'bin/index.js',
|
||||
output: {
|
||||
file: 'dist/index.es5.js',
|
||||
format: 'cjs',
|
||||
banner: banner + '\n// ES5 Compatible Build for legacy JavaScript environments',
|
||||
sourcemap: true,
|
||||
exports: 'named'
|
||||
},
|
||||
plugins: [
|
||||
resolve({
|
||||
browser: true,
|
||||
preferBuiltins: false
|
||||
}),
|
||||
commonjs({
|
||||
include: /node_modules/
|
||||
}),
|
||||
babel({
|
||||
babelHelpers: 'bundled',
|
||||
exclude: 'node_modules/**',
|
||||
extensions: ['.js', '.ts'],
|
||||
presets: [
|
||||
['@babel/preset-env', {
|
||||
targets: {
|
||||
browsers: ['ie >= 9', 'chrome >= 30', 'firefox >= 30', 'safari >= 8']
|
||||
},
|
||||
modules: false,
|
||||
loose: true,
|
||||
forceAllTransforms: true
|
||||
}]
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-transform-optional-chaining',
|
||||
'@babel/plugin-transform-nullish-coalescing-operator'
|
||||
]
|
||||
}),
|
||||
terser({
|
||||
ecma: 5,
|
||||
format: {
|
||||
comments: /^!/
|
||||
},
|
||||
compress: {
|
||||
drop_console: false,
|
||||
drop_debugger: false
|
||||
}
|
||||
})
|
||||
],
|
||||
external,
|
||||
treeshake: {
|
||||
moduleSideEffects: false
|
||||
}
|
||||
},
|
||||
|
||||
// 类型定义构建
|
||||
{
|
||||
input: 'bin/index.d.ts',
|
||||
output: {
|
||||
file: 'dist/index.d.ts',
|
||||
format: 'es',
|
||||
banner: `/**
|
||||
* @esengine/behavior-tree v${pkg.version}
|
||||
* TypeScript definitions
|
||||
*/`
|
||||
},
|
||||
plugins: [
|
||||
dts({
|
||||
respectExternal: true
|
||||
})
|
||||
],
|
||||
external: ['@esengine/ecs-framework']
|
||||
}
|
||||
];
|
||||
357
packages/framework/behavior-tree/src/BehaviorTreeBuilder.ts
Normal file
357
packages/framework/behavior-tree/src/BehaviorTreeBuilder.ts
Normal file
@@ -0,0 +1,357 @@
|
||||
import { BehaviorTreeData, BehaviorNodeData } from './execution/BehaviorTreeData';
|
||||
import { NodeType } from './Types/TaskStatus';
|
||||
|
||||
/**
|
||||
* 行为树构建器
|
||||
*
|
||||
* 提供流式API构建行为树数据结构
|
||||
*/
|
||||
export class BehaviorTreeBuilder {
|
||||
private treeData: BehaviorTreeData;
|
||||
private nodeStack: string[] = [];
|
||||
private nodeIdCounter: number = 0;
|
||||
|
||||
private constructor(treeName: string) {
|
||||
this.treeData = {
|
||||
id: `tree_${Date.now()}`,
|
||||
name: treeName,
|
||||
rootNodeId: '',
|
||||
nodes: new Map(),
|
||||
blackboardVariables: new Map()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建构建器
|
||||
*/
|
||||
static create(treeName: string = 'BehaviorTree'): BehaviorTreeBuilder {
|
||||
return new BehaviorTreeBuilder(treeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义黑板变量
|
||||
*/
|
||||
defineBlackboardVariable(key: string, initialValue: any): BehaviorTreeBuilder {
|
||||
if (!this.treeData.blackboardVariables) {
|
||||
this.treeData.blackboardVariables = new Map();
|
||||
}
|
||||
this.treeData.blackboardVariables.set(key, initialValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加序列节点
|
||||
*/
|
||||
sequence(name?: string): BehaviorTreeBuilder {
|
||||
return this.addCompositeNode('Sequence', name || 'Sequence');
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加选择器节点
|
||||
*/
|
||||
selector(name?: string): BehaviorTreeBuilder {
|
||||
return this.addCompositeNode('Selector', name || 'Selector');
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加并行节点
|
||||
*/
|
||||
parallel(name?: string, config?: { successPolicy?: string; failurePolicy?: string }): BehaviorTreeBuilder {
|
||||
return this.addCompositeNode('Parallel', name || 'Parallel', config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加并行选择器节点
|
||||
*/
|
||||
parallelSelector(name?: string, config?: { failurePolicy?: string }): BehaviorTreeBuilder {
|
||||
return this.addCompositeNode('ParallelSelector', name || 'ParallelSelector', config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加随机序列节点
|
||||
*/
|
||||
randomSequence(name?: string): BehaviorTreeBuilder {
|
||||
return this.addCompositeNode('RandomSequence', name || 'RandomSequence');
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加随机选择器节点
|
||||
*/
|
||||
randomSelector(name?: string): BehaviorTreeBuilder {
|
||||
return this.addCompositeNode('RandomSelector', name || 'RandomSelector');
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加反转装饰器
|
||||
*/
|
||||
inverter(name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('Inverter', name || 'Inverter');
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加重复装饰器
|
||||
*/
|
||||
repeater(repeatCount: number, name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('Repeater', name || 'Repeater', { repeatCount });
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加总是成功装饰器
|
||||
*/
|
||||
alwaysSucceed(name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('AlwaysSucceed', name || 'AlwaysSucceed');
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加总是失败装饰器
|
||||
*/
|
||||
alwaysFail(name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('AlwaysFail', name || 'AlwaysFail');
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加直到成功装饰器
|
||||
*/
|
||||
untilSuccess(name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('UntilSuccess', name || 'UntilSuccess');
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加直到失败装饰器
|
||||
*/
|
||||
untilFail(name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('UntilFail', name || 'UntilFail');
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加条件装饰器
|
||||
*/
|
||||
conditional(blackboardKey: string, expectedValue: any, operator?: string, name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('Conditional', name || 'Conditional', {
|
||||
blackboardKey,
|
||||
expectedValue,
|
||||
operator: operator || 'equals'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加冷却装饰器
|
||||
*/
|
||||
cooldown(cooldownTime: number, name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('Cooldown', name || 'Cooldown', { cooldownTime });
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加超时装饰器
|
||||
*/
|
||||
timeout(timeout: number, name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('Timeout', name || 'Timeout', { timeout });
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加等待动作
|
||||
*/
|
||||
wait(duration: number, name?: string): BehaviorTreeBuilder {
|
||||
return this.addActionNode('Wait', name || 'Wait', { duration });
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加日志动作
|
||||
*/
|
||||
log(message: string, name?: string): BehaviorTreeBuilder {
|
||||
return this.addActionNode('Log', name || 'Log', { message });
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加设置黑板值动作
|
||||
*/
|
||||
setBlackboardValue(key: string, value: any, name?: string): BehaviorTreeBuilder {
|
||||
return this.addActionNode('SetBlackboardValue', name || 'SetBlackboardValue', { key, value });
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加修改黑板值动作
|
||||
*/
|
||||
modifyBlackboardValue(key: string, operation: string, value: number, name?: string): BehaviorTreeBuilder {
|
||||
return this.addActionNode('ModifyBlackboardValue', name || 'ModifyBlackboardValue', {
|
||||
key,
|
||||
operation,
|
||||
value
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加执行动作
|
||||
*/
|
||||
executeAction(actionName: string, name?: string): BehaviorTreeBuilder {
|
||||
return this.addActionNode('ExecuteAction', name || 'ExecuteAction', { actionName });
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加黑板比较条件
|
||||
*/
|
||||
blackboardCompare(key: string, compareValue: any, operator?: string, name?: string): BehaviorTreeBuilder {
|
||||
return this.addConditionNode('BlackboardCompare', name || 'BlackboardCompare', {
|
||||
key,
|
||||
compareValue,
|
||||
operator: operator || 'equals'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加黑板存在检查条件
|
||||
*/
|
||||
blackboardExists(key: string, name?: string): BehaviorTreeBuilder {
|
||||
return this.addConditionNode('BlackboardExists', name || 'BlackboardExists', { key });
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加随机概率条件
|
||||
*/
|
||||
randomProbability(probability: number, name?: string): BehaviorTreeBuilder {
|
||||
return this.addConditionNode('RandomProbability', name || 'RandomProbability', { probability });
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加执行条件
|
||||
*/
|
||||
executeCondition(conditionName: string, name?: string): BehaviorTreeBuilder {
|
||||
return this.addConditionNode('ExecuteCondition', name || 'ExecuteCondition', { conditionName });
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束当前节点,返回父节点
|
||||
*/
|
||||
end(): BehaviorTreeBuilder {
|
||||
if (this.nodeStack.length > 0) {
|
||||
this.nodeStack.pop();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建行为树数据
|
||||
*/
|
||||
build(): BehaviorTreeData {
|
||||
if (!this.treeData.rootNodeId) {
|
||||
throw new Error('No root node defined. Add at least one node to the tree.');
|
||||
}
|
||||
return this.treeData;
|
||||
}
|
||||
|
||||
private addCompositeNode(implementationType: string, name: string, config: Record<string, any> = {}): BehaviorTreeBuilder {
|
||||
const nodeId = this.generateNodeId();
|
||||
const node: BehaviorNodeData = {
|
||||
id: nodeId,
|
||||
name,
|
||||
nodeType: NodeType.Composite,
|
||||
implementationType,
|
||||
children: [],
|
||||
config
|
||||
};
|
||||
|
||||
this.treeData.nodes.set(nodeId, node);
|
||||
|
||||
if (!this.treeData.rootNodeId) {
|
||||
this.treeData.rootNodeId = nodeId;
|
||||
}
|
||||
|
||||
if (this.nodeStack.length > 0) {
|
||||
const parentId = this.nodeStack[this.nodeStack.length - 1]!;
|
||||
const parentNode = this.treeData.nodes.get(parentId);
|
||||
if (parentNode && parentNode.children) {
|
||||
parentNode.children.push(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
this.nodeStack.push(nodeId);
|
||||
return this;
|
||||
}
|
||||
|
||||
private addDecoratorNode(implementationType: string, name: string, config: Record<string, any> = {}): BehaviorTreeBuilder {
|
||||
const nodeId = this.generateNodeId();
|
||||
const node: BehaviorNodeData = {
|
||||
id: nodeId,
|
||||
name,
|
||||
nodeType: NodeType.Decorator,
|
||||
implementationType,
|
||||
children: [],
|
||||
config
|
||||
};
|
||||
|
||||
this.treeData.nodes.set(nodeId, node);
|
||||
|
||||
if (!this.treeData.rootNodeId) {
|
||||
this.treeData.rootNodeId = nodeId;
|
||||
}
|
||||
|
||||
if (this.nodeStack.length > 0) {
|
||||
const parentId = this.nodeStack[this.nodeStack.length - 1]!;
|
||||
const parentNode = this.treeData.nodes.get(parentId);
|
||||
if (parentNode && parentNode.children) {
|
||||
parentNode.children.push(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
this.nodeStack.push(nodeId);
|
||||
return this;
|
||||
}
|
||||
|
||||
private addActionNode(implementationType: string, name: string, config: Record<string, any> = {}): BehaviorTreeBuilder {
|
||||
const nodeId = this.generateNodeId();
|
||||
const node: BehaviorNodeData = {
|
||||
id: nodeId,
|
||||
name,
|
||||
nodeType: NodeType.Action,
|
||||
implementationType,
|
||||
config
|
||||
};
|
||||
|
||||
this.treeData.nodes.set(nodeId, node);
|
||||
|
||||
if (!this.treeData.rootNodeId) {
|
||||
this.treeData.rootNodeId = nodeId;
|
||||
}
|
||||
|
||||
if (this.nodeStack.length > 0) {
|
||||
const parentId = this.nodeStack[this.nodeStack.length - 1]!;
|
||||
const parentNode = this.treeData.nodes.get(parentId);
|
||||
if (parentNode && parentNode.children) {
|
||||
parentNode.children.push(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private addConditionNode(implementationType: string, name: string, config: Record<string, any> = {}): BehaviorTreeBuilder {
|
||||
const nodeId = this.generateNodeId();
|
||||
const node: BehaviorNodeData = {
|
||||
id: nodeId,
|
||||
name,
|
||||
nodeType: NodeType.Condition,
|
||||
implementationType,
|
||||
config
|
||||
};
|
||||
|
||||
this.treeData.nodes.set(nodeId, node);
|
||||
|
||||
if (!this.treeData.rootNodeId) {
|
||||
this.treeData.rootNodeId = nodeId;
|
||||
}
|
||||
|
||||
if (this.nodeStack.length > 0) {
|
||||
const parentId = this.nodeStack[this.nodeStack.length - 1]!;
|
||||
const parentNode = this.treeData.nodes.get(parentId);
|
||||
if (parentNode && parentNode.children) {
|
||||
parentNode.children.push(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private generateNodeId(): string {
|
||||
return `node_${this.nodeIdCounter++}`;
|
||||
}
|
||||
}
|
||||
92
packages/framework/behavior-tree/src/BehaviorTreeStarter.ts
Normal file
92
packages/framework/behavior-tree/src/BehaviorTreeStarter.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Entity, Core } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreeData } from './execution/BehaviorTreeData';
|
||||
import { BehaviorTreeRuntimeComponent } from './execution/BehaviorTreeRuntimeComponent';
|
||||
import { BehaviorTreeAssetManager } from './execution/BehaviorTreeAssetManager';
|
||||
|
||||
/**
|
||||
* 行为树启动辅助类
|
||||
*
|
||||
* 提供便捷方法来启动、停止行为树
|
||||
*/
|
||||
export class BehaviorTreeStarter {
|
||||
/**
|
||||
* 启动行为树
|
||||
*
|
||||
* @param entity 游戏实体
|
||||
* @param treeData 行为树数据
|
||||
* @param autoStart 是否自动开始执行
|
||||
*/
|
||||
static start(entity: Entity, treeData: BehaviorTreeData, autoStart: boolean = true): void {
|
||||
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
assetManager.loadAsset(treeData);
|
||||
|
||||
let runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
if (!runtime) {
|
||||
runtime = new BehaviorTreeRuntimeComponent();
|
||||
entity.addComponent(runtime);
|
||||
}
|
||||
|
||||
runtime.treeAssetId = treeData.id;
|
||||
runtime.autoStart = autoStart;
|
||||
|
||||
if (treeData.blackboardVariables) {
|
||||
for (const [key, value] of treeData.blackboardVariables.entries()) {
|
||||
runtime.setBlackboardValue(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (autoStart) {
|
||||
runtime.isRunning = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止行为树
|
||||
*
|
||||
* @param entity 游戏实体
|
||||
*/
|
||||
static stop(entity: Entity): void {
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
if (runtime) {
|
||||
runtime.isRunning = false;
|
||||
runtime.resetAllStates();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停行为树
|
||||
*
|
||||
* @param entity 游戏实体
|
||||
*/
|
||||
static pause(entity: Entity): void {
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
if (runtime) {
|
||||
runtime.isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复行为树
|
||||
*
|
||||
* @param entity 游戏实体
|
||||
*/
|
||||
static resume(entity: Entity): void {
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
if (runtime) {
|
||||
runtime.isRunning = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重启行为树
|
||||
*
|
||||
* @param entity 游戏实体
|
||||
*/
|
||||
static restart(entity: Entity): void {
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
if (runtime) {
|
||||
runtime.resetAllStates();
|
||||
runtime.isRunning = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
export enum BlackboardValueType {
|
||||
// 基础类型
|
||||
String = 'string',
|
||||
Number = 'number',
|
||||
Boolean = 'boolean',
|
||||
|
||||
// 数学类型
|
||||
Vector2 = 'vector2',
|
||||
Vector3 = 'vector3',
|
||||
Vector4 = 'vector4',
|
||||
Quaternion = 'quaternion',
|
||||
Color = 'color',
|
||||
|
||||
// 引用类型
|
||||
GameObject = 'gameObject',
|
||||
Transform = 'transform',
|
||||
Component = 'component',
|
||||
AssetReference = 'assetReference',
|
||||
|
||||
// 集合类型
|
||||
Array = 'array',
|
||||
Map = 'map',
|
||||
|
||||
// 高级类型
|
||||
Enum = 'enum',
|
||||
Struct = 'struct',
|
||||
Function = 'function',
|
||||
|
||||
// 游戏特定类型
|
||||
EntityId = 'entityId',
|
||||
NodePath = 'nodePath',
|
||||
ResourcePath = 'resourcePath',
|
||||
AnimationState = 'animationState',
|
||||
AudioClip = 'audioClip',
|
||||
Material = 'material',
|
||||
Texture = 'texture'
|
||||
}
|
||||
|
||||
export interface Vector2 {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Vector3 extends Vector2 {
|
||||
z: number;
|
||||
}
|
||||
|
||||
export interface Vector4 extends Vector3 {
|
||||
w: number;
|
||||
}
|
||||
|
||||
export interface Quaternion {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
w: number;
|
||||
}
|
||||
|
||||
export interface Color {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
a: number;
|
||||
}
|
||||
|
||||
export interface BlackboardTypeDefinition {
|
||||
type: BlackboardValueType;
|
||||
displayName: string;
|
||||
category: 'basic' | 'math' | 'reference' | 'collection' | 'advanced' | 'game';
|
||||
defaultValue: any;
|
||||
editorComponent?: string; // 自定义编辑器组件
|
||||
validator?: (value: any) => boolean;
|
||||
converter?: (value: any) => any;
|
||||
}
|
||||
|
||||
export const BlackboardTypes: Record<BlackboardValueType, BlackboardTypeDefinition> = {
|
||||
[BlackboardValueType.String]: {
|
||||
type: BlackboardValueType.String,
|
||||
displayName: '字符串',
|
||||
category: 'basic',
|
||||
defaultValue: '',
|
||||
validator: (v) => typeof v === 'string'
|
||||
},
|
||||
[BlackboardValueType.Number]: {
|
||||
type: BlackboardValueType.Number,
|
||||
displayName: '数字',
|
||||
category: 'basic',
|
||||
defaultValue: 0,
|
||||
validator: (v) => typeof v === 'number'
|
||||
},
|
||||
[BlackboardValueType.Boolean]: {
|
||||
type: BlackboardValueType.Boolean,
|
||||
displayName: '布尔值',
|
||||
category: 'basic',
|
||||
defaultValue: false,
|
||||
validator: (v) => typeof v === 'boolean'
|
||||
},
|
||||
[BlackboardValueType.Vector2]: {
|
||||
type: BlackboardValueType.Vector2,
|
||||
displayName: '二维向量',
|
||||
category: 'math',
|
||||
defaultValue: { x: 0, y: 0 },
|
||||
editorComponent: 'Vector2Editor',
|
||||
validator: (v) => v && typeof v.x === 'number' && typeof v.y === 'number'
|
||||
},
|
||||
[BlackboardValueType.Vector3]: {
|
||||
type: BlackboardValueType.Vector3,
|
||||
displayName: '三维向量',
|
||||
category: 'math',
|
||||
defaultValue: { x: 0, y: 0, z: 0 },
|
||||
editorComponent: 'Vector3Editor',
|
||||
validator: (v) => v && typeof v.x === 'number' && typeof v.y === 'number' && typeof v.z === 'number'
|
||||
},
|
||||
[BlackboardValueType.Color]: {
|
||||
type: BlackboardValueType.Color,
|
||||
displayName: '颜色',
|
||||
category: 'math',
|
||||
defaultValue: { r: 1, g: 1, b: 1, a: 1 },
|
||||
editorComponent: 'ColorEditor',
|
||||
validator: (v) => v && typeof v.r === 'number' && typeof v.g === 'number' && typeof v.b === 'number' && typeof v.a === 'number'
|
||||
},
|
||||
[BlackboardValueType.GameObject]: {
|
||||
type: BlackboardValueType.GameObject,
|
||||
displayName: '游戏对象',
|
||||
category: 'reference',
|
||||
defaultValue: null,
|
||||
editorComponent: 'GameObjectPicker'
|
||||
},
|
||||
[BlackboardValueType.Transform]: {
|
||||
type: BlackboardValueType.Transform,
|
||||
displayName: '变换组件',
|
||||
category: 'reference',
|
||||
defaultValue: null,
|
||||
editorComponent: 'ComponentPicker'
|
||||
},
|
||||
[BlackboardValueType.AssetReference]: {
|
||||
type: BlackboardValueType.AssetReference,
|
||||
displayName: '资源引用',
|
||||
category: 'reference',
|
||||
defaultValue: null,
|
||||
editorComponent: 'AssetPicker'
|
||||
},
|
||||
[BlackboardValueType.EntityId]: {
|
||||
type: BlackboardValueType.EntityId,
|
||||
displayName: '实体ID',
|
||||
category: 'game',
|
||||
defaultValue: -1,
|
||||
validator: (v) => typeof v === 'number' && v >= -1
|
||||
},
|
||||
[BlackboardValueType.ResourcePath]: {
|
||||
type: BlackboardValueType.ResourcePath,
|
||||
displayName: '资源路径',
|
||||
category: 'game',
|
||||
defaultValue: '',
|
||||
editorComponent: 'AssetPathPicker'
|
||||
},
|
||||
[BlackboardValueType.Array]: {
|
||||
type: BlackboardValueType.Array,
|
||||
displayName: '数组',
|
||||
category: 'collection',
|
||||
defaultValue: [],
|
||||
editorComponent: 'ArrayEditor'
|
||||
},
|
||||
[BlackboardValueType.Map]: {
|
||||
type: BlackboardValueType.Map,
|
||||
displayName: '映射表',
|
||||
category: 'collection',
|
||||
defaultValue: {},
|
||||
editorComponent: 'MapEditor'
|
||||
},
|
||||
[BlackboardValueType.Enum]: {
|
||||
type: BlackboardValueType.Enum,
|
||||
displayName: '枚举',
|
||||
category: 'advanced',
|
||||
defaultValue: '',
|
||||
editorComponent: 'EnumPicker'
|
||||
},
|
||||
[BlackboardValueType.AnimationState]: {
|
||||
type: BlackboardValueType.AnimationState,
|
||||
displayName: '动画状态',
|
||||
category: 'game',
|
||||
defaultValue: '',
|
||||
editorComponent: 'AnimationStatePicker'
|
||||
},
|
||||
[BlackboardValueType.AudioClip]: {
|
||||
type: BlackboardValueType.AudioClip,
|
||||
displayName: '音频片段',
|
||||
category: 'game',
|
||||
defaultValue: null,
|
||||
editorComponent: 'AudioClipPicker'
|
||||
},
|
||||
[BlackboardValueType.Material]: {
|
||||
type: BlackboardValueType.Material,
|
||||
displayName: '材质',
|
||||
category: 'game',
|
||||
defaultValue: null,
|
||||
editorComponent: 'MaterialPicker'
|
||||
},
|
||||
[BlackboardValueType.Texture]: {
|
||||
type: BlackboardValueType.Texture,
|
||||
displayName: '纹理',
|
||||
category: 'game',
|
||||
defaultValue: null,
|
||||
editorComponent: 'TexturePicker'
|
||||
},
|
||||
[BlackboardValueType.Vector4]: {
|
||||
type: BlackboardValueType.Vector4,
|
||||
displayName: '四维向量',
|
||||
category: 'math',
|
||||
defaultValue: { x: 0, y: 0, z: 0, w: 0 },
|
||||
editorComponent: 'Vector4Editor'
|
||||
},
|
||||
[BlackboardValueType.Quaternion]: {
|
||||
type: BlackboardValueType.Quaternion,
|
||||
displayName: '四元数',
|
||||
category: 'math',
|
||||
defaultValue: { x: 0, y: 0, z: 0, w: 1 },
|
||||
editorComponent: 'QuaternionEditor'
|
||||
},
|
||||
[BlackboardValueType.Component]: {
|
||||
type: BlackboardValueType.Component,
|
||||
displayName: '组件',
|
||||
category: 'reference',
|
||||
defaultValue: null,
|
||||
editorComponent: 'ComponentPicker'
|
||||
},
|
||||
[BlackboardValueType.Struct]: {
|
||||
type: BlackboardValueType.Struct,
|
||||
displayName: '结构体',
|
||||
category: 'advanced',
|
||||
defaultValue: {},
|
||||
editorComponent: 'StructEditor'
|
||||
},
|
||||
[BlackboardValueType.Function]: {
|
||||
type: BlackboardValueType.Function,
|
||||
displayName: '函数',
|
||||
category: 'advanced',
|
||||
defaultValue: null,
|
||||
editorComponent: 'FunctionPicker'
|
||||
},
|
||||
[BlackboardValueType.NodePath]: {
|
||||
type: BlackboardValueType.NodePath,
|
||||
displayName: '节点路径',
|
||||
category: 'game',
|
||||
defaultValue: '',
|
||||
editorComponent: 'NodePathPicker'
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,303 @@
|
||||
import { NodeType, BlackboardValueType } from '../Types/TaskStatus';
|
||||
|
||||
/**
|
||||
* 行为树资产元数据
|
||||
*/
|
||||
export interface AssetMetadata {
|
||||
name: string;
|
||||
description?: string;
|
||||
version: string;
|
||||
createdAt?: string;
|
||||
modifiedAt?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 黑板变量定义
|
||||
*/
|
||||
export interface BlackboardVariableDefinition {
|
||||
name: string;
|
||||
type: BlackboardValueType;
|
||||
defaultValue: any;
|
||||
readonly?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树节点配置数据
|
||||
*/
|
||||
export interface BehaviorNodeConfigData {
|
||||
className?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树节点数据(运行时格式)
|
||||
*/
|
||||
export interface BehaviorTreeNodeData {
|
||||
id: string;
|
||||
name: string;
|
||||
nodeType: NodeType;
|
||||
|
||||
// 节点类型特定数据
|
||||
data: BehaviorNodeConfigData;
|
||||
|
||||
// 子节点ID列表
|
||||
children: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 属性绑定定义
|
||||
*/
|
||||
export interface PropertyBinding {
|
||||
nodeId: string;
|
||||
propertyName: string;
|
||||
variableName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树资产(运行时格式)
|
||||
*
|
||||
* 这是用于游戏运行时的优化格式,不包含编辑器UI信息
|
||||
*/
|
||||
export interface BehaviorTreeAsset {
|
||||
/**
|
||||
* 资产格式版本
|
||||
*/
|
||||
version: string;
|
||||
|
||||
/**
|
||||
* 元数据
|
||||
*/
|
||||
metadata: AssetMetadata;
|
||||
|
||||
/**
|
||||
* 根节点ID
|
||||
*/
|
||||
rootNodeId: string;
|
||||
|
||||
/**
|
||||
* 所有节点数据(扁平化存储,通过children建立层级)
|
||||
*/
|
||||
nodes: BehaviorTreeNodeData[];
|
||||
|
||||
/**
|
||||
* 黑板变量定义
|
||||
*/
|
||||
blackboard: BlackboardVariableDefinition[];
|
||||
|
||||
/**
|
||||
* 属性绑定
|
||||
*/
|
||||
propertyBindings?: PropertyBinding[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 资产验证结果
|
||||
*/
|
||||
export interface AssetValidationResult {
|
||||
valid: boolean;
|
||||
errors?: string[];
|
||||
warnings?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 资产验证器
|
||||
*/
|
||||
export class BehaviorTreeAssetValidator {
|
||||
/**
|
||||
* 验证资产数据的完整性和正确性
|
||||
*/
|
||||
static validate(asset: BehaviorTreeAsset): AssetValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
// 检查版本
|
||||
if (!asset.version) {
|
||||
errors.push('Missing version field');
|
||||
}
|
||||
|
||||
// 检查元数据
|
||||
if (!asset.metadata || !asset.metadata.name) {
|
||||
errors.push('Missing or invalid metadata');
|
||||
}
|
||||
|
||||
// 检查根节点
|
||||
if (!asset.rootNodeId) {
|
||||
errors.push('Missing rootNodeId');
|
||||
}
|
||||
|
||||
// 检查节点列表
|
||||
if (!asset.nodes || !Array.isArray(asset.nodes)) {
|
||||
errors.push('Missing or invalid nodes array');
|
||||
} else {
|
||||
const nodeIds = new Set<string>();
|
||||
const rootNode = asset.nodes.find((n) => n.id === asset.rootNodeId);
|
||||
|
||||
if (!rootNode) {
|
||||
errors.push(`Root node '${asset.rootNodeId}' not found in nodes array`);
|
||||
}
|
||||
|
||||
// 检查节点ID唯一性
|
||||
for (const node of asset.nodes) {
|
||||
if (!node.id) {
|
||||
errors.push('Node missing id field');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nodeIds.has(node.id)) {
|
||||
errors.push(`Duplicate node id: ${node.id}`);
|
||||
}
|
||||
nodeIds.add(node.id);
|
||||
|
||||
// 检查节点类型
|
||||
if (!node.nodeType) {
|
||||
errors.push(`Node ${node.id} missing nodeType`);
|
||||
}
|
||||
|
||||
// 检查子节点引用
|
||||
if (node.children) {
|
||||
for (const childId of node.children) {
|
||||
if (!asset.nodes.find((n) => n.id === childId)) {
|
||||
errors.push(`Node ${node.id} references non-existent child: ${childId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有孤立节点
|
||||
const referencedNodes = new Set<string>([asset.rootNodeId]);
|
||||
const collectReferencedNodes = (nodeId: string) => {
|
||||
const node = asset.nodes.find((n) => n.id === nodeId);
|
||||
if (node && node.children) {
|
||||
for (const childId of node.children) {
|
||||
referencedNodes.add(childId);
|
||||
collectReferencedNodes(childId);
|
||||
}
|
||||
}
|
||||
};
|
||||
collectReferencedNodes(asset.rootNodeId);
|
||||
|
||||
for (const node of asset.nodes) {
|
||||
if (!referencedNodes.has(node.id)) {
|
||||
warnings.push(`Orphaned node detected: ${node.id} (${node.name})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查黑板定义
|
||||
if (asset.blackboard && Array.isArray(asset.blackboard)) {
|
||||
const varNames = new Set<string>();
|
||||
for (const variable of asset.blackboard) {
|
||||
if (!variable.name) {
|
||||
errors.push('Blackboard variable missing name');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (varNames.has(variable.name)) {
|
||||
errors.push(`Duplicate blackboard variable: ${variable.name}`);
|
||||
}
|
||||
varNames.add(variable.name);
|
||||
|
||||
if (!variable.type) {
|
||||
errors.push(`Blackboard variable ${variable.name} missing type`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查属性绑定
|
||||
if (asset.propertyBindings && Array.isArray(asset.propertyBindings)) {
|
||||
const nodeIds = new Set(asset.nodes.map((n) => n.id));
|
||||
const varNames = new Set(asset.blackboard?.map((v) => v.name) || []);
|
||||
|
||||
for (const binding of asset.propertyBindings) {
|
||||
if (!nodeIds.has(binding.nodeId)) {
|
||||
errors.push(`Property binding references non-existent node: ${binding.nodeId}`);
|
||||
}
|
||||
|
||||
if (!varNames.has(binding.variableName)) {
|
||||
errors.push(`Property binding references non-existent variable: ${binding.variableName}`);
|
||||
}
|
||||
|
||||
if (!binding.propertyName) {
|
||||
errors.push('Property binding missing propertyName');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result: AssetValidationResult = {
|
||||
valid: errors.length === 0
|
||||
};
|
||||
|
||||
if (errors.length > 0) {
|
||||
result.errors = errors;
|
||||
}
|
||||
|
||||
if (warnings.length > 0) {
|
||||
result.warnings = warnings;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资产统计信息
|
||||
*/
|
||||
static getStats(asset: BehaviorTreeAsset): {
|
||||
nodeCount: number;
|
||||
actionCount: number;
|
||||
conditionCount: number;
|
||||
compositeCount: number;
|
||||
decoratorCount: number;
|
||||
blackboardVariableCount: number;
|
||||
propertyBindingCount: number;
|
||||
maxDepth: number;
|
||||
} {
|
||||
let actionCount = 0;
|
||||
let conditionCount = 0;
|
||||
let compositeCount = 0;
|
||||
let decoratorCount = 0;
|
||||
|
||||
for (const node of asset.nodes) {
|
||||
switch (node.nodeType) {
|
||||
case NodeType.Action:
|
||||
actionCount++;
|
||||
break;
|
||||
case NodeType.Condition:
|
||||
conditionCount++;
|
||||
break;
|
||||
case NodeType.Composite:
|
||||
compositeCount++;
|
||||
break;
|
||||
case NodeType.Decorator:
|
||||
decoratorCount++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算最大深度
|
||||
const getDepth = (nodeId: string, currentDepth: number = 0): number => {
|
||||
const node = asset.nodes.find((n) => n.id === nodeId);
|
||||
if (!node || !node.children || node.children.length === 0) {
|
||||
return currentDepth;
|
||||
}
|
||||
|
||||
let maxChildDepth = currentDepth;
|
||||
for (const childId of node.children) {
|
||||
const childDepth = getDepth(childId, currentDepth + 1);
|
||||
maxChildDepth = Math.max(maxChildDepth, childDepth);
|
||||
}
|
||||
return maxChildDepth;
|
||||
};
|
||||
|
||||
return {
|
||||
nodeCount: asset.nodes.length,
|
||||
actionCount,
|
||||
conditionCount,
|
||||
compositeCount,
|
||||
decoratorCount,
|
||||
blackboardVariableCount: asset.blackboard?.length || 0,
|
||||
propertyBindingCount: asset.propertyBindings?.length || 0,
|
||||
maxDepth: getDepth(asset.rootNodeId)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
import { createLogger, BinarySerializer } from '@esengine/ecs-framework';
|
||||
import type { BehaviorTreeAsset } from './BehaviorTreeAsset';
|
||||
import { BehaviorTreeAssetValidator } from './BehaviorTreeAsset';
|
||||
import { EditorFormatConverter, type EditorFormat } from './EditorFormatConverter';
|
||||
|
||||
const logger = createLogger('BehaviorTreeAssetSerializer');
|
||||
|
||||
/**
|
||||
* 行为树序列化格式
|
||||
* Behavior tree serialization format
|
||||
*/
|
||||
export type BehaviorTreeSerializationFormat = 'json' | 'binary';
|
||||
|
||||
/**
|
||||
* 序列化选项
|
||||
*/
|
||||
export interface SerializationOptions {
|
||||
/**
|
||||
* 序列化格式
|
||||
*/
|
||||
format: BehaviorTreeSerializationFormat;
|
||||
|
||||
/**
|
||||
* 是否美化JSON输出(仅format='json'时有效)
|
||||
*/
|
||||
pretty?: boolean;
|
||||
|
||||
/**
|
||||
* 是否在序列化前验证资产
|
||||
*/
|
||||
validate?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化选项
|
||||
*/
|
||||
export interface DeserializationOptions {
|
||||
/**
|
||||
* 是否在反序列化后验证资产
|
||||
*/
|
||||
validate?: boolean;
|
||||
|
||||
/**
|
||||
* 是否严格模式(验证失败抛出异常)
|
||||
*/
|
||||
strict?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树资产序列化器
|
||||
*
|
||||
* 支持JSON和二进制两种格式
|
||||
*/
|
||||
export class BehaviorTreeAssetSerializer {
|
||||
/**
|
||||
* 序列化资产
|
||||
*
|
||||
* @param asset 行为树资产
|
||||
* @param options 序列化选项
|
||||
* @returns 序列化后的数据(字符串或Uint8Array)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // JSON格式
|
||||
* const jsonData = BehaviorTreeAssetSerializer.serialize(asset, { format: 'json', pretty: true });
|
||||
*
|
||||
* // 二进制格式
|
||||
* const binaryData = BehaviorTreeAssetSerializer.serialize(asset, { format: 'binary' });
|
||||
* ```
|
||||
*/
|
||||
static serialize(
|
||||
asset: BehaviorTreeAsset,
|
||||
options: SerializationOptions = { format: 'json', pretty: true }
|
||||
): string | Uint8Array {
|
||||
// 验证资产(如果需要)
|
||||
if (options.validate !== false) {
|
||||
const validation = BehaviorTreeAssetValidator.validate(asset);
|
||||
if (!validation.valid) {
|
||||
const errors = validation.errors?.join(', ') || 'Unknown error';
|
||||
throw new Error(`资产验证失败: ${errors}`);
|
||||
}
|
||||
|
||||
if (validation.warnings && validation.warnings.length > 0) {
|
||||
logger.warn(`资产验证警告: ${validation.warnings.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 根据格式序列化
|
||||
if (options.format === 'json') {
|
||||
return this.serializeToJSON(asset, options.pretty);
|
||||
} else {
|
||||
return this.serializeToBinary(asset);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化为JSON格式
|
||||
*/
|
||||
private static serializeToJSON(asset: BehaviorTreeAsset, pretty: boolean = true): string {
|
||||
try {
|
||||
const json = pretty
|
||||
? JSON.stringify(asset, null, 2)
|
||||
: JSON.stringify(asset);
|
||||
|
||||
logger.info(`已序列化为JSON: ${json.length} 字符`);
|
||||
return json;
|
||||
} catch (error) {
|
||||
throw new Error(`JSON序列化失败: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化为二进制格式
|
||||
*/
|
||||
private static serializeToBinary(asset: BehaviorTreeAsset): Uint8Array {
|
||||
try {
|
||||
const binary = BinarySerializer.encode(asset);
|
||||
logger.info(`已序列化为二进制: ${binary.length} 字节`);
|
||||
return binary;
|
||||
} catch (error) {
|
||||
throw new Error(`二进制序列化失败: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化资产
|
||||
*
|
||||
* @param data 序列化的数据(字符串或Uint8Array)
|
||||
* @param options 反序列化选项
|
||||
* @returns 行为树资产
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 从JSON加载
|
||||
* const asset = BehaviorTreeAssetSerializer.deserialize(jsonString);
|
||||
*
|
||||
* // 从二进制加载
|
||||
* const asset = BehaviorTreeAssetSerializer.deserialize(binaryData);
|
||||
* ```
|
||||
*/
|
||||
static deserialize(
|
||||
data: string | Uint8Array,
|
||||
options: DeserializationOptions = { validate: true, strict: true }
|
||||
): BehaviorTreeAsset {
|
||||
let asset: BehaviorTreeAsset;
|
||||
|
||||
try {
|
||||
if (typeof data === 'string') {
|
||||
asset = this.deserializeFromJSON(data);
|
||||
} else {
|
||||
asset = this.deserializeFromBinary(data);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`反序列化失败: ${error}`);
|
||||
}
|
||||
|
||||
// 验证资产(如果需要)
|
||||
if (options.validate !== false) {
|
||||
const validation = BehaviorTreeAssetValidator.validate(asset);
|
||||
|
||||
if (!validation.valid) {
|
||||
const errors = validation.errors?.join(', ') || 'Unknown error';
|
||||
if (options.strict) {
|
||||
throw new Error(`资产验证失败: ${errors}`);
|
||||
} else {
|
||||
logger.error(`资产验证失败: ${errors}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (validation.warnings && validation.warnings.length > 0) {
|
||||
logger.warn(`资产验证警告: ${validation.warnings.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
return asset;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从JSON反序列化
|
||||
*/
|
||||
private static deserializeFromJSON(json: string): BehaviorTreeAsset {
|
||||
try {
|
||||
const data = JSON.parse(json);
|
||||
|
||||
// 检测是否是编辑器格式(EditorFormat)
|
||||
// 编辑器格式有 nodes/connections/blackboard,但没有 rootNodeId
|
||||
// 运行时资产格式有 rootNodeId
|
||||
const isEditorFormat = !data.rootNodeId && data.nodes && data.connections;
|
||||
|
||||
if (isEditorFormat) {
|
||||
logger.info('检测到编辑器格式,正在转换为运行时资产格式...');
|
||||
const editorData = data as EditorFormat;
|
||||
const asset = EditorFormatConverter.toAsset(editorData);
|
||||
logger.info(`已从编辑器格式转换: ${asset.nodes.length} 个节点`);
|
||||
return asset;
|
||||
} else {
|
||||
const asset = data as BehaviorTreeAsset;
|
||||
logger.info(`已从运行时资产格式反序列化: ${asset.nodes.length} 个节点`);
|
||||
return asset;
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`JSON解析失败: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从二进制反序列化
|
||||
*/
|
||||
private static deserializeFromBinary(binary: Uint8Array): BehaviorTreeAsset {
|
||||
try {
|
||||
const asset = BinarySerializer.decode(binary) as BehaviorTreeAsset;
|
||||
logger.info(`已从二进制反序列化: ${asset.nodes.length} 个节点`);
|
||||
return asset;
|
||||
} catch (error) {
|
||||
throw new Error(`二进制解码失败: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测数据格式
|
||||
*
|
||||
* @param data 序列化的数据
|
||||
* @returns 格式类型
|
||||
*/
|
||||
static detectFormat(data: string | Uint8Array): BehaviorTreeSerializationFormat {
|
||||
if (typeof data === 'string') {
|
||||
return 'json';
|
||||
} else {
|
||||
return 'binary';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取序列化数据的信息(不完全反序列化)
|
||||
*
|
||||
* @param data 序列化的数据
|
||||
* @returns 资产元信息
|
||||
*/
|
||||
static getInfo(data: string | Uint8Array): {
|
||||
format: BehaviorTreeSerializationFormat;
|
||||
name: string;
|
||||
version: string;
|
||||
nodeCount: number;
|
||||
blackboardVariableCount: number;
|
||||
size: number;
|
||||
} | null {
|
||||
try {
|
||||
const format = this.detectFormat(data);
|
||||
let asset: BehaviorTreeAsset;
|
||||
|
||||
if (format === 'json') {
|
||||
asset = JSON.parse(data as string);
|
||||
} else {
|
||||
asset = BinarySerializer.decode(data as Uint8Array) as BehaviorTreeAsset;
|
||||
}
|
||||
|
||||
const size = typeof data === 'string' ? data.length : data.length;
|
||||
|
||||
return {
|
||||
format,
|
||||
name: asset.metadata.name,
|
||||
version: asset.version,
|
||||
nodeCount: asset.nodes.length,
|
||||
blackboardVariableCount: asset.blackboard.length,
|
||||
size
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`获取资产信息失败: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换格式
|
||||
*
|
||||
* @param data 源数据
|
||||
* @param targetFormat 目标格式
|
||||
* @param pretty 是否美化JSON(仅当目标格式为json时有效)
|
||||
* @returns 转换后的数据
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // JSON转二进制
|
||||
* const binary = BehaviorTreeAssetSerializer.convert(jsonString, 'binary');
|
||||
*
|
||||
* // 二进制转JSON
|
||||
* const json = BehaviorTreeAssetSerializer.convert(binaryData, 'json', true);
|
||||
* ```
|
||||
*/
|
||||
static convert(
|
||||
data: string | Uint8Array,
|
||||
targetFormat: BehaviorTreeSerializationFormat,
|
||||
pretty: boolean = true
|
||||
): string | Uint8Array {
|
||||
const asset = this.deserialize(data, { validate: false });
|
||||
|
||||
return this.serialize(asset, {
|
||||
format: targetFormat,
|
||||
pretty,
|
||||
validate: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较两个资产数据的大小
|
||||
*
|
||||
* @param jsonData JSON格式数据
|
||||
* @param binaryData 二进制格式数据
|
||||
* @returns 压缩率(百分比)
|
||||
*/
|
||||
static compareSize(jsonData: string, binaryData: Uint8Array): {
|
||||
jsonSize: number;
|
||||
binarySize: number;
|
||||
compressionRatio: number;
|
||||
savedBytes: number;
|
||||
} {
|
||||
const jsonSize = jsonData.length;
|
||||
const binarySize = binaryData.length;
|
||||
const savedBytes = jsonSize - binarySize;
|
||||
const compressionRatio = (savedBytes / jsonSize) * 100;
|
||||
|
||||
return {
|
||||
jsonSize,
|
||||
binarySize,
|
||||
compressionRatio,
|
||||
savedBytes
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
import type { BehaviorTreeAsset, AssetMetadata, BehaviorTreeNodeData, BlackboardVariableDefinition, PropertyBinding } from './BehaviorTreeAsset';
|
||||
import { NodeType, BlackboardValueType } from '../Types/TaskStatus';
|
||||
|
||||
const logger = createLogger('EditorFormatConverter');
|
||||
|
||||
/**
|
||||
* 编辑器节点格式
|
||||
*/
|
||||
export interface EditorNodeTemplate {
|
||||
displayName: string;
|
||||
category: string;
|
||||
type: NodeType;
|
||||
className?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface EditorNodeData {
|
||||
nodeType?: string;
|
||||
className?: string;
|
||||
variableName?: string;
|
||||
name?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface EditorNode {
|
||||
id: string;
|
||||
template: EditorNodeTemplate;
|
||||
data: EditorNodeData;
|
||||
position: { x: number; y: number };
|
||||
children: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑器连接格式
|
||||
*/
|
||||
export interface EditorConnection {
|
||||
from: string;
|
||||
to: string;
|
||||
fromProperty?: string;
|
||||
toProperty?: string;
|
||||
connectionType: 'node' | 'property';
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑器格式
|
||||
*/
|
||||
export interface EditorFormat {
|
||||
version?: string;
|
||||
metadata?: {
|
||||
name: string;
|
||||
description?: string;
|
||||
createdAt?: string;
|
||||
modifiedAt?: string;
|
||||
};
|
||||
nodes: EditorNode[];
|
||||
connections: EditorConnection[];
|
||||
blackboard: Record<string, any>;
|
||||
canvasState?: {
|
||||
offset: { x: number; y: number };
|
||||
scale: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑器格式转换器
|
||||
*
|
||||
* 将编辑器格式转换为运行时资产格式
|
||||
*/
|
||||
export class EditorFormatConverter {
|
||||
/**
|
||||
* 转换编辑器格式为资产格式
|
||||
*
|
||||
* @param editorData 编辑器数据
|
||||
* @param metadata 可选的元数据覆盖
|
||||
* @returns 行为树资产
|
||||
*/
|
||||
static toAsset(editorData: EditorFormat, metadata?: Partial<AssetMetadata>): BehaviorTreeAsset {
|
||||
logger.info('开始转换编辑器格式到资产格式');
|
||||
|
||||
const rootNode = this.findRootNode(editorData.nodes);
|
||||
if (!rootNode) {
|
||||
throw new Error('未找到根节点');
|
||||
}
|
||||
|
||||
const assetMetadata: AssetMetadata = {
|
||||
name: metadata?.name || editorData.metadata?.name || 'Untitled Behavior Tree',
|
||||
version: metadata?.version || editorData.version || '1.0.0'
|
||||
};
|
||||
|
||||
const description = metadata?.description || editorData.metadata?.description;
|
||||
if (description) {
|
||||
assetMetadata.description = description;
|
||||
}
|
||||
|
||||
const createdAt = metadata?.createdAt || editorData.metadata?.createdAt;
|
||||
if (createdAt) {
|
||||
assetMetadata.createdAt = createdAt;
|
||||
}
|
||||
|
||||
const modifiedAt = metadata?.modifiedAt || new Date().toISOString();
|
||||
if (modifiedAt) {
|
||||
assetMetadata.modifiedAt = modifiedAt;
|
||||
}
|
||||
|
||||
const nodes = this.convertNodes(editorData.nodes);
|
||||
|
||||
const blackboard = this.convertBlackboard(editorData.blackboard);
|
||||
|
||||
const propertyBindings = this.convertPropertyBindings(
|
||||
editorData.connections,
|
||||
editorData.nodes,
|
||||
blackboard
|
||||
);
|
||||
|
||||
const asset: BehaviorTreeAsset = {
|
||||
version: '1.0.0',
|
||||
metadata: assetMetadata,
|
||||
rootNodeId: rootNode.id,
|
||||
nodes,
|
||||
blackboard
|
||||
};
|
||||
|
||||
if (propertyBindings.length > 0) {
|
||||
asset.propertyBindings = propertyBindings;
|
||||
}
|
||||
|
||||
logger.info(`转换完成: ${nodes.length}个节点, ${blackboard.length}个黑板变量, ${propertyBindings.length}个属性绑定`);
|
||||
|
||||
return asset;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找根节点
|
||||
*/
|
||||
private static findRootNode(nodes: EditorNode[]): EditorNode | null {
|
||||
return nodes.find((node) =>
|
||||
node.template.category === '根节点' ||
|
||||
node.data.nodeType === 'root'
|
||||
) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换节点列表
|
||||
*/
|
||||
private static convertNodes(editorNodes: EditorNode[]): BehaviorTreeNodeData[] {
|
||||
return editorNodes.map((node) => this.convertNode(node));
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换单个节点
|
||||
*/
|
||||
private static convertNode(editorNode: EditorNode): BehaviorTreeNodeData {
|
||||
const data = { ...editorNode.data };
|
||||
|
||||
delete data.nodeType;
|
||||
|
||||
if (editorNode.template.className) {
|
||||
data.className = editorNode.template.className;
|
||||
}
|
||||
|
||||
return {
|
||||
id: editorNode.id,
|
||||
name: editorNode.template.displayName || editorNode.data.name || 'Node',
|
||||
nodeType: editorNode.template.type,
|
||||
data,
|
||||
children: editorNode.children || []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换黑板变量
|
||||
*/
|
||||
private static convertBlackboard(blackboard: Record<string, any>): BlackboardVariableDefinition[] {
|
||||
const variables: BlackboardVariableDefinition[] = [];
|
||||
|
||||
for (const [name, value] of Object.entries(blackboard)) {
|
||||
const type = this.inferBlackboardType(value);
|
||||
|
||||
variables.push({
|
||||
name,
|
||||
type,
|
||||
defaultValue: value
|
||||
});
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* 推断黑板变量类型
|
||||
*/
|
||||
private static inferBlackboardType(value: any): BlackboardValueType {
|
||||
if (typeof value === 'number') {
|
||||
return BlackboardValueType.Number;
|
||||
} else if (typeof value === 'string') {
|
||||
return BlackboardValueType.String;
|
||||
} else if (typeof value === 'boolean') {
|
||||
return BlackboardValueType.Boolean;
|
||||
} else {
|
||||
return BlackboardValueType.Object;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换属性绑定
|
||||
*/
|
||||
private static convertPropertyBindings(
|
||||
connections: EditorConnection[],
|
||||
nodes: EditorNode[],
|
||||
blackboard: BlackboardVariableDefinition[]
|
||||
): PropertyBinding[] {
|
||||
const bindings: PropertyBinding[] = [];
|
||||
const blackboardVarNames = new Set(blackboard.map((v) => v.name));
|
||||
|
||||
const propertyConnections = connections.filter((conn) => conn.connectionType === 'property');
|
||||
|
||||
for (const conn of propertyConnections) {
|
||||
const fromNode = nodes.find((n) => n.id === conn.from);
|
||||
const toNode = nodes.find((n) => n.id === conn.to);
|
||||
|
||||
if (!fromNode || !toNode || !conn.toProperty) {
|
||||
logger.warn(`跳过无效的属性连接: from=${conn.from}, to=${conn.to}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let variableName: string | undefined;
|
||||
|
||||
if (fromNode.data.nodeType === 'blackboard-variable') {
|
||||
variableName = fromNode.data.variableName;
|
||||
} else if (conn.fromProperty) {
|
||||
variableName = conn.fromProperty;
|
||||
}
|
||||
|
||||
if (!variableName) {
|
||||
logger.warn(`无法确定变量名: from节点=${fromNode.template.displayName}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!blackboardVarNames.has(variableName)) {
|
||||
logger.warn(`属性绑定引用了不存在的黑板变量: ${variableName}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
bindings.push({
|
||||
nodeId: toNode.id,
|
||||
propertyName: conn.toProperty,
|
||||
variableName
|
||||
});
|
||||
}
|
||||
|
||||
return bindings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从资产格式转换回编辑器格式(用于加载)
|
||||
*
|
||||
* @param asset 行为树资产
|
||||
* @returns 编辑器格式数据
|
||||
*/
|
||||
static fromAsset(asset: BehaviorTreeAsset): EditorFormat {
|
||||
logger.info('开始转换资产格式到编辑器格式');
|
||||
|
||||
const nodes = this.convertNodesFromAsset(asset.nodes);
|
||||
|
||||
const blackboard: Record<string, any> = {};
|
||||
for (const variable of asset.blackboard) {
|
||||
blackboard[variable.name] = variable.defaultValue;
|
||||
}
|
||||
|
||||
const connections = this.convertPropertyBindingsToConnections(
|
||||
asset.propertyBindings || []
|
||||
);
|
||||
|
||||
const nodeConnections = this.buildNodeConnections(asset.nodes);
|
||||
connections.push(...nodeConnections);
|
||||
|
||||
const metadata: { name: string; description?: string; createdAt?: string; modifiedAt?: string } = {
|
||||
name: asset.metadata.name
|
||||
};
|
||||
|
||||
if (asset.metadata.description) {
|
||||
metadata.description = asset.metadata.description;
|
||||
}
|
||||
|
||||
if (asset.metadata.createdAt) {
|
||||
metadata.createdAt = asset.metadata.createdAt;
|
||||
}
|
||||
|
||||
if (asset.metadata.modifiedAt) {
|
||||
metadata.modifiedAt = asset.metadata.modifiedAt;
|
||||
}
|
||||
|
||||
const editorData: EditorFormat = {
|
||||
version: asset.metadata.version,
|
||||
metadata,
|
||||
nodes,
|
||||
connections,
|
||||
blackboard,
|
||||
canvasState: {
|
||||
offset: { x: 0, y: 0 },
|
||||
scale: 1
|
||||
}
|
||||
};
|
||||
|
||||
logger.info(`转换完成: ${nodes.length}个节点, ${connections.length}个连接`);
|
||||
|
||||
return editorData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从资产格式转换节点
|
||||
*/
|
||||
private static convertNodesFromAsset(assetNodes: BehaviorTreeNodeData[]): EditorNode[] {
|
||||
return assetNodes.map((node, index) => {
|
||||
const position = {
|
||||
x: 100 + (index % 5) * 250,
|
||||
y: 100 + Math.floor(index / 5) * 150
|
||||
};
|
||||
|
||||
const template: any = {
|
||||
displayName: node.name,
|
||||
category: this.inferCategory(node.nodeType),
|
||||
type: node.nodeType
|
||||
};
|
||||
|
||||
if (node.data.className) {
|
||||
template.className = node.data.className;
|
||||
}
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
template,
|
||||
data: { ...node.data },
|
||||
position,
|
||||
children: node.children
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 推断节点分类
|
||||
*/
|
||||
private static inferCategory(nodeType: NodeType): string {
|
||||
switch (nodeType) {
|
||||
case NodeType.Action:
|
||||
return '动作';
|
||||
case NodeType.Condition:
|
||||
return '条件';
|
||||
case NodeType.Composite:
|
||||
return '组合';
|
||||
case NodeType.Decorator:
|
||||
return '装饰器';
|
||||
default:
|
||||
return '其他';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将属性绑定转换为连接
|
||||
*/
|
||||
private static convertPropertyBindingsToConnections(
|
||||
bindings: PropertyBinding[]
|
||||
): EditorConnection[] {
|
||||
const connections: EditorConnection[] = [];
|
||||
|
||||
for (const binding of bindings) {
|
||||
connections.push({
|
||||
from: 'blackboard',
|
||||
to: binding.nodeId,
|
||||
toProperty: binding.propertyName,
|
||||
connectionType: 'property'
|
||||
});
|
||||
}
|
||||
|
||||
return connections;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据children关系构建节点连接
|
||||
*/
|
||||
private static buildNodeConnections(nodes: BehaviorTreeNodeData[]): EditorConnection[] {
|
||||
const connections: EditorConnection[] = [];
|
||||
|
||||
for (const node of nodes) {
|
||||
for (const childId of node.children) {
|
||||
connections.push({
|
||||
from: node.id,
|
||||
to: childId,
|
||||
connectionType: 'node'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return connections;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
import { BehaviorTreeData, BehaviorNodeData } from '../execution/BehaviorTreeData';
|
||||
import { NodeType, AbortType } from '../Types/TaskStatus';
|
||||
|
||||
/**
|
||||
* 编辑器节点数据接口
|
||||
*/
|
||||
interface EditorNode {
|
||||
id: string;
|
||||
template: {
|
||||
type: string;
|
||||
className: string;
|
||||
displayName?: string;
|
||||
};
|
||||
data: Record<string, any>;
|
||||
children?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑器连接数据接口
|
||||
*/
|
||||
interface EditorConnection {
|
||||
from: string;
|
||||
to: string;
|
||||
connectionType: 'node' | 'property';
|
||||
fromProperty?: string;
|
||||
toProperty?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑器行为树数据接口
|
||||
*/
|
||||
interface EditorBehaviorTreeData {
|
||||
version?: string;
|
||||
metadata?: {
|
||||
name: string;
|
||||
description?: string;
|
||||
createdAt?: string;
|
||||
modifiedAt?: string;
|
||||
};
|
||||
nodes: EditorNode[];
|
||||
connections?: EditorConnection[];
|
||||
blackboard?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑器格式到运行时格式的转换器
|
||||
*
|
||||
* 负责将编辑器的 JSON 格式(包含UI信息)转换为运行时的 BehaviorTreeData 格式
|
||||
*/
|
||||
export class EditorToBehaviorTreeDataConverter {
|
||||
/**
|
||||
* 将编辑器 JSON 字符串转换为运行时 BehaviorTreeData
|
||||
*/
|
||||
static fromEditorJSON(json: string): BehaviorTreeData {
|
||||
const editorData: EditorBehaviorTreeData = JSON.parse(json);
|
||||
return this.convert(editorData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将编辑器数据对象转换为运行时 BehaviorTreeData
|
||||
*/
|
||||
static convert(editorData: EditorBehaviorTreeData): BehaviorTreeData {
|
||||
// 查找根节点
|
||||
const rootNode = editorData.nodes.find((n) =>
|
||||
n.template.type === 'root' || n.data['nodeType'] === 'root'
|
||||
);
|
||||
|
||||
if (!rootNode) {
|
||||
throw new Error('Behavior tree must have a root node');
|
||||
}
|
||||
|
||||
// 构建属性绑定映射:nodeId -> { propertyName -> blackboardKey }
|
||||
const propertyBindingsMap = this.buildPropertyBindingsMap(editorData);
|
||||
|
||||
// 转换所有节点(过滤掉不可执行的节点,如黑板变量节点)
|
||||
const nodesMap = new Map<string, BehaviorNodeData>();
|
||||
for (const editorNode of editorData.nodes) {
|
||||
// 跳过黑板变量节点,它们只用于编辑器的可视化绑定
|
||||
if (this.isNonExecutableNode(editorNode)) {
|
||||
continue;
|
||||
}
|
||||
const propertyBindings = propertyBindingsMap.get(editorNode.id);
|
||||
const behaviorNodeData = this.convertNode(editorNode, propertyBindings);
|
||||
nodesMap.set(behaviorNodeData.id, behaviorNodeData);
|
||||
}
|
||||
|
||||
// 转换黑板变量
|
||||
const blackboardVariables = editorData.blackboard
|
||||
? new Map(Object.entries(editorData.blackboard))
|
||||
: new Map();
|
||||
|
||||
return {
|
||||
id: this.generateTreeId(editorData),
|
||||
name: editorData.metadata?.name || 'Untitled',
|
||||
rootNodeId: rootNode.id,
|
||||
nodes: nodesMap,
|
||||
blackboardVariables
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从连接数据构建属性绑定映射
|
||||
* 处理 connectionType === 'property' 的连接,将黑板变量节点连接到目标节点的属性
|
||||
*/
|
||||
private static buildPropertyBindingsMap(
|
||||
editorData: EditorBehaviorTreeData
|
||||
): Map<string, Record<string, string>> {
|
||||
const bindingsMap = new Map<string, Record<string, string>>();
|
||||
|
||||
if (!editorData.connections) {
|
||||
return bindingsMap;
|
||||
}
|
||||
|
||||
// 构建节点 ID 到变量名的映射(用于黑板变量节点)
|
||||
const nodeToVariableMap = new Map<string, string>();
|
||||
for (const node of editorData.nodes) {
|
||||
if (node.data['nodeType'] === 'blackboard-variable' && node.data['variableName']) {
|
||||
nodeToVariableMap.set(node.id, node.data['variableName']);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理属性连接
|
||||
for (const conn of editorData.connections) {
|
||||
if (conn.connectionType === 'property' && conn.toProperty) {
|
||||
const variableName = nodeToVariableMap.get(conn.from);
|
||||
if (variableName) {
|
||||
// 获取或创建目标节点的绑定记录
|
||||
let bindings = bindingsMap.get(conn.to);
|
||||
if (!bindings) {
|
||||
bindings = {};
|
||||
bindingsMap.set(conn.to, bindings);
|
||||
}
|
||||
// 将属性绑定到黑板变量
|
||||
bindings[conn.toProperty] = variableName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bindingsMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换单个节点
|
||||
* @param editorNode 编辑器节点数据
|
||||
* @param propertyBindings 从连接中提取的属性绑定(可选)
|
||||
*/
|
||||
private static convertNode(
|
||||
editorNode: EditorNode,
|
||||
propertyBindings?: Record<string, string>
|
||||
): BehaviorNodeData {
|
||||
const nodeType = this.mapNodeType(editorNode.template.type);
|
||||
const config = this.extractConfig(editorNode.data);
|
||||
// 从节点数据中提取绑定
|
||||
const dataBindings = this.extractBindings(editorNode.data);
|
||||
// 合并连接绑定和数据绑定(连接绑定优先)
|
||||
const bindings = { ...dataBindings, ...propertyBindings };
|
||||
const abortType = this.extractAbortType(editorNode.data);
|
||||
|
||||
// 获取 implementationType:优先从 template.className,其次从 data 中的类型字段
|
||||
let implementationType: string | undefined = editorNode.template.className;
|
||||
if (!implementationType) {
|
||||
// 尝试从 data 中提取类型
|
||||
implementationType = this.extractImplementationType(editorNode.data, nodeType);
|
||||
}
|
||||
|
||||
if (!implementationType) {
|
||||
console.warn(`[EditorToBehaviorTreeDataConverter] Node ${editorNode.id} has no implementationType, using fallback`);
|
||||
// 根据节点类型使用默认实现
|
||||
implementationType = this.getDefaultImplementationType(nodeType);
|
||||
}
|
||||
|
||||
return {
|
||||
id: editorNode.id,
|
||||
name: editorNode.template.displayName || editorNode.template.className || implementationType,
|
||||
nodeType,
|
||||
implementationType,
|
||||
children: editorNode.children || [],
|
||||
config,
|
||||
...(Object.keys(bindings).length > 0 && { bindings }),
|
||||
...(abortType && { abortType })
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为不可执行的节点(如黑板变量节点)
|
||||
* 这些节点只在编辑器中使用,不参与运行时执行
|
||||
*/
|
||||
private static isNonExecutableNode(editorNode: EditorNode): boolean {
|
||||
const nodeType = editorNode.data['nodeType'];
|
||||
// 黑板变量节点不需要执行,只用于可视化绑定
|
||||
return nodeType === 'blackboard-variable';
|
||||
}
|
||||
|
||||
/**
|
||||
* 从节点数据中提取实现类型
|
||||
*
|
||||
* 优先级:
|
||||
* 1. template.className(标准方式)
|
||||
* 2. data 中的类型字段(compositeType, actionType 等)
|
||||
* 3. 特殊节点类型的默认值(如 Root)
|
||||
*/
|
||||
private static extractImplementationType(data: Record<string, any>, nodeType: NodeType): string | undefined {
|
||||
// 节点类型到数据字段的映射
|
||||
const typeFieldMap: Record<NodeType, string> = {
|
||||
[NodeType.Composite]: 'compositeType',
|
||||
[NodeType.Decorator]: 'decoratorType',
|
||||
[NodeType.Action]: 'actionType',
|
||||
[NodeType.Condition]: 'conditionType',
|
||||
[NodeType.Root]: '', // Root 没有对应的数据字段
|
||||
};
|
||||
|
||||
const field = typeFieldMap[nodeType];
|
||||
if (field && data[field]) {
|
||||
return data[field];
|
||||
}
|
||||
|
||||
// Root 节点的特殊处理
|
||||
if (nodeType === NodeType.Root) {
|
||||
return 'Root';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点类型的默认实现
|
||||
* 当无法确定具体实现类型时使用
|
||||
*/
|
||||
private static getDefaultImplementationType(nodeType: NodeType): string {
|
||||
// 节点类型到默认实现的映射
|
||||
const defaultImplementations: Record<NodeType, string> = {
|
||||
[NodeType.Root]: 'Root',
|
||||
[NodeType.Composite]: 'Sequence',
|
||||
[NodeType.Decorator]: 'Inverter',
|
||||
[NodeType.Action]: 'Wait',
|
||||
[NodeType.Condition]: 'AlwaysTrue',
|
||||
};
|
||||
|
||||
return defaultImplementations[nodeType] || 'Unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射节点类型
|
||||
*/
|
||||
private static mapNodeType(type: string): NodeType {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'root':
|
||||
return NodeType.Root;
|
||||
case 'composite':
|
||||
return NodeType.Composite;
|
||||
case 'decorator':
|
||||
return NodeType.Decorator;
|
||||
case 'action':
|
||||
return NodeType.Action;
|
||||
case 'condition':
|
||||
return NodeType.Condition;
|
||||
default:
|
||||
throw new Error(`Unknown node type: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取节点配置(过滤掉内部字段和绑定字段)
|
||||
*/
|
||||
private static extractConfig(data: Record<string, any>): Record<string, any> {
|
||||
const config: Record<string, any> = {};
|
||||
const internalFields = new Set(['nodeType', 'abortType']);
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
// 跳过内部字段
|
||||
if (internalFields.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 跳过黑板绑定字段(它们会被提取到 bindings 中)
|
||||
if (this.isBinding(value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
config[key] = value;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取黑板变量绑定
|
||||
*/
|
||||
private static extractBindings(data: Record<string, any>): Record<string, string> {
|
||||
const bindings: Record<string, string> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (this.isBinding(value)) {
|
||||
bindings[key] = this.extractBindingKey(value);
|
||||
}
|
||||
}
|
||||
|
||||
return bindings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为黑板绑定
|
||||
*/
|
||||
private static isBinding(value: any): boolean {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return value._isBlackboardBinding === true ||
|
||||
value.type === 'blackboard' ||
|
||||
(value.blackboardKey !== undefined);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取黑板绑定的键名
|
||||
*/
|
||||
private static extractBindingKey(binding: any): string {
|
||||
return binding.blackboardKey || binding.key || binding.value || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取中止类型(条件装饰器使用)
|
||||
*/
|
||||
private static extractAbortType(data: Record<string, any>): AbortType | undefined {
|
||||
if (!data['abortType']) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const abortTypeStr = String(data['abortType']).toLowerCase();
|
||||
switch (abortTypeStr) {
|
||||
case 'none':
|
||||
return AbortType.None;
|
||||
case 'self':
|
||||
return AbortType.Self;
|
||||
case 'lowerpriority':
|
||||
case 'lower_priority':
|
||||
return AbortType.LowerPriority;
|
||||
case 'both':
|
||||
return AbortType.Both;
|
||||
default:
|
||||
return AbortType.None;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成行为树ID
|
||||
*/
|
||||
private static generateTreeId(editorData: EditorBehaviorTreeData): string {
|
||||
if (editorData.metadata?.name) {
|
||||
// 将名称转换为合法ID(移除特殊字符)
|
||||
return editorData.metadata.name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
}
|
||||
return `tree_${Date.now()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将运行时格式转换回编辑器格式(用于双向转换)
|
||||
*/
|
||||
static toEditorJSON(treeData: BehaviorTreeData): string {
|
||||
const editorData = this.convertToEditor(treeData);
|
||||
return JSON.stringify(editorData, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将运行时 BehaviorTreeData 转换为编辑器格式
|
||||
*/
|
||||
static convertToEditor(treeData: BehaviorTreeData): EditorBehaviorTreeData {
|
||||
const nodes: EditorNode[] = [];
|
||||
|
||||
for (const [_id, nodeData] of treeData.nodes) {
|
||||
nodes.push(this.convertNodeToEditor(nodeData));
|
||||
}
|
||||
|
||||
const blackboard = treeData.blackboardVariables
|
||||
? Object.fromEntries(treeData.blackboardVariables)
|
||||
: {};
|
||||
|
||||
return {
|
||||
version: '1.0.0',
|
||||
metadata: {
|
||||
name: treeData.name,
|
||||
description: '',
|
||||
modifiedAt: new Date().toISOString()
|
||||
},
|
||||
nodes,
|
||||
blackboard
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将运行时节点转换为编辑器节点
|
||||
*/
|
||||
private static convertNodeToEditor(nodeData: BehaviorNodeData): EditorNode {
|
||||
const data: Record<string, any> = { ...nodeData.config };
|
||||
|
||||
// 添加绑定回数据对象
|
||||
if (nodeData.bindings) {
|
||||
for (const [key, blackboardKey] of Object.entries(nodeData.bindings)) {
|
||||
data[key] = {
|
||||
_isBlackboardBinding: true,
|
||||
blackboardKey
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 添加中止类型
|
||||
if (nodeData.abortType !== undefined) {
|
||||
data['abortType'] = nodeData.abortType;
|
||||
}
|
||||
|
||||
// 获取节点类型字符串
|
||||
let typeStr: string;
|
||||
if (typeof nodeData.nodeType === 'string') {
|
||||
typeStr = nodeData.nodeType;
|
||||
} else {
|
||||
typeStr = 'action'; // 默认值
|
||||
}
|
||||
|
||||
const result: EditorNode = {
|
||||
id: nodeData.id,
|
||||
template: {
|
||||
type: typeStr,
|
||||
className: nodeData.implementationType,
|
||||
displayName: nodeData.name
|
||||
},
|
||||
data
|
||||
};
|
||||
|
||||
if (nodeData.children && nodeData.children.length > 0) {
|
||||
result.children = nodeData.children;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
import { NodeType } from '../Types/TaskStatus';
|
||||
import { NodeMetadataRegistry, ConfigFieldDefinition, NodeMetadata } from '../execution/NodeMetadata';
|
||||
|
||||
/**
|
||||
* 节点数据JSON格式
|
||||
*/
|
||||
export interface NodeDataJSON {
|
||||
nodeType: string;
|
||||
compositeType?: string;
|
||||
decoratorType?: string;
|
||||
actionType?: string;
|
||||
conditionType?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树节点属性类型常量
|
||||
* Behavior tree node property type constants
|
||||
*/
|
||||
export const NodePropertyType = {
|
||||
/** 字符串 */
|
||||
String: 'string',
|
||||
/** 数值 */
|
||||
Number: 'number',
|
||||
/** 布尔值 */
|
||||
Boolean: 'boolean',
|
||||
/** 选择框 */
|
||||
Select: 'select',
|
||||
/** 黑板变量引用 */
|
||||
Blackboard: 'blackboard',
|
||||
/** 代码编辑器 */
|
||||
Code: 'code',
|
||||
/** 变量引用 */
|
||||
Variable: 'variable',
|
||||
/** 资产引用 */
|
||||
Asset: 'asset'
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 节点属性类型(支持自定义扩展)
|
||||
* Node property type (supports custom extensions)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 使用内置类型
|
||||
* type: NodePropertyType.String
|
||||
*
|
||||
* // 使用自定义类型
|
||||
* type: 'color-picker'
|
||||
* type: 'curve-editor'
|
||||
* ```
|
||||
*/
|
||||
export type NodePropertyType = (typeof NodePropertyType)[keyof typeof NodePropertyType] | string;
|
||||
|
||||
/**
|
||||
* 属性定义(用于编辑器)
|
||||
*/
|
||||
export interface PropertyDefinition {
|
||||
name: string;
|
||||
type: NodePropertyType;
|
||||
label: string;
|
||||
description?: string;
|
||||
defaultValue?: any;
|
||||
options?: Array<{ label: string; value: any }>;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
required?: boolean;
|
||||
|
||||
/**
|
||||
* 字段编辑器配置
|
||||
*
|
||||
* 指定使用哪个字段编辑器以及相关选项
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* fieldEditor: {
|
||||
* type: 'asset',
|
||||
* options: { fileExtension: '.btree' }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
fieldEditor?: {
|
||||
type: string;
|
||||
options?: Record<string, any>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 自定义渲染配置
|
||||
*
|
||||
* 用于指定编辑器如何渲染此属性
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* renderConfig: {
|
||||
* component: 'ColorPicker', // 渲染器组件名称
|
||||
* props: { // 传递给组件的属性
|
||||
* showAlpha: true,
|
||||
* presets: ['#FF0000', '#00FF00']
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
renderConfig?: {
|
||||
/** 渲染器组件名称或路径 */
|
||||
component?: string;
|
||||
/** 传递给渲染器的属性配置 */
|
||||
props?: Record<string, any>;
|
||||
/** 渲染器的样式类名 */
|
||||
className?: string;
|
||||
/** 渲染器的内联样式 */
|
||||
style?: Record<string, any>;
|
||||
/** 其他自定义配置 */
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证规则
|
||||
*
|
||||
* 用于在编辑器中验证输入
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* validation: {
|
||||
* pattern: /^\d+$/,
|
||||
* message: '只能输入数字',
|
||||
* validator: (value) => value > 0
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
validation?: {
|
||||
/** 正则表达式验证 */
|
||||
pattern?: RegExp | string;
|
||||
/** 验证失败的提示信息 */
|
||||
message?: string;
|
||||
/** 自定义验证函数 */
|
||||
validator?: string; // 函数字符串,编辑器会解析
|
||||
/** 最小长度(字符串) */
|
||||
minLength?: number;
|
||||
/** 最大长度(字符串) */
|
||||
maxLength?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* 是否允许多个连接
|
||||
* 默认 false,只允许一个黑板变量连接
|
||||
*/
|
||||
allowMultipleConnections?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点模板(用于编辑器)
|
||||
*/
|
||||
export interface NodeTemplate {
|
||||
type: NodeType;
|
||||
displayName: string;
|
||||
category: string;
|
||||
icon?: string;
|
||||
description: string;
|
||||
color?: string;
|
||||
className?: string;
|
||||
componentClass?: Function;
|
||||
requiresChildren?: boolean;
|
||||
minChildren?: number;
|
||||
maxChildren?: number;
|
||||
defaultConfig: Partial<NodeDataJSON>;
|
||||
properties: PropertyDefinition[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点模板库
|
||||
*/
|
||||
export class NodeTemplates {
|
||||
/**
|
||||
* 获取所有节点模板
|
||||
*/
|
||||
static getAllTemplates(): NodeTemplate[] {
|
||||
const allMetadata = NodeMetadataRegistry.getAllMetadata();
|
||||
return allMetadata.map((metadata) => this.convertMetadataToTemplate(metadata));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据类型和子类型获取模板
|
||||
*/
|
||||
static getTemplate(type: NodeType, subType: string): NodeTemplate | undefined {
|
||||
return this.getAllTemplates().find((t) => {
|
||||
if (t.type !== type) return false;
|
||||
const config: any = t.defaultConfig;
|
||||
|
||||
switch (type) {
|
||||
case NodeType.Composite:
|
||||
return config.compositeType === subType;
|
||||
case NodeType.Decorator:
|
||||
return config.decoratorType === subType;
|
||||
case NodeType.Action:
|
||||
return config.actionType === subType;
|
||||
case NodeType.Condition:
|
||||
return config.conditionType === subType;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 将NodeMetadata转换为NodeTemplate
|
||||
*/
|
||||
private static convertMetadataToTemplate(metadata: NodeMetadata): NodeTemplate {
|
||||
const properties = this.convertConfigSchemaToProperties(metadata.configSchema || {});
|
||||
|
||||
const defaultConfig: Partial<NodeDataJSON> = {
|
||||
nodeType: this.nodeTypeToString(metadata.nodeType)
|
||||
};
|
||||
|
||||
switch (metadata.nodeType) {
|
||||
case NodeType.Composite:
|
||||
defaultConfig.compositeType = metadata.implementationType;
|
||||
break;
|
||||
case NodeType.Decorator:
|
||||
defaultConfig.decoratorType = metadata.implementationType;
|
||||
break;
|
||||
case NodeType.Action:
|
||||
defaultConfig.actionType = metadata.implementationType;
|
||||
break;
|
||||
case NodeType.Condition:
|
||||
defaultConfig.conditionType = metadata.implementationType;
|
||||
break;
|
||||
}
|
||||
|
||||
if (metadata.configSchema) {
|
||||
for (const [key, field] of Object.entries(metadata.configSchema)) {
|
||||
const fieldDef = field as ConfigFieldDefinition;
|
||||
if (fieldDef.default !== undefined) {
|
||||
defaultConfig[key] = fieldDef.default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据节点类型生成默认颜色和图标
|
||||
const { icon, color } = this.getIconAndColorByType(metadata.nodeType, metadata.category || '');
|
||||
|
||||
// 应用子节点约束
|
||||
const constraints = metadata.childrenConstraints || this.getDefaultConstraintsByNodeType(metadata.nodeType);
|
||||
|
||||
const template: NodeTemplate = {
|
||||
type: metadata.nodeType,
|
||||
displayName: metadata.displayName,
|
||||
category: metadata.category || this.getCategoryByNodeType(metadata.nodeType),
|
||||
description: metadata.description || '',
|
||||
className: metadata.implementationType,
|
||||
icon,
|
||||
color,
|
||||
defaultConfig,
|
||||
properties
|
||||
};
|
||||
|
||||
if (constraints) {
|
||||
if (constraints.min !== undefined) {
|
||||
template.minChildren = constraints.min;
|
||||
template.requiresChildren = constraints.min > 0;
|
||||
}
|
||||
if (constraints.max !== undefined) {
|
||||
template.maxChildren = constraints.max;
|
||||
}
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点类型的默认约束
|
||||
*/
|
||||
private static getDefaultConstraintsByNodeType(nodeType: NodeType): { min?: number; max?: number } | undefined {
|
||||
switch (nodeType) {
|
||||
case NodeType.Composite:
|
||||
return { min: 1 };
|
||||
case NodeType.Decorator:
|
||||
return { min: 1, max: 1 };
|
||||
case NodeType.Action:
|
||||
case NodeType.Condition:
|
||||
return { max: 0 };
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将ConfigSchema转换为PropertyDefinition数组
|
||||
*/
|
||||
private static convertConfigSchemaToProperties(
|
||||
configSchema: Record<string, ConfigFieldDefinition>
|
||||
): PropertyDefinition[] {
|
||||
const properties: PropertyDefinition[] = [];
|
||||
|
||||
for (const [name, field] of Object.entries(configSchema)) {
|
||||
const property: PropertyDefinition = {
|
||||
name,
|
||||
type: this.mapFieldTypeToPropertyType(field),
|
||||
label: name
|
||||
};
|
||||
|
||||
if (field.description !== undefined) {
|
||||
property.description = field.description;
|
||||
}
|
||||
|
||||
if (field.default !== undefined) {
|
||||
property.defaultValue = field.default;
|
||||
}
|
||||
|
||||
if (field.min !== undefined) {
|
||||
property.min = field.min;
|
||||
}
|
||||
|
||||
if (field.max !== undefined) {
|
||||
property.max = field.max;
|
||||
}
|
||||
|
||||
if (field.allowMultipleConnections !== undefined) {
|
||||
property.allowMultipleConnections = field.allowMultipleConnections;
|
||||
}
|
||||
|
||||
if (field.options) {
|
||||
property.options = field.options.map((opt) => ({
|
||||
label: opt,
|
||||
value: opt
|
||||
}));
|
||||
}
|
||||
|
||||
if (field.supportBinding) {
|
||||
property.renderConfig = {
|
||||
component: 'BindableInput',
|
||||
props: {
|
||||
supportBinding: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
properties.push(property);
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射字段类型到属性类型
|
||||
*/
|
||||
private static mapFieldTypeToPropertyType(field: ConfigFieldDefinition): NodePropertyType {
|
||||
if (field.options && field.options.length > 0) {
|
||||
return NodePropertyType.Select;
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
case 'string':
|
||||
return NodePropertyType.String;
|
||||
case 'number':
|
||||
return NodePropertyType.Number;
|
||||
case 'boolean':
|
||||
return NodePropertyType.Boolean;
|
||||
case 'array':
|
||||
case 'object':
|
||||
default:
|
||||
return NodePropertyType.String;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NodeType转字符串
|
||||
*/
|
||||
private static nodeTypeToString(nodeType: NodeType): string {
|
||||
switch (nodeType) {
|
||||
case NodeType.Composite:
|
||||
return 'composite';
|
||||
case NodeType.Decorator:
|
||||
return 'decorator';
|
||||
case NodeType.Action:
|
||||
return 'action';
|
||||
case NodeType.Condition:
|
||||
return 'condition';
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据NodeType获取默认分类
|
||||
*/
|
||||
private static getCategoryByNodeType(nodeType: NodeType): string {
|
||||
switch (nodeType) {
|
||||
case NodeType.Composite:
|
||||
return '组合';
|
||||
case NodeType.Decorator:
|
||||
return '装饰器';
|
||||
case NodeType.Action:
|
||||
return '动作';
|
||||
case NodeType.Condition:
|
||||
return '条件';
|
||||
default:
|
||||
return '其他';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据节点类型获取默认图标和颜色
|
||||
*/
|
||||
private static getIconAndColorByType(nodeType: NodeType, _category: string): { icon: string; color: string } {
|
||||
// 根据节点类型设置默认值
|
||||
switch (nodeType) {
|
||||
case NodeType.Composite:
|
||||
return { icon: 'GitBranch', color: '#1976d2' }; // 蓝色
|
||||
case NodeType.Decorator:
|
||||
return { icon: 'Settings', color: '#fb8c00' }; // 橙色
|
||||
case NodeType.Action:
|
||||
return { icon: 'Play', color: '#388e3c' }; // 绿色
|
||||
case NodeType.Condition:
|
||||
return { icon: 'HelpCircle', color: '#d32f2f' }; // 红色
|
||||
default:
|
||||
return { icon: 'Circle', color: '#757575' }; // 灰色
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
import { BlackboardValueType, BlackboardVariable } from '../Types/TaskStatus';
|
||||
|
||||
/**
|
||||
* 全局黑板配置
|
||||
*/
|
||||
export interface GlobalBlackboardConfig {
|
||||
version: string;
|
||||
variables: BlackboardVariable[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局黑板服务
|
||||
*
|
||||
* 提供所有行为树共享的全局变量存储
|
||||
*
|
||||
* 使用方式:
|
||||
* ```typescript
|
||||
* // 注册服务(在 BehaviorTreePlugin.install 中自动完成)
|
||||
* core.services.registerSingleton(GlobalBlackboardService);
|
||||
*
|
||||
* // 获取服务
|
||||
* const blackboard = core.services.resolve(GlobalBlackboardService);
|
||||
* ```
|
||||
*/
|
||||
export class GlobalBlackboardService implements IService {
|
||||
private variables: Map<string, BlackboardVariable> = new Map();
|
||||
|
||||
dispose(): void {
|
||||
this.variables.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义全局变量
|
||||
*/
|
||||
defineVariable(
|
||||
name: string,
|
||||
type: BlackboardValueType,
|
||||
initialValue: any,
|
||||
options?: {
|
||||
readonly?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
): void {
|
||||
const variable: BlackboardVariable = {
|
||||
name,
|
||||
type,
|
||||
value: initialValue,
|
||||
readonly: options?.readonly ?? false
|
||||
};
|
||||
|
||||
if (options?.description !== undefined) {
|
||||
variable.description = options.description;
|
||||
}
|
||||
|
||||
this.variables.set(name, variable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局变量值
|
||||
*/
|
||||
getValue<T = any>(name: string): T | undefined {
|
||||
const variable = this.variables.get(name);
|
||||
return variable?.value as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置全局变量值
|
||||
*/
|
||||
setValue(name: string, value: any, force: boolean = false): boolean {
|
||||
const variable = this.variables.get(name);
|
||||
|
||||
if (!variable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (variable.readonly && !force) {
|
||||
return false;
|
||||
}
|
||||
|
||||
variable.value = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查全局变量是否存在
|
||||
*/
|
||||
hasVariable(name: string): boolean {
|
||||
return this.variables.has(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除全局变量
|
||||
*/
|
||||
removeVariable(name: string): boolean {
|
||||
return this.variables.delete(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有变量名
|
||||
*/
|
||||
getVariableNames(): string[] {
|
||||
return Array.from(this.variables.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有变量
|
||||
*/
|
||||
getAllVariables(): BlackboardVariable[] {
|
||||
return Array.from(this.variables.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有全局变量
|
||||
*/
|
||||
clear(): void {
|
||||
this.variables.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置变量
|
||||
*/
|
||||
setVariables(values: Record<string, any>): void {
|
||||
for (const [name, value] of Object.entries(values)) {
|
||||
const variable = this.variables.get(name);
|
||||
if (variable && !variable.readonly) {
|
||||
variable.value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取变量
|
||||
*/
|
||||
getVariables(names: string[]): Record<string, any> {
|
||||
const result: Record<string, any> = {};
|
||||
for (const name of names) {
|
||||
const value = this.getValue(name);
|
||||
if (value !== undefined) {
|
||||
result[name] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出配置
|
||||
*/
|
||||
exportConfig(): GlobalBlackboardConfig {
|
||||
return {
|
||||
version: '1.0',
|
||||
variables: Array.from(this.variables.values())
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入配置
|
||||
*/
|
||||
importConfig(config: GlobalBlackboardConfig): void {
|
||||
this.variables.clear();
|
||||
for (const variable of config.variables) {
|
||||
this.variables.set(variable.name, variable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化为 JSON
|
||||
*/
|
||||
toJSON(): string {
|
||||
return JSON.stringify(this.exportConfig(), null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JSON 反序列化
|
||||
*/
|
||||
static fromJSON(json: string): GlobalBlackboardConfig {
|
||||
return JSON.parse(json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @zh 资产管理器接口(可选依赖)
|
||||
* @en Asset manager interface (optional dependency)
|
||||
*
|
||||
* @zh 行为树系统的可选资产管理器接口。
|
||||
* 当与 ESEngine 的 asset-system 集成时,传入 IAssetManager 实例。
|
||||
* 不使用 ESEngine 时,可以直接使用 BehaviorTreeAssetManager.loadFromEditorJSON()。
|
||||
*
|
||||
* @en Optional asset manager interface for behavior tree system.
|
||||
* When integrating with ESEngine's asset-system, pass an IAssetManager instance.
|
||||
* When not using ESEngine, use BehaviorTreeAssetManager.loadFromEditorJSON() directly.
|
||||
*/
|
||||
|
||||
import type { BehaviorTreeData } from '../execution/BehaviorTreeData';
|
||||
|
||||
/**
|
||||
* @zh 行为树资产内容
|
||||
* @en Behavior tree asset content
|
||||
*/
|
||||
export interface IBehaviorTreeAssetContent {
|
||||
/** @zh 行为树数据 @en Behavior tree data */
|
||||
data: BehaviorTreeData;
|
||||
/** @zh 文件路径 @en File path */
|
||||
path: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 简化的资产管理器接口
|
||||
* @en Simplified asset manager interface
|
||||
*
|
||||
* @zh 这是行为树系统需要的最小资产管理器接口。
|
||||
* ESEngine 的 IAssetManager 实现此接口。
|
||||
* 其他引擎可以提供自己的实现。
|
||||
*
|
||||
* @en This is the minimal asset manager interface required by the behavior tree system.
|
||||
* ESEngine's IAssetManager implements this interface.
|
||||
* Other engines can provide their own implementation.
|
||||
*/
|
||||
export interface IBTAssetManager {
|
||||
/**
|
||||
* @zh 通过 GUID 加载资产
|
||||
* @en Load asset by GUID
|
||||
*/
|
||||
loadAsset(guid: string): Promise<{ asset: IBehaviorTreeAssetContent | null } | null>;
|
||||
|
||||
/**
|
||||
* @zh 通过 GUID 获取已加载的资产
|
||||
* @en Get loaded asset by GUID
|
||||
*/
|
||||
getAsset<T = IBehaviorTreeAssetContent>(guid: string): T | null;
|
||||
}
|
||||
127
packages/framework/behavior-tree/src/Types/TaskStatus.ts
Normal file
127
packages/framework/behavior-tree/src/Types/TaskStatus.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* 任务执行状态
|
||||
*/
|
||||
export enum TaskStatus {
|
||||
/** 无效状态 - 节点未初始化或已被重置 */
|
||||
Invalid = 0,
|
||||
/** 成功 - 节点执行成功完成 */
|
||||
Success = 1,
|
||||
/** 失败 - 节点执行失败 */
|
||||
Failure = 2,
|
||||
/** 运行中 - 节点正在执行,需要在后续帧继续 */
|
||||
Running = 3
|
||||
}
|
||||
|
||||
/**
|
||||
* 内置节点类型常量
|
||||
*/
|
||||
export const NodeType = {
|
||||
/** 根节点 - 行为树的起始节点 */
|
||||
Root: 'root',
|
||||
/** 复合节点 - 有多个子节点 */
|
||||
Composite: 'composite',
|
||||
/** 装饰器节点 - 有一个子节点 */
|
||||
Decorator: 'decorator',
|
||||
/** 动作节点 - 叶子节点 */
|
||||
Action: 'action',
|
||||
/** 条件节点 - 叶子节点 */
|
||||
Condition: 'condition'
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 节点类型(支持自定义扩展)
|
||||
*
|
||||
* 使用内置类型或自定义字符串
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 使用内置类型
|
||||
* type: NodeType.Action
|
||||
*
|
||||
* // 使用自定义类型
|
||||
* type: 'custom-behavior'
|
||||
* ```
|
||||
*/
|
||||
export type NodeType = typeof NodeType[keyof typeof NodeType] | string;
|
||||
|
||||
/**
|
||||
* 复合节点类型
|
||||
*/
|
||||
export enum CompositeType {
|
||||
/** 序列 - 按顺序执行,全部成功才成功 */
|
||||
Sequence = 'sequence',
|
||||
/** 选择 - 按顺序执行,任一成功则成功 */
|
||||
Selector = 'selector',
|
||||
/** 并行 - 同时执行所有子节点 */
|
||||
Parallel = 'parallel',
|
||||
/** 并行选择 - 并行执行,任一成功则成功 */
|
||||
ParallelSelector = 'parallel-selector',
|
||||
/** 随机序列 - 随机顺序执行序列 */
|
||||
RandomSequence = 'random-sequence',
|
||||
/** 随机选择 - 随机顺序执行选择 */
|
||||
RandomSelector = 'random-selector'
|
||||
}
|
||||
|
||||
/**
|
||||
* 装饰器节点类型
|
||||
*/
|
||||
export enum DecoratorType {
|
||||
/** 反转 - 反转子节点结果 */
|
||||
Inverter = 'inverter',
|
||||
/** 重复 - 重复执行子节点 */
|
||||
Repeater = 'repeater',
|
||||
/** 直到成功 - 重复直到成功 */
|
||||
UntilSuccess = 'until-success',
|
||||
/** 直到失败 - 重复直到失败 */
|
||||
UntilFail = 'until-fail',
|
||||
/** 总是成功 - 无论子节点结果都返回成功 */
|
||||
AlwaysSucceed = 'always-succeed',
|
||||
/** 总是失败 - 无论子节点结果都返回失败 */
|
||||
AlwaysFail = 'always-fail',
|
||||
/** 条件装饰器 - 基于条件执行子节点 */
|
||||
Conditional = 'conditional',
|
||||
/** 冷却 - 冷却时间内阻止执行 */
|
||||
Cooldown = 'cooldown',
|
||||
/** 超时 - 超时则返回失败 */
|
||||
Timeout = 'timeout'
|
||||
}
|
||||
|
||||
/**
|
||||
* 中止类型
|
||||
*
|
||||
* 用于动态优先级和条件重新评估
|
||||
*/
|
||||
export enum AbortType {
|
||||
/** 无 - 不中止任何节点 */
|
||||
None = 'none',
|
||||
/** 自身 - 条件变化时可以中止自身的执行 */
|
||||
Self = 'self',
|
||||
/** 低优先级 - 条件满足时可以中止低优先级的兄弟节点 */
|
||||
LowerPriority = 'lower-priority',
|
||||
/** 两者 - 可以中止自身和低优先级节点 */
|
||||
Both = 'both'
|
||||
}
|
||||
|
||||
/**
|
||||
* 黑板变量类型
|
||||
*/
|
||||
export enum BlackboardValueType {
|
||||
String = 'string',
|
||||
Number = 'number',
|
||||
Boolean = 'boolean',
|
||||
Vector2 = 'vector2',
|
||||
Vector3 = 'vector3',
|
||||
Object = 'object',
|
||||
Array = 'array'
|
||||
}
|
||||
|
||||
/**
|
||||
* 黑板变量定义
|
||||
*/
|
||||
export interface BlackboardVariable {
|
||||
name: string;
|
||||
type: BlackboardValueType;
|
||||
value: any;
|
||||
readonly?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
10
packages/framework/behavior-tree/src/constants.ts
Normal file
10
packages/framework/behavior-tree/src/constants.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Behavior Tree Constants
|
||||
* 行为树常量
|
||||
*/
|
||||
|
||||
// Asset type constant for behavior tree
|
||||
// 行为树资产类型常量
|
||||
// 必须与 module.json 中 assetExtensions 定义的类型一致
|
||||
// Must match the type defined in module.json assetExtensions
|
||||
export const BehaviorTreeAssetType = 'behavior-tree' as const;
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @zh ESEngine 资产加载器
|
||||
* @en ESEngine asset loader
|
||||
*
|
||||
* @zh 实现 IAssetLoader 接口,用于通过 AssetManager 加载行为树文件。
|
||||
* 此文件仅在使用 ESEngine 时需要。
|
||||
*
|
||||
* @en Implements IAssetLoader interface for loading behavior tree files via AssetManager.
|
||||
* This file is only needed when using ESEngine.
|
||||
*/
|
||||
|
||||
import type {
|
||||
IAssetLoader,
|
||||
IAssetParseContext,
|
||||
IAssetContent,
|
||||
AssetContentType
|
||||
} from '@esengine/asset-system';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreeData } from '../execution/BehaviorTreeData';
|
||||
import { BehaviorTreeAssetManager } from '../execution/BehaviorTreeAssetManager';
|
||||
import { EditorToBehaviorTreeDataConverter } from '../Serialization/EditorToBehaviorTreeDataConverter';
|
||||
import { BehaviorTreeAssetType } from '../constants';
|
||||
|
||||
/**
|
||||
* @zh 行为树资产接口
|
||||
* @en Behavior tree asset interface
|
||||
*/
|
||||
export interface IBehaviorTreeAsset {
|
||||
/** @zh 行为树数据 @en Behavior tree data */
|
||||
data: BehaviorTreeData;
|
||||
/** @zh 文件路径 @en File path */
|
||||
path: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 行为树加载器
|
||||
* @en Behavior tree loader implementing IAssetLoader interface
|
||||
*/
|
||||
export class BehaviorTreeLoader implements IAssetLoader<IBehaviorTreeAsset> {
|
||||
readonly supportedType = BehaviorTreeAssetType;
|
||||
readonly supportedExtensions = ['.btree'];
|
||||
readonly contentType: AssetContentType = 'text';
|
||||
|
||||
/**
|
||||
* @zh 从内容解析行为树资产
|
||||
* @en Parse behavior tree asset from content
|
||||
*/
|
||||
async parse(content: IAssetContent, context: IAssetParseContext): Promise<IBehaviorTreeAsset> {
|
||||
if (!content.text) {
|
||||
throw new Error('Behavior tree content is empty');
|
||||
}
|
||||
|
||||
// Convert to runtime data
|
||||
const treeData = EditorToBehaviorTreeDataConverter.fromEditorJSON(content.text);
|
||||
|
||||
// Use file path as ID
|
||||
const assetPath = context.metadata.path;
|
||||
treeData.id = assetPath;
|
||||
|
||||
// Also register to BehaviorTreeAssetManager for legacy code
|
||||
const btAssetManager = Core.services.tryResolve(BehaviorTreeAssetManager);
|
||||
if (btAssetManager) {
|
||||
btAssetManager.loadAsset(treeData);
|
||||
}
|
||||
|
||||
return {
|
||||
data: treeData,
|
||||
path: assetPath
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 释放资产
|
||||
* @en Dispose asset
|
||||
*/
|
||||
dispose(asset: IBehaviorTreeAsset): void {
|
||||
const btAssetManager = Core.services.tryResolve(BehaviorTreeAssetManager);
|
||||
if (btAssetManager && asset.data) {
|
||||
btAssetManager.unloadAsset(asset.data.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @zh ESEngine 集成模块
|
||||
* @en ESEngine integration module
|
||||
*
|
||||
* @zh 此文件包含与 ESEngine 引擎核心集成的代码。
|
||||
* 使用 Cocos/Laya 等其他引擎时不需要此文件。
|
||||
*
|
||||
* @en This file contains code for integrating with ESEngine engine-core.
|
||||
* Not needed when using other engines like Cocos/Laya.
|
||||
*/
|
||||
|
||||
import type { IScene, ServiceContainer, IComponentRegistry } from '@esengine/ecs-framework';
|
||||
import type { IRuntimeModule, IRuntimePlugin, ModuleManifest, SystemContext } from '@esengine/engine-core';
|
||||
import { AssetManagerToken } from '@esengine/asset-system';
|
||||
|
||||
import { BehaviorTreeRuntimeComponent } from '../execution/BehaviorTreeRuntimeComponent';
|
||||
import { BehaviorTreeExecutionSystem } from '../execution/BehaviorTreeExecutionSystem';
|
||||
import { BehaviorTreeAssetManager } from '../execution/BehaviorTreeAssetManager';
|
||||
import { GlobalBlackboardService } from '../Services/GlobalBlackboardService';
|
||||
import { BehaviorTreeLoader } from './BehaviorTreeLoader';
|
||||
import { BehaviorTreeAssetType } from '../constants';
|
||||
import { BehaviorTreeSystemToken } from '../tokens';
|
||||
|
||||
// Re-export tokens for ESEngine users
|
||||
export { BehaviorTreeSystemToken } from '../tokens';
|
||||
|
||||
class BehaviorTreeRuntimeModule implements IRuntimeModule {
|
||||
private _loaderRegistered = false;
|
||||
|
||||
registerComponents(registry: IComponentRegistry): void {
|
||||
registry.register(BehaviorTreeRuntimeComponent);
|
||||
}
|
||||
|
||||
registerServices(services: ServiceContainer): void {
|
||||
if (!services.isRegistered(GlobalBlackboardService)) {
|
||||
services.registerSingleton(GlobalBlackboardService);
|
||||
}
|
||||
if (!services.isRegistered(BehaviorTreeAssetManager)) {
|
||||
services.registerSingleton(BehaviorTreeAssetManager);
|
||||
}
|
||||
}
|
||||
|
||||
createSystems(scene: IScene, context: SystemContext): void {
|
||||
// Get dependencies from service registry
|
||||
const assetManager = context.services.get(AssetManagerToken);
|
||||
|
||||
if (!this._loaderRegistered && assetManager) {
|
||||
assetManager.registerLoader(BehaviorTreeAssetType, new BehaviorTreeLoader());
|
||||
this._loaderRegistered = true;
|
||||
}
|
||||
|
||||
// Use ECS service container from context.services
|
||||
const ecsServices = (context as { ecsServices?: ServiceContainer }).ecsServices;
|
||||
const behaviorTreeSystem = new BehaviorTreeExecutionSystem(ecsServices);
|
||||
|
||||
if (assetManager) {
|
||||
behaviorTreeSystem.setAssetManager(assetManager);
|
||||
}
|
||||
|
||||
if (context.isEditor) {
|
||||
behaviorTreeSystem.enabled = false;
|
||||
}
|
||||
|
||||
scene.addSystem(behaviorTreeSystem);
|
||||
|
||||
// Register service to service registry
|
||||
context.services.register(BehaviorTreeSystemToken, behaviorTreeSystem);
|
||||
}
|
||||
}
|
||||
|
||||
const manifest: ModuleManifest = {
|
||||
id: 'behavior-tree',
|
||||
name: '@esengine/behavior-tree',
|
||||
displayName: 'Behavior Tree',
|
||||
version: '1.0.0',
|
||||
description: 'AI behavior tree system',
|
||||
category: 'AI',
|
||||
icon: 'GitBranch',
|
||||
isCore: false,
|
||||
defaultEnabled: false,
|
||||
isEngineModule: true,
|
||||
canContainContent: true,
|
||||
dependencies: ['core'],
|
||||
exports: { components: ['BehaviorTreeComponent'] },
|
||||
editorPackage: '@esengine/behavior-tree-editor'
|
||||
};
|
||||
|
||||
export const BehaviorTreePlugin: IRuntimePlugin = {
|
||||
manifest,
|
||||
runtimeModule: new BehaviorTreeRuntimeModule()
|
||||
};
|
||||
|
||||
export { BehaviorTreeRuntimeModule };
|
||||
39
packages/framework/behavior-tree/src/esengine/index.ts
Normal file
39
packages/framework/behavior-tree/src/esengine/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @zh ESEngine 集成入口
|
||||
* @en ESEngine integration entry point
|
||||
*
|
||||
* @zh 此模块包含与 ESEngine 引擎核心集成所需的所有代码。
|
||||
* 使用 Cocos/Laya 等其他引擎时,只需导入主模块即可。
|
||||
*
|
||||
* @en This module contains all code required for ESEngine engine-core integration.
|
||||
* When using other engines like Cocos/Laya, just import the main module.
|
||||
*
|
||||
* @example ESEngine 使用方式 / ESEngine usage:
|
||||
* ```typescript
|
||||
* import { BehaviorTreePlugin } from '@esengine/behavior-tree/esengine';
|
||||
*
|
||||
* // Register with ESEngine plugin system
|
||||
* engine.registerPlugin(BehaviorTreePlugin);
|
||||
* ```
|
||||
*
|
||||
* @example Cocos/Laya 使用方式 / Cocos/Laya usage:
|
||||
* ```typescript
|
||||
* import {
|
||||
* BehaviorTreeAssetManager,
|
||||
* BehaviorTreeExecutionSystem
|
||||
* } from '@esengine/behavior-tree';
|
||||
*
|
||||
* // Load behavior tree from JSON
|
||||
* const assetManager = new BehaviorTreeAssetManager();
|
||||
* assetManager.loadFromEditorJSON(jsonContent);
|
||||
*
|
||||
* // Add system to your ECS world
|
||||
* world.addSystem(new BehaviorTreeExecutionSystem());
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Runtime module and plugin
|
||||
export { BehaviorTreeRuntimeModule, BehaviorTreePlugin, BehaviorTreeSystemToken } from './BehaviorTreeRuntimeModule';
|
||||
|
||||
// Asset loader for ESEngine asset-system
|
||||
export { BehaviorTreeLoader, type IBehaviorTreeAsset } from './BehaviorTreeLoader';
|
||||
@@ -0,0 +1,136 @@
|
||||
import { BehaviorTreeData } from './BehaviorTreeData';
|
||||
import { createLogger, IService } from '@esengine/ecs-framework';
|
||||
import { EditorToBehaviorTreeDataConverter } from '../Serialization/EditorToBehaviorTreeDataConverter';
|
||||
|
||||
const logger = createLogger('BehaviorTreeAssetManager');
|
||||
|
||||
/**
|
||||
* 行为树资产管理器(服务)
|
||||
*
|
||||
* 管理所有共享的BehaviorTreeData
|
||||
* 多个实例可以引用同一份数据
|
||||
*
|
||||
* 使用方式:
|
||||
* ```typescript
|
||||
* // 注册服务
|
||||
* Core.services.registerSingleton(BehaviorTreeAssetManager);
|
||||
*
|
||||
* // 使用服务
|
||||
* const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
* ```
|
||||
*/
|
||||
export class BehaviorTreeAssetManager implements IService {
|
||||
/**
|
||||
* 已加载的行为树资产
|
||||
*/
|
||||
private assets: Map<string, BehaviorTreeData> = new Map();
|
||||
|
||||
/**
|
||||
* 加载行为树资产
|
||||
*/
|
||||
loadAsset(asset: BehaviorTreeData): void {
|
||||
if (this.assets.has(asset.id)) {
|
||||
logger.warn(`行为树资产已存在,将被覆盖: ${asset.id}`);
|
||||
}
|
||||
this.assets.set(asset.id, asset);
|
||||
logger.info(`行为树资产已加载: ${asset.name} (${asset.nodes.size}个节点)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从编辑器 JSON 格式加载行为树资产
|
||||
*
|
||||
* @param json 编辑器导出的 JSON 字符串
|
||||
* @returns 加载的行为树数据
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
* const jsonContent = await readFile('path/to/tree.btree');
|
||||
* const treeData = assetManager.loadFromEditorJSON(jsonContent);
|
||||
* ```
|
||||
*/
|
||||
loadFromEditorJSON(json: string): BehaviorTreeData {
|
||||
try {
|
||||
const treeData = EditorToBehaviorTreeDataConverter.fromEditorJSON(json);
|
||||
this.loadAsset(treeData);
|
||||
return treeData;
|
||||
} catch (error) {
|
||||
logger.error('从编辑器JSON加载失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量加载多个行为树资产(从编辑器JSON)
|
||||
*
|
||||
* @param jsonDataList JSON字符串列表
|
||||
* @returns 成功加载的资产数量
|
||||
*/
|
||||
loadMultipleFromEditorJSON(jsonDataList: string[]): number {
|
||||
let successCount = 0;
|
||||
for (const json of jsonDataList) {
|
||||
try {
|
||||
this.loadFromEditorJSON(json);
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
logger.error('批量加载时出错:', error);
|
||||
}
|
||||
}
|
||||
logger.info(`批量加载完成: ${successCount}/${jsonDataList.length} 个资产`);
|
||||
return successCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取行为树资产
|
||||
*/
|
||||
getAsset(assetId: string): BehaviorTreeData | undefined {
|
||||
return this.assets.get(assetId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查资产是否存在
|
||||
*/
|
||||
hasAsset(assetId: string): boolean {
|
||||
return this.assets.has(assetId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载行为树资产
|
||||
*/
|
||||
unloadAsset(assetId: string): boolean {
|
||||
const result = this.assets.delete(assetId);
|
||||
if (result) {
|
||||
logger.info(`行为树资产已卸载: ${assetId}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有资产
|
||||
*/
|
||||
clearAll(): void {
|
||||
this.assets.clear();
|
||||
logger.info('所有行为树资产已清空');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已加载资产数量
|
||||
*/
|
||||
getAssetCount(): number {
|
||||
return this.assets.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有资产ID
|
||||
*/
|
||||
getAllAssetIds(): string[] {
|
||||
return Array.from(this.assets.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源(实现IService接口)
|
||||
*/
|
||||
dispose(): void {
|
||||
this.clearAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { TaskStatus, NodeType, AbortType } from '../Types/TaskStatus';
|
||||
|
||||
/**
|
||||
* 行为树节点定义(纯数据结构)
|
||||
*
|
||||
* 不依赖Entity,可以被多个实例共享
|
||||
*/
|
||||
export interface BehaviorNodeData {
|
||||
/** 节点唯一ID */
|
||||
id: string;
|
||||
|
||||
/** 节点名称(用于调试) */
|
||||
name: string;
|
||||
|
||||
/** 节点类型 */
|
||||
nodeType: NodeType;
|
||||
|
||||
/** 节点实现类型(对应Component类名) */
|
||||
implementationType: string;
|
||||
|
||||
/** 子节点ID列表 */
|
||||
children?: string[];
|
||||
|
||||
/** 节点特定配置数据 */
|
||||
config: Record<string, any>;
|
||||
|
||||
/** 属性到黑板变量的绑定映射 */
|
||||
bindings?: Record<string, string>;
|
||||
|
||||
/** 中止类型(条件装饰器使用) */
|
||||
abortType?: AbortType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树定义(可共享的Asset)
|
||||
*/
|
||||
export interface BehaviorTreeData {
|
||||
/** 树ID */
|
||||
id: string;
|
||||
|
||||
/** 树名称 */
|
||||
name: string;
|
||||
|
||||
/** 根节点ID */
|
||||
rootNodeId: string;
|
||||
|
||||
/** 所有节点(扁平化存储) */
|
||||
nodes: Map<string, BehaviorNodeData>;
|
||||
|
||||
/** 黑板变量定义 */
|
||||
blackboardVariables?: Map<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点运行时状态
|
||||
*
|
||||
* 每个BehaviorTreeRuntimeComponent实例独立维护
|
||||
*/
|
||||
export interface NodeRuntimeState {
|
||||
/** 当前执行状态 */
|
||||
status: TaskStatus;
|
||||
|
||||
/** 当前执行的子节点索引(复合节点使用) */
|
||||
currentChildIndex: number;
|
||||
|
||||
/** 执行顺序号(用于调试和可视化) */
|
||||
executionOrder?: number;
|
||||
|
||||
/** 开始执行时间(某些节点需要) */
|
||||
startTime?: number;
|
||||
|
||||
/** 上次执行时间(冷却节点使用) */
|
||||
lastExecutionTime?: number;
|
||||
|
||||
/** 当前重复次数(重复节点使用) */
|
||||
repeatCount?: number;
|
||||
|
||||
/** 缓存的结果(某些条件节点使用) */
|
||||
cachedResult?: any;
|
||||
|
||||
/** 洗牌后的索引(随机节点使用) */
|
||||
shuffledIndices?: number[];
|
||||
|
||||
/** 是否被中止 */
|
||||
isAborted?: boolean;
|
||||
|
||||
/** 上次条件评估结果(条件装饰器使用) */
|
||||
lastConditionResult?: boolean;
|
||||
|
||||
/** 正在观察的黑板键(条件装饰器使用) */
|
||||
observedKeys?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建默认的运行时状态
|
||||
*/
|
||||
export function createDefaultRuntimeState(): NodeRuntimeState {
|
||||
return {
|
||||
status: TaskStatus.Invalid,
|
||||
currentChildIndex: 0
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
import { EntitySystem, Matcher, Entity, Time, Core, ECSSystem, ServiceContainer } from '@esengine/ecs-framework';
|
||||
import type { IBTAssetManager, IBehaviorTreeAssetContent } from '../Types/AssetManagerInterface';
|
||||
import { BehaviorTreeRuntimeComponent } from './BehaviorTreeRuntimeComponent';
|
||||
import { BehaviorTreeAssetManager } from './BehaviorTreeAssetManager';
|
||||
import { NodeExecutorRegistry, NodeExecutionContext } from './NodeExecutor';
|
||||
import { BehaviorTreeData, BehaviorNodeData } from './BehaviorTreeData';
|
||||
import { TaskStatus } from '../Types/TaskStatus';
|
||||
import { NodeMetadataRegistry } from './NodeMetadata';
|
||||
import './Executors';
|
||||
|
||||
/**
|
||||
* 行为树执行系统
|
||||
*
|
||||
* 统一处理所有行为树的执行
|
||||
*/
|
||||
@ECSSystem('BehaviorTreeExecution')
|
||||
export class BehaviorTreeExecutionSystem extends EntitySystem {
|
||||
private btAssetManager: BehaviorTreeAssetManager | null = null;
|
||||
private executorRegistry: NodeExecutorRegistry;
|
||||
private _services: ServiceContainer | null = null;
|
||||
|
||||
/** 引用外部资产管理器(可选,由外部模块设置) */
|
||||
private _assetManager: IBTAssetManager | null = null;
|
||||
|
||||
/** 已警告过的缺失资产,避免重复警告 */
|
||||
private _warnedMissingAssets: Set<string> = new Set();
|
||||
|
||||
constructor(services?: ServiceContainer) {
|
||||
super(Matcher.empty().all(BehaviorTreeRuntimeComponent));
|
||||
this._services = services || null;
|
||||
this.executorRegistry = new NodeExecutorRegistry();
|
||||
this.registerBuiltInExecutors();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 设置外部资产管理器引用(可选)
|
||||
* @en Set external asset manager reference (optional)
|
||||
*
|
||||
* @zh 当与 ESEngine 集成时,由 BehaviorTreeRuntimeModule 调用。
|
||||
* 不使用 ESEngine 时,可以不调用此方法,
|
||||
* 直接使用 BehaviorTreeAssetManager.loadFromEditorJSON() 加载资产。
|
||||
*
|
||||
* @en Called by BehaviorTreeRuntimeModule when integrating with ESEngine.
|
||||
* When not using ESEngine, you can skip this and use
|
||||
* BehaviorTreeAssetManager.loadFromEditorJSON() to load assets directly.
|
||||
*/
|
||||
setAssetManager(assetManager: IBTAssetManager | null): void {
|
||||
this._assetManager = assetManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动所有 autoStart 的行为树(用于预览模式)
|
||||
* Start all autoStart behavior trees (for preview mode)
|
||||
*
|
||||
* 由于编辑器模式下系统默认禁用,实体添加时 onAdded 不会处理自动启动。
|
||||
* 预览开始时需要手动调用此方法来启动所有需要自动启动的行为树。
|
||||
*/
|
||||
startAllAutoStartTrees(): void {
|
||||
if (!this.scene) {
|
||||
this.logger.warn('Scene not available, cannot start auto-start trees');
|
||||
return;
|
||||
}
|
||||
|
||||
const entities = this.scene.entities.findEntitiesWithComponent(BehaviorTreeRuntimeComponent);
|
||||
for (const entity of entities) {
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
if (runtime && runtime.autoStart && runtime.treeAssetId && !runtime.isRunning) {
|
||||
this.ensureAssetLoaded(runtime.treeAssetId).then(() => {
|
||||
if (runtime && runtime.autoStart && !runtime.isRunning) {
|
||||
runtime.start();
|
||||
this.logger.debug(`Auto-started behavior tree for entity: ${entity.name}`);
|
||||
}
|
||||
}).catch(e => {
|
||||
this.logger.error(`Failed to load behavior tree for entity ${entity.name}:`, e);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当实体添加到系统时,处理自动启动
|
||||
* Handle auto-start when entity is added to system
|
||||
*/
|
||||
protected override onAdded(entity: Entity): void {
|
||||
// 只有在系统启用时才自动启动
|
||||
// Only auto-start when system is enabled
|
||||
if (!this.enabled) return;
|
||||
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
if (runtime && runtime.autoStart && runtime.treeAssetId && !runtime.isRunning) {
|
||||
// 先尝试加载资产(如果是文件路径)
|
||||
this.ensureAssetLoaded(runtime.treeAssetId).then(() => {
|
||||
// 检查实体是否仍然有效
|
||||
if (runtime && runtime.autoStart && !runtime.isRunning) {
|
||||
runtime.start();
|
||||
this.logger.debug(`Auto-started behavior tree for entity: ${entity.name}`);
|
||||
}
|
||||
}).catch(e => {
|
||||
this.logger.error(`Failed to load behavior tree for entity ${entity.name}:`, e);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保行为树资产已加载
|
||||
* Ensure behavior tree asset is loaded
|
||||
*/
|
||||
private async ensureAssetLoaded(assetGuid: string): Promise<void> {
|
||||
const btAssetManager = this.getBTAssetManager();
|
||||
|
||||
// 如果资产已存在,直接返回
|
||||
if (btAssetManager.hasAsset(assetGuid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用 AssetManager 加载(必须通过 setAssetManager 设置)
|
||||
// Use AssetManager (must be set via setAssetManager)
|
||||
if (!this._assetManager) {
|
||||
this.logger.warn(`AssetManager not set, cannot load: ${assetGuid}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用 loadAsset 通过 GUID 加载,而不是 loadAssetByPath
|
||||
// Use loadAsset with GUID instead of loadAssetByPath
|
||||
const result = await this._assetManager.loadAsset(assetGuid);
|
||||
if (result && result.asset) {
|
||||
this.logger.debug(`Behavior tree loaded via AssetManager: ${assetGuid}`);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.warn(`Failed to load via AssetManager: ${assetGuid}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
private getBTAssetManager(): BehaviorTreeAssetManager {
|
||||
if (!this.btAssetManager) {
|
||||
// 优先使用传入的 services,否则回退到全局 Core.services
|
||||
// Prefer passed services, fallback to global Core.services
|
||||
const services = this._services || Core.services;
|
||||
if (!services) {
|
||||
throw new Error('ServiceContainer is not available. Ensure Core.create() was called.');
|
||||
}
|
||||
this.btAssetManager = services.resolve(BehaviorTreeAssetManager);
|
||||
}
|
||||
return this.btAssetManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取行为树数据
|
||||
* Get behavior tree data from AssetManager or BehaviorTreeAssetManager
|
||||
*
|
||||
* 优先从 AssetManager 获取(新方式),如果没有再从 BehaviorTreeAssetManager 获取(兼容旧方式)
|
||||
*/
|
||||
private getTreeData(assetGuid: string): BehaviorTreeData | undefined {
|
||||
// 1. 优先从 AssetManager 获取(如果已加载)
|
||||
// First try AssetManager (preferred way)
|
||||
if (this._assetManager) {
|
||||
// 使用 getAsset 通过 GUID 获取,而不是 getAssetByPath
|
||||
// Use getAsset with GUID instead of getAssetByPath
|
||||
const cachedAsset = this._assetManager.getAsset<IBehaviorTreeAssetContent>(assetGuid);
|
||||
if (cachedAsset?.data) {
|
||||
return cachedAsset.data;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 回退到 BehaviorTreeAssetManager(兼容旧方式)
|
||||
// Fallback to BehaviorTreeAssetManager (legacy support)
|
||||
return this.getBTAssetManager().getAsset(assetGuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册所有执行器(包括内置和插件提供的)
|
||||
*/
|
||||
private registerBuiltInExecutors(): void {
|
||||
const constructors = NodeMetadataRegistry.getAllExecutorConstructors();
|
||||
|
||||
for (const [implementationType, ExecutorClass] of constructors) {
|
||||
try {
|
||||
const instance = new ExecutorClass();
|
||||
this.executorRegistry.register(implementationType, instance);
|
||||
} catch (error) {
|
||||
this.logger.error(`注册执行器失败: ${implementationType}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取执行器注册表
|
||||
*/
|
||||
getExecutorRegistry(): NodeExecutorRegistry {
|
||||
return this.executorRegistry;
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent)!;
|
||||
|
||||
if (!runtime.isRunning) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const treeData = this.getTreeData(runtime.treeAssetId);
|
||||
if (!treeData) {
|
||||
// 只警告一次,避免每帧重复输出
|
||||
// Only warn once to avoid repeated output every frame
|
||||
if (!this._warnedMissingAssets.has(runtime.treeAssetId)) {
|
||||
this._warnedMissingAssets.add(runtime.treeAssetId);
|
||||
this.logger.warn(`未找到行为树资产: ${runtime.treeAssetId}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果标记了需要重置,先重置状态
|
||||
if (runtime.needsReset) {
|
||||
runtime.resetAllStates();
|
||||
runtime.needsReset = false;
|
||||
}
|
||||
|
||||
// 初始化黑板变量(如果行为树定义了默认值)
|
||||
// Initialize blackboard variables from tree definition
|
||||
if (treeData.blackboardVariables && treeData.blackboardVariables.size > 0) {
|
||||
runtime.initializeBlackboard(treeData.blackboardVariables);
|
||||
}
|
||||
|
||||
this.executeTree(entity, runtime, treeData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行整个行为树
|
||||
*/
|
||||
private executeTree(
|
||||
entity: Entity,
|
||||
runtime: BehaviorTreeRuntimeComponent,
|
||||
treeData: BehaviorTreeData
|
||||
): void {
|
||||
const rootNode = treeData.nodes.get(treeData.rootNodeId);
|
||||
if (!rootNode) {
|
||||
this.logger.error(`未找到根节点: ${treeData.rootNodeId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const status = this.executeNode(entity, runtime, rootNode, treeData);
|
||||
|
||||
// 如果树完成了,标记在下一个tick时重置状态
|
||||
// 这样UI可以看到节点的最终状态
|
||||
if (status !== TaskStatus.Running) {
|
||||
runtime.needsReset = true;
|
||||
} else {
|
||||
runtime.needsReset = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单个节点
|
||||
*/
|
||||
private executeNode(
|
||||
entity: Entity,
|
||||
runtime: BehaviorTreeRuntimeComponent,
|
||||
nodeData: BehaviorNodeData,
|
||||
treeData: BehaviorTreeData
|
||||
): TaskStatus {
|
||||
const state = runtime.getNodeState(nodeData.id);
|
||||
|
||||
if (runtime.shouldAbort(nodeData.id)) {
|
||||
runtime.clearAbortRequest(nodeData.id);
|
||||
state.isAborted = true;
|
||||
|
||||
const executor = this.executorRegistry.get(nodeData.implementationType);
|
||||
if (executor && executor.reset) {
|
||||
const context = this.createContext(entity, runtime, nodeData, treeData);
|
||||
executor.reset(context);
|
||||
}
|
||||
|
||||
runtime.activeNodeIds.delete(nodeData.id);
|
||||
state.status = TaskStatus.Failure;
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
runtime.activeNodeIds.add(nodeData.id);
|
||||
state.isAborted = false;
|
||||
|
||||
if (state.executionOrder === undefined) {
|
||||
runtime.executionOrderCounter++;
|
||||
state.executionOrder = runtime.executionOrderCounter;
|
||||
}
|
||||
|
||||
const executor = this.executorRegistry.get(nodeData.implementationType);
|
||||
if (!executor) {
|
||||
this.logger.error(`未找到执行器: ${nodeData.implementationType}`);
|
||||
state.status = TaskStatus.Failure;
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const context = this.createContext(entity, runtime, nodeData, treeData);
|
||||
|
||||
try {
|
||||
const status = executor.execute(context);
|
||||
state.status = status;
|
||||
|
||||
if (status !== TaskStatus.Running) {
|
||||
runtime.activeNodeIds.delete(nodeData.id);
|
||||
|
||||
if (executor.reset) {
|
||||
executor.reset(context);
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
} catch (error) {
|
||||
this.logger.error(`执行节点时发生错误: ${nodeData.name}`, error);
|
||||
state.status = TaskStatus.Failure;
|
||||
runtime.activeNodeIds.delete(nodeData.id);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建执行上下文
|
||||
*/
|
||||
private createContext(
|
||||
entity: Entity,
|
||||
runtime: BehaviorTreeRuntimeComponent,
|
||||
nodeData: BehaviorNodeData,
|
||||
treeData: BehaviorTreeData
|
||||
): NodeExecutionContext {
|
||||
return {
|
||||
entity,
|
||||
nodeData,
|
||||
state: runtime.getNodeState(nodeData.id),
|
||||
runtime,
|
||||
treeData,
|
||||
deltaTime: Time.deltaTime,
|
||||
totalTime: Time.totalTime,
|
||||
executeChild: (childId: string) => {
|
||||
const childData = treeData.nodes.get(childId);
|
||||
if (!childData) {
|
||||
this.logger.warn(`未找到子节点: ${childId}`);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
return this.executeNode(entity, runtime, childData, treeData);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行子节点列表
|
||||
*/
|
||||
executeChildren(
|
||||
context: NodeExecutionContext,
|
||||
childIndices?: number[]
|
||||
): TaskStatus[] {
|
||||
const { nodeData, treeData, entity, runtime } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const results: TaskStatus[] = [];
|
||||
const indicesToExecute = childIndices ||
|
||||
Array.from({ length: nodeData.children.length }, (_, i) => i);
|
||||
|
||||
for (const index of indicesToExecute) {
|
||||
if (index >= nodeData.children.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[index]!;
|
||||
const childData = treeData.nodes.get(childId);
|
||||
|
||||
if (!childData) {
|
||||
this.logger.warn(`未找到子节点: ${childId}`);
|
||||
results.push(TaskStatus.Failure);
|
||||
continue;
|
||||
}
|
||||
|
||||
const status = this.executeNode(entity, runtime, childData, treeData);
|
||||
results.push(status);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
import { Component, ECSComponent, Property } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
|
||||
import { NodeRuntimeState, createDefaultRuntimeState } from './BehaviorTreeData';
|
||||
import { TaskStatus } from '../Types/TaskStatus';
|
||||
|
||||
/**
|
||||
* 黑板变化监听器
|
||||
*/
|
||||
export type BlackboardChangeListener = (key: string, newValue: any, oldValue: any) => void;
|
||||
|
||||
/**
|
||||
* 黑板观察者信息
|
||||
*/
|
||||
interface BlackboardObserver {
|
||||
nodeId: string;
|
||||
keys: Set<string>;
|
||||
callback: BlackboardChangeListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树运行时组件
|
||||
*
|
||||
* 挂载到游戏Entity上,引用共享的BehaviorTreeData
|
||||
* 维护该Entity独立的运行时状态
|
||||
*/
|
||||
@ECSComponent('BehaviorTreeRuntime')
|
||||
@Serializable({ version: 1 })
|
||||
export class BehaviorTreeRuntimeComponent extends Component {
|
||||
/**
|
||||
* 引用的行为树资产ID(可序列化)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'asset', label: 'Behavior Tree', extensions: ['.btree'] })
|
||||
treeAssetId: string = '';
|
||||
|
||||
/**
|
||||
* 是否自动启动
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Auto Start' })
|
||||
autoStart: boolean = true;
|
||||
|
||||
/**
|
||||
* 是否正在运行
|
||||
*/
|
||||
@IgnoreSerialization()
|
||||
isRunning: boolean = false;
|
||||
|
||||
/**
|
||||
* 节点运行时状态(每个节点独立)
|
||||
* 不序列化,每次加载时重新初始化
|
||||
*/
|
||||
@IgnoreSerialization()
|
||||
private nodeStates: Map<string, NodeRuntimeState> = new Map();
|
||||
|
||||
/**
|
||||
* 黑板数据(该Entity独立的数据)
|
||||
* 不序列化,通过初始化设置
|
||||
*/
|
||||
@IgnoreSerialization()
|
||||
private blackboard: Map<string, any> = new Map();
|
||||
|
||||
/**
|
||||
* 黑板观察者列表
|
||||
*/
|
||||
@IgnoreSerialization()
|
||||
private blackboardObservers: Map<string, BlackboardObserver[]> = new Map();
|
||||
|
||||
/**
|
||||
* 当前激活的节点ID列表(用于调试)
|
||||
*/
|
||||
@IgnoreSerialization()
|
||||
activeNodeIds: Set<string> = new Set();
|
||||
|
||||
/**
|
||||
* 标记是否需要在下一个tick重置状态
|
||||
*/
|
||||
@IgnoreSerialization()
|
||||
needsReset: boolean = false;
|
||||
|
||||
/**
|
||||
* 需要中止的节点ID列表
|
||||
*/
|
||||
@IgnoreSerialization()
|
||||
nodesToAbort: Set<string> = new Set();
|
||||
|
||||
/**
|
||||
* 执行顺序计数器(用于调试和可视化)
|
||||
*/
|
||||
@IgnoreSerialization()
|
||||
executionOrderCounter: number = 0;
|
||||
|
||||
/**
|
||||
* 获取节点运行时状态
|
||||
*/
|
||||
getNodeState(nodeId: string): NodeRuntimeState {
|
||||
if (!this.nodeStates.has(nodeId)) {
|
||||
this.nodeStates.set(nodeId, createDefaultRuntimeState());
|
||||
}
|
||||
return this.nodeStates.get(nodeId)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置节点状态
|
||||
*/
|
||||
resetNodeState(nodeId: string): void {
|
||||
const state = this.getNodeState(nodeId);
|
||||
state.status = TaskStatus.Invalid;
|
||||
state.currentChildIndex = 0;
|
||||
delete state.startTime;
|
||||
delete state.lastExecutionTime;
|
||||
delete state.repeatCount;
|
||||
delete state.cachedResult;
|
||||
delete state.shuffledIndices;
|
||||
delete state.isAborted;
|
||||
delete state.lastConditionResult;
|
||||
delete state.observedKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置所有节点状态
|
||||
*/
|
||||
resetAllStates(): void {
|
||||
this.nodeStates.clear();
|
||||
this.activeNodeIds.clear();
|
||||
this.executionOrderCounter = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取黑板值
|
||||
*/
|
||||
getBlackboardValue<T = any>(key: string): T | undefined {
|
||||
return this.blackboard.get(key) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置黑板值
|
||||
*/
|
||||
setBlackboardValue(key: string, value: any): void {
|
||||
const oldValue = this.blackboard.get(key);
|
||||
this.blackboard.set(key, value);
|
||||
|
||||
if (oldValue !== value) {
|
||||
this.notifyBlackboardChange(key, value, oldValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查黑板是否有某个键
|
||||
*/
|
||||
hasBlackboardKey(key: string): boolean {
|
||||
return this.blackboard.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化黑板(从树定义的默认值)
|
||||
*/
|
||||
initializeBlackboard(variables?: Map<string, any>): void {
|
||||
if (variables) {
|
||||
variables.forEach((value, key) => {
|
||||
if (!this.blackboard.has(key)) {
|
||||
this.blackboard.set(key, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空黑板
|
||||
*/
|
||||
clearBlackboard(): void {
|
||||
this.blackboard.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动行为树
|
||||
*/
|
||||
start(): void {
|
||||
this.isRunning = true;
|
||||
this.resetAllStates();
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止行为树
|
||||
*/
|
||||
stop(): void {
|
||||
this.isRunning = false;
|
||||
this.activeNodeIds.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停行为树
|
||||
*/
|
||||
pause(): void {
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复行为树
|
||||
*/
|
||||
resume(): void {
|
||||
this.isRunning = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册黑板观察者
|
||||
*/
|
||||
observeBlackboard(nodeId: string, keys: string[], callback: BlackboardChangeListener): void {
|
||||
const observer: BlackboardObserver = {
|
||||
nodeId,
|
||||
keys: new Set(keys),
|
||||
callback
|
||||
};
|
||||
|
||||
for (const key of keys) {
|
||||
if (!this.blackboardObservers.has(key)) {
|
||||
this.blackboardObservers.set(key, []);
|
||||
}
|
||||
this.blackboardObservers.get(key)!.push(observer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消注册黑板观察者
|
||||
*/
|
||||
unobserveBlackboard(nodeId: string): void {
|
||||
for (const observers of this.blackboardObservers.values()) {
|
||||
const index = observers.findIndex((o) => o.nodeId === nodeId);
|
||||
if (index !== -1) {
|
||||
observers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知黑板变化
|
||||
*/
|
||||
private notifyBlackboardChange(key: string, newValue: any, oldValue: any): void {
|
||||
const observers = this.blackboardObservers.get(key);
|
||||
if (!observers) return;
|
||||
|
||||
for (const observer of observers) {
|
||||
try {
|
||||
observer.callback(key, newValue, oldValue);
|
||||
} catch (error) {
|
||||
console.error(`黑板观察者回调错误 (节点: ${observer.nodeId}):`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求中止节点
|
||||
*/
|
||||
requestAbort(nodeId: string): void {
|
||||
this.nodesToAbort.add(nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查节点是否需要中止
|
||||
*/
|
||||
shouldAbort(nodeId: string): boolean {
|
||||
return this.nodesToAbort.has(nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除中止请求
|
||||
*/
|
||||
clearAbortRequest(nodeId: string): void {
|
||||
this.nodesToAbort.delete(nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有中止请求
|
||||
*/
|
||||
clearAllAbortRequests(): void {
|
||||
this.nodesToAbort.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 总是失败装饰器执行器
|
||||
*
|
||||
* 无论子节点结果如何都返回失败
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'AlwaysFail',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: '总是失败',
|
||||
description: '无论子节点结果如何都返回失败',
|
||||
category: 'Decorator'
|
||||
})
|
||||
export class AlwaysFailExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
if (context.nodeData.children && context.nodeData.children.length > 0) {
|
||||
context.runtime.resetNodeState(context.nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 总是成功装饰器执行器
|
||||
*
|
||||
* 无论子节点结果如何都返回成功
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'AlwaysSucceed',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: '总是成功',
|
||||
description: '无论子节点结果如何都返回成功',
|
||||
category: 'Decorator'
|
||||
})
|
||||
export class AlwaysSucceedExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
if (context.nodeData.children && context.nodeData.children.length > 0) {
|
||||
context.runtime.resetNodeState(context.nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 黑板比较条件执行器
|
||||
*
|
||||
* 比较黑板中的值
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'BlackboardCompare',
|
||||
nodeType: NodeType.Condition,
|
||||
displayName: '黑板比较',
|
||||
description: '比较黑板中的值',
|
||||
category: 'Condition',
|
||||
configSchema: {
|
||||
key: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '黑板变量名'
|
||||
},
|
||||
compareValue: {
|
||||
type: 'object',
|
||||
description: '比较值',
|
||||
supportBinding: true
|
||||
},
|
||||
operator: {
|
||||
type: 'string',
|
||||
default: 'equals',
|
||||
description: '比较运算符',
|
||||
options: ['equals', 'notEquals', 'greaterThan', 'lessThan', 'greaterOrEqual', 'lessOrEqual']
|
||||
}
|
||||
}
|
||||
})
|
||||
export class BlackboardCompare implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime } = context;
|
||||
const key = BindingHelper.getValue<string>(context, 'key', '');
|
||||
const compareValue = BindingHelper.getValue(context, 'compareValue');
|
||||
const operator = BindingHelper.getValue<string>(context, 'operator', 'equals');
|
||||
|
||||
if (!key) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const actualValue = runtime.getBlackboardValue(key);
|
||||
|
||||
if (this.compare(actualValue, compareValue, operator)) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
private compare(actualValue: any, compareValue: any, operator: string): boolean {
|
||||
switch (operator) {
|
||||
case 'equals':
|
||||
return actualValue === compareValue;
|
||||
case 'notEquals':
|
||||
return actualValue !== compareValue;
|
||||
case 'greaterThan':
|
||||
return actualValue > compareValue;
|
||||
case 'lessThan':
|
||||
return actualValue < compareValue;
|
||||
case 'greaterOrEqual':
|
||||
return actualValue >= compareValue;
|
||||
case 'lessOrEqual':
|
||||
return actualValue <= compareValue;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 黑板存在检查条件执行器
|
||||
*
|
||||
* 检查黑板中是否存在指定的键
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'BlackboardExists',
|
||||
nodeType: NodeType.Condition,
|
||||
displayName: '黑板存在',
|
||||
description: '检查黑板中是否存在指定的键',
|
||||
category: 'Condition',
|
||||
configSchema: {
|
||||
key: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '黑板变量名'
|
||||
},
|
||||
checkNull: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: '检查是否为null'
|
||||
}
|
||||
}
|
||||
})
|
||||
export class BlackboardExists implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime } = context;
|
||||
const key = BindingHelper.getValue<string>(context, 'key', '');
|
||||
const checkNull = BindingHelper.getValue<boolean>(context, 'checkNull', false);
|
||||
|
||||
if (!key) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const value = runtime.getBlackboardValue(key);
|
||||
|
||||
if (value === undefined) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
if (checkNull && value === null) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import { TaskStatus, NodeType, AbortType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 条件装饰器执行器
|
||||
*
|
||||
* 根据条件决定是否执行子节点
|
||||
* 支持动态优先级和中止机制
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Conditional',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: '条件',
|
||||
description: '根据条件决定是否执行子节点',
|
||||
category: 'Decorator',
|
||||
configSchema: {
|
||||
blackboardKey: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '黑板变量名'
|
||||
},
|
||||
expectedValue: {
|
||||
type: 'object',
|
||||
description: '期望值',
|
||||
supportBinding: true
|
||||
},
|
||||
operator: {
|
||||
type: 'string',
|
||||
default: 'equals',
|
||||
description: '比较运算符',
|
||||
options: ['equals', 'notEquals', 'greaterThan', 'lessThan', 'greaterOrEqual', 'lessOrEqual']
|
||||
},
|
||||
abortType: {
|
||||
type: 'string',
|
||||
default: 'none',
|
||||
description: '中止类型',
|
||||
options: ['none', 'self', 'lower-priority', 'both']
|
||||
}
|
||||
}
|
||||
})
|
||||
export class ConditionalExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, runtime, state } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const blackboardKey = BindingHelper.getValue<string>(context, 'blackboardKey', '');
|
||||
const expectedValue = BindingHelper.getValue(context, 'expectedValue');
|
||||
const operator = BindingHelper.getValue<string>(context, 'operator', 'equals');
|
||||
const abortType = (nodeData.abortType || AbortType.None) as AbortType;
|
||||
|
||||
if (!blackboardKey) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const actualValue = runtime.getBlackboardValue(blackboardKey);
|
||||
const conditionMet = this.evaluateCondition(actualValue, expectedValue, operator);
|
||||
|
||||
const wasRunning = state.status === TaskStatus.Running;
|
||||
|
||||
if (abortType !== AbortType.None) {
|
||||
if (!state.observedKeys || state.observedKeys.length === 0) {
|
||||
state.observedKeys = [blackboardKey];
|
||||
this.setupObserver(context, blackboardKey, expectedValue, operator, abortType);
|
||||
}
|
||||
|
||||
if (state.lastConditionResult !== undefined && state.lastConditionResult !== conditionMet) {
|
||||
if (conditionMet) {
|
||||
this.handleConditionBecameTrue(context, abortType);
|
||||
} else if (wasRunning) {
|
||||
this.handleConditionBecameFalse(context, abortType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.lastConditionResult = conditionMet;
|
||||
|
||||
if (!conditionMet) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
private evaluateCondition(actualValue: any, expectedValue: any, operator: string): boolean {
|
||||
switch (operator) {
|
||||
case 'equals':
|
||||
return actualValue === expectedValue;
|
||||
case 'notEquals':
|
||||
return actualValue !== expectedValue;
|
||||
case 'greaterThan':
|
||||
return actualValue > expectedValue;
|
||||
case 'lessThan':
|
||||
return actualValue < expectedValue;
|
||||
case 'greaterOrEqual':
|
||||
return actualValue >= expectedValue;
|
||||
case 'lessOrEqual':
|
||||
return actualValue <= expectedValue;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置黑板观察者
|
||||
*/
|
||||
private setupObserver(
|
||||
context: NodeExecutionContext,
|
||||
blackboardKey: string,
|
||||
expectedValue: any,
|
||||
operator: string,
|
||||
abortType: AbortType
|
||||
): void {
|
||||
const { nodeData, runtime } = context;
|
||||
|
||||
runtime.observeBlackboard(nodeData.id, [blackboardKey], (_key, newValue) => {
|
||||
const conditionMet = this.evaluateCondition(newValue, expectedValue, operator);
|
||||
const lastResult = context.state.lastConditionResult;
|
||||
|
||||
if (lastResult !== undefined && lastResult !== conditionMet) {
|
||||
if (conditionMet) {
|
||||
this.handleConditionBecameTrue(context, abortType);
|
||||
} else {
|
||||
this.handleConditionBecameFalse(context, abortType);
|
||||
}
|
||||
}
|
||||
|
||||
context.state.lastConditionResult = conditionMet;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理条件变为true
|
||||
*/
|
||||
private handleConditionBecameTrue(context: NodeExecutionContext, abortType: AbortType): void {
|
||||
if (abortType === AbortType.LowerPriority || abortType === AbortType.Both) {
|
||||
this.requestAbortLowerPriority(context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理条件变为false
|
||||
*/
|
||||
private handleConditionBecameFalse(context: NodeExecutionContext, abortType: AbortType): void {
|
||||
const { nodeData, runtime } = context;
|
||||
|
||||
if (abortType === AbortType.Self || abortType === AbortType.Both) {
|
||||
if (nodeData.children && nodeData.children.length > 0) {
|
||||
runtime.requestAbort(nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求中止低优先级节点
|
||||
*/
|
||||
private requestAbortLowerPriority(context: NodeExecutionContext): void {
|
||||
const { runtime } = context;
|
||||
runtime.requestAbort('__lower_priority__');
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
const { nodeData, runtime, state } = context;
|
||||
|
||||
if (state.observedKeys && state.observedKeys.length > 0) {
|
||||
runtime.unobserveBlackboard(nodeData.id);
|
||||
delete state.observedKeys;
|
||||
}
|
||||
|
||||
delete state.lastConditionResult;
|
||||
|
||||
if (nodeData.children && nodeData.children.length > 0) {
|
||||
runtime.resetNodeState(nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 冷却装饰器执行器
|
||||
*
|
||||
* 子节点执行成功后进入冷却时间
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Cooldown',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: '冷却',
|
||||
description: '子节点执行成功后进入冷却时间',
|
||||
category: 'Decorator',
|
||||
configSchema: {
|
||||
cooldownTime: {
|
||||
type: 'number',
|
||||
default: 1.0,
|
||||
description: '冷却时间(秒)',
|
||||
min: 0,
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class CooldownExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, state, totalTime } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const cooldownTime = BindingHelper.getValue<number>(context, 'cooldownTime', 1.0);
|
||||
|
||||
if (state.lastExecutionTime !== undefined) {
|
||||
const timeSinceLastExecution = totalTime - state.lastExecutionTime;
|
||||
if (timeSinceLastExecution < cooldownTime) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (status === TaskStatus.Success) {
|
||||
state.lastExecutionTime = totalTime;
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
delete context.state.lastExecutionTime;
|
||||
if (context.nodeData.children && context.nodeData.children.length > 0) {
|
||||
context.runtime.resetNodeState(context.nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 执行动作执行器
|
||||
*
|
||||
* 执行自定义动作逻辑
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'ExecuteAction',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '执行动作',
|
||||
description: '执行自定义动作逻辑',
|
||||
category: 'Action',
|
||||
configSchema: {
|
||||
actionName: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '动作名称(黑板中action_前缀的函数)'
|
||||
}
|
||||
}
|
||||
})
|
||||
export class ExecuteAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime, entity } = context;
|
||||
const actionName = BindingHelper.getValue<string>(context, 'actionName', '');
|
||||
|
||||
if (!actionName) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const actionFunction = runtime.getBlackboardValue<(entity: NodeExecutionContext['entity']) => TaskStatus>(`action_${actionName}`);
|
||||
|
||||
if (!actionFunction || typeof actionFunction !== 'function') {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
try {
|
||||
return actionFunction(entity);
|
||||
} catch (error) {
|
||||
console.error(`ExecuteAction failed: ${error}`);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 执行条件执行器
|
||||
*
|
||||
* 执行自定义条件逻辑
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'ExecuteCondition',
|
||||
nodeType: NodeType.Condition,
|
||||
displayName: '执行条件',
|
||||
description: '执行自定义条件逻辑',
|
||||
category: 'Condition',
|
||||
configSchema: {
|
||||
conditionName: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '条件名称(黑板中condition_前缀的函数)'
|
||||
}
|
||||
}
|
||||
})
|
||||
export class ExecuteCondition implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime, entity } = context;
|
||||
const conditionName = BindingHelper.getValue<string>(context, 'conditionName', '');
|
||||
|
||||
if (!conditionName) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const conditionFunction = runtime.getBlackboardValue<(entity: NodeExecutionContext['entity']) => boolean>(`condition_${conditionName}`);
|
||||
|
||||
if (!conditionFunction || typeof conditionFunction !== 'function') {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
try {
|
||||
return conditionFunction(entity) ? TaskStatus.Success : TaskStatus.Failure;
|
||||
} catch (error) {
|
||||
console.error(`ExecuteCondition failed: ${error}`);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 反转装饰器执行器
|
||||
*
|
||||
* 反转子节点的执行结果
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Inverter',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: '反转',
|
||||
description: '反转子节点的执行结果',
|
||||
category: 'Decorator',
|
||||
childrenConstraints: {
|
||||
min: 1,
|
||||
max: 1
|
||||
}
|
||||
})
|
||||
export class InverterExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (status === TaskStatus.Success) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
if (status === TaskStatus.Failure) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
if (context.nodeData.children && context.nodeData.children.length > 0) {
|
||||
context.runtime.resetNodeState(context.nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 日志动作执行器
|
||||
*
|
||||
* 输出日志信息
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Log',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '日志',
|
||||
description: '输出日志信息',
|
||||
category: 'Action',
|
||||
configSchema: {
|
||||
message: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '日志消息,支持{key}占位符引用黑板变量',
|
||||
supportBinding: true
|
||||
},
|
||||
logLevel: {
|
||||
type: 'string',
|
||||
default: 'info',
|
||||
description: '日志级别',
|
||||
options: ['info', 'warn', 'error']
|
||||
}
|
||||
}
|
||||
})
|
||||
export class LogAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime } = context;
|
||||
const message = BindingHelper.getValue<string>(context, 'message', '');
|
||||
const logLevel = BindingHelper.getValue<string>(context, 'logLevel', 'info');
|
||||
|
||||
const finalMessage = this.replaceBlackboardVariables(message, runtime);
|
||||
|
||||
this.log(finalMessage, logLevel);
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
private replaceBlackboardVariables(message: string, runtime: NodeExecutionContext['runtime']): string {
|
||||
if (!message.includes('{') || !message.includes('}')) {
|
||||
return message;
|
||||
}
|
||||
|
||||
// 使用限制长度的正则表达式避免 ReDoS 攻击
|
||||
// 限制占位符名称最多100个字符,只允许字母、数字、下划线和点号
|
||||
return message.replace(/\{([\w.]{1,100})\}/g, (_, key) => {
|
||||
const value = runtime.getBlackboardValue(key.trim());
|
||||
return value !== undefined ? String(value) : `{${key}}`;
|
||||
});
|
||||
}
|
||||
|
||||
private log(message: string, level: string): void {
|
||||
switch (level) {
|
||||
case 'error':
|
||||
console.error(message);
|
||||
break;
|
||||
case 'warn':
|
||||
console.warn(message);
|
||||
break;
|
||||
case 'info':
|
||||
default:
|
||||
console.log(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 修改黑板值动作执行器
|
||||
*
|
||||
* 对黑板中的数值进行运算
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'ModifyBlackboardValue',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '修改黑板值',
|
||||
description: '对黑板中的数值进行运算',
|
||||
category: 'Action',
|
||||
configSchema: {
|
||||
key: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '黑板变量名'
|
||||
},
|
||||
operation: {
|
||||
type: 'string',
|
||||
default: 'add',
|
||||
description: '运算类型',
|
||||
options: ['add', 'subtract', 'multiply', 'divide', 'set']
|
||||
},
|
||||
value: {
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: '操作数',
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class ModifyBlackboardValue implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime } = context;
|
||||
const key = BindingHelper.getValue<string>(context, 'key', '');
|
||||
const operation = BindingHelper.getValue<string>(context, 'operation', 'add');
|
||||
const value = BindingHelper.getValue<number>(context, 'value', 0);
|
||||
|
||||
if (!key) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const currentValue = runtime.getBlackboardValue<number>(key) || 0;
|
||||
let newValue: number;
|
||||
|
||||
switch (operation) {
|
||||
case 'add':
|
||||
newValue = currentValue + value;
|
||||
break;
|
||||
case 'subtract':
|
||||
newValue = currentValue - value;
|
||||
break;
|
||||
case 'multiply':
|
||||
newValue = currentValue * value;
|
||||
break;
|
||||
case 'divide':
|
||||
newValue = value !== 0 ? currentValue / value : currentValue;
|
||||
break;
|
||||
case 'set':
|
||||
newValue = value;
|
||||
break;
|
||||
default:
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
runtime.setBlackboardValue(key, newValue);
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 并行节点执行器
|
||||
*
|
||||
* 同时执行所有子节点
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Parallel',
|
||||
nodeType: NodeType.Composite,
|
||||
displayName: '并行',
|
||||
description: '同时执行所有子节点',
|
||||
category: 'Composite',
|
||||
configSchema: {
|
||||
successPolicy: {
|
||||
type: 'string',
|
||||
default: 'all',
|
||||
description: '成功策略',
|
||||
options: ['all', 'one']
|
||||
},
|
||||
failurePolicy: {
|
||||
type: 'string',
|
||||
default: 'one',
|
||||
description: '失败策略',
|
||||
options: ['all', 'one']
|
||||
}
|
||||
},
|
||||
childrenConstraints: {
|
||||
min: 2
|
||||
}
|
||||
})
|
||||
export class ParallelExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData } = context;
|
||||
const successPolicy = BindingHelper.getValue<string>(context, 'successPolicy', 'all');
|
||||
const failurePolicy = BindingHelper.getValue<string>(context, 'failurePolicy', 'one');
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
let hasRunning = false;
|
||||
let successCount = 0;
|
||||
let failureCount = 0;
|
||||
|
||||
for (const childId of nodeData.children) {
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
hasRunning = true;
|
||||
} else if (status === TaskStatus.Success) {
|
||||
successCount++;
|
||||
} else if (status === TaskStatus.Failure) {
|
||||
failureCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (successPolicy === 'one' && successCount > 0) {
|
||||
this.stopAllChildren(context);
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
if (successPolicy === 'all' && successCount === nodeData.children.length) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
if (failurePolicy === 'one' && failureCount > 0) {
|
||||
this.stopAllChildren(context);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
if (failurePolicy === 'all' && failureCount === nodeData.children.length) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
return hasRunning ? TaskStatus.Running : TaskStatus.Success;
|
||||
}
|
||||
|
||||
private stopAllChildren(context: NodeExecutionContext): void {
|
||||
const { nodeData, runtime } = context;
|
||||
if (!nodeData.children) return;
|
||||
|
||||
for (const childId of nodeData.children) {
|
||||
runtime.activeNodeIds.delete(childId);
|
||||
runtime.resetNodeState(childId);
|
||||
}
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
const { nodeData, runtime } = context;
|
||||
if (!nodeData.children) return;
|
||||
|
||||
for (const childId of nodeData.children) {
|
||||
runtime.resetNodeState(childId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 并行选择器执行器
|
||||
*
|
||||
* 并行执行子节点,任一成功则成功
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'ParallelSelector',
|
||||
nodeType: NodeType.Composite,
|
||||
displayName: '并行选择器',
|
||||
description: '并行执行子节点,任一成功则成功',
|
||||
category: 'Composite',
|
||||
configSchema: {
|
||||
failurePolicy: {
|
||||
type: 'string',
|
||||
default: 'all',
|
||||
description: '失败策略',
|
||||
options: ['all', 'one']
|
||||
}
|
||||
}
|
||||
})
|
||||
export class ParallelSelectorExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData } = context;
|
||||
const failurePolicy = BindingHelper.getValue<string>(context, 'failurePolicy', 'all');
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
let hasRunning = false;
|
||||
let successCount = 0;
|
||||
let failureCount = 0;
|
||||
|
||||
for (const childId of nodeData.children) {
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
hasRunning = true;
|
||||
} else if (status === TaskStatus.Success) {
|
||||
successCount++;
|
||||
} else if (status === TaskStatus.Failure) {
|
||||
failureCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0) {
|
||||
this.stopAllChildren(context);
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
if (failurePolicy === 'one' && failureCount > 0) {
|
||||
this.stopAllChildren(context);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
if (failurePolicy === 'all' && failureCount === nodeData.children.length) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
return hasRunning ? TaskStatus.Running : TaskStatus.Failure;
|
||||
}
|
||||
|
||||
private stopAllChildren(context: NodeExecutionContext): void {
|
||||
const { nodeData, runtime } = context;
|
||||
if (!nodeData.children) return;
|
||||
|
||||
for (const childId of nodeData.children) {
|
||||
runtime.activeNodeIds.delete(childId);
|
||||
runtime.resetNodeState(childId);
|
||||
}
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
const { nodeData, runtime } = context;
|
||||
if (!nodeData.children) return;
|
||||
|
||||
for (const childId of nodeData.children) {
|
||||
runtime.resetNodeState(childId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 随机概率条件执行器
|
||||
*
|
||||
* 根据概率返回成功或失败
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'RandomProbability',
|
||||
nodeType: NodeType.Condition,
|
||||
displayName: '随机概率',
|
||||
description: '根据概率返回成功或失败',
|
||||
category: 'Condition',
|
||||
configSchema: {
|
||||
probability: {
|
||||
type: 'number',
|
||||
default: 0.5,
|
||||
description: '成功概率(0-1)',
|
||||
min: 0,
|
||||
max: 1,
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class RandomProbability implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const probability = BindingHelper.getValue<number>(context, 'probability', 0.5);
|
||||
|
||||
const clampedProbability = Math.max(0, Math.min(1, probability));
|
||||
|
||||
if (Math.random() < clampedProbability) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 随机选择器执行器
|
||||
*
|
||||
* 随机顺序执行子节点,任一成功则成功
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'RandomSelector',
|
||||
nodeType: NodeType.Composite,
|
||||
displayName: '随机选择器',
|
||||
description: '随机顺序执行子节点,任一成功则成功',
|
||||
category: 'Composite'
|
||||
})
|
||||
export class RandomSelectorExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, state } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
if (!state.shuffledIndices || state.shuffledIndices.length === 0) {
|
||||
state.shuffledIndices = this.shuffleIndices(nodeData.children.length);
|
||||
}
|
||||
|
||||
while (state.currentChildIndex < state.shuffledIndices.length) {
|
||||
const shuffledIndex = state.shuffledIndices[state.currentChildIndex]!;
|
||||
const childId = nodeData.children[shuffledIndex]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (status === TaskStatus.Success) {
|
||||
state.currentChildIndex = 0;
|
||||
delete state.shuffledIndices;
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
state.currentChildIndex++;
|
||||
}
|
||||
|
||||
state.currentChildIndex = 0;
|
||||
delete state.shuffledIndices;
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
private shuffleIndices(length: number): number[] {
|
||||
const indices = Array.from({ length }, (_, i) => i);
|
||||
for (let i = indices.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
const temp = indices[i]!;
|
||||
indices[i] = indices[j]!;
|
||||
indices[j] = temp;
|
||||
}
|
||||
return indices;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
context.state.currentChildIndex = 0;
|
||||
delete context.state.shuffledIndices;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 随机序列执行器
|
||||
*
|
||||
* 随机顺序执行子节点序列,全部成功才成功
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'RandomSequence',
|
||||
nodeType: NodeType.Composite,
|
||||
displayName: '随机序列',
|
||||
description: '随机顺序执行子节点,全部成功才成功',
|
||||
category: 'Composite',
|
||||
childrenConstraints: {
|
||||
min: 1
|
||||
}
|
||||
})
|
||||
export class RandomSequenceExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, state } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
if (!state.shuffledIndices || state.shuffledIndices.length === 0) {
|
||||
state.shuffledIndices = this.shuffleIndices(nodeData.children.length);
|
||||
}
|
||||
|
||||
while (state.currentChildIndex < state.shuffledIndices.length) {
|
||||
const shuffledIndex = state.shuffledIndices[state.currentChildIndex]!;
|
||||
const childId = nodeData.children[shuffledIndex]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (status === TaskStatus.Failure) {
|
||||
state.currentChildIndex = 0;
|
||||
delete state.shuffledIndices;
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
state.currentChildIndex++;
|
||||
}
|
||||
|
||||
state.currentChildIndex = 0;
|
||||
delete state.shuffledIndices;
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
private shuffleIndices(length: number): number[] {
|
||||
const indices = Array.from({ length }, (_, i) => i);
|
||||
for (let i = indices.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
const temp = indices[i]!;
|
||||
indices[i] = indices[j]!;
|
||||
indices[j] = temp;
|
||||
}
|
||||
return indices;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
context.state.currentChildIndex = 0;
|
||||
delete context.state.shuffledIndices;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 重复装饰器执行器
|
||||
*
|
||||
* 重复执行子节点指定次数
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Repeater',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: '重复',
|
||||
description: '重复执行子节点指定次数',
|
||||
category: 'Decorator',
|
||||
configSchema: {
|
||||
repeatCount: {
|
||||
type: 'number',
|
||||
default: 1,
|
||||
description: '重复次数(-1表示无限循环)',
|
||||
supportBinding: true
|
||||
},
|
||||
endOnFailure: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: '子节点失败时是否结束'
|
||||
}
|
||||
},
|
||||
childrenConstraints: {
|
||||
min: 1,
|
||||
max: 1
|
||||
}
|
||||
})
|
||||
export class RepeaterExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, state, runtime } = context;
|
||||
const repeatCount = BindingHelper.getValue<number>(context, 'repeatCount', 1);
|
||||
const endOnFailure = BindingHelper.getValue<boolean>(context, 'endOnFailure', false);
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
|
||||
if (!state.repeatCount) {
|
||||
state.repeatCount = 0;
|
||||
}
|
||||
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (status === TaskStatus.Failure && endOnFailure) {
|
||||
state.repeatCount = 0;
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
state.repeatCount++;
|
||||
runtime.resetNodeState(childId);
|
||||
|
||||
const shouldContinue = (repeatCount === -1) || (state.repeatCount < repeatCount);
|
||||
|
||||
if (shouldContinue) {
|
||||
return TaskStatus.Running;
|
||||
} else {
|
||||
state.repeatCount = 0;
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
delete context.state.repeatCount;
|
||||
if (context.nodeData.children && context.nodeData.children.length > 0) {
|
||||
context.runtime.resetNodeState(context.nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 根节点执行器
|
||||
*
|
||||
* 行为树的入口节点,执行其唯一的子节点
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Root',
|
||||
nodeType: NodeType.Root,
|
||||
displayName: '根节点',
|
||||
description: '行为树的入口节点',
|
||||
category: 'Root',
|
||||
childrenConstraints: {
|
||||
min: 1,
|
||||
max: 1
|
||||
}
|
||||
})
|
||||
export class RootExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData } = context;
|
||||
|
||||
// 根节点必须有且仅有一个子节点
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
return context.executeChild(childId);
|
||||
}
|
||||
|
||||
reset(_context: NodeExecutionContext): void {
|
||||
// 根节点没有需要重置的状态
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 选择器节点执行器
|
||||
*
|
||||
* 按顺序执行子节点,任一成功则成功,全部失败才失败
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Selector',
|
||||
nodeType: NodeType.Composite,
|
||||
displayName: '选择器',
|
||||
description: '按顺序执行子节点,任一成功则成功',
|
||||
category: 'Composite',
|
||||
childrenConstraints: {
|
||||
min: 1
|
||||
}
|
||||
})
|
||||
export class SelectorExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, state } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
while (state.currentChildIndex < nodeData.children.length) {
|
||||
const childId = nodeData.children[state.currentChildIndex]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (status === TaskStatus.Success) {
|
||||
state.currentChildIndex = 0;
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
state.currentChildIndex++;
|
||||
}
|
||||
|
||||
state.currentChildIndex = 0;
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
context.state.currentChildIndex = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 序列节点执行器
|
||||
*
|
||||
* 按顺序执行子节点,全部成功才成功,任一失败则失败
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Sequence',
|
||||
nodeType: NodeType.Composite,
|
||||
displayName: '序列',
|
||||
description: '按顺序执行子节点,全部成功才成功',
|
||||
category: 'Composite',
|
||||
childrenConstraints: {
|
||||
min: 1
|
||||
}
|
||||
})
|
||||
export class SequenceExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, state } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
while (state.currentChildIndex < nodeData.children.length) {
|
||||
const childId = nodeData.children[state.currentChildIndex]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (status === TaskStatus.Failure) {
|
||||
state.currentChildIndex = 0;
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
state.currentChildIndex++;
|
||||
}
|
||||
|
||||
state.currentChildIndex = 0;
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
context.state.currentChildIndex = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* Service执行接口
|
||||
*/
|
||||
export interface IServiceExecutor {
|
||||
/**
|
||||
* Service开始执行
|
||||
*/
|
||||
onServiceStart?(context: NodeExecutionContext): void;
|
||||
|
||||
/**
|
||||
* Service每帧更新
|
||||
*/
|
||||
onServiceTick(context: NodeExecutionContext): void;
|
||||
|
||||
/**
|
||||
* Service结束执行
|
||||
*/
|
||||
onServiceEnd?(context: NodeExecutionContext): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service注册表
|
||||
*/
|
||||
class ServiceRegistry {
|
||||
private static services: Map<string, IServiceExecutor> = new Map();
|
||||
|
||||
static register(name: string, service: IServiceExecutor): void {
|
||||
this.services.set(name, service);
|
||||
}
|
||||
|
||||
static get(name: string): IServiceExecutor | undefined {
|
||||
return this.services.get(name);
|
||||
}
|
||||
|
||||
static has(name: string): boolean {
|
||||
return this.services.has(name);
|
||||
}
|
||||
|
||||
static unregister(name: string): boolean {
|
||||
return this.services.delete(name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Service装饰器执行器
|
||||
*
|
||||
* 在子节点执行期间持续运行后台逻辑
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Service',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: 'Service',
|
||||
description: '在子节点执行期间持续运行后台逻辑',
|
||||
category: 'Decorator',
|
||||
configSchema: {
|
||||
serviceName: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Service名称'
|
||||
},
|
||||
tickInterval: {
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: 'Service更新间隔(秒,0表示每帧更新)',
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class ServiceDecorator implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, state, totalTime } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const serviceName = BindingHelper.getValue<string>(context, 'serviceName', '');
|
||||
const tickInterval = BindingHelper.getValue<number>(context, 'tickInterval', 0);
|
||||
|
||||
if (!serviceName) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const service = ServiceRegistry.get(serviceName);
|
||||
if (!service) {
|
||||
console.warn(`未找到Service: ${serviceName}`);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
if (state.status !== TaskStatus.Running) {
|
||||
state.startTime = totalTime;
|
||||
state.lastExecutionTime = totalTime;
|
||||
|
||||
if (service.onServiceStart) {
|
||||
service.onServiceStart(context);
|
||||
}
|
||||
}
|
||||
|
||||
const shouldTick = tickInterval === 0 ||
|
||||
(state.lastExecutionTime !== undefined &&
|
||||
(totalTime - state.lastExecutionTime) >= tickInterval);
|
||||
|
||||
if (shouldTick) {
|
||||
service.onServiceTick(context);
|
||||
state.lastExecutionTime = totalTime;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
const childStatus = context.executeChild(childId);
|
||||
|
||||
if (childStatus !== TaskStatus.Running) {
|
||||
if (service.onServiceEnd) {
|
||||
service.onServiceEnd(context);
|
||||
}
|
||||
}
|
||||
|
||||
return childStatus;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
const { nodeData, runtime, state } = context;
|
||||
|
||||
const serviceName = BindingHelper.getValue<string>(context, 'serviceName', '');
|
||||
if (serviceName) {
|
||||
const service = ServiceRegistry.get(serviceName);
|
||||
if (service && service.onServiceEnd) {
|
||||
service.onServiceEnd(context);
|
||||
}
|
||||
}
|
||||
|
||||
delete state.startTime;
|
||||
delete state.lastExecutionTime;
|
||||
|
||||
if (nodeData.children && nodeData.children.length > 0) {
|
||||
runtime.resetNodeState(nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { ServiceRegistry };
|
||||
@@ -0,0 +1,43 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 设置黑板值动作执行器
|
||||
*
|
||||
* 设置黑板中的变量值
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'SetBlackboardValue',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '设置黑板值',
|
||||
description: '设置黑板中的变量值',
|
||||
category: 'Action',
|
||||
configSchema: {
|
||||
key: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '黑板变量名'
|
||||
},
|
||||
value: {
|
||||
type: 'object',
|
||||
description: '要设置的值',
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class SetBlackboardValue implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime } = context;
|
||||
const key = BindingHelper.getValue<string>(context, 'key', '');
|
||||
const value = BindingHelper.getValue(context, 'value');
|
||||
|
||||
if (!key) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
runtime.setBlackboardValue(key, value);
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
import { BehaviorTreeAssetManager } from '../BehaviorTreeAssetManager';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* SubTree执行器
|
||||
*
|
||||
* 引用并执行其他行为树,实现模块化和复用
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'SubTree',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '子树',
|
||||
description: '引用并执行其他行为树',
|
||||
category: 'Special',
|
||||
configSchema: {
|
||||
treeAssetId: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '要执行的行为树资产ID',
|
||||
supportBinding: true
|
||||
},
|
||||
shareBlackboard: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: '是否共享黑板数据'
|
||||
}
|
||||
}
|
||||
})
|
||||
export class SubTreeExecutor implements INodeExecutor {
|
||||
private assetManager: BehaviorTreeAssetManager | null = null;
|
||||
|
||||
private getAssetManager(): BehaviorTreeAssetManager {
|
||||
if (!this.assetManager) {
|
||||
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
}
|
||||
return this.assetManager;
|
||||
}
|
||||
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime, state, entity } = context;
|
||||
|
||||
const treeAssetId = BindingHelper.getValue<string>(context, 'treeAssetId', '');
|
||||
const shareBlackboard = BindingHelper.getValue<boolean>(context, 'shareBlackboard', true);
|
||||
|
||||
if (!treeAssetId) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const assetManager = this.getAssetManager();
|
||||
const subTreeData = assetManager.getAsset(treeAssetId);
|
||||
|
||||
if (!subTreeData) {
|
||||
console.warn(`未找到子树资产: ${treeAssetId}`);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const rootNode = subTreeData.nodes.get(subTreeData.rootNodeId);
|
||||
if (!rootNode) {
|
||||
console.warn(`子树根节点未找到: ${subTreeData.rootNodeId}`);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
if (!shareBlackboard && state.status !== TaskStatus.Running) {
|
||||
if (subTreeData.blackboardVariables) {
|
||||
for (const [key, value] of subTreeData.blackboardVariables.entries()) {
|
||||
if (!runtime.hasBlackboardKey(key)) {
|
||||
runtime.setBlackboardValue(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const subTreeContext: NodeExecutionContext = {
|
||||
entity,
|
||||
nodeData: rootNode,
|
||||
state: runtime.getNodeState(rootNode.id),
|
||||
runtime,
|
||||
treeData: subTreeData,
|
||||
deltaTime: context.deltaTime,
|
||||
totalTime: context.totalTime,
|
||||
executeChild: (childId: string) => {
|
||||
const childData = subTreeData.nodes.get(childId);
|
||||
if (!childData) {
|
||||
console.warn(`子树节点未找到: ${childId}`);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const childContext: NodeExecutionContext = {
|
||||
entity,
|
||||
nodeData: childData,
|
||||
state: runtime.getNodeState(childId),
|
||||
runtime,
|
||||
treeData: subTreeData,
|
||||
deltaTime: context.deltaTime,
|
||||
totalTime: context.totalTime,
|
||||
executeChild: subTreeContext.executeChild
|
||||
};
|
||||
|
||||
return this.executeSubTreeNode(childContext);
|
||||
}
|
||||
};
|
||||
|
||||
return this.executeSubTreeNode(subTreeContext);
|
||||
}
|
||||
|
||||
private executeSubTreeNode(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, runtime } = context;
|
||||
|
||||
const state = runtime.getNodeState(nodeData.id);
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[state.currentChildIndex]!;
|
||||
const childStatus = context.executeChild(childId);
|
||||
|
||||
if (childStatus === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (childStatus === TaskStatus.Failure) {
|
||||
state.currentChildIndex = 0;
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
state.currentChildIndex++;
|
||||
|
||||
if (state.currentChildIndex >= nodeData.children.length) {
|
||||
state.currentChildIndex = 0;
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
const treeAssetId = BindingHelper.getValue<string>(context, 'treeAssetId', '');
|
||||
|
||||
if (treeAssetId) {
|
||||
const assetManager = this.getAssetManager();
|
||||
const subTreeData = assetManager.getAsset(treeAssetId);
|
||||
|
||||
if (subTreeData) {
|
||||
const rootNode = subTreeData.nodes.get(subTreeData.rootNodeId);
|
||||
if (rootNode) {
|
||||
context.runtime.resetNodeState(rootNode.id);
|
||||
|
||||
if (rootNode.children) {
|
||||
for (const childId of rootNode.children) {
|
||||
context.runtime.resetNodeState(childId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 超时装饰器执行器
|
||||
*
|
||||
* 限制子节点的执行时间
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Timeout',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: '超时',
|
||||
description: '限制子节点的执行时间',
|
||||
category: 'Decorator',
|
||||
configSchema: {
|
||||
timeout: {
|
||||
type: 'number',
|
||||
default: 1.0,
|
||||
description: '超时时间(秒)',
|
||||
min: 0,
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class TimeoutExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, state, totalTime } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const timeout = BindingHelper.getValue<number>(context, 'timeout', 1.0);
|
||||
|
||||
if (state.startTime === undefined) {
|
||||
state.startTime = totalTime;
|
||||
}
|
||||
|
||||
const elapsedTime = totalTime - state.startTime;
|
||||
if (elapsedTime >= timeout) {
|
||||
delete state.startTime;
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
delete state.startTime;
|
||||
return status;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
delete context.state.startTime;
|
||||
if (context.nodeData.children && context.nodeData.children.length > 0) {
|
||||
context.runtime.resetNodeState(context.nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 直到失败装饰器执行器
|
||||
*
|
||||
* 重复执行子节点直到失败
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'UntilFail',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: '直到失败',
|
||||
description: '重复执行子节点直到失败',
|
||||
category: 'Decorator'
|
||||
})
|
||||
export class UntilFailExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, runtime } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (status === TaskStatus.Failure) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
runtime.resetNodeState(childId);
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
if (context.nodeData.children && context.nodeData.children.length > 0) {
|
||||
context.runtime.resetNodeState(context.nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 直到成功装饰器执行器
|
||||
*
|
||||
* 重复执行子节点直到成功
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'UntilSuccess',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: '直到成功',
|
||||
description: '重复执行子节点直到成功',
|
||||
category: 'Decorator'
|
||||
})
|
||||
export class UntilSuccessExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, runtime } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (status === TaskStatus.Success) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
runtime.resetNodeState(childId);
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
if (context.nodeData.children && context.nodeData.children.length > 0) {
|
||||
context.runtime.resetNodeState(context.nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 等待动作执行器
|
||||
*
|
||||
* 等待指定时间后返回成功
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Wait',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '等待',
|
||||
description: '等待指定时间后返回成功',
|
||||
category: 'Action',
|
||||
configSchema: {
|
||||
duration: {
|
||||
type: 'number',
|
||||
default: 1.0,
|
||||
description: '等待时长(秒)',
|
||||
min: 0,
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class WaitAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { state, totalTime } = context;
|
||||
const duration = BindingHelper.getValue<number>(context, 'duration', 1.0);
|
||||
|
||||
if (!state.startTime) {
|
||||
state.startTime = totalTime;
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (totalTime - state.startTime >= duration) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
delete context.state.startTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { TaskStatus } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
|
||||
|
||||
/**
|
||||
* 等待动作执行器
|
||||
*
|
||||
* 等待指定时间后返回成功
|
||||
*/
|
||||
export class WaitActionExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { state, nodeData, totalTime } = context;
|
||||
const duration = nodeData.config['duration'] as number || 1.0;
|
||||
|
||||
if (!state.startTime) {
|
||||
state.startTime = totalTime;
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (totalTime - state.startTime >= duration) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
delete context.state.startTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
export { RootExecutor } from './RootExecutor';
|
||||
export { SequenceExecutor } from './SequenceExecutor';
|
||||
export { SelectorExecutor } from './SelectorExecutor';
|
||||
export { ParallelExecutor } from './ParallelExecutor';
|
||||
export { ParallelSelectorExecutor } from './ParallelSelectorExecutor';
|
||||
export { RandomSequenceExecutor } from './RandomSequenceExecutor';
|
||||
export { RandomSelectorExecutor } from './RandomSelectorExecutor';
|
||||
|
||||
export { InverterExecutor } from './InverterExecutor';
|
||||
export { RepeaterExecutor } from './RepeaterExecutor';
|
||||
export { AlwaysSucceedExecutor } from './AlwaysSucceedExecutor';
|
||||
export { AlwaysFailExecutor } from './AlwaysFailExecutor';
|
||||
export { UntilSuccessExecutor } from './UntilSuccessExecutor';
|
||||
export { UntilFailExecutor } from './UntilFailExecutor';
|
||||
export { ConditionalExecutor } from './ConditionalExecutor';
|
||||
export { CooldownExecutor } from './CooldownExecutor';
|
||||
export { TimeoutExecutor } from './TimeoutExecutor';
|
||||
export { ServiceDecorator, ServiceRegistry } from './ServiceDecorator';
|
||||
export type { IServiceExecutor } from './ServiceDecorator';
|
||||
|
||||
export { WaitAction } from './WaitAction';
|
||||
export { LogAction } from './LogAction';
|
||||
export { SetBlackboardValue } from './SetBlackboardValue';
|
||||
export { ModifyBlackboardValue } from './ModifyBlackboardValue';
|
||||
export { ExecuteAction } from './ExecuteAction';
|
||||
export { SubTreeExecutor } from './SubTreeExecutor';
|
||||
|
||||
export { BlackboardCompare } from './BlackboardCompare';
|
||||
export { BlackboardExists } from './BlackboardExists';
|
||||
export { RandomProbability } from './RandomProbability';
|
||||
export { ExecuteCondition } from './ExecuteCondition';
|
||||
181
packages/framework/behavior-tree/src/execution/NodeExecutor.ts
Normal file
181
packages/framework/behavior-tree/src/execution/NodeExecutor.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { Entity } from '@esengine/ecs-framework';
|
||||
import { TaskStatus } from '../Types/TaskStatus';
|
||||
import { BehaviorNodeData, BehaviorTreeData, NodeRuntimeState } from './BehaviorTreeData';
|
||||
import { BehaviorTreeRuntimeComponent } from './BehaviorTreeRuntimeComponent';
|
||||
|
||||
/**
|
||||
* 节点执行上下文
|
||||
*
|
||||
* 包含执行节点所需的所有信息
|
||||
*/
|
||||
export interface NodeExecutionContext {
|
||||
/** 游戏Entity(行为树宿主) */
|
||||
readonly entity: Entity;
|
||||
|
||||
/** 节点数据 */
|
||||
readonly nodeData: BehaviorNodeData;
|
||||
|
||||
/** 节点运行时状态 */
|
||||
readonly state: NodeRuntimeState;
|
||||
|
||||
/** 运行时组件(访问黑板等) */
|
||||
readonly runtime: BehaviorTreeRuntimeComponent;
|
||||
|
||||
/** 行为树数据(访问子节点等) */
|
||||
readonly treeData: BehaviorTreeData;
|
||||
|
||||
/** 当前帧增量时间 */
|
||||
readonly deltaTime: number;
|
||||
|
||||
/** 总时间 */
|
||||
readonly totalTime: number;
|
||||
|
||||
/** 执行子节点 */
|
||||
executeChild(childId: string): TaskStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点执行器接口
|
||||
*
|
||||
* 所有节点类型都需要实现对应的执行器
|
||||
* 执行器是无状态的,状态存储在NodeRuntimeState中
|
||||
*/
|
||||
export interface INodeExecutor {
|
||||
/**
|
||||
* 执行节点逻辑
|
||||
*
|
||||
* @param context 执行上下文
|
||||
* @returns 执行结果状态
|
||||
*/
|
||||
execute(context: NodeExecutionContext): TaskStatus;
|
||||
|
||||
/**
|
||||
* 重置节点状态(可选)
|
||||
*
|
||||
* 当节点完成或被中断时调用
|
||||
*/
|
||||
reset?(context: NodeExecutionContext): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复合节点执行结果
|
||||
*/
|
||||
export interface CompositeExecutionResult {
|
||||
/** 节点状态 */
|
||||
status: TaskStatus;
|
||||
|
||||
/** 要激活的子节点索引列表(undefined表示激活所有) */
|
||||
activateChildren?: number[];
|
||||
|
||||
/** 是否停止所有子节点 */
|
||||
stopAllChildren?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复合节点执行器接口
|
||||
*/
|
||||
export interface ICompositeExecutor extends INodeExecutor {
|
||||
/**
|
||||
* 执行复合节点逻辑
|
||||
*
|
||||
* @param context 执行上下文
|
||||
* @returns 复合节点执行结果
|
||||
*/
|
||||
executeComposite(context: NodeExecutionContext): CompositeExecutionResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定辅助工具
|
||||
*
|
||||
* 处理配置属性的黑板绑定
|
||||
*/
|
||||
export class BindingHelper {
|
||||
/**
|
||||
* 获取配置值(考虑黑板绑定)
|
||||
*
|
||||
* @param context 执行上下文
|
||||
* @param configKey 配置键名
|
||||
* @param defaultValue 默认值
|
||||
* @returns 解析后的值
|
||||
*/
|
||||
static getValue<T = any>(
|
||||
context: NodeExecutionContext,
|
||||
configKey: string,
|
||||
defaultValue?: T
|
||||
): T {
|
||||
const { nodeData, runtime } = context;
|
||||
|
||||
if (nodeData.bindings && nodeData.bindings[configKey]) {
|
||||
const blackboardKey = nodeData.bindings[configKey];
|
||||
const boundValue = runtime.getBlackboardValue<T>(blackboardKey);
|
||||
return boundValue !== undefined ? boundValue : (defaultValue as T);
|
||||
}
|
||||
|
||||
const configValue = nodeData.config[configKey];
|
||||
return configValue !== undefined ? configValue : (defaultValue as T);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查配置是否绑定到黑板变量
|
||||
*/
|
||||
static hasBinding(context: NodeExecutionContext, configKey: string): boolean {
|
||||
return !!(context.nodeData.bindings && context.nodeData.bindings[configKey]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取绑定的黑板变量名
|
||||
*/
|
||||
static getBindingKey(context: NodeExecutionContext, configKey: string): string | undefined {
|
||||
return context.nodeData.bindings?.[configKey];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点执行器注册表
|
||||
*
|
||||
* 管理所有节点类型的执行器
|
||||
*/
|
||||
export class NodeExecutorRegistry {
|
||||
private executors: Map<string, INodeExecutor> = new Map();
|
||||
|
||||
/**
|
||||
* 注册执行器
|
||||
*
|
||||
* @param implementationType 节点实现类型(对应BehaviorNodeData.implementationType)
|
||||
* @param executor 执行器实例
|
||||
*/
|
||||
register(implementationType: string, executor: INodeExecutor): void {
|
||||
if (this.executors.has(implementationType)) {
|
||||
console.warn(`执行器已存在,将被覆盖: ${implementationType}`);
|
||||
}
|
||||
this.executors.set(implementationType, executor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取执行器
|
||||
*/
|
||||
get(implementationType: string): INodeExecutor | undefined {
|
||||
return this.executors.get(implementationType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有执行器
|
||||
*/
|
||||
has(implementationType: string): boolean {
|
||||
return this.executors.has(implementationType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销执行器
|
||||
*/
|
||||
unregister(implementationType: string): boolean {
|
||||
return this.executors.delete(implementationType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有执行器
|
||||
*/
|
||||
clear(): void {
|
||||
this.executors.clear();
|
||||
}
|
||||
}
|
||||
108
packages/framework/behavior-tree/src/execution/NodeMetadata.ts
Normal file
108
packages/framework/behavior-tree/src/execution/NodeMetadata.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { NodeType } from '../Types/TaskStatus';
|
||||
|
||||
/**
|
||||
* 配置参数定义
|
||||
*/
|
||||
export interface ConfigFieldDefinition {
|
||||
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
|
||||
default?: any;
|
||||
description?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
options?: string[];
|
||||
supportBinding?: boolean;
|
||||
allowMultipleConnections?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 子节点约束配置
|
||||
*/
|
||||
export interface ChildrenConstraints {
|
||||
min?: number;
|
||||
max?: number;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点元数据
|
||||
*/
|
||||
export interface NodeMetadata {
|
||||
implementationType: string;
|
||||
nodeType: NodeType;
|
||||
displayName: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
configSchema?: Record<string, ConfigFieldDefinition>;
|
||||
childrenConstraints?: ChildrenConstraints;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点元数据默认值
|
||||
*/
|
||||
export class NodeMetadataDefaults {
|
||||
static getDefaultConstraints(nodeType: NodeType): ChildrenConstraints | undefined {
|
||||
switch (nodeType) {
|
||||
case NodeType.Composite:
|
||||
return { min: 1 };
|
||||
case NodeType.Decorator:
|
||||
return { min: 1, max: 1 };
|
||||
case NodeType.Action:
|
||||
case NodeType.Condition:
|
||||
return { max: 0 };
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点元数据注册表
|
||||
*/
|
||||
export class NodeMetadataRegistry {
|
||||
private static metadataMap: Map<string, NodeMetadata> = new Map();
|
||||
private static executorClassMap: Map<Function, string> = new Map();
|
||||
private static executorConstructors: Map<string, new () => any> = new Map();
|
||||
|
||||
static register(target: Function, metadata: NodeMetadata): void {
|
||||
this.metadataMap.set(metadata.implementationType, metadata);
|
||||
this.executorClassMap.set(target, metadata.implementationType);
|
||||
this.executorConstructors.set(metadata.implementationType, target as new () => any);
|
||||
}
|
||||
|
||||
static getMetadata(implementationType: string): NodeMetadata | undefined {
|
||||
return this.metadataMap.get(implementationType);
|
||||
}
|
||||
|
||||
static getAllMetadata(): NodeMetadata[] {
|
||||
return Array.from(this.metadataMap.values());
|
||||
}
|
||||
|
||||
static getByCategory(category: string): NodeMetadata[] {
|
||||
return this.getAllMetadata().filter((m) => m.category === category);
|
||||
}
|
||||
|
||||
static getByNodeType(nodeType: NodeType): NodeMetadata[] {
|
||||
return this.getAllMetadata().filter((m) => m.nodeType === nodeType);
|
||||
}
|
||||
|
||||
static getImplementationType(executorClass: Function): string | undefined {
|
||||
return this.executorClassMap.get(executorClass);
|
||||
}
|
||||
|
||||
static getExecutorConstructor(implementationType: string): (new () => any) | undefined {
|
||||
return this.executorConstructors.get(implementationType);
|
||||
}
|
||||
|
||||
static getAllExecutorConstructors(): Map<string, new () => any> {
|
||||
return new Map(this.executorConstructors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点执行器元数据装饰器
|
||||
*/
|
||||
export function NodeExecutorMetadata(metadata: NodeMetadata) {
|
||||
return function (target: Function) {
|
||||
NodeMetadataRegistry.register(target, metadata);
|
||||
};
|
||||
}
|
||||
11
packages/framework/behavior-tree/src/execution/index.ts
Normal file
11
packages/framework/behavior-tree/src/execution/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export type { BehaviorTreeData, BehaviorNodeData, NodeRuntimeState } from './BehaviorTreeData';
|
||||
export { createDefaultRuntimeState } from './BehaviorTreeData';
|
||||
export { BehaviorTreeRuntimeComponent } from './BehaviorTreeRuntimeComponent';
|
||||
export { BehaviorTreeAssetManager } from './BehaviorTreeAssetManager';
|
||||
export type { INodeExecutor, NodeExecutionContext } from './NodeExecutor';
|
||||
export { NodeExecutorRegistry, BindingHelper } from './NodeExecutor';
|
||||
export { BehaviorTreeExecutionSystem } from './BehaviorTreeExecutionSystem';
|
||||
export type { NodeMetadata, ConfigFieldDefinition, NodeExecutorMetadata } from './NodeMetadata';
|
||||
export { NodeMetadataRegistry } from './NodeMetadata';
|
||||
|
||||
export * from './Executors';
|
||||
67
packages/framework/behavior-tree/src/index.ts
Normal file
67
packages/framework/behavior-tree/src/index.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @esengine/behavior-tree
|
||||
*
|
||||
* @zh AI 行为树系统,支持运行时执行和可视化编辑
|
||||
* @en AI Behavior Tree System with runtime execution and visual editor support
|
||||
*
|
||||
* @zh 此包是通用的行为树实现,可以与任何 ECS 框架配合使用。
|
||||
* 对于 ESEngine 集成,请从 '@esengine/behavior-tree/esengine' 导入插件。
|
||||
*
|
||||
* @en This package is a generic behavior tree implementation that works with any ECS framework.
|
||||
* For ESEngine integration, import the plugin from '@esengine/behavior-tree/esengine'.
|
||||
*
|
||||
* @example Cocos/Laya/通用 ECS 使用方式:
|
||||
* ```typescript
|
||||
* import {
|
||||
* BehaviorTreeAssetManager,
|
||||
* BehaviorTreeExecutionSystem,
|
||||
* BehaviorTreeRuntimeComponent
|
||||
* } from '@esengine/behavior-tree';
|
||||
*
|
||||
* // 1. Register service
|
||||
* Core.services.registerSingleton(BehaviorTreeAssetManager);
|
||||
*
|
||||
* // 2. Load behavior tree from JSON
|
||||
* const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
* assetManager.loadFromEditorJSON(jsonContent);
|
||||
*
|
||||
* // 3. Add component to entity
|
||||
* entity.addComponent(new BehaviorTreeRuntimeComponent());
|
||||
*
|
||||
* // 4. Add system to scene
|
||||
* scene.addSystem(new BehaviorTreeExecutionSystem());
|
||||
* ```
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
// Constants
|
||||
export { BehaviorTreeAssetType } from './constants';
|
||||
|
||||
// Types
|
||||
export * from './Types/TaskStatus';
|
||||
export type { IBTAssetManager, IBehaviorTreeAssetContent } from './Types/AssetManagerInterface';
|
||||
|
||||
// Execution (runtime core)
|
||||
export * from './execution';
|
||||
|
||||
// Utilities
|
||||
export * from './BehaviorTreeStarter';
|
||||
export * from './BehaviorTreeBuilder';
|
||||
|
||||
// Serialization
|
||||
export * from './Serialization/NodeTemplates';
|
||||
export * from './Serialization/BehaviorTreeAsset';
|
||||
export * from './Serialization/EditorFormatConverter';
|
||||
export * from './Serialization/BehaviorTreeAssetSerializer';
|
||||
export * from './Serialization/EditorToBehaviorTreeDataConverter';
|
||||
|
||||
// Services
|
||||
export * from './Services/GlobalBlackboardService';
|
||||
|
||||
// Blackboard types (excluding BlackboardValueType which is already exported from TaskStatus)
|
||||
export type { BlackboardTypeDefinition } from './Blackboard/BlackboardTypes';
|
||||
export { BlackboardTypes } from './Blackboard/BlackboardTypes';
|
||||
|
||||
// Service tokens (using ecs-framework's createServiceToken, not engine-core)
|
||||
export { BehaviorTreeSystemToken } from './tokens';
|
||||
17
packages/framework/behavior-tree/src/tokens.ts
Normal file
17
packages/framework/behavior-tree/src/tokens.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 行为树模块服务令牌
|
||||
* Behavior tree module service tokens
|
||||
*/
|
||||
|
||||
import { createServiceToken } from '@esengine/ecs-framework';
|
||||
import type { BehaviorTreeExecutionSystem } from './execution/BehaviorTreeExecutionSystem';
|
||||
|
||||
// ============================================================================
|
||||
// 行为树模块导出的令牌 | Tokens exported by behavior tree module
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 行为树执行系统令牌
|
||||
* Behavior tree execution system token
|
||||
*/
|
||||
export const BehaviorTreeSystemToken = createServiceToken<BehaviorTreeExecutionSystem>('behaviorTreeSystem');
|
||||
23
packages/framework/behavior-tree/tsconfig.build.json
Normal file
23
packages/framework/behavior-tree/tsconfig.build.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"jsx": "react-jsx",
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
43
packages/framework/behavior-tree/tsconfig.json
Normal file
43
packages/framework/behavior-tree/tsconfig.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": false,
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM"
|
||||
],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"composite": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"jsx": "react-jsx",
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"plugin.json"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"src/esengine",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../core"
|
||||
}
|
||||
]
|
||||
}
|
||||
13
packages/framework/behavior-tree/tsup.config.ts
Normal file
13
packages/framework/behavior-tree/tsup.config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { runtimeOnlyPreset } from '../../tools/build-config/src/presets/plugin-tsup';
|
||||
|
||||
export default defineConfig({
|
||||
...runtimeOnlyPreset({
|
||||
tsupConfig: {
|
||||
entry: {
|
||||
index: 'src/index.ts'
|
||||
}
|
||||
}
|
||||
}),
|
||||
tsconfig: 'tsconfig.build.json'
|
||||
});
|
||||
43
packages/framework/blueprint/module.json
Normal file
43
packages/framework/blueprint/module.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"id": "blueprint",
|
||||
"name": "@esengine/blueprint",
|
||||
"displayName": "Blueprint",
|
||||
"description": "Visual scripting system | 可视化脚本系统",
|
||||
"version": "1.0.0",
|
||||
"category": "AI",
|
||||
"icon": "Workflow",
|
||||
"tags": [
|
||||
"visual",
|
||||
"scripting",
|
||||
"blueprint",
|
||||
"nodes"
|
||||
],
|
||||
"isCore": false,
|
||||
"defaultEnabled": false,
|
||||
"isEngineModule": true,
|
||||
"canContainContent": true,
|
||||
"platforms": [
|
||||
"web",
|
||||
"desktop"
|
||||
],
|
||||
"dependencies": [
|
||||
"core"
|
||||
],
|
||||
"exports": {
|
||||
"components": [
|
||||
"BlueprintComponent"
|
||||
],
|
||||
"systems": [
|
||||
"BlueprintSystem"
|
||||
],
|
||||
"other": [
|
||||
"Blueprint",
|
||||
"BlueprintNode",
|
||||
"BlueprintGraph"
|
||||
]
|
||||
},
|
||||
"editorPackage": "@esengine/blueprint-editor",
|
||||
"requiresWasm": false,
|
||||
"outputPath": "dist/index.js",
|
||||
"pluginExport": "BlueprintPlugin"
|
||||
}
|
||||
58
packages/framework/blueprint/package.json
Normal file
58
packages/framework/blueprint/package.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "@esengine/blueprint",
|
||||
"version": "1.0.0",
|
||||
"description": "Visual scripting system - works with any ECS framework (ESEngine, Cocos, Laya, etc.)",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf dist tsconfig.tsbuildinfo",
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"blueprint",
|
||||
"visual-scripting",
|
||||
"game-engine",
|
||||
"cocos",
|
||||
"laya",
|
||||
"esengine"
|
||||
],
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"@types/node": "^20.19.17",
|
||||
"rimraf": "^5.0.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/esengine/esengine.git",
|
||||
"directory": "packages/framework/blueprint"
|
||||
}
|
||||
}
|
||||
30
packages/framework/blueprint/plugin.json
Normal file
30
packages/framework/blueprint/plugin.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"id": "@esengine/blueprint",
|
||||
"name": "Blueprint System",
|
||||
"version": "1.0.0",
|
||||
"description": "Visual scripting system for creating game logic without code",
|
||||
"category": "scripting",
|
||||
"loadingPhase": "default",
|
||||
"enabledByDefault": true,
|
||||
"canContainContent": true,
|
||||
"isEnginePlugin": false,
|
||||
"modules": [
|
||||
{
|
||||
"name": "BlueprintRuntime",
|
||||
"type": "runtime",
|
||||
"entry": "./src/runtime.ts"
|
||||
},
|
||||
{
|
||||
"name": "BlueprintEditor",
|
||||
"type": "editor",
|
||||
"entry": "./src/editor/index.ts"
|
||||
}
|
||||
],
|
||||
"dependencies": [
|
||||
{
|
||||
"id": "@esengine/core",
|
||||
"version": ">=1.0.0"
|
||||
}
|
||||
],
|
||||
"icon": "Workflow"
|
||||
}
|
||||
@@ -0,0 +1,575 @@
|
||||
/**
|
||||
* @zh 蓝图组合器接口和实现
|
||||
* @en Blueprint Composer Interface and Implementation
|
||||
*
|
||||
* @zh 将多个蓝图片段组合成一个完整的蓝图
|
||||
* @en Composes multiple blueprint fragments into a complete blueprint
|
||||
*/
|
||||
|
||||
import type { BlueprintAsset, BlueprintVariable } from '../types/blueprint';
|
||||
import type { BlueprintNode, BlueprintConnection } from '../types/nodes';
|
||||
import type { IBlueprintFragment } from './BlueprintFragment';
|
||||
|
||||
// =============================================================================
|
||||
// 槽位定义 | Slot Definition
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 片段槽位
|
||||
* @en Fragment slot
|
||||
*
|
||||
* @zh 组合器中放置片段的位置
|
||||
* @en A position in the composer where a fragment is placed
|
||||
*/
|
||||
export interface FragmentSlot {
|
||||
/**
|
||||
* @zh 槽位 ID
|
||||
* @en Slot ID
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* @zh 槽位名称
|
||||
* @en Slot name
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* @zh 放置的片段
|
||||
* @en Placed fragment
|
||||
*/
|
||||
readonly fragment: IBlueprintFragment;
|
||||
|
||||
/**
|
||||
* @zh 在组合图中的位置偏移
|
||||
* @en Position offset in the composed graph
|
||||
*/
|
||||
readonly position: { x: number; y: number };
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 槽位间连接
|
||||
* @en Connection between slots
|
||||
*/
|
||||
export interface SlotConnection {
|
||||
/**
|
||||
* @zh 连接 ID
|
||||
* @en Connection ID
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* @zh 源槽位 ID
|
||||
* @en Source slot ID
|
||||
*/
|
||||
readonly fromSlotId: string;
|
||||
|
||||
/**
|
||||
* @zh 源引脚名称
|
||||
* @en Source pin name
|
||||
*/
|
||||
readonly fromPin: string;
|
||||
|
||||
/**
|
||||
* @zh 目标槽位 ID
|
||||
* @en Target slot ID
|
||||
*/
|
||||
readonly toSlotId: string;
|
||||
|
||||
/**
|
||||
* @zh 目标引脚名称
|
||||
* @en Target pin name
|
||||
*/
|
||||
readonly toPin: string;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 组合器接口 | Composer Interface
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 蓝图组合器接口
|
||||
* @en Blueprint composer interface
|
||||
*
|
||||
* @zh 用于将多个蓝图片段组合成一个完整蓝图
|
||||
* @en Used to compose multiple blueprint fragments into a complete blueprint
|
||||
*/
|
||||
export interface IBlueprintComposer {
|
||||
/**
|
||||
* @zh 组合器名称
|
||||
* @en Composer name
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* @zh 获取所有槽位
|
||||
* @en Get all slots
|
||||
*/
|
||||
getSlots(): FragmentSlot[];
|
||||
|
||||
/**
|
||||
* @zh 获取所有连接
|
||||
* @en Get all connections
|
||||
*/
|
||||
getConnections(): SlotConnection[];
|
||||
|
||||
/**
|
||||
* @zh 添加片段到槽位
|
||||
* @en Add fragment to slot
|
||||
*
|
||||
* @param fragment - @zh 蓝图片段 @en Blueprint fragment
|
||||
* @param slotId - @zh 槽位 ID @en Slot ID
|
||||
* @param options - @zh 选项 @en Options
|
||||
*/
|
||||
addFragment(
|
||||
fragment: IBlueprintFragment,
|
||||
slotId: string,
|
||||
options?: {
|
||||
name?: string;
|
||||
position?: { x: number; y: number };
|
||||
}
|
||||
): void;
|
||||
|
||||
/**
|
||||
* @zh 移除槽位
|
||||
* @en Remove slot
|
||||
*
|
||||
* @param slotId - @zh 槽位 ID @en Slot ID
|
||||
*/
|
||||
removeSlot(slotId: string): void;
|
||||
|
||||
/**
|
||||
* @zh 连接两个槽位的引脚
|
||||
* @en Connect pins between two slots
|
||||
*
|
||||
* @param fromSlotId - @zh 源槽位 ID @en Source slot ID
|
||||
* @param fromPin - @zh 源引脚名称 @en Source pin name
|
||||
* @param toSlotId - @zh 目标槽位 ID @en Target slot ID
|
||||
* @param toPin - @zh 目标引脚名称 @en Target pin name
|
||||
*/
|
||||
connect(
|
||||
fromSlotId: string,
|
||||
fromPin: string,
|
||||
toSlotId: string,
|
||||
toPin: string
|
||||
): void;
|
||||
|
||||
/**
|
||||
* @zh 断开连接
|
||||
* @en Disconnect
|
||||
*
|
||||
* @param connectionId - @zh 连接 ID @en Connection ID
|
||||
*/
|
||||
disconnect(connectionId: string): void;
|
||||
|
||||
/**
|
||||
* @zh 验证组合是否有效
|
||||
* @en Validate if the composition is valid
|
||||
*/
|
||||
validate(): CompositionValidationResult;
|
||||
|
||||
/**
|
||||
* @zh 编译成蓝图资产
|
||||
* @en Compile into blueprint asset
|
||||
*/
|
||||
compile(): BlueprintAsset;
|
||||
|
||||
/**
|
||||
* @zh 清空组合器
|
||||
* @en Clear the composer
|
||||
*/
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 验证结果 | Validation Result
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 组合验证结果
|
||||
* @en Composition validation result
|
||||
*/
|
||||
export interface CompositionValidationResult {
|
||||
/**
|
||||
* @zh 是否有效
|
||||
* @en Whether valid
|
||||
*/
|
||||
readonly isValid: boolean;
|
||||
|
||||
/**
|
||||
* @zh 错误列表
|
||||
* @en Error list
|
||||
*/
|
||||
readonly errors: CompositionError[];
|
||||
|
||||
/**
|
||||
* @zh 警告列表
|
||||
* @en Warning list
|
||||
*/
|
||||
readonly warnings: CompositionWarning[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 组合错误
|
||||
* @en Composition error
|
||||
*/
|
||||
export interface CompositionError {
|
||||
readonly type: 'missing-connection' | 'type-mismatch' | 'cycle-detected' | 'invalid-slot';
|
||||
readonly message: string;
|
||||
readonly slotId?: string;
|
||||
readonly pinName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 组合警告
|
||||
* @en Composition warning
|
||||
*/
|
||||
export interface CompositionWarning {
|
||||
readonly type: 'unused-output' | 'unconnected-input';
|
||||
readonly message: string;
|
||||
readonly slotId?: string;
|
||||
readonly pinName?: string;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 组合器实现 | Composer Implementation
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 蓝图组合器实现
|
||||
* @en Blueprint composer implementation
|
||||
*/
|
||||
export class BlueprintComposer implements IBlueprintComposer {
|
||||
readonly name: string;
|
||||
|
||||
private slots: Map<string, FragmentSlot> = new Map();
|
||||
private connections: Map<string, SlotConnection> = new Map();
|
||||
private connectionIdCounter = 0;
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
getSlots(): FragmentSlot[] {
|
||||
return Array.from(this.slots.values());
|
||||
}
|
||||
|
||||
getConnections(): SlotConnection[] {
|
||||
return Array.from(this.connections.values());
|
||||
}
|
||||
|
||||
addFragment(
|
||||
fragment: IBlueprintFragment,
|
||||
slotId: string,
|
||||
options?: {
|
||||
name?: string;
|
||||
position?: { x: number; y: number };
|
||||
}
|
||||
): void {
|
||||
if (this.slots.has(slotId)) {
|
||||
throw new Error(`Slot '${slotId}' already exists`);
|
||||
}
|
||||
|
||||
const slot: FragmentSlot = {
|
||||
id: slotId,
|
||||
name: options?.name ?? fragment.name,
|
||||
fragment,
|
||||
position: options?.position ?? { x: 0, y: 0 }
|
||||
};
|
||||
|
||||
this.slots.set(slotId, slot);
|
||||
}
|
||||
|
||||
removeSlot(slotId: string): void {
|
||||
if (!this.slots.has(slotId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all connections involving this slot
|
||||
const toRemove: string[] = [];
|
||||
for (const [id, conn] of this.connections) {
|
||||
if (conn.fromSlotId === slotId || conn.toSlotId === slotId) {
|
||||
toRemove.push(id);
|
||||
}
|
||||
}
|
||||
for (const id of toRemove) {
|
||||
this.connections.delete(id);
|
||||
}
|
||||
|
||||
this.slots.delete(slotId);
|
||||
}
|
||||
|
||||
connect(
|
||||
fromSlotId: string,
|
||||
fromPin: string,
|
||||
toSlotId: string,
|
||||
toPin: string
|
||||
): void {
|
||||
const fromSlot = this.slots.get(fromSlotId);
|
||||
const toSlot = this.slots.get(toSlotId);
|
||||
|
||||
if (!fromSlot) {
|
||||
throw new Error(`Source slot '${fromSlotId}' not found`);
|
||||
}
|
||||
if (!toSlot) {
|
||||
throw new Error(`Target slot '${toSlotId}' not found`);
|
||||
}
|
||||
|
||||
const fromPinDef = fromSlot.fragment.outputs.find(p => p.name === fromPin);
|
||||
const toPinDef = toSlot.fragment.inputs.find(p => p.name === toPin);
|
||||
|
||||
if (!fromPinDef) {
|
||||
throw new Error(`Output pin '${fromPin}' not found in slot '${fromSlotId}'`);
|
||||
}
|
||||
if (!toPinDef) {
|
||||
throw new Error(`Input pin '${toPin}' not found in slot '${toSlotId}'`);
|
||||
}
|
||||
|
||||
const connectionId = `conn_${++this.connectionIdCounter}`;
|
||||
|
||||
const connection: SlotConnection = {
|
||||
id: connectionId,
|
||||
fromSlotId,
|
||||
fromPin,
|
||||
toSlotId,
|
||||
toPin
|
||||
};
|
||||
|
||||
this.connections.set(connectionId, connection);
|
||||
}
|
||||
|
||||
disconnect(connectionId: string): void {
|
||||
this.connections.delete(connectionId);
|
||||
}
|
||||
|
||||
validate(): CompositionValidationResult {
|
||||
const errors: CompositionError[] = [];
|
||||
const warnings: CompositionWarning[] = [];
|
||||
|
||||
// Check for required inputs without connections
|
||||
for (const slot of this.slots.values()) {
|
||||
for (const input of slot.fragment.inputs) {
|
||||
const hasConnection = Array.from(this.connections.values()).some(
|
||||
c => c.toSlotId === slot.id && c.toPin === input.name
|
||||
);
|
||||
|
||||
if (!hasConnection && input.defaultValue === undefined) {
|
||||
warnings.push({
|
||||
type: 'unconnected-input',
|
||||
message: `Input '${input.name}' in slot '${slot.id}' is not connected`,
|
||||
slotId: slot.id,
|
||||
pinName: input.name
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check for unused outputs
|
||||
for (const output of slot.fragment.outputs) {
|
||||
const hasConnection = Array.from(this.connections.values()).some(
|
||||
c => c.fromSlotId === slot.id && c.fromPin === output.name
|
||||
);
|
||||
|
||||
if (!hasConnection) {
|
||||
warnings.push({
|
||||
type: 'unused-output',
|
||||
message: `Output '${output.name}' in slot '${slot.id}' is not connected`,
|
||||
slotId: slot.id,
|
||||
pinName: output.name
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check type compatibility
|
||||
for (const conn of this.connections.values()) {
|
||||
const fromSlot = this.slots.get(conn.fromSlotId);
|
||||
const toSlot = this.slots.get(conn.toSlotId);
|
||||
|
||||
if (!fromSlot || !toSlot) {
|
||||
errors.push({
|
||||
type: 'invalid-slot',
|
||||
message: `Invalid slot reference in connection '${conn.id}'`
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const fromPinDef = fromSlot.fragment.outputs.find(p => p.name === conn.fromPin);
|
||||
const toPinDef = toSlot.fragment.inputs.find(p => p.name === conn.toPin);
|
||||
|
||||
if (fromPinDef && toPinDef && fromPinDef.type !== toPinDef.type) {
|
||||
if (fromPinDef.type !== 'any' && toPinDef.type !== 'any') {
|
||||
errors.push({
|
||||
type: 'type-mismatch',
|
||||
message: `Type mismatch: '${fromPinDef.type}' -> '${toPinDef.type}' in connection '${conn.id}'`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
warnings
|
||||
};
|
||||
}
|
||||
|
||||
compile(): BlueprintAsset {
|
||||
const nodes: BlueprintNode[] = [];
|
||||
const connections: BlueprintConnection[] = [];
|
||||
const variables: BlueprintVariable[] = [];
|
||||
|
||||
const nodeIdMap = new Map<string, Map<string, string>>();
|
||||
|
||||
// Copy nodes from each fragment with new IDs
|
||||
let nodeIdCounter = 0;
|
||||
for (const slot of this.slots.values()) {
|
||||
const slotNodeMap = new Map<string, string>();
|
||||
nodeIdMap.set(slot.id, slotNodeMap);
|
||||
|
||||
for (const node of slot.fragment.graph.nodes) {
|
||||
const newNodeId = `node_${++nodeIdCounter}`;
|
||||
slotNodeMap.set(node.id, newNodeId);
|
||||
|
||||
nodes.push({
|
||||
...node,
|
||||
id: newNodeId,
|
||||
position: {
|
||||
x: node.position.x + slot.position.x,
|
||||
y: node.position.y + slot.position.y
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Copy internal connections
|
||||
for (const conn of slot.fragment.graph.connections) {
|
||||
const newFromId = slotNodeMap.get(conn.fromNodeId);
|
||||
const newToId = slotNodeMap.get(conn.toNodeId);
|
||||
|
||||
if (newFromId && newToId) {
|
||||
connections.push({
|
||||
...conn,
|
||||
id: `conn_internal_${connections.length}`,
|
||||
fromNodeId: newFromId,
|
||||
toNodeId: newToId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Copy variables (with slot prefix to avoid conflicts)
|
||||
for (const variable of slot.fragment.graph.variables) {
|
||||
variables.push({
|
||||
...variable,
|
||||
name: `${slot.id}_${variable.name}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create connections between slots based on exposed pins
|
||||
for (const slotConn of this.connections.values()) {
|
||||
const fromSlot = this.slots.get(slotConn.fromSlotId);
|
||||
const toSlot = this.slots.get(slotConn.toSlotId);
|
||||
|
||||
if (!fromSlot || !toSlot) continue;
|
||||
|
||||
const fromPinDef = fromSlot.fragment.outputs.find(p => p.name === slotConn.fromPin);
|
||||
const toPinDef = toSlot.fragment.inputs.find(p => p.name === slotConn.toPin);
|
||||
|
||||
if (!fromPinDef || !toPinDef) continue;
|
||||
|
||||
const fromNodeMap = nodeIdMap.get(slotConn.fromSlotId);
|
||||
const toNodeMap = nodeIdMap.get(slotConn.toSlotId);
|
||||
|
||||
if (!fromNodeMap || !toNodeMap) continue;
|
||||
|
||||
const fromNodeId = fromNodeMap.get(fromPinDef.internalNodeId);
|
||||
const toNodeId = toNodeMap.get(toPinDef.internalNodeId);
|
||||
|
||||
if (fromNodeId && toNodeId) {
|
||||
connections.push({
|
||||
id: `conn_slot_${connections.length}`,
|
||||
fromNodeId,
|
||||
fromPin: fromPinDef.internalPinName,
|
||||
toNodeId,
|
||||
toPin: toPinDef.internalPinName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
type: 'blueprint',
|
||||
metadata: {
|
||||
name: this.name,
|
||||
description: `Composed from ${this.slots.size} fragments`,
|
||||
createdAt: Date.now(),
|
||||
modifiedAt: Date.now()
|
||||
},
|
||||
variables,
|
||||
nodes,
|
||||
connections
|
||||
};
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.slots.clear();
|
||||
this.connections.clear();
|
||||
this.connectionIdCounter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 工厂函数 | Factory Functions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 创建蓝图组合器
|
||||
* @en Create blueprint composer
|
||||
*/
|
||||
export function createComposer(name: string): IBlueprintComposer {
|
||||
return new BlueprintComposer(name);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 组合资产格式 | Composition Asset Format
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 蓝图组合资产格式
|
||||
* @en Blueprint composition asset format
|
||||
*/
|
||||
export interface BlueprintCompositionAsset {
|
||||
/**
|
||||
* @zh 格式版本
|
||||
* @en Format version
|
||||
*/
|
||||
version: number;
|
||||
|
||||
/**
|
||||
* @zh 资产类型标识
|
||||
* @en Asset type identifier
|
||||
*/
|
||||
type: 'blueprint-composition';
|
||||
|
||||
/**
|
||||
* @zh 组合名称
|
||||
* @en Composition name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* @zh 槽位数据
|
||||
* @en Slot data
|
||||
*/
|
||||
slots: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
fragmentId: string;
|
||||
position: { x: number; y: number };
|
||||
}>;
|
||||
|
||||
/**
|
||||
* @zh 连接数据
|
||||
* @en Connection data
|
||||
*/
|
||||
connections: SlotConnection[];
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
/**
|
||||
* @zh 蓝图片段接口和实现
|
||||
* @en Blueprint Fragment Interface and Implementation
|
||||
*
|
||||
* @zh 定义可重用的蓝图片段,用于组合系统
|
||||
* @en Defines reusable blueprint fragments for the composition system
|
||||
*/
|
||||
|
||||
import type { BlueprintAsset } from '../types/blueprint';
|
||||
import type { BlueprintPinType } from '../types/pins';
|
||||
|
||||
// =============================================================================
|
||||
// 暴露引脚定义 | Exposed Pin Definition
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 暴露引脚定义
|
||||
* @en Exposed pin definition
|
||||
*
|
||||
* @zh 片段对外暴露的引脚,可与其他片段连接
|
||||
* @en Pins exposed by the fragment that can be connected to other fragments
|
||||
*/
|
||||
export interface ExposedPin {
|
||||
/**
|
||||
* @zh 引脚名称
|
||||
* @en Pin name
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* @zh 显示名称
|
||||
* @en Display name
|
||||
*/
|
||||
readonly displayName: string;
|
||||
|
||||
/**
|
||||
* @zh 引脚类型
|
||||
* @en Pin type
|
||||
*/
|
||||
readonly type: BlueprintPinType;
|
||||
|
||||
/**
|
||||
* @zh 引脚方向
|
||||
* @en Pin direction
|
||||
*/
|
||||
readonly direction: 'input' | 'output';
|
||||
|
||||
/**
|
||||
* @zh 描述
|
||||
* @en Description
|
||||
*/
|
||||
readonly description?: string;
|
||||
|
||||
/**
|
||||
* @zh 默认值(仅输入引脚)
|
||||
* @en Default value (input pins only)
|
||||
*/
|
||||
readonly defaultValue?: unknown;
|
||||
|
||||
/**
|
||||
* @zh 关联的内部节点 ID
|
||||
* @en Associated internal node ID
|
||||
*/
|
||||
readonly internalNodeId: string;
|
||||
|
||||
/**
|
||||
* @zh 关联的内部引脚名称
|
||||
* @en Associated internal pin name
|
||||
*/
|
||||
readonly internalPinName: string;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 蓝图片段接口 | Blueprint Fragment Interface
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 蓝图片段接口
|
||||
* @en Blueprint fragment interface
|
||||
*
|
||||
* @zh 代表一个可重用的蓝图逻辑单元,如技能、卡牌效果等
|
||||
* @en Represents a reusable unit of blueprint logic, such as skills, card effects, etc.
|
||||
*/
|
||||
export interface IBlueprintFragment {
|
||||
/**
|
||||
* @zh 片段唯一标识
|
||||
* @en Fragment unique identifier
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* @zh 片段名称
|
||||
* @en Fragment name
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* @zh 片段描述
|
||||
* @en Fragment description
|
||||
*/
|
||||
readonly description?: string;
|
||||
|
||||
/**
|
||||
* @zh 片段分类
|
||||
* @en Fragment category
|
||||
*/
|
||||
readonly category?: string;
|
||||
|
||||
/**
|
||||
* @zh 片段标签
|
||||
* @en Fragment tags
|
||||
*/
|
||||
readonly tags?: string[];
|
||||
|
||||
/**
|
||||
* @zh 暴露的输入引脚
|
||||
* @en Exposed input pins
|
||||
*/
|
||||
readonly inputs: ExposedPin[];
|
||||
|
||||
/**
|
||||
* @zh 暴露的输出引脚
|
||||
* @en Exposed output pins
|
||||
*/
|
||||
readonly outputs: ExposedPin[];
|
||||
|
||||
/**
|
||||
* @zh 内部蓝图图
|
||||
* @en Internal blueprint graph
|
||||
*/
|
||||
readonly graph: BlueprintAsset;
|
||||
|
||||
/**
|
||||
* @zh 片段版本
|
||||
* @en Fragment version
|
||||
*/
|
||||
readonly version?: string;
|
||||
|
||||
/**
|
||||
* @zh 图标名称
|
||||
* @en Icon name
|
||||
*/
|
||||
readonly icon?: string;
|
||||
|
||||
/**
|
||||
* @zh 颜色(用于可视化)
|
||||
* @en Color (for visualization)
|
||||
*/
|
||||
readonly color?: string;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 蓝图片段实现 | Blueprint Fragment Implementation
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 蓝图片段配置
|
||||
* @en Blueprint fragment configuration
|
||||
*/
|
||||
export interface BlueprintFragmentConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
tags?: string[];
|
||||
inputs?: ExposedPin[];
|
||||
outputs?: ExposedPin[];
|
||||
graph: BlueprintAsset;
|
||||
version?: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 蓝图片段实现
|
||||
* @en Blueprint fragment implementation
|
||||
*/
|
||||
export class BlueprintFragment implements IBlueprintFragment {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly description?: string;
|
||||
readonly category?: string;
|
||||
readonly tags?: string[];
|
||||
readonly inputs: ExposedPin[];
|
||||
readonly outputs: ExposedPin[];
|
||||
readonly graph: BlueprintAsset;
|
||||
readonly version?: string;
|
||||
readonly icon?: string;
|
||||
readonly color?: string;
|
||||
|
||||
constructor(config: BlueprintFragmentConfig) {
|
||||
this.id = config.id;
|
||||
this.name = config.name;
|
||||
this.description = config.description;
|
||||
this.category = config.category;
|
||||
this.tags = config.tags;
|
||||
this.inputs = config.inputs ?? [];
|
||||
this.outputs = config.outputs ?? [];
|
||||
this.graph = config.graph;
|
||||
this.version = config.version;
|
||||
this.icon = config.icon;
|
||||
this.color = config.color;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取所有暴露引脚
|
||||
* @en Get all exposed pins
|
||||
*/
|
||||
getAllExposedPins(): ExposedPin[] {
|
||||
return [...this.inputs, ...this.outputs];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 通过名称查找输入引脚
|
||||
* @en Find input pin by name
|
||||
*/
|
||||
findInput(name: string): ExposedPin | undefined {
|
||||
return this.inputs.find(p => p.name === name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 通过名称查找输出引脚
|
||||
* @en Find output pin by name
|
||||
*/
|
||||
findOutput(name: string): ExposedPin | undefined {
|
||||
return this.outputs.find(p => p.name === name);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 工厂函数 | Factory Functions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 创建暴露引脚
|
||||
* @en Create exposed pin
|
||||
*/
|
||||
export function createExposedPin(
|
||||
name: string,
|
||||
type: BlueprintPinType,
|
||||
direction: 'input' | 'output',
|
||||
internalNodeId: string,
|
||||
internalPinName: string,
|
||||
options?: {
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
defaultValue?: unknown;
|
||||
}
|
||||
): ExposedPin {
|
||||
return {
|
||||
name,
|
||||
displayName: options?.displayName ?? name,
|
||||
type,
|
||||
direction,
|
||||
description: options?.description,
|
||||
defaultValue: options?.defaultValue,
|
||||
internalNodeId,
|
||||
internalPinName
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建蓝图片段
|
||||
* @en Create blueprint fragment
|
||||
*/
|
||||
export function createFragment(config: BlueprintFragmentConfig): IBlueprintFragment {
|
||||
return new BlueprintFragment(config);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 片段资产格式 | Fragment Asset Format
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 蓝图片段资产格式
|
||||
* @en Blueprint fragment asset format
|
||||
*
|
||||
* @zh 用于序列化和反序列化片段
|
||||
* @en Used for serializing and deserializing fragments
|
||||
*/
|
||||
export interface BlueprintFragmentAsset {
|
||||
/**
|
||||
* @zh 格式版本
|
||||
* @en Format version
|
||||
*/
|
||||
version: number;
|
||||
|
||||
/**
|
||||
* @zh 资产类型标识
|
||||
* @en Asset type identifier
|
||||
*/
|
||||
type: 'blueprint-fragment';
|
||||
|
||||
/**
|
||||
* @zh 片段数据
|
||||
* @en Fragment data
|
||||
*/
|
||||
fragment: {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
tags?: string[];
|
||||
inputs: ExposedPin[];
|
||||
outputs: ExposedPin[];
|
||||
version?: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh 内部蓝图图
|
||||
* @en Internal blueprint graph
|
||||
*/
|
||||
graph: BlueprintAsset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从资产创建片段
|
||||
* @en Create fragment from asset
|
||||
*/
|
||||
export function fragmentFromAsset(asset: BlueprintFragmentAsset): IBlueprintFragment {
|
||||
return new BlueprintFragment({
|
||||
...asset.fragment,
|
||||
graph: asset.graph
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 将片段转为资产
|
||||
* @en Convert fragment to asset
|
||||
*/
|
||||
export function fragmentToAsset(fragment: IBlueprintFragment): BlueprintFragmentAsset {
|
||||
return {
|
||||
version: 1,
|
||||
type: 'blueprint-fragment',
|
||||
fragment: {
|
||||
id: fragment.id,
|
||||
name: fragment.name,
|
||||
description: fragment.description,
|
||||
category: fragment.category,
|
||||
tags: fragment.tags,
|
||||
inputs: fragment.inputs,
|
||||
outputs: fragment.outputs,
|
||||
version: fragment.version,
|
||||
icon: fragment.icon,
|
||||
color: fragment.color
|
||||
},
|
||||
graph: fragment.graph
|
||||
};
|
||||
}
|
||||
208
packages/framework/blueprint/src/composition/FragmentRegistry.ts
Normal file
208
packages/framework/blueprint/src/composition/FragmentRegistry.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* @zh 片段注册表
|
||||
* @en Fragment Registry
|
||||
*
|
||||
* @zh 管理和查询蓝图片段
|
||||
* @en Manages and queries blueprint fragments
|
||||
*/
|
||||
|
||||
import type { IBlueprintFragment } from './BlueprintFragment';
|
||||
|
||||
// =============================================================================
|
||||
// 片段注册表接口 | Fragment Registry Interface
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 片段过滤器
|
||||
* @en Fragment filter
|
||||
*/
|
||||
export interface FragmentFilter {
|
||||
/**
|
||||
* @zh 按分类过滤
|
||||
* @en Filter by category
|
||||
*/
|
||||
category?: string;
|
||||
|
||||
/**
|
||||
* @zh 按标签过滤(任意匹配)
|
||||
* @en Filter by tags (any match)
|
||||
*/
|
||||
tags?: string[];
|
||||
|
||||
/**
|
||||
* @zh 按名称搜索
|
||||
* @en Search by name
|
||||
*/
|
||||
search?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 片段注册表接口
|
||||
* @en Fragment registry interface
|
||||
*/
|
||||
export interface IFragmentRegistry {
|
||||
/**
|
||||
* @zh 注册片段
|
||||
* @en Register fragment
|
||||
*/
|
||||
register(fragment: IBlueprintFragment): void;
|
||||
|
||||
/**
|
||||
* @zh 注销片段
|
||||
* @en Unregister fragment
|
||||
*/
|
||||
unregister(id: string): void;
|
||||
|
||||
/**
|
||||
* @zh 获取片段
|
||||
* @en Get fragment
|
||||
*/
|
||||
get(id: string): IBlueprintFragment | undefined;
|
||||
|
||||
/**
|
||||
* @zh 检查片段是否存在
|
||||
* @en Check if fragment exists
|
||||
*/
|
||||
has(id: string): boolean;
|
||||
|
||||
/**
|
||||
* @zh 获取所有片段
|
||||
* @en Get all fragments
|
||||
*/
|
||||
getAll(): IBlueprintFragment[];
|
||||
|
||||
/**
|
||||
* @zh 按条件过滤片段
|
||||
* @en Filter fragments by criteria
|
||||
*/
|
||||
filter(filter: FragmentFilter): IBlueprintFragment[];
|
||||
|
||||
/**
|
||||
* @zh 获取所有分类
|
||||
* @en Get all categories
|
||||
*/
|
||||
getCategories(): string[];
|
||||
|
||||
/**
|
||||
* @zh 获取所有标签
|
||||
* @en Get all tags
|
||||
*/
|
||||
getTags(): string[];
|
||||
|
||||
/**
|
||||
* @zh 清空注册表
|
||||
* @en Clear registry
|
||||
*/
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 片段注册表实现 | Fragment Registry Implementation
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 片段注册表实现
|
||||
* @en Fragment registry implementation
|
||||
*/
|
||||
export class FragmentRegistry implements IFragmentRegistry {
|
||||
private fragments: Map<string, IBlueprintFragment> = new Map();
|
||||
|
||||
register(fragment: IBlueprintFragment): void {
|
||||
if (this.fragments.has(fragment.id)) {
|
||||
console.warn(`Fragment '${fragment.id}' already registered, overwriting`);
|
||||
}
|
||||
this.fragments.set(fragment.id, fragment);
|
||||
}
|
||||
|
||||
unregister(id: string): void {
|
||||
this.fragments.delete(id);
|
||||
}
|
||||
|
||||
get(id: string): IBlueprintFragment | undefined {
|
||||
return this.fragments.get(id);
|
||||
}
|
||||
|
||||
has(id: string): boolean {
|
||||
return this.fragments.has(id);
|
||||
}
|
||||
|
||||
getAll(): IBlueprintFragment[] {
|
||||
return Array.from(this.fragments.values());
|
||||
}
|
||||
|
||||
filter(filter: FragmentFilter): IBlueprintFragment[] {
|
||||
let results = this.getAll();
|
||||
|
||||
if (filter.category) {
|
||||
results = results.filter(f => f.category === filter.category);
|
||||
}
|
||||
|
||||
if (filter.tags && filter.tags.length > 0) {
|
||||
results = results.filter(f =>
|
||||
f.tags && filter.tags!.some(t => f.tags!.includes(t))
|
||||
);
|
||||
}
|
||||
|
||||
if (filter.search) {
|
||||
const searchLower = filter.search.toLowerCase();
|
||||
results = results.filter(f =>
|
||||
f.name.toLowerCase().includes(searchLower) ||
|
||||
f.description?.toLowerCase().includes(searchLower)
|
||||
);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
getCategories(): string[] {
|
||||
const categories = new Set<string>();
|
||||
for (const fragment of this.fragments.values()) {
|
||||
if (fragment.category) {
|
||||
categories.add(fragment.category);
|
||||
}
|
||||
}
|
||||
return Array.from(categories).sort();
|
||||
}
|
||||
|
||||
getTags(): string[] {
|
||||
const tags = new Set<string>();
|
||||
for (const fragment of this.fragments.values()) {
|
||||
if (fragment.tags) {
|
||||
for (const tag of fragment.tags) {
|
||||
tags.add(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(tags).sort();
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.fragments.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取片段数量
|
||||
* @en Get fragment count
|
||||
*/
|
||||
get size(): number {
|
||||
return this.fragments.size;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 单例实例 | Singleton Instance
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 默认片段注册表实例
|
||||
* @en Default fragment registry instance
|
||||
*/
|
||||
export const defaultFragmentRegistry = new FragmentRegistry();
|
||||
|
||||
/**
|
||||
* @zh 创建片段注册表
|
||||
* @en Create fragment registry
|
||||
*/
|
||||
export function createFragmentRegistry(): IFragmentRegistry {
|
||||
return new FragmentRegistry();
|
||||
}
|
||||
57
packages/framework/blueprint/src/composition/index.ts
Normal file
57
packages/framework/blueprint/src/composition/index.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @zh 蓝图组合系统导出
|
||||
* @en Blueprint Composition System Export
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// 片段 | Fragment
|
||||
// =============================================================================
|
||||
|
||||
export type {
|
||||
ExposedPin,
|
||||
IBlueprintFragment,
|
||||
BlueprintFragmentConfig,
|
||||
BlueprintFragmentAsset
|
||||
} from './BlueprintFragment';
|
||||
|
||||
export {
|
||||
BlueprintFragment,
|
||||
createExposedPin,
|
||||
createFragment,
|
||||
fragmentFromAsset,
|
||||
fragmentToAsset
|
||||
} from './BlueprintFragment';
|
||||
|
||||
// =============================================================================
|
||||
// 组合器 | Composer
|
||||
// =============================================================================
|
||||
|
||||
export type {
|
||||
FragmentSlot,
|
||||
SlotConnection,
|
||||
IBlueprintComposer,
|
||||
CompositionValidationResult,
|
||||
CompositionError,
|
||||
CompositionWarning,
|
||||
BlueprintCompositionAsset
|
||||
} from './BlueprintComposer';
|
||||
|
||||
export {
|
||||
BlueprintComposer,
|
||||
createComposer
|
||||
} from './BlueprintComposer';
|
||||
|
||||
// =============================================================================
|
||||
// 注册表 | Registry
|
||||
// =============================================================================
|
||||
|
||||
export type {
|
||||
FragmentFilter,
|
||||
IFragmentRegistry
|
||||
} from './FragmentRegistry';
|
||||
|
||||
export {
|
||||
FragmentRegistry,
|
||||
defaultFragmentRegistry,
|
||||
createFragmentRegistry
|
||||
} from './FragmentRegistry';
|
||||
67
packages/framework/blueprint/src/esengine/BlueprintPlugin.ts
Normal file
67
packages/framework/blueprint/src/esengine/BlueprintPlugin.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @zh ESEngine 蓝图插件
|
||||
* @en ESEngine Blueprint Plugin
|
||||
*
|
||||
* @zh 此文件包含与 ESEngine 引擎核心集成的代码。
|
||||
* 使用 Cocos/Laya 等其他引擎时不需要此文件。
|
||||
*
|
||||
* @en This file contains code for integrating with ESEngine engine-core.
|
||||
* Not needed when using other engines like Cocos/Laya.
|
||||
*/
|
||||
|
||||
import type { IRuntimePlugin, ModuleManifest, IRuntimeModule } from '@esengine/engine-core';
|
||||
|
||||
/**
|
||||
* @zh 蓝图运行时模块
|
||||
* @en Blueprint Runtime Module
|
||||
*
|
||||
* @zh 注意:蓝图使用自定义系统 (IBlueprintSystem) 而非 EntitySystem,
|
||||
* 因此这里不实现 createSystems。蓝图系统应使用 createBlueprintSystem(scene) 手动创建。
|
||||
*
|
||||
* @en Note: Blueprint uses a custom system (IBlueprintSystem) instead of EntitySystem,
|
||||
* so createSystems is not implemented here. Blueprint systems should be created
|
||||
* manually using createBlueprintSystem(scene).
|
||||
*/
|
||||
class BlueprintRuntimeModule implements IRuntimeModule {
|
||||
async onInitialize(): Promise<void> {
|
||||
// Blueprint system initialization
|
||||
}
|
||||
|
||||
onDestroy(): void {
|
||||
// Cleanup
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 蓝图的插件清单
|
||||
* @en Plugin manifest for Blueprint
|
||||
*/
|
||||
const manifest: ModuleManifest = {
|
||||
id: 'blueprint',
|
||||
name: '@esengine/blueprint',
|
||||
displayName: 'Blueprint',
|
||||
version: '1.0.0',
|
||||
description: '可视化脚本系统',
|
||||
category: 'AI',
|
||||
icon: 'Workflow',
|
||||
isCore: false,
|
||||
defaultEnabled: false,
|
||||
isEngineModule: true,
|
||||
dependencies: ['core'],
|
||||
exports: {
|
||||
components: ['BlueprintComponent'],
|
||||
systems: ['BlueprintSystem']
|
||||
},
|
||||
requiresWasm: false
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh 蓝图插件
|
||||
* @en Blueprint Plugin
|
||||
*/
|
||||
export const BlueprintPlugin: IRuntimePlugin = {
|
||||
manifest,
|
||||
runtimeModule: new BlueprintRuntimeModule()
|
||||
};
|
||||
|
||||
export { BlueprintRuntimeModule };
|
||||
37
packages/framework/blueprint/src/esengine/index.ts
Normal file
37
packages/framework/blueprint/src/esengine/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @zh ESEngine 集成入口
|
||||
* @en ESEngine integration entry point
|
||||
*
|
||||
* @zh 此模块包含与 ESEngine 引擎核心集成所需的所有代码。
|
||||
* 使用 Cocos/Laya 等其他引擎时,只需导入主模块即可。
|
||||
*
|
||||
* @en This module contains all code required for ESEngine engine-core integration.
|
||||
* When using other engines like Cocos/Laya, just import the main module.
|
||||
*
|
||||
* @example ESEngine 使用方式 / ESEngine usage:
|
||||
* ```typescript
|
||||
* import { BlueprintPlugin } from '@esengine/blueprint/esengine';
|
||||
*
|
||||
* // Register with ESEngine plugin system
|
||||
* engine.registerPlugin(BlueprintPlugin);
|
||||
* ```
|
||||
*
|
||||
* @example Cocos/Laya 使用方式 / Cocos/Laya usage:
|
||||
* ```typescript
|
||||
* import {
|
||||
* createBlueprintSystem,
|
||||
* createBlueprintComponentData
|
||||
* } from '@esengine/blueprint';
|
||||
*
|
||||
* // Create blueprint system for your scene
|
||||
* const blueprintSystem = createBlueprintSystem(scene);
|
||||
*
|
||||
* // Add to your game loop
|
||||
* function update(dt) {
|
||||
* blueprintSystem.process(blueprintEntities, dt);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Runtime module and plugin
|
||||
export { BlueprintPlugin, BlueprintRuntimeModule } from './BlueprintPlugin';
|
||||
67
packages/framework/blueprint/src/index.ts
Normal file
67
packages/framework/blueprint/src/index.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @esengine/blueprint - Visual scripting system for ECS Framework
|
||||
*
|
||||
* @zh 蓝图可视化脚本系统 - 可与任何 ECS 框架配合使用
|
||||
* @en Visual scripting system - works with any ECS framework
|
||||
*
|
||||
* @zh 此包是通用的可视化脚本实现,可以与任何 ECS 框架配合使用。
|
||||
* 对于 ESEngine 集成,请从 '@esengine/blueprint/esengine' 导入插件。
|
||||
*
|
||||
* @en This package is a generic visual scripting implementation that works with any ECS framework.
|
||||
* For ESEngine integration, import the plugin from '@esengine/blueprint/esengine'.
|
||||
*
|
||||
* @example Cocos/Laya/通用 ECS 使用方式:
|
||||
* ```typescript
|
||||
* import {
|
||||
* createBlueprintSystem,
|
||||
* createBlueprintComponentData
|
||||
* } from '@esengine/blueprint';
|
||||
*
|
||||
* // Create blueprint system for your scene
|
||||
* const blueprintSystem = createBlueprintSystem(scene);
|
||||
*
|
||||
* // Create component data
|
||||
* const componentData = createBlueprintComponentData();
|
||||
* componentData.blueprintAsset = loadedAsset;
|
||||
*
|
||||
* // Add to your game loop
|
||||
* function update(dt) {
|
||||
* blueprintSystem.process(blueprintEntities, dt);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
// Types
|
||||
export * from './types';
|
||||
|
||||
// Runtime
|
||||
export * from './runtime';
|
||||
|
||||
// Triggers
|
||||
export * from './triggers';
|
||||
|
||||
// Composition
|
||||
export * from './composition';
|
||||
|
||||
// Nodes (import to register)
|
||||
import './nodes';
|
||||
|
||||
// Re-export commonly used items
|
||||
export { NodeRegistry, RegisterNode } from './runtime/NodeRegistry';
|
||||
export { BlueprintVM } from './runtime/BlueprintVM';
|
||||
export {
|
||||
createBlueprintComponentData,
|
||||
initializeBlueprintVM,
|
||||
startBlueprint,
|
||||
stopBlueprint,
|
||||
tickBlueprint,
|
||||
cleanupBlueprint
|
||||
} from './runtime/BlueprintComponent';
|
||||
export {
|
||||
createBlueprintSystem,
|
||||
triggerBlueprintEvent,
|
||||
triggerCustomBlueprintEvent
|
||||
} from './runtime/BlueprintSystem';
|
||||
export { createEmptyBlueprint, validateBlueprintAsset } from './types/blueprint';
|
||||
91
packages/framework/blueprint/src/nodes/debug/Print.ts
Normal file
91
packages/framework/blueprint/src/nodes/debug/Print.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Print Node - Outputs a message for debugging
|
||||
* 打印节点 - 输出调试消息
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
||||
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
|
||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
||||
|
||||
/**
|
||||
* Print node template
|
||||
* Print 节点模板
|
||||
*/
|
||||
export const PrintTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Print',
|
||||
title: 'Print String',
|
||||
category: 'debug',
|
||||
color: '#785EF0',
|
||||
description: 'Prints a message to the console for debugging (打印消息到控制台用于调试)',
|
||||
keywords: ['log', 'debug', 'console', 'output', 'print'],
|
||||
inputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
type: 'exec',
|
||||
displayName: ''
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
type: 'string',
|
||||
displayName: 'Message',
|
||||
defaultValue: 'Hello Blueprint!'
|
||||
},
|
||||
{
|
||||
name: 'printToScreen',
|
||||
type: 'bool',
|
||||
displayName: 'Print to Screen',
|
||||
defaultValue: true
|
||||
},
|
||||
{
|
||||
name: 'duration',
|
||||
type: 'float',
|
||||
displayName: 'Duration',
|
||||
defaultValue: 2.0
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
type: 'exec',
|
||||
displayName: ''
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Print node executor
|
||||
* Print 节点执行器
|
||||
*/
|
||||
@RegisterNode(PrintTemplate)
|
||||
export class PrintExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const message = context.evaluateInput(node.id, 'message', 'Hello Blueprint!');
|
||||
const printToScreen = context.evaluateInput(node.id, 'printToScreen', true);
|
||||
const duration = context.evaluateInput(node.id, 'duration', 2.0);
|
||||
|
||||
// Console output
|
||||
// 控制台输出
|
||||
console.log(`[Blueprint] ${message}`);
|
||||
|
||||
// Screen output via event (handled by runtime)
|
||||
// 通过事件输出到屏幕(由运行时处理)
|
||||
if (printToScreen) {
|
||||
const event = new CustomEvent('blueprint:print', {
|
||||
detail: {
|
||||
message: String(message),
|
||||
duration: Number(duration),
|
||||
entityId: context.entity.id,
|
||||
entityName: context.entity.name
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nextExec: 'exec'
|
||||
};
|
||||
}
|
||||
}
|
||||
6
packages/framework/blueprint/src/nodes/debug/index.ts
Normal file
6
packages/framework/blueprint/src/nodes/debug/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Debug Nodes - Tools for debugging blueprints
|
||||
* 调试节点 - 蓝图调试工具
|
||||
*/
|
||||
|
||||
export * from './Print';
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Event Begin Play Node - Triggered when the blueprint starts
|
||||
* 开始播放事件节点 - 蓝图启动时触发
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
||||
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
|
||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
||||
|
||||
/**
|
||||
* EventBeginPlay node template
|
||||
* EventBeginPlay 节点模板
|
||||
*/
|
||||
export const EventBeginPlayTemplate: BlueprintNodeTemplate = {
|
||||
type: 'EventBeginPlay',
|
||||
title: 'Event Begin Play',
|
||||
category: 'event',
|
||||
color: '#CC0000',
|
||||
description: 'Triggered once when the blueprint starts executing (蓝图开始执行时触发一次)',
|
||||
keywords: ['start', 'begin', 'init', 'event'],
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
type: 'exec',
|
||||
displayName: ''
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* EventBeginPlay node executor
|
||||
* EventBeginPlay 节点执行器
|
||||
*/
|
||||
@RegisterNode(EventBeginPlayTemplate)
|
||||
export class EventBeginPlayExecutor implements INodeExecutor {
|
||||
execute(_node: BlueprintNode, _context: ExecutionContext): ExecutionResult {
|
||||
// Event nodes just trigger execution flow
|
||||
// 事件节点只触发执行流
|
||||
return {
|
||||
nextExec: 'exec'
|
||||
};
|
||||
}
|
||||
}
|
||||
118
packages/framework/blueprint/src/nodes/events/EventCollision.ts
Normal file
118
packages/framework/blueprint/src/nodes/events/EventCollision.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* @zh 碰撞事件节点 - 碰撞发生时触发
|
||||
* @en Event Collision Node - Triggered on collision events
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
||||
import { ExecutionResult } from '../../runtime/ExecutionContext';
|
||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
||||
|
||||
/**
|
||||
* @zh EventCollisionEnter 节点模板
|
||||
* @en EventCollisionEnter node template
|
||||
*/
|
||||
export const EventCollisionEnterTemplate: BlueprintNodeTemplate = {
|
||||
type: 'EventCollisionEnter',
|
||||
title: 'Event Collision Enter',
|
||||
category: 'event',
|
||||
color: '#CC0000',
|
||||
description: 'Triggered when collision starts / 碰撞开始时触发',
|
||||
keywords: ['collision', 'enter', 'hit', 'overlap', 'event'],
|
||||
menuPath: ['Event', 'Collision', 'Enter'],
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
type: 'exec',
|
||||
displayName: ''
|
||||
},
|
||||
{
|
||||
name: 'otherEntityId',
|
||||
type: 'string',
|
||||
displayName: 'Other Entity'
|
||||
},
|
||||
{
|
||||
name: 'pointX',
|
||||
type: 'float',
|
||||
displayName: 'Point X'
|
||||
},
|
||||
{
|
||||
name: 'pointY',
|
||||
type: 'float',
|
||||
displayName: 'Point Y'
|
||||
},
|
||||
{
|
||||
name: 'normalX',
|
||||
type: 'float',
|
||||
displayName: 'Normal X'
|
||||
},
|
||||
{
|
||||
name: 'normalY',
|
||||
type: 'float',
|
||||
displayName: 'Normal Y'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh EventCollisionEnter 节点执行器
|
||||
* @en EventCollisionEnter node executor
|
||||
*/
|
||||
@RegisterNode(EventCollisionEnterTemplate)
|
||||
export class EventCollisionEnterExecutor implements INodeExecutor {
|
||||
execute(_node: BlueprintNode): ExecutionResult {
|
||||
return {
|
||||
nextExec: 'exec',
|
||||
outputs: {
|
||||
otherEntityId: '',
|
||||
pointX: 0,
|
||||
pointY: 0,
|
||||
normalX: 0,
|
||||
normalY: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh EventCollisionExit 节点模板
|
||||
* @en EventCollisionExit node template
|
||||
*/
|
||||
export const EventCollisionExitTemplate: BlueprintNodeTemplate = {
|
||||
type: 'EventCollisionExit',
|
||||
title: 'Event Collision Exit',
|
||||
category: 'event',
|
||||
color: '#CC0000',
|
||||
description: 'Triggered when collision ends / 碰撞结束时触发',
|
||||
keywords: ['collision', 'exit', 'end', 'separate', 'event'],
|
||||
menuPath: ['Event', 'Collision', 'Exit'],
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
type: 'exec',
|
||||
displayName: ''
|
||||
},
|
||||
{
|
||||
name: 'otherEntityId',
|
||||
type: 'string',
|
||||
displayName: 'Other Entity'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh EventCollisionExit 节点执行器
|
||||
* @en EventCollisionExit node executor
|
||||
*/
|
||||
@RegisterNode(EventCollisionExitTemplate)
|
||||
export class EventCollisionExitExecutor implements INodeExecutor {
|
||||
execute(_node: BlueprintNode): ExecutionResult {
|
||||
return {
|
||||
nextExec: 'exec',
|
||||
outputs: {
|
||||
otherEntityId: ''
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Event End Play Node - Triggered when the blueprint stops
|
||||
* 结束播放事件节点 - 蓝图停止时触发
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
||||
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
|
||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
||||
|
||||
/**
|
||||
* EventEndPlay node template
|
||||
* EventEndPlay 节点模板
|
||||
*/
|
||||
export const EventEndPlayTemplate: BlueprintNodeTemplate = {
|
||||
type: 'EventEndPlay',
|
||||
title: 'Event End Play',
|
||||
category: 'event',
|
||||
color: '#CC0000',
|
||||
description: 'Triggered once when the blueprint stops executing (蓝图停止执行时触发一次)',
|
||||
keywords: ['stop', 'end', 'destroy', 'event'],
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
type: 'exec',
|
||||
displayName: ''
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* EventEndPlay node executor
|
||||
* EventEndPlay 节点执行器
|
||||
*/
|
||||
@RegisterNode(EventEndPlayTemplate)
|
||||
export class EventEndPlayExecutor implements INodeExecutor {
|
||||
execute(_node: BlueprintNode, _context: ExecutionContext): ExecutionResult {
|
||||
return {
|
||||
nextExec: 'exec'
|
||||
};
|
||||
}
|
||||
}
|
||||
79
packages/framework/blueprint/src/nodes/events/EventInput.ts
Normal file
79
packages/framework/blueprint/src/nodes/events/EventInput.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* @zh 输入事件节点 - 输入触发时触发
|
||||
* @en Event Input Node - Triggered on input events
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
||||
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
|
||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
||||
|
||||
/**
|
||||
* @zh EventInput 节点模板
|
||||
* @en EventInput node template
|
||||
*/
|
||||
export const EventInputTemplate: BlueprintNodeTemplate = {
|
||||
type: 'EventInput',
|
||||
title: 'Event Input',
|
||||
category: 'event',
|
||||
color: '#CC0000',
|
||||
description: 'Triggered when input action occurs / 输入动作发生时触发',
|
||||
keywords: ['input', 'key', 'button', 'action', 'event'],
|
||||
menuPath: ['Event', 'Input'],
|
||||
inputs: [
|
||||
{
|
||||
name: 'action',
|
||||
type: 'string',
|
||||
displayName: 'Action',
|
||||
defaultValue: ''
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
type: 'exec',
|
||||
displayName: ''
|
||||
},
|
||||
{
|
||||
name: 'action',
|
||||
type: 'string',
|
||||
displayName: 'Action'
|
||||
},
|
||||
{
|
||||
name: 'value',
|
||||
type: 'float',
|
||||
displayName: 'Value'
|
||||
},
|
||||
{
|
||||
name: 'pressed',
|
||||
type: 'bool',
|
||||
displayName: 'Pressed'
|
||||
},
|
||||
{
|
||||
name: 'released',
|
||||
type: 'bool',
|
||||
displayName: 'Released'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh EventInput 节点执行器
|
||||
* @en EventInput node executor
|
||||
*
|
||||
* @zh 注意:事件节点的输出由 VM 在触发时通过 setOutputs 设置
|
||||
* @en Note: Event node outputs are set by VM via setOutputs when triggered
|
||||
*/
|
||||
@RegisterNode(EventInputTemplate)
|
||||
export class EventInputExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, _context: ExecutionContext): ExecutionResult {
|
||||
return {
|
||||
nextExec: 'exec',
|
||||
outputs: {
|
||||
action: node.data?.action ?? '',
|
||||
value: 0,
|
||||
pressed: false,
|
||||
released: false
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @zh 消息事件节点 - 接收消息时触发
|
||||
* @en Event Message Node - Triggered when message is received
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
||||
import { ExecutionResult } from '../../runtime/ExecutionContext';
|
||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
||||
|
||||
/**
|
||||
* @zh EventMessage 节点模板
|
||||
* @en EventMessage node template
|
||||
*/
|
||||
export const EventMessageTemplate: BlueprintNodeTemplate = {
|
||||
type: 'EventMessage',
|
||||
title: 'Event Message',
|
||||
category: 'event',
|
||||
color: '#CC0000',
|
||||
description: 'Triggered when a message is received / 接收到消息时触发',
|
||||
keywords: ['message', 'receive', 'broadcast', 'event', 'signal'],
|
||||
menuPath: ['Event', 'Message'],
|
||||
inputs: [
|
||||
{
|
||||
name: 'messageName',
|
||||
type: 'string',
|
||||
displayName: 'Message Name',
|
||||
defaultValue: ''
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
type: 'exec',
|
||||
displayName: ''
|
||||
},
|
||||
{
|
||||
name: 'messageName',
|
||||
type: 'string',
|
||||
displayName: 'Message'
|
||||
},
|
||||
{
|
||||
name: 'senderId',
|
||||
type: 'string',
|
||||
displayName: 'Sender ID'
|
||||
},
|
||||
{
|
||||
name: 'payload',
|
||||
type: 'any',
|
||||
displayName: 'Payload'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh EventMessage 节点执行器
|
||||
* @en EventMessage node executor
|
||||
*/
|
||||
@RegisterNode(EventMessageTemplate)
|
||||
export class EventMessageExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode): ExecutionResult {
|
||||
return {
|
||||
nextExec: 'exec',
|
||||
outputs: {
|
||||
messageName: node.data?.messageName ?? '',
|
||||
senderId: '',
|
||||
payload: null
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
132
packages/framework/blueprint/src/nodes/events/EventState.ts
Normal file
132
packages/framework/blueprint/src/nodes/events/EventState.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* @zh 状态事件节点 - 状态机状态变化时触发
|
||||
* @en Event State Node - Triggered on state machine state changes
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
||||
import { ExecutionResult } from '../../runtime/ExecutionContext';
|
||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
||||
|
||||
/**
|
||||
* @zh EventStateEnter 节点模板
|
||||
* @en EventStateEnter node template
|
||||
*/
|
||||
export const EventStateEnterTemplate: BlueprintNodeTemplate = {
|
||||
type: 'EventStateEnter',
|
||||
title: 'Event State Enter',
|
||||
category: 'event',
|
||||
color: '#CC0000',
|
||||
description: 'Triggered when entering a state / 进入状态时触发',
|
||||
keywords: ['state', 'enter', 'fsm', 'machine', 'event'],
|
||||
menuPath: ['Event', 'State', 'Enter'],
|
||||
inputs: [
|
||||
{
|
||||
name: 'stateName',
|
||||
type: 'string',
|
||||
displayName: 'State Name',
|
||||
defaultValue: ''
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
type: 'exec',
|
||||
displayName: ''
|
||||
},
|
||||
{
|
||||
name: 'stateMachineId',
|
||||
type: 'string',
|
||||
displayName: 'State Machine'
|
||||
},
|
||||
{
|
||||
name: 'currentState',
|
||||
type: 'string',
|
||||
displayName: 'Current State'
|
||||
},
|
||||
{
|
||||
name: 'previousState',
|
||||
type: 'string',
|
||||
displayName: 'Previous State'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh EventStateEnter 节点执行器
|
||||
* @en EventStateEnter node executor
|
||||
*/
|
||||
@RegisterNode(EventStateEnterTemplate)
|
||||
export class EventStateEnterExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode): ExecutionResult {
|
||||
return {
|
||||
nextExec: 'exec',
|
||||
outputs: {
|
||||
stateMachineId: '',
|
||||
currentState: node.data?.stateName ?? '',
|
||||
previousState: ''
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh EventStateExit 节点模板
|
||||
* @en EventStateExit node template
|
||||
*/
|
||||
export const EventStateExitTemplate: BlueprintNodeTemplate = {
|
||||
type: 'EventStateExit',
|
||||
title: 'Event State Exit',
|
||||
category: 'event',
|
||||
color: '#CC0000',
|
||||
description: 'Triggered when exiting a state / 退出状态时触发',
|
||||
keywords: ['state', 'exit', 'leave', 'fsm', 'machine', 'event'],
|
||||
menuPath: ['Event', 'State', 'Exit'],
|
||||
inputs: [
|
||||
{
|
||||
name: 'stateName',
|
||||
type: 'string',
|
||||
displayName: 'State Name',
|
||||
defaultValue: ''
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
type: 'exec',
|
||||
displayName: ''
|
||||
},
|
||||
{
|
||||
name: 'stateMachineId',
|
||||
type: 'string',
|
||||
displayName: 'State Machine'
|
||||
},
|
||||
{
|
||||
name: 'currentState',
|
||||
type: 'string',
|
||||
displayName: 'Current State'
|
||||
},
|
||||
{
|
||||
name: 'previousState',
|
||||
type: 'string',
|
||||
displayName: 'Previous State'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh EventStateExit 节点执行器
|
||||
* @en EventStateExit node executor
|
||||
*/
|
||||
@RegisterNode(EventStateExitTemplate)
|
||||
export class EventStateExitExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode): ExecutionResult {
|
||||
return {
|
||||
nextExec: 'exec',
|
||||
outputs: {
|
||||
stateMachineId: '',
|
||||
currentState: '',
|
||||
previousState: node.data?.stateName ?? ''
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
50
packages/framework/blueprint/src/nodes/events/EventTick.ts
Normal file
50
packages/framework/blueprint/src/nodes/events/EventTick.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Event Tick Node - Triggered every frame
|
||||
* 每帧事件节点 - 每帧触发
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
||||
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
|
||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
||||
|
||||
/**
|
||||
* EventTick node template
|
||||
* EventTick 节点模板
|
||||
*/
|
||||
export const EventTickTemplate: BlueprintNodeTemplate = {
|
||||
type: 'EventTick',
|
||||
title: 'Event Tick',
|
||||
category: 'event',
|
||||
color: '#CC0000',
|
||||
description: 'Triggered every frame during execution (执行期间每帧触发)',
|
||||
keywords: ['update', 'frame', 'tick', 'event'],
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
type: 'exec',
|
||||
displayName: ''
|
||||
},
|
||||
{
|
||||
name: 'deltaTime',
|
||||
type: 'float',
|
||||
displayName: 'Delta Seconds'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* EventTick node executor
|
||||
* EventTick 节点执行器
|
||||
*/
|
||||
@RegisterNode(EventTickTemplate)
|
||||
export class EventTickExecutor implements INodeExecutor {
|
||||
execute(_node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
return {
|
||||
nextExec: 'exec',
|
||||
outputs: {
|
||||
deltaTime: context.deltaTime
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
70
packages/framework/blueprint/src/nodes/events/EventTimer.ts
Normal file
70
packages/framework/blueprint/src/nodes/events/EventTimer.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @zh 定时器事件节点 - 定时器触发时调用
|
||||
* @en Event Timer Node - Triggered when timer fires
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
||||
import { ExecutionResult } from '../../runtime/ExecutionContext';
|
||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
||||
|
||||
/**
|
||||
* @zh EventTimer 节点模板
|
||||
* @en EventTimer node template
|
||||
*/
|
||||
export const EventTimerTemplate: BlueprintNodeTemplate = {
|
||||
type: 'EventTimer',
|
||||
title: 'Event Timer',
|
||||
category: 'event',
|
||||
color: '#CC0000',
|
||||
description: 'Triggered when a timer fires / 定时器触发时执行',
|
||||
keywords: ['timer', 'delay', 'schedule', 'event', 'interval'],
|
||||
menuPath: ['Event', 'Timer'],
|
||||
inputs: [
|
||||
{
|
||||
name: 'timerId',
|
||||
type: 'string',
|
||||
displayName: 'Timer ID',
|
||||
defaultValue: ''
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
type: 'exec',
|
||||
displayName: ''
|
||||
},
|
||||
{
|
||||
name: 'timerId',
|
||||
type: 'string',
|
||||
displayName: 'Timer ID'
|
||||
},
|
||||
{
|
||||
name: 'isRepeating',
|
||||
type: 'bool',
|
||||
displayName: 'Is Repeating'
|
||||
},
|
||||
{
|
||||
name: 'timesFired',
|
||||
type: 'int',
|
||||
displayName: 'Times Fired'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh EventTimer 节点执行器
|
||||
* @en EventTimer node executor
|
||||
*/
|
||||
@RegisterNode(EventTimerTemplate)
|
||||
export class EventTimerExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode): ExecutionResult {
|
||||
return {
|
||||
nextExec: 'exec',
|
||||
outputs: {
|
||||
timerId: node.data?.timerId ?? '',
|
||||
isRepeating: false,
|
||||
timesFired: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
16
packages/framework/blueprint/src/nodes/events/index.ts
Normal file
16
packages/framework/blueprint/src/nodes/events/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @zh 事件节点 - 蓝图执行的入口点
|
||||
* @en Event Nodes - Entry points for blueprint execution
|
||||
*/
|
||||
|
||||
// 生命周期事件 | Lifecycle events
|
||||
export * from './EventBeginPlay';
|
||||
export * from './EventTick';
|
||||
export * from './EventEndPlay';
|
||||
|
||||
// 触发器事件 | Trigger events
|
||||
export * from './EventInput';
|
||||
export * from './EventCollision';
|
||||
export * from './EventMessage';
|
||||
export * from './EventTimer';
|
||||
export * from './EventState';
|
||||
11
packages/framework/blueprint/src/nodes/index.ts
Normal file
11
packages/framework/blueprint/src/nodes/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Blueprint Nodes - All node definitions and executors
|
||||
* 蓝图节点 - 所有节点定义和执行器
|
||||
*/
|
||||
|
||||
// Import all nodes to trigger registration
|
||||
// 导入所有节点以触发注册
|
||||
export * from './events';
|
||||
export * from './debug';
|
||||
export * from './time';
|
||||
export * from './math';
|
||||
122
packages/framework/blueprint/src/nodes/math/MathOperations.ts
Normal file
122
packages/framework/blueprint/src/nodes/math/MathOperations.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Math Operation Nodes - Basic arithmetic operations
|
||||
* 数学运算节点 - 基础算术运算
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
||||
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
|
||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
||||
|
||||
// Add Node (加法节点)
|
||||
export const AddTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Add',
|
||||
title: 'Add',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Adds two numbers together (将两个数字相加)',
|
||||
keywords: ['add', 'plus', 'sum', '+', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'float', displayName: 'A', defaultValue: 0 },
|
||||
{ name: 'b', type: 'float', displayName: 'B', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(AddTemplate)
|
||||
export class AddExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const a = Number(context.evaluateInput(node.id, 'a', 0));
|
||||
const b = Number(context.evaluateInput(node.id, 'b', 0));
|
||||
return { outputs: { result: a + b } };
|
||||
}
|
||||
}
|
||||
|
||||
// Subtract Node (减法节点)
|
||||
export const SubtractTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Subtract',
|
||||
title: 'Subtract',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Subtracts B from A (从 A 减去 B)',
|
||||
keywords: ['subtract', 'minus', '-', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'float', displayName: 'A', defaultValue: 0 },
|
||||
{ name: 'b', type: 'float', displayName: 'B', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(SubtractTemplate)
|
||||
export class SubtractExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const a = Number(context.evaluateInput(node.id, 'a', 0));
|
||||
const b = Number(context.evaluateInput(node.id, 'b', 0));
|
||||
return { outputs: { result: a - b } };
|
||||
}
|
||||
}
|
||||
|
||||
// Multiply Node (乘法节点)
|
||||
export const MultiplyTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Multiply',
|
||||
title: 'Multiply',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Multiplies two numbers (将两个数字相乘)',
|
||||
keywords: ['multiply', 'times', '*', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'float', displayName: 'A', defaultValue: 0 },
|
||||
{ name: 'b', type: 'float', displayName: 'B', defaultValue: 1 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(MultiplyTemplate)
|
||||
export class MultiplyExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const a = Number(context.evaluateInput(node.id, 'a', 0));
|
||||
const b = Number(context.evaluateInput(node.id, 'b', 1));
|
||||
return { outputs: { result: a * b } };
|
||||
}
|
||||
}
|
||||
|
||||
// Divide Node (除法节点)
|
||||
export const DivideTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Divide',
|
||||
title: 'Divide',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Divides A by B (A 除以 B)',
|
||||
keywords: ['divide', '/', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'float', displayName: 'A', defaultValue: 0 },
|
||||
{ name: 'b', type: 'float', displayName: 'B', defaultValue: 1 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(DivideTemplate)
|
||||
export class DivideExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const a = Number(context.evaluateInput(node.id, 'a', 0));
|
||||
const b = Number(context.evaluateInput(node.id, 'b', 1));
|
||||
|
||||
// Prevent division by zero (防止除零)
|
||||
if (b === 0) {
|
||||
return { outputs: { result: 0 } };
|
||||
}
|
||||
|
||||
return { outputs: { result: a / b } };
|
||||
}
|
||||
}
|
||||
6
packages/framework/blueprint/src/nodes/math/index.ts
Normal file
6
packages/framework/blueprint/src/nodes/math/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Math Nodes - Mathematical operation nodes
|
||||
* 数学节点 - 数学运算节点
|
||||
*/
|
||||
|
||||
export * from './MathOperations';
|
||||
57
packages/framework/blueprint/src/nodes/time/Delay.ts
Normal file
57
packages/framework/blueprint/src/nodes/time/Delay.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Delay Node - Pauses execution for a specified duration
|
||||
* 延迟节点 - 暂停执行指定的时长
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
||||
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
|
||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
||||
|
||||
/**
|
||||
* Delay node template
|
||||
* Delay 节点模板
|
||||
*/
|
||||
export const DelayTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Delay',
|
||||
title: 'Delay',
|
||||
category: 'flow',
|
||||
color: '#FFFFFF',
|
||||
description: 'Pauses execution for a specified number of seconds (暂停执行指定的秒数)',
|
||||
keywords: ['wait', 'delay', 'pause', 'sleep', 'timer'],
|
||||
inputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
type: 'exec',
|
||||
displayName: ''
|
||||
},
|
||||
{
|
||||
name: 'duration',
|
||||
type: 'float',
|
||||
displayName: 'Duration',
|
||||
defaultValue: 1.0
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
type: 'exec',
|
||||
displayName: 'Completed'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Delay node executor
|
||||
* Delay 节点执行器
|
||||
*/
|
||||
@RegisterNode(DelayTemplate)
|
||||
export class DelayExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const duration = context.evaluateInput(node.id, 'duration', 1.0) as number;
|
||||
|
||||
return {
|
||||
nextExec: 'exec',
|
||||
delay: duration
|
||||
};
|
||||
}
|
||||
}
|
||||
45
packages/framework/blueprint/src/nodes/time/GetDeltaTime.ts
Normal file
45
packages/framework/blueprint/src/nodes/time/GetDeltaTime.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Get Delta Time Node - Returns the time since last frame
|
||||
* 获取增量时间节点 - 返回上一帧以来的时间
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
||||
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
|
||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
||||
|
||||
/**
|
||||
* GetDeltaTime node template
|
||||
* GetDeltaTime 节点模板
|
||||
*/
|
||||
export const GetDeltaTimeTemplate: BlueprintNodeTemplate = {
|
||||
type: 'GetDeltaTime',
|
||||
title: 'Get Delta Time',
|
||||
category: 'time',
|
||||
color: '#4FC3F7',
|
||||
description: 'Returns the time elapsed since the last frame in seconds (返回上一帧以来经过的时间,单位秒)',
|
||||
keywords: ['delta', 'time', 'frame', 'dt'],
|
||||
isPure: true,
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: 'deltaTime',
|
||||
type: 'float',
|
||||
displayName: 'Delta Seconds'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* GetDeltaTime node executor
|
||||
* GetDeltaTime 节点执行器
|
||||
*/
|
||||
@RegisterNode(GetDeltaTimeTemplate)
|
||||
export class GetDeltaTimeExecutor implements INodeExecutor {
|
||||
execute(_node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
return {
|
||||
outputs: {
|
||||
deltaTime: context.deltaTime
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
45
packages/framework/blueprint/src/nodes/time/GetTime.ts
Normal file
45
packages/framework/blueprint/src/nodes/time/GetTime.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Get Time Node - Returns the total time since blueprint started
|
||||
* 获取时间节点 - 返回蓝图启动以来的总时间
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
||||
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
|
||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
||||
|
||||
/**
|
||||
* GetTime node template
|
||||
* GetTime 节点模板
|
||||
*/
|
||||
export const GetTimeTemplate: BlueprintNodeTemplate = {
|
||||
type: 'GetTime',
|
||||
title: 'Get Game Time',
|
||||
category: 'time',
|
||||
color: '#4FC3F7',
|
||||
description: 'Returns the total time since the blueprint started in seconds (返回蓝图启动以来的总时间,单位秒)',
|
||||
keywords: ['time', 'total', 'elapsed', 'game'],
|
||||
isPure: true,
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: 'time',
|
||||
type: 'float',
|
||||
displayName: 'Seconds'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* GetTime node executor
|
||||
* GetTime 节点执行器
|
||||
*/
|
||||
@RegisterNode(GetTimeTemplate)
|
||||
export class GetTimeExecutor implements INodeExecutor {
|
||||
execute(_node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
return {
|
||||
outputs: {
|
||||
time: context.time
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
8
packages/framework/blueprint/src/nodes/time/index.ts
Normal file
8
packages/framework/blueprint/src/nodes/time/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Time Nodes - Time-related utility nodes
|
||||
* 时间节点 - 时间相关的工具节点
|
||||
*/
|
||||
|
||||
export * from './GetDeltaTime';
|
||||
export * from './GetTime';
|
||||
export * from './Delay';
|
||||
116
packages/framework/blueprint/src/runtime/BlueprintComponent.ts
Normal file
116
packages/framework/blueprint/src/runtime/BlueprintComponent.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Blueprint Component - Attaches a blueprint to an entity
|
||||
* 蓝图组件 - 将蓝图附加到实体
|
||||
*/
|
||||
|
||||
import type { Entity, IScene } from '@esengine/ecs-framework';
|
||||
import { BlueprintAsset } from '../types/blueprint';
|
||||
import { BlueprintVM } from './BlueprintVM';
|
||||
|
||||
/**
|
||||
* Component interface for ECS integration
|
||||
* 用于 ECS 集成的组件接口
|
||||
*/
|
||||
export interface IBlueprintComponent {
|
||||
/** Entity ID this component belongs to (此组件所属的实体ID) */
|
||||
entityId: number | null;
|
||||
|
||||
/** Blueprint asset reference (蓝图资产引用) */
|
||||
blueprintAsset: BlueprintAsset | null;
|
||||
|
||||
/** Blueprint asset path for serialization (用于序列化的蓝图资产路径) */
|
||||
blueprintPath: string;
|
||||
|
||||
/** Auto-start execution when entity is created (实体创建时自动开始执行) */
|
||||
autoStart: boolean;
|
||||
|
||||
/** Enable debug mode for VM (启用 VM 调试模式) */
|
||||
debug: boolean;
|
||||
|
||||
/** Runtime VM instance (运行时 VM 实例) */
|
||||
vm: BlueprintVM | null;
|
||||
|
||||
/** Whether the blueprint has started (蓝图是否已启动) */
|
||||
isStarted: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a blueprint component data object
|
||||
* 创建蓝图组件数据对象
|
||||
*/
|
||||
export function createBlueprintComponentData(): IBlueprintComponent {
|
||||
return {
|
||||
entityId: null,
|
||||
blueprintAsset: null,
|
||||
blueprintPath: '',
|
||||
autoStart: true,
|
||||
debug: false,
|
||||
vm: null,
|
||||
isStarted: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the VM for a blueprint component
|
||||
* 为蓝图组件初始化 VM
|
||||
*/
|
||||
export function initializeBlueprintVM(
|
||||
component: IBlueprintComponent,
|
||||
entity: Entity,
|
||||
scene: IScene
|
||||
): void {
|
||||
if (!component.blueprintAsset) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create VM instance
|
||||
// 创建 VM 实例
|
||||
component.vm = new BlueprintVM(component.blueprintAsset, entity, scene);
|
||||
component.vm.debug = component.debug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start blueprint execution
|
||||
* 开始蓝图执行
|
||||
*/
|
||||
export function startBlueprint(component: IBlueprintComponent): void {
|
||||
if (component.vm && !component.isStarted) {
|
||||
component.vm.start();
|
||||
component.isStarted = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop blueprint execution
|
||||
* 停止蓝图执行
|
||||
*/
|
||||
export function stopBlueprint(component: IBlueprintComponent): void {
|
||||
if (component.vm && component.isStarted) {
|
||||
component.vm.stop();
|
||||
component.isStarted = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update blueprint execution
|
||||
* 更新蓝图执行
|
||||
*/
|
||||
export function tickBlueprint(component: IBlueprintComponent, deltaTime: number): void {
|
||||
if (component.vm && component.isStarted) {
|
||||
component.vm.tick(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up blueprint resources
|
||||
* 清理蓝图资源
|
||||
*/
|
||||
export function cleanupBlueprint(component: IBlueprintComponent): void {
|
||||
if (component.vm) {
|
||||
if (component.isStarted) {
|
||||
component.vm.stop();
|
||||
}
|
||||
component.vm = null;
|
||||
component.isStarted = false;
|
||||
}
|
||||
}
|
||||
121
packages/framework/blueprint/src/runtime/BlueprintSystem.ts
Normal file
121
packages/framework/blueprint/src/runtime/BlueprintSystem.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Blueprint Execution System - Manages blueprint lifecycle and execution
|
||||
* 蓝图执行系统 - 管理蓝图生命周期和执行
|
||||
*/
|
||||
|
||||
import type { Entity, IScene } from '@esengine/ecs-framework';
|
||||
import {
|
||||
IBlueprintComponent,
|
||||
initializeBlueprintVM,
|
||||
startBlueprint,
|
||||
tickBlueprint,
|
||||
cleanupBlueprint
|
||||
} from './BlueprintComponent';
|
||||
|
||||
/**
|
||||
* Blueprint system interface for engine integration
|
||||
* 用于引擎集成的蓝图系统接口
|
||||
*/
|
||||
export interface IBlueprintSystem {
|
||||
/** Process entities with blueprint components (处理带有蓝图组件的实体) */
|
||||
process(entities: IBlueprintEntity[], deltaTime: number): void;
|
||||
|
||||
/** Called when entity is added to system (实体添加到系统时调用) */
|
||||
onEntityAdded(entity: IBlueprintEntity): void;
|
||||
|
||||
/** Called when entity is removed from system (实体从系统移除时调用) */
|
||||
onEntityRemoved(entity: IBlueprintEntity): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity with blueprint component
|
||||
* 带有蓝图组件的实体
|
||||
*/
|
||||
export interface IBlueprintEntity extends Entity {
|
||||
/** Blueprint component data (蓝图组件数据) */
|
||||
blueprintComponent: IBlueprintComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a blueprint execution system
|
||||
* 创建蓝图执行系统
|
||||
*/
|
||||
export function createBlueprintSystem(scene: IScene): IBlueprintSystem {
|
||||
return {
|
||||
process(entities: IBlueprintEntity[], deltaTime: number): void {
|
||||
for (const entity of entities) {
|
||||
const component = entity.blueprintComponent;
|
||||
|
||||
// Skip if no blueprint asset loaded
|
||||
// 如果没有加载蓝图资产则跳过
|
||||
if (!component.blueprintAsset) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Initialize VM if needed
|
||||
// 如果需要则初始化 VM
|
||||
if (!component.vm) {
|
||||
initializeBlueprintVM(component, entity, scene);
|
||||
}
|
||||
|
||||
// Auto-start if enabled
|
||||
// 如果启用则自动启动
|
||||
if (component.autoStart && !component.isStarted) {
|
||||
startBlueprint(component);
|
||||
}
|
||||
|
||||
// Tick the blueprint
|
||||
// 更新蓝图
|
||||
tickBlueprint(component, deltaTime);
|
||||
}
|
||||
},
|
||||
|
||||
onEntityAdded(entity: IBlueprintEntity): void {
|
||||
const component = entity.blueprintComponent;
|
||||
|
||||
if (component.blueprintAsset) {
|
||||
initializeBlueprintVM(component, entity, scene);
|
||||
|
||||
if (component.autoStart) {
|
||||
startBlueprint(component);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onEntityRemoved(entity: IBlueprintEntity): void {
|
||||
cleanupBlueprint(entity.blueprintComponent);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to manually trigger blueprint events
|
||||
* 手动触发蓝图事件的工具
|
||||
*/
|
||||
export function triggerBlueprintEvent(
|
||||
entity: IBlueprintEntity,
|
||||
eventType: string,
|
||||
data?: Record<string, unknown>
|
||||
): void {
|
||||
const vm = entity.blueprintComponent.vm;
|
||||
|
||||
if (vm && entity.blueprintComponent.isStarted) {
|
||||
vm.triggerEvent(eventType, data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to trigger custom events by name
|
||||
* 按名称触发自定义事件的工具
|
||||
*/
|
||||
export function triggerCustomBlueprintEvent(
|
||||
entity: IBlueprintEntity,
|
||||
eventName: string,
|
||||
data?: Record<string, unknown>
|
||||
): void {
|
||||
const vm = entity.blueprintComponent.vm;
|
||||
|
||||
if (vm && entity.blueprintComponent.isStarted) {
|
||||
vm.triggerCustomEvent(eventName, data);
|
||||
}
|
||||
}
|
||||
336
packages/framework/blueprint/src/runtime/BlueprintVM.ts
Normal file
336
packages/framework/blueprint/src/runtime/BlueprintVM.ts
Normal file
@@ -0,0 +1,336 @@
|
||||
/**
|
||||
* Blueprint Virtual Machine - Executes blueprint graphs
|
||||
* 蓝图虚拟机 - 执行蓝图图
|
||||
*/
|
||||
|
||||
import type { Entity, IScene } from '@esengine/ecs-framework';
|
||||
import { BlueprintNode } from '../types/nodes';
|
||||
import { BlueprintAsset } from '../types/blueprint';
|
||||
import { ExecutionContext, ExecutionResult } from './ExecutionContext';
|
||||
import { NodeRegistry } from './NodeRegistry';
|
||||
|
||||
/**
|
||||
* Pending execution frame (for delayed/async execution)
|
||||
* 待处理的执行帧(用于延迟/异步执行)
|
||||
*/
|
||||
interface PendingExecution {
|
||||
nodeId: string;
|
||||
execPin: string;
|
||||
resumeTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event trigger types
|
||||
* 事件触发类型
|
||||
*/
|
||||
export type EventType =
|
||||
| 'BeginPlay'
|
||||
| 'Tick'
|
||||
| 'EndPlay'
|
||||
| 'Collision'
|
||||
| 'TriggerEnter'
|
||||
| 'TriggerExit'
|
||||
| 'Custom';
|
||||
|
||||
/**
|
||||
* Blueprint Virtual Machine
|
||||
* 蓝图虚拟机
|
||||
*/
|
||||
export class BlueprintVM {
|
||||
/** Execution context (执行上下文) */
|
||||
private _context: ExecutionContext;
|
||||
|
||||
/** Pending executions (delayed nodes) (待处理的执行) */
|
||||
private _pendingExecutions: PendingExecution[] = [];
|
||||
|
||||
/** Event node cache by type (按类型缓存的事件节点) */
|
||||
private _eventNodes: Map<string, BlueprintNode[]> = new Map();
|
||||
|
||||
/** Whether the VM is running (VM 是否运行中) */
|
||||
private _isRunning: boolean = false;
|
||||
|
||||
/** Current execution time (当前执行时间) */
|
||||
private _currentTime: number = 0;
|
||||
|
||||
/** Maximum execution steps per frame (每帧最大执行步骤) */
|
||||
private _maxStepsPerFrame: number = 1000;
|
||||
|
||||
/** Debug mode (调试模式) */
|
||||
debug: boolean = false;
|
||||
|
||||
constructor(blueprint: BlueprintAsset, entity: Entity, scene: IScene) {
|
||||
this._context = new ExecutionContext(blueprint, entity, scene);
|
||||
this._cacheEventNodes();
|
||||
}
|
||||
|
||||
get context(): ExecutionContext {
|
||||
return this._context;
|
||||
}
|
||||
|
||||
get isRunning(): boolean {
|
||||
return this._isRunning;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache event nodes by type for quick lookup
|
||||
* 按类型缓存事件节点以便快速查找
|
||||
*/
|
||||
private _cacheEventNodes(): void {
|
||||
for (const node of this._context.blueprint.nodes) {
|
||||
// Event nodes start with "Event"
|
||||
// 事件节点以 "Event" 开头
|
||||
if (node.type.startsWith('Event')) {
|
||||
const eventType = node.type;
|
||||
if (!this._eventNodes.has(eventType)) {
|
||||
this._eventNodes.set(eventType, []);
|
||||
}
|
||||
this._eventNodes.get(eventType)!.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the VM
|
||||
* 启动 VM
|
||||
*/
|
||||
start(): void {
|
||||
this._isRunning = true;
|
||||
this._currentTime = 0;
|
||||
|
||||
// Trigger BeginPlay event
|
||||
// 触发 BeginPlay 事件
|
||||
this.triggerEvent('EventBeginPlay');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the VM
|
||||
* 停止 VM
|
||||
*/
|
||||
stop(): void {
|
||||
// Trigger EndPlay event
|
||||
// 触发 EndPlay 事件
|
||||
this.triggerEvent('EventEndPlay');
|
||||
|
||||
this._isRunning = false;
|
||||
this._pendingExecutions = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause the VM
|
||||
* 暂停 VM
|
||||
*/
|
||||
pause(): void {
|
||||
this._isRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume the VM
|
||||
* 恢复 VM
|
||||
*/
|
||||
resume(): void {
|
||||
this._isRunning = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the VM (called every frame)
|
||||
* 更新 VM(每帧调用)
|
||||
*/
|
||||
tick(deltaTime: number): void {
|
||||
if (!this._isRunning) return;
|
||||
|
||||
this._currentTime += deltaTime;
|
||||
this._context.deltaTime = deltaTime;
|
||||
this._context.time = this._currentTime;
|
||||
|
||||
// Process pending delayed executions
|
||||
// 处理待处理的延迟执行
|
||||
this._processPendingExecutions();
|
||||
|
||||
// Trigger Tick event
|
||||
// 触发 Tick 事件
|
||||
this.triggerEvent('EventTick');
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger an event by type
|
||||
* 按类型触发事件
|
||||
*/
|
||||
triggerEvent(eventType: string, data?: Record<string, unknown>): void {
|
||||
const eventNodes = this._eventNodes.get(eventType);
|
||||
if (!eventNodes) return;
|
||||
|
||||
for (const node of eventNodes) {
|
||||
this._executeFromNode(node, 'exec', data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a custom event by name
|
||||
* 按名称触发自定义事件
|
||||
*/
|
||||
triggerCustomEvent(eventName: string, data?: Record<string, unknown>): void {
|
||||
const eventNodes = this._eventNodes.get('EventCustom');
|
||||
if (!eventNodes) return;
|
||||
|
||||
for (const node of eventNodes) {
|
||||
if (node.data.eventName === eventName) {
|
||||
this._executeFromNode(node, 'exec', data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute from a starting node
|
||||
* 从起始节点执行
|
||||
*/
|
||||
private _executeFromNode(
|
||||
startNode: BlueprintNode,
|
||||
startPin: string,
|
||||
eventData?: Record<string, unknown>
|
||||
): void {
|
||||
// Clear output cache for new execution
|
||||
// 为新执行清除输出缓存
|
||||
this._context.clearOutputCache();
|
||||
|
||||
// Set event data as node outputs
|
||||
// 设置事件数据为节点输出
|
||||
if (eventData) {
|
||||
this._context.setOutputs(startNode.id, eventData);
|
||||
}
|
||||
|
||||
// Follow execution chain
|
||||
// 跟随执行链
|
||||
let currentNodeId: string | null = startNode.id;
|
||||
let currentPin: string = startPin;
|
||||
let steps = 0;
|
||||
|
||||
while (currentNodeId && steps < this._maxStepsPerFrame) {
|
||||
steps++;
|
||||
|
||||
// Get connected nodes from current exec pin
|
||||
// 从当前执行引脚获取连接的节点
|
||||
const connections = this._context.getConnectionsFromPin(currentNodeId, currentPin);
|
||||
|
||||
if (connections.length === 0) {
|
||||
// No more connections, end execution
|
||||
// 没有更多连接,结束执行
|
||||
break;
|
||||
}
|
||||
|
||||
// Execute connected node
|
||||
// 执行连接的节点
|
||||
const nextConn = connections[0];
|
||||
const result = this._executeNode(nextConn.toNodeId);
|
||||
|
||||
if (result.error) {
|
||||
console.error(`Blueprint error in node ${nextConn.toNodeId}: ${result.error}`);
|
||||
break;
|
||||
}
|
||||
|
||||
if (result.delay && result.delay > 0) {
|
||||
// Schedule delayed execution
|
||||
// 安排延迟执行
|
||||
this._pendingExecutions.push({
|
||||
nodeId: nextConn.toNodeId,
|
||||
execPin: result.nextExec ?? 'exec',
|
||||
resumeTime: this._currentTime + result.delay
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (result.yield) {
|
||||
// Yield execution until next frame
|
||||
// 暂停执行直到下一帧
|
||||
break;
|
||||
}
|
||||
|
||||
if (result.nextExec === null) {
|
||||
// Explicitly stop execution
|
||||
// 显式停止执行
|
||||
break;
|
||||
}
|
||||
|
||||
// Continue to next node
|
||||
// 继续到下一个节点
|
||||
currentNodeId = nextConn.toNodeId;
|
||||
currentPin = result.nextExec ?? 'exec';
|
||||
}
|
||||
|
||||
if (steps >= this._maxStepsPerFrame) {
|
||||
console.warn('Blueprint execution exceeded maximum steps, possible infinite loop');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a single node
|
||||
* 执行单个节点
|
||||
*/
|
||||
private _executeNode(nodeId: string): ExecutionResult {
|
||||
const node = this._context.getNode(nodeId);
|
||||
if (!node) {
|
||||
return { error: `Node not found: ${nodeId}` };
|
||||
}
|
||||
|
||||
const executor = NodeRegistry.instance.getExecutor(node.type);
|
||||
if (!executor) {
|
||||
return { error: `No executor for node type: ${node.type}` };
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.debug) {
|
||||
console.log(`[Blueprint] Executing: ${node.type} (${nodeId})`);
|
||||
}
|
||||
|
||||
const result = executor.execute(node, this._context);
|
||||
|
||||
// Cache outputs
|
||||
// 缓存输出
|
||||
if (result.outputs) {
|
||||
this._context.setOutputs(nodeId, result.outputs);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
return { error: `Execution error: ${error}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process pending delayed executions
|
||||
* 处理待处理的延迟执行
|
||||
*/
|
||||
private _processPendingExecutions(): void {
|
||||
const stillPending: PendingExecution[] = [];
|
||||
|
||||
for (const pending of this._pendingExecutions) {
|
||||
if (this._currentTime >= pending.resumeTime) {
|
||||
// Resume execution
|
||||
// 恢复执行
|
||||
const node = this._context.getNode(pending.nodeId);
|
||||
if (node) {
|
||||
this._executeFromNode(node, pending.execPin);
|
||||
}
|
||||
} else {
|
||||
stillPending.push(pending);
|
||||
}
|
||||
}
|
||||
|
||||
this._pendingExecutions = stillPending;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get instance variables for serialization
|
||||
* 获取实例变量用于序列化
|
||||
*/
|
||||
getInstanceVariables(): Map<string, unknown> {
|
||||
return this._context.getInstanceVariables();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set instance variables from serialization
|
||||
* 从序列化设置实例变量
|
||||
*/
|
||||
setInstanceVariables(variables: Map<string, unknown>): void {
|
||||
this._context.setInstanceVariables(variables);
|
||||
}
|
||||
}
|
||||
270
packages/framework/blueprint/src/runtime/ExecutionContext.ts
Normal file
270
packages/framework/blueprint/src/runtime/ExecutionContext.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* Execution Context - Runtime context for blueprint execution
|
||||
* 执行上下文 - 蓝图执行的运行时上下文
|
||||
*/
|
||||
|
||||
import type { Entity, IScene } from '@esengine/ecs-framework';
|
||||
import { BlueprintNode, BlueprintConnection } from '../types/nodes';
|
||||
import { BlueprintAsset } from '../types/blueprint';
|
||||
|
||||
/**
|
||||
* Result of node execution
|
||||
* 节点执行的结果
|
||||
*/
|
||||
export interface ExecutionResult {
|
||||
/**
|
||||
* Next exec pin to follow (null to stop, undefined to continue default)
|
||||
* 下一个要执行的引脚(null 停止,undefined 继续默认)
|
||||
*/
|
||||
nextExec?: string | null;
|
||||
|
||||
/**
|
||||
* Output values by pin name
|
||||
* 按引脚名称的输出值
|
||||
*/
|
||||
outputs?: Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Whether to yield execution (for async operations)
|
||||
* 是否暂停执行(用于异步操作)
|
||||
*/
|
||||
yield?: boolean;
|
||||
|
||||
/**
|
||||
* Delay before continuing (in seconds)
|
||||
* 继续前的延迟(秒)
|
||||
*/
|
||||
delay?: number;
|
||||
|
||||
/**
|
||||
* Error message if execution failed
|
||||
* 执行失败时的错误消息
|
||||
*/
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execution context provides access to runtime services
|
||||
* 执行上下文提供对运行时服务的访问
|
||||
*/
|
||||
export class ExecutionContext {
|
||||
/** Current blueprint asset (当前蓝图资产) */
|
||||
readonly blueprint: BlueprintAsset;
|
||||
|
||||
/** Owner entity (所有者实体) */
|
||||
readonly entity: Entity;
|
||||
|
||||
/** Current scene (当前场景) */
|
||||
readonly scene: IScene;
|
||||
|
||||
/** Frame delta time (帧增量时间) */
|
||||
deltaTime: number = 0;
|
||||
|
||||
/** Total time since start (开始以来的总时间) */
|
||||
time: number = 0;
|
||||
|
||||
/** Instance variables (实例变量) */
|
||||
private _instanceVariables: Map<string, unknown> = new Map();
|
||||
|
||||
/** Local variables (per-execution) (局部变量,每次执行) */
|
||||
private _localVariables: Map<string, unknown> = new Map();
|
||||
|
||||
/** Global variables (shared) (全局变量,共享) */
|
||||
private static _globalVariables: Map<string, unknown> = new Map();
|
||||
|
||||
/** Node output cache for current execution (当前执行的节点输出缓存) */
|
||||
private _outputCache: Map<string, Record<string, unknown>> = new Map();
|
||||
|
||||
/** Connection lookup by target (按目标的连接查找) */
|
||||
private _connectionsByTarget: Map<string, BlueprintConnection[]> = new Map();
|
||||
|
||||
/** Connection lookup by source (按源的连接查找) */
|
||||
private _connectionsBySource: Map<string, BlueprintConnection[]> = new Map();
|
||||
|
||||
constructor(blueprint: BlueprintAsset, entity: Entity, scene: IScene) {
|
||||
this.blueprint = blueprint;
|
||||
this.entity = entity;
|
||||
this.scene = scene;
|
||||
|
||||
// Initialize instance variables with defaults
|
||||
// 使用默认值初始化实例变量
|
||||
for (const variable of blueprint.variables) {
|
||||
if (variable.scope === 'instance') {
|
||||
this._instanceVariables.set(variable.name, variable.defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
// Build connection lookup maps
|
||||
// 构建连接查找映射
|
||||
this._buildConnectionMaps();
|
||||
}
|
||||
|
||||
private _buildConnectionMaps(): void {
|
||||
for (const conn of this.blueprint.connections) {
|
||||
// By target
|
||||
const targetKey = `${conn.toNodeId}.${conn.toPin}`;
|
||||
if (!this._connectionsByTarget.has(targetKey)) {
|
||||
this._connectionsByTarget.set(targetKey, []);
|
||||
}
|
||||
this._connectionsByTarget.get(targetKey)!.push(conn);
|
||||
|
||||
// By source
|
||||
const sourceKey = `${conn.fromNodeId}.${conn.fromPin}`;
|
||||
if (!this._connectionsBySource.has(sourceKey)) {
|
||||
this._connectionsBySource.set(sourceKey, []);
|
||||
}
|
||||
this._connectionsBySource.get(sourceKey)!.push(conn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a node by ID
|
||||
* 通过ID获取节点
|
||||
*/
|
||||
getNode(nodeId: string): BlueprintNode | undefined {
|
||||
return this.blueprint.nodes.find(n => n.id === nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connections to a target pin
|
||||
* 获取到目标引脚的连接
|
||||
*/
|
||||
getConnectionsToPin(nodeId: string, pinName: string): BlueprintConnection[] {
|
||||
return this._connectionsByTarget.get(`${nodeId}.${pinName}`) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connections from a source pin
|
||||
* 获取从源引脚的连接
|
||||
*/
|
||||
getConnectionsFromPin(nodeId: string, pinName: string): BlueprintConnection[] {
|
||||
return this._connectionsBySource.get(`${nodeId}.${pinName}`) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate an input pin value (follows connections or uses default)
|
||||
* 计算输入引脚值(跟随连接或使用默认值)
|
||||
*/
|
||||
evaluateInput(nodeId: string, pinName: string, defaultValue?: unknown): unknown {
|
||||
const connections = this.getConnectionsToPin(nodeId, pinName);
|
||||
|
||||
if (connections.length === 0) {
|
||||
// Use default from node data or provided default
|
||||
// 使用节点数据的默认值或提供的默认值
|
||||
const node = this.getNode(nodeId);
|
||||
return node?.data[pinName] ?? defaultValue;
|
||||
}
|
||||
|
||||
// Get value from connected output
|
||||
// 从连接的输出获取值
|
||||
const conn = connections[0];
|
||||
const cachedOutputs = this._outputCache.get(conn.fromNodeId);
|
||||
|
||||
if (cachedOutputs && conn.fromPin in cachedOutputs) {
|
||||
return cachedOutputs[conn.fromPin];
|
||||
}
|
||||
|
||||
// Need to execute the source node first (lazy evaluation)
|
||||
// 需要先执行源节点(延迟求值)
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set output values for a node (cached for current execution)
|
||||
* 设置节点的输出值(为当前执行缓存)
|
||||
*/
|
||||
setOutputs(nodeId: string, outputs: Record<string, unknown>): void {
|
||||
this._outputCache.set(nodeId, outputs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached outputs for a node
|
||||
* 获取节点的缓存输出
|
||||
*/
|
||||
getOutputs(nodeId: string): Record<string, unknown> | undefined {
|
||||
return this._outputCache.get(nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear output cache (call at start of new execution)
|
||||
* 清除输出缓存(在新执行开始时调用)
|
||||
*/
|
||||
clearOutputCache(): void {
|
||||
this._outputCache.clear();
|
||||
this._localVariables.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a variable value
|
||||
* 获取变量值
|
||||
*/
|
||||
getVariable(name: string): unknown {
|
||||
// Check local first, then instance, then global
|
||||
// 先检查局部,然后实例,然后全局
|
||||
if (this._localVariables.has(name)) {
|
||||
return this._localVariables.get(name);
|
||||
}
|
||||
if (this._instanceVariables.has(name)) {
|
||||
return this._instanceVariables.get(name);
|
||||
}
|
||||
if (ExecutionContext._globalVariables.has(name)) {
|
||||
return ExecutionContext._globalVariables.get(name);
|
||||
}
|
||||
|
||||
// Return default from variable definition
|
||||
// 返回变量定义的默认值
|
||||
const varDef = this.blueprint.variables.find(v => v.name === name);
|
||||
return varDef?.defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a variable value
|
||||
* 设置变量值
|
||||
*/
|
||||
setVariable(name: string, value: unknown): void {
|
||||
const varDef = this.blueprint.variables.find(v => v.name === name);
|
||||
|
||||
if (!varDef) {
|
||||
// Treat unknown variables as local
|
||||
// 将未知变量视为局部变量
|
||||
this._localVariables.set(name, value);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (varDef.scope) {
|
||||
case 'local':
|
||||
this._localVariables.set(name, value);
|
||||
break;
|
||||
case 'instance':
|
||||
this._instanceVariables.set(name, value);
|
||||
break;
|
||||
case 'global':
|
||||
ExecutionContext._globalVariables.set(name, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all instance variables (for serialization)
|
||||
* 获取所有实例变量(用于序列化)
|
||||
*/
|
||||
getInstanceVariables(): Map<string, unknown> {
|
||||
return new Map(this._instanceVariables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set instance variables (for deserialization)
|
||||
* 设置实例变量(用于反序列化)
|
||||
*/
|
||||
setInstanceVariables(variables: Map<string, unknown>): void {
|
||||
this._instanceVariables = new Map(variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear global variables (for scene reset)
|
||||
* 清除全局变量(用于场景重置)
|
||||
*/
|
||||
static clearGlobalVariables(): void {
|
||||
ExecutionContext._globalVariables.clear();
|
||||
}
|
||||
}
|
||||
151
packages/framework/blueprint/src/runtime/NodeRegistry.ts
Normal file
151
packages/framework/blueprint/src/runtime/NodeRegistry.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Node Registry - Manages node templates and executors
|
||||
* 节点注册表 - 管理节点模板和执行器
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../types/nodes';
|
||||
import { ExecutionContext, ExecutionResult } from './ExecutionContext';
|
||||
|
||||
/**
|
||||
* Node executor interface - implements the logic for a node type
|
||||
* 节点执行器接口 - 实现节点类型的逻辑
|
||||
*/
|
||||
export interface INodeExecutor {
|
||||
/**
|
||||
* Execute the node
|
||||
* 执行节点
|
||||
*
|
||||
* @param node - Node instance (节点实例)
|
||||
* @param context - Execution context (执行上下文)
|
||||
* @returns Execution result (执行结果)
|
||||
*/
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Node definition combines template with executor
|
||||
* 节点定义组合模板和执行器
|
||||
*/
|
||||
export interface NodeDefinition {
|
||||
template: BlueprintNodeTemplate;
|
||||
executor: INodeExecutor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Node Registry - singleton that holds all registered node types
|
||||
* 节点注册表 - 持有所有注册节点类型的单例
|
||||
*/
|
||||
export class NodeRegistry {
|
||||
private static _instance: NodeRegistry;
|
||||
private _nodes: Map<string, NodeDefinition> = new Map();
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static get instance(): NodeRegistry {
|
||||
if (!NodeRegistry._instance) {
|
||||
NodeRegistry._instance = new NodeRegistry();
|
||||
}
|
||||
return NodeRegistry._instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a node type
|
||||
* 注册节点类型
|
||||
*/
|
||||
register(template: BlueprintNodeTemplate, executor: INodeExecutor): void {
|
||||
if (this._nodes.has(template.type)) {
|
||||
console.warn(`Node type "${template.type}" is already registered, overwriting`);
|
||||
}
|
||||
this._nodes.set(template.type, { template, executor });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a node definition by type
|
||||
* 通过类型获取节点定义
|
||||
*/
|
||||
get(type: string): NodeDefinition | undefined {
|
||||
return this._nodes.get(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get node template by type
|
||||
* 通过类型获取节点模板
|
||||
*/
|
||||
getTemplate(type: string): BlueprintNodeTemplate | undefined {
|
||||
return this._nodes.get(type)?.template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get node executor by type
|
||||
* 通过类型获取节点执行器
|
||||
*/
|
||||
getExecutor(type: string): INodeExecutor | undefined {
|
||||
return this._nodes.get(type)?.executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node type is registered
|
||||
* 检查节点类型是否已注册
|
||||
*/
|
||||
has(type: string): boolean {
|
||||
return this._nodes.has(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered templates
|
||||
* 获取所有注册的模板
|
||||
*/
|
||||
getAllTemplates(): BlueprintNodeTemplate[] {
|
||||
return Array.from(this._nodes.values()).map(d => d.template);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get templates by category
|
||||
* 按类别获取模板
|
||||
*/
|
||||
getTemplatesByCategory(category: string): BlueprintNodeTemplate[] {
|
||||
return this.getAllTemplates().filter(t => t.category === category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search templates by keyword
|
||||
* 按关键词搜索模板
|
||||
*/
|
||||
searchTemplates(keyword: string): BlueprintNodeTemplate[] {
|
||||
const lower = keyword.toLowerCase();
|
||||
return this.getAllTemplates().filter(t =>
|
||||
t.title.toLowerCase().includes(lower) ||
|
||||
t.type.toLowerCase().includes(lower) ||
|
||||
t.keywords?.some(k => k.toLowerCase().includes(lower)) ||
|
||||
t.description?.toLowerCase().includes(lower)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all registrations (for testing)
|
||||
* 清除所有注册(用于测试)
|
||||
*/
|
||||
clear(): void {
|
||||
this._nodes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorator for registering node executors
|
||||
* 用于注册节点执行器的装饰器
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @RegisterNode(EventTickTemplate)
|
||||
* class EventTickExecutor implements INodeExecutor {
|
||||
* execute(node, context) { ... }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function RegisterNode(template: BlueprintNodeTemplate) {
|
||||
return function<T extends new () => INodeExecutor>(constructor: T) {
|
||||
const executor = new constructor();
|
||||
NodeRegistry.instance.register(template, executor);
|
||||
return constructor;
|
||||
};
|
||||
}
|
||||
10
packages/framework/blueprint/src/runtime/index.ts
Normal file
10
packages/framework/blueprint/src/runtime/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Blueprint Runtime - Execution engine for blueprints
|
||||
* 蓝图运行时 - 蓝图执行引擎
|
||||
*/
|
||||
|
||||
export * from './ExecutionContext';
|
||||
export * from './NodeRegistry';
|
||||
export * from './BlueprintVM';
|
||||
export * from './BlueprintComponent';
|
||||
export * from './BlueprintSystem';
|
||||
22
packages/framework/blueprint/src/tokens.ts
Normal file
22
packages/framework/blueprint/src/tokens.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Blueprint 模块服务令牌
|
||||
* Blueprint module service tokens
|
||||
*
|
||||
* 定义 blueprint 模块导出的服务令牌。
|
||||
* Defines service tokens exported by blueprint module.
|
||||
*
|
||||
* 注意:当前 Blueprint 模块主要通过组件和系统工作,
|
||||
* 暂时没有需要通过服务令牌暴露的全局服务。
|
||||
* 未来可能添加 BlueprintDebuggerToken 等。
|
||||
*
|
||||
* Note: Blueprint module currently works mainly through components and systems,
|
||||
* no global services need to be exposed via service tokens yet.
|
||||
* May add BlueprintDebuggerToken etc. in the future.
|
||||
*/
|
||||
|
||||
// 当前无服务令牌
|
||||
// No service tokens currently
|
||||
|
||||
// 预留导出位置,便于将来扩展
|
||||
// Reserved export location for future extension
|
||||
export {};
|
||||
497
packages/framework/blueprint/src/triggers/BlueprintTrigger.ts
Normal file
497
packages/framework/blueprint/src/triggers/BlueprintTrigger.ts
Normal file
@@ -0,0 +1,497 @@
|
||||
/**
|
||||
* @zh 蓝图触发器
|
||||
* @en Blueprint Trigger
|
||||
*
|
||||
* @zh 定义触发器的核心实现
|
||||
* @en Defines core trigger implementation
|
||||
*/
|
||||
|
||||
import type { TriggerType, ITriggerContext } from './TriggerTypes';
|
||||
import type { ITriggerCondition } from './TriggerCondition';
|
||||
import { AlwaysTrueCondition } from './TriggerCondition';
|
||||
|
||||
// =============================================================================
|
||||
// 触发器接口 | Trigger Interface
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 触发器回调函数类型
|
||||
* @en Trigger callback function type
|
||||
*/
|
||||
export type TriggerCallback = (context: ITriggerContext) => void;
|
||||
|
||||
/**
|
||||
* @zh 蓝图触发器接口
|
||||
* @en Blueprint trigger interface
|
||||
*/
|
||||
export interface IBlueprintTrigger {
|
||||
/**
|
||||
* @zh 触发器唯一标识
|
||||
* @en Trigger unique identifier
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* @zh 触发器类型
|
||||
* @en Trigger type
|
||||
*/
|
||||
readonly type: TriggerType;
|
||||
|
||||
/**
|
||||
* @zh 触发器条件
|
||||
* @en Trigger conditions
|
||||
*/
|
||||
readonly condition: ITriggerCondition;
|
||||
|
||||
/**
|
||||
* @zh 是否启用
|
||||
* @en Is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
|
||||
/**
|
||||
* @zh 优先级(越高越先执行)
|
||||
* @en Priority (higher executes first)
|
||||
*/
|
||||
readonly priority: number;
|
||||
|
||||
/**
|
||||
* @zh 检查是否应该触发
|
||||
* @en Check if should fire
|
||||
*/
|
||||
shouldFire(context: ITriggerContext): boolean;
|
||||
|
||||
/**
|
||||
* @zh 执行触发器
|
||||
* @en Execute trigger
|
||||
*/
|
||||
fire(context: ITriggerContext): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 触发器配置
|
||||
* @en Trigger configuration
|
||||
*/
|
||||
export interface TriggerConfig {
|
||||
/**
|
||||
* @zh 触发器 ID
|
||||
* @en Trigger ID
|
||||
*/
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* @zh 触发器类型
|
||||
* @en Trigger type
|
||||
*/
|
||||
type: TriggerType;
|
||||
|
||||
/**
|
||||
* @zh 触发条件
|
||||
* @en Trigger condition
|
||||
*/
|
||||
condition?: ITriggerCondition;
|
||||
|
||||
/**
|
||||
* @zh 是否启用
|
||||
* @en Is enabled
|
||||
*/
|
||||
enabled?: boolean;
|
||||
|
||||
/**
|
||||
* @zh 优先级
|
||||
* @en Priority
|
||||
*/
|
||||
priority?: number;
|
||||
|
||||
/**
|
||||
* @zh 回调函数
|
||||
* @en Callback function
|
||||
*/
|
||||
callback?: TriggerCallback;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 触发器实现 | Trigger Implementation
|
||||
// =============================================================================
|
||||
|
||||
let _triggerId = 0;
|
||||
|
||||
/**
|
||||
* @zh 生成唯一触发器 ID
|
||||
* @en Generate unique trigger ID
|
||||
*/
|
||||
function generateTriggerId(): string {
|
||||
return `trigger_${++_triggerId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 蓝图触发器实现
|
||||
* @en Blueprint trigger implementation
|
||||
*/
|
||||
export class BlueprintTrigger implements IBlueprintTrigger {
|
||||
readonly id: string;
|
||||
readonly type: TriggerType;
|
||||
readonly condition: ITriggerCondition;
|
||||
readonly priority: number;
|
||||
enabled: boolean;
|
||||
|
||||
private readonly _callback?: TriggerCallback;
|
||||
private readonly _callbacks: Set<TriggerCallback> = new Set();
|
||||
|
||||
constructor(config: TriggerConfig) {
|
||||
this.id = config.id ?? generateTriggerId();
|
||||
this.type = config.type;
|
||||
this.condition = config.condition ?? new AlwaysTrueCondition();
|
||||
this.priority = config.priority ?? 0;
|
||||
this.enabled = config.enabled ?? true;
|
||||
this._callback = config.callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查是否应该触发
|
||||
* @en Check if should fire
|
||||
*/
|
||||
shouldFire(context: ITriggerContext): boolean {
|
||||
if (!this.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.type !== this.type && this.type !== 'custom') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.condition.evaluate(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 执行触发器
|
||||
* @en Execute trigger
|
||||
*/
|
||||
fire(context: ITriggerContext): void {
|
||||
if (this._callback) {
|
||||
this._callback(context);
|
||||
}
|
||||
|
||||
for (const callback of this._callbacks) {
|
||||
callback(context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 添加回调
|
||||
* @en Add callback
|
||||
*/
|
||||
addCallback(callback: TriggerCallback): void {
|
||||
this._callbacks.add(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 移除回调
|
||||
* @en Remove callback
|
||||
*/
|
||||
removeCallback(callback: TriggerCallback): void {
|
||||
this._callbacks.delete(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 清除所有回调
|
||||
* @en Clear all callbacks
|
||||
*/
|
||||
clearCallbacks(): void {
|
||||
this._callbacks.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 触发器注册表 | Trigger Registry
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 触发器注册表接口
|
||||
* @en Trigger registry interface
|
||||
*/
|
||||
export interface ITriggerRegistry {
|
||||
/**
|
||||
* @zh 注册触发器
|
||||
* @en Register trigger
|
||||
*/
|
||||
register(trigger: IBlueprintTrigger): void;
|
||||
|
||||
/**
|
||||
* @zh 注销触发器
|
||||
* @en Unregister trigger
|
||||
*/
|
||||
unregister(triggerId: string): boolean;
|
||||
|
||||
/**
|
||||
* @zh 获取触发器
|
||||
* @en Get trigger
|
||||
*/
|
||||
get(triggerId: string): IBlueprintTrigger | undefined;
|
||||
|
||||
/**
|
||||
* @zh 获取所有触发器
|
||||
* @en Get all triggers
|
||||
*/
|
||||
getAll(): IBlueprintTrigger[];
|
||||
|
||||
/**
|
||||
* @zh 按类型获取触发器
|
||||
* @en Get triggers by type
|
||||
*/
|
||||
getByType(type: TriggerType): IBlueprintTrigger[];
|
||||
|
||||
/**
|
||||
* @zh 清除所有触发器
|
||||
* @en Clear all triggers
|
||||
*/
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 触发器注册表实现
|
||||
* @en Trigger registry implementation
|
||||
*/
|
||||
export class TriggerRegistry implements ITriggerRegistry {
|
||||
private readonly _triggers: Map<string, IBlueprintTrigger> = new Map();
|
||||
private readonly _triggersByType: Map<TriggerType, Set<string>> = new Map();
|
||||
|
||||
/**
|
||||
* @zh 注册触发器
|
||||
* @en Register trigger
|
||||
*/
|
||||
register(trigger: IBlueprintTrigger): void {
|
||||
if (this._triggers.has(trigger.id)) {
|
||||
console.warn(`Trigger ${trigger.id} already registered, overwriting`);
|
||||
}
|
||||
|
||||
this._triggers.set(trigger.id, trigger);
|
||||
|
||||
if (!this._triggersByType.has(trigger.type)) {
|
||||
this._triggersByType.set(trigger.type, new Set());
|
||||
}
|
||||
this._triggersByType.get(trigger.type)!.add(trigger.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 注销触发器
|
||||
* @en Unregister trigger
|
||||
*/
|
||||
unregister(triggerId: string): boolean {
|
||||
const trigger = this._triggers.get(triggerId);
|
||||
if (!trigger) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._triggers.delete(triggerId);
|
||||
|
||||
const typeSet = this._triggersByType.get(trigger.type);
|
||||
if (typeSet) {
|
||||
typeSet.delete(triggerId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取触发器
|
||||
* @en Get trigger
|
||||
*/
|
||||
get(triggerId: string): IBlueprintTrigger | undefined {
|
||||
return this._triggers.get(triggerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取所有触发器
|
||||
* @en Get all triggers
|
||||
*/
|
||||
getAll(): IBlueprintTrigger[] {
|
||||
return Array.from(this._triggers.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 按类型获取触发器
|
||||
* @en Get triggers by type
|
||||
*/
|
||||
getByType(type: TriggerType): IBlueprintTrigger[] {
|
||||
const typeSet = this._triggersByType.get(type);
|
||||
if (!typeSet) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const triggers: IBlueprintTrigger[] = [];
|
||||
for (const id of typeSet) {
|
||||
const trigger = this._triggers.get(id);
|
||||
if (trigger) {
|
||||
triggers.push(trigger);
|
||||
}
|
||||
}
|
||||
|
||||
return triggers.sort((a, b) => b.priority - a.priority);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 清除所有触发器
|
||||
* @en Clear all triggers
|
||||
*/
|
||||
clear(): void {
|
||||
this._triggers.clear();
|
||||
this._triggersByType.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取触发器数量
|
||||
* @en Get trigger count
|
||||
*/
|
||||
get count(): number {
|
||||
return this._triggers.size;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 工厂函数 | Factory Functions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 创建触发器
|
||||
* @en Create trigger
|
||||
*/
|
||||
export function createTrigger(config: TriggerConfig): BlueprintTrigger {
|
||||
return new BlueprintTrigger(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建 Tick 触发器
|
||||
* @en Create tick trigger
|
||||
*/
|
||||
export function createTickTrigger(
|
||||
callback?: TriggerCallback,
|
||||
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
|
||||
): BlueprintTrigger {
|
||||
return new BlueprintTrigger({
|
||||
id: options?.id,
|
||||
type: 'tick',
|
||||
condition: options?.condition,
|
||||
priority: options?.priority,
|
||||
callback
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建输入触发器
|
||||
* @en Create input trigger
|
||||
*/
|
||||
export function createInputTrigger(
|
||||
callback?: TriggerCallback,
|
||||
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
|
||||
): BlueprintTrigger {
|
||||
return new BlueprintTrigger({
|
||||
id: options?.id,
|
||||
type: 'input',
|
||||
condition: options?.condition,
|
||||
priority: options?.priority,
|
||||
callback
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建碰撞触发器
|
||||
* @en Create collision trigger
|
||||
*/
|
||||
export function createCollisionTrigger(
|
||||
callback?: TriggerCallback,
|
||||
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
|
||||
): BlueprintTrigger {
|
||||
return new BlueprintTrigger({
|
||||
id: options?.id,
|
||||
type: 'collision',
|
||||
condition: options?.condition,
|
||||
priority: options?.priority,
|
||||
callback
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建消息触发器
|
||||
* @en Create message trigger
|
||||
*/
|
||||
export function createMessageTrigger(
|
||||
callback?: TriggerCallback,
|
||||
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
|
||||
): BlueprintTrigger {
|
||||
return new BlueprintTrigger({
|
||||
id: options?.id,
|
||||
type: 'message',
|
||||
condition: options?.condition,
|
||||
priority: options?.priority,
|
||||
callback
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建定时器触发器
|
||||
* @en Create timer trigger
|
||||
*/
|
||||
export function createTimerTrigger(
|
||||
callback?: TriggerCallback,
|
||||
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
|
||||
): BlueprintTrigger {
|
||||
return new BlueprintTrigger({
|
||||
id: options?.id,
|
||||
type: 'timer',
|
||||
condition: options?.condition,
|
||||
priority: options?.priority,
|
||||
callback
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建状态进入触发器
|
||||
* @en Create state enter trigger
|
||||
*/
|
||||
export function createStateEnterTrigger(
|
||||
callback?: TriggerCallback,
|
||||
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
|
||||
): BlueprintTrigger {
|
||||
return new BlueprintTrigger({
|
||||
id: options?.id,
|
||||
type: 'stateEnter',
|
||||
condition: options?.condition,
|
||||
priority: options?.priority,
|
||||
callback
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建状态退出触发器
|
||||
* @en Create state exit trigger
|
||||
*/
|
||||
export function createStateExitTrigger(
|
||||
callback?: TriggerCallback,
|
||||
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
|
||||
): BlueprintTrigger {
|
||||
return new BlueprintTrigger({
|
||||
id: options?.id,
|
||||
type: 'stateExit',
|
||||
condition: options?.condition,
|
||||
priority: options?.priority,
|
||||
callback
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建自定义触发器
|
||||
* @en Create custom trigger
|
||||
*/
|
||||
export function createCustomTrigger(
|
||||
callback?: TriggerCallback,
|
||||
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
|
||||
): BlueprintTrigger {
|
||||
return new BlueprintTrigger({
|
||||
id: options?.id,
|
||||
type: 'custom',
|
||||
condition: options?.condition,
|
||||
priority: options?.priority,
|
||||
callback
|
||||
});
|
||||
}
|
||||
479
packages/framework/blueprint/src/triggers/TriggerCondition.ts
Normal file
479
packages/framework/blueprint/src/triggers/TriggerCondition.ts
Normal file
@@ -0,0 +1,479 @@
|
||||
/**
|
||||
* @zh 触发器条件系统
|
||||
* @en Trigger Condition System
|
||||
*
|
||||
* @zh 提供触发器触发前的条件检查能力
|
||||
* @en Provides condition checking before trigger fires
|
||||
*/
|
||||
|
||||
import type {
|
||||
ITriggerContext,
|
||||
TriggerType,
|
||||
IInputTriggerContext,
|
||||
IMessageTriggerContext,
|
||||
IStateTriggerContext,
|
||||
ITimerTriggerContext,
|
||||
ICollisionTriggerContext,
|
||||
ICustomTriggerContext
|
||||
} from './TriggerTypes';
|
||||
|
||||
// =============================================================================
|
||||
// 条件接口 | Condition Interface
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 触发器条件接口
|
||||
* @en Trigger condition interface
|
||||
*/
|
||||
export interface ITriggerCondition {
|
||||
/**
|
||||
* @zh 条件类型标识
|
||||
* @en Condition type identifier
|
||||
*/
|
||||
readonly type: string;
|
||||
|
||||
/**
|
||||
* @zh 评估条件是否满足
|
||||
* @en Evaluate if condition is met
|
||||
*
|
||||
* @param context - @zh 触发器上下文 @en Trigger context
|
||||
* @returns @zh 条件是否满足 @en Whether condition is met
|
||||
*/
|
||||
evaluate(context: ITriggerContext): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 条件组合逻辑
|
||||
* @en Condition combination logic
|
||||
*/
|
||||
export type ConditionLogic = 'and' | 'or';
|
||||
|
||||
// =============================================================================
|
||||
// 复合条件 | Composite Conditions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 复合条件 - 组合多个条件
|
||||
* @en Composite condition - combines multiple conditions
|
||||
*/
|
||||
export class CompositeCondition implements ITriggerCondition {
|
||||
readonly type = 'composite';
|
||||
|
||||
constructor(
|
||||
private readonly _conditions: ITriggerCondition[],
|
||||
private readonly _logic: ConditionLogic = 'and'
|
||||
) {}
|
||||
|
||||
evaluate(context: ITriggerContext): boolean {
|
||||
if (this._conditions.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this._logic === 'and') {
|
||||
return this._conditions.every(c => c.evaluate(context));
|
||||
} else {
|
||||
return this._conditions.some(c => c.evaluate(context));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 非条件 - 取反
|
||||
* @en Not condition - negates
|
||||
*/
|
||||
export class NotCondition implements ITriggerCondition {
|
||||
readonly type = 'not';
|
||||
|
||||
constructor(private readonly _condition: ITriggerCondition) {}
|
||||
|
||||
evaluate(context: ITriggerContext): boolean {
|
||||
return !this._condition.evaluate(context);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 通用条件 | Generic Conditions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 始终为真的条件
|
||||
* @en Always true condition
|
||||
*/
|
||||
export class AlwaysTrueCondition implements ITriggerCondition {
|
||||
readonly type = 'alwaysTrue';
|
||||
|
||||
evaluate(_context: ITriggerContext): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 始终为假的条件
|
||||
* @en Always false condition
|
||||
*/
|
||||
export class AlwaysFalseCondition implements ITriggerCondition {
|
||||
readonly type = 'alwaysFalse';
|
||||
|
||||
evaluate(_context: ITriggerContext): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 触发器类型条件
|
||||
* @en Trigger type condition
|
||||
*/
|
||||
export class TriggerTypeCondition implements ITriggerCondition {
|
||||
readonly type = 'triggerType';
|
||||
|
||||
constructor(private readonly _allowedTypes: TriggerType[]) {}
|
||||
|
||||
evaluate(context: ITriggerContext): boolean {
|
||||
return this._allowedTypes.includes(context.type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 实体 ID 条件
|
||||
* @en Entity ID condition
|
||||
*/
|
||||
export class EntityIdCondition implements ITriggerCondition {
|
||||
readonly type = 'entityId';
|
||||
|
||||
constructor(
|
||||
private readonly _entityId: string,
|
||||
private readonly _checkSource: boolean = true
|
||||
) {}
|
||||
|
||||
evaluate(context: ITriggerContext): boolean {
|
||||
if (this._checkSource) {
|
||||
return context.sourceEntityId === this._entityId;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 自定义函数条件
|
||||
* @en Custom function condition
|
||||
*/
|
||||
export class FunctionCondition implements ITriggerCondition {
|
||||
readonly type = 'function';
|
||||
|
||||
constructor(
|
||||
private readonly _predicate: (context: ITriggerContext) => boolean
|
||||
) {}
|
||||
|
||||
evaluate(context: ITriggerContext): boolean {
|
||||
return this._predicate(context);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 特定类型条件 | Type-Specific Conditions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 输入动作条件
|
||||
* @en Input action condition
|
||||
*/
|
||||
export class InputActionCondition implements ITriggerCondition {
|
||||
readonly type = 'inputAction';
|
||||
|
||||
constructor(
|
||||
private readonly _action: string,
|
||||
private readonly _checkPressed?: boolean,
|
||||
private readonly _checkReleased?: boolean
|
||||
) {}
|
||||
|
||||
evaluate(context: ITriggerContext): boolean {
|
||||
if (context.type !== 'input') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const inputContext = context as unknown as IInputTriggerContext;
|
||||
|
||||
if (inputContext.action !== this._action) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._checkPressed !== undefined && inputContext.pressed !== this._checkPressed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._checkReleased !== undefined && inputContext.released !== this._checkReleased) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 消息名称条件
|
||||
* @en Message name condition
|
||||
*/
|
||||
export class MessageNameCondition implements ITriggerCondition {
|
||||
readonly type = 'messageName';
|
||||
|
||||
constructor(private readonly _messageName: string) {}
|
||||
|
||||
evaluate(context: ITriggerContext): boolean {
|
||||
if (context.type !== 'message') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const messageContext = context as unknown as IMessageTriggerContext;
|
||||
return messageContext.messageName === this._messageName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 状态名称条件
|
||||
* @en State name condition
|
||||
*/
|
||||
export class StateNameCondition implements ITriggerCondition {
|
||||
readonly type = 'stateName';
|
||||
|
||||
constructor(
|
||||
private readonly _stateName: string,
|
||||
private readonly _checkCurrent: boolean = true
|
||||
) {}
|
||||
|
||||
evaluate(context: ITriggerContext): boolean {
|
||||
if (context.type !== 'stateEnter' && context.type !== 'stateExit') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stateContext = context as unknown as IStateTriggerContext;
|
||||
|
||||
if (this._checkCurrent) {
|
||||
return stateContext.currentState === this._stateName;
|
||||
} else {
|
||||
return stateContext.previousState === this._stateName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 定时器 ID 条件
|
||||
* @en Timer ID condition
|
||||
*/
|
||||
export class TimerIdCondition implements ITriggerCondition {
|
||||
readonly type = 'timerId';
|
||||
|
||||
constructor(private readonly _timerId: string) {}
|
||||
|
||||
evaluate(context: ITriggerContext): boolean {
|
||||
if (context.type !== 'timer') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const timerContext = context as unknown as ITimerTriggerContext;
|
||||
return timerContext.timerId === this._timerId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 碰撞实体条件
|
||||
* @en Collision entity condition
|
||||
*/
|
||||
export class CollisionEntityCondition implements ITriggerCondition {
|
||||
readonly type = 'collisionEntity';
|
||||
|
||||
constructor(
|
||||
private readonly _otherEntityId?: string,
|
||||
private readonly _checkEnter?: boolean,
|
||||
private readonly _checkExit?: boolean
|
||||
) {}
|
||||
|
||||
evaluate(context: ITriggerContext): boolean {
|
||||
if (context.type !== 'collision') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const collisionContext = context as unknown as ICollisionTriggerContext;
|
||||
|
||||
if (this._otherEntityId !== undefined && collisionContext.otherEntityId !== this._otherEntityId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._checkEnter !== undefined && collisionContext.isEnter !== this._checkEnter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._checkExit !== undefined && collisionContext.isExit !== this._checkExit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 自定义事件名称条件
|
||||
* @en Custom event name condition
|
||||
*/
|
||||
export class CustomEventCondition implements ITriggerCondition {
|
||||
readonly type = 'customEvent';
|
||||
|
||||
constructor(private readonly _eventName: string) {}
|
||||
|
||||
evaluate(context: ITriggerContext): boolean {
|
||||
if (context.type !== 'custom') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const customContext = context as unknown as ICustomTriggerContext;
|
||||
return customContext.eventName === this._eventName;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 条件构建器 | Condition Builder
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 条件构建器 - 链式 API
|
||||
* @en Condition builder - fluent API
|
||||
*/
|
||||
export class ConditionBuilder {
|
||||
private _conditions: ITriggerCondition[] = [];
|
||||
private _logic: ConditionLogic = 'and';
|
||||
|
||||
/**
|
||||
* @zh 设置组合逻辑为 AND
|
||||
* @en Set combination logic to AND
|
||||
*/
|
||||
and(): this {
|
||||
this._logic = 'and';
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 设置组合逻辑为 OR
|
||||
* @en Set combination logic to OR
|
||||
*/
|
||||
or(): this {
|
||||
this._logic = 'or';
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 添加触发器类型条件
|
||||
* @en Add trigger type condition
|
||||
*/
|
||||
ofType(...types: TriggerType[]): this {
|
||||
this._conditions.push(new TriggerTypeCondition(types));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 添加实体 ID 条件
|
||||
* @en Add entity ID condition
|
||||
*/
|
||||
fromEntity(entityId: string): this {
|
||||
this._conditions.push(new EntityIdCondition(entityId));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 添加输入动作条件
|
||||
* @en Add input action condition
|
||||
*/
|
||||
onInput(action: string, options?: { pressed?: boolean; released?: boolean }): this {
|
||||
this._conditions.push(new InputActionCondition(action, options?.pressed, options?.released));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 添加消息条件
|
||||
* @en Add message condition
|
||||
*/
|
||||
onMessage(messageName: string): this {
|
||||
this._conditions.push(new MessageNameCondition(messageName));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 添加状态条件
|
||||
* @en Add state condition
|
||||
*/
|
||||
onState(stateName: string, checkCurrent: boolean = true): this {
|
||||
this._conditions.push(new StateNameCondition(stateName, checkCurrent));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 添加定时器条件
|
||||
* @en Add timer condition
|
||||
*/
|
||||
onTimer(timerId: string): this {
|
||||
this._conditions.push(new TimerIdCondition(timerId));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 添加碰撞条件
|
||||
* @en Add collision condition
|
||||
*/
|
||||
onCollision(options?: { entityId?: string; isEnter?: boolean; isExit?: boolean }): this {
|
||||
this._conditions.push(new CollisionEntityCondition(
|
||||
options?.entityId,
|
||||
options?.isEnter,
|
||||
options?.isExit
|
||||
));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 添加自定义事件条件
|
||||
* @en Add custom event condition
|
||||
*/
|
||||
onCustomEvent(eventName: string): this {
|
||||
this._conditions.push(new CustomEventCondition(eventName));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 添加自定义函数条件
|
||||
* @en Add custom function condition
|
||||
*/
|
||||
where(predicate: (context: ITriggerContext) => boolean): this {
|
||||
this._conditions.push(new FunctionCondition(predicate));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 添加取反条件
|
||||
* @en Add negated condition
|
||||
*/
|
||||
not(condition: ITriggerCondition): this {
|
||||
this._conditions.push(new NotCondition(condition));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 构建条件
|
||||
* @en Build condition
|
||||
*/
|
||||
build(): ITriggerCondition {
|
||||
if (this._conditions.length === 0) {
|
||||
return new AlwaysTrueCondition();
|
||||
}
|
||||
|
||||
if (this._conditions.length === 1) {
|
||||
return this._conditions[0];
|
||||
}
|
||||
|
||||
return new CompositeCondition(this._conditions, this._logic);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建条件构建器
|
||||
* @en Create condition builder
|
||||
*/
|
||||
export function condition(): ConditionBuilder {
|
||||
return new ConditionBuilder();
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user