WinForms ブロック崩し拡張チュートリアル

~ブロック配置・スコア管理・ゲームオーバー処理を実装しよう~

この資料では、C# の WinForms アプリケーションを用いて、以下の機能を持つブロック崩しゲームの基本形を実装します。

  • パドル操作: 左右キーによるパドルの移動
  • ボールの動きと反射: 壁やパドルとの衝突処理
  • ブロックの配置: 画面上部に複数のブロックを配置し、衝突判定を実施
  • スコア管理: ブロック破壊時にスコアを加算
  • ゲームオーバー処理: ボールが画面下部に到達した場合、ゲーム終了

必要なステップ

サンプルコード

以下のコードは、各機能を実装したサンプルです。
必要に応じて、ブロックの数やサイズ、スコアの加算値などを変更して、オリジナルのゲームに拡張することが可能です。

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;

public partial class Form1 : Form
{
    // パドル関連
    private bool isLeftPressed = false;
    private bool isRightPressed = false;
    private float paddleX;
    private const float PaddleWidth = 100.0f;
    private const float PaddleHeight = 20.0f;
    private const float PaddleSpeed = 5.0f;

    // ボール関連
    private float ballX;
    private float ballY;
    private float ballVelocityX = 3.0f;
    private float ballVelocityY = -3.0f;
    private const float BallRadius = 10.0f;

    // ブロック関連
    private List<RectangleF> blocks;
    private const int BlockRows = 5;
    private const int BlockColumns = 10;
    private const float BlockWidth = 70;
    private const float BlockHeight = 20;
    private const float BlockPadding = 5;
    private const float BlockTopOffset = 30;

    // ゲーム状態とスコア
    private int score = 0;
    private bool isGameOver = false;

    private const int TimerInterval = 16; // 約60FPS
    private Timer gameTimer;

    public Form1()
    {
        InitializeComponent();
        InitializeGame();
    }

    // ゲーム初期化処理
    private void InitializeGame()
    {
        // ダブルバッファリングで描画のちらつきを防止
        this.DoubleBuffered = true;
        this.ClientSize = new Size(800, 600);
        this.Text = "Breakout Game";

        // キーイベントの登録
        this.KeyDown += Form1_KeyDown;
        this.KeyUp += Form1_KeyUp;

        // タイマーの設定(約60FPS)
        gameTimer = new Timer { Interval = TimerInterval };
        gameTimer.Tick += Timer_Tick;
        gameTimer.Start();

        // パドルの初期位置(画面下部中央)
        paddleX = (this.ClientSize.Width - PaddleWidth) / 2;

        // ボールの初期位置(画面中央)
        ballX = this.ClientSize.Width / 2;
        ballY = this.ClientSize.Height / 2;

        // ブロックの初期配置
        InitializeBlocks();
    }

    // ブロック配置の初期化処理
    private void InitializeBlocks()
    {
        blocks = new List<RectangleF>();

        // ブロック全体の横幅を計算し、中央配置の開始X座標を決定
        float totalWidth = BlockColumns * BlockWidth + (BlockColumns - 1) * BlockPadding;
        float startX = (this.ClientSize.Width - totalWidth) / 2;

        for (int row = 0; row < BlockRows; row++)
        {
            for (int col = 0; col < BlockColumns; col++)
            {
                float x = startX + col * (BlockWidth + BlockPadding);
                float y = BlockTopOffset + row * (BlockHeight + BlockPadding);
                blocks.Add(new RectangleF(x, y, BlockWidth, BlockHeight));
            }
        }
    }

    // キー入力の状態管理
    private void SetKeyState(Keys key, bool isPressed)
    {
        switch (key)
        {
            case Keys.Left:
                isLeftPressed = isPressed;
                break;
            case Keys.Right:
                isRightPressed = isPressed;
                break;
        }
    }

    private void Form1_KeyDown(object sender, KeyEventArgs e)
    {
        SetKeyState(e.KeyCode, true);
    }

    private void Form1_KeyUp(object sender, KeyEventArgs e)
    {
        SetKeyState(e.KeyCode, false);
    }

    // タイマー処理:パドルとボールの状態更新および衝突判定
    private void Timer_Tick(object sender, EventArgs e)
    {
        if (isGameOver)
        {
            gameTimer.Stop();
            return;
        }

        UpdatePaddlePosition();
        UpdateBallPosition();
        CheckBallCollision();
        Invalidate(); // 再描画要求
    }

    // パドルの位置更新処理
    private void UpdatePaddlePosition()
    {
        if (isLeftPressed)
            paddleX -= PaddleSpeed;
        if (isRightPressed)
            paddleX += PaddleSpeed;
        // パドルが画面外に出ないよう制限
        paddleX = Math.Max(0, Math.Min(paddleX, this.ClientSize.Width - PaddleWidth));
    }

    // ボールの位置更新処理
    private void UpdateBallPosition()
    {
        ballX += ballVelocityX;
        ballY += ballVelocityY;
    }

