【C#入門】Visual Studioでスネークゲームを作る

スネークゲームのイメージ Tsubatter
スポンサーリンク

Visual StudioのWindows フォーム アプリケーションを使用すると、C#で簡単にそれらしいゲームを作ることができます。
前回、C言語で作ったスネークゲームをC#で作り直してみたいと思います。

C言語のスネークゲームについては以下の記事を参照してください。
ほとんど同じ量のソースコードしか書いていませんが、出来上がるアプリケーションのクオリティは圧倒的にC#で作った方が高いことがわかります!

C言語で簡単なスネークゲームを作ってみる
C言語でできることのご紹介として、簡単なスネークゲームを作りました。 C言語は基本的に組み込みソフトを開発するための言語という紹介をしてきましたが、内容をきちんと理解していれば思い通りのゲームを作ることも可能です。 今回作成したゲームは、C...
こんな人にオススメ
  • C#という名前はよく聞くけどイメージできない
  • C#とC言語の違いがわからない
  • 本格的なゲームを作ってみたい
  • オブジェクト指向を勉強したい

C#とは

C#はマイクロソフトが開発したオブジェクト指向プログラミング言語の一つで、WindowsアプリケーションやWebアプリケーションなどの開発に使用されます。

Windowsアプリケーションの開発には、統合開発環境であるVisual Studioを用いることが一般的です。
Visual StudioでC#を使う場合、同じくマイクロソフトが提供している.NET Frameworkというプラットフォームを活用することで、いろんな機能の実装がとても簡単にできるようになっています。

そのC#のメリット・デメリットについて、C言語と比較しながら見てみましょう。

チェック

C言語のメリット:

  • メモリを自分で管理できるため、高速で効率的なプログラムを作成できる。
  • ハードウェアを制御するドライバソフトなど、低レベルの処理に適している。

C言語のデメリット:

  • メモリを自分で管理するため、メモリリークやバッファオーバーフローなどのバグが発生しやすい。
  • オブジェクト指向プログラミングには向いていない。

C#のメリット:

  • .NET Frameworkに組み込まれていて、豊富な機能やライブラリを利用できるため、高度なアプリケーションを開発できる。
  • オブジェクト指向プログラミングで実装できるため、コードの再利用性が高く結合度が弱い。
  • 安全性が高く、メモリ管理が自動化されているため、バグが発生しにくい。

C#のデメリット:

  • 実行速度がC言語に比べて遅い。
  • C言語と比べて文法が複雑。

スネークゲームとは

画面の中でヘビを動かして、餌を食べていき成長していくゲームです。
壁、もしくは自分の体に当たるとゲームオーバーです。

スネークゲームのイメージ

環境

OSWindows11
統合開発環境Visual Studio 2022(Windows フォーム アプリケーション)
.NET Framework4.7.2
今回開発に使用した環境

ソースコード

ソースコードは以下のgitにて公開しています。

Tsubablog / SnakeGame · GitLab
GitLab.com

以下の手順で、ゲームを実行することができます。

実行方法
  1. gitからソースコードをダウンロード
  2. Visual Studioをインストール
  3. 「SnakeGame.sln」を起動して、「Release」バリアントでビルドを実行
  4. 下記の通り、画像ファイル全てをコピー
    (コピー元)\snakegame-master\images\(png画像ファイル全て)
    (コピー先)\snakegame-master\bin\Release\(このパスへ画像データを全てコピー)
  5. \snakegame-master\bin\Release\SnakeGame.exeをダブルクリックで実行

解説

プロジェクトの作成

C#をインストールしたVisual Studioを起動して、「新しいプロジェクトを作成」をクリックする。

「Windows フォーム アプリケーション」を選択して「次へ」をクリックする。

Windows フォーム アプリケーション

プロジェクトに任意の名前をつけて「作成」を押下する。

プロジェクト名を入力する

以上でプロジェクトの作成は完了です。

Formの作成

