1.组合节点全部添加记忆功能;

2.优化并行节点和随机选择节点的逻辑;
3.修复随机选择节点和并行节点关闭时,子节点不能正确关闭的bug
This commit is contained in:
gongxh
2025-10-17 11:37:53 +08:00
parent 99ba5a210b
commit 3bd4fc25ab
18 changed files with 8827 additions and 84 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,286 @@
juese1.png
size: 1024,1024
format: RGBA8888
filter: Linear,Linear
repeat: none
右手_1
rotate: true
xy: 415, 448
size: 57, 125
orig: 57, 125
offset: 0, 0
index: -1
右眼1
rotate: true
xy: 417, 110
size: 26, 36
orig: 28, 38
offset: 1, 1
index: -1
右眼2
rotate: false
xy: 230, 326
size: 40, 49
orig: 42, 51
offset: 1, 1
index: -1
右眼上
rotate: false
xy: 377, 648
size: 59, 25
orig: 59, 25
offset: 0, 0
index: -1
哭嘴巴
rotate: false
xy: 367, 110
size: 48, 26
orig: 48, 26
offset: 0, 0
index: -1
哭眼睛
rotate: false
xy: 2, 5
size: 42, 20
orig: 42, 20
offset: 0, 0
index: -1
嘴巴
rotate: true
xy: 199, 4
size: 28, 7
orig: 30, 9
offset: 1, 1
index: -1
嘴巴2
rotate: false
xy: 308, 2
size: 34, 18
orig: 34, 18
offset: 0, 0
index: -1
rotate: true
xy: 205, 108
size: 213, 160
orig: 215, 162
offset: 1, 1
index: -1
头发1
rotate: true
xy: 363, 333
size: 112, 107
orig: 112, 107
offset: 0, 0
index: -1
头发2
rotate: false
xy: 455, 8
size: 147, 128
orig: 147, 128
offset: 0, 0
index: -1
头发3
rotate: true
xy: 367, 138
size: 193, 110
orig: 195, 112
offset: 1, 1
index: -1
左眼1
rotate: false
xy: 438, 651
size: 26, 36
orig: 28, 38
offset: 1, 1
index: -1
左眼2
rotate: false
xy: 482, 608
size: 40, 48
orig: 42, 50
offset: 1, 1
index: -1
左眼上
rotate: true
xy: 203, 323
size: 52, 25
orig: 52, 25
offset: 0, 0
index: -1
扫把
rotate: false
xy: 61, 34
size: 142, 287
orig: 142, 287
offset: 0, 0
index: -1
抹布
rotate: true
xy: 377, 842
size: 179, 175
orig: 179, 175
offset: 0, 0
index: -1
rotate: false
xy: 391, 11
size: 53, 13
orig: 53, 13
offset: 0, 0
index: -1
收音机1
rotate: false
xy: 336, 331
size: 25, 114
orig: 25, 114
offset: 0, 0
index: -1
收音机2
rotate: true
xy: 377, 675
size: 165, 47
orig: 165, 47
offset: 0, 0
index: -1
收音机3
rotate: true
xy: 46, 7
size: 18, 151
orig: 18, 151
offset: 0, 0
index: -1
椅子
rotate: true
xy: 208, 7
size: 99, 62
orig: 99, 62
offset: 0, 0
index: -1
水印
rotate: false
xy: 2, 323
size: 199, 323
orig: 199, 323
offset: 0, 0
index: -1
白眼
rotate: false
xy: 482, 513
size: 45, 45
orig: 45, 45
offset: 0, 0
index: -1
相机
rotate: true
xy: 426, 689
size: 151, 108
orig: 151, 108
offset: 0, 0
index: -1
相机2
rotate: false
xy: 262, 447
size: 151, 199
orig: 151, 199
offset: 0, 0
index: -1
相机光
rotate: false
xy: 2, 648
size: 373, 373
orig: 373, 373
offset: 0, 0
index: -1
眉毛右
rotate: false
xy: 466, 673
size: 42, 14
orig: 44, 16
offset: 1, 1
index: -1
眉毛左
rotate: false
xy: 466, 658
size: 40, 13
orig: 42, 15
offset: 1, 1
index: -1
眉毛皱眉
rotate: false
xy: 344, 5
size: 45, 15
orig: 45, 15
offset: 0, 0
index: -1
眼泪1
rotate: true
xy: 2, 27
size: 294, 57
orig: 294, 57
offset: 0, 0
index: -1
眼泪2
rotate: true
xy: 203, 377
size: 269, 57
orig: 269, 57
offset: 0, 0
index: -1
眼泪3
rotate: false
xy: 308, 22
size: 58, 84
orig: 58, 84
offset: 0, 0
index: -1
组 4
rotate: false
xy: 368, 26
size: 75, 82
orig: 77, 84
offset: 1, 1
index: -1
腿_1
rotate: false
xy: 272, 14
size: 34, 92
orig: 34, 92
offset: 0, 0
index: -1
话筒
rotate: false
xy: 272, 323
size: 62, 122
orig: 64, 124
offset: 1, 1
index: -1
身体
rotate: false
xy: 472, 369
size: 73, 77
orig: 75, 79
offset: 1, 1
index: -1
阴影
rotate: true
xy: 415, 507
size: 139, 65
orig: 141, 67
offset: 1, 1
index: -1
领结
rotate: true
xy: 482, 560
size: 46, 40
orig: 48, 42
offset: 1, 1
index: -1
鼻子
rotate: true
xy: 272, 4
size: 8, 12
orig: 10, 14
offset: 1, 1
index: -1

