こんにちは、あんみんどうふです。
2年ぐらい前に流行った「2048」というパズルゲームをC#.NETで作ってみたので紹介します。
前回のヨットはもう少しで完成するので近いうちに・・・。
2048とは
(画像元: https://github.com/gabrielecirulli/2048)
4×4マスの中で同じ数字同士を足していき、2048が完成したらゲームクリア。
上、下、左、右の4方向に移動可能で、移動した方向へすべての数字が移動します。移動する量は1マスだけでなく、端まで移動します。
移動した際に同じ数字がぶつかることで足されます。例えば、4と4がぶつかると8になります。
移動する度に空いてる場所に2か4の数字が生成されます。これをどんどん足していき、「2048」を目指していくゲームです。
開発環境
いつも通りです。
- Visual Studio 2019
- C#.NET (.NET Core 3.1)
ゲーム画面
スコアと盤面だけでシンプルな感じにしています。方向キーで操作。
進めていく度に数字が大きくなり、足すのが難しくなっていきます。
盤面もかなり荒れてきました・・・。
盤面が全て埋まり、かつ隣接した同じ数字が無くなったら移動できないのでゲームオーバーです。2048まで増やすのはかなり根気が要ります・・・。
中身
全部だと長いので主要部分だけコード公開します。
静的フィールド(グローバル変数)
全メソッドで使う変数です。現在スコア、フィールド、1辺のマス数、ステータス。
今更ですけどメソッドの外の領域を「静的フィールド」っていうんですね、初めて知りました。
1 2 3 4 | int score = 0; // スコア int[,] field; // フィールド int EDGE = 4; // フィールドの1辺マス数 int status = 0; // ゲームのステータス(0=通常, 1=ゲームクリア, 2=ゲームオーバー) |
描画処理
各数字の色は1つ1つ定義しています。違和感なくグラデーション状に取得出来たらスマートだったのですが、技術が足りず。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | /// <summary> /// フィールドを描画する。 /// </summary> void Drawing() { int square = fieldView.Width / EDGE; // PictureBox Bitmap canvas = new Bitmap(fieldView.Width, fieldView.Height); Graphics g = Graphics.FromImage(canvas); for (int i = 0; i < EDGE; i++) { for (int j = 0; j < EDGE; j++) { Color color; switch (field[i, j]) { case 0: color = Color.FromArgb(205, 192, 180); break; case 2: color = Color.FromArgb(238, 228, 218); break; case 4: color = Color.FromArgb(237, 224, 200); break; case 8: color = Color.FromArgb(242, 177, 121); break; case 16: color = Color.FromArgb(245, 149, 99); break; case 32: color = Color.FromArgb(247, 123, 94); break; case 64: color = Color.FromArgb(246, 94, 59); break; case 128: color = Color.FromArgb(237, 207, 115); break; case 256: color = Color.FromArgb(237, 204, 98); break; case 512: color = Color.FromArgb(237, 200, 80); break; case 1024: color = Color.FromArgb(237, 197, 63); break; case 2048: color = Color.FromArgb(237, 194, 45); break; default: color = Color.FromArgb(255, 134, 1); break; } SolidBrush brush = new SolidBrush(color); g.FillRectangle(brush, j * square, i * square, square, square); brush.Dispose(); if (field[i, j] > 0) { Font font = new Font("Consolas", 20, FontStyle.Bold); StringFormat sf = new StringFormat(); sf.Alignment = StringAlignment.Center; sf.LineAlignment = StringAlignment.Center; Brush b = Brushes.DimGray; g.DrawString(field[i, j].ToString(), font, b, j * square + square / 2, i * square + square / 2, sf); } } } // 線 for (int i = 1; i < EDGE; i++) { g.DrawLine(Pens.LightGray, square * i, 0, square * i, fieldView.Height); g.DrawLine(Pens.LightGray, 0, square * i, fieldView.Height, square * i); } g.Dispose(); fieldView.Image = canvas; } |
移動処理
「入力した方向に空間があれば移動、なければそのまま」といった処理を列(行)数分ループすることで、端から端まで移動しています。
移動が終わると移動した方向と逆方向の端にランダムで2か4の数字が生成されます。訳あって原作と仕様が異なるポイントです。
(原作では方向関係なく空いてるマスのうちのランダム)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | /// <summary> /// 盤面を入力したキーの方向に移動する。 /// </summary> /// <param name="key">キー</param> void Moving(KeyEventArgs key) { // 新規に生成する数字の座標 Random randY = new Random(); Random randX = new Random(); int rY, rX; // 新規に生成する数字の決定(3/4 => "2", 1/4 => "4") Random randNum = new Random(); int rNum = randNum.Next(0, 4); int num = rNum == 0 ? 4 : 2; // 移動できたなら1つ数字を追加するフラグ bool add = false; switch (key.KeyData) { case Keys.Up: // 移動 for (int loop = 0; loop < EDGE; loop++) { for (int i = 1; i < EDGE; i++) { for (int j = 0; j < EDGE; j++) { if (field[i - 1, j] == 0) { field[i - 1, j] = field[i, j]; field[i, j] = 0; add = true; } } } } // 加算 for(int i = 1; i < EDGE; i++) { for(int j = 0; j < EDGE; j++) { if (field[i, j] == field[i - 1, j]) { // 移動元に加算してから、移動先に上書きする形で移動する field[i, j] += field[i - 1, j]; Scoring(field[i - 1, j]); // スコア加算 for(int loop = i; loop < EDGE; loop++) { field[loop - 1, j] = field[loop, j]; field[loop, j] = 0; } add = true; } } } // 追加 if (add) { // 移動か加算をした場合、1つ追加する do rX = randX.Next(0, EDGE); while (field[3, rX] != 0); field[3, rX] = num; } break; // ※※※ 長いので上キー以降省略 ※※※ } Drawing(); GameCheck(); } |
また、1つの行(列)に同じ数字が4つ並んでいる時、1回の移動で複数回加算されないようにちょっと工夫しています。(38~56行目)
一度の移動で加算される回数は各数字1回のみです。
例えば、2–2–2–2の順に並んでいる状態で1回右へ移動すると0-0-4–4になるようにしています。(0-0-0-8では2回加算されているので×)
ゲームチェック
全マスチェックしてどこかのマスに2048が存在していればクリア、動けないようであればゲームオーバー。
(クリア後も高スコアを目指して継続することができます。通常時と同様、動けなくなるとゲームオーバーになります。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | /// <summary> /// ゲームの状態をチェックする。 /// 2048が完成していればクリア、全マス埋まってしまうとゲームオーバー。 /// </summary> void GameCheck() { int cantMoveCount = 0; // 移動できないマス数 for (int i = 0; i < EDGE; i++) { for(int j = 0; j < EDGE; j++) { // クリアチェック if (field[i, j] == 2048 && status == 0) status = 1; // 上右下左の順にチェック bool[] checkNext = new bool[4]; if (i - 1 < 0 || field[i - 1, j] != 0 && field[i - 1, j] != field[i, j]) checkNext[0] = true; if (j + 1 >= EDGE || field[i, j + 1] != 0 && field[i, j + 1] != field[i, j]) checkNext[1] = true; if (i + 1 >= EDGE || field[i + 1, j] != 0 && field[i + 1, j] != field[i, j]) checkNext[2] = true; if (j - 1 < 0 || field[i, j - 1] != 0 && field[i, j - 1] != field[i, j]) checkNext[3] = true; // どの方向にも移動できないマスを数える if (checkNext[0] && checkNext[1] && checkNext[2] && checkNext[3]) cantMoveCount++; } } // 移動できないマス数が辺×辺(16マス)ならゲームオーバー if (cantMoveCount == EDGE * EDGE) status = 2; // ゲームクリア if (status == 1) { labelGameStatus.Text = "CLEAR!!"; labelGameStatus.Visible = true; } // ゲームオーバー else if (status == 2) { labelGameStatus.Text = "GAMEOVER"; labelGameStatus.Visible = true; } } |
以上です。全部で500行ぐらいのコードで収まりました。
今回の開発でこのゲーム初めてプレイしたんですけど、めちゃめちゃハマってしまいました。
テトリスしかり、どうもこういう単純なパズルゲームには沼にはまってしまうようで・・・。
また何か作ったら紹介します。それでは。