From 0d9f048f39d4c2fddc932c9c2e14f37b6ac3d8c2 Mon Sep 17 00:00:00 2001 From: gongxh Date: Wed, 4 Jun 2025 23:10:59 +0800 Subject: [PATCH] init --- .gitignore | 9 + .npmignore | 5 + Notes.md | 11 + README.md | 141 ++++++++++ package.json | 53 ++++ rollup.config.mjs | 86 +++++++ src/behaviortree/Agent.ts | 48 ++++ src/behaviortree/BTNode/Action.ts | 189 ++++++++++++++ src/behaviortree/BTNode/BaseNode.ts | 113 +++++++++ src/behaviortree/BTNode/Composite.ts | 206 +++++++++++++++ src/behaviortree/BTNode/Condition.ts | 24 ++ src/behaviortree/BTNode/Decorator.ts | 367 +++++++++++++++++++++++++++ src/behaviortree/BehaviorTree.ts | 61 +++++ src/behaviortree/Blackboard.ts | 105 ++++++++ src/behaviortree/Ticker.ts | 55 ++++ src/behaviortree/header.ts | 27 ++ src/kunpocc-behaviortree.ts | 14 + tsconfig.json | 24 ++ 18 files changed, 1538 insertions(+) create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 Notes.md create mode 100644 README.md create mode 100644 package.json create mode 100644 rollup.config.mjs create mode 100644 src/behaviortree/Agent.ts create mode 100644 src/behaviortree/BTNode/Action.ts create mode 100644 src/behaviortree/BTNode/BaseNode.ts create mode 100644 src/behaviortree/BTNode/Composite.ts create mode 100644 src/behaviortree/BTNode/Condition.ts create mode 100644 src/behaviortree/BTNode/Decorator.ts create mode 100644 src/behaviortree/BehaviorTree.ts create mode 100644 src/behaviortree/Blackboard.ts create mode 100644 src/behaviortree/Ticker.ts create mode 100644 src/behaviortree/header.ts create mode 100644 src/kunpocc-behaviortree.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..296d188 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# 文件 +.DS_Store +package-lock.json + +# 文件夹 +.vscode/ +node_modules/ +dist/ +build/ \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..061267b --- /dev/null +++ b/.npmignore @@ -0,0 +1,5 @@ +# 文件夹 +node_modules/ +libs/ +build/ +src/ \ No newline at end of file diff --git a/Notes.md b/Notes.md new file mode 100644 index 0000000..be0eefd --- /dev/null +++ b/Notes.md @@ -0,0 +1,11 @@ +发布版本 + +```bash +npm publish --otp +``` + +删除指定版本 + +```bash +npm unpublish kunpocc-behaviortree@1.0.23 --otp +``` \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..568b11b --- /dev/null +++ b/README.md @@ -0,0 +1,141 @@ +## 行为树 + +> 行为树是一种强大的 AI 决策系统,用于实现复杂的游戏 AI 行为。 + +#### 基本概念 + +1. 节点状态 +```typescript +enum Status { + SUCCESS, // 成功 + FAILURE, // 失败 + RUNNING // 运行中 +} +``` + +2. 节点类型 +- **动作节点 (Action)**:执行具体行为的叶子节点 +- **组合节点 (Composite)**:控制子节点执行顺序的节点 +- **条件节点 (Condition)**:判断条件的节点 +- **装饰节点 (Decorator)**:修饰其他节点行为的节点 + +#### 使用示例 + +```typescript +import { + BehaviorTree, + Sequence, + Selector, + Parallel, + Success, + Failure, + WaitTime, + Agent, + Blackboard +} from 'kunpocc-behaviortree'; + +// 1. 创建行为树 +const tree = new BehaviorTree( + new Sequence( // 顺序节点:按顺序执行所有子节点 + new WaitTime(2), // 等待2秒 + new Selector( // 选择节点:选择一个可执行的子节点 + new Success(() => { + console.log("执行成功动作"); + }), + new Failure(() => { + console.log("执行失败动作"); + }) + ) + ) +); + +// 2. 创建代理和黑板 +const agent = new Agent(); // AI代理 +const blackboard = new Blackboard(); // 共享数据黑板 + +// 3. 执行行为树 +tree.tick(agent, blackboard); +``` + +#### 常用节点 + +1. 组合节点 + + ```typescript + // 顺序节点:按顺序执行所有子节点,直到遇到失败或运行中的节点 + new Sequence(childNode1, childNode2, childNode3); + + // 选择节点:选择第一个成功或运行中的子节点 + new Selector(childNode1, childNode2, childNode3); + + // 并行节点:同时执行所有子节点 + new Parallel(childNode1, childNode2, childNode3); + + // 记忆顺序节点:记住上次执行的位置 + new MemSequence(childNode1, childNode2, childNode3); + + // 记忆选择节点:记住上次执行的位置 + new MemSelector(childNode1, childNode2, childNode3); + + // 随机选择节点:随机选择一个子节点执行 + new RandomSelector(childNode1, childNode2, childNode3); + ``` + +2. 动作节点 + + ```typescript + // 成功节点 + new Success(() => { + // 执行动作 + }); + + // 失败节点 + new Failure(() => { + // 执行动作 + }); + + // 运行中节点 + new Running(() => { + // 持续执行的动作 + }); + + // 等待节点 + new WaitTime(2); // 等待2秒 + new WaitTicks(5); // 等待5个tick + ``` + +3. 使用黑板共享数据 + + ```typescript + // 在节点中使用黑板 + class CustomAction extends Action { + tick(ticker: Ticker): Status { + // 获取数据 + const data = ticker.blackboard.get("key"); + + // 设置数据 + ticker.blackboard.set("key", "value"); + + return Status.SUCCESS; + } + } + ``` + + +#### 注意事项 + +1. 节点状态说明: + - `SUCCESS`:节点执行成功 + - `FAILURE`:节点执行失败 + - `RUNNING`:节点正在执行中 +2. 组合节点特性: + - `Sequence`:所有子节点返回 SUCCESS 才返回 SUCCESS + - `Selector`:任一子节点返回 SUCCESS 就返回 SUCCESS + - `Parallel`:并行执行所有子节点 + - `MemSequence/MemSelector`:会记住上次执行位置 +3. 性能优化: + - 使用黑板共享数据,避免重复计算 + - 合理使用记忆节点,减少重复执行 + - 控制行为树的深度,避免过于复杂 + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..f9dc7f1 --- /dev/null +++ b/package.json @@ -0,0 +1,53 @@ +{ + "name": "kunpocc-behaviortree", + "version": "0.0.1", + "description": "行为树", + "main": "./dist/kunpocc-behaviortree.cjs", + "module": "./dist/kunpocc-behaviortree.mjs", + "types": "./dist/kunpocc-behaviortree.d.ts", + "exports": { + ".": { + "require": "./dist/kunpocc-behaviortree.cjs", + "import": "./dist/kunpocc-behaviortree.mjs", + "types": "./dist/kunpocc-behaviortree.d.ts", + "default": "./dist/kunpocc-behaviortree.cjs" + }, + "./min": { + "require": "./dist/kunpocc-behaviortree.min.cjs", + "import": "./dist/kunpocc-behaviortree.min.mjs" + } + }, + "scripts": { + "clean": "rm -rf dist", + "build": "npm run clean && rollup -c rollup.config.mjs" + }, + "files": [ + "dist/kunpocc-behaviortree.cjs", + "dist/kunpocc-behaviortree.mjs", + "dist/kunpocc-behaviortree.min.cjs", + "dist/kunpocc-behaviortree.min.mjs", + "dist/kunpocc-behaviortree.d.ts", + "libs/lib.ali.api.d.ts", + "libs/lib.bytedance.api.d.ts", + "libs/lib.wx.api.d.ts" + ], + "author": "gongxh", + "license": "ISC", + "repository": { + "type": "gitlab", + "url": "https://github.com/Gongxh0901/kunpolibrary" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "devDependencies": { + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^12.1.2", + "@types/lodash": "^4.17.13", + "@types/node": "^22.10.2", + "rollup": "^4.28.1", + "rollup-plugin-dts": "^6.1.1", + "ts-node": "^10.9.2", + "tslib": "^2.6.2" + } +} diff --git a/rollup.config.mjs b/rollup.config.mjs new file mode 100644 index 0000000..bff0d65 --- /dev/null +++ b/rollup.config.mjs @@ -0,0 +1,86 @@ +import terser from '@rollup/plugin-terser'; +import typescript from '@rollup/plugin-typescript'; +import dts from 'rollup-plugin-dts'; + +export default [ + { + // 生成未压缩的 JS 文件 + input: 'src/kunpocc-behaviortree.ts', + external: ['cc', 'fairygui-cc'], + output: [ + { + file: 'dist/kunpocc-behaviortree.mjs', + format: 'esm', + name: 'kunpocc-behaviortree' + }, + { + file: 'dist/kunpocc-behaviortree.cjs', + format: 'cjs', + name: 'kunpocc-behaviortree' + } + ], + plugins: [ + typescript({ + tsconfig: './tsconfig.json', + importHelpers: false, + compilerOptions: { + target: "es6", + module: "es6", + experimentalDecorators: true, // 启用ES装饰器。 + strict: true, + strictNullChecks: false, + moduleResolution: "Node", + skipLibCheck: true, + esModuleInterop: true, + } + }) + ] + }, + { + // 生成压缩的 JS 文件 + input: 'src/kunpocc-behaviortree.ts', + external: ['cc', 'fairygui-cc'], + output: [ + { + file: 'dist/kunpocc-behaviortree.min.mjs', + format: 'esm', + name: 'kunpocc-behaviortree' + }, + { + file: 'dist/kunpocc-behaviortree.min.cjs', + format: 'cjs', + name: 'kunpocc-behaviortree' + } + ], + plugins: [ + typescript({ + tsconfig: './tsconfig.json', + importHelpers: false, + compilerOptions: { + target: "es6", + module: "es6", + experimentalDecorators: true, // 启用ES装饰器。 + strict: true, + strictNullChecks: false, + moduleResolution: "Node", + skipLibCheck: true, + esModuleInterop: true, + } + }), + terser() + ] + }, + { + // 生成声明文件的配置 + input: 'src/kunpocc-behaviortree.ts', + output: { + file: 'dist/kunpocc-behaviortree.d.ts', + format: 'es' + }, + plugins: [dts({ + compilerOptions: { + stripInternal: true + } + })] + } +]; \ No newline at end of file diff --git a/src/behaviortree/Agent.ts b/src/behaviortree/Agent.ts new file mode 100644 index 0000000..96d0c1e --- /dev/null +++ b/src/behaviortree/Agent.ts @@ -0,0 +1,48 @@ +import { BehaviorTree } from "./BehaviorTree"; +import { Blackboard } from "./Blackboard"; +import { Ticker } from "./Ticker"; + +/** 代理 */ +export class Agent { + /** 行为树 */ + public tree: BehaviorTree; + /** 黑板 */ + public blackboard: Blackboard; + /** 更新器 */ + public ticker: Ticker; + /** + * constructor + * @param subject // 主体 + * @param tree 行为树 + */ + constructor(subject: any, tree: BehaviorTree) { + this.tree = tree; + this.blackboard = new Blackboard(); + this.ticker = new Ticker(subject, this.blackboard, tree); + } + + /** + * 执行 + */ + public tick(): void { + this.tree.tick(this, this.blackboard, this.ticker); + if (this.blackboard.interrupt) { + this.blackboard.interrupt = false; + + let ticker = this.ticker; + ticker.openNodes.length = 0; + ticker.nodeCount = 0; + + this.blackboard.clear(); + } + } + + /** + * 打断行为树,重新开始执行(如果当前在节点中,下一帧才会清理) + */ + public interruptBTree(): void { + if (!this.blackboard.interruptDefend) { + this.blackboard.interrupt = true; + } + } +} \ No newline at end of file diff --git a/src/behaviortree/BTNode/Action.ts b/src/behaviortree/BTNode/Action.ts new file mode 100644 index 0000000..b87b878 --- /dev/null +++ b/src/behaviortree/BTNode/Action.ts @@ -0,0 +1,189 @@ +import { Status } from "../header"; +import { Ticker } from "../Ticker"; +import { BaseNode } from "./BaseNode"; + +/** + * 动作节点 + * 没有子节点 + */ +export abstract class Action extends BaseNode { + constructor() { + super(); + } +} + +/** + * 失败节点(无子节点) + * 直接返回FAILURE + */ +export class Failure extends Action { + /** 执行函数 @internal */ + private _func: () => void; + constructor(func: () => void) { + super(); + this._func = func; + } + + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + this._func(); + return Status.FAILURE; + } +} + +/** + * 逻辑节点,一直执行 (无子节点) + * 直接返回RUNING + */ +export class Running extends Action { + /** 执行函数 @internal */ + private _func: () => void; + constructor(func: () => void) { + super(); + this._func = func; + } + + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + this._func(); + return Status.RUNNING; + } +} + +/** + * 成功节点 无子节点 + * 直接返回SUCCESS + */ +export class Success extends Action { + /** 执行函数 @internal */ + private _func: () => void; + constructor(func: () => void) { + super(); + this._func = func; + } + + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + this._func(); + return Status.SUCCESS; + } +} +/** + * 次数等待节点(无子节点) + * 次数内,返回RUNING + * 超次,返回SUCCESS + */ +export class WaitTicks extends Action { + /** 最大次数 @internal */ + private _maxTicks: number; + /** 经过的次数 @internal */ + private _elapsedTicks: number; + constructor(maxTicks: number = 0) { + super(); + this._maxTicks = maxTicks; + this._elapsedTicks = 0; + } + + /** + * 打开 + * @param {Ticker} ticker + */ + public open(ticker: Ticker): void { + this._elapsedTicks = 0; + } + + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + if (++this._elapsedTicks >= this._maxTicks) { + this._elapsedTicks = 0; + return Status.SUCCESS; + } + return Status.RUNNING; + } +} + +/** + * 时间等待节点(无子节点) + * 时间到后返回SUCCESS,否则返回RUNING + */ +export class WaitTime extends Action { + /** 等待时间(毫秒 ms) @internal */ + private _duration: number; + constructor(duration: number = 0) { + super(); + this._duration = duration * 1000; + } + + /** + * 打开 + * @param {Ticker} ticker + */ + public open(ticker: Ticker): void { + let startTime = new Date().getTime(); + ticker.blackboard.set("startTime", startTime, ticker.tree.id, this.id); + } + + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + let currTime = new Date().getTime(); + let startTime = ticker.blackboard.get("startTime", ticker.tree.id, this.id); + if (currTime - startTime >= this._duration) { + return Status.SUCCESS; + } + return Status.RUNNING; + } +} + +/** + * 行为树防止被打断节点 + * 直接返回 SUCCESS + * 和 InterruptDefendCancel 必须成对出现 + */ +export class InterruptDefend extends Action { + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + ticker.blackboard.interruptDefend = true; + return Status.SUCCESS; + } +} + +/** + * 行为树被打断取消节点 + * 直接返回 SUCCESS + * 和 InterruptDefend 必须成对出现 + */ +export class InterruptDefendCancel extends Action { + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + ticker.blackboard.interruptDefend = false; + return Status.SUCCESS; + } +} \ No newline at end of file diff --git a/src/behaviortree/BTNode/BaseNode.ts b/src/behaviortree/BTNode/BaseNode.ts new file mode 100644 index 0000000..7244fa9 --- /dev/null +++ b/src/behaviortree/BTNode/BaseNode.ts @@ -0,0 +1,113 @@ +import { createUUID, Status } from "../header"; +import { Ticker } from "../Ticker"; + +/** + * 基础节点 + * 所有节点全部继承自 BaseNode + */ +export abstract class BaseNode { + /** 唯一标识 */ + public id: string; + /** 子节点 */ + public children: BaseNode[]; + + /** + * 创建 + * @param children 子节点列表 + */ + constructor(children?: BaseNode[]) { + this.id = createUUID(); + this.children = []; + if (!children) { + return; + } + for (let i = 0; i < children.length; i++) { + this.children.push(children[i]); + } + } + + /** + * 执行节点 + * @param ticker 更新器 + * @returns {Status} 状态 + */ + public _execute(ticker: Ticker): Status { + /* ENTER */ + this._enter(ticker); + if (!ticker.blackboard.get("isOpen", ticker.tree.id, this.id)) { + this._open(ticker); + } + let status = this._tick(ticker); + if (status !== Status.RUNNING) { + this._close(ticker); + } + this._exit(ticker); + return status; + } + + /** + * 进入节点 + * @param ticker 更新器 + * @internal + */ + public _enter(ticker: Ticker): void { + ticker.enterNode(this); + this.enter(ticker); + } + + /** + * 打开节点 + * @param ticker 更新器 + * @internal + */ + public _open(ticker: Ticker): void { + ticker.openNode(this); + ticker.blackboard.set("isOpen", true, ticker.tree.id, this.id); + this.open(ticker); + } + + /** + * 更新节点 + * @param ticker 更新器 + * @internal + */ + public _tick(ticker: Ticker): Status { + ticker.tickNode(this); + return this.tick(ticker); + } + + /** + * 关闭节点 + * @param ticker 更新器 + * @internal + */ + public _close(ticker: Ticker): void { + ticker.closeNode(this); + ticker.blackboard.set("isOpen", false, ticker.tree.id, this.id); + this.close(ticker); + } + + /** + * 退出节点 + * @param ticker 更新器 + * @internal + */ + public _exit(ticker: Ticker): void { + ticker.exitNode(this); + this.exit(ticker); + } + + enter(ticker: Ticker): void { + + } + open(ticker: Ticker): void { + + } + close(ticker: Ticker): void { + + } + exit(ticker: Ticker): void { + + } + abstract tick(ticker: Ticker): Status; +} \ No newline at end of file diff --git a/src/behaviortree/BTNode/Composite.ts b/src/behaviortree/BTNode/Composite.ts new file mode 100644 index 0000000..f59c94c --- /dev/null +++ b/src/behaviortree/BTNode/Composite.ts @@ -0,0 +1,206 @@ +import { Status } from "../header"; +import { Ticker } from "../Ticker"; +import { BaseNode } from "./BaseNode"; + +/** + * 可以包含多个节点的集合装饰器基类 + * + */ +export abstract class Composite extends BaseNode { + constructor(...children: BaseNode[]) { + super(children); + } +} + +/** + * 记忆选择节点 + * 选择不为 FAILURE 的节点 + * 任意一个Child Node返回不为 FAILURE, 本Node向自己的Parent Node也返回Child Node状态 + */ +export class MemSelector extends Composite { + /** + * 打开 + * @param {Ticker} ticker + */ + public open(ticker: Ticker): void { + super.open(ticker); + ticker.blackboard.set("runningChild", 0, ticker.tree.id, this.id); + } + + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + let childIndex = ticker.blackboard.get("runningChild", ticker.tree.id, this.id) as number; + + for (let i = childIndex; i < this.children.length; i++) { + let status = this.children[i]._execute(ticker); + + if (status !== Status.FAILURE) { + if (status === Status.RUNNING) { + ticker.blackboard.set("runningChild", i, ticker.tree.id, this.id); + } + return status; + } + } + + return Status.FAILURE; + } +} + +/** + * 记忆顺序节点 + * 如果上次执行到 RUNING 的节点, 下次进入节点后, 直接从 RUNING 节点开始 + * 遇到 RUNING 或者 FAILURE 停止迭代 + * 任意一个Child Node返回不为 SUCCESS, 本Node向自己的Parent Node也返回Child Node状态 + * 所有节点都返回 SUCCESS, 本节点才返回 SUCCESS + */ +export class MemSequence extends Composite { + /** + * 打开 + * @param {Ticker} ticker + */ + public open(ticker: Ticker): void { + super.open(ticker); + ticker.blackboard.set("runningChild", 0, ticker.tree.id, this.id); + } + + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + let childIndex = ticker.blackboard.get("runningChild", ticker.tree.id, this.id) as number; + for (let i = childIndex; i < this.children.length; i++) { + let status = this.children[i]._execute(ticker); + if (status !== Status.SUCCESS) { + if (status === Status.RUNNING) { + ticker.blackboard.set("runningChild", i, ticker.tree.id, this.id); + } + return status; + } + } + return Status.SUCCESS; + } +} + +/** + * 随机选择节点 + * 从Child Node中随机选择一个执行 + */ +export class RandomSelector extends Composite { + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + let childIndex = (Math.random() * this.children.length) | 0; + let child = this.children[childIndex]; + let status = child._execute(ticker); + + return status; + } +} + +/** + * 选择节点,选择不为 FAILURE 的节点 + * 当执行本类型Node时,它将从begin到end迭代执行自己的Child Node: + * 如遇到一个Child Node执行后返回 SUCCESS 或者 RUNING,那停止迭代,本Node向自己的Parent Node也返回 SUCCESS 或 RUNING + */ +export class Selector extends Composite { + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + for (let i = 0; i < this.children.length; i++) { + let status = this.children[i]._execute(ticker); + if (status !== Status.FAILURE) { + return status; + } + } + return Status.FAILURE; + } +} + +/** + * 顺序节点 + * 当执行本类型Node时,它将从begin到end迭代执行自己的Child Node: + * 遇到 FAILURE 或 RUNING, 那停止迭代,返回FAILURE 或 RUNING + * 所有节点都返回 SUCCESS, 本节点才返回 SUCCESS + */ +export class Sequence extends Composite { + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + for (let i = 0; i < this.children.length; i++) { + let status = this.children[i]._execute(ticker); + if (status !== Status.SUCCESS) { + return status; + } + } + return Status.SUCCESS; + } +} + +/** + * 并行节点 每次进入全部重新执行一遍 + * 当执行本类型Node时,它将从begin到end迭代执行自己的Child Node: + * 1. 当存在Child Node执行后返回 FAILURE, 本节点返回 FAILURE + * 2. 当存在Child Node执行后返回 RUNING, 本节点返回 RUNING + * 所有节点都返回 SUCCESS, 本节点才返回 SUCCESS + */ +export class Parallel extends Composite { + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + let result = Status.SUCCESS; + for (let i = 0; i < this.children.length; i++) { + let status = this.children[i]._execute(ticker); + if (status == Status.FAILURE) { + result = Status.FAILURE; + } else if (result == Status.SUCCESS && status == Status.RUNNING) { + result = Status.RUNNING; + } + } + return result; + } +} + +/** + * 并行节点 每次进入全部重新执行一遍 + * 当执行本类型Node时,它将从begin到end迭代执行自己的Child Node: + * 1. 当存在Child Node执行后返回 FAILURE, 本节点返回 FAILURE + * 2. 任意 Child Node 返回 SUCCESS, 本节点返回 SUCCESS + * 否则返回 RUNNING + */ +export class ParallelAnySuccess extends Composite { + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + let result = Status.RUNNING; + for (let i = 0; i < this.children.length; i++) { + let status = this.children[i]._execute(ticker); + if (status == Status.FAILURE) { + result = Status.FAILURE; + } else if (result == Status.RUNNING && status == Status.SUCCESS) { + result = Status.SUCCESS; + } + } + return result; + } +} \ No newline at end of file diff --git a/src/behaviortree/BTNode/Condition.ts b/src/behaviortree/BTNode/Condition.ts new file mode 100644 index 0000000..9302397 --- /dev/null +++ b/src/behaviortree/BTNode/Condition.ts @@ -0,0 +1,24 @@ +import { Status } from "../header"; +import { Ticker } from "../Ticker"; +import { Action } from "./Action"; + +/** + * 条件节点 + */ +export class Condition extends Action { + /** 执行函数 @internal */ + private _func: (subject: any) => boolean = null; + constructor(func: (subject: any) => boolean) { + super(); + this._func = func; + } + + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + return this._func(ticker.subject) ? Status.SUCCESS : Status.FAILURE; + } +} \ No newline at end of file diff --git a/src/behaviortree/BTNode/Decorator.ts b/src/behaviortree/BTNode/Decorator.ts new file mode 100644 index 0000000..72ebcf9 --- /dev/null +++ b/src/behaviortree/BTNode/Decorator.ts @@ -0,0 +1,367 @@ +import { Status } from "../header"; +import { Ticker } from "../Ticker"; +import { BaseNode } from "./BaseNode"; + +/** + * 修饰节点基类 + * 只能包含一个子节点 + */ +export abstract class Decorator extends BaseNode { + constructor(child: BaseNode) { + super([child]); + } +} + +/** + * 失败节点 + * 必须且只能包含一个子节点 + * 直接返回 FAILURE + * @extends Decorator + */ +export class Failer extends Decorator { + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + if (this.children.length !== 1) { + throw new Error("(Failer)节点必须包含一个子节点"); + } + let child = this.children[0]; + child._execute(ticker); + return Status.FAILURE; + } +} + +/** + * 结果反转节点 + * 必须且只能包含一个子节点 + * 第一个Child Node节点, 返回 FAILURE, 本Node向自己的Parent Node也返回 SUCCESS + * 第一个Child Node节点, 返回 SUCCESS, 本Node向自己的Parent Node也返回 FAILURE + */ +export class Inverter extends Decorator { + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + if (this.children.length !== 1) { + throw new Error("(Inverter)节点必须包含一个子节点"); + } + let child = this.children[0]; + let status = child._execute(ticker); + if (status === Status.SUCCESS) { + status = Status.FAILURE; + } else if (status === Status.FAILURE) { + status = Status.SUCCESS; + } + return status; + } +} + +/** + * 次数限制节点 + * 必须且只能包含一个子节点 + * 次数限制内, 根据Child Node的结果, 本Node向自己的Parent Node也返回相同的结果 + * 次数超过后, 直接返回 FAILURE + */ +export class LimiterTicks extends Decorator { + /** 最大次数 @internal */ + private _maxTicks: number; + /** 当前执行过的次数 @internal */ + private _elapsedTicks: number; + + /** + * 创建 + * @param maxTicks 最大次数 + * @param child 子节点 + */ + constructor(maxTicks: number, child: BaseNode) { + super(child); + this._maxTicks = maxTicks; + this._elapsedTicks = 0; + } + + /** + * 打开 + * @param {Ticker} ticker + */ + public open(ticker: Ticker): void { + super.open(ticker); + this._elapsedTicks = 0; + } + + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + if (this.children.length !== 1) { + throw new Error("(LimiterTicks)节点必须包含一个子节点"); + } + let child = this.children[0]; + if (++this._elapsedTicks > this._maxTicks) { + this._elapsedTicks = 0; + return Status.FAILURE; + } + return child._execute(ticker); + } +} + +/** + * 时间限制节点 + * 只能包含一个子节点 + * 规定时间内, 根据Child Node的结果, 本Node向自己的Parent Node也返回相同的结果 + * 超时后, 直接返回 FAILURE + */ +export class LimiterTime extends Decorator { + /** 最大时间 (毫秒 ms) @internal */ + private _maxTime: number; + + /** + * 时间限制节点 + * @param maxTime 最大时间 (微秒ms) + * @param child 子节点 + */ + constructor(maxTime: number, child: BaseNode) { + super(child); + this._maxTime = maxTime * 1000; + } + + /** + * 打开 + * @param {Ticker} ticker + */ + public open(ticker: Ticker): void { + super.open(ticker); + let startTime = new Date().getTime(); + ticker.blackboard.set("startTime", startTime, ticker.tree.id, this.id); + } + + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + if (this.children.length !== 1) { + throw new Error("(LimiterTime)节点必须包含一个子节点"); + } + + let child = this.children[0]; + let currTime = new Date().getTime(); + let startTime = ticker.blackboard.get("startTime", ticker.tree.id, this.id); + + if (currTime - startTime > this._maxTime) { + return Status.FAILURE; + } + + return child._execute(ticker); + } +} + +/** + * 循环节点 + * 必须且只能包含一个子节点 + * 如果maxLoop < 0, 直接返回成功 + * 否则等待次数超过之后, 返回Child Node的结果(RUNING的次数不计算在内) + */ +export class Repeater extends Decorator { + /** 最大循环次数 @internal */ + private _maxLoop: number; + + /** + * 创建 + * @param child 子节点 + * @param maxLoop 最大循环次数 + */ + constructor(child: BaseNode, maxLoop: number = -1) { + super(child); + this._maxLoop = maxLoop; + } + + /** + * 打开 + * @param {Ticker} ticker + */ + public open(ticker: Ticker): void { + ticker.blackboard.set("i", 0, ticker.tree.id, this.id); + } + + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + if (this.children.length !== 1) { + throw new Error("(Repeater)节点必须包含一个子节点"); + } + + let child = this.children[0]; + let i = ticker.blackboard.get("i", ticker.tree.id, this.id); + let status = Status.SUCCESS; + + while (this._maxLoop < 0 || i < this._maxLoop) { + status = child._execute(ticker); + + if (status === Status.SUCCESS || status === Status.FAILURE) { + i++; + } else { + break; + } + } + + ticker.blackboard.set("i", i, ticker.tree.id, this.id); + return status; + } +} + +/** + * 循环节点 + * 只能包含一个子节点 + * 如果maxLoop < 0, 直接返回成功 + * 当Child Node返回 FAILURE, 本Node向自己的Parent Node返回 FAILURE + * 循环次数大于等于maxLoop时, 返回Child Node的结果 + */ +export class RepeatUntilFailure extends Decorator { + /** 最大循环次数 @internal */ + private _maxLoop: number; + + constructor(child: BaseNode, maxLoop: number = -1) { + super(child); + this._maxLoop = maxLoop; + } + + /** + * 打开 + * @param {Ticker} ticker + */ + public open(ticker: Ticker): void { + ticker.blackboard.set("i", 0, ticker.tree.id, this.id); + } + + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + if (this.children.length !== 1) { + throw new Error("(RepeatUntilFailure)节点必须包含一个子节点"); + } + + let child = this.children[0]; + let i = ticker.blackboard.get("i", ticker.tree.id, this.id); + let status = Status.SUCCESS; + + while (this._maxLoop < 0 || i < this._maxLoop) { + status = child._execute(ticker); + + if (status === Status.SUCCESS) { + i++; + } else { + break; + } + } + + ticker.blackboard.set("i", i, ticker.tree.id, this.id); + return status; + } +} + +/** + * 循环节点(只能包含一个子节点) + * 如果maxLoop < 0, 直接返回失败 + * 当Child Node返回 SUCCESS, 本Node向自己的Parent Node返回 SUCCESS + * 循环次数大于等于maxLoop时, 返回Child Node的结果 + */ +export class RepeatUntilSuccess extends Decorator { + /** 最大循环次数 @internal */ + private _maxLoop: number; + + /** + * 创建 + * @param child 子节点 + * @param maxLoop 最大循环次数 + */ + constructor(child: BaseNode, maxLoop: number = -1) { + super(child); + this._maxLoop = maxLoop; + } + + /** + * 打开 + * @param {Ticker} ticker + */ + public open(ticker: Ticker): void { + ticker.blackboard.set("i", 0, ticker.tree.id, this.id); + } + + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + if (this.children.length !== 1) { + throw new Error("(RepeatUntilSuccess)节点必须包含一个子节点"); + } + let child = this.children[0]; + let i = ticker.blackboard.get("i", ticker.tree.id, this.id); + let status = Status.FAILURE; + while (this._maxLoop < 0 || i < this._maxLoop) { + status = child._execute(ticker); + if (status === Status.FAILURE) { + i++; + } else { + break; + } + } + ticker.blackboard.set("i", i, ticker.tree.id, this.id); + return status; + } +} + +/** + * 逻辑节点, 一直执行(只能包含一个子节点) + * 直接返回 RUNING + */ +export class Runner extends Decorator { + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + if (this.children.length !== 1) { + throw new Error("(Runner)节点必须包含一个子节点"); + } + let child = this.children[0]; + child._execute(ticker); + return Status.RUNNING; + } +} + +/** + * 成功节点(包含一个子节点) + * 直接返回 SUCCESS + */ +export class Succeeder extends Decorator { + /** + * 执行 + * @param {Ticker} ticker + * @returns {Status} + */ + public tick(ticker: Ticker): Status { + if (this.children.length !== 1) { + throw new Error("(Succeeder)节点必须包含一个子节点"); + } + let child = this.children[0]; + child._execute(ticker); + return Status.SUCCESS; + } +} \ No newline at end of file diff --git a/src/behaviortree/BehaviorTree.ts b/src/behaviortree/BehaviorTree.ts new file mode 100644 index 0000000..ad33f81 --- /dev/null +++ b/src/behaviortree/BehaviorTree.ts @@ -0,0 +1,61 @@ +import { Blackboard } from "./Blackboard"; +import { BaseNode } from "./BTNode/BaseNode"; +import { createUUID } from "./header"; +import { Ticker } from "./Ticker"; + +/** + * 行为树 + * 所有节点全部添加到树中 + */ +export class BehaviorTree { + /** 行为树ID @internal */ + private _id: string; + /** 行为树跟节点 @internal */ + private _root: BaseNode; + /** + * constructor + * @param root 根节点 + */ + constructor(root: BaseNode) { + this._id = createUUID(); + this._root = root; + } + + /** + * 执行 + * @param subject 主体 + * @param blackboard 黑板 + * @param ticker 更新器 + */ + public tick(subject: any, blackboard: Blackboard, ticker?: Ticker): void { + ticker = ticker || new Ticker(subject, blackboard, this); + ticker.openNodes.length = 0; + this._root._execute(ticker); + // 上次打开的节点 + let lastOpenNodes = blackboard.get("openNodes", this._id) as BaseNode[]; + // 当前打开的节点 + let currOpenNodes = ticker.openNodes; + let start = 0; + for (let i = 0; i < Math.min(lastOpenNodes.length, currOpenNodes.length); i++) { + start = i + 1; + if (lastOpenNodes[i] !== currOpenNodes[i]) { + break; + } + } + // 关闭不需要的节点 + for (let i = lastOpenNodes.length - 1; i >= start; i--) { + lastOpenNodes[i]._close(ticker); + } + /* POPULATE BLACKBOARD */ + blackboard.set("openNodes", currOpenNodes, this._id); + blackboard.set("nodeCount", ticker.nodeCount, this._id); + } + + get id(): string { + return this._id; + } + + get root(): BaseNode { + return this._root; + } +} \ No newline at end of file diff --git a/src/behaviortree/Blackboard.ts b/src/behaviortree/Blackboard.ts new file mode 100644 index 0000000..6fcec06 --- /dev/null +++ b/src/behaviortree/Blackboard.ts @@ -0,0 +1,105 @@ +/** + * 行为树数据 + */ +interface ITreeData { + nodeMemory: { [nodeScope: string]: any }; + openNodes: any[]; +} + +/** 平台 */ +export class Blackboard { + /** 行为树打断保护 */ + public interruptDefend: boolean = false; + /** 打断行为树的标记 */ + public interrupt: boolean = false; + /** 基础记忆 @internal */ + private _baseMemory: any; + /** 树记忆 @internal */ + private _treeMemory: { [treeScope: string]: ITreeData }; + + constructor() { + this._baseMemory = {}; + this._treeMemory = {}; + } + + /** + * 清除 + */ + public clear(): void { + this._baseMemory = {}; + this._treeMemory = {}; + } + + /** + * 设置 + * @param key 键 + * @param value 值 + * @param treeScope 树范围 + * @param nodeScope 节点范围 + */ + public set(key: string, value: any, treeScope?: string, nodeScope?: string): void { + let memory = this._getMemory(treeScope, nodeScope); + memory[key] = value; + } + + /** + * 获取 + * @param key 键 + * @param treeScope 树范围 + * @param nodeScope 节点范围 + * @returns 值 + */ + public get(key: string, treeScope?: string, nodeScope?: string): any { + let memory = this._getMemory(treeScope, nodeScope); + return memory[key]; + } + + /** + * 获取树记忆 + * @param treeScope 树范围 + * @returns 树记忆 + * @internal + */ + private _getTreeMemory(treeScope: string): ITreeData { + if (!this._treeMemory[treeScope]) { + this._treeMemory[treeScope] = { + nodeMemory: {}, + openNodes: [], + }; + } + return this._treeMemory[treeScope]; + } + + /** + * 获取节点记忆 + * @param treeMemory 树记忆 + * @param nodeScope 节点范围 + * @returns 节点记忆 + * @internal + */ + private _getNodeMemory(treeMemory: ITreeData, nodeScope: string): { [key: string]: any } { + let memory = treeMemory.nodeMemory; + if (!memory[nodeScope]) { + memory[nodeScope] = {}; + } + return memory[nodeScope]; + } + + /** + * 获取记忆 + * @param treeScope 树范围 + * @param nodeScope 节点范围 + * @returns 记忆 + * @internal + */ + private _getMemory(treeScope?: string, nodeScope?: string): { [key: string]: any } { + let memory = this._baseMemory; + if (treeScope) { + memory = this._getTreeMemory(treeScope); + if (nodeScope) { + memory = this._getNodeMemory(memory, nodeScope); + } + } + return memory; + } +} \ No newline at end of file diff --git a/src/behaviortree/Ticker.ts b/src/behaviortree/Ticker.ts new file mode 100644 index 0000000..27e3728 --- /dev/null +++ b/src/behaviortree/Ticker.ts @@ -0,0 +1,55 @@ +import { BehaviorTree } from "./BehaviorTree"; +import { Blackboard } from "./Blackboard"; +import { BaseNode } from "./BTNode/BaseNode"; + +export class Ticker { + tree: BehaviorTree; // 行为树跟节点 + openNodes: BaseNode[]; // 当前打开的节点 + nodeCount: number; // 当前打开的节点数量 + blackboard: Blackboard; // 数据容器 + debug: any; + subject: any; + constructor(subject: any, blackboard: Blackboard, tree: BehaviorTree) { + this.tree = tree; + this.openNodes = []; + this.nodeCount = 0; + this.debug = null; + this.subject = subject; + this.blackboard = blackboard; + } + + /** + * 进入节点 + * @param node 节点 + */ + public enterNode(node: BaseNode): void { + this.nodeCount++; + this.openNodes.push(node); + } + + /** + * 打开节点 + * @param node 节点 + */ + public openNode(node: BaseNode): void { } + + /** + * 更新节点 + * @param node 节点 + */ + public tickNode(node: BaseNode): void { } + + /** + * 关闭节点 + * @param node 节点 + */ + public closeNode(node: BaseNode): void { + this.openNodes.pop(); + } + + /** + * 退出节点 + * @param node 节点 + */ + public exitNode(node: BaseNode): void { } +} \ No newline at end of file diff --git a/src/behaviortree/header.ts b/src/behaviortree/header.ts new file mode 100644 index 0000000..458bb68 --- /dev/null +++ b/src/behaviortree/header.ts @@ -0,0 +1,27 @@ +export const enum Status { + FAILURE, + SUCCESS, + RUNNING, +} + +/** + * 创建UUID + * @returns UUID + * @internal + */ +export function createUUID(): string { + let s: string[] = Array(36); + let hexDigits = "0123456789abcdef"; + for (let i = 0; i < 36; i++) { + let start = Math.floor(Math.random() * 0x10); + s[i] = hexDigits.substring(start, start + 1); + } + // bits 12-15 of the time_hi_and_version field to 0010 + s[14] = "4"; + // bits 6-7 of the clock_seq_hi_and_reserved to 01 + let start = (parseInt(s[19], 16) & 0x3) | 0x8; + s[19] = hexDigits.substring(start, start + 1); + s[8] = s[13] = s[18] = s[23] = "-"; + let uuid = s.join(""); + return uuid; +} \ No newline at end of file diff --git a/src/kunpocc-behaviortree.ts b/src/kunpocc-behaviortree.ts new file mode 100644 index 0000000..57aded1 --- /dev/null +++ b/src/kunpocc-behaviortree.ts @@ -0,0 +1,14 @@ + +/** 行为树 */ +export { Agent as Agent } from "./behaviortree/Agent"; +export { BehaviorTree } from "./behaviortree/BehaviorTree"; +export { Blackboard } from "./behaviortree/Blackboard"; +export * as Action from "./behaviortree/BTNode/Action"; +export { BaseNode as Node } from "./behaviortree/BTNode/BaseNode"; +export * as Composite from "./behaviortree/BTNode/Composite"; + +export { Condition } from "./behaviortree/BTNode/Condition"; +export * as Decorator from "./behaviortree/BTNode/Decorator"; +export { Status } from "./behaviortree/header"; +export { Ticker } from "./behaviortree/Ticker"; + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2b43378 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "es6", // + "module": "commonjs", // + "experimentalDecorators": true, // 启用ES装饰器。 + "strict": true, + "strictNullChecks": false, + "moduleResolution": "Node", + "skipLibCheck": true, + "esModuleInterop": true, + "stripInternal": true, + "types": [] + }, + "include": [ + "./src/**/*" + // "libs" + ], + // 排除 + "exclude": [ + "node_modules", + "dist", + "build" + ] +} \ No newline at end of file