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

~パドルとボールの衝突判定を実装しよう~

この資料では、C# の WinForms アプリケーションを使い、シンプルなブロック崩し風ゲームの基本部分(パドルとボールの描画、移動、衝突判定)を実装する方法を学びます。プログラミング初心者でも理解しやすいように、コードの各部分について詳しく解説しています。

必要なステップ


1. チュートリアルの概要

目的

  • パドルの操作: 左右キー入力によりパドルを動かす
  • ボールの移動: 毎フレームボールを移動させる
  • 衝突判定: ボールが画面の壁やパドルに当たった際の反射処理を実装する

前提知識

  • C# の基本文法
  • WinForms アプリケーションの作成方法
  • イベントハンドリングの基礎

2. サンプルコード

以下のコードは、WinForms アプリケーション内でパドルとボールの動作および衝突判定を実装した例です。コード内に各処理の詳細なコメントも記載しています。

using System;
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;

    // タイマー間隔(約60FPS)
    private const int TimerInterval = 16;

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

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

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

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

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

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

    // キー入力の状態管理
    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)
    {
        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)
        {
            // 画面下端到達時は反射(ここをゲームオーバー処理に変更も可能)
            ballY = this.ClientSize.Height - BallRadius;
            ballVelocityY = -ballVelocityY;
        }

        // パドルとの衝突判定
        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;  // 左右方向への加速調整
        }
    }

    // 描画処理(パドルとボールの描画)
    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);
    }
}

3. 各パートの詳細解説

3.1 ゲームの初期化

  • ダブルバッファリング: 描画のちらつきを防ぐために有効化しています。
  • フォームの設定: 画面サイズやタイトルを設定。
  • キーイベントの登録: パドル操作のため、KeyDown と KeyUp のイベントハンドラーを登録。
  • タイマーの設定: 約 60FPS(16ms間隔)で状態更新を行うためにタイマーを使用。
  • 初期配置: パドルは画面下部中央、ボールは画面中央に配置します。

3.2 キー入力とパドルの移動

  • SetKeyState メソッド: 左右キーの押下状態を管理。
  • UpdatePaddlePosition: 押下状態に応じ、パドルの X 座標を更新。画面外に出ないよう制限も実施。

3.3 ボールの動きと衝突判定

  • UpdateBallPosition: ボールの座標を毎フレーム速度分だけ更新。
  • CheckBallCollision:
    • 壁との衝突: 左右および上端でボールの位置を補正し、速度を反転。
    • 下端との衝突: 実際のゲームではライフ減少やゲームオーバー処理に変更可能ですが、ここでは反射処理。
    • パドルとの衝突: パドルとボールの矩形で判定。ボールが下方向に動いている場合、パドルの上部で反射し、衝突位置により左右の速度を調整しています。

3.4 描画処理

  • OnPaint メソッド: パドルとボールをそれぞれ矩形、楕円として描画。背景は白でクリアしています。

4. まとめ

このチュートリアルでは、WinForms を使ったシンプルなブロック崩し風シミュレーションを実装しました。

  • パドルの操作(左右キー入力)
  • ボールの動きと反射処理
  • 衝突判定(壁・パドルとの当たり判定)

これらの基本機能を実装することで、さらにブロックの配置、スコア管理、ゲームオーバー処理などを拡張して、本格的なブロック崩しゲームに発展させることが可能です。初心者の方も、この資料を参考にぜひ自分なりのアレンジを加えてみてください。


次のステップ