mirror of
				https://github.com/flame-engine/flame.git
				synced 2025-11-04 13:08:09 +08:00 
			
		
		
		
	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
		
			
				
	
	
		
			324 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			324 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
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;
 | 
						|
  }
 | 
						|
}
 |