diff --git a/.docs/fbt_example.png b/.docs/fbt_example.png index b6ae8f7..6cfaad1 100644 Binary files a/.docs/fbt_example.png and b/.docs/fbt_example.png differ diff --git a/.docs/fbt_icon.png b/.docs/fbt_icon.png index 03ba9ea..b0a0af5 100644 Binary files a/.docs/fbt_icon.png and b/.docs/fbt_icon.png differ diff --git a/README.md b/README.md index 6dd96f5..cb94d7d 100644 --- a/README.md +++ b/README.md @@ -2,111 +2,219 @@ # "Functional Behavior Tree" Design Pattern in C# -Highly efficient, functional-style implementation of behavior tree in C# designed to enable clear AI logic, convenient debugging, and fast execution with zero memory allocation. - -The codebase is minimal, simple, and easy to understand. Once familiarized with the approach, you can quickly reproduce or adapt it. -Rather than a library, this solution is better described as a design pattern. - -The implementation has no strict dependency on Unity and can be integrated into any C# project. +Functional-style behavior tree in C#/Unity: simple, fast, debug-friendly, and memory-efficient behavior tree in C#/Unity. # Overview -## Intro - -There are many different implementations of Behaviour Tree in Unity. Typically, it's some kind of node editor, a large set of nodes, a kind of debugging tools, and a lot of internal service code whose efficiency we can only guess at. Debugging is often a big problem. - -**Functional Behavior Tree is a software design pattern that offers a different, professional approach**. - -- Instead of a node editor, you define the behavior tree inside C# using simplest and clear syntax. -- Instead of a using heavy libraries with ton of internal code, you use a thin simple pattern that is absolutely transparent for you. -- Instead of a specialized debugger, you use C# debugger inside you favorite IDE. - -This package includes the full source code of Functional Behavior Tree (FBT) parrern in C# and the example of using. You can use FBT it as a black box to create AI for your NPC, or you can improve it yourself by adding your nodes. - ## Key Features 1. **Clear and Concise Behavior Tree Definition** - Behavior trees are defined directly in code using functions calls and lambda expressions, resulting in clear and compact logic. + Behavior trees are defined directly in code using functions calls and lambda expressions, resulting in clear and compact logic. 2. **Ease Debug** - The tree definition and execution code are the same, which means you can place breakpoints inside any anonymous delegate, and they will work correctly. + The tree definition and execution code are the same, which means you can place breakpoints inside code fragments, and they will behave as expected. No special complex "behaviour tree debugger" is required, you will use your favorite C# IDE. 3. **Zero memory allocation** No memory is allocated for the tree structure because it is embedded directly into the code. No memory is allocated for delegate instances, thanks to the use of static anonymous delegates. - No memory is allocated for transferring parameters to functions due to the use of 'params Collection' arguments (in C# 13) or functions with predefined argument sets (in earlier versions of C#) instead of 'params arrays'. + No memory is allocated to transfer arguments to functions due to the use functions with predefined argument sets (in earlier versions of C#) or 'params Collection' arguments (in C# 13) instead of 'params arrays'. 4. **High speed** - The implementation relies solely on function invocations, static delegates, conditional expressions, and loops. - No expensive features are used (e.g., garbage collection, hash tables, closures, etc.). + No expensive features are used (e.g., garbage collection, hash tables, closures, etc.). + The implementation relies solely on function invocations, static delegates, conditional expressions, and loops. 5. **Minimal and highly readable code** - The simplest version of the library, containing only classic nodes, is just a single .cs file with several tens lines (excluding comments). - -## Requirements -C# 9 (Unity 2021.2 and later) is required because of using static anonymous delegates. - -## Installation - -1. Create a Unity project (Unity 2021.2 or later is required). -2. Install **Functional Behavior Tree**: - - **Option 1:** Install the package "Functional Behavior Tree" from the Unity Asset Store. - - **Option 2:** Clone the Git repository [FunctionalBT](https://github.com/dmitrybaltin/FunctionalBT.git) to a subfolder inside the **Assets** directory of your project. + The entire codebase consists of just a few .cs files, totaling a few hundred lines. ## Usage Example -Detailed examples of using Functional Behavior Tree (FBT) can be found in the folder examples/example1. To run the example, open the scene file located at examples/example1/Scene.unity in Unity. - -This example demonstrates a simple behavior tree for an NPC represented as a circle. The NPC can idle, move toward the player (also represented as a circle), or attack the player, depending on specific conditions. +This example shows a simple behavior tree for an NPC on a 2D surface that can idle, approach the player, and attack. ```csharp -public class NpcFbt : ExtendedFbt -{ - public static void Execute(NpcBoard b) + public static class NpcFbt { - ConditionalAction(b, - static b => !b.IsRagdollEnabled, - static b => ConditionalAction(b, - static b => !b.IsAttacking, - static b => Selector(b, - static b => Sequencer(b, - static b => b.FindTarget(), - static b => Selector(b, - static b => b.Attack(), - static b => ConditionalAction(b, - static b => b.NavigateRequired(), - static b => b.Move()))), - static bd => bd.Idle()))); + public static void ExecuteBT(this NpcBoard b) => + b.Sequencer( //Classic Sequencer node + static b => b.PreUpdate(), //The first child of Sequencer realized as a delegate Func + static b => b.Selector( //The decond child of Sequencer is a Classic Selector node + static b => b.If( //The first child of Selector a Classic Conditional node + static b => b.PlayerDistance < 1f, //Condition + static b => b.Sequencer( //This Sequencer node is executed when the condition is true + static b => b.SetColor(Color.red), + static b => b.OscillateScale(1, 1.5f, 0.25f), + static b => b.AddForce(b.Config.baseClosePlayerForce))), + static b => b.ConditionalSequencer( //Using ConditionalSequencer instead of If + Sequencer (see above) + static b => b.PlayerDistance < 3f, + static b => b.SetColor(Color.magenta), + static b => b.SetScale(1f, 0.1f), + static b => b.AddForce(b.Config.baseClosePlayerForce)), + static b => b.ConditionalSequencer( + static b => b.PlayerDistance < 8f, + static b => b.SetColor(Color.yellow), + static b => b.SetScale(1f, 1f), + static b => b.AddForce(b.Config.baseDistantPlayerForce)), + static b => b.SetColor(Color.grey), + static b => b.SetScale(1f, 1f))); } -} ``` Key points to note: 1. **Classes** - 1. **NpcBoard** is a custom **blackboard** class created by the user that contains the data and methods related to the NPC, which are controlled by the NpcFbt behavior tree. - 1. **NpcFbt** is a custom class responsible for implementing the AI logic for this NPC. It contains only one static function Execute() and does not contain any data fields. - 1. **NpcBoard** instance is stored in an external container (e.g., a MonoBehaviour in Unity), which calls **NpcBoard.Execute()** during the update cycle. - 1. **ExtendedFbt** is a class of this library implementing the code of nodes. + 1. **NpcFbt** is a custom class implementing NPC behavior tree. It is static and contains the only one extension function ExecuteBT(). + 1. **NpcBoard** is a custom **blackboard** class created by the user that contains the data and methods related to NPC. **NpcBoard** instance is stored in an external container (e.g., a MonoBehaviour in Unity), which calls **NpcBoard.Execute()** during the update cycle. 1. **Methods** - 1. **Action()**, **Sequencer()**, **Selector()**, and **ConditionalAction()** are static methods of the ExtendedFbt class from this library, implementing different nodes. - 1. **b.FindTarget()**, **b.Attack()**, **b.Move()**, and **b.Idle()** are defined by the user inside NpcBoard class. + 1. **Action()**, **Sequencer()**, **Selector()**, and **ConditionalAction()** are extensions methods from this library, implementing different nodes. + 1. **b.AddForce()**, **b.SetColor()**, **b.SetScale()**, and **b.PreUpdate()** are defined by the user. + +This implementation is simple, zero allocation and fast and focused purely on logic, making it easy to debug. 1. **Zero memory allocation** - 1. **static** modifier before anonymous delegates guarantee avoiding closures, therefore no memory allocation required for every delegates call. - 1. Every lambda function uses the only a single internal variable, **b**, and there are no closures here. - 2. All all these **b** variables point to the same **NpcBoard** instance received as an argument, but they are independent variables. - 3. Every tree node function receives the blackboard as a first parameter and forwards it to child nodes through delegates. - 4. Any accidental reference to a variable from a different lambda function would create a closure, causing memory allocation, but the **static** modifier prevents such situations. + 1. **static** modifier before anonymous delegates guarantee avoiding closures, therefore no memory allocation required for every delegates call. Every lambda function uses the only a single internal variable, **b**, and there are no closures here. 1. Functions with multiple arguments (**Selector**, **Sequence**, etc) avoid using **params arrays** definition that's why no memory allocated for these calls. +2. You can set breakpoints on any anonymous delegate or tree node function. When the execution reaches these breakpoints, the debugger will pause correctly, allowing you to inspect the state at that point. -As shown in the example code, this implementation is extremely simple, zero allocation and fast and focused purely on logic, making it easy to debug. You can set breakpoints on any anonymous delegate or tree node function. When the execution reaches these breakpoints, the debugger will pause correctly, allowing you to inspect the state at that point. -Here is an illustration of breakpoints in the code: ![Example of debugging](.docs/fbt_example.png) -## Functional Behavior Tree pattern code for Unity +For detailed examples of using Functional Behavior Tree (FBT), see the [FBT Example repository](https://github.com/dmitrybaltin/FbtExample), which contains ready-to-run projects demonstrating common use cases. + +## How does it work + +1. As usual, a special Status object is used as the return value for each node. +```csharp + public enum Status + { + Success = 0, + Failure = 1, + Running = 2, + } +``` + +2. Every node is realized as a static extension function but not a class. + For example here is a full code of a Selector node: +```csharp + public static Status Selector(this T board, + Func f1, + Func f2, + Func f3 = null, + Func f4 = null, + Func f5 = null, + Func f6 = null, + Func f7 = null, + Func f8 = null) + { + var s = f1?.Invoke(board) ?? Status.Failure; if (s is Status.Running or Status.Success) return s; + s = f2?.Invoke(board) ?? Status.Failure; if (s is Status.Running or Status.Success) return s; + s = f3?.Invoke(board) ?? Status.Failure; if (s is Status.Running or Status.Success) return s; + s = f4?.Invoke(board) ?? Status.Failure; if (s is Status.Running or Status.Success) return s; + s = f5?.Invoke(board) ?? Status.Failure; if (s is Status.Running or Status.Success) return s; + s = f6?.Invoke(board) ?? Status.Failure; if (s is Status.Running or Status.Success) return s; + s = f7?.Invoke(board) ?? Status.Failure; if (s is Status.Running or Status.Success) return s; + s = f8?.Invoke(board) ?? Status.Failure; if (s is Status.Running or Status.Success) return s; + + return s; + } +``` + +3. Action nodes require no implementation; any `Func` delegate can serve as an action node. For example: +```csharp + public Status AddForce(float playerForce) + { + var force = (_playerWorldPos - _body.worldCenterOfMass) * (playerForce * Time.deltaTime); + _body.AddForce(force, ForceMode.VelocityChange); + return Status.Success; + } +``` + +## Installation + +You can install Functional Behavior Tree (FBT) in Unity using one of the following methods: + +### 1. Install from GitHub as a Unity Package +1. Open your Unity project. +1. Go to **Window → Package Manager**. +1. Click the **+** button in the top-left corner and choose **Add package from git URL...**. +1. Enter the URL: https://github.com/dmitrybaltin/FunctionalBT.git +1. Click **Add**. The package will be imported into your project. + +### 2. Install via OpenUPM + +1. Open your Unity project. +2. Go to **Edit → Project Settings → Package Manager → Scoped Registries** +3. Add a new registry for OpenUPM: + - **Name:** OpenUPM + - **URL:** `https://package.openupm.com` + - **Scopes:** `com.baltin` +4. Open the **Package Manager** (`Window → Package Manager`). +5. Click **+ → Add package from git URL...** (or search in the registry if the package appears) and enter: **com.baltin.fbt** + +### 3. Install as a Git Submodule + +1. Navigate to your Unity project folder in a terminal. +1. Run +``` +git submodule add https://github.com/dmitrybaltin/FunctionalBT.git Packages/FunctionalBT +git submodule update --init --recursive +``` + +## Requirements +C# 9 (Unity 2021.2 and later) is required because of using static anonymous delegates. + +# Functional Behavior Tree Philosophy + +This section explains the design philosophy, implementation details, and optimizations behind the Functional Behavior Tree (BT) library. It also highlights key constraints, the reasoning behind chosen approaches, and solutions to specific challenges. + +## Why does it created + +There are many different implementations of Behavior Trees in Unity. Typically, they include a node editor, a large set of nodes, some debugging tools, and a lot of internal service code whose efficiency is hard to assess. Debugging is often a major challenge. + +There are also some implementations in C#, such as [Fluid Behavior Tree](https://github.com/ashblue/fluid-behavior-tree), but they have some drawbacks. In particular, debugging can be difficult (requiring a special debugger), the codebase is quite heavy, unnecessary memory allocations occur, and the code is not very compact. + +**Functional Behavior Tree is a simple software design pattern that offers the following approach**. + +- Instead of a node editor, you define the behavior tree inside C# using simplest and clear syntax. +- Instead of a using heavy libraries with ton of internal code, you use a thin simple pattern that is absolutely transparent for you. +- Instead of a specialized debugger, you use C# debugger inside you favorite IDE. + +## Initial Constraints + +The library was designed with the following limitations in mind: + +1. **Classic Behavior Tree Execution:** + - Implements a traditional behavior tree that executes completely during each game loop cycle, rather than adopting an event-driven approach. + +2. **Code-Only Implementation:** + - Focuses solely on C# code, avoiding the need for visual editors (e.g., Behavior Designer) or custom languages (e.g., PandaBT). + +3. **Separation of Concerns:** + - The behavior tree and the managed object (commonly referred to as the Blackboard) are treated as separate entities. Nodes operate on the Blackboard shared across all nodes within the same tree instance. + +## Behavior Trees: A Functional Approach + +Many different implementations of Behaviour Trees in Unity involve a node editor, a large set of nodes, a runtime system, and debugging tools, along with a lot of internal service code whose efficiency is often uncertain. However, most of these implementations treat the tree as a graph, which leads to unnecessarily complex designs, where nodes are represented as objects requiring individual classes and intricate visual editors. As a result, debugging becomes challenging due to the separation of node creation and execution across various parts of the codebase, making specialized tools necessary. + +### The Functional Alternative + +To truly understand the Behaviour Tree, we should think of it as a function, not a graph. + +Indeed, each node in the BT is a function (not an object!), with multiple inputs and a single output, and it doesn’t require memory (there is no need to store any state between function calls). Therefore, the tree as a whole, or any of its subtrees, is also a function that calls the functions of its child nodes. + +In this library: + +- **Nodes are functions, not objects:** + - Each node is a function with multiple inputs and one output, avoiding the need for memory allocation. + +- **Tree as a recursive function:** + - The behavior tree is a recursive function that calls nested node functions. + +- **Simplified debugging:** + - Functional programming principles ensure clarity, making code easy to debug using standard IDE tools. + +This approach results in clean, readable, and efficient code with minimal boilerplate, all while being fast and memory-efficient. + +# Functional Behavior Tree pattern code for Unity Below is a full implementation of the Functional Behavior Tree pattern, including all the classic nodes (**Selector**, **Sequencer**, **Conditional**, and **Inverter**) and the required supporting code: the **Status** enum and a couple of extension methods for it. -In total, the code is just over 100 lines, including comments. -You can also find the same code in the file [LightestFBT.cs](src/LightestFBT.cs). +In total, the code is just over ~100 lines, including comments. +You can also find the same code in the file [MainNodes.cs](src/MainNodes.cs). The main point here is an **each node is a static function rather than an object**. That's why the code is minimal and contains the core logic only: 1. Every node - is the only static function, containing the required logic. @@ -122,30 +230,7 @@ The main point here is an **each node is a static function rather than an object Running = 2, } - public static class StatusExtensions - { - /// - /// Invert Status - /// - /// Source status to invert - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Status Invert(this Status status) - { - return status switch - { - Status.Failure => Status.Success, - Status.Success => Status.Failure, - _ => Status.Running, - }; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Status ToStatus(this bool value) => - value ? Status.Success : Status.Failure; - } - - public class LightestFbt + public static class MainNodes { /// /// Classic inverter node @@ -154,7 +239,7 @@ The main point here is an **each node is a static function rather than an object /// Delegate receiving T and returning Status /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Status Inverter(T board, Func func) + public static Status Inverter(this T board, Func func) => func.Invoke(board).Invert(); /// @@ -165,14 +250,38 @@ The main point here is an **each node is a static function rather than an object /// Action to execute if condition is true. Delegates receiving T and returning Status /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Status If(T board, Func condition, Func func) + public static Status If(this T board, Func condition, Func func) => condition.Invoke(board) ? func.Invoke(board): Status.Failure; + /// + /// Execute the given 'func' delegate if the given condition is true + /// Else execute 'elseFunc' delegate + /// + /// Blackboard object + /// Condition given as a delegate returning true + /// Action to execute if condition is true. Delegate receiving T and returning Status + /// Action to execute if condition is false. Delegate receiving T and returning Status + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Status If(this T board, Func condition, Func func, Func elseFunc) + => condition.Invoke(board) ? func.Invoke(board) : elseFunc.Invoke(board); + +#if !NET9_0_OR_GREATER /// /// Classic selector node /// + /// Blackboard object + /// Delegate receiving T and returning Status + /// Delegate receiving T and returning Status + /// Optional delegate receiving T and returning Status + /// Optional delegate receiving T and returning Status + /// Optional delegate receiving T and returning Status + /// Optional delegate receiving T and returning Status + /// Optional delegate receiving T and returning Status + /// Optional delegate receiving T and returning Status + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Status Selector(T board, + public static Status Selector(this T board, Func f1, Func f2, Func f3 = null, @@ -198,8 +307,17 @@ The main point here is an **each node is a static function rather than an object /// Classic sequencer node /// /// Blackboard object + /// Delegate receiving T and returning Status + /// Delegate receiving T and returning Status + /// Optional delegate receiving T and returning Status + /// Optional delegate receiving T and returning Status + /// Optional delegate receiving T and returning Status + /// Optional delegate receiving T and returning Status + /// Optional delegate receiving T and returning Status + /// Optional delegate receiving T and returning Status + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Status Sequencer(T board, + public static Status Sequencer(this T board, Func f1, Func f2, Func f3 = null, @@ -223,16 +341,14 @@ The main point here is an **each node is a static function rather than an object #endif ``` -There is one peculiar detail here: the Selector and Sequencer functions are implemented with multiple arguments that have default values, effectively functioning as variadic functions, instead of using the classic params arrays. +There is one notable detail: the Selector and Sequencer functions use multiple arguments with default values, effectively acting as variadic functions, instead of relying on the classic `params` arrays. -This choice is intentional, not a bug. The params arrays feature is not memory-efficient because it creates a new array on the heap each time the function is executed. +This design is intentional. `params` arrays are inefficient because they create a new array on the heap every time the function is called. -The downside of the chosen method is the limited maximum number of child nodes (8). However, this is not a significant issue. -This number (8) was chosen based on practical experience and is almost always sufficient for behavior tree logic. -If more child nodes are needed, there are two simple solutions: +The drawback of this approach is a limit on the maximum number of child nodes (8). In practice, however, this is rarely an issue. The number 8 was chosen based on experience and is usually sufficient for behavior tree logic. If more nodes are required, there are two simple solutions: -1. Place several nodes hierarchically, one on top of the other. Each additional "level" in this hierarchy exponentially increases the number of child nodes. -1. Modify the Sequencer and Selector functions to add more input arguments (this takes about 10 minutes of coding). +1. Organize nodes hierarchically, placing them one inside another. Each extra "level" in the hierarchy exponentially increases the possible number of children. +1. Extend the Sequencer and Selector functions by adding more input arguments (this takes about 10 minutes of coding). ## Functional Behavior Tree pattern code for C#13 (not for Unity) @@ -290,125 +406,18 @@ For more details, see the official documentation on [params collections](https:/ However, since Unity likely won't support C# 13 in the near future, you'll need to use a different approach, as shown earlier. While this alternative is not as syntactically elegant, it is still an efficient solution for handling multiple arguments. -### Note -No actions are required to switch between "*Unity mode*" and "*C#13 mode*" because this is handled automatically using preprocessor directives (`#if !NET9_0_OR_GREATER`) in the code. -If C#13 is supported, it will be used automatically. - - -## Library Versions -For convenience, the tree is split into three separate classes that inherit from each other. - -- LightestFBT: The lightest tree, where only four classic nodes are available: Action, Inverter, Selector, and Sequencer. If you're new to Behavior Trees, I recommend starting with this class. -- ExtendedFbt: An extended version with additional, user-friendly nodes. It's essentially syntactic sugar. -Includes an Actions() node for performing Action-type functions (which can be quite useful). -Conditional nodes that evaluate a boolean condition via a delegate before executing actions. -- ExperimentalFbt: This contains nodes whose usefulness I am still uncertain about, or those still under development. - -## Conclusions - -### Ease of Definition and Debugging - -The library was designed to provide an easy-to-debug, textual (C#) solution, and this goal has been fully achieved. -The behavior tree implementation is compact, and debugging is intuitive, leveraging your IDE’s capabilities. -You can set breakpoints anywhere in the code, and they will trigger correctly when that part of the tree is executed, making it easy to trace and understand the tree’s behavior. - -### Memory Consumption - -The BT code is extremely lightweight. -- The tree structure is not stored in memory; it is defined directly in the code as nested functions. -- No memory is allocated for delegates, as only static delegates and static functions are used. -- Passing parameters explicitly to functions, without relying on *params arrays*, thus avoiding heap allocations. - -### Performance - -The code is designed for high performance as it avoids complex operations such as memory allocation suach as GS, hashtables etc. Instead, it relies on simple operations: conditional operators, basic loops, direct method calls. -These minimal operations ensure that the code runs efficiently with little overhead, and further optimization is likely unnecessary. Any performance bottlenecks are more likely to arise from other parts of your code rather than from this implementation. - -#### Potential Bottleneck with DOTS, Jobs, and Burst -If your project is fully built on DOTS and you are actively using Jobs and the Burst compiler for speed optimization, FBT could potentially become a bottleneck. The reason is that the library is built around delegates, which are reference types and not compatible with Jobs and Burst optimizations. - -#### Potential Solution -If this becomes a problem, one potential solution could involve transitioning from delegates to function pointers. Function pointers are value types and could be more compatible with Jobs and Burst, potentially improving performance in such scenarios. - -Please let me know if this becomes an issue, and I'd be happy to explore possible adjustments. - -### Asynchronous Operations - -A limitation of the current solution is its handling of asynchronous operations. - -For example, when performing raycasts, especially with a large number of NPCs, it is often more efficient to batch the raycasts operations. In this case, it would be ideal to pause the tree, wait for the raycast results, and then continue execution on the next frame. - -This can be easily achieved with asynchronous functions. However, since this implementation uses regular functions instead of async ones, implementing this behavior requires introducing additional flags within the blackboard and checking them in conditions. - -It may make sense to develop an asynchronous version of this pattern. While it would be more convenient for such cases, it could be less efficient in terms of memory and performance. However, the performance loss is likely to be negligible compared to other bottlenecks in your code, and the added convenience may be more valuable. - -To solve this problem I plan to create an asynchronous version of the tree, supporting both standard async/await and UniTask. - -## Development Plans - -The core idea has been successfully implemented, and the result is close to 100% of the expected outcome. The planned development will be evolutionary, including: -- Enhancing the library based on user feedback. -- Optimization, if good ideas arise. -- Adding more nodes. -- Improving the handling of variable argument lists. -- Attempting to reduce boilerplate code in behavior trees (although this is unlikely, it’s not ruled out). - -Additionally, I’m considering the creation of a more advanced behavior tree based on the same functional principles, such as an asynchronous tree (using async functions) or an event-driven tree. However, it’s still unclear whether these more complex BT models would justify the time and effort required to implement them. - ---- -# Functional Behavior Tree Philosophy - -This section explains the design philosophy, implementation details, and optimizations behind the Functional Behavior Tree (BT) library. It also highlights key constraints, the reasoning behind chosen approaches, and solutions to specific challenges. - -## Initial Constraints - -The library was designed with the following limitations in mind: - -1. **Classic Behavior Tree Execution:** - - Implements a traditional behavior tree that executes completely during each game loop cycle, rather than adopting an event-driven approach. - -2. **Code-Only Implementation:** - - Focuses solely on C# code, avoiding the need for visual editors (e.g., Behavior Designer) or custom languages (e.g., PandaBT). - -3. **Separation of Concerns:** - - The behavior tree and the managed object (commonly referred to as the Blackboard) are treated as separate entities. Nodes operate on the Blackboard shared across all nodes within the same tree instance. - - -## Behavior Trees: A Functional Approach - -Many different implementations of Behaviour Trees in Unity involve a node editor, a large set of nodes, a runtime system, and debugging tools, along with a lot of internal service code whose efficiency is often uncertain. However, most of these implementations treat the tree as a graph, which leads to unnecessarily complex designs, where nodes are represented as objects requiring individual classes and intricate visual editors. As a result, debugging becomes challenging due to the separation of node creation and execution across various parts of the codebase, making specialized tools necessary. - -### The Functional Alternative - -To truly understand the Behaviour Tree, we should think of it as a function, not a graph. - -Indeed, each node in the BT is a function (not an object!), with multiple inputs and a single output, and it doesn’t require memory (there is no need to store any state between function calls). Therefore, the tree as a whole, or any of its subtrees, is also a function that calls the functions of its child nodes. - -In this library: - -- **Nodes are functions, not objects:** - - Each node is a function with multiple inputs and one output, avoiding the need for memory allocation. - -- **Tree as a recursive function:** - - The behavior tree is a recursive function that calls nested node functions. - -- **Simplified debugging:** - - Functional programming principles ensure clarity, making code easy to debug using standard IDE tools. - -This approach results in clean, readable, and efficient code with minimal boilerplate, all while being fast and memory-efficient. - --- ## Achieving Zero Memory Allocation ### The Problem -Below is the code from an earlier version of the pattern: +Below is the code from an earlier version of the pattern. ```csharp - public class MyLaconicFunctionalBt : LaconicFunctionalBt + public class MyFunctionalBt : FunctionalBt { - public MyLaconicFunctionalBt(ActorBoard board) : base(board) { } + public MyFunctionalBt(ActorBoard board) : base(board) { } public Status Execute() { @@ -436,25 +445,25 @@ It works not so bad but unfortunatelly allocated significant memory due to: 2. **Dynamic Arrays:** - Functions like `Sequencer` and `Selector` used `params`, leading to heap-allocated arrays for each call. -Memory allocation during each game loop cycle, especially for large scenes with many NPCs, caused performance bottlenecks. +Memory allocation during each game loop cycle, especially for large scenes with many NPCs, caused performance bottlenecks. +By the way, Fluid Behavior Tree library have the same memory allocation problems. ### Solutions I eventually switched to the following implementation: ```csharp - public class MyLaconicFunctionalBt : LaconicFunctionalBt + public static class MyFunctionalBt { - public static Status Execute(ActorBoard b) - { - return - Selector( - b => b.SetColor(Color.grey), - b => b.SetColor(Color.red)); - } + public static Status Execute(ActorBoard b) => + b.Selector( + b => b.SetColor(Color.grey), + b => b.SetColor(Color.red)); } - public class + + public class ActorBoard { + //..... Status SetColor(Color color) { View.SetColor(color); @@ -498,12 +507,63 @@ Dynamic memory allocation for `params` was addressed by: 3. **Auto-Generated Functions:** - For advanced use cases, additional overloads (e.g., for up to 20 parameters) can be generated to optimize performance further. -## Conclusion +### Note +No actions are required to switch between "*Unity mode*" and "*C#13 mode*" because this is handled automatically using preprocessor directives (`#if !NET9_0_OR_GREATER`) in the code. +If C#13 is supported, it will be used automatically. + +## Conclusions + +### Ease of Definition and Debugging -The Functional Behavior Tree library provides a streamlined, efficient, and modern solution to behavior tree implementation. By leveraging functional programming principles, memory optimizations, and modern C# features, it delivers: +The library was designed to provide an easy-to-debug, textual (C#) solution, and this goal has been fully achieved. +The behavior tree implementation is compact, and debugging is intuitive, leveraging your IDE’s capabilities. +You can set breakpoints anywhere in the code, and they will trigger correctly when that part of the tree is executed, making it easy to trace and understand the tree’s behavior. -- Simple and effective debugging. -- Zero runtime memory allocation. -- Clean and readable syntax. +### Memory Consumption -While some compromises were necessary for compatibility, the overall design remains robust and aligns closely with the original vision. +The BT code is extremely lightweight. +- The tree structure is not stored in memory; it is defined directly in the code as nested functions. +- No memory is allocated for delegates, as only static delegates and static functions are used. +- Passing parameters explicitly to functions, without relying on *params arrays*, thus avoiding heap allocations. + +### Performance + +The code is designed for high performance as it avoids complex operations such as memory allocation suach as GS, hashtables etc. Instead, it relies on simple operations: conditional operators, basic loops, direct method calls. +These minimal operations ensure that the code runs efficiently with little overhead, and further optimization is likely unnecessary. Any performance bottlenecks are more likely to arise from other parts of your code rather than from this implementation. + +#### Potential Bottleneck with DOTS, Jobs, and Burst +If your project is fully built on DOTS and you are actively using Jobs and the Burst compiler for speed optimization, FBT could potentially become a bottleneck. The reason is that the library is built around delegates, which are reference types and not compatible with Jobs and Burst optimizations. + +#### Potential Solutions + +So far I haven’t needed to optimize this solution in practice, but there is definitely room for optimization. + +Since FBT and UniTaskFBT rely on reference types (delegates), they cannot be placed entirely into Unity Jobs. However, there are several other approaches worth exploring: + +- **Parallel execution of the tree.** + A synchronous tree can be parallelized with `Parallel.For`. In experiments with many NPCs this gave a 2–5x speedup (depending on device and build type, mono/IL2CPP) compared to a regular `for` loop. The caveat is that the code must be thread-safe, and engine functions cannot be called from inside `Parallel.For`. + +- **Batching engine calls.** + The tree nodes themselves are lightweight; the heavy part is engine calls triggered from the tree. One optimization is batching, e.g. using `RaycastCommand`. Instead of calling `Physics.Raycast` directly from the tree, commands can be queued and executed in a batch before the next tick, with the tree resuming on results. This also works well in combination with `Parallel.For`. + +- **Running the tree on a separate thread.** + In theory the tree could be executed in its own thread. In practice this likely brings all the downsides of multithreading without much benefit. + +### Asynchronous Operations + +A limitation of the current solution is its handling of asynchronous operations. + +For example, when performing raycasts, especially with a large number of NPCs, it is often more efficient to batch the raycasts operations. In this case, it would be ideal to pause the tree, wait for the raycast results, and then continue execution on the next frame. + +To solve this problem I started a new project [Unitask Functional Behavior Tree](https://github.com/dmitrybaltin/UnitaskFBT) based on async functions. + +# Development Plans + +The core idea has been successfully implemented, and the result is close to 100% of the expected outcome. The planned development will be evolutionary, including: +- Enhancing the library based on user feedback. +- Optimization, if good ideas arise. +- Adding more nodes. +- Improving the handling of variable argument lists. +- Attempting to reduce boilerplate code in behavior trees (although this is unlikely, it’s not ruled out). + +Additionally, I’m considering the creation of a more advanced behavior tree based on the same functional principles, such as an asynchronous tree (using async functions) or an event-driven tree. However, it’s still unclear whether these more complex BT models would justify the time and effort required to implement them. diff --git a/Runtime/ExtendedFBT.cs b/Runtime/ExtendedNodes.cs similarity index 99% rename from Runtime/ExtendedFBT.cs rename to Runtime/ExtendedNodes.cs index 5f84979..34f3e08 100644 --- a/Runtime/ExtendedFBT.cs +++ b/Runtime/ExtendedNodes.cs @@ -8,7 +8,7 @@ namespace Baltin.FBT /// Extended version of Functional Behaviour tree containing some additional convenient nodes /// /// Blackboard type - public static class ExtendedFbt + public static class ExtendedNodes { /// /// Action node, using Action as a delegate to execute diff --git a/Runtime/ExtendedFBT.cs.meta b/Runtime/ExtendedNodes.cs.meta similarity index 100% rename from Runtime/ExtendedFBT.cs.meta rename to Runtime/ExtendedNodes.cs.meta diff --git a/Runtime/LightestFBT.cs b/Runtime/MainNodes.cs similarity index 99% rename from Runtime/LightestFBT.cs rename to Runtime/MainNodes.cs index 69eeeef..31c3df7 100644 --- a/Runtime/LightestFBT.cs +++ b/Runtime/MainNodes.cs @@ -42,7 +42,7 @@ public static Status ToStatus(this bool value) => /// 3. Extremely fast, because here inside there are only the simplest conditions, loops and procedure calls /// /// A type of 'blackboard' that is an interface to the data and behavior of the controlled object. - public static class LightestFbt + public static class MainNodes { /// /// Classic inverter node diff --git a/Runtime/LightestFBT.cs.meta b/Runtime/MainNodes.cs.meta similarity index 100% rename from Runtime/LightestFBT.cs.meta rename to Runtime/MainNodes.cs.meta diff --git a/Runtime/ParallelFbtNodes.cs b/Runtime/ParallelNodes.cs similarity index 99% rename from Runtime/ParallelFbtNodes.cs rename to Runtime/ParallelNodes.cs index 8a006e6..f61ca61 100644 --- a/Runtime/ParallelFbtNodes.cs +++ b/Runtime/ParallelNodes.cs @@ -18,7 +18,7 @@ public enum ParallelPolicy /// Here are some controversial nodes that I am not sure are necessary. /// /// Blackboard type - public static class ParallelFbtNodes + public static class ParallelNodes { /// /// Current status of the parallel node aggregating statuses of all the children nodes diff --git a/Runtime/ParallelFbtNodes.cs.meta b/Runtime/ParallelNodes.cs.meta similarity index 100% rename from Runtime/ParallelFbtNodes.cs.meta rename to Runtime/ParallelNodes.cs.meta diff --git a/package.json b/package.json index 49cf6b8..6a9d4f3 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "com.baltin.fbt", - "version": "0.5.4", + "version": "0.6.0", "displayName": "FunctionalBT", "description": "Functional Behavior Tree Design Pattern Implementation", - "unity": "2019.3", - "keywords": ["unity", "tools", "library", "behavior tree", "ai", "npc", "async", "task"], + "unity": "2021.2", + "keywords": ["unity", "tools", "library", "behavior tree", "ai", "npc"], "author": { "name": "Dmitry Baltin", "email": "baltin.dmitry@gmail.com",