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パーティクルと UI オーバーレイ

活用シーン

  1. 01Flame の 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. ゲームクラス:適切なミックスイン(HasCollisionDetection、HasKeyboardHandlerComponents)を使って FlameGame を拡張します
  2. コンポーネント:SpriteComponent、PositionComponent、SpriteAnimationComponent を使ってエンティティを構築します
  3. ゲームループ:ロジックには update(double dt) を、描画には render(Canvas canvas) を実装します
  4. 衝突:CollisionCallbacks ミックスインを追加し、ヒットボックス(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());
  }
}

低レベルのゲーム(カスタム実装向け):

class MyGame extends Game {
  @override
  void render(Canvas canvas) { /* Custom rendering */ }
  
  @override
  void update(double dt) { /* Custom logic */ }
}
必須のミックスイン
ミックスイン目的
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);
}

オーディオ

BGM
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();
  }
}

参考資料

各トピックの詳しいガイドについては、次を参照してください。

コメント