スネークゲームは3つの画面で構成します。
仕様は以下の通りです。

スネークゲームの画面遷移

メニューフォーム

プロジェクトを作成したときに、「Form1.cs」が自動で作成されていると思います。
まずはForm1を「メニュー」画面に設定します。

ポイント

Visual Studioの上部に以下の文章が出ている場合は、「ディスプレイ設定」から「拡大/縮小」を「100%」に設定してVisual Studioを再起動してください。

パソコンの画面は小さなドットの集まりで表示されていますが、決められた領域の中にいくつドットがあるかを表す単位をdpi(dot per inch)といいます。

最近のパソコンは高解像度で領域当たりのドットが多いので、昔ながらの画像を表示すると小さい領域で表示できてしまうため、画像が小さく表示されます。

「拡大/縮小」は、小さく表示されることを防ぐために拡大して表示してくれる機能ですが、この機能がONになっていると、異なる解像度のPCでアプリケーションを表示したときに、レイアウトが崩れます。

まずは自動で作成されたファイル「Form1.cs」のファイル名を変更しておきます。
ソリューションエクスプローラーから「Form1.cs」を右クリックして「名前の変更」を選択し、「Menu.cs」という名前に変更します。

「Form1.cs」のファイル名を変更する

次にForm1にボタンを2つ追加します。

左のタブにある「ツールボックス」から「Button」をForm1の上にドラッグ&ドロップすることでボタンを追加することができます。
これを2回繰り返してボタンを2個つくりましょう。

フォーム1にボタンを追加する

次にフォームとボタンのプロパティ、イベントを設定します。
まずは「Form1」を選択した状態で、右下の「プロパティ」タブから「プロパティ」アイコンを選択して、それぞれ下表の通り設定します。

部品のプロパティを設定する

●Form1

プロパティ名設定値
(Name)FMenu
AutoScaleModeDpi
Location0, 0
Size280, 171
Textメニュー

同様の方法でボタンのプロパティも以下の通りに設定します。

●Button1

プロパティ名設定値
(Name)BStart
Location12, 12
Size240, 50
Textスタート

●Button2

プロパティ名設定値
(Name)BEnd
Location12, 68
Size240, 50
Textおわる

完成系は以下のようになります。

メニューフォームの完成系

スネークゲーム フォーム

2つ目のフォームを追加して、スネークゲームのメイン画面を作成します。

「プロジェクト」タブの「フォームの追加」をクリックして新しいフォームを追加します。

フォームを追加する

フォームに「SnakeGame.cs」という名前を付けて「追加」をクリックします。

作成したフォーム上に「パネル」を配置します。
パネルは、複数の部品をひとまとめにするのに便利です。
(後ほどパネルという親の部品に子の部品を紐づけていきます。)

フォームにパネルを追加する

フォームとパネルのプロパティをそれぞれ下表のように設定します。

●Form1

プロパティ名設定値
(Name)FSnakeGame
AutoScaleModeDpi
Location0, 0
Size599, 622
Textスネークゲーム

●panel1

プロパティ名設定値
(Name)PField
BackColorDarkKhaki
Location12, 12
Size560, 560

完成系は以下のようになります。

スネークゲーム フォームの完成系

リザルト フォーム

同様の手順で「Result.cs」のフォームを追加して、ボタンを2つ、ラベルを1つ追加します。
フォームとボタンのプロパティを下表のように設定します。

●Form1

プロパティ名設定値
(Name)FResult
AutoScaleModeDpi
Location0, 0
Size346, 184
Textリザルト

同様の方法でボタンのプロパティも以下の通りに設定します。

●Button1

プロパティ名設定値
(Name)BReplay
Location12, 83
Size150, 50
Textリプレイ

●Button2

プロパティ名設定値
(Name)BEnd
Location168, 83
Size150, 50
Textおわる

●Label1

プロパティ名設定値
(Name)LResult
Location98, 28
Size137, 30
TextGameOver

完成系は以下のようになります。

