读书人

基于Unity举动树设计与实现的尝试

发布时间: 2013-04-05 10:24:33 作者: rapoo

基于Unity行为树设计与实现的尝试

查阅了一些行为树资料,目前最主要是参考了这篇文章,看完后感觉行为树实乃强大,绝对是替代状态机的不二之选。但从理论看起来很简单的行为树,真正着手起来却发现很多细节无从下手。


总结起来,就是:

1、行为树只是单纯的一棵决策树,还是决策+控制树。为了防止不必要的麻烦,我目前设计成单纯的决策树。

2、什么时候执行行为树的问题,也就是行为树的Tick问题,是在条件变化的时候执行一次,还是只要对象激活,就在Update里面一直Tick。前者明显很节省开销,但那样设计的最终结果可能是最后陷入事件发送的泥潭中。那么一直Tick可能是最简单的办法,于是就引下面出新的问题。目前采用了一直Tick的办法。

3、基本上可以明显节点有 Composite Node、Decorator Node、Condition Node、Action Node,但具体细节就很头疼。比如组合节点里的Sequence Node。这个节点是不是在每个Tick周期都从头迭代一次子节点,还是记录正在运行的子节点。每次都迭代子节点,就感觉开销有点大。记录运行节点就会出现条件冗余问题,具体后面再讨论。目前采用保存当前运行节点的办法。

4、条件节点(Condition Node)的位置问题。看到很多设计都是条件节点在最后才进行判断,而实际上,如果把条件放在组合节点处,就可以有效短路判断,不再往下迭代。于是我就采用了这种方法。


设计开始

在Google Code上看到的某个行为树框架,用的是抽象类做节点。考虑到C#不能多继承,抽象类可能会导致某些时候会很棘手,所以还是用接口。虽然目前还未发现接口的好处。

在进行抽象设计的时候,接口的纯粹性虽然看起来更加清晰,不过有时候遇到需要重复使用某些类函数的时候就挺麻烦,让人感觉有点不利于复用。


虽然每一组都只是简单的居中,不过效果看起来还可以接受


然后从图中就可以看到问题了。所有正条件,都会有一个反条件,不这么做就无法在条件改变时,让当前节点返回FALSE,从而让行为树去寻找其他节点。而如果用状态机来做的话,条件肯定只用判断一次,比如

private ICompositeNode rootNode = new SelectorNode();    private WarriorInputData inputData = new WarriorInputData();    private WarriorOutPutData outputData = new WarriorOutPutData();// Use this for initialization    public void Start()    {        inputData.attribute = GetComponent<CharacterAttribute>();        rootNode.nodeName += "根";        //条件        var hasNoTarget = new PreconditionNOT(() => { return inputData.attribute.hasTarget; });        hasNoTarget.nodeName = "无目标";        var hasTarget = new Precondition(hasNoTarget);        hasTarget.nodeName = "发现目标";        var isAnger = new Precondition(() => { return inputData.attribute.isAnger; });        isAnger.nodeName = "愤怒状态";        var isNotAnger = new PreconditionNOT(isAnger);        isNotAnger.nodeName = "非愤怒状态";        var HPLessThan500 = new Precondition(() => { return inputData.attribute.health < 500; });        HPLessThan500.nodeName = "血少于500";        var HPMoreThan500 = new PreconditionNOT(HPLessThan500);        HPMoreThan500.nodeName = "血大于500";        var isAlert = new Precondition(() => { return inputData.attribute.isAlert; });        isAlert.nodeName = "警戒";        var isNotAlert = new PreconditionNOT(isAlert);        isNotAlert.nodeName = "非警戒";        var patrolNode = new SequenceNode();        patrolNode.nodeName += "巡逻";        patrolNode.AddCondition(hasNoTarget);        patrolNode.AddCondition(isNotAlert);        patrolNode.AddNode(new PatrolAction());        var alert = new SequenceNode();        alert.nodeName += "警戒";        alert.AddCondition(hasNoTarget);        alert.AddCondition(isAlert);        alert.AddNode(new AlertAction());                var runaway = new SequenceNode();        runaway.nodeName += "逃跑";        runaway.AddCondition(hasTarget);        runaway.AddCondition(HPLessThan500);        runaway.AddNode(new RunAwayAction());        var attack = new SelectorNode();        attack.nodeName += "攻击";        attack.AddCondition(hasTarget);        attack.AddCondition(HPMoreThan500);        var attackCrazy = new SequenceNode();        attackCrazy.nodeName += "疯狂攻击";        attackCrazy.AddCondition(isAnger);        attackCrazy.AddNode(new CrazyAttackAction());        attack.AddNode(attackCrazy);        var attackNormal = new SequenceNode();        attackNormal.nodeName += "普通攻击";        attackNormal.AddCondition(isNotAnger);        attackNormal.AddNode(new AttackAction());        attack.AddNode(attackNormal);        rootNode.AddNode(patrolNode);        rootNode.AddNode(alert);        rootNode.AddNode(runaway);        rootNode.AddNode(attack);        var ret = rootNode.Enter(inputData);        if (!ret)        {            Debug.Log("无可执行节点!");        }}// Update is called once per framevoid Update () {        var ret = rootNode.Tick(inputData, outputData);        if (!ret)            rootNode.Leave(inputData);        if (rootNode.status == RunStatus.Completed)        {            ret = rootNode.Enter(inputData);            if (!ret)                rootNode.Leave(inputData);        }        else if (rootNode.status == RunStatus.Failure)        {            Debug.Log("BT Failed");            enabled = false;        }        if (outputData.action != inputData.action)        {            OnActionChange(outputData.action, inputData.action);            inputData.action = outputData.action;        }}    void OnActionChange(WarriorActon action, WarriorActon lastAction) {      //  print("OnActionChange "+action+" last:"+lastAction);        switch (lastAction)        {            case WarriorActon.ePatrol:                GetComponent<WarriorPatrol>().enabled = false;                break;            case WarriorActon.eAttack:            case WarriorActon.eCrazyAttack:                GetComponent<WarriorAttack>().enabled = false;                break;            case WarriorActon.eRunAway:                GetComponent<WarriorRunAway>().enabled = false;                break;            case WarriorActon.eAlert:                GetComponent<WarriorAlert>().enabled = false;                break;        }        switch (action) {             case WarriorActon.ePatrol:                GetComponent<WarriorPatrol>().enabled = true;                break;            case WarriorActon.eAttack:                var attack = GetComponent<WarriorAttack>();                attack.revenge = false;                attack.enabled = true;                break;            case WarriorActon.eCrazyAttack:                var crazyAttack = GetComponent<WarriorAttack>();                crazyAttack.revenge = true;                crazyAttack.enabled = true;                break;            case WarriorActon.eRunAway:                GetComponent<WarriorRunAway>().enabled = true;                break;            case WarriorActon.eAlert:                GetComponent<WarriorAlert>().enabled = true;                break;            case WarriorActon.eIdle:                GetComponent<WarriorPatrol>().enabled = false;                GetComponent<WarriorAttack>().enabled = false;                GetComponent<WarriorRunAway>().enabled = false;                break;        }    }





读书人网 >其他相关

热点推荐