こんにちは、きうちです。
すっかり寒くなりましたね!秋のような、もう冬のような。
秋はお芋とか栗とかおいしいものいっぱいだし、
冬も鍋物なんかがおいしくなる季節ですね。
気温が下がるとお外も走りやすくなるし、スポーツの秋最高!
運動のあとのお風呂やビールもまたよし。
その季節その季節でいいものはいっぱいありますね。
★ ★ ★
今日は「あれを作る」と題してお送りしようかと思います。
あれってなんだ、というわけなんですが、作ろうとしているのは以下のようなシンプルなゲームです。

- 画面に1から10までの数字が描かれた円盤が並ぶ。
- マウスで1から順番にクリックする。
- クリックされた円盤は消える。
- すべての円盤を消すと、そのステージはクリア。
これ、昔からよくあるゲームだと思いますが、Webで調べても統一的な名前は見当たらないですね。
なので、とりあえず「あれ」と呼ぶことにしました。
なお、1から10って書きましたが別に数字はもっと大きくてもよいですね。
★ ★ ★
言語は例によってC#にします。Windowsフォームアプリとして作成します。
画面サイズは1024×768にします。
背景は黒にします。
DoubleBufferedは・・・まあ、今回はそこまで動きが激しくないのでそのままでいいかな。まあ、trueにしてもいいですね。
フォームクラスの実装に入る前に、まず以下のような円盤の情報を保持するクラスを作ります。
    internal class Disk // 円盤の情報
    {
        public int N { get; set; } // 番号
        public int X { get; set; } // X座標
        public int Y { get; set; } // Y座標
        public int R { get; set; } // 半径
        public bool Erased { get; set; } = false; // 消去されたか否か
        internal Disk(int n, int x, int y, int r) // コンストラクタ
        {
            N = n;
            X = x;
            Y = y;
            R = r;
        }
    }
まあ、その円盤の番号や座標や半径を保持するためのクラスです。
その円盤が消去済みか?のフラグも持たせます。
ちょっと今更なんですが、プログラム中に書いているコメントについて、1つおことわりを・・・
私がこのブログで書いているプログラムはコメントが // で、行末に書いてあったりますが、
本来はメソッドやフィールドのコメントは /// コメント(ドキュメントコメント)で書くべきですね。
単に、このブログに載せる際に「タグに使われる小なり記号・大なり記号をエスケープするのが面倒」なので、こうしています。
悪しからず、ご了承いただければ幸いですm(_ _)m
もちろん、現場でコーディングしているときはちゃんと /// コメント書いています^^
★ ★ ★
続いて、フォームクラスのコーディング。
乱数生成用のRandomのインスタンスと、先ほど定義したDiskクラスを要素に持つリスト。
それから、円盤の半径と円盤の数。
このあたりをフィールドに持たせます。
public partial class Form1 : Form // フォーム画面クラス
{
    private Random random = new Random(); // 乱数生成用
    private List list = new List(); // 円盤のリスト
    private int r = 32; // 円盤の半径
    private int numberOfDisks = 10; // 円盤の数
    ...
}
  
半径はとりあえず32ピクセルにしておきます。
円盤の数は10です。もちろんこれを大きくするとその分円盤がたくさん描かれて、クリアするまでよりたくさんの円盤をクリックしないといけなくなります。
コンストラクタでは、InitializeComponentの呼び出しの後で、新規ゲームを開始するためのメソッドを呼びます。
    public Form1() // コンストラクタ
    {
        InitializeComponent();
        StartNewGame(); // 新規ゲーム開始
    }
新規ゲームを開始するためのメソッドはこんな風に実装します。
    public void StartNewGame() // 新規ゲーム開始
    {
        list.Clear(); // リストクリア
        for (int i = 0; i < numberOfDisks; i++) // 決められた数だけ円盤を生成してリストに追加する
        {
            int x = random.Next(r, ClientSize.Width - r);
            int y = random.Next(r, ClientSize.Height - r);
            list.Add(new Disk(i + 1, x, y, r));
        }
        Refresh(); // 画面を再描画する
    }
リストをクリアした後、決められた数だけ円盤のデータを作り、リストに格納します。
それが終わったら画面の再描画。
その描画処理は以下のような感じで書きます。
    private void Form1_Paint(object sender, PaintEventArgs e) // 描画処理
    {
        foreach (Disk d in list) // 円盤の数だけ繰り返す
        {
            if (!d.Erased) // 未消去のものであれば
            {
                string n = d.N.ToString(); // 番号を文字列化する
                e.Graphics.FillEllipse(Brushes.Aqua, d.X - d.R, d.Y - d.R, d.R * 2, d.R * 2); // 円盤を描く
                e.Graphics.DrawString(n, new Font("MS ゴシック", 16), Brushes.Blue, d.X - 10 - ((n.Length - 1) * 8), d.Y - 12); // 番号を描く
            }
        }
    }
基本は円盤の数だけ繰り返し。
if文で未消去の円盤か否か判定し、未消去であれば円を描いてその上に番号を描きます。
字を書く位置のX座標は、2桁の場合は1桁の時よりも少し左に描きたいので、文字数に応じて座標が変わるようにしています。
フォームデザイナから、このメソッドをフォームのPaintイベントとして設定しておきます。

★ ★ ★
ここまで書いたらいったん実行してみます。