リザルト フォームの完成系

これでフォームの作成は完了です。

ポイント
  • プロパティ
    部品の設定ができます。
  • イベント
    部品に対して、特定のインプットを条件に処理を実行します。

メニュー フォームの実装

「Menu.cs[デザイン]」を開き、「スタート」ボタンを選択した状態で、「プロパティ」タブの「イベント」から「Click」の欄に「BStart_click」と入力します。

ボタンがクリックされたときのイベントを追加する

すると、「Menu.cs」ファイルが開かれて、「BStart_click」の関数が自動で作成されます。
「スタート」ボタンをクリックするとこの関数が呼び出されます。
ここにボタンをクリックしたときの処理を実装していきます。

同様に「おわる」ボタンにも「BEnd_click」関数を追加します。

それぞれの処理を実装すると、「Menu.cs」ファイルは以下のようになります。

        private void BStart_click(object sender, EventArgs e)
        {

            // スネークゲーム フォームのインスタンスを生成
            FSnakeGame f_SnakeGame = new FSnakeGame();
            // Showで画面に表示
            // 自分自身のポインタを引数で渡すことで
            // スネークゲームフォームからメニューフォームを操作できる
            f_SnakeGame.Show(this);

            // メニューフォームを隠す(インスタンスは残る)
            Hide();
        }

        private void BEnd_click(object sender, EventArgs e)
        {
            // メニューフォームを閉じる
            Close();
        }

これで、「スタート」ボタンを押すと「FSnakeGame」フォームが立ち上がり、「おわる」ボタンを押すとアプリケーションが終了する実装ができあがりました。

リザルト フォームの実装

追加する実装は以下の通りです。

        private void BReplay_click(object sender, EventArgs e)
        {
            // 親(スネークゲーム フォーム)を開く
            Owner.Show();
            Hide();
        }

        private void BEnd_click(object sender, EventArgs e)
        {
            // アプリケーションを終了
            // 残っている親のインスタンスも含めて全て閉じる
            Application.Exit();
        }

        private void FResult_formClosed(object sender, FormClosedEventArgs e)
        {
            Application.Exit();
        }

これで「リプレイ」ボタンを押すとスネークゲームフォームを開き、「おわる」ボタンを押すとアプリケーションが終了します。

また、フォームにイベントで「FormClosed」を追加しています。
「×」ボタンを押したときに親のフォームが残ったままリザルトフォームが消えてしまい、プロセスが残り続ける状態を防ぎます。

スネークゲーム フォームの実装

スネークゲーム フォームの実装では、以下のようなクラス関係で作りたいと思います。

スネークゲーム フォームのクラス図
ポイント
  • CConstants
    フィールドの大きさやスネークの長さなどの定数値をまとめたスタティックなクラスです。
  • FSnakeGame
    タイマーやキーボード入力のイベントハンドラを持たせます。
    このクラスが定期的に上記2つのイベントを処理することでゲームを進行します。
    (C言語のmain関数に近い処理をします。)
  • CSnake
    スネークの座標や移動の計算など、スネークに関わる処理をまとめたクラスです。
    FSnakeGameにメンバ変数として持たせます。
  • CFood
    餌の座標や作成など、餌に関わる処理をまとめたクラスです。
    FSnakeGameにメンバ変数として持たせます。
  • Position
    座標(x, y)を定義します。
    スネークや餌にメンバ変数として持たせることで、座標を管理します。

CConstants

スネーク フォーム内で使用する定数を定義します。
C#ではマクロ(#define)を使用できないので、定数のみを集めたクラスを宣言することにします。