    // 衝突判定処理(壁、パドル、ブロック)
    private void CheckBallCollision()
    {
        // 壁との衝突判定
        if (ballX - BallRadius < 0)
        {
            ballX = BallRadius;
            ballVelocityX = -ballVelocityX;
        }
        else if (ballX + BallRadius > this.ClientSize.Width)
        {
            ballX = this.ClientSize.Width - BallRadius;
            ballVelocityX = -ballVelocityX;
        }

        if (ballY - BallRadius < 0)
        {
            ballY = BallRadius;
            ballVelocityY = -ballVelocityY;
        }
        else if (ballY - BallRadius > this.ClientSize.Height)
        {
            // ボールが下端に到達したらゲームオーバー
            isGameOver = true;
            return;
        }

        // パドルとの衝突判定
        RectangleF paddleRect = new RectangleF(paddleX, this.ClientSize.Height - PaddleHeight - 30, PaddleWidth, PaddleHeight);
        RectangleF ballRect = new RectangleF(ballX - BallRadius, ballY - BallRadius, BallRadius * 2, BallRadius * 2);

        if (paddleRect.IntersectsWith(ballRect) && ballVelocityY > 0)
        {
            ballY = paddleRect.Top - BallRadius;
            ballVelocityY = -Math.Abs(ballVelocityY);
            // 衝突位置に応じた水平速度の調整
            float hitPos = (ballX - paddleRect.Left) / paddleRect.Width;
            ballVelocityX = (hitPos - 0.5f) * 10;
        }

        // ブロックとの衝突判定
        for (int i = 0; i < blocks.Count; i++)
        {
            if (blocks[i].IntersectsWith(ballRect))
            {
                // ブロック破壊:リストから該当ブロックを削除
                blocks.RemoveAt(i);
                i--; // 削除後のインデックス調整

                // ボールを反射(ここでは単純にY方向の速度を反転)
                ballVelocityY = -ballVelocityY;

                // スコア加算(例:1ブロックにつき10点)
                score += 10;

                // 1フレーム内で複数ブロックとの衝突が起こらないようにループを抜ける
                break;
            }
        }
    }

    // 描画処理
    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);
        Graphics g = e.Graphics;
        g.Clear(Color.White);

        // パドル描画
        g.FillRectangle(Brushes.Blue, paddleX, this.ClientSize.Height - PaddleHeight - 30, PaddleWidth, PaddleHeight);

        // ボール描画
        g.FillEllipse(Brushes.Red, ballX - BallRadius, ballY - BallRadius, BallRadius * 2, BallRadius * 2);

        // ブロック描画
        foreach (RectangleF block in blocks)
        {
            g.FillRectangle(Brushes.Green, block);
            g.DrawRectangle(Pens.Black, block.X, block.Y, block.Width, block.Height);
        }

        // スコア描画
        using (Font font = new Font("Arial", 14))
        {
            g.DrawString("Score: " + score, font, Brushes.Black, 10, 10);
        }

        // ゲームオーバー時の表示
        if (isGameOver)
        {
            string gameOverText = "Game Over";
            using (Font font = new Font("Arial", 24))
            {
                SizeF textSize = g.MeasureString(gameOverText, font);
                g.DrawString(gameOverText, font, Brushes.Red, (this.ClientSize.Width - textSize.Width) / 2, (this.ClientSize.Height - textSize.Height) / 2);
            }
        }
    }
}

各パートの詳細解説

1. ゲーム初期化

  • フォーム設定とダブルバッファリング: 描画のちらつきを防ぎ、ウィンドウサイズやタイトルを設定します。
  • キーイベントとタイマー: パドル操作のために KeyDown/KeyUp を登録し、約60FPSで状態更新を行うタイマーを開始します。
  • パドル・ボール・ブロックの初期配置: パドルは画面下部中央、ボールは画面中央に配置。ブロックは上部に行列状に並べています。

2. パドル操作とボールの動き

  • パドル移動: 左右キーの状態に応じてパドルのX座標を更新し、画面外に出ないように制限します。
  • ボール移動: 毎フレーム、ボールの位置を速度に従って更新します。

3. 衝突判定とゲーム状態管理

  • 壁との衝突: ボールが画面端に到達した場合、反射するように速度を反転。
  • パドル衝突: パドルとの接触時、ボールの位置を補正し、反射と衝突位置に応じた左右への加速を実施。
  • ブロック衝突: ボールがブロックに接触した場合、該当ブロックを削除し、スコアを加算。
  • ゲームオーバー: ボールが画面下部に到達した場合、ゲーム終了状態にしタイマーを停止します。

4. 描画処理

  • 各オブジェクトの描画: パドル、ボール、ブロックをそれぞれ描画し、スコアを画面上部に表示。
  • ゲームオーバー表示: ゲーム終了時は、中央に「Game Over」のメッセージを表示します。

まとめ

この拡張チュートリアルでは、WinForms を用いてパドルとボールの動作に加え、ブロック配置、スコア管理、ゲームオーバー処理を実装しました。
このサンプルを基に、さらにブロックのパターン変更やライフ管理、レベルアップ、効果音やアニメーションの追加など、さまざまな拡張が可能です。初心者の方もぜひ自分なりの工夫を加えて、オリジナルのブロック崩しゲームを作成してみてください。

次のステップ