mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-01 19:12:31 +08:00
feat: Add initial version of behavior_tree and flame_behavior_tree package (#3045)
First pass on adding behavior tree for flame. This PR adds 2 packages: - behavior_tree: A pure dart implementation of behavior tree. - flame_behavior_tree: A bridge package that integrates behavior_tree with flame. Demo: https://github.com/flame-engine/flame/assets/33748002/1d2b00ab-1b6e-406e-9052-a24370c8f1ab
This commit is contained in:
10
packages/flame_behavior_tree/.metadata
Normal file
10
packages/flame_behavior_tree/.metadata
Normal file
@ -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
|
||||
0
packages/flame_behavior_tree/CHANGELOG.md
Normal file
0
packages/flame_behavior_tree/CHANGELOG.md
Normal file
21
packages/flame_behavior_tree/LICENSE
Normal file
21
packages/flame_behavior_tree/LICENSE
Normal file
@ -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.
|
||||
82
packages/flame_behavior_tree/README.md
Normal file
82
packages/flame_behavior_tree/README.md
Normal file
@ -0,0 +1,82 @@
|
||||
<!-- markdownlint-disable MD013 -->
|
||||
<p align="center">
|
||||
<a href="https://flame-engine.org">
|
||||
<img alt="flame" width="200px" src="https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">This is a bridge package that integrates the <a href="https://github.com/flame-engine/flame/tree/main/packages/flame_behavior_tree/behavior_tree">behavior_tree</a> dart package with <a href="https://flame-engine.org/">Flame engine</a>.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a title="Pub" href="https://pub.dev/packages/flame_behavior_tree" ><img src="https://img.shields.io/pub/v/flame_behavior_tree.svg?style=popout" /></a>
|
||||
<a title="Test" href="https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain"><img src="https://github.com/flame-engine/flame/workflows/cicd/badge.svg?branch=main&event=push"/></a>
|
||||
<a title="Discord" href="https://discord.gg/pxrBmy4"><img src="https://img.shields.io/discord/509714518008528896.svg"/></a>
|
||||
<a title="Melos" href="https://github.com/invertase/melos"><img src="https://img.shields.io/badge/maintained%20with-melos-f700ff.svg"/></a>
|
||||
</p>
|
||||
|
||||
---
|
||||
<!-- markdownlint-enable MD013 -->
|
||||
|
||||
|
||||
## 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<void> 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<void> 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.
|
||||
1
packages/flame_behavior_tree/analysis_options.yaml
Normal file
1
packages/flame_behavior_tree/analysis_options.yaml
Normal file
@ -0,0 +1 @@
|
||||
include: package:flame_lint/analysis_options.yaml
|
||||
21
packages/flame_behavior_tree/behavior_tree/LICENSE
Normal file
21
packages/flame_behavior_tree/behavior_tree/LICENSE
Normal file
@ -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.
|
||||
72
packages/flame_behavior_tree/behavior_tree/README.md
Normal file
72
packages/flame_behavior_tree/behavior_tree/README.md
Normal file
@ -0,0 +1,72 @@
|
||||
<!-- markdownlint-disable MD013 -->
|
||||
<p align="center">
|
||||
<a href="https://flame-engine.org">
|
||||
<img alt="flame" width="200px" src="https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
This package provides a simple and easy to use <a href="https://en.wikipedia.org/wiki/Behavior_tree">behavior tree</a> API in pure dart.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a title="Pub" href="https://pub.dev/packages/behavior_tree" ><img src="https://img.shields.io/pub/v/behavior_tree.svg?style=popout" /></a>
|
||||
<a title="Test" href="https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain"><img src="https://github.com/flame-engine/flame/workflows/cicd/badge.svg?branch=main&event=push"/></a>
|
||||
<a title="Discord" href="https://discord.gg/pxrBmy4"><img src="https://img.shields.io/discord/509714518008528896.svg"/></a>
|
||||
<a title="Melos" href="https://github.com/invertase/melos"><img src="https://img.shields.io/badge/maintained%20with-melos-f700ff.svg"/></a>
|
||||
</p>
|
||||
|
||||
---
|
||||
<!-- markdownlint-enable MD013 -->
|
||||
|
||||
|
||||
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();
|
||||
```
|
||||
@ -0,0 +1 @@
|
||||
include: package:flame_lint/analysis_options.yaml
|
||||
@ -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;
|
||||
}
|
||||
@ -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';
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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<NodeInterface>? children})
|
||||
: _children = children ?? <NodeInterface>[];
|
||||
|
||||
final List<NodeInterface> _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();
|
||||
}
|
||||
}
|
||||
@ -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<NodeInterface>? children})
|
||||
: _children = children ?? <NodeInterface>[];
|
||||
|
||||
final List<NodeInterface> _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();
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
33
packages/flame_behavior_tree/behavior_tree/lib/src/node.dart
Normal file
33
packages/flame_behavior_tree/behavior_tree/lib/src/node.dart
Normal file
@ -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();
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:behavior_tree/behavior_tree.dart';
|
||||
|
||||
typedef AsyncTaskCallback = Future<NodeStatus> 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
20
packages/flame_behavior_tree/behavior_tree/pubspec.yaml
Normal file
20
packages/flame_behavior_tree/behavior_tree/pubspec.yaml
Normal file
@ -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
|
||||
@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -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 {}
|
||||
@ -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 {}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
42
packages/flame_behavior_tree/example/.metadata
Normal file
42
packages/flame_behavior_tree/example/.metadata
Normal file
@ -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'
|
||||
@ -0,0 +1 @@
|
||||
include: package:flame_lint/analysis_options.yaml
|
||||
323
packages/flame_behavior_tree/example/lib/main.dart
Normal file
323
packages/flame_behavior_tree/example/lib/main.dart
Normal file
@ -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<GameWorld>;
|
||||
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<MyGame>.controlled(
|
||||
gameFactory: () => MyGame(
|
||||
world: GameWorld(),
|
||||
camera: CameraComponent.withFixedResolution(
|
||||
width: gameWidth,
|
||||
height: gameHeight,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GameWorld extends World with HasGameReference {
|
||||
@override
|
||||
Future<void> 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<void> 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;
|
||||
}
|
||||
}
|
||||
25
packages/flame_behavior_tree/example/pubspec.yaml
Normal file
25
packages/flame_behavior_tree/example/pubspec.yaml
Normal file
@ -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
|
||||
@ -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';
|
||||
57
packages/flame_behavior_tree/lib/src/has_behavior_tree.dart
Normal file
57
packages/flame_behavior_tree/lib/src/has_behavior_tree.dart
Normal file
@ -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<T extends NodeInterface> 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<void> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
25
packages/flame_behavior_tree/pubspec.yaml
Normal file
25
packages/flame_behavior_tree/pubspec.yaml
Normal file
@ -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
|
||||
@ -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<TypeError>()));
|
||||
|
||||
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 {}
|
||||
Reference in New Issue
Block a user