Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make the behavior tree creation more readable #108

Open
JeremyVansnick opened this issue Feb 1, 2025 · 2 comments
Open

Make the behavior tree creation more readable #108

JeremyVansnick opened this issue Feb 1, 2025 · 2 comments
Assignees
Labels
enhancement New feature or request

Comments

@JeremyVansnick
Copy link

JeremyVansnick commented Feb 1, 2025

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());
                    });
                });
            });
        });
    });
});
@JeremyVansnick JeremyVansnick changed the title Make the behavior tree creation more readable / easy Make the behavior tree creation more readable Feb 1, 2025
@ashblue
Copy link
Owner

ashblue commented Feb 3, 2025

The usage example on this looks really good and is far more readable. I'll have to prototype out some changes. But I'm thinking the old commands should be marked deprecated and this should be put in as a new feature for feedback. I'll have to find some time to give this a whirl and prototype out a few things. It all looks good in your code though.

@ashblue ashblue self-assigned this Feb 3, 2025
@ashblue ashblue added the enhancement New feature or request label Feb 3, 2025
@JeremyVansnick
Copy link
Author

I'm really enjoying using this approach so far! It's much easier to use.
I agree it's best to deprecate the old approach.

Now when working on a unit AI, I no longer need to enter runtime just to see how the tree looks like or manage these complex indentations - I can fully concentrate on the behavior of the tree without losing focus. What a joy!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants