mirror of
				https://github.com/flame-engine/flame.git
				synced 2025-11-01 01:18:38 +08:00 
			
		
		
		
	 faf2df4b8c
			
		
	
	faf2df4b8c
	
	
	
		
			
			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;
 | |
|   }
 | |
| }
 |