Jump to content
Frequently Asked Questions
  • Are you not able to open the client? Try following our getting started guide
  • Still not working? Try downloading and running JarFix
  • Help! My bot doesn't do anything! Enable fresh start in client settings and restart the client
  • How to purchase with PayPal/OSRS/Crypto gold? You can purchase vouchers from other users
  • Try asking for help in the chatbox
  • Behaviour trees for better code structure


    jgs95
     Share

    Recommended Posts

    Hi all,

    Introduction

    This post is about using the behaviour tree AI concept to improve the structure of your scripts.  When I started out writing scripts I used the binary tree approach but this resulted in quite a deep/nested structure which wasn't easy to maintain.  I found the binary tree approach wasn't suitable for sequences.  I.e. a linear sequence of N actions, as on each game loop the tree would need to be traversed, and context data would need to be evaluated to determine the next step in the sequence.

    After looking around online I found this article: Behaviour trees for AI and how they work

    This article provides a decent introduction to behaviour trees, what they are, what they do and how they are structured, so I'd recommend you read that before applying the behaviour tree approach.

    API Design

    I wrote my own framework a few weeks ago to implement this approach but it wasn't easy to modify once the script was finished, so I looked for an approach that has better maintainability and is easy to setup the structure and understand/read.  I found this github repository that provides a decent Fluent API for behaviour trees but needed to adapt it as it didn't implement the behaviour trees in the same way as the above article.  So I adapted the API and wrote a proof of concept script that simply chops Logs in Lumbridge.

    Code - Main Class

    Here is the main script class with comments providing an explanation.

    @ScriptManifest(name = "Woodcutter", version = 1, author = "JGFighter95", category = Category.WOODCUTTING)
    public class Woodcutter extends AbstractScript {
        private Node<WoodcutterData> tree;
        private WoodcutterData woodcutterData;
    
        @Override
        public int onLoop() {
            this.tree.tick(woodcutterData);
            return (int) Calculations.nextGaussianRandom(600, 100);
        }
    
        @Override
        public void onStart() {
            // WoodcutterData is accessible by the nodes in the tree. This class can be used
            // to provide a data context/information sharing object. It can also contain helper
            // methods to provide code reuse.
            this.woodcutterData = new WoodcutterData();
    
            // I have a context property of type MethodContext, so the nodes can use methods
            // such as getLocalPlayer()
            this.woodcutterData.context = this;
    
            // Set the generic property of TreeBuilder to WoodcutterData. This can be
            // any class you want and it doesn't need to extend any abstract class or
            // implement any interface.
            this.tree = new TreeBuilder<WoodcutterData>()
                 // Repeat until fail has 1 child node and executes it until it returns
                 // a status of Failure.
                .repeatUntilFail()
                    // Selector is a composite node (meaning it has one or many child nodes).
                    // As soon as one of its children return Success, the selector returns
                    // Success back to its parent. If a chlid returns Failure it will keep
                    // processing each child until they are all processed. If they are all
                    // processed and none have returned Success, the selector will return
                    // Failure. This is good for fallback scenarios, i.e. attempt action 1
                    // and if it fails fall back to action 2, and so on.
                    .selector()
                    
                        // Insert is used to add a sub-tree to the tree. You could modularise
                        // functionality into trees, and reuse them in other scripts by inserting
                        // them. 
                        .insert(woodcuttingSubTree())
                        .insert(bankSubTree())
                    .finish()
                .finish()
                .build();
        }
    
        // The woodcutting subtree performs the woodcutting section of the script: walk to
        // trees, find a tree, chop it down, wait for it to complete, then repeat until
        // the inventory is full.
        private Node<WoodcutterData> woodcuttingSubTree() {
            return new TreeBuilder<WoodcutterData>()
                // Like the selector described above, the sequence is also a composite. However,
                // for the sequence to return Success, each of its children must return Success.
                .sequence()
                    // Condition is a type of leaf node. If it returns true, the sequence will
                    // continue, otherwise it will end and return Failure to its parent.
                    .condition(new HasEnoughInventorySpace())
                    .sequence()
                        // An action is simply a leaf node (no children) which performs an 
                        // action.
                        .action(new WalkToTrees())
                    
                        // Perform the woodcutting sequence until it returns Failure, which
                        // would happen when the HasEnoughInventorySpace returns Failure.
                        .repeatUntilFail()
                            .sequence()
                                .condition(new HasEnoughInventorySpace())
                                .action(new FindTree())
                                .action(new ChopDownTree())
                                .action(new WaitForLogs())
                            // After all the composite childs are done, you must call finish()
                            // to move the node pointer in the tree builder back until it
                            // gets to the root level. YOu don't really need to understand how
                            // it works, just that each composite child needs to be 'finished' once
                            // its children have been added, otherwise the tree won't be built
                            // properly.
                            .finish()
                        .finish()
                    .finish()
                .finish()
                .build();
        }
    
        private Node<WoodcutterData> bankSubTree() {
            return new TreeBuilder<WoodcutterData>()
                .sequence()
                    .action(new WalkToBank())
                    .action(new OpenBank())
                    .action(new DepositLogs())
                    .action(new CloseBank())
                .finish()
                .build();
        }
    }

    Code - HasEnoughInventorySpace (Condition)

    public class HasEnoughInventorySpace implements Function<WoodcutterData, Boolean> {
        @Override
        public Boolean apply(WoodcutterData woodcutterData) {
            MethodProvider.log("Condition: HasEnoughInventorySpace");
            return Inventory.emptySlotCount() >= 1;
        }
    }

    Code - FindTree (Action)

    public class FindTree implements Function<WoodcutterData, State> {
        @Override
        public State apply(WoodcutterData woodcutterData) {
            MethodProvider.log("Action: FindTree");
            GameObject tree = GameObjects.closest(new Filter<GameObject>() {
                @Override
                public boolean match(GameObject treeCandidate) {
                    if (!treeCandidate.getName().equals("Tree")) return false;
                    if (!treeCandidate.hasAction("Chop down")) return false;
                    List<Player> playersNextToTree = Players.all(new Filter<Player>() {
                        @Override
                        public boolean match(Player player) {
                            if (player.getName().equals(woodcutterData.context.getLocalPlayer().getName())) return false;
                            if (player.distance(treeCandidate) > 1) return false;
                            return true;
                        }
                    });
                    if (playersNextToTree.size() > 0) return false;
                    return true;
                }
            });
            if (tree == null) return State.RUNNING;
            woodcutterData.tree = tree;
            return State.SUCCESS;
        }
    }

    BitBucket Repository

    For the full script here is the bit bucket repository: jonathanstewart147 / db_behaviour_trees1 — Bitbucket

    Conclusion

    Hopefully this post made sense but please ask any questions and I will be happy to discuss them.

    If you think about any improvements to this approach, or if there is something already out there, please let me know! :)

     

    Happy scripting!

    Link to comment
    Share on other sites

    • 2 months later...

    Create an account or sign in to comment

    You need to be a member in order to leave a comment

    Create an account

    Sign up for a new account in our community. It's easy!

    Register a new account

    Sign in

    Already have an account? Sign in here.

    Sign In Now
     Share

    ×
    ×
    • Create New...

    Important Information

    We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.