行为树由多种不同类型的节点构成,它们都拥有一个共同的核心功能,即它们会返回三种状态中的一个作为结果。这三种状态分别是:
- 成功-Success;
- 失败-Failure;
- 运行中-Running;
前两个,正如它们的名字,是用来向它们的父节点通知运行的成功或失败的结果。第三种是指还在运行中,结果还未决定,在下一个 tick 的时候再去检查这个节点的运行结果。
这个功能非常重要,它可以让一个节点持续运行一段时间来维持某些行为。比如一个“walk(行走)”的节点会在计算寻路和让角色保持行走的过程中持续返回“Running”来让角色保持这一状态。如果寻路因为某些原因失败,或是除了某些状况让行走的行为不得不中止,那么这个节点会返回“Failure”来告诉它的父节点;如果这个角色走到了指定的目的地,那么节点返回“Success”来表示这个行走的指令已经成功完成。
文字截取于:https://www.indienova.com/indie-game-development/ai-behavior-trees-how-they-work/#iah-6
这篇文章延续上一篇,这篇主要是对Running状态进行一个记述。
现在通过2天的学习,我对状态书有了一点了解,我觉得有必要按照我的思路去手动写下代码,以便更好的理解。
上篇文章的行为树如下:
1.Selector是一个选择节点,他会根据它的子节点下面的type以及func进行选择是否要进行执行某个子节点
2.如果有钱,就执行Sequence下面的所有行为:买车和买房
但是,现在我们有个问题,如果现在我正在买房,下一次在遍历行为树的时候,我们希望很友好的说“不”,毕竟一栋房子都没搞定,谁会有心思去考虑下一栋的问题或者理解买房子是与状态的,某个时间内不允许修改。这也是我们这篇要讲的Running问题
我们的程序会变成类似以下这样(绿色部分下面的分支是我们将要做的):
我们将要在原来的Sequence_1下新增一个Selector替换action_2
效果在线预览
行为树:
//tree
var tree ={
"name": "",
"indexchild": 1,
"height": 50,
"textwidth": 28,
"width": 56,
"children": [
{
"name": "Selector_1",
"indexchild": 1,
"height": 50,
"textwidth": 54,
"width": 82,
"children":
[
{
"name": "Filter_1",
"indexchild": 1,
"height": 50,
"textwidth": 55,
"width": 83,
"children": [
{
"name": "Sequence_1",
"indexchild": 1,
"height": 50,
"textwidth": 62,
"width": 90,
"children": [
{
"name": "Action_2",
"indexchild": 1,
"height": 50,
"textwidth": 51,
"width": 79,
"level": 5,
"func": "BuyCar",
"y": 384,
"selected": false,
"valid": true,
"x": 213.15972180497,
"sleep": false,
"id": "node8",
"sim": "",
"textlines": 3,
"type": "Action",
"levelindex": 5
},
{
"name": "Action_3",
"indexchild": 2,
"height": 50,
"textwidth": 44,
"width": 72,
"level": 5,
// "func": "isBuySugaring",//"BuySugar"
"y": 384,
"selected": false,
"valid": true,
"x": 328.65972180497,
"sleep": false,
"id": "node9",
"sim": "",
"textlines": 3,
"type": "Selector",//Action
"levelindex": 6,
"children":[
// {
// "name": "Action_3",
// "indexchild": 2,
// "height": 50,
// "textwidth": 44,
// "width": 72,
// "level": 5,
// "func": "BuySugar",
// "y": 384,
// "selected": false,
// "valid": true,
// "x": 328.65972180497,
// "sleep": false,
// "id": "node9",
// "sim": "",
// "textlines": 3,
// "type": "Action",
// "levelindex": 6,
// }
{
"name": "Action_3",
"indexchild": 2,
"height": 50,
"textwidth": 44,
"width": 72,
"level": 5,
"func": "isBuySugaring",//"BuySugar"
"y": 384,
"selected": false,
"valid": true,
"x": 328.65972180497,
"sleep": false,
"id": "node9",
"sim": "",
"textlines": 3,
"type": "Filter",//Action
"levelindex": 6,
"children":[
{
"name": "Action_3",
"indexchild": 2,
"height": 50,
"textwidth": 44,
"width": 72,
"level": 5,
"func": "",//
"y": 384,
"selected": false,
"valid": true,
"x": 328.65972180497,
"sleep": false,
"id": "node9",
"sim": "",
"textlines": 3,
"type": "Sequence",//Action
"levelindex": 6,
"children":[
{
"name": "Action_3",
"indexchild": 2,
"height": 50,
"textwidth": 44,
"width": 72,
"level": 5,
"func": "BuySugar",
"y": 384,
"selected": false,
"valid": true,
"x": 328.65972180497,
"sleep": false,
"id": "node9",
"sim": "",
"textlines": 3,
"type": "Action",
"levelindex": 6,
}
]
}
]
}
]
}
],
"level": 4,
"func": "",
"y": 288,
"selected": false,
"valid": true,
"x": 261.90972180497,
"sleep": false,
"id": "node7",
"sim": "",
"textlines": 3,
"type": "Sequence",
"levelindex": 4
}
],
"level": 3,
"func": "HasMoney",
"y": 192,
"selected": false,
"valid": true,
"x": 265.40972180497,
"sleep": false,
"id": "node2",
"sim": "",
"textlines": 3,
"type": "Filter",
"levelindex": 3
},
{
"name": "Sleepy",
"indexchild": 2,
"height": 50,
"textwidth": 49,
"width": 117,
"level": 3,
"func": "Rest",
"y": 192,
"selected": false,
"valid": true,
"x": 363.75957639191,
"sleep": false,
"id": "node6",
"sim": "",
"textlines": 3,
"type": "Condition",
"levelindex": 7
},
{
"name": "Action_3",
"indexchild": 3,
"height": 50,
"textwidth": 47,
"width": 75,
"level": 3,
"func": "GoHome",
"y": 192,
"selected": true,
"valid": true,
"x": 515.59027819503,
"sleep": false,
"id": "node5",
"sim": "",
"textlines": 3,
"type": "Action",
"levelindex": 8
}
],
"level": 2,
"func": "",
"y": 96,
"selected": false,
"valid": true,
"x": 387,
"sleep": false,
"id": "node1",
"sim": "",
"textlines": 3,
"type": "Selector",
"levelindex": 2
}],
"level": 1,
"func": "",
"selected": false,
"valid": true,
"y": 0,
"x": 400,
"id": "__start__",
"sleep": false,
"textlines": 3,
"type": "Start",
"levelindex": 1
};
行为树处理程序(未改变):
var BT = {
//-- 1、调用前保证BT中的函数全部都在obj表中
//-- 2、假设输入合法
run: function(bt, object) {
obj = object;
var first_children = bt.children[0]; //Selecter选择器(数组只有一个元素),type为selecter
console.log("first::", first_children.type)
BT[first_children.type](first_children); //执行BT里面的selecter函数
},
// -- Composite Node
//-- Selector
Selector(node) {
var return_value = false;
let childArray = ipairs(node.children);
for (var child in childArray) {
var childValue = node.children[child];
console.log("Selecter type:", childValue.type)
//调用filter,filter属于Selecter派发事件的范畴,属于Selecter的内容
//如果有合适条件的,意思返回true,就执行当前分支的,不在进行下一个执行(selecter的作用)
if (BT[childValue.type](childValue) == true) {
console.log("Selecter type: true:", childValue.type)
return_value = true;
break;
}
}
return return_value; //因为【当前函数】是在上一级函数里面调用的,所以等同“告诉上一级流程是否为true或者false”
},
//-- Sequence,标记为:"并列的动作
Sequence(node) {
var return_value = true;
console.log("Sequence:1=》2=》3", ipairs(node.children).length)
let childArray = ipairs(node.children);
for (var child in childArray) {
var childValue = childArray[child];
console.log("=======1232343",childValue)
console.log("Sequence type:", childValue.type, "~function:", childValue.func)
//调用action
if (BT[childValue.type](childValue) == false) {
return_value = false;
break;
}
}
return return_value; //因为【当前函数】是在上一级函数里面调用的,所以等同“告诉上一级流程是否为true或者false”
},
//-- Behaviour Node
//-- Action
Action(node) {
obj[node.func]();
return true;
},
//-- Condition Action
Condition(node) {
if (obj[node.name]()) {
obj[node.func]();
return true;
} else {
return false; //因为【当前函数】是在上一级函数里面调用的,所以等同“告诉上一级流程是否为true或者false”
}
},
//-- Decorator Node
//-- Yet nothing ...
//-- Condition Node
//-- Filter,过滤,如果满足条件才继续,否则就中断
Filter(node) {
console.log("Filtr:", node.func)
//执行统一层级的条件判断,可以在node.func对应的函数里面判断是 pending?success?false状态来确定是否要进行
//console.log("Filter=====",node.func,obj[node.func])
if (obj[node.func]()) {
var first_children = node.children[0];
console.log("Filter.type:",first_children.type)
BT[first_children.type](first_children);
//执行指定【类型】函数,例如执行买房,可是我正在买中。。 就不需要再次买了
//是否,可以在行为函数里面加判断,但是这样就和行为树的节点(Selecter)功能冲突了
//这种情况我们可以,在Sequence分支里面再加一个selecter
return true;
} else {
return false; //因为【当前函数】是在上一级函数里面调用的,所以等同“告诉上一级流程是否为true或者false”
}
}
};
其他部分:
// 如果有钱了,就买糖、买车(如果正在买,就3秒后再允许买);
// 否则,需要回家取钱,如果累了,则休息(可能休息后会回家吧),如果不累,就回家取钱!
//================并且约定Condition条件判断的函数名使用name来指定
var obj;
var ipairs = function(obj) {
if (obj) {
return obj;
} else {
return {};
}
}
/*
省略=============
var BT={}
var tree={}
*/
//状态==========(状态里面的布尔值和对应的状态为正关系)
var STATE = {
BuySugar: true,//能否买房,能为true,否为false
}
//定时器管理
var Timer = {
BuySugar: {
Timer: 0,
number: 5
}
}
//定时器处理函数(状态管理。。。伪代码)
function TimerDown(timeKey) {
STATE[timeKey] = false; //状态正在中...
var canBuy=STATE[timeKey]?"能买":"不能卖";
document.querySelector("#time").innerHTML=Timer[timeKey].number+"==="+canBuy;
if (!Timer[timeKey].Timer) {
Timer[timeKey].Timer = setInterval(function() {
if (Timer[timeKey].number < 0) {
Timer[timeKey].number = 5;
clearInterval(Timer[timeKey].Timer);
Timer[timeKey].Timer = null;
STATE[timeKey] = true; //可以继续买,状态结束(成功或失败)
console.log("TIME,TIME,TIME,END",Timer[timeKey].Timer,Timer[timeKey].number,STATE[timeKey],"STATE.BuySugar:",STATE.BuySugar)
} else {
Timer[timeKey].number -= 1;
STATE[timeKey] = false; //不可以继续买房,状态正在中...
}
var canBuy=STATE[timeKey]?"能买":"不能卖";
document.querySelector("#time").innerHTML=Timer[timeKey].number+"==="+canBuy;
}, 500);
}
else{
STATE[timeKey] = true;
}
}
var person = {
isBuySugaring() {
if(STATE.BuySugar){
console.log("能买房!")
}
else{
console.log("%c 我还在买房中呢,和买房的小姐姐在瞎JB扯,不要打断我!等3秒后下再去买下一栋把,五环买!!",'color:#fff;background:red;');
}
return STATE.BuySugar;
},
HasMoney: function() { console.log("person:hasMoney?"); return true },
Sleepy: function() { console.log("person:sleepy?"); return true },
Rest: function() { console.log('person:rest=======') },
BuyCar: function() { console.log('person:buy car======') },
BuySugar: function() {
console.log('%c person:buy sugar=====,来买房去了,拉~拉~。','color:#fff;background:green;');
TimerDown("BuySugar");
},
GoHome: function() { console.log('person:go home=====') },
}
for (var i = 0; i <= 1; i++) {
BT.run(tree, person);
}
//demo dom交互==================
window.onload=function(){
document.querySelector("#btn").onclick=function(){
BT.run(tree, person);
}
}
核心改动是在Person里面新增了 isBuySugaring函数来控制是否能买房
如果其他条件都具备的情况下,我们要给人物加一个需要持续的状态
- 只需要在行为树JSON里面加一个Seletor
- 给Person加一个返回状态的函数
本篇完。