WinFormsで実装するC#推し箱ゲームの設計と実装

基本構造の設計

推し箱ゲームの主要なモジュールは、GUI表示マップ管理プレイヤー操作衝突判定、および勝利条件の評価である。本稿では、C#のWinFormsを用いた実装を紹介する。

主要なコード実装

1. GUIとマップ初期化

using System;
using System.Drawing;
using System.Windows.Forms;

public class GameWindow : Form
{
    private PictureBox[,] gridCells;  // マップセルの描画用
    private PictureBox playerSprite;  // プレイヤーの表示オブジェクト
    private int[,] levelLayout = {
        {1,1,1,1,1},
        {1,0,0,2,1},
        {1,0,3,0,1},
        {1,0,0,0,1},
        {1,1,1,1,1}
    };

    public GameWindow()
    {
        this.Size = new Size(400, 400);
        SetupGrid();
        InitializePlayer();
    }

    private void SetupGrid()
    {
        int rows = levelLayout.GetLength(0);
        int cols = levelLayout.GetLength(1);
        gridCells = new PictureBox[rows, cols];

        for (int row = 0; row < rows; row++)
        {
            for (int col = 0; col < cols; col++)
            {
                var cell = new PictureBox();
                cell.Width = 40;
                cell.Height = 40;
                cell.BorderStyle = BorderStyle.FixedSingle;
                cell.Location = new Point(col * 40, row * 40);
                UpdateCellAppearance(row, col);
                this.Controls.Add(cell);
                gridCells[row, col] = cell;
            }
        }
    }

    private void UpdateCellAppearance(int r, int c)
    {
        Color bgColor = Color.White;
        switch (levelLayout[r, c])
        {
            case 1: bgColor = Color.DarkGray; break;     // 壁
            case 2: bgColor = Color.LightBlue; break;   // 箱
            case 3: bgColor = Color.Gold; break;        // 目標位置
        }
        gridCells[r, c].BackColor = bgColor;
    }
}

2. プレイヤーの操作と移動ロジック

private int playerRow = 1, playerCol = 1;
private const int CELL_SIZE = 40;

private void InitializePlayer()
{
    playerSprite = new PictureBox();
    playerSprite.Width = 30;
    playerSprite.Height = 30;
    playerSprite.BackColor = Color.Red;
    playerSprite.Location = new Point(playerCol * CELL_SIZE + 5, playerRow * CELL_SIZE + 5);
    this.Controls.Add(playerSprite);
}

protected override void OnKeyDown(KeyEventArgs e)
{
    int nextRow = playerRow, nextCol = playerCol;

    switch (e.KeyCode)
    {
        case Keys.Up: nextRow--; break;
        case Keys.Down: nextRow++; break;
        case Keys.Left: nextCol--; break;
        case Keys.Right: nextCol++; break;
    }

    if (CanMove(nextRow, nextCol))
    {
        UpdatePlayerPosition(nextRow, nextCol);
        CheckVictory();
    }
}

private bool CanMove(int newRow, int newCol)
{
    if (newRow < 0 || newRow >= levelLayout.GetLength(0) || 
        newCol < 0 || newCol >= levelLayout.GetLength(1)) return false;

    int target = levelLayout[newRow, newCol];
    if (target == 1) return false;  // 壁に衝突

    if (target == 2)  // 箱がある場合
    {
        int boxNextRow = newRow + (newRow - playerRow);
        int boxNextCol = newCol + (newCol - playerCol);

        if (boxNextRow < 0 || boxNextRow >= levelLayout.GetLength(0) ||
            boxNextCol < 0 || boxNextCol >= levelLayout.GetLength(1)) return false;

        if (levelLayout[boxNextRow, boxNextCol] == 0)  // 箱が移動できるか確認
        {
            levelLayout[boxNextRow, boxNextCol] = 2;  // 箱を移動
            levelLayout[newRow, newCol] = 0;          // 元の位置を空地にする
            return true;
        }
        return false;
    }

    // プレイヤーの位置更新
    levelLayout[playerRow, playerCol] = 0;
    playerRow = newRow;
    playerCol = newCol;
    levelLayout[playerRow, playerCol] = 3;  // プレイヤーが目標地点にいることを示す
    return true;
}

3. 勝利条件の判定

private void CheckVictory()
{
    bool allBoxesOnTarget = true;
    for (int i = 0; i < levelLayout.GetLength(0); i++)
    {
        for (int j = 0; j < levelLayout.GetLength(1); j++)
        {
            if (levelLayout[i, j] == 2)  // 箱がまだ目標にない
            {
                allBoxesOnTarget = false;
                break;
            }
        }
        if (!allBoxesOnTarget) break;
    }

    if (allBoxesOnTarget)
    {
        MessageBox.Show("クリア!おめでとうございます!", "勝利", MessageBoxButtons.OK, MessageBoxIcon.Information);
        this.Close();
    }
}

拡張機能と改善点

  • 複数ステージ対応:JSON形式のファイルに各レベルデータを保存し、外部から読み込むことで柔軟なステージ管理が可能。
  • アニメーション効果:Timerコンポーネントを使用して、プレイヤーの移動をスムーズにアニメーション化。
  • 音声フィードバックSoundPlayerクラスを利用して、移動や箱の押し出し時に効果音を再生。
  • 進行状態の保存BinaryFormatterまたはJsonSerializerを使って、現在のステージや手数をセーブ可能。

プロジェクト構成例

SokobanProject/
├── GameWindow.cs           // ゲームメインフォーム
├── LevelManager.cs         // レベルの読み込み・解析
├── PlayerController.cs     // 移動処理と衝突判定
└── Resources/
    ├── images/
    └── sounds/

デバッグとパフォーマンス最適化のヒント

  1. 衝突判定の高速化:事前に可動範囲を計算し、リアルタイムでのチェックを減らす。
  2. メモリリーク防止:不要なPictureBoxは破棄してメモリ使用量を抑える。
  3. 入力の重複抑制KeyPreview = trueを設定し、KeyPressイベントで連打をフィルタリング。

多箱対応の実装例

// 箱の移動をより一般的な交換処理に変更
private void SwapTiles(int r1, int c1, int r2, int c2)
{
    int temp = levelLayout[r1, c1];
    levelLayout[r1, c1] = levelLayout[r2, c2];
    levelLayout[r2, c2] = temp;
}

// CheckMove内の箱の処理を簡略化
if (targetTile == 2)
{
    int boxNextRow = newRow + (newRow - playerRow);
    int boxNextCol = newCol + (newCol - playerCol);

    if (IsInBounds(boxNextRow, boxNextCol) && (levelLayout[boxNextRow, boxNextCol] == 0 || levelLayout[boxNextRow, boxNextCol] == 3))
    {
        SwapTiles(newRow, newCol, boxNextRow, boxNextCol);
        return true;
    }
    return false;
}

動作の確認

  • 基本操作:方向キーで赤いプレイヤーを動かし、青い箱を黄色の目標位置に押し込む。
  • 勝利判定:すべての箱が目標位置に到達すると、「クリア!」メッセージが表示され、ゲーム終了。

タグ: C# WinForms 推し箱ゲーム GUIプログラミング ゲーム開発

8月31日 04:49 投稿