ポイント
  • staticなクラス
    自動でメモリ上にインスタンスが生成されるので、自分でインスタンスを生成できない。
    アクセス修飾子をpublicにすると、どこからでもアクセスが可能。
  • const
    constで定義した数値は変更することができないため、定数として使用可能。
    staticなクラスでどこからでもアクセスできるが、書き換えられないので安心。
    // 定数用クラス
    static class CConstants
    {
        // フィールドの横幅と縦幅
        public const int WIDTH = 10;
        public const int HEIGHT = 10;

        // スネークの頭の初期位置
        public const int STARTX = WIDTH / 2;
        public const int STARTY = HEIGHT / 2;

        // スネークの最大長
        public const int SNAKE_MAX_LENGTH = (WIDTH - 2) * (HEIGHT - 2);

        // キー入力用定数
        public enum Direction
        {
            left,
            up,
            right,
            down,
            non
        };
    }

FSnakeGame

コンストラクタとメンバ変数

コンストラクタは、インスタンスが生成されたときに必ず一度呼ばれる処理です。
基本的にメンバ変数の初期化処理を書くことが多いです。

スネークゲーム フォームクラスのコンストラクタではPictureBoxという部品を、デザイナーではなく手書きで実装します。
これは、PictureBoxを配列で宣言してプログラム中で扱いやすくするためです。

PictureBoxは定数のWIDTH × HEIGTH分だけパネルの上に作成して、状況に応じて壁・スネークの頭・スネークの胴体・餌のどれかを表示します。

PictureBoxの仕様
ポイント
  • 部品の手書き実装
    デザイナーで部品を作って、実装でプロパティを変更することができる。
    デザイナーで作った部品を実装で消したりすると不整合が起こりエラーになる。
    実装で一から部品を作ることができる。
  • 部品の配列
    複数の部品を配列で宣言することで、for文などとの相性を良くする。
  • メンバ変数
    メンバ変数はコンストラクタで必ず初期化する。
    初期化をしないとnull参照が起こり、意図しない動作になる可能性がある。
        public FSnakeGame()
        {
            // 自動で生成される関数
            // デザイナー画面で追加した部品を初期化してくれます。
            // この関数の中身を下手にいじるとデザイナーと不整合が起こるので
            // 触らないでください。
            InitializeComponent();

            // フィールドの大きさに合わせてフォームサイズを調整
            this.ClientSize = new System.Drawing.Size(20 + 55 * CConstants.WIDTH, 20 + 55 * CConstants.HEIGHT);

            // フィールドの大きさに合わせてパネルサイズを調整
            this.P_Field.Size = new System.Drawing.Size(55 * CConstants.WIDTH, 55 * CConstants.HEIGHT);

            //ResumeLayout()が呼び出されるまで、複数のLayoutイベントが発生しないようにする
            SuspendLayout();

            //PictureBoxをフィールドのマスの分だけ生成する
            for (int i = 0; i < CConstants.WIDTH; i++)
            {
                for (int j = 0; j < CConstants.HEIGHT; j++)
                {
                    // インスタンスの生成
                    Pbox[i, j] = new PictureBox();
                    P_Field.Controls.Add(Pbox[i, j]);
                    Pbox[i, j].Name = "Pbox[" + i.ToString() + "][" + j.ToString() + "]";
                    Pbox[i, j].Location = new Point((55 * i), (55 * j));
                    Pbox[i, j].Size = new Size(50, 50);
                }
            }
            ResumeLayout();

            // 自分で定期的に初期化したい変数や処理をinit()関数にまとめて呼び出す。
            init();
        }

        private void init()
        {
            m_inKeyDir = CConstants.Direction.left;
            m_snake.initSnake();
            m_food.makeFood(m_snake.getPos(), m_snake.getLength());
            // 壁は最初に1度だけ描画して、以降そのままにしておく
            for (int i = 0; i < CConstants.HEIGHT; i++)
            {
                for (int j = 0; j < CConstants.WIDTH; j++)
                {
                    if (i == 0 || i == (CConstants.HEIGHT - 1) || j == 0 || j == (CConstants.WIDTH - 1)) m_Pbox[i, j].ImageLocation = @"wall.png";
                }
            }
            // タイマーを有効にする
            this.T_CycleTimer.Enabled = true;
        }

        // メンバ変数
        private PictureBox[,] m_Pbox = new PictureBox[CConstants.WIDTH, CConstants.HEIGHT];
        private CSnake m_snake = new CSnake();
        // 入力されたキーを記憶しておき、タイマーが発火時に進める
        private CConstants.Direction m_inKeyDir;
        private CFood m_food = new CFood();
キーボード入力

ユーザーが入力したキーボードをプログラムが検知する処理です。

ポイント
  • マルチスレッドで、常にキーが入力されたかを監視してくれる。
  • キーを監視するプログラムが処理中でも、別のプログラムを平行で処理できる。

デザイナーの画面でフォームに対してイベントプロパティから「KeyDown」の欄に「FSnakeGame_KeyDown」と入力します。
すると、ソースコードに自動で「FSnakeGame_KeyDown」の関数が作成されます。

そこへ以下のように実装します。

        private void FSnakeGame_keyDown(object sender, KeyEventArgs e)
        {
            switch (e.KeyCode)
            {

                // 入力されたキーに対応する定数を格納する
                case Keys.Left:
                    m_inKeyDir = CConstants.Direction.left;
                    break;
                case Keys.Up:
                    m_inKeyDir = CConstants.Direction.up;
                    break;
                case Keys.Right:
                    m_inKeyDir = CConstants.Direction.right;
                    break;
                case Keys.Down:
                    m_inKeyDir = CConstants.Direction.down;
                    break;
                default:
                    // 何もしない
                    break;
        }

キーが入力されると、引数の「e」に入力されたキーが格納された状態でこの関数が呼ばれます。
入力されたキーは「e.KeyCode」で参照することができるので、Switch文でカーソルキーの何が入力されたかを判断します。

入力されたキーはメンバー変数の「m_inKeyDir」に保存しておき、のちほどスネークを動かすときに使います。
一定時間が経過したときに最後に入力されたキーを元にスネークを動かしたいので、最後に入力したキーで「m_inKeyDir」を上書きします。

タイマー

デザイナー画面で「Timer」の部品を追加します。

スネークゲーム フォームにタイマーを追加する

デザイナー画面の左下にタイマー部品が追加されるので、プロパティとイベントを下表の通りに設定します。

追加したタイマーをダブルクリックする

●timer1

プロパティ名設定値
(Name)T_CycleTimer
EnableTrue
GenerateMemberTrue
Interval500
ModifiersPrivate
イベント名設定値
TickT_CycleTimer_tick

ソースコードに、タイマーが発火したときに呼ばれる関数が自動で追加されます。
ここに記載した実装が500[ms]の間隔で実行されます。

タイマーが発火すると呼ばれる関数

ここへ以下の実装を追加します。

        private void T_CycleTimer_tick(object sender, EventArgs e)
        {
            // ヘビを最後に入力されたキーの方向へ進める
            m_snake.move(m_inKeyDir);
            // ヘビが餌や壁に当たったか判定する
            judge();
            // フィールドを描画する
            viewField();
        }

これによって、以下の処理を500[ms]ごとに繰り返します。

  1. 最後に入力された方向キーを元にスネークを動かす。
  2. 動いた先で状況を判定する。
    (餌を食べたか、壁や体に当たったか、クリアか)
  3. 現在の状況をフィールドに表示する。
判定処理

スネークが動いたときに、動いた先の状況を判定する処理です。

ポイント
  • 壁に当たった場合
    スネークの頭のx座標、もしくはy座標が壁の座標にいた場合、壁に当たったと判定してリザルト画面を表示する。
  • 体に当たった場合
    スネークの頭の座標を、体の5番目からしっぽまでの座標と順番に比較していき、一致しているところがあれば体に当たったと判定してリザルト画面を表示する。
  • 餌に当たった場合
    スネークの頭の座標を餌の座標と比較して、一致していれば餌を食べたと判定して、餌の座標を移動して、スネークの長さを1つ伸ばす。
        private void judge()
        {
            // 頭が壁に当たったらアウト
            // リザルトフォームを表示する
            if (m_snake.getBodyX(0) == 0 || m_snake.getBodyX(0) == (CConstants.WIDTH - 1) || m_snake.getBodyY(0) == 0 || m_snake.getBodyY(0) == (CConstants.HEIGHT - 1))
            {
                // タイマーをオフにして、リザルト画面を表示
                this.T_CycleTimer.Enabled = false;
                // リザルトフォームに文字列を渡す
                FResult f_gameOver = new FResult("Game Over...");
                f_gameOver.Show(this);
                Hide();
            }

            // 頭が体に当たったらアウト
            // リザルトフォームを表示する
            if (m_snake.getLength() < 5); // 処理を飛ばす 
            else if (m_snake.getLength() >= 5)
            {
                for (int i = 4; i < m_snake.getLength(); i++)
                {
                    if (m_snake.getBodyX(0) == m_snake.getBodyX(i) && m_snake.getBodyY(0) == m_snake.getBodyY(i))
                    {
                        this.T_CycleTimer.Enabled = false;
                        FResult f_gameOver = new FResult("Game Over...");
                        f_gameOver.Show(this);
                        Hide();
                    }
                }
            }

            // 餌を食べた
            if (m_snake.getBodyX(0) == m_food.getBodyX() && m_snake.getBodyY(0) == m_food.getBodyY())
            {
                // ヘビが最大長になったらクリア!
                // リザルトフォームを表示する
                if (m_snake.getLength() == (CConstants.SNAKE_MAX_LENGTH - 1))
                {
                    this.T_CycleTimer.Enabled = false;
                    FResult f_gameOver = new FResult("Clear!!");
                    f_gameOver.Show(this);
                    Hide();
                }
                // 餌を移動する
                m_food.makeFood(m_snake.getPos(), m_snake.getLength());

                // スネークを延ばす
                m_snake.growSnake();
            }
        }
画面表示処理

画面を表示する処理を実装します。

タイマーで500[ms]ごとにスネークや餌の位置が変わるので、その度に画面を最新の状態に表示する関数です。

ポイント
  • 最初にフィールドの画像を全て初期化して何も表示されていない状態にする。
    (壁はinit()関数で設定してから座標が変わらないので、表示しっぱなし)
  • スネークの頭、体がある座標のPictureBoxにスネークの画像を表示する。
  • 餌がある座標のPictureBoxに餌の画像を表示する。
        private void viewField()
        {
            // フィールドの壁以外を初期化
            for (int i = 0; i < CConstants.WIDTH; i++)
            {
                for (int j = 0; j < CConstants.HEIGHT; j++)
                {
                    // 壁であれば初期化対象から除外する
                    if (i == 0 || i == (CConstants.HEIGHT - 1) || j == 0 || j == (CConstants.WIDTH - 1)) continue;
                    // 壁でなければ初期化する
                    m_Pbox[i, j].Image = null;
                }
            }

            // スネークをフィールドに反映
            for (int i = 0; i < m_snake.getLength(); i++)
            {
                if(i == 0) m_Pbox[m_snake.getBodyX(i), ((CConstants.HEIGHT - 1) - m_snake.getBodyY(i))].ImageLocation = @"SnakeHead.png";
                else m_Pbox[m_snake.getBodyX(i), ((CConstants.HEIGHT - 1) - m_snake.getBodyY(i))].ImageLocation = @"SnakeBody.png";
            }

            // 餌をフィールドに反映
            m_Pbox[m_food.getBodyX(), ((CConstants.HEIGHT - 1) - m_food.getBodyY())].ImageLocation = @"food.png";
        }

CSnake

コンストラクタとメンバ変数

コンストラクタの中で独自の初期化関数であるinitSnake()を呼び出し、initSnake()の中でメンバ変数の初期化を行います。

スネークはリザルト画面で「リプレイ」ボタンを押したときなど、何度も初期化したいケースがあるので、このような作りにしました。
コンストラクタはクラスのインスタンスを作ったときに一度だけ呼ばれますが、initSnake()は外部から何度でも呼び出すことができます。

ポイント
  • 体の座標を配列で持つ。
  • 現在のスネークの長さを持ち、この長さまでスネークを表示する。
        // コンストラクタ
    public CSnake()
        {
            initSnake();
        }

        // ヘビを初期化する
        public void initSnake()
        {
            m_snakeLen = 2;
            // スネークの初期化
            Array.Clear(m_bodyPos, 0, CConstants.SNAKE_MAX_LENGTH);
            m_bodyPos[0].x = CConstants.STARTX;
            m_bodyPos[0].y = CConstants.STARTY;

            m_bodyPos[1].x = CConstants.STARTX + 1;
            m_bodyPos[1].y = CConstants.STARTY;
        }

        // メンバ変数
        private int m_snakeLen;
        private Position[] m_bodyPos = new Position[CConstants.SNAKE_MAX_LENGTH];
スネークを動かす処理

move()は入力されたキーの方向を引数に渡せば、その方向へスネークを移動してくれる関数です。
ただし、首がある方向へキーが入力されれば無効として、入力と逆(進行方向)へスネークを進めます。

しっぽの方から順に配列の座標をシフトしていき、最後に頭の座標を書き換えます。

        public void move(CConstants.Direction direction)
        {

            switch (direction)
            {
                case CConstants.Direction.left:
                    //首がある方向のキー入力だった場合は受け付けない
                    if (m_bodyPos[0].x == (m_bodyPos[1].x + 1))
                    {
                        shiftSnake(); // 首から下の座標を頭方向にシフトする
                        m_bodyPos[0].x += 1; // 頭を移動方向へシフトする
                        break;
                    }
                    shiftSnake();
                    m_bodyPos[0].x -= 1;
                    break;
                case CConstants.Direction.up:
                    if (m_bodyPos[0].y == (m_bodyPos[1].y - 1))
                    {
                        shiftSnake();
                        m_bodyPos[0].y -= 1;
                        break;
                    }
                    shiftSnake();
                    m_bodyPos[0].y += 1;
                    break; 
                case CConstants.Direction.right:
                    if (m_bodyPos[0].x == (m_bodyPos[1].x - 1))
                    {
                        shiftSnake();
                        m_bodyPos[0].x -= 1;
                        break;
                    }
                    shiftSnake();
                    m_bodyPos[0].x += 1;
                    break;
                case CConstants.Direction.down:
                    if (m_bodyPos[0].y == (m_bodyPos[1].y + 1))
                    {
                        shiftSnake();
                        m_bodyPos[0].y += 1;
                        break;
                    }
                    shiftSnake();
                    m_bodyPos[0].y -= 1;
                    break;
                default:
                    // 何もしない
                    break;
        }

        // 首から下の座標を頭方向にシフトする
        private void shiftSnake()
        {
            for (int i = 1; i < m_snakeLen; i++)
            {
                m_bodyPos[m_snakeLen - i].x = m_bodyPos[m_snakeLen - (i + 1)].x;
                m_bodyPos[m_snakeLen - i].y = m_bodyPos[m_snakeLen - (i + 1)].y;
            }
        }
スネークを伸ばす処理

スネークが餌を食べたら、長さを1増やし、スネーク配列のしっぽを1つ伸ばし、座標は元のしっぽと同じものを代入します。
(しっぽが2つ重なった状態になります)

        public void growSnake()
        {
            m_snakeLen += 1;
            m_bodyPos[m_snakeLen - 1].x = m_bodyPos[m_snakeLen - 2].x;
            m_bodyPos[m_snakeLen - 1].y = m_bodyPos[m_snakeLen - 2].y;
        }
スネークの座標などを取得する処理

FSnakeGameクラスから、CSnakeクラスの座標や長さなどを取得する関数を用意します。

        // 体の位置を格納した配列を返す
        public Position[] getPos()
        {
            return m_bodyPos;
        }

        // 体の長さを返す
        public int getLength()
        {
            return m_snakeLen;
        }

        // 指定されたインデックスの体のx座標を返す
        public int getBodyX(int index)
        {
            return m_bodyPos[index].x;
        }

        // 指定されたインデックスの体のy座標を返す
        public int getBodyY(int index)
        {
            return m_bodyPos[index].y;
        }

CFood

コンストラクタとメンバ変数

餌は座標の情報のみあれば作ることができるので、メンバ変数は座標のみです。

        // コンストラクタ 
        public CFood()
        {
            m_bodyPos.x = 0;
            m_bodyPos.y = 0;
        }

        // メンバ変数
        private Position m_bodyPos = new Position();
餌を作る処理

餌の座標は乱数で決定します。
ただし、スネークと重なっていた場合は別の場所に餌を移さないといけません。

このときにもう一度乱数を使うと、スネークが長くなって空きマスが少なくなった際に、運よく空きマスに餌が作られる確率が小さく処理時間がかかってしまいます。

そこで、スネークと重なった場合は横にずらす仕組みにしています。

public void makeFood(Position[] excludePos, int indexLen)
        {
            // 餌の座標を初期化
            m_bodyPos.x = 0;
            m_bodyPos.y = 0;

            while (true)
            {
                // 餌の座標を1~8でランダムに生成する
                Random r = new Random();
                m_bodyPos.x = 1 + r.Next(CConstants.WIDTH - 3);
                m_bodyPos.y = 1 + r.Next(CConstants.HEIGHT - 3);

                // ヘビの頭からしっぽまでで重なっていないかを確認する
                for (int i = 0; i < indexLen; i++)
                {
                    // ヘビのどこかと重なっていた場合
                    if (excludePos[i].x == m_bodyPos.x && excludePos[i].y == m_bodyPos.y)
                    {
                        // 重なった場所から1マスずらす
                        // 右下角だったら左上角に移す
                        if (m_bodyPos.x == (CConstants.WIDTH - 2) && m_bodyPos.y == (CConstants.HEIGHT - 2))
                        {
                            m_bodyPos.x = 1;
                            m_bodyPos.y = 1;
                        }
                        // 一番右の列だったら一番左の列に移す
                        else if (m_bodyPos.x == CConstants.WIDTH - 2)
                        {
                            m_bodyPos.x = 1;
                            m_bodyPos.y += 1;
                        }
                        // その他の場合は右に一つずらす
                        else
                        {
                            m_bodyPos.x += 1;
                        }
                        // もう一度ヘビの頭から順に重なっていないか確認する
                        i = -1;
                    }
                }
                // 餌がヘビの体のどことも重なっていなければループを抜ける
                break;
            }
        }
餌の座標を取得する処理

FSnakeGameクラスから餌の座標を取得する関数を用意します。

 // 餌のx座標を返す
        public int getBodyX()
        {
            return m_bodyPos.x;
        }

        // 餌のy座標を返す
        public int getBodyY()
        {
            return m_bodyPos.y;
        }

Position

x, y座標の構造体を宣言しておきます。
スネークと餌の座標を管理するために使います。

struct Position
    {
        public int x, y;
    }

以上で実装はすべて完了です。

まとめ

これで簡単にそれらしいゲームを作成することができました。
C#で作り直したことで、C言語バージョンではできなかった画像の表示などができ、より高度なものになっています。

実際にC#を使用して作業してみることで、C#への理解も深められるはずです。
みなさんもぜひ一度作ってみてください。

Tsubatter
スポンサーリンク
tsubablog

メーカーで組み込みプログラマーとして勤務しているサラリーマンです。
プログラミングの楽しさについて発信しています。

業務で扱っている言語はC、C++です。

tsubablogをフォローする
つばブログ

コメント

タイトルとURLをコピーしました