・・・描かれましたね!
ここでは円盤は水色、字は青にしてみましたがお好みで変更してみてくださいね。
★ ★ ★
続いて、「マウスでクリックしたら消える」というのをやってみます。
フォームのマウスクリックイベントとして定義します。
Clickイベントではなく、MouseClickイベントにするのがミソです。クリック座標が得られますからね。
以下のような感じ。
        private void Form1_MouseClick(object sender, MouseEventArgs e) // フォームマウスクリック時処理
        {
            int erasedCount = 0; // 消去カウンタ初期化
            int totalErasedCount = 0; // トータル消去カウンタ初期化
            foreach (Disk d in list) // 円盤の数だけ繰り返す
            {
                int xx = d.X - e.X;
                int yy = d.Y - e.Y;
                double r0 = Math.Sqrt((xx * xx) + (yy * yy));
                if (r0 <= r) // クリック位置が円盤の中であれば
                {
                    d.Erased = true; // 該当する円盤の消去フラグを立てる
                    erasedCount++; // 消去カウンタを1増やす
                }
                if (d.Erased) // 円盤が消去済みであれば
                {
                    totalErasedCount++; // トータル消去カウンタを1増やす
                }
            }
            if (erasedCount >= 1) // 消去カウンタが1以上であれば
            {
                Refresh(); // 画面再描画
            }
            if (totalErasedCount == list.Count) // 円盤が全部消去されたら
            {
                MessageBox.Show(this, "Clear!"); // Clear!とメッセージボックスで表示
                StartNewGame(); // 新規ゲーム開始
            }
        }
イベント引数でクリックした座標を得られますので、その位置が円盤のいずれかの内側になっているかを見ます。
上記コードのforeach文のところがそれです。
d.X, d.Y が円盤の中心の座標であり、e.Xとe.Yがクリックした座標です。

あとはその中心からクリック位置までの距離を測り、それが円盤の半径以下であれば「クリックされた」とみなします。
クリックされた円盤の Erased フラグを立てます。と同時に、消去カウンタを+1します。
また、Erasedフラグが立っているか見て、立っていたらトータル消去カウンタを+1します。
全部終わったらまず消去カウンタが1以上か判定し、そうであれば画面を再描画します。
さらに、トータル消去カウンタが円盤の数と一致しているか見ます。一致していれば「全部消した」ということなのでメッセージボックスで「Clear!」と表示します。
このメッセージボックスでOKが押されたら、StartNewGame メソッドを呼び出して新しいゲームを開始します。
フォームデザイナから、このメソッドをフォームのMouseClickイベントとして設定しておきます。
★ ★ ★
さてまた実行してみましょう。
・・・クリックすると、その円盤が消えていきますね!
★ ★ ★
消えていきはするんですが、実はこのままだと順番にクリックしなくても消せてしまいます。
そこで「順番通りでないと消えない」ように手を加えましょう。
「今、何番を消す番か?」を示すフィールド currentNo を追加します。
        int currentNo = 0; // 現在番号(=次に消去すべき円盤の番号)
そして、StartNewGameメソッドの最後に、これを1で初期化する処理を追加します。
        public void StartNewGame() // 新規ゲーム開始
        {
            list.Clear(); // リストクリア
            for (int i = 0; i < numberOfDisks; i++) // 決められた数だけ円盤を生成してリストに追加する
            {
                int x = random.Next(r, ClientSize.Width - r);
                int y = random.Next(r, ClientSize.Height - r);
                list.Add(new Disk(i + 1, x, y, r));
            }
            Refresh(); // 画面を再描画する
            currentNo = 1; // ★追加
        }
また、マウスクリック時の消去判定を、番が合っている時しか行わないようにします。
そして、円盤を1つ消去できた時は currentNo を +1 します。
        private void Form1_MouseClick(object sender, MouseEventArgs e) // フォームマウスクリック時処理
        {
            int erasedCount = 0; // 消去カウンタ初期化
            int totalErasedCount = 0; // トータル消去カウンタ初期化
            foreach (Disk d in list) // 円盤の数だけ繰り返す
            {
                if (d.N == currentNo) // ★追加
                { 
                    int xx = d.X - e.X;
                    int yy = d.Y - e.Y;
                    double r0 = Math.Sqrt((xx * xx) + (yy * yy));
                    if (r0 <= r) // クリック位置が円盤の中であれば
                    {
                        d.Erased = true; // 該当する円盤の消去フラグを立てる
                        erasedCount++; // 消去カウンタを1増やす
                        currentNo++; // 現在番号を1増やす // ★追加
                    }
                } // 閉じカッコ
                if (d.Erased) // 円盤が消去済みであれば
                {
                    totalErasedCount++; // トータル消去カウンタを1増やす
                }
            }
            ...
        }
実行してみましょう。
・・・順番通りにしか消せなくなりましたね!
ちょっとわかりにくいかもしれませんが、何度かわざと違うやつをクリックして消えないことを確認しています。
★ ★ ★
といったところで、今日はここまで。
せっかくなので、次回、もう少しゲームらしくしたいなと思います。
それでは!
追伸
長らく間が空いてしまいましたが、以前投稿した「飛び交うレーザー(後編)」の最後で述べていたスクリーンセーバー版、ようやく公開できました!
↑の最後にもその旨記載してありますが、こちらでもお伝えします。
Releaseでビルドすると、bin\Release 配下に RefrectionLaserScreenSaver.exe と共に RefrectionLaserScreenSaver.scr が生成されます。
RefrectionLaserScreenSaver.scr を右クリックすると、右クリックメニューに「インストール」があるので、それを選ぶとインストールされます。
と同時に、スクリーンセーバー設定画面が開きます。

アンインストールする時は、スクリーンセーバー設定画面で他のスクリーンセーバーを選びなおせば消えます。
ソースは以下です。どうぞご覧ください。
https://github.com/YasuhikoKiuchi/RefrectionLaserScreenSaver
タグ: C#, Windowsフォームアプリ, プログラミング, ミニゲーム, 趣味プログラミング