Skip to content

Make the behavior tree creation more readable #108

Open
@JeremyVansnick

Description

@JeremyVansnick

I managed to do this approach when you code a behavior tree you don't have to call End(), and you don't need to do any manual formatting.

The concept is simple, you just need to add this wrapper class to the standard version of Fluid Behavior Tree:
(I may have changed some task names but the concept is really simple, so you can change this script below to fit your own tasks easily)

using System;
using CleverCrow.Fluid.BTs.Trees;
using CleverCrow.Fluid.BTs.Tasks;
using UnityEngine;

namespace BehaviorTreeFormatter
{
    // DSL wrapper to allow block-style (curly-brace) formatting for behavior trees.
    public class BehaviorTreeFormatter
    {
        private BehaviorTreeBuilder Builder;

        public BehaviorTreeFormatter(GameObject owner, string name)
        {
            Builder = new BehaviorTreeBuilder(owner, name);
        }

        public BehaviorTree Build()
        {
            return Builder.Build();
        }

        public void Selector(string name, Action block)
        {
            Builder.Selector(name);
            block();
            End();
        }

        public void Sequence(string name, Action block)
        {
            Builder.Sequence(name);
            block();
            End();
        }

        public void Simultaneous(int successesToStop, int failuresToStop, int continuousToStop, string name,
            Action block)
        {
            Builder.Simultaneous(successesToStop, failuresToStop, continuousToStop, name);
            block();
            End();
        }

        public void Decorator(string name, Func<ITask, TaskStatus> logic, Action block)
        {
            Builder.Decorator(name, logic);
            block();
            End();
        }

        public void While(string name, Func<ITask, TaskStatus> logic, Action block)
        {
            Builder.While(name, logic);
            block();
            End();
        }

        public void Condition(string name, Func<bool> condition)
        {
            Builder.Condition(name, condition);
        }

        public void Do(string name, Func<TaskStatus> action)
        {
            Builder.Do(name, action);
        }

        public void ReturnSuccess(string name, Action block)
        {
            Builder.ReturnSuccess(name);
            block();
            End();
        }

        public void ReturnFailure(string name, Action _block)
        {
            Builder.ReturnFailure(name);
            _block();
            End();
        }

        public void AddNode(ITask node, string name = null)
        {
            Builder.AddNode(node, name);
        }

        public void ResetTree()
        {
            Builder.ResetTree();
        }
        
        private void End()
        {
            Builder.End();
        }
    }

    public static class BT
    {
        public static BehaviorTree CreateTree(GameObject owner, string name, Action<BehaviorTreeFormatter> buildAction)
        {
            var dsl = new BehaviorTreeFormatter(owner, name);
            buildAction(dsl);
            return dsl.Build();
        }
    }
}

Here's how it looks like when using the approach from the wrapper class above in an implementation.

var behaviorTree = BT.CreateTree(gameObject, nameof(Tree_Ally_Fighter), bt =>
{
    bt.Selector("Root Selector", () =>
    {
        bt.Condition("Is Disabled?", IsDisabled);

        bt.Selector("Bhv Selector", () =>
        {
            bt.Sequence("Skill command", () =>
            {
                bt.Condition("Is Skill Commanded?", IsSkillCommanded);
                bt.ReturnFailure("Return failure on success", () => { bt.AddNode(Node_DoSkillCommand()); });
            });

            bt.Sequence("Attack command", () =>
            {
                bt.Condition("Is AttackMove Commanded?", IsAttackMoveCommanded);
                bt.Simultaneous(1, -1, -1, "Simultaneous run", () =>
                {
                    bt.Do("Search for new targets", () =>
                    {
                        var targetToAttack = GetTargetToAttack(transform.position);
                        CombatTracker.Attack(targetToAttack);
                        return TaskStatus.Continue;
                    });

                    bt.Selector("Attack, move or exit", () =>
                    {
                        bt.Decorator("Stop if no attack target", _task =>
                        {
                            if (CombatTracker.HasAttackTarget)
                            {
                                var result = _task.Update();
                                if (result == TaskStatus.Success) return TaskStatus.Failure;
                                return result;
                            }
                            else
                            {
                                return TaskStatus.Failure;
                            }
                        }, () => { bt.AddNode(Node_DoUnitCombat()); });

                        bt.Decorator("Stop when has attack target", _task =>
                        {
                            if (!CombatTracker.HasAttackTarget)
                            {
                                var result = _task.Update();
                                if (result == TaskStatus.Success) return TaskStatus.Failure;
                                return result;
                            }
                            else
                            {
                                return TaskStatus.Failure;
                            }
                        }, () => { bt.AddNode(Node_DoMoveCommand()); });

                        bt.Condition("Reached final destination",
                            () => Pather.FinishedPathing());
                    });
                });
            });
        });
    });
});

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions