Flutter行为树管理插件flame_behavior_tree的使用

Flutter行为树管理插件flame_behavior_tree的使用


特性

该插件提供了一个 HasBehaviorTree 混入类,适用于 Flame 的 Component。你可以将这个混入添加到任何组件中,并且它会自动处理与组件更新同步的行为树。


开始使用

首先,在你的 Flutter 项目中添加 flame_behavior_tree 包:

flutter pub add flame_behavior_tree

使用方法

1. 添加 HasBehaviorTree 混入

在你希望实现某种AI行为的组件中添加 HasBehaviorTree 混入。

class MyComponent extends PositionComponent with HasBehaviorTree {
  // 组件的其他逻辑
}

2. 设置行为树

在组件的 onLoad 方法中设置行为树并指定其根节点。

class MyComponent extends PositionComponent with HasBehaviorTree {
  Future<void> onLoad() async {
    treeRoot = Selector(
      children: [
        Sequence(children: [task1, condition, task2]),
        Sequence(...),
      ]
    );
    super.onLoad();
  }
}

3. 调整行为树的更新频率

通过调整 tickInterval 来控制行为树的更新频率。

class MyComponent extends PositionComponent with HasBehaviorTree {
  Future<void> onLoad() async {
    treeRoot = Selector(...);
    tickInterval = 4;
    super.onLoad();
  }
}

完整示例

下面是一个完整的示例,展示了如何使用 flame_behavior_tree 插件来实现一个简单的 AI 行为。

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](/user/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](/user/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](/user/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](/user/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;
  }
}

更多关于Flutter行为树管理插件flame_behavior_tree的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter行为树管理插件flame_behavior_tree的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


flame_behavior_tree 是一个用于 Flutter 游戏开发的插件,它基于行为树(Behavior Tree)的概念,允许开发者以模块化和可维护的方式管理游戏中的 AI 行为。行为树是一种树状结构,用于定义和控制 AI 实体的决策过程。

安装

首先,你需要在 pubspec.yaml 文件中添加 flame_behavior_tree 依赖:

dependencies:
  flame_behavior_tree: ^0.1.0

然后运行 flutter pub get 来安装依赖。

基本概念

行为树由多个节点组成,每个节点负责执行特定的任务或决策。常见的节点类型包括:

  1. Action Node: 执行具体的动作。
  2. Condition Node: 检查某个条件是否满足。
  3. Composite Node: 包含多个子节点,并决定如何执行这些子节点。常见的复合节点有 SequenceSelector
  4. Decorator Node: 修饰子节点,改变其行为。

使用示例

1. 创建行为树

首先,你需要创建一个行为树。以下是一个简单的例子:

import 'package:flame_behavior_tree/flame_behavior_tree.dart';

void main() {
  // 创建一个行为树
  final behaviorTree = Sequence([
    // 条件节点:检查是否有敌人
    Condition((blackboard) => blackboard['hasEnemy'] == true),
    // 动作节点:攻击敌人
    Action((blackboard) {
      print('Attacking enemy!');
      return Status.success;
    }),
  ]);

  // 创建一个黑板(用于存储数据)
  final blackboard = {'hasEnemy': true};

  // 执行行为树
  behaviorTree.tick(blackboard);
}

2. 使用复合节点

复合节点可以包含多个子节点,并决定它们的执行顺序。例如,Sequence 会依次执行子节点,直到其中一个失败:

final behaviorTree = Sequence([
  Condition((blackboard) => blackboard['hasEnemy'] == true),
  Action((blackboard) {
    print('Attacking enemy!');
    return Status.success;
  }),
  Action((blackboard) {
    print('Enemy defeated!');
    return Status.success;
  }),
]);

3. 使用 Selector

Selector 会依次执行子节点,直到其中一个成功:

final behaviorTree = Selector([
  Condition((blackboard) => blackboard['hasEnemy'] == true),
  Action((blackboard) {
    print('No enemy found, patrolling...');
    return Status.success;
  }),
]);

4. 使用 Decorator

Decorator 节点可以修饰子节点的行为。例如,Inverter 会反转子节点的结果:

final behaviorTree = Inverter(
  Condition((blackboard) => blackboard['hasEnemy'] == true),
);

集成到 Flame 游戏中

你可以将行为树集成到 Flame 游戏的 Component 中,例如在 update 方法中调用行为树的 tick 方法:

import 'package:flame/game.dart';
import 'package:flame_behavior_tree/flame_behavior_tree.dart';

class MyGame extends FlameGame {
  late BehaviorTree behaviorTree;

  [@override](/user/override)
  Future<void> onLoad() async {
    behaviorTree = Sequence([
      Condition((blackboard) => blackboard['hasEnemy'] == true),
      Action((blackboard) {
        print('Attacking enemy!');
        return Status.success;
      }),
    ]);
  }

  [@override](/user/override)
  void update(double dt) {
    final blackboard = {'hasEnemy': true};
    behaviorTree.tick(blackboard);
    super.update(dt);
  }
}
回到顶部