This commit is contained in:
PC-20230316NUNE\Administrator
2023-10-23 18:56:01 +08:00
parent ac94959a45
commit 77d44ee300
328 changed files with 16429 additions and 2 deletions

View File

@@ -0,0 +1,16 @@
/*
* @Author: OreoWang
* @Email: ihc523@163.com
* @Date: 2022-04-07 14:12:09
* @LastEditors: OreoWang
* @LastEditTime: 2022-04-25 09:47:07
* @Description: 用于在 Inspector 中显示编辑按钮
*/
import { _decorator, Component } from "cc";
const {ccclass, disallowMultiple} = _decorator;
@ccclass("BehaviorButton")
@disallowMultiple
export default class BehaviorButton extends Component {
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "57329531-3fd5-4e01-af0c-7224cf8483a6",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,71 @@
/*
* @Author: OreoWang
* @Email: ihc523@163.com
* @Date: 2022-04-07 14:12:09
* @LastEditors: OreoWang
* @LastEditTime: 2022-04-26 10:51:23
* @Description: 行为树日志输出选项
*/
import { _decorator, Component } from "cc";
const { ccclass, property, requireComponent, disallowMultiple } = _decorator;
import { IBehaviorLogOptions } from "../core/behavior/behavior-tree-interface";
import BehaviorButton from "./BehaviorButton";
export const DefaultLogOptions: IBehaviorLogOptions = {
logAbort: true,
logInterrupt: true,
logExecute: true,
logUpdate: false,
logEnter: false,
logExit: false,
logEnable: false,
logDisable: false,
logLoad: false,
logDestroy: false,
}
@ccclass("BehaviorLogOptions")
@requireComponent(BehaviorButton)
@disallowMultiple
export default class BehaviorLogOptions extends Component implements IBehaviorLogOptions {
@property({
tooltip: "当任务被中止时是否打印日志",
})
logAbort: boolean = true;
@property({
tooltip: "当中断产生时是否打印日志",
})
logInterrupt: boolean = true;
@property
logExecute: boolean = true;
@property
logUpdate: boolean = false;
@property
logLoad: boolean = false;
@property
logDestroy: boolean = false;
@property
logEnter: boolean = false;
@property
logExit: boolean = false;
@property
logEnable: boolean = false;
@property
logDisable: boolean = false;
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "0047be34-4c09-4525-a687-e428f9a9b00f",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,190 @@
/*
* @Author: OreoWang
* @Email: ihc523@163.com
* @Date: 2022-03-22 21:50:54
* @LastEditors: OreoWang
* @LastEditTime: 2022-07-15 09:30:18
* @Description:
*/
import { game, _decorator } from "cc";
import * as core from "../core/main";
import { BehaviorStatus } from "../core/main";
const logger = console;
export default class BehaviorManager {
/** 运行 */
running: Set<core.IBehaviorTree> = new Set();
/** 挂起 */
suspend: Set<core.IBehaviorTree> = new Set();
/**
* 行为树统一帧率
* 默认为 cc.game.frameRate
*/
private frameRate = 0;
/** 期望帧率对应的每帧时间(以 s 为单位) */
private frameTime = 0;
/** 每帧时间增量 */
private deltaTime = 0;
// /** 行为树持续时间(是每帧时间增量叠加后的时间总和) */
// private duration = 0;
// /** 行为树 tick 次数 */
// private ticks = 0;
protected static _instance: BehaviorManager = null;
static getInstance() {
if (!BehaviorManager._instance) {
BehaviorManager._instance = new BehaviorManager();
}
return BehaviorManager._instance;
}
static deleteInstance() {
BehaviorManager._instance = null;
}
constructor() {
// this.duration = 0
// this.ticks = 0
this.setFrameRate(Number(game.frameRate));
}
getFrameRate(){
return this.frameRate;
}
/**
* 设置行为树执行帧率但真正的FPS还取决于 cc.game.frameRate
* 注意:行为树的帧率不会比 cc.game.frameRate 大
* @param frameRate
*/
setFrameRate(frameRate: number){
const gRate = Number(game.frameRate);
if(frameRate <= 0 || frameRate > gRate){
logger.warn("Invalid frame rate!");
frameRate = gRate;
}
if(this.frameRate != frameRate){
this.frameRate = frameRate;
this.frameTime = (1000 / this.frameRate) / 1000 - 0.0001;
this.running.forEach(context=>{
if(context.getFrameRate() > this.frameRate){
context.setFrameRate(this.frameRate);
}
})
this.suspend.forEach(context => {
if(context.getFrameRate() > this.frameRate){
context.setFrameRate(this.frameRate);
}
})
}
}
/**
* 将行为树添加到 running 集合中
* @param context
*/
runBehavior(context: core.IBehaviorTree) {
this.resumeBehavior(context);
}
/**
* 将行为树添加到集合中
* @description context.startWhenEnabled 为 true 时,行为树添加到 running 集合,否则添加到 suspend 集合
* @param context
*/
addBehavior(context) {
if (context.startWhenEnabled) {
this.resumeBehavior(context);
}
else {
this.pauseBehavior(context);
}
}
removeBehavior(context: core.IBehaviorTree) {
if (this.running.has(context)) {
this.running.delete(context);
}
if (this.suspend.has(context)) {
this.suspend.delete(context);
}
}
pauseBehavior(context: core.IBehaviorTree) {
if (!this.suspend.has(context)) {
context.onPause();
this.suspend.add(context);
}
if (this.running.has(context)) {
this.running.delete(context);
}
}
resumeBehavior(context: core.IBehaviorTree) {
if (!this.running.has(context)) {
context.onResume();
this.running.add(context);
}
if (this.suspend.has(context)) {
this.suspend.delete(context);
}
}
stopBehavior(context: core.IBehaviorTree) {
if (this.running.has(context)) {
context.onStop();
this.running.delete(context);
}
if (!this.suspend.has(context)) {
this.suspend.add(context);
}
}
/**
* 更新状态
* @param {*} delta 上一次tick时间间隔
*/
tick(delta: number) {
// this.duration += delta
// this.ticks += 1
this.running.forEach(context => {
const status = context.onTick(delta);
if (status != BehaviorStatus.Running) {
if (context.restartWhenComplete) {
context.onRestart();
}
else {
context.onFinished();
this.stopBehavior(context);
}
}
})
}
update(delta: number) {
this.deltaTime += delta;
if(this.deltaTime < this.frameTime){
return;
}
this.tick(delta);
this.deltaTime -= this.frameTime;
}
onEnable() {
this.suspend.forEach(context => {
if (context.isSuspended) {
this.resumeBehavior(context);
}
})
}
onDisable() {
this.running.forEach(context => {
if (context.pauseWhenDisabled) {
this.pauseBehavior(context);
}
})
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "aadd8789-6a9d-43da-aa6a-08a96c22acea",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,356 @@
/*
* @Author: OreoWang
* @Email: ihc523@163.com
* @Date: 2022-04-07 14:12:09
* @LastEditors: OreoWang
* @LastEditTime: 2022-04-26 10:51:23
* @Description:
*/
import { _decorator, Component, JsonAsset } from "cc";
import { DEV } from "cc/env";
const { ccclass, property, requireComponent, disallowMultiple } = _decorator;
import * as btNode from "../node/main";
import * as btCore from "../core/main";
import { BehaviorEntity, IBehaviorTree, Blackboard, logger, BehaviorStatus, TTreeAsset, BehaviorNode, BehaviorTask, BehaviorEventTarget, TBehaviorTreeEventInterface } from "../core/main";
import BehaviorLogOptions, { DefaultLogOptions } from "./BehaviorLogOptions";
import BehaviorManager from "./BehaviorManager";
import { game } from "cc";
@ccclass("BehaviorTree")
// @requireComponent(BehaviorButton)
@requireComponent(BehaviorLogOptions)
@disallowMultiple
export default class BehaviorTree extends Component implements IBehaviorTree {
@property({
type: JsonAsset,
tooltip: "绑定行为树编辑器数据资源"
})
jsonAsset: JsonAsset = null;
@property({
tooltip: `设置当前行为树执行帧率,为 0 表示与 BehaviorManager.frameRate 保持一致。
如需统一设置帧率,可以使用 BehaviorManager.getInstance().setFrameRate(rate)。
注意:行为树的帧率不会比 cc.game.frameRate 大`,
min: 0,
step: 1,
})
frameRate = 0;
@property({
tooltip: "节点激活时开始运行"
})
startWhenEnabled = true;
@property({
tooltip: "节点禁用时暂停运行"
})
pauseWhenDisabled = false;
@property({
tooltip: "当一次行为树全部结束时,重新开始执行该行为树"
})
restartWhenComplete = false;
@property({
tooltip: "当重新开始执行行为树时,重置各节点数据"
})
resetValuesOnRestart = false;
@property({
tooltip: "当行为树状态变动时输出日志"
})
logTaskChanges = false;
/**
* 行为树事件委托对象
* 事件类型详见: IBehaviorTreeEventInterface
*/
delegate: BehaviorEventTarget<TBehaviorTreeEventInterface> = new BehaviorEventTarget<TBehaviorTreeEventInterface>();
/** 行为树执行日志粒度控制 */
logOptions: BehaviorLogOptions = null;
/** 行为树持续时间(是每帧时间增量叠加后的时间总和) */
duration = 0;
/** 行为树 tick 总次数 */
ticks = 0;
tickLoggers: Array<string> = [];
lastLoggers: Array<string> = [];
/** 所有节点(包括组合节点、任务节点、装饰器等所有节点) */
allNodes: Array<BehaviorNode> = [];
/** 所有任务节点(key为该节点在行为树编辑器中对应的序号) */
allTasks: Array<BehaviorTask> = [];
/** 行为树使用的黑板变量 */
blackboard: Blackboard = null;
/** 行为树当前状态 */
status: BehaviorStatus = BehaviorStatus.None;
/** 行为树是否已挂起 */
isSuspended: boolean = false;
/** 行为树是否已执行结束 */
isCompleted: boolean = false;
protected _inited = false;
// protected _utils: BehaviorTreeUtils = null;
protected _root: BehaviorEntity = null;
protected get _manager() {
return BehaviorManager.getInstance();
}
onLoad() {
if (!this.jsonAsset?.json) return;
this.reuse();
}
onDestroy() {
this.unuse();
}
unuse(){
if(!this._inited){
return;
}
this._inited = false;
this.status = BehaviorStatus.None;
this.duration = 0;
this.ticks = 0;
this.allNodes.length = 0;
this.allTasks.length = 0;
this.tickLoggers.length = 0;
this.lastLoggers.length = 0;
this.isSuspended = false;
this.isCompleted = false;
if (this._root) {
this._root.destroy();
this._root = null;
}
if (this.blackboard) {
this.blackboard.destroy();
this.blackboard = null;
}
this._manager.removeBehavior(this);
}
reuse(){
this.loadJsonAsset(this.jsonAsset);
}
loadJsonAsset(jsonAsset: JsonAsset){
if(!jsonAsset || !jsonAsset.json) return;
if(this._inited) return;
this._inited = true;
this.setFrameRate(this.frameRate);
this.jsonAsset = jsonAsset;
this.logOptions = this.getComponent(BehaviorLogOptions);
if(!this.logOptions){
this.logOptions = this.addComponent(BehaviorLogOptions);
for (const key in DefaultLogOptions) {
this.logOptions[key] = !!DefaultLogOptions[key];
}
}
const jsonObect = jsonAsset.json;
const json: TTreeAsset = jsonObect as TTreeAsset;
this.blackboard = new Blackboard(this, json.blackboard);
if (this.loadTree(json)) {
this._manager.addBehavior(this);
}
}
private loadTree(tree: TTreeAsset) {
if (!tree?.root) {
logger.error('load failed -- tree is invalid')
return false;
}
this._root = null;
this.allNodes.length = 0;
this.allTasks.length = 0;
this.tickLoggers.length = 0;
this.lastLoggers.length = 0;
let successs = false;
this.delegate.emit("onDeserializeBefore");
// 创建根节点
const options = tree.root.config?.label || {} as btCore.ILabelConfig;
if (options.uuid) {
const instance: BehaviorEntity = btCore.deserializeNode(null, tree.root, this);
if (instance) {
this._root = instance
successs = true;
}
else {
logger.error("Can't find class by uuid: ", options.uuid);
}
}
this.delegate.emit("onDeserializeAfter");
return successs;
}
getFrameRate(){
return this.frameRate;
}
/**
* 设置当前行为树执行帧率但真正的FPS还取决于 BehaviorManager.frameRate 和 cc.game.frameRate
* 如需统一设置帧率,可以使用 BehaviorManager.getInstance().setFrameRate(rate)
* 注意:行为树的帧率不会比 cc.game.frameRate 大
* @param frameRate
*/
setFrameRate(frameRate: number){
const mRate = this._manager.getFrameRate();
if(frameRate <= 0 || frameRate > mRate){
if(frameRate != 0){
logger.warn(`Invalid frame rate! tree.frameRate=${frameRate}, manage.frameRate=${mRate}, game.frameRate=${game.frameRate}`);
}
frameRate = mRate;
}
this.frameRate = frameRate;
this.frameTime = (1000 / this.frameRate) / 1000 - 0.0001;
}
/** 期望帧率对应的每帧时间(以 s 为单位) */
private frameTime = 0;
/**
* 行为树反序列化每个节点回调
* @param node
*/
onDeserialize(node: BehaviorNode) {
this.allNodes.push(node);
if(node instanceof btNode.Task){
/** 开发环境下,检查任务节点的 tag 是否有重复。一般来说tag 都是唯一的 */
if(DEV){
const {tag, order} = node.nodeConfig;
if(!!tag){
let temp = this.getTask(tag);
if(temp){
console.warn(`The node has duplicate tags. tag:${tag}, orders:[${temp.nodeConfig.order}, ${order}]`)
}
}
}
this.allTasks.push(node);
}
this.delegate.emit("onDeserialize", node);
}
/**
* 行为树根节点
* @returns
*/
getRoot(): btCore.BehaviorEntity | null{
return this._root;
}
/**
* 根据指定的 tag 获取某个任务
* @param tag string
* @returns
*/
getTask(tag: string): btNode.Task | null{
let node = this.allTasks.find(v=>v.nodeConfig.tag == tag);
return node;
}
/**
* 根据任务在行为树中的序号获取某个任务
* @param order number
* @returns
*/
getTaskByOrder(order: number): btNode.Task | null{
let node = this.allTasks.find(v=>v.nodeConfig.order == order);
return node;
}
/** 行为树组件被附加到的 cc.Node 节点 */
getTargetRoot() {
return this.node;
}
/**
* 根据子节点路径获取 cc.Node 节点
* @param path
* @returns
*/
getTargetByPath(path: string){
return btCore.getTargetByPath(this.node, path);
}
/** 某个类型的 log 是否启用 */
isLogEnabled(key: btCore.TBehaviorLogKey): boolean {
return this.logOptions[key];
}
private _isLogTaskChanged: boolean = false;
onHandleTreeLog(msg: string){
if(this.logTaskChanges){
if(!this._isLogTaskChanged){
let length = this.tickLoggers.length;
if(this.lastLoggers.length > length){
let temp = this.lastLoggers[length];
if(temp != msg){
this._isLogTaskChanged = true;
}
}
else{
this._isLogTaskChanged = true;
}
}
this.tickLoggers.push(msg);
}
}
onTick(delta: number) {
if (!this._root) {
logger.error('tick failed -- root is null')
return BehaviorStatus.None;
}
this.duration += delta;
this.ticks += 1;
this.tickLoggers.length = 0;
this._isLogTaskChanged = false;
this.status = this._root.execute();
if(this._isLogTaskChanged){
let msg = `[ BehaviorTree - <${this._root.nodeTitle}> onTick(${this.ticks}) : status = ${BehaviorStatus[this.status]} ]\n` + this.tickLoggers.join("\n");
logger.log(msg);
this.lastLoggers = this.tickLoggers;
this.tickLoggers = [];
}
// this.deltaTime -= this.frameTime;
return this.status;
}
onFinished() {
this.isCompleted = true;
}
onRestart() {
this.isCompleted = false;
if (this.resetValuesOnRestart) {
this._root.reset();
}
}
onPause() {
this.isSuspended = true;
};
onResume() {
this.isSuspended = false;
};
onStop() {
this.isSuspended = false;
this.isCompleted = false;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "e021fcd1-5fdd-4265-b7e2-b1677d99aa04",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,12 @@
{
"ver": "1.1.0",
"importer": "directory",
"imported": true,
"uuid": "b984fcf4-64af-453f-b850-c2a4a8c63935",
"files": [],
"subMetas": {},
"userData": {
"compressionType": {},
"isRemoteBundle": {}
}
}

View File

@@ -0,0 +1,12 @@
{
"ver": "1.1.0",
"importer": "directory",
"imported": true,
"uuid": "1ccd7aaf-8fa6-4e7d-9f9f-f1f3efa54f09",
"files": [],
"subMetas": {},
"userData": {
"compressionType": {},
"isRemoteBundle": {}
}
}

View File

@@ -0,0 +1,12 @@
{
"ver": "1.1.0",
"importer": "directory",
"imported": true,
"uuid": "896d1aa0-bf42-4a94-a570-b3d697ce5c90",
"files": [],
"subMetas": {},
"userData": {
"compressionType": {},
"isRemoteBundle": {}
}
}

View File

@@ -0,0 +1,12 @@
{
"ver": "1.1.0",
"importer": "directory",
"imported": true,
"uuid": "9a75f94e-4879-4193-bf1f-068858fa88e6",
"files": [],
"subMetas": {},
"userData": {
"compressionType": {},
"isRemoteBundle": {}
}
}