diff --git a/packages/flame_behavior_tree/.metadata b/packages/flame_behavior_tree/.metadata
new file mode 100644
index 000000000..48326e7c2
--- /dev/null
+++ b/packages/flame_behavior_tree/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "bae5e49bc2a867403c43b2aae2de8f8c33b037e4"
+ channel: "stable"
+
+project_type: package
diff --git a/packages/flame_behavior_tree/CHANGELOG.md b/packages/flame_behavior_tree/CHANGELOG.md
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/flame_behavior_tree/LICENSE b/packages/flame_behavior_tree/LICENSE
new file mode 100644
index 000000000..0cf87ff46
--- /dev/null
+++ b/packages/flame_behavior_tree/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Blue Fire
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/flame_behavior_tree/README.md b/packages/flame_behavior_tree/README.md
new file mode 100644
index 000000000..8740e79e0
--- /dev/null
+++ b/packages/flame_behavior_tree/README.md
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+This is a bridge package that integrates the behavior_tree dart package with Flame engine.
+
+
+
+
+
+
+
+
+
+---
+
+
+
+## Features
+
+This package provides a `HasBehaviorTree` mixin for Flame `Components`. It can be added to any
+`Component` and it takes care of ticking the behavior tree along with the component's update.
+
+
+## Getting started
+
+Add this package to your Flutter project using:
+
+```bash
+flutter pub add flame_behavior_tree
+```
+
+
+## Usage
+
+- Add the `HasBehaviorTree` mixin to the component that wants to follow a certain AI behavior.
+
+ ```dart
+ class MyComponent extends Position with HasBehaviorTree {
+
+ }
+ ```
+
+- Set-up a behavior tree and set its root as the `treeRoot` of the `HasBehaviorTree`.
+
+```dart
+class MyComponent extends PositionComponent with HasBehaviorTree {
+ Future onLoad() async {
+ treeRoot = Selector(
+ children: [
+ Sequence(children: [task1, condition, task2]),
+ Sequence(...),
+ ]
+ );
+ super.onLoad();
+ }
+}
+```
+
+- Increase the `tickInterval` to make the tree tick less frequently.
+
+```dart
+class MyComponent extends PositionComponent with HasBehaviorTree {
+ Future onLoad() async {
+ treeRoot = Selector(...);
+ tickInterval = 4;
+ super.onLoad();
+ }
+}
+```
+
+
+## Additional information
+
+When working with behavior trees, keep in mind that
+
+- nodes of a behavior tree do not necessarily update on every frame.
+- avoid storing data in nodes as much as possible because it can go out of sync with rest of the
+game as nodes are not ticked on every frame.
diff --git a/packages/flame_behavior_tree/analysis_options.yaml b/packages/flame_behavior_tree/analysis_options.yaml
new file mode 100644
index 000000000..ba5631f3b
--- /dev/null
+++ b/packages/flame_behavior_tree/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:flame_lint/analysis_options.yaml
\ No newline at end of file
diff --git a/packages/flame_behavior_tree/behavior_tree/CHANGELOG.md b/packages/flame_behavior_tree/behavior_tree/CHANGELOG.md
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/flame_behavior_tree/behavior_tree/LICENSE b/packages/flame_behavior_tree/behavior_tree/LICENSE
new file mode 100644
index 000000000..0cf87ff46
--- /dev/null
+++ b/packages/flame_behavior_tree/behavior_tree/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Blue Fire
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/flame_behavior_tree/behavior_tree/README.md b/packages/flame_behavior_tree/behavior_tree/README.md
new file mode 100644
index 000000000..b29e372ac
--- /dev/null
+++ b/packages/flame_behavior_tree/behavior_tree/README.md
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+This package provides a simple and easy to use behavior tree API in pure dart.
+
+
+
+
+
+
+
+
+
+---
+
+
+
+Behavior tree is a very common way of implementing AI behavior in game and robotics. Using this, you
+can break-down a complex behavior of an in game AI, into multiple smaller nodes.
+
+
+## Features
+
+- Nodes
+ - Composite
+ - Sequence: Continues execution until one of the children fails.
+ - Selector: Continues execution until one of the children succeeds.
+ - Decorator
+ - Inverter: Flips the status of the child node.
+ - Limiter: Limits the number of ticks for child node.
+ - Task
+ - Task: Executes a given callback when ticked.
+ - AsyncTask: Executes an async callback when ticked.
+ - Condition: Checks a condition when ticked.
+
+
+## Getting started
+
+Add this package to your dart project using,
+
+```bash
+dart pub add behavior_tree
+```
+
+
+## Usage
+
+- Create a behavior tree.
+
+```dart
+final treeRoot = Sequence(
+ children: [
+ Condition(() => isHungry),
+ Task(() => goToShop()),
+ Task(() => buyFood()),
+ Task(() => goToHome()),
+ Task(() => eatFood()),
+ ]
+);
+```
+
+- Tick the root node to update the tree.
+
+```dart
+final treeRoot = ...;
+treeRoot.tick();
+```
diff --git a/packages/flame_behavior_tree/behavior_tree/analysis_options.yaml b/packages/flame_behavior_tree/behavior_tree/analysis_options.yaml
new file mode 100644
index 000000000..85732fa02
--- /dev/null
+++ b/packages/flame_behavior_tree/behavior_tree/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:flame_lint/analysis_options.yaml
diff --git a/packages/flame_behavior_tree/behavior_tree/example/behavior_tree_example.dart b/packages/flame_behavior_tree/behavior_tree/example/behavior_tree_example.dart
new file mode 100644
index 000000000..b45d76675
--- /dev/null
+++ b/packages/flame_behavior_tree/behavior_tree/example/behavior_tree_example.dart
@@ -0,0 +1,39 @@
+import 'package:behavior_tree/behavior_tree.dart';
+
+bool isHungry = true;
+
+void main() {
+ // Create a sequence of tasks
+ final treeRoot = Sequence(
+ children: [
+ Condition(() => isHungry),
+ Task(goToShop),
+ Task(buyFood),
+ Task(goToHome),
+ Task(eatFood),
+ ],
+ );
+
+ // Tick the tree
+ treeRoot.tick();
+}
+
+NodeStatus goToShop() {
+ // Go to the shop
+ return NodeStatus.success;
+}
+
+NodeStatus buyFood() {
+ // Buy food
+ return NodeStatus.success;
+}
+
+NodeStatus goToHome() {
+ // Go home
+ return NodeStatus.success;
+}
+
+NodeStatus eatFood() {
+ // Eat food
+ return NodeStatus.success;
+}
diff --git a/packages/flame_behavior_tree/behavior_tree/lib/behavior_tree.dart b/packages/flame_behavior_tree/behavior_tree/lib/behavior_tree.dart
new file mode 100644
index 000000000..c6e6ece57
--- /dev/null
+++ b/packages/flame_behavior_tree/behavior_tree/lib/behavior_tree.dart
@@ -0,0 +1,12 @@
+/// Behavior tree implementation in dart
+library behavior_tree;
+
+export 'src/base_node.dart';
+export 'src/composites/selector.dart';
+export 'src/composites/sequence.dart';
+export 'src/decorators/inverter.dart';
+export 'src/decorators/limiter.dart';
+export 'src/node.dart';
+export 'src/tasks/async_task.dart';
+export 'src/tasks/condition.dart';
+export 'src/tasks/task.dart';
diff --git a/packages/flame_behavior_tree/behavior_tree/lib/src/base_node.dart b/packages/flame_behavior_tree/behavior_tree/lib/src/base_node.dart
new file mode 100644
index 000000000..8c67e57ec
--- /dev/null
+++ b/packages/flame_behavior_tree/behavior_tree/lib/src/base_node.dart
@@ -0,0 +1,21 @@
+import 'package:behavior_tree/behavior_tree.dart';
+import 'package:meta/meta.dart';
+
+/// A base class for all the nodes.
+abstract class BaseNode implements NodeInterface {
+ NodeStatus _status = NodeStatus.notStarted;
+
+ @override
+ NodeStatus get status => _status;
+
+ @override
+ set status(NodeStatus value) {
+ _status = value;
+ }
+
+ @override
+ @mustCallSuper
+ void reset() {
+ _status = NodeStatus.notStarted;
+ }
+}
diff --git a/packages/flame_behavior_tree/behavior_tree/lib/src/composites/selector.dart b/packages/flame_behavior_tree/behavior_tree/lib/src/composites/selector.dart
new file mode 100644
index 000000000..a4a12cad8
--- /dev/null
+++ b/packages/flame_behavior_tree/behavior_tree/lib/src/composites/selector.dart
@@ -0,0 +1,31 @@
+import 'package:behavior_tree/behavior_tree.dart';
+
+/// A composite node that stops at its first non-failing child node.
+class Selector extends BaseNode implements NodeInterface {
+ /// Creates a selector node for given [children] nodes.
+ Selector({List? children})
+ : _children = children ?? [];
+
+ final List _children;
+
+ @override
+ void tick() {
+ for (final node in _children) {
+ node.tick();
+
+ if (node.status != NodeStatus.failure) {
+ status = node.status;
+ return;
+ }
+ }
+ status = NodeStatus.failure;
+ }
+
+ @override
+ void reset() {
+ for (final node in _children) {
+ node.reset();
+ }
+ super.reset();
+ }
+}
diff --git a/packages/flame_behavior_tree/behavior_tree/lib/src/composites/sequence.dart b/packages/flame_behavior_tree/behavior_tree/lib/src/composites/sequence.dart
new file mode 100644
index 000000000..8e32b01b7
--- /dev/null
+++ b/packages/flame_behavior_tree/behavior_tree/lib/src/composites/sequence.dart
@@ -0,0 +1,31 @@
+import 'package:behavior_tree/behavior_tree.dart';
+
+/// A composite node that stops at its first successful child node.
+class Sequence extends BaseNode implements NodeInterface {
+ /// Creates a sequence node for given [children] nodes.
+ Sequence({List? children})
+ : _children = children ?? [];
+
+ final List _children;
+
+ @override
+ void tick() {
+ for (final node in _children) {
+ node.tick();
+
+ if (node.status != NodeStatus.success) {
+ status = node.status;
+ return;
+ }
+ }
+ status = NodeStatus.success;
+ }
+
+ @override
+ void reset() {
+ for (final node in _children) {
+ node.reset();
+ }
+ super.reset();
+ }
+}
diff --git a/packages/flame_behavior_tree/behavior_tree/lib/src/decorators/inverter.dart b/packages/flame_behavior_tree/behavior_tree/lib/src/decorators/inverter.dart
new file mode 100644
index 000000000..776dbf887
--- /dev/null
+++ b/packages/flame_behavior_tree/behavior_tree/lib/src/decorators/inverter.dart
@@ -0,0 +1,39 @@
+import 'package:behavior_tree/behavior_tree.dart';
+
+/// A decorator node that inverts [child]'s status if it is not
+/// [NodeStatus.running].
+class Inverter extends BaseNode implements NodeInterface {
+ /// Creates an inverter node for given [child] node.
+ Inverter(this.child) {
+ _invertStatus();
+ }
+
+ /// The child node whose status needs to be inverted.
+ final NodeInterface child;
+
+ @override
+ void tick() {
+ child.tick();
+ _invertStatus();
+ }
+
+ void _invertStatus() {
+ switch (child.status) {
+ case NodeStatus.notStarted:
+ status = NodeStatus.notStarted;
+ case NodeStatus.running:
+ status = NodeStatus.running;
+ case NodeStatus.success:
+ status = NodeStatus.failure;
+ case NodeStatus.failure:
+ status = NodeStatus.success;
+ }
+ }
+
+ @override
+ void reset() {
+ super.reset();
+ child.reset();
+ _invertStatus();
+ }
+}
diff --git a/packages/flame_behavior_tree/behavior_tree/lib/src/decorators/limiter.dart b/packages/flame_behavior_tree/behavior_tree/lib/src/decorators/limiter.dart
new file mode 100644
index 000000000..57086f604
--- /dev/null
+++ b/packages/flame_behavior_tree/behavior_tree/lib/src/decorators/limiter.dart
@@ -0,0 +1,48 @@
+import 'package:behavior_tree/behavior_tree.dart';
+
+/// A decorator node that limits the number of times [child] can be ticked.
+class Limiter extends BaseNode implements NodeInterface {
+ /// Creates a limiter node for given [child] node and [limit].
+ ///
+ /// Once this node has been ticked [limit] number of times, it stops ticking
+ /// the child node. After this, [status] will keep returning the status of
+ /// child the last time it was ticked. This behavior can be overridden by
+ /// providing an optional [statusAfterLimit].
+ Limiter(
+ this.child,
+ this.limit, {
+ NodeStatus? statusAfterLimit,
+ }) : _statusAfterLimit = statusAfterLimit {
+ status =
+ (_tickCount < limit) ? child.status : _statusAfterLimit ?? child.status;
+ }
+
+ var _tickCount = 0;
+ final NodeStatus? _statusAfterLimit;
+
+ /// The child node whose ticks are to be limited.
+ final NodeInterface child;
+
+ /// The max number of times [child] can be ticked.
+ final int limit;
+
+ /// Returns the number of times [child] has been ticked.
+ int get tickCount => _tickCount;
+
+ @override
+ void tick() {
+ if (_tickCount < limit) {
+ child.tick();
+ ++_tickCount;
+ }
+ status =
+ (_tickCount < limit) ? child.status : _statusAfterLimit ?? child.status;
+ }
+
+ @override
+ void reset() {
+ _tickCount = 0;
+ child.reset();
+ super.reset();
+ }
+}
diff --git a/packages/flame_behavior_tree/behavior_tree/lib/src/node.dart b/packages/flame_behavior_tree/behavior_tree/lib/src/node.dart
new file mode 100644
index 000000000..9bed85a11
--- /dev/null
+++ b/packages/flame_behavior_tree/behavior_tree/lib/src/node.dart
@@ -0,0 +1,33 @@
+import 'package:behavior_tree/behavior_tree.dart';
+
+/// The valid values for status of a node.
+enum NodeStatus {
+ /// Indicates that the node has not been ticked yet.
+ notStarted,
+
+ /// Indicates that the node is running.
+ running,
+
+ /// Indicates that the node has completed successfully.
+ success,
+
+ /// Indicates that the node has failed.
+ failure,
+}
+
+/// An interface which all the nodes implement.
+///
+/// Some examples are [Selector], [Sequence], [Inverter] and [Limiter].
+abstract interface class NodeInterface {
+ /// Returns the current status of this node.
+ NodeStatus get status;
+
+ /// Sets the status of this node.
+ set status(NodeStatus value);
+
+ /// Updates the node and re-evaluates its status.
+ void tick();
+
+ /// Resets the node to its initial state.
+ void reset();
+}
diff --git a/packages/flame_behavior_tree/behavior_tree/lib/src/tasks/async_task.dart b/packages/flame_behavior_tree/behavior_tree/lib/src/tasks/async_task.dart
new file mode 100644
index 000000000..aa06bf494
--- /dev/null
+++ b/packages/flame_behavior_tree/behavior_tree/lib/src/tasks/async_task.dart
@@ -0,0 +1,27 @@
+import 'dart:async';
+
+import 'package:behavior_tree/behavior_tree.dart';
+
+typedef AsyncTaskCallback = Future Function();
+
+/// This is a leaf node that will execute the given async task when ticked.
+/// While the callback is executing, this node will report [status] as
+/// [NodeStatus.running]. Once the callback finishes, the status will be updated
+/// to the returned value of the callback.
+class AsyncTask extends BaseNode implements NodeInterface {
+ /// Creates an async task node for given [callback].
+ AsyncTask(AsyncTaskCallback callback) : _callback = callback;
+
+ final AsyncTaskCallback _callback;
+
+ @override
+ void tick() {
+ if (status != NodeStatus.running) {
+ status = NodeStatus.running;
+
+ _callback().then((returnedStatus) {
+ status = returnedStatus;
+ });
+ }
+ }
+}
diff --git a/packages/flame_behavior_tree/behavior_tree/lib/src/tasks/condition.dart b/packages/flame_behavior_tree/behavior_tree/lib/src/tasks/condition.dart
new file mode 100644
index 000000000..1e1c37135
--- /dev/null
+++ b/packages/flame_behavior_tree/behavior_tree/lib/src/tasks/condition.dart
@@ -0,0 +1,18 @@
+import 'package:behavior_tree/behavior_tree.dart';
+
+typedef ConditionCallback = bool Function();
+
+/// This is a leaf node that will updates its [status] based on
+/// [conditionCallback].
+class Condition extends BaseNode {
+ /// Creates a condition node for given [conditionCallback].
+ Condition(this.conditionCallback);
+
+ /// The callback that will be executed when the condition is ticked.
+ final ConditionCallback conditionCallback;
+
+ @override
+ void tick() {
+ status = conditionCallback() ? NodeStatus.success : NodeStatus.failure;
+ }
+}
diff --git a/packages/flame_behavior_tree/behavior_tree/lib/src/tasks/task.dart b/packages/flame_behavior_tree/behavior_tree/lib/src/tasks/task.dart
new file mode 100644
index 000000000..7ffc147ed
--- /dev/null
+++ b/packages/flame_behavior_tree/behavior_tree/lib/src/tasks/task.dart
@@ -0,0 +1,17 @@
+import 'package:behavior_tree/behavior_tree.dart';
+
+/// The type of callback used by the [Task] node.
+typedef TaskCallback = NodeStatus Function();
+
+/// This is a leaf node that will execute the given task when ticked.
+class Task extends BaseNode implements NodeInterface {
+ /// Creates a task node for given [taskCallback].
+ Task(this.taskCallback);
+
+ /// The callback that will be executed when the task is ticked.
+ /// It should return the status of the task.
+ final TaskCallback taskCallback;
+
+ @override
+ void tick() => status = taskCallback();
+}
diff --git a/packages/flame_behavior_tree/behavior_tree/pubspec.yaml b/packages/flame_behavior_tree/behavior_tree/pubspec.yaml
new file mode 100644
index 000000000..840acab01
--- /dev/null
+++ b/packages/flame_behavior_tree/behavior_tree/pubspec.yaml
@@ -0,0 +1,20 @@
+name: behavior_tree
+description: A behavior tree implementation written in dart. This package is designed to be used for implementing AI in games.
+version: 0.1.0
+repository: https://github.com/flame-engine/flame/tree/main/packages/flame_behavior_tree/behavior_tree
+funding:
+ - https://opencollective.com/blue-fire
+ - https://github.com/sponsors/bluefireteam
+ - https://patreon.com/bluefireoss
+
+environment:
+ sdk: ">=3.0.0 <4.0.0"
+
+dependencies:
+ meta: ^1.9.1
+
+dev_dependencies:
+ flame_lint: ^1.1.2
+ lints: ^3.0.0
+ mocktail: ^1.0.1
+ test: any
diff --git a/packages/flame_behavior_tree/behavior_tree/test/async_task_test.dart b/packages/flame_behavior_tree/behavior_tree/test/async_task_test.dart
new file mode 100644
index 000000000..65e71f9f7
--- /dev/null
+++ b/packages/flame_behavior_tree/behavior_tree/test/async_task_test.dart
@@ -0,0 +1,19 @@
+import 'package:behavior_tree/behavior_tree.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('AsyncTask', () {
+ test('sets status to running when ticked.', () {
+ final asyncTask = AsyncTask(() async => NodeStatus.success);
+ asyncTask.tick();
+ expect(asyncTask.status, NodeStatus.running);
+ });
+
+ test('updates status to the returned value of the callback.', () async {
+ final asyncTask = AsyncTask(() async => NodeStatus.failure);
+ asyncTask.tick();
+ await Future.delayed(Duration.zero); // Wait for the callback to complete
+ expect(asyncTask.status, equals(NodeStatus.failure));
+ });
+ });
+}
diff --git a/packages/flame_behavior_tree/behavior_tree/test/conditon_test.dart b/packages/flame_behavior_tree/behavior_tree/test/conditon_test.dart
new file mode 100644
index 000000000..335f0e686
--- /dev/null
+++ b/packages/flame_behavior_tree/behavior_tree/test/conditon_test.dart
@@ -0,0 +1,18 @@
+import 'package:behavior_tree/behavior_tree.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('Condition', () {
+ test('status is success when condition returns true', () {
+ final condition = Condition(() => true);
+ condition.tick();
+ expect(condition.status, NodeStatus.success);
+ });
+
+ test('status is failure when condition returns false', () {
+ final condition = Condition(() => false);
+ condition.tick();
+ expect(condition.status, NodeStatus.failure);
+ });
+ });
+}
diff --git a/packages/flame_behavior_tree/behavior_tree/test/inverter_test.dart b/packages/flame_behavior_tree/behavior_tree/test/inverter_test.dart
new file mode 100644
index 000000000..e6a93c33d
--- /dev/null
+++ b/packages/flame_behavior_tree/behavior_tree/test/inverter_test.dart
@@ -0,0 +1,45 @@
+import 'package:behavior_tree/behavior_tree.dart';
+import 'package:mocktail/mocktail.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('Inverter', () {
+ final alwaysFailure = _MockNode();
+ final alwaysSuccess = _MockNode();
+ final alwaysRunning = _MockNode();
+
+ setUp(() {
+ reset(alwaysFailure);
+ reset(alwaysSuccess);
+ reset(alwaysRunning);
+
+ when(() => alwaysFailure.status).thenReturn(NodeStatus.failure);
+ when(() => alwaysSuccess.status).thenReturn(NodeStatus.success);
+ when(() => alwaysRunning.status).thenReturn(NodeStatus.running);
+ });
+
+ test('can be instantiated.', () {
+ expect(() => Inverter(alwaysRunning), returnsNormally);
+ });
+
+ test('default status is inverted child status.', () {
+ final inverter = Inverter(alwaysSuccess);
+ expect(inverter.status, NodeStatus.failure);
+ });
+
+ test('inverts status of child.', () {
+ final inverter1 = Inverter(alwaysSuccess)..tick();
+ expect(inverter1.status, NodeStatus.failure);
+
+ final inverter2 = Inverter(alwaysFailure)..tick();
+ expect(inverter2.status, NodeStatus.success);
+ });
+
+ test('keeping running if child is running.', () {
+ final inverter = Inverter(alwaysRunning)..tick();
+ expect(inverter.status, NodeStatus.running);
+ });
+ });
+}
+
+class _MockNode extends Mock implements NodeInterface {}
diff --git a/packages/flame_behavior_tree/behavior_tree/test/limiter_test.dart b/packages/flame_behavior_tree/behavior_tree/test/limiter_test.dart
new file mode 100644
index 000000000..963442666
--- /dev/null
+++ b/packages/flame_behavior_tree/behavior_tree/test/limiter_test.dart
@@ -0,0 +1,71 @@
+import 'package:behavior_tree/behavior_tree.dart';
+import 'package:mocktail/mocktail.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('Limiter', () {
+ final alwaysFailure = _MockNode();
+ final alwaysSuccess = _MockNode();
+ final alwaysRunning = _MockNode();
+
+ setUp(() {
+ reset(alwaysFailure);
+ reset(alwaysSuccess);
+ reset(alwaysRunning);
+
+ when(() => alwaysFailure.status).thenReturn(NodeStatus.failure);
+ when(() => alwaysSuccess.status).thenReturn(NodeStatus.success);
+ when(() => alwaysRunning.status).thenReturn(NodeStatus.running);
+ });
+
+ test('can be instantiated.', () {
+ expect(() => Limiter(alwaysRunning, 5), returnsNormally);
+ });
+
+ test('default status same as status of child.', () {
+ final limiter = Limiter(alwaysSuccess, 5);
+ expect(limiter.status, alwaysSuccess.status);
+ });
+
+ test('limits tick count of child.', () {
+ const limit = 5;
+ final limiter = Limiter(alwaysRunning, limit);
+
+ var count = 0;
+ while (count < 23) {
+ limiter.tick();
+ ++count;
+ }
+
+ expect(limiter.tickCount, limit);
+ expect(limiter.status, alwaysRunning.status);
+ });
+
+ test('overrides status after crossing limit.', () {
+ const limit = 5;
+ final failAfterLimit = Limiter(
+ alwaysSuccess,
+ limit,
+ statusAfterLimit: NodeStatus.failure,
+ );
+
+ final succeedAfterLimit = Limiter(
+ alwaysRunning,
+ limit,
+ statusAfterLimit: NodeStatus.success,
+ );
+
+ var count = 0;
+ while (count < 23) {
+ failAfterLimit.tick();
+ succeedAfterLimit.tick();
+ ++count;
+ }
+
+ expect(failAfterLimit.status, NodeStatus.failure);
+ expect(succeedAfterLimit.status, NodeStatus.success);
+ });
+ });
+}
+
+class _MockNode extends Mock implements NodeInterface {}
diff --git a/packages/flame_behavior_tree/behavior_tree/test/selector_test.dart b/packages/flame_behavior_tree/behavior_tree/test/selector_test.dart
new file mode 100644
index 000000000..baef7ef3e
--- /dev/null
+++ b/packages/flame_behavior_tree/behavior_tree/test/selector_test.dart
@@ -0,0 +1,128 @@
+import 'package:behavior_tree/behavior_tree.dart';
+import 'package:mocktail/mocktail.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('Selector', () {
+ const nTries = 20;
+ final alwaysFailure = _MockNode();
+ final alwaysSuccess = _MockNode();
+ final alwaysRunning = _MockNode();
+ final successAfterTries = _StatusAfterNTries(nTries, NodeStatus.success);
+ final failureAfterTries = _StatusAfterNTries(nTries, NodeStatus.failure);
+
+ setUp(() {
+ reset(alwaysFailure);
+ reset(alwaysSuccess);
+ reset(alwaysRunning);
+
+ when(() => alwaysFailure.status).thenReturn(NodeStatus.failure);
+ when(() => alwaysSuccess.status).thenReturn(NodeStatus.success);
+ when(() => alwaysRunning.status).thenReturn(NodeStatus.running);
+
+ successAfterTries.reset();
+ failureAfterTries.reset();
+ });
+
+ test('can be instantiated without the children.', () {
+ expect(Selector.new, returnsNormally);
+ });
+
+ test('can be instantiated with the children.', () {
+ expect(
+ () => Selector(children: [Selector(), Selector()]),
+ returnsNormally,
+ );
+ });
+
+ test('default status is not started.', () {
+ final selector = Selector();
+ expect(selector.status, NodeStatus.notStarted);
+ });
+
+ test('can be ticked without the children.', () {
+ final selector = Selector();
+ expect(selector.tick, returnsNormally);
+ });
+
+ test('can be ticked with the children.', () {
+ final selector = Selector(children: [Selector(), Selector()]);
+ expect(selector.tick, returnsNormally);
+ });
+
+ test('succeeds if any one of the children succeeds.', () {
+ final selector = Selector(children: [alwaysFailure, alwaysSuccess])
+ ..tick();
+ expect(selector.status, NodeStatus.success);
+ });
+
+ test('fails if all of the children fail.', () {
+ final selector = Selector(children: [alwaysFailure, alwaysFailure])
+ ..tick();
+ expect(selector.status, NodeStatus.failure);
+ });
+
+ test('runs until all children fail.', () {
+ final selector = Selector(
+ children: [alwaysFailure, failureAfterTries],
+ );
+
+ var count = 0;
+ while (count <= nTries) {
+ selector.tick();
+
+ expect(
+ selector.status,
+ count == nTries ? NodeStatus.failure : NodeStatus.running,
+ );
+
+ ++count;
+ }
+
+ verify(alwaysFailure.tick).called(count);
+ expect(failureAfterTries.tickCount, count);
+ });
+
+ test('runs until one of the children succeeds.', () {
+ final selector = Selector(children: [successAfterTries, alwaysFailure]);
+
+ var count = 0;
+ while (count <= nTries) {
+ selector.tick();
+
+ expect(
+ selector.status,
+ count == nTries ? NodeStatus.success : NodeStatus.running,
+ );
+
+ ++count;
+ }
+
+ verifyNever(alwaysFailure.tick);
+ expect(successAfterTries.tickCount, count);
+ });
+ });
+}
+
+class _MockNode extends Mock implements NodeInterface {}
+
+class _StatusAfterNTries extends BaseNode implements NodeInterface {
+ _StatusAfterNTries(this.nTries, this.statusAfterTries);
+
+ final int nTries;
+ final NodeStatus statusAfterTries;
+
+ var _tickCount = 0;
+ int get tickCount => _tickCount;
+
+ @override
+ void tick() {
+ status = _tickCount++ < nTries ? NodeStatus.running : statusAfterTries;
+ }
+
+ @override
+ void reset() {
+ super.reset();
+ _tickCount = 0;
+ }
+}
diff --git a/packages/flame_behavior_tree/behavior_tree/test/sequence_test.dart b/packages/flame_behavior_tree/behavior_tree/test/sequence_test.dart
new file mode 100644
index 000000000..9d492d4e4
--- /dev/null
+++ b/packages/flame_behavior_tree/behavior_tree/test/sequence_test.dart
@@ -0,0 +1,128 @@
+import 'package:behavior_tree/behavior_tree.dart';
+import 'package:mocktail/mocktail.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('Sequence', () {
+ const nTries = 20;
+ final alwaysFailure = _MockNode();
+ final alwaysSuccess = _MockNode();
+ final alwaysRunning = _MockNode();
+ final successAfterTries = _StatusAfterNTries(nTries, NodeStatus.success);
+ final failureAfterTries = _StatusAfterNTries(nTries, NodeStatus.failure);
+
+ setUp(() {
+ reset(alwaysFailure);
+ reset(alwaysSuccess);
+ reset(alwaysRunning);
+
+ when(() => alwaysFailure.status).thenReturn(NodeStatus.failure);
+ when(() => alwaysSuccess.status).thenReturn(NodeStatus.success);
+ when(() => alwaysRunning.status).thenReturn(NodeStatus.running);
+
+ successAfterTries.reset();
+ failureAfterTries.reset();
+ });
+
+ test('can be instantiated without the children.', () {
+ expect(Sequence.new, returnsNormally);
+ });
+
+ test('can be instantiated with the children.', () {
+ expect(
+ () => Sequence(children: [Sequence(), Sequence()]),
+ returnsNormally,
+ );
+ });
+
+ test('default status is not started.', () {
+ final sequence = Sequence();
+ expect(sequence.status, NodeStatus.notStarted);
+ });
+
+ test('can be ticked without the children.', () {
+ final selector = Sequence();
+ expect(selector.tick, returnsNormally);
+ });
+
+ test('can be ticked with the children.', () {
+ final selector = Sequence(children: [Sequence(), Sequence()]);
+ expect(selector.tick, returnsNormally);
+ });
+
+ test('succeeds if all of the children succeed.', () {
+ final sequence = Sequence(children: [alwaysSuccess, alwaysSuccess])
+ ..tick();
+ expect(sequence.status, NodeStatus.success);
+ });
+
+ test('fails if any of the children fails.', () {
+ final sequence = Sequence(children: [alwaysSuccess, alwaysFailure])
+ ..tick();
+ expect(sequence.status, NodeStatus.failure);
+ });
+
+ test('runs until first failure.', () {
+ final sequence = Sequence(
+ children: [alwaysSuccess, failureAfterTries],
+ );
+
+ var count = 0;
+ while (count <= nTries) {
+ sequence.tick();
+
+ expect(
+ sequence.status,
+ count == nTries ? NodeStatus.failure : NodeStatus.running,
+ );
+
+ ++count;
+ }
+
+ verify(alwaysSuccess.tick).called(count);
+ expect(failureAfterTries.tickCount, count);
+ });
+
+ test('runs until all children succeed.', () {
+ final sequence = Sequence(children: [alwaysSuccess, successAfterTries]);
+
+ var count = 0;
+ while (count <= nTries) {
+ sequence.tick();
+
+ expect(
+ sequence.status,
+ count == nTries ? NodeStatus.success : NodeStatus.running,
+ );
+
+ ++count;
+ }
+
+ verify(alwaysSuccess.tick).called(count);
+ expect(successAfterTries.tickCount, count);
+ });
+ });
+}
+
+class _MockNode extends Mock implements NodeInterface {}
+
+class _StatusAfterNTries extends BaseNode implements NodeInterface {
+ _StatusAfterNTries(this.nTries, this.statusAfterTries);
+
+ final int nTries;
+ final NodeStatus statusAfterTries;
+
+ var _tickCount = 0;
+ int get tickCount => _tickCount;
+
+ @override
+ void tick() {
+ status = _tickCount++ < nTries ? NodeStatus.running : statusAfterTries;
+ }
+
+ @override
+ void reset() {
+ super.reset();
+ _tickCount = 0;
+ }
+}
diff --git a/packages/flame_behavior_tree/behavior_tree/test/task_test.dart b/packages/flame_behavior_tree/behavior_tree/test/task_test.dart
new file mode 100644
index 000000000..7b94edfd8
--- /dev/null
+++ b/packages/flame_behavior_tree/behavior_tree/test/task_test.dart
@@ -0,0 +1,25 @@
+import 'package:behavior_tree/behavior_tree.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('Task', () {
+ test('returns the status returned by the task callback', () {
+ const expectedStatus = NodeStatus.success;
+ final task = Task(() => expectedStatus);
+
+ task.tick();
+ expect(task.status, equals(expectedStatus));
+ });
+
+ test('executes the task callback when ticked', () {
+ var executed = false;
+ final task = Task(() {
+ executed = true;
+ return NodeStatus.success;
+ });
+
+ task.tick();
+ expect(executed, isTrue);
+ });
+ });
+}
diff --git a/packages/flame_behavior_tree/example/.metadata b/packages/flame_behavior_tree/example/.metadata
new file mode 100644
index 000000000..251f0182b
--- /dev/null
+++ b/packages/flame_behavior_tree/example/.metadata
@@ -0,0 +1,42 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "bae5e49bc2a867403c43b2aae2de8f8c33b037e4"
+ channel: "stable"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
+ base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
+ - platform: android
+ create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
+ base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
+ - platform: linux
+ create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
+ base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
+ - platform: macos
+ create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
+ base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
+ - platform: web
+ create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
+ base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
+ - platform: windows
+ create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
+ base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/packages/flame_behavior_tree/example/analysis_options.yaml b/packages/flame_behavior_tree/example/analysis_options.yaml
new file mode 100644
index 000000000..85732fa02
--- /dev/null
+++ b/packages/flame_behavior_tree/example/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:flame_lint/analysis_options.yaml
diff --git a/packages/flame_behavior_tree/example/lib/main.dart b/packages/flame_behavior_tree/example/lib/main.dart
new file mode 100644
index 000000000..dbf3f83c2
--- /dev/null
+++ b/packages/flame_behavior_tree/example/lib/main.dart
@@ -0,0 +1,323 @@
+import 'dart:async';
+
+import 'dart:math';
+
+import 'package:flame/components.dart';
+import 'package:flame/effects.dart';
+import 'package:flame/events.dart';
+import 'package:flame/game.dart';
+import 'package:flame/palette.dart';
+import 'package:flame_behavior_tree/flame_behavior_tree.dart';
+import 'package:flutter/material.dart';
+
+typedef MyGame = FlameGame;
+const gameWidth = 320.0;
+const gameHeight = 180.0;
+
+void main() {
+ runApp(const MainApp());
+}
+
+class MainApp extends StatelessWidget {
+ const MainApp({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ home: Scaffold(
+ body: GameWidget.controlled(
+ gameFactory: () => MyGame(
+ world: GameWorld(),
+ camera: CameraComponent.withFixedResolution(
+ width: gameWidth,
+ height: gameHeight,
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class GameWorld extends World with HasGameReference {
+ @override
+ Future onLoad() async {
+ game.camera.moveTo(Vector2(gameWidth * 0.5, gameHeight * 0.5));
+
+ final house = RectangleComponent(
+ size: Vector2(100, 100),
+ position: Vector2(gameWidth * 0.5, 10),
+ paint: BasicPalette.cyan.paint()
+ ..strokeWidth = 5
+ ..style = PaintingStyle.stroke,
+ anchor: Anchor.topCenter,
+ );
+
+ final door = Door(
+ size: Vector2(20, 4),
+ position: Vector2(40, house.size.y),
+ anchor: Anchor.centerLeft,
+ );
+
+ final agent = Agent(
+ door: door,
+ house: house,
+ position: Vector2(gameWidth * 0.76, gameHeight * 0.9),
+ );
+
+ await house.add(door);
+ await addAll([house, agent]);
+ }
+}
+
+class Door extends RectangleComponent with TapCallbacks {
+ Door({super.position, super.size, super.anchor})
+ : super(paint: BasicPalette.brown.paint());
+
+ bool isOpen = false;
+ bool _isInProgress = false;
+ bool _isKnocking = false;
+
+ @override
+ void onTapDown(TapDownEvent event) {
+ if (!_isInProgress) {
+ _isInProgress = true;
+ add(
+ RotateEffect.to(
+ isOpen ? 0 : -pi * 0.5,
+ EffectController(duration: 0.5, curve: Curves.easeInOut),
+ onComplete: () {
+ isOpen = !isOpen;
+ _isInProgress = false;
+ },
+ ),
+ );
+ }
+ }
+
+ void knock() {
+ if (!_isKnocking) {
+ _isKnocking = true;
+ add(
+ MoveEffect.by(
+ Vector2(0, -1),
+ EffectController(
+ alternate: true,
+ duration: 0.1,
+ repeatCount: 2,
+ ),
+ onComplete: () {
+ _isKnocking = false;
+ },
+ ),
+ );
+ }
+ }
+}
+
+class Agent extends PositionComponent with HasBehaviorTree {
+ Agent({required this.door, required this.house, required Vector2 position})
+ : _startPosition = position.clone(),
+ super(position: position);
+
+ final Door door;
+ final PositionComponent house;
+ final Vector2 _startPosition;
+
+ @override
+ Future onLoad() async {
+ await add(CircleComponent(radius: 3, anchor: Anchor.center));
+ _setupBehaviorTree();
+ super.onLoad();
+ }
+
+ void _setupBehaviorTree() {
+ var isInside = false;
+ var isAtTheDoor = false;
+ var isAtCenterOfHouse = false;
+ var isMoving = false;
+ var wantsToGoOutside = false;
+
+ final walkTowardsDoorInside = Task(() {
+ if (!isAtTheDoor) {
+ isMoving = true;
+
+ add(
+ MoveEffect.to(
+ door.absolutePosition + Vector2(door.size.x * 0.8, -15),
+ EffectController(
+ duration: 3,
+ curve: Curves.easeInOut,
+ ),
+ onComplete: () {
+ isMoving = false;
+ isAtTheDoor = true;
+ isAtCenterOfHouse = false;
+ },
+ ),
+ );
+ }
+ return isAtTheDoor ? NodeStatus.success : NodeStatus.running;
+ });
+
+ final stepOutTheDoor = Task(() {
+ if (isInside) {
+ isMoving = true;
+ add(
+ MoveEffect.to(
+ door.absolutePosition + Vector2(door.size.x * 0.5, 10),
+ EffectController(
+ duration: 2,
+ curve: Curves.easeInOut,
+ ),
+ onComplete: () {
+ isMoving = false;
+ isInside = false;
+ },
+ ),
+ );
+ }
+ return !isInside ? NodeStatus.success : NodeStatus.running;
+ });
+
+ final walkTowardsInitialPosition = Task(
+ () {
+ if (isAtTheDoor) {
+ isMoving = true;
+ isAtTheDoor = false;
+
+ add(
+ MoveEffect.to(
+ _startPosition,
+ EffectController(
+ duration: 3,
+ curve: Curves.easeInOut,
+ ),
+ onComplete: () {
+ isMoving = false;
+ wantsToGoOutside = false;
+ },
+ ),
+ );
+ }
+
+ return !wantsToGoOutside ? NodeStatus.success : NodeStatus.running;
+ },
+ );
+
+ final walkTowardsDoorOutside = Task(() {
+ if (!isAtTheDoor) {
+ isMoving = true;
+ add(
+ MoveEffect.to(
+ door.absolutePosition + Vector2(door.size.x * 0.5, 10),
+ EffectController(
+ duration: 3,
+ curve: Curves.easeInOut,
+ ),
+ onComplete: () {
+ isMoving = false;
+ isAtTheDoor = true;
+ },
+ ),
+ );
+ }
+ return isAtTheDoor ? NodeStatus.success : NodeStatus.running;
+ });
+
+ final walkTowardsCenterOfTheHouse = Task(() {
+ if (!isAtCenterOfHouse) {
+ isMoving = true;
+ isInside = true;
+
+ add(
+ MoveEffect.to(
+ house.absoluteCenter,
+ EffectController(
+ duration: 3,
+ curve: Curves.easeInOut,
+ ),
+ onComplete: () {
+ isMoving = false;
+ wantsToGoOutside = true;
+ isAtTheDoor = false;
+ isAtCenterOfHouse = true;
+ },
+ ),
+ );
+ }
+ return isInside ? NodeStatus.success : NodeStatus.running;
+ });
+
+ final checkIfDoorIsOpen = Condition(() => door.isOpen);
+
+ final knockTheDoor = Task(() {
+ door.knock();
+ return NodeStatus.success;
+ });
+
+ final goOutsideSequence = Sequence(
+ children: [
+ Condition(() => wantsToGoOutside),
+ Selector(
+ children: [
+ Sequence(
+ children: [
+ Condition(() => isInside),
+ walkTowardsDoorInside,
+ Selector(
+ children: [
+ Sequence(
+ children: [
+ checkIfDoorIsOpen,
+ stepOutTheDoor,
+ ],
+ ),
+ knockTheDoor,
+ ],
+ ),
+ ],
+ ),
+ walkTowardsInitialPosition,
+ ],
+ ),
+ ],
+ );
+
+ final goInsideSequence = Sequence(
+ children: [
+ Condition(() => !wantsToGoOutside),
+ Selector(
+ children: [
+ Sequence(
+ children: [
+ Condition(() => !isInside),
+ walkTowardsDoorOutside,
+ Selector(
+ children: [
+ Sequence(
+ children: [
+ checkIfDoorIsOpen,
+ walkTowardsCenterOfTheHouse,
+ ],
+ ),
+ knockTheDoor,
+ ],
+ ),
+ ],
+ ),
+ ],
+ ),
+ ],
+ );
+
+ treeRoot = Selector(
+ children: [
+ Condition(() => isMoving),
+ goOutsideSequence,
+ goInsideSequence,
+ ],
+ );
+ tickInterval = 2;
+ }
+}
diff --git a/packages/flame_behavior_tree/example/pubspec.yaml b/packages/flame_behavior_tree/example/pubspec.yaml
new file mode 100644
index 000000000..4155b245c
--- /dev/null
+++ b/packages/flame_behavior_tree/example/pubspec.yaml
@@ -0,0 +1,25 @@
+name: flame_behavior_tree_example
+description: "A simple demo to show usage of behavior trees in flame."
+publish_to: 'none'
+version: 0.1.0
+funding:
+ - https://opencollective.com/blue-fire
+ - https://github.com/sponsors/bluefireteam
+ - https://patreon.com/bluefireoss
+
+environment:
+ sdk: ">=3.0.0 <4.0.0"
+
+dependencies:
+ flame: ^1.16.0
+ flame_behavior_tree: ^0.1.0
+ flutter:
+ sdk: flutter
+
+dev_dependencies:
+ flame_lint: ^1.1.2
+ flutter_test:
+ sdk: flutter
+
+flutter:
+ uses-material-design: true
diff --git a/packages/flame_behavior_tree/lib/flame_behavior_tree.dart b/packages/flame_behavior_tree/lib/flame_behavior_tree.dart
new file mode 100644
index 000000000..e90ce54b3
--- /dev/null
+++ b/packages/flame_behavior_tree/lib/flame_behavior_tree.dart
@@ -0,0 +1,5 @@
+/// A bridge package that integrates behavior_tree package with flame.
+library flame_behavior_tree;
+
+export 'package:behavior_tree/behavior_tree.dart';
+export 'src/has_behavior_tree.dart';
diff --git a/packages/flame_behavior_tree/lib/src/has_behavior_tree.dart b/packages/flame_behavior_tree/lib/src/has_behavior_tree.dart
new file mode 100644
index 000000000..e978745d2
--- /dev/null
+++ b/packages/flame_behavior_tree/lib/src/has_behavior_tree.dart
@@ -0,0 +1,57 @@
+import 'dart:async';
+
+import 'package:behavior_tree/behavior_tree.dart';
+import 'package:flame/components.dart';
+import 'package:flutter/foundation.dart';
+
+/// A mixin on [Component] to indicate that the component has a behavior tree.
+///
+/// Reference to the behavior tree for this component can be set or accessed
+/// via [treeRoot]. The update frequency of the tree can be reduced by
+/// increasing [tickInterval]. By default, the tree will be updated on every
+/// update of the component.
+mixin HasBehaviorTree on Component {
+ T? _treeRoot;
+ Timer? _timer;
+ double _tickInterval = 0;
+
+ /// The delay between any two ticks of the behavior tree.
+ double get tickInterval => _tickInterval;
+ set tickInterval(double interval) {
+ _tickInterval = interval;
+
+ if (_tickInterval > 0) {
+ _timer ??= Timer(interval, repeat: true);
+ _timer?.limit = interval;
+ } else {
+ _timer?.onTick = null;
+ _timer = null;
+ _tickInterval = 0;
+ }
+ }
+
+ /// The root node of the behavior tree.
+ T get treeRoot => _treeRoot!;
+ set treeRoot(T value) {
+ _treeRoot = value;
+ _timer?.onTick = _treeRoot!.tick;
+ }
+
+ @override
+ @mustCallSuper
+ Future onLoad() async {
+ super.onLoad();
+ _timer?.onTick = _treeRoot?.tick;
+ }
+
+ @override
+ @mustCallSuper
+ void update(double dt) {
+ super.update(dt);
+ if (_tickInterval > 0) {
+ _timer?.update(dt);
+ } else {
+ _treeRoot?.tick();
+ }
+ }
+}
diff --git a/packages/flame_behavior_tree/pubspec.yaml b/packages/flame_behavior_tree/pubspec.yaml
new file mode 100644
index 000000000..9975ab1d0
--- /dev/null
+++ b/packages/flame_behavior_tree/pubspec.yaml
@@ -0,0 +1,25 @@
+name: flame_behavior_tree
+description: A bridge package that integrates behavior_tree package with flame.
+version: 0.1.0
+homepage: https://github.com/flame-engine/flame/tree/main/packages/flame_behavior_tree
+funding:
+ - https://opencollective.com/blue-fire
+ - https://github.com/sponsors/bluefireteam
+ - https://patreon.com/bluefireoss
+
+environment:
+ sdk: ">=3.0.0 <4.0.0"
+ flutter: ">=3.19.0"
+
+dependencies:
+ behavior_tree: ^1.0.0
+ flame: ^1.16.0
+ flutter:
+ sdk: flutter
+
+dev_dependencies:
+ flame_lint: ^1.1.2
+ flame_test: ^1.16.0
+ flutter_test:
+ sdk: flutter
+ mocktail: ^1.0.1
diff --git a/packages/flame_behavior_tree/test/has_behavior_tree_test.dart b/packages/flame_behavior_tree/test/has_behavior_tree_test.dart
new file mode 100644
index 000000000..c7f0d36ec
--- /dev/null
+++ b/packages/flame_behavior_tree/test/has_behavior_tree_test.dart
@@ -0,0 +1,98 @@
+import 'package:flame/components.dart';
+import 'package:flame_behavior_tree/flame_behavior_tree.dart';
+import 'package:flame_test/flame_test.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mocktail/mocktail.dart';
+
+void main() {
+ group('HasBehaviorTree', () {
+ final alwaysFailure = _MockNode();
+ final alwaysSuccess = _MockNode();
+ final alwaysRunning = _MockNode();
+
+ setUp(() {
+ reset(alwaysFailure);
+ reset(alwaysSuccess);
+ reset(alwaysRunning);
+
+ when(() => alwaysFailure.status).thenReturn(NodeStatus.failure);
+ when(() => alwaysSuccess.status).thenReturn(NodeStatus.success);
+ when(() => alwaysRunning.status).thenReturn(NodeStatus.running);
+ });
+
+ testWithFlameGame(
+ 'updates with null tree.',
+ (game) async {
+ final component = _BehaviorTreeComponent();
+ expect(() => game.add(component), returnsNormally);
+ },
+ );
+
+ test('tick interval can be changed', () {
+ final component = _BehaviorTreeComponent();
+ expect(component.tickInterval, 0);
+
+ component.tickInterval = 3;
+ expect(component.tickInterval, 3);
+
+ component.tickInterval = -53;
+ expect(component.tickInterval, 0);
+ });
+
+ test('throws if treeNode is accessed before setting.', () {
+ final component = _BehaviorTreeComponent();
+ expect(() => component.treeRoot, throwsA(isA()));
+
+ component.treeRoot = _MockNode();
+ expect(() => component.treeRoot, returnsNormally);
+ });
+
+ testWithFlameGame(
+ 'updates without errors with a valid tree.',
+ (game) async {
+ final component = _BehaviorTreeComponent()
+ ..treeRoot =
+ Sequence(children: [alwaysSuccess, alwaysFailure, alwaysRunning]);
+
+ expect(() async => await game.add(component), returnsNormally);
+
+ await game.ready();
+ expect(() => game.update(10), returnsNormally);
+
+ verify(alwaysSuccess.tick).called(1);
+ verify(alwaysFailure.tick).called(1);
+ verifyNever(alwaysRunning.tick);
+ },
+ );
+
+ testWithFlameGame(
+ 'tree updates at a slower rate.',
+ (game) async {
+ final component = _BehaviorTreeComponent()
+ ..treeRoot =
+ Sequence(children: [alwaysSuccess, alwaysFailure, alwaysRunning])
+ ..tickInterval = 1;
+
+ await game.add(component);
+ await game.ready();
+
+ const dt = 1 / 60;
+ const gameTime = 3.0;
+ var elapsedTime = 0.0;
+
+ while (elapsedTime < gameTime) {
+ game.update(dt);
+ elapsedTime += dt;
+ }
+
+ verify(alwaysSuccess.tick).called(gameTime.toInt());
+ verify(alwaysFailure.tick).called(gameTime.toInt());
+ verifyNever(alwaysRunning.tick);
+ },
+ );
+ });
+}
+
+class _BehaviorTreeComponent extends Component with HasBehaviorTree {}
+
+class _MockNode extends Mock implements NodeInterface {}