View File

@@ -0,0 +1,12 @@
{
"ver": "1.0.0",
"importer": "*",
"imported": true,
"uuid": "2a140e27-a3cf-4808-87f1-234d62f6a3af",
"files": [
".atlas",
".json"
],
"subMetas": {},
"userData": {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
{
"ver": "1.2.7",
"importer": "spine-data",
"imported": true,
"uuid": "6aa6ec55-2434-467e-becb-934b6411ff37",
"files": [
".json"
],
"subMetas": {},
"userData": {
"atlasUuid": "2a140e27-a3cf-4808-87f1-234d62f6a3af"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -0,0 +1,42 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "f3e02866-6afe-4e54-aea5-d742ce999083",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "f3e02866-6afe-4e54-aea5-d742ce999083@6c48a",
"displayName": "juese1",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "repeat",
"wrapModeT": "repeat",
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0,
"isUuid": true,
"imageUuidOrDatabaseUri": "f3e02866-6afe-4e54-aea5-d742ce999083",
"visible": false
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "texture",
"hasAlpha": true,
"fixAlphaTransparencyArtifacts": false,
"redirect": "f3e02866-6afe-4e54-aea5-d742ce999083@6c48a"
}
}

View File

@@ -202,7 +202,10 @@
"skeleton": {
"__id__": 6
},
"btConfig": null,
"btConfig": {
"__uuid__": "c8aeef5d-6d0e-4093-848e-7d8f1ca30261",
"__expectedType__": "cc.JsonAsset"
},
"_id": "69LhmWaZRIUpmYvdiN82Ha"
},
{
@@ -226,11 +229,11 @@
"a": 255
},
"_skeletonData": {
"__uuid__": "39a7d8cd-533a-479a-b909-9575bf720338",
"__uuid__": "6aa6ec55-2434-467e-becb-934b6411ff37",
"__expectedType__": "sp.SkeletonData"
},
"defaultSkin": "default",
"defaultAnimation": "idle",
"defaultAnimation": "zoulu",
"_premultipliedAlpha": true,
"_timeScale": 1,
"_preCacheMode": 0,
@@ -304,13 +307,13 @@
"__prefab": null,
"_contentSize": {
"__type__": "cc.Size",
"width": 419.8399963378906,
"height": 686.0800170898438
"width": 606,
"height": 398.8900146484375
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.45412539378136013,
"y": 0.011660447470739235
"x": 0.2154950434618657,
"y": 0.07997191819064561
},
"_id": "c9XaAZS6pNILxWx4jmSYKE"
},

