mcpField
F
flutter-flame-games

flutter-flame-games

github.com/mauriciog87开发393 次安装39 周前更新
代码仓库

关于 flutter-flame-games

为使用 Flutter 和 Flame 的开发者提供 2D 游戏开发指南,涵盖组件、游戏循环、碰撞、输入及常见游戏系统。

主要功能

  1. 01FlameGame 与组件架构
  2. 02精灵及动画处理
  3. 03碰撞检测与命中框
  4. 04键盘、触控和摇杆输入
  5. 05摄像机、地图与音效
  6. 06粒子效果及界面叠层

适用场景

  1. 01搭建 Flame 2D 游戏基础结构
  2. 02实现角色移动与输入控制
  3. 03添加碰撞、地图和摄像机
  4. 04为游戏加入音效及粒子效果

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/mauriciog87/flutter-agent-skill --skill flutter-flame-games

安装后在 Claude Code 中输入/flutter-flame-games确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
audio-particles.md
camera-tiled.md
collision-system.md
components.md
input-handling.md

详细介绍

Flame 游戏开发

使用 Flutter 和 Flame 引擎构建 2D 游戏的专业指南。

快速入门

开始开发新的 Flame 游戏或审查代码时:

  1. 游戏类:扩展 FlameGame,并添加适当的 mixin(HasCollisionDetection、HasKeyboardHandlerComponents)
  2. 组件:使用 SpriteComponent、PositionComponent 或 SpriteAnimationComponent 构建实体
  3. 游戏循环:实现 update(double dt) 处理逻辑,并实现 render(Canvas canvas) 进行绘制
  4. 碰撞:添加 CollisionCallbacks mixin 并定义 hitbox(CircleHitbox、RectangleHitbox)
  5. 输入:使用 KeyboardHandler 处理键盘输入,使用 TapCallbacks 处理触摸输入,使用 JoystickComponent 处理摇杆输入

项目设置

flutter create my_game
cd my_game
flutter pub add flame

如需音频支持:

flutter pub add flame_audio

如需 Tiled 地图支持:

flutter pub add flame_tiled
资源结构
assets/
├── images/
│   ├── player.png
│   ├── enemy_spritesheet.png
│   └── background.png
├── audio/
│   ├── bgm.mp3
│   └── sfx_jump.wav
└── tiles/
    └── map.tmx

在 pubspec.yaml 中注册:

flutter:
  assets:
    - assets/images/
    - assets/audio/
    - assets/tiles/

核心架构

游戏类选择

FlameGame(适用于大多数游戏,推荐):

class MyGame extends FlameGame with HasCollisionDetection {
  @override
  Future<void> onLoad() async {
    await add(Player());
  }
}

低级 Game(用于自定义实现):

class MyGame extends Game {
  @override
  void render(Canvas canvas) { /* Custom rendering */ }
  
  @override
  void update(double dt) { /* Custom logic */ }
}
必备 Mixin
Mixin用途
HasCollisionDetection启用碰撞系统
HasKeyboardHandlerComponents支持处理键盘输入
HasGameReference提供对游戏实例的访问
组件层级结构
Component
├── PositionComponent
│   ├── SpriteComponent
│   ├── SpriteAnimationComponent
│   ├── TextComponent
│   └── ShapeComponent
├── ParticleSystemComponent
└── ParallaxComponent
组件生命周期
class MyComponent extends PositionComponent {
  @override
  Future<void> onLoad() async {
    // Called once when added to game
    // Load assets, initialize state
  }
  
  @override
  void update(double dt) {
    // Called every frame
    // dt = delta time in seconds
  }
  
  @override
  void render(Canvas canvas) {
    // Called every frame for drawing
  }
  
  @override
  void onRemove() {
    // Cleanup before removal
  }
}

使用精灵

静态精灵
class Player extends SpriteComponent {
  Player() : super(size: Vector2(64, 64), position: Vector2(100, 100));
  
  @override
  Future<void> onLoad() async {
    sprite = await Sprite.load('player.png');
  }
}
从 SpriteSheet 创建精灵
class Enemy extends SpriteComponent {
  @override
  Future<void> onLoad() async {
    final spriteSheet = await SpriteSheet.load(
      'enemies.png',
      srcSize: Vector2(32, 32),
    );
    sprite = spriteSheet.getSprite(0, 0); // row, column
  }
}
PositionComponent(自定义绘制)
class Circle extends PositionComponent {
  final Paint paint = Paint()..color = Colors.red;
  
