基本構造の設計
推し箱ゲームの主要なモジュールは、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/
デバッグとパフォーマンス最適化のヒント
- 衝突判定の高速化:事前に可動範囲を計算し、リアルタイムでのチェックを減らす。
- メモリリーク防止:不要なPictureBoxは破棄してメモリ使用量を抑える。
- 入力の重複抑制:
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;
}
動作の確認
- 基本操作:方向キーで赤いプレイヤーを動かし、青い箱を黄色の目標位置に押し込む。
- 勝利判定:すべての箱が目標位置に到達すると、「クリア!」メッセージが表示され、ゲーム終了。