Flutterで作る本格的なPongゲーム！スコア表示付き



## Flutterでゲームを作ろう！

こんにちは、Flutter開発者の皆さん！今回は、Flutterを使って本格的なPongゲームを作成する方法を詳しく解説します。スマートフォン向けに最適化され、右上にスコアを表示する機能も実装します。さあ、一緩に作っていきましょう！

### 1. プロジェクトのセットアップ

まずは、新しいFlutterプロジェクトを作成し、必要なパッケージをインストールします。

```bash
flutter create pong_game
cd pong_game
flutter pub add flame
```

### 2. main.dartファイルの作成

`lib/main.dart`ファイルを以下のように編集します。

```dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'pong_game.dart';

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        body: GameWidget(game: PongGame()),
      ),
    ),
  );
}
```

### 3. pong_game.dartファイルの作成

`lib/pong_game.dart`ファイルを作成し、以下のコードを追加します。

```dart
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'dart:math' as math;

class PongGame extends FlameGame with TapDetector, HasCollisionDetection {
  late Ball ball;
  late Paddle playerPaddle;
  late Paddle aiPaddle;
  late TextComponent playerScoreText;
  late TextComponent aiScoreText;
  int playerScore = 0;
  int aiScore = 0;

  @override
  Future<void> onLoad() async {
    await super.onLoad();

    ball = Ball();
    playerPaddle = Paddle(isPlayer: true);
    aiPaddle = Paddle(isPlayer: false);
    playerScoreText = TextComponent(
      text: 'Player: $playerScore',
      position: Vector2(size.x - 100, 20),
      textRenderer: TextPaint(style: const TextStyle(color: Colors.white, fontSize: 16)),
    );
    aiScoreText = TextComponent(
      text: 'AI: $aiScore',
      position: Vector2(size.x - 100, 40),
      textRenderer: TextPaint(style: const TextStyle(color: Colors.white, fontSize: 16)),
    );

    add(ball);
    add(playerPaddle);
    add(aiPaddle);
    add(playerScoreText);
    add(aiScoreText);
  }

  @override
  void update(double dt) {
    super.update(dt);
    aiPaddle.moveTowards(ball.y);
    checkCollisions();
  }

  void checkCollisions() {
    if (ball.collidesWithPaddle(playerPaddle) || ball.collidesWithPaddle(aiPaddle)) {
      ball.bounceOffPaddle();
    }

    if (ball.x <= 0) {
      aiScore++;
      aiScoreText.text = 'AI: $aiScore';
      resetBall();
    } else if (ball.x >= size.x) {
      playerScore++;
      playerScoreText.text = 'Player: $playerScore';
      resetBall();
    }
  }

  void resetBall() {
    ball.position = size / 2;
    ball.velocity = Vector2(ball.speed, 0);
    if (math.Random().nextBool()) {
      ball.velocity.x *= -1;
    }
  }

  @override
  void onTapDown(TapDownInfo info) {
    playerPaddle.moveTo(info.eventPosition.game.y);
  }
}

class Ball extends PositionComponent with CollisionCallbacks {
  static const double radius = 10;
  double speed = 300;
  late Vector2 velocity;

  Ball() : super(size: Vector2.all(radius * 2)) {
    anchor = Anchor.center;
  }

  @override
  void onLoad() {
    super.onLoad();
    position = gameRef.size / 2;
    velocity = Vector2(speed, 0);
    if (math.Random().nextBool()) {
      velocity.x *= -1;
    }
  }

  @override
  void update(double dt) {
    super.update(dt);
    position += velocity * dt;

    if (y <= radius || y >= gameRef.size.y - radius) {
      velocity.y *= -1;
    }
  }

  bool collidesWithPaddle(Paddle paddle) {
    return position.x >= paddle.x &&
        position.x <= paddle.x + paddle.width &&
        position.y >= paddle.y &&
        position.y <= paddle.y + paddle.height;
  }

  void bounceOffPaddle() {
    velocity.x *= -1.1; // Increase speed slightly
    velocity.y = (position.y - (gameRef.size.y / 2)) * 2;
    velocity = velocity.normalized() * speed;
  }

  @override
  void render(Canvas canvas) {
    super.render(canvas);
    canvas.drawCircle(Offset(radius, radius), radius, Paint()..color = Colors.white);
  }
}

class Paddle extends PositionComponent {
  static const double width = 10;
  static const double height = 100;
  final bool isPlayer;

  Paddle({required this.isPlayer}) : super(size: Vector2(width, height)) {
    anchor = Anchor.center;
  }

  @override
  void onLoad() {
    super.onLoad();
    y = gameRef.size.y / 2;
    x = isPlayer ? 30 : gameRef.size.x - 30;
  }

  void moveTo(double yPosition) {
    y = yPosition.clamp(height / 2, gameRef.size.y - height / 2);
  }

  void moveTowards(double targetY) {
    const aiSpeed = 3;
    if ((targetY - y).abs() > aiSpeed) {
      y += (targetY > y ? aiSpeed : -aiSpeed);
    }
  }

  @override
  void render(Canvas canvas) {
    super.render(canvas);
    canvas.drawRect(
      Rect.fromLTWH(0, 0, width, height),
      Paint()..color = Colors.white,
    );
  }
}
```

### 4. コードの解説

#### PongGameクラス

- `onLoad()`: ゲームの初期化を行います。ボール、パドル、スコアテキストを作成し、画面に追加します。
- `update()`: 毎フレーム呼び出され、AIパドルの動きやコリジョンチェックを行います。
- `checkCollisions()`: ボールとパドルの衝突判定、得点の更新を行います。
- `resetBall()`: ボールの位置と速度をリセットします。
- `onTapDown()`: 画面タップ時にプレイヤーのパドルを移動させます。

#### Ballクラス

- ボールの動きや衝突判定のロジックを実装しています。
- `update()`: ボールの位置を更新し、画面端での反射を処理します。
- `collidesWithPaddle()`: パドルとの衝突判定を行います。
- `bounceOffPaddle()`: パドルに当たった時の挙動を定義します。

#### Paddleクラス

- プレイヤーとAIのパドルを表現します。
- `moveTo()`: プレイヤーのパドル移動に使用します。
- `moveTowards()`: AIパドルの移動に使用します。

### 5. ゲームの実行

プロジェクトのルートディレクトリで以下のコマンドを実行します。

```bash
flutter run
```

これで、スマートフォン上でPongゲームが動作し、右上にスコアが表示されるはずです！

## まとめ

今回は、Flutterを使って本格的なPongゲームを作成しました。Flameエンジンを活用することで、ゲームの基本的な構造を簡単に実装できました。さらに、スコア表示機能を追加することで、よりゲーム性の高いアプリケーションになりました。

このコードをベースに、さらなる機能追加や改良を行ってみてください。例えば、難易度設定、サウンド効果、パーティクル効果などを追加すると、より魅力的なゲームになるでしょう。

Flutterでのゲーム開発は非常に楽しく、多くの可能性を秘めています。ぜひ、自分なりのアイデアを取り入れて、オリジナルのゲームを作ってみてください！

それでは、楽しいFlutter開発を！