  Circle() : super(size: Vector2.all(50));
  
  @override
  void render(Canvas canvas) {
    canvas.drawCircle(
      size.toOffset() / 2,
      size.x / 2,
      paint,
    );
  }
}

精灵动画

基础动画
class AnimatedPlayer extends SpriteAnimationComponent {
  AnimatedPlayer() : super(size: Vector2(64, 64));
  
  @override
  Future<void> onLoad() async {
    final spriteSheet = await SpriteSheet.load(
      'player_run.png',
      srcSize: Vector2(64, 64),
    );
    
    animation = spriteSheet.createAnimation(
      row: 0,
      stepTime: 0.1, // seconds per frame
      to: 8, // number of frames
    );
  }
}
多个动画
class Character extends SpriteAnimationComponent with HasGameReference {
  late final SpriteAnimation runAnimation;
  late final SpriteAnimation idleAnimation;
  late final SpriteAnimation jumpAnimation;
  
  @override
  Future<void> onLoad() async {
    final spritesheet = await game.images.load('character.png');
    
    runAnimation = _createAnimation(spritesheet, row: 0, frames: 8);
    idleAnimation = _createAnimation(spritesheet, row: 1, frames: 4);
    jumpAnimation = _createAnimation(spritesheet, row: 2, frames: 6);
    
    animation = idleAnimation;
  }
  
  SpriteAnimation _createAnimation(
    Image image, {
    required int row,
    required int frames,
  }) {
    return SpriteAnimation.fromFrameData(
      image,
      SpriteAnimationData.sequenced(
        amount: frames,
        amountPerRow: frames,
        textureSize: Vector2(64, 64),
        texturePosition: Vector2(0, row * 64),
        stepTime: 0.1,
      ),
    );
  }
  
  void run() => animation = runAnimation;
  void idle() => animation = idleAnimation;
  void jump() => animation = jumpAnimation;
}

碰撞检测

基础设置
// Game level
class MyGame extends FlameGame with HasCollisionDetection {
  @override
  Future<void> onLoad() async {
    add(Player());
    add(Enemy());
    add(ScreenHitbox()); // Boundary collision
  }
}

// Component level
class Player extends PositionComponent with CollisionCallbacks {
  @override
  Future<void> onLoad() async {
    add(CircleHitbox(radius: 20));
    // OR
    add(RectangleHitbox(size: Vector2(40, 40)));
    // OR
    add(PolygonHitbox([
      Vector2(0, 0),
      Vector2(20, 10),
      Vector2(0, 20),
    ]));
  }
  
  @override
  void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
    if (other is Enemy) {
      takeDamage();
    }
  }
  
  @override
  void onCollisionStart(
    Set<Vector2> intersectionPoints,
    PositionComponent other,
  ) {
    // Called once when collision begins
  }
  
  @override
  void onCollisionEnd(PositionComponent other) {
    // Called once when collision ends
  }
}
碰撞类型
// Customize collision behavior
class MyHitbox extends CircleHitbox {
  MyHitbox() : super(radius: 20) {
    collisionType = CollisionType.passive; // Won't trigger collisions actively
    // Options: active, passive, inactive
  }
}

输入处理

键盘输入
class Player extends SpriteComponent with KeyboardHandler {
  Vector2 velocity = Vector2.zero();
  double speed = 200;
  
  @override
  bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
    velocity = Vector2.zero();
    
    if (keysPressed.contains(LogicalKeyboardKey.keyA)) {
      velocity.x = -1;
    }
    if (keysPressed.contains(LogicalKeyboardKey.keyD)) {
      velocity.x = 1;
    }
    if (keysPressed.contains(LogicalKeyboardKey.keyW)) {
      velocity.y = -1;
    }
    if (keysPressed.contains(LogicalKeyboardKey.keyS)) {
      velocity.y = 1;
    }
    
    return true;
  }
  
  @override
  void update(double dt) {
    position += velocity.normalized() * speed * dt;
  }
}
触摸/点击输入
class Button extends SpriteComponent with TapCallbacks {
  @override
  void onTapDown(TapDownEvent event) {
    scale = Vector2.all(0.9); // Visual feedback
  }
  