View File

@@ -116,3 +116,16 @@ export class BTCondition extends BT.ConditionDecorator {
return Math.random() > this._value;
}
}
//////////////////////////自定义行为树////////////////////////////////////
/** 爆发状态条件节点 */
@BT.ClassCondition("BTConditionTest", { name: "爆发状态条件节点", group: "基础条件节点", desc: "" })
export class BTConditionInCrit extends BT.Condition {
public isEligible(): boolean {
let isCrit: boolean = this.get("isCrit")
// console.log("是否处于爆发状态?", isCrit)
return isCrit;
}
}

View File

@@ -13,7 +13,7 @@ export class GameEntry extends Component {
private _tree: BT.BehaviorTree<sp.Skeleton> = null;
start(): void {
console.log("btConfig", this.btConfig);
let btTree1: BT.INodeConfig[] = this.btConfig.json["bt-tree1"]
let btTree1: BT.INodeConfig[] = this.btConfig.json["actor2"]
this._tree = BT.createBehaviorTree(btTree1, this.skeleton);
}

View File

@@ -0,0 +1,481 @@
{
"name": "actor2",
"description": "",
"nodes": [
{
"id": "1760666774273_6twatctr8",
"className": "Selector",
"name": "选择节点",
"position": {
"x": -60,
"y": -220
},
"parameters": {},
"children": [
"1760666778409_kl1cri6xp",
"1760668712628_4untk0pc1",
"1760668771864_xs1f57m7t"
]
},
{
"id": "1760666778409_kl1cri6xp",
"className": "Sequence",
"name": "顺序节点",
"position": {
"x": -620,
"y": -120
},
"parameters": {},
"children": [
"1760666838184_0sf89zrd4",
"1760666881111_eu9y7q5m6"
],
"alias": "待机顺序节点"
},
{
"id": "1760666838184_0sf89zrd4",
"className": "BTConditionRandom",
"name": "随机条件节点",
"position": {
"x": -680,
"y": -60
},
"parameters": {
"_value": 0.6
},
"children": []
},
{
"id": "1760666881111_eu9y7q5m6",
"className": "LimitTime",
"name": "时间限制器",
"position": {
"x": -540,
"y": -40
},
"parameters": {
"_max": 4
},
"children": [
"1760666881111_qoyqb9mkm"
]
},
{
"id": "1760666881111_qoyqb9mkm",
"className": "BTAnimation",
"name": "播放动画",
"position": {
"x": -540,
"y": 40
},
"parameters": {
"_name": "daiji",
"_loop": true
},
"children": []
},
{
"id": "1760668712628_4untk0pc1",
"className": "Sequence",
"name": "顺序节点",
"position": {
"x": -340,
"y": -120
},
"parameters": {},
"children": [
"1760668729192_kuio9twvr",
"1760668729192_pvcbrqpga"
],
"alias": "走路顺序节点"
},
{
"id": "1760668729192_kuio9twvr",
"className": "BTConditionRandom",
"name": "随机条件节点",
"position": {
"x": -400,
"y": -60
},
"parameters": {
"_value": 0.6
},
"children": []
},
{
"id": "1760668729192_pvcbrqpga",
"className": "LimitTime",
"name": "时间限制器",
"position": {
"x": -260,
"y": -40
},
"parameters": {
"_max": 4
},
"children": [
"1760668729192_6f5mhy8o5"
]
},
{
"id": "1760668729192_6f5mhy8o5",
"className": "BTAnimation",
"name": "播放动画",
"position": {
"x": -260,
"y": 40
},
"parameters": {
"_name": "zoulu",
"_loop": true
},
"children": []
},
{
"id": "1760668771864_s2nrgxu59",
"className": "BTAnimation",
"name": "播放动画",
"position": {
"x": -120,
"y": 40
},
"parameters": {
"_name": "shouyin1",
"_loop": false
},
"children": []
},
{
"id": "1760668771864_ryllyy413",
"className": "BTAnimation",
"name": "播放动画",
"position": {
"x": 20,
"y": 40
},
"parameters": {
"_name": "shouyin2",
"_loop": false
},
"children": []
},
{
"id": "1760668771864_36a49lgd5",
"className": "BTAnimation",
"name": "播放动画",
"position": {
"x": 160,
"y": 40
},
"parameters": {
"_name": "dasao1",
"_loop": false
},
"children": []
},
{
"id": "1760668771864_xs1f57m7t",
"className": "RandomSelector",
"name": "随机选择节点",
"position": {
"x": 220,
"y": -120
},
"parameters": {},
"children": [
"1760669249155_dd86swcyd",
"1760669282323_w8e5afa7f",
"1760669285498_33g4ibw4s",
"1760669288762_vavcm4qce",
"1760669292090_vri0pwmjl",
"1760669293676_4iizzuu5y"
]
},
{
"id": "1760668771864_v45om2x5m",
"className": "BTAnimation",
"name": "播放动画",
"position": {
"x": 300,
"y": 40
},
"parameters": {
"_name": "dasao2",
"_loop": false
},
"children": []
},
{
"id": "1760668771864_l7co5r94f",
"className": "BTAnimation",
"name": "播放动画",
"position": {
"x": 440,
"y": 40
},
"parameters": {
"_name": "sheying1",
"_loop": false
},
"children": []
},
{
"id": "1760668771864_mve2r80e3",
"className": "BTAnimation",
"name": "播放动画",
"position": {
"x": 580,
"y": 40
},
"parameters": {
"_name": "sheying2",
"_loop": false
},
"children": []
},
{
"id": "1760669249155_dd86swcyd",
"className": "WeightDecorator",
"name": "权重装饰节点",
"position": {
"x": -120,
"y": -40
},
"parameters": {
"_weight": 1
},
"children": [
"1760668771864_s2nrgxu59"
]
},
{
"id": "1760669282323_w8e5afa7f",
"className": "WeightDecorator",
"name": "权重装饰节点",
"position": {
"x": 20,
"y": -40
},
"parameters": {
"_weight": 1
},
"children": [
"1760668771864_ryllyy413"
]
},
{
"id": "1760669285498_33g4ibw4s",
"className": "WeightDecorator",
"name": "权重装饰节点",
"position": {
"x": 160,
"y": -40
},
"parameters": {
"_weight": 1
},
"children": [
"1760668771864_36a49lgd5"
]
},
{
"id": "1760669288762_vavcm4qce",
"className": "WeightDecorator",
"name": "权重装饰节点",
"position": {
"x": 300,
"y": -40
},
"parameters": {
"_weight": 1
},
"children": [
"1760668771864_v45om2x5m"
]
},
{
"id": "1760669292090_vri0pwmjl",
"className": "WeightDecorator",
"name": "权重装饰节点",
"position": {
"x": 440,
"y": -40
},
"parameters": {
"_weight": 1
},
"children": [
"1760668771864_l7co5r94f"
]
},
{
"id": "1760669293676_4iizzuu5y",
"className": "WeightDecorator",
"name": "权重装饰节点",
"position": {
"x": 580,
"y": -40
},
"parameters": {
"_weight": 1
},
"children": [
"1760668771864_mve2r80e3"
]
}
],
"connections": [
{
"id": "conn_1760666871430_poh4dvjwi",
"sourceNodeId": "1760666778409_kl1cri6xp",
"targetNodeId": "1760666838184_0sf89zrd4",
"sourcePointType": "child",
"targetPointType": "parent"
},
{
"id": "conn_1760666881111_a8i6ryu0v",
"sourceNodeId": "1760666881111_eu9y7q5m6",
"targetNodeId": "1760666881111_qoyqb9mkm",
"sourcePointType": "child",
"targetPointType": "parent"
},
{
"id": "conn_1760666914926_34fuxnrwc",
"sourceNodeId": "1760666778409_kl1cri6xp",
"targetNodeId": "1760666881111_eu9y7q5m6",
"sourcePointType": "child",
"targetPointType": "parent"
},
{
"id": "conn_1760668704512_mpippypy6",
"sourceNodeId": "1760666774273_6twatctr8",
"targetNodeId": "1760666778409_kl1cri6xp",
"sourcePointType": "child",
"targetPointType": "parent"
},
{
"id": "conn_1760668729192_qt1bumeu7",
"sourceNodeId": "1760668729192_pvcbrqpga",
"targetNodeId": "1760668729192_6f5mhy8o5",
"sourcePointType": "child",
"targetPointType": "parent"
},
{
"id": "conn_1760668731720_c2a4cu2iz",
"sourceNodeId": "1760668712628_4untk0pc1",
"targetNodeId": "1760668729192_kuio9twvr",
"sourcePointType": "child",
"targetPointType": "parent"
},
{
"id": "conn_1760668733320_y6s636wtc",
"sourceNodeId": "1760668712628_4untk0pc1",
"targetNodeId": "1760668729192_pvcbrqpga",
"sourcePointType": "child",
"targetPointType": "parent"
},
{
"id": "conn_1760668756520_yxs004ghe",
"sourceNodeId": "1760666774273_6twatctr8",
"targetNodeId": "1760668712628_4untk0pc1",
"sourcePointType": "child",
"targetPointType": "parent"
},
{
"id": "conn_1760669254025_9of7apgsm",
"sourceNodeId": "1760668771864_xs1f57m7t",
"targetNodeId": "1760669249155_dd86swcyd",
"sourcePointType": "child",
"targetPointType": "parent"
},
{
"id": "conn_1760669265721_rbxmewqrx",
"sourceNodeId": "1760669249155_dd86swcyd",
"targetNodeId": "1760668771864_s2nrgxu59",
"sourcePointType": "child",
"targetPointType": "parent"
},
{
"id": "conn_1760669297425_93y6h29te",
"sourceNodeId": "1760669282323_w8e5afa7f",
"targetNodeId": "1760668771864_ryllyy413",
"sourcePointType": "child",
"targetPointType": "parent"
},
{
"id": "conn_1760669300401_wj5smi6q5",
"sourceNodeId": "1760669285498_33g4ibw4s",
"targetNodeId": "1760668771864_36a49lgd5",
"sourcePointType": "child",
"targetPointType": "parent"
},
{
"id": "conn_1760669303074_2zcgl7tmh",
"sourceNodeId": "1760669288762_vavcm4qce",
"targetNodeId": "1760668771864_v45om2x5m",
"sourcePointType": "child",
"targetPointType": "parent"
},
{
"id": "conn_1760669305922_el07uklys",
"sourceNodeId": "1760669292090_vri0pwmjl",
"targetNodeId": "1760668771864_l7co5r94f",
"sourcePointType": "child",
"targetPointType": "parent"
},
{
"id": "conn_1760669308449_h0p002ff4",
"sourceNodeId": "1760669293676_4iizzuu5y",
"targetNodeId": "1760668771864_mve2r80e3",
"sourcePointType": "child",
"targetPointType": "parent"
},
{
"id": "conn_1760669311177_631n86t83",
"sourceNodeId": "1760668771864_xs1f57m7t",
"targetNodeId": "1760669282323_w8e5afa7f",
"sourcePointType": "child",
"targetPointType": "parent"
},
{
"id": "conn_1760669313009_n87mb2ovs",
"sourceNodeId": "1760668771864_xs1f57m7t",
"targetNodeId": "1760669285498_33g4ibw4s",
"sourcePointType": "child",
"targetPointType": "parent"
},
{
"id": "conn_1760669314577_967btvjvq",
"sourceNodeId": "1760668771864_xs1f57m7t",
"targetNodeId": "1760669288762_vavcm4qce",
"sourcePointType": "child",
"targetPointType": "parent"
},
{
"id": "conn_1760669316441_h73u37spc",
"sourceNodeId": "1760668771864_xs1f57m7t",
"targetNodeId": "1760669292090_vri0pwmjl",
"sourcePointType": "child",
"targetPointType": "parent"
},
{
"id": "conn_1760669318825_j3noil5ka",
"sourceNodeId": "1760668771864_xs1f57m7t",
"targetNodeId": "1760669293676_4iizzuu5y",
"sourcePointType": "child",
"targetPointType": "parent"
},
{
"id": "conn_1760670761598_u2fpnqus3",
"sourceNodeId": "1760666774273_6twatctr8",
"targetNodeId": "1760668771864_xs1f57m7t",
"sourcePointType": "child",
"targetPointType": "parent"
}
],
"canvasScale": 0.9294397018150369,
"canvasOffset": {
"x": 713.7056029818497,
"y": 429.08390345073457
}
}

View File

@@ -486,9 +486,9 @@
"targetPointType": "parent"
}
],
"canvasScale": 1.0893125857312862,
"canvasScale": 0.7146979874982969,
"canvasOffset": {
"x": 1076.4697723606591,
"y": 543.3385223005863
"x": 845.1442956244039,
"y": 486.99278521877983
}
}

View File

@@ -1,11 +0,0 @@
{
"name": "test-bttree",
"description": "",
"nodes": [],
"connections": [],
"canvasScale": 1,
"canvasOffset": {
"x": 723,
"y": 531.5
}
}

View File

@@ -0,0 +1,14 @@
{
"__version__": "1.0.1",
"panelSize": {
"width": 1800,
"height": 1172,
"timestamp": 1759221857309
},
"panel": {
"size": {
"width": 2056,
"height": 900
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "kunpocc-behaviortree",
"version": "0.1.3",
"version": "0.1.5",
"description": "行为树",
"main": "./dist/kunpocc-behaviortree.cjs",
"module": "./dist/kunpocc-behaviortree.mjs",

View File

@@ -38,6 +38,14 @@ export interface IBTNode {
/** 获取关联的实体 */
getEntity<T>(): T;
/**
* 递归清理子节点的打开状态
* 用途:
* 1.装饰节点的子节点未关闭,但是装饰节点关闭时,需要清理子节点的打开状态
* 2.组合节点关闭,但是子节点可能还在运行,需要清理子节点的打开状态
*/
cleanupChild(): void;
}
@@ -92,15 +100,14 @@ export abstract class BTNode implements IBTNode {
*/
protected open(): void { }
protected close(): void { }
/**
* 清理子节点的打开状态
* 一般用于装饰节点的非子节点关闭时, 用来清理子节点的打开状态
*/
protected cleanupChild(): void {
const child = this.children[0];
if (child && this._local.openNodes.has(child)) {
this._local.openNodes.delete(child);
(child as BTNode).close();
public cleanupChild(): void {
for (const child of this.children) {
if (this._local.openNodes.has(child)) {
child.cleanupChild();
this._local.openNodes.delete(child);
(child as BTNode).close();
}
}
}

View File

@@ -7,11 +7,22 @@ import { WeightDecorator } from "./Decorator";
/**
* 组合节点基类
* 有多个子节点
*
* 组合节点全部都有记忆能力
*/
export abstract class Composite extends BTNode {
constructor(...children: IBTNode[]) {
super(children);
}
public override _initialize(global: IBlackboard, branch: IBlackboard): void {
super._initialize(global, branch);
this._local = branch.createChild();
}
protected override open(): void {
this.set(`__nRunningIndex`, 0);
}
}
/**
@@ -23,16 +34,6 @@ export abstract class Composite extends BTNode {
*/
@BT.ClassComposite("Selector", { name: "选择节点", group: "基础组合节点", desc: "子节点从左到右执行, 子节点状态: 成功则选择成立, 失败继续下一个, 执行中则返回执行中, 下次从该节点开始" })
export class Selector extends Composite {
public override _initialize(global: IBlackboard, branch: IBlackboard): void {
super._initialize(global, branch);
this._local = branch.createChild();
}
protected override open(): void {
super.open();
this.set(`__nRunningIndex`, 0);
}
public tick(dt: number): Status {
let index = this.get<number>(`__nRunningIndex`);
for (let i = index; i < this.children.length; i++) {
@@ -59,16 +60,6 @@ export class Selector extends Composite {
*/
@BT.ClassComposite("Sequence", { name: "顺序节点", group: "基础组合节点", desc: "子节点从左到右执行, 子节点状态: 成功则继续下一个, 失败则停止迭代返回失败, 执行中返回执行中, 下次从该节点开始" })
export class Sequence extends Composite {
public override _initialize(global: IBlackboard, branch: IBlackboard): void {
super._initialize(global, branch);
this._local = branch.createChild();
}
protected override open(): void {
super.open();
this.set(`__nRunningIndex`, 0);
}
public tick(dt: number): Status {
let index = this.get<number>(`__nRunningIndex`);
for (let i = index; i < this.children.length; i++) {
@@ -87,22 +78,49 @@ export class Sequence extends Composite {
}
/**
* 并行节点 从左到右依次执行所有子节点
* 注意:这里的"并行"是逻辑概念,实际是顺序执行
* 并行节点 从左到右依次执行所有子节点 必定全部执行一遍
* 返回优先级 FAILURE > RUNNING > SUCCESS
* 注意:这里的"并行"是逻辑概念,实际是顺序执行
*
* 记忆机制: 已经失败的子节点不会重复执行,只执行运行中的子节点
*/
@BT.ClassComposite("Parallel", { name: "并行节点", group: "基础组合节点", desc: "依次执行所有子节点(从左到右), 子节点状态: 任意失败则失败 > 任意执行中则执行中 > 全部成功则成功" })
export class Parallel extends Composite {
protected override open(): void {
super.open();
// 初始化所有子节点状态为RUNNING
for (let i = 0; i < this.children.length; i++) {
this.set(`__childStatus_${i}`, Status.RUNNING);
}
}
public tick(dt: number): Status {
let result = Status.SUCCESS;
for (let i = 0; i < this.children.length; i++) {
let status = this.children[i]!._execute(dt);
if (result === Status.FAILURE || status === Status.FAILURE) {
// 获取该子节点的缓存状态
let status = this.get<Status>(`__childStatus_${i}`);
// 只执行还在运行中的子节点
if (status === Status.RUNNING) {
status = this.children[i]!._execute(dt);
// 缓存子节点状态
if (status !== Status.FAILURE && result !== Status.FAILURE) {
this.set(`__childStatus_${i}`, status);
}
}
// 优先级: FAILURE > RUNNING > SUCCESS
// 必须执行完所有子节点,不能提前终止
if (status === Status.FAILURE) {
result = Status.FAILURE;
} else if (status === Status.RUNNING) {
} else if (status === Status.RUNNING && result !== Status.FAILURE) {
result = Status.RUNNING;
}
}
// 所有子节点执行完毕后如果不是RUNNING则清理
if (result !== Status.RUNNING) {
this.cleanupChild();
}
return result;
}
}
@@ -110,7 +128,7 @@ export class Parallel extends Composite {
/**
* 随机选择节点
* 随机选择一个子节点执行
* 随机选择一个子节点执行,支持权重
* 返回子节点状态
*/
@BT.ClassComposite("RandomSelector", { name: "随机选择节点", group: "基础组合节点", desc: "随机选择一个子节点执行, 返回子节点状态" })
@@ -124,14 +142,26 @@ export class RandomSelector extends Composite {
this._weights = [];
for (const child of children) {
const weight = this.getChildWeight(child);
const weight = (child instanceof WeightDecorator) ? child.weight : 1;
this._totalWeight += weight;
this._weights.push(this._totalWeight);
}
}
private getChildWeight(child: IBTNode): number {
return (child instanceof WeightDecorator) ? (child.weight) : 1;
protected override open(): void {
this.set(`__nRunningIndex`, -1);
}
/** 根据权重随机选择子节点索引 */
private selectRandomIndex(): number {
const randomValue = Math.random() * this._totalWeight;
// 线性查找(行为树子节点数量通常很少,性能足够)
for (let i = 0; i < this._weights.length; i++) {
if (randomValue < this._weights[i]!) {
return i;
}
}
return this._weights.length - 1;
}
public tick(dt: number): Status {
@@ -139,45 +169,67 @@ export class RandomSelector extends Composite {
return Status.FAILURE;
}
// 基于权重的随机选择
const randomValue = Math.random() * this._totalWeight;
// 使用二分查找找到对应的子节点索引O(log n)复杂度)
let left = 0;
let right = this._weights.length - 1;
let childIndex = 0;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (this._weights[mid]! > randomValue) {
childIndex = mid;
right = mid - 1;
} else {
left = mid + 1;
}
// 获取或选择子节点索引
let index = this.get<number>(`__nRunningIndex`);
if (index === -1) {
index = this.selectRandomIndex();
}
const status = this.children[childIndex]!._execute(dt);
// 执行选中的子节点
const status = this.children[index]!._execute(dt);
// 如果返回RUNNING记录索引以便下次继续执行
if (status === Status.RUNNING) {
this.set(`__nRunningIndex`, index);
}
return status;
}
}
/**
* 并行任意成功节点 从左到右依次执行所有子节点
* 注意:这里的"并行"是逻辑概念,实际是顺序执行
* 并行任意成功节点 从左到右依次执行所有子节点 必定全部执行一遍
* 返回优先级 SUCCESS > RUNNING > FAILURE
* 注意:这里的"并行"是逻辑概念,实际是顺序执行
*
* 记忆机制: 已经失败的子节点不会重复执行,只执行运行中的子节点
*/
@BT.ClassComposite("ParallelAnySuccess", { name: "并行任意成功节点", group: "基础组合节点", desc: "依次执行所有子节点(从左到右), 任意一个成功则成功 > 任意一个执行中则执行中 > 全部失败则失败" })
export class ParallelAnySuccess extends Composite {
protected override open(): void {
super.open();
// 初始化所有子节点状态为RUNNING
for (let i = 0; i < this.children.length; i++) {
this.set(`__childStatus_${i}`, Status.RUNNING);
}
}
public tick(dt: number): Status {
let result = Status.FAILURE;
for (let i = 0; i < this.children.length; i++) {
let status = this.children[i]!._execute(dt);
if (result === Status.SUCCESS || status === Status.SUCCESS) {
// 获取该子节点的缓存状态
let status = this.get<Status>(`__childStatus_${i}`);
// 只执行还在运行中的子节点
if (status === Status.RUNNING) {
status = this.children[i]!._execute(dt);
// 缓存子节点状态
if (status !== Status.SUCCESS && result !== Status.SUCCESS) {
this.set(`__childStatus_${i}`, status);
}
}
// 优先级: SUCCESS > RUNNING > FAILURE
if (status === Status.SUCCESS) {
result = Status.SUCCESS;
} else if (status === Status.RUNNING) {
} else if (status === Status.RUNNING && result !== Status.SUCCESS) {
result = Status.RUNNING;
}
}
// 所有子节点执行完毕后如果不是RUNNING则清理
if (result !== Status.RUNNING) {
this.cleanupChild();
}
return result;
}
}

View File

@@ -30,6 +30,7 @@ export abstract class ConditionDecorator extends Decorator {
if (this.isEligible()) {
return this.children[0]!._execute(dt);
}
this.cleanupChild();
return Status.FAILURE;
}
}