  @override
  void onTapUp(TapUpEvent event) {
    scale = Vector2.all(1.0);
    onPressed();
  }
  
  @override
  void onTapCancel(TapCancelEvent event) {
    scale = Vector2.all(1.0);
  }
  
  void onPressed() {
    // Handle button press
  }
}
拖动输入
class DraggableObject extends PositionComponent with DragCallbacks {
  Vector2? dragStartPosition;
  Vector2? objectStartPosition;
  
  @override
  void onDragStart(DragStartEvent event) {
    dragStartPosition = event.localPosition;
    objectStartPosition = position.clone();
  }
  
  @override
  void onDragUpdate(DragUpdateEvent event) {
    if (dragStartPosition != null && objectStartPosition != null) {
      position = objectStartPosition! + event.localPosition - dragStartPosition!;
    }
  }
  
  @override
  void onDragEnd(DragEndEvent event) {
    dragStartPosition = null;
    objectStartPosition = null;
  }
}

摄像机系统

摄像机跟随玩家
class MyGame extends FlameGame with HasCollisionDetection {
  late final Player player;
  
  @override
  Future<void> onLoad() async {
    final world = World();
    player = Player();
    
    await world.add(player);
    await add(world);
    
    camera = CameraComponent.withFixedResolution(
      world: world,
      width: 800,
      height: 600,
    );
    await add(camera);
    
    camera.follow(player);
  }
}
带边界的摄像机
camera.follow(
  player,
  maxSpeed: 300,
  snap: false, // Smooth following
);

// Set world bounds
camera.setBounds(
  Rectangle.fromLTRB(0, 0, mapWidth, mapHeight),
);
缩放
// Zoom in
camera.viewfinder.zoom = 2.0;

// Animated zoom
await camera.viewfinder.zoomTo(2.0, speed: 2.0);

Tiled 地图

加载 Tiled 地图
class MyGame extends FlameGame {
  @override
  Future<void> onLoad() async {
    final mapComponent = await TiledComponent.load(
      'map.tmx',
      Vector2(32, 32), // tile size
    );
    await add(mapComponent);
    
    // Access object layer
    final objectLayer = mapComponent.tileMap.getLayer<ObjectGroup>('objects');
    for (final object in objectLayer?.objects ?? []) {
      if (object.name == 'player_spawn') {
        add(Player(position: Vector2(object.x, object.y)));
      }
    }
  }
}
瓦片碰撞
// In Tiled editor, add custom property "collision" = true to tiles
// Then in code:

@override
Future<void> onLoad() async {
  final map = await TiledComponent.load('map.tmx', Vector2(32, 32));
  await add(map);
  
  // Generate collision blocks from tile layer
  final collisionLayer = map.tileMap.getLayer<TileLayer>('ground');
  final collisionBlocks = <PositionComponent>[];
  
  for (var y = 0; y < collisionLayer!.height; y++) {
    for (var x = 0; x < collisionLayer.width; x++) {
      final tile = collisionLayer.tileData![y][x];
      if (tile.tile > 0) {
        collisionBlocks.add(
          PositionComponent(
            position: Vector2(x * 32.0, y * 32.0),
            size: Vector2.all(32),
          )..add(RectangleHitbox()),
        );
      }
    }
  }
  
  addAll(collisionBlocks);
}

音频

背景音乐
import 'package:flame_audio/flame_audio.dart';

class MyGame extends FlameGame {
  @override
  Future<void> onLoad() async {
    await FlameAudio.audioCache.load('bgm.mp3');
    FlameAudio.bgm.initialize();
    FlameAudio.bgm.play('bgm.mp3', volume: 0.5);
  }
  
  @override
  void onRemove() {
    FlameAudio.bgm.dispose();
    super.onRemove();
  }
}
音效
class Player extends PositionComponent {
  void jump() {
    FlameAudio.play('jump.wav', volume: 0.8);
    velocity.y = -300;
  }
  
  void collectCoin() {
    FlameAudio.play('coin.wav');
  }
}

粒子效果

基础粒子
import 'package:flame/particles.dart';

class Explosion extends ParticleSystemComponent {
  Explosion({required Vector2 position})
      : super(
          position: position,
          particle: Particle.generate(
            count: 20,
            lifespan: 0.5,
            generator: (i) => AcceleratedParticle(
              acceleration: Vector2.random() * 100,
              speed: Vector2.random() * 200,
              child: CircleParticle(
                radius: 4,
                paint: Paint()..color = Colors.orange,
              ),
            ),
          ),
        );
}
计算粒子(自定义)
Particle.generate(
  count: 12,
  lifespan: 1.0,
  generator: (i) {
    final angle = (i / 12) * 2 * pi;
    return ComputedParticle(
      renderer: (canvas, particle) {
        final progress = particle.progress;
        final radius = 10 * (1 - progress);
        final alpha = (1 - progress) * 255;
        
        canvas.drawCircle(
          Offset(cos(angle) * 50 * progress, sin(angle) * 50 * progress),
          radius,
          Paint()..color = Colors.red.withAlpha(alpha.toInt()),
        );
      },
    );
  },
)

UI 覆盖层

设置
void main() {
  runApp(
    GameWidget(
      game: MyGame(),
      overlayBuilderMap: {
        'PauseMenu': (context, game) => PauseMenuOverlay(),
        'HUD': (context, game) => HudOverlay(game: game as MyGame),
      },
      initialActiveOverlays: const ['HUD'],
    ),
  );
}
显示/隐藏覆盖层
class MyGame extends FlameGame {
  void showPauseMenu() {
    overlays.add('PauseMenu');
    pauseEngine();
  }
  
  void resumeGame() {
    overlays.remove('PauseMenu');
    resumeEngine();
  }
}
HUD 示例
class HudOverlay extends StatelessWidget {
  final MyGame game;
  
  const HudOverlay({required this.game});
  
  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<int>(
      valueListenable: game.score,
      builder: (context, score, child) {
        return Positioned(
          top: 20,
          left: 20,
          child: Text(
            'Score: $score',
            style: TextStyle(fontSize: 24, color: Colors.white),
          ),
        );
      },
    );
  }
}

视差背景

class MyGame extends FlameGame {
  @override
  Future<void> onLoad() async {
    final parallax = await loadParallaxComponent(
      [
        ParallaxImageData('bg_layer1.png'),
        ParallaxImageData('bg_layer2.png'),
        ParallaxImageData('bg_layer3.png'),
      ],
      baseVelocity: Vector2(50, 0),
      velocityMultiplierDelta: Vector2(1.5, 0),
    );
    
    add(parallax);
  }
}

摇杆组件

class MyGame extends FlameGame {
  late final JoystickComponent joystick;
  late final Player player;
  
  @override
  Future<void> onLoad() async {
    player = Player();
    await add(player);
    
    joystick = JoystickComponent(
      knob: CircleComponent(
        radius: 20,
        paint: Paint()..color = Colors.red.withAlpha(200),
      ),
      background: CircleComponent(
        radius: 50,
        paint: Paint()..color = Colors.black.withAlpha(100),
      ),
      margin: const EdgeInsets.only(left: 40, bottom: 40),
    );
    await add(joystick);
  }
  
  @override
  void update(double dt) {
    if (joystick.direction != JoystickDirection.idle) {
      player.velocity = joystick.delta * 5;
    }
    super.update(dt);
  }
}

性能最佳实践

使用 Delta Time

始终将移动/动画乘以 dt,以确保不受帧率影响:

@override
void update(double dt) {
  // Good - frame-rate independent
  position.x += speed * dt;
  
  // Bad - frame-rate dependent
  position.x += speed;
}
对象池

重复使用频繁创建和销毁的对象:

class BulletPool {
  final List<Bullet> _available = [];
  final List<Bullet> _inUse = [];
  
  Bullet acquire() {
    if (_available.isEmpty) {
      return Bullet();
    }
    final bullet = _available.removeLast();
    _inUse.add(bullet);
    return bullet;
  }
  
  void release(Bullet bullet) {
    _inUse.remove(bullet);
    _available.add(bullet);
  }
}
清理

始终释放资源:

class Player extends PositionComponent {
  late final SpriteAnimationTicker ticker;
  
  @override
  void onRemove() {
    ticker.dispose();
    super.onRemove();
  }
}

参考资料

有关特定主题的详细指南:

评论

支持 Markdown