今回は昔がなつかしくなるレトロなゲーム・万引少年をアスキーアートでつくります。こんな感じのゲームになりますが、デバッグ情報がタイトルバーに出力されちゃってますが、ご愛敬ということで・・・。
Contents
万引少年はどんなゲーム?
万引少年は、1979年に日本で発表されたゲームソフトです。「万引少年A」を操作し、深夜営業のスーパー・ストアで閉店時間までにすべての商品を万引きします。店内では文部省(当時存在したのは文部科学省ではなく「文部省」。時代を感じます)から派遣された監視員Kが見張っており、彼に万引するところを見つかったり、閉店時間を過ぎると、監視員に捕まって警察へと連行される。
これは実際にプレイしている様子です。コンピュータはMZ-80、当時はパソコンではなくマイコンと呼ばれていいました。
アスキーアートでゲームをつくる
このゲームの面白さはアスキーアートでつくられた警備員Kの動き。どう見ても阿波踊りを踊ってるようにしか見えないです。「文部省から派遣された監視員K(本名:菊山)」というだけあって身体自体が「K」に見えます。こういうゲームはアスキーアートで作るに限ります。
万引きの舞台はオリジナルではセブンイレブインという名称ですが、ここではコンビニの名称を一部伏せ字にしました。セブンイレブインさん、ごめんね。それから万引きは窃盗罪であり、最長10年の懲役または50万円以下の罰金になります。絶対にやらないように。ダメ、絶対!
シーンをつくる
このゲームは大きくわけて複数のシーンで構成されています。最初の「アイテテヨカッタ」、実際に店内で万引きをするシーン、警備員にみつかって捕まってしまうシーン、すべての商品を万引きしてステージクリアのあとに表示される「アシタモタノムゾ」のシーン、そして警備員につかまった少年が警察署の近くまで連行されて警備員によって締め上げられるシーンの5つがります。
1 2 3 4 5 6 7 8 |
enum Scene { Opening, // 万引き開始するまえの各ステージのオープニングシーン Store1, // 店内のシーン:店内で警備員の目を盗んで万引きをする Store2, // 店内のシーン:店内で警備員にみつかって警備員が少年の近くに移動し捕まえる Clear, // ステージクリアシーン:すべての商品を万引きして悪友に「明日も頼むぜ」 Police, // ゲームオーバーシーン:警備員に警察署まで連行されとっちめられる } |
Positionクラスは商品が置かれている座標を管理するためのものです。
1 2 3 4 5 6 7 8 9 10 11 |
public class Position { public int X; public int Y; public Position(int x, int y) { X = x; Y = y; } } |
ゲームをつくる
ではゲームをつくってみましょう。
フィールド変数
実際にForm1クラスで使うフィールド変数を示します。
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 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
public partial class Form1 : Form { // タイマー Timer Timer1 = new Timer(); Random _random = new Random(); // 文字を描画するときのフォント Font FieldFont = new Font("MS ゴシック", 12); // フォームに描画する文字列 String FieldString = ""; // ゲームのシーン Scene _scene = Scene.Store1; // Opening オープニング // Store1 万引き実行 // Store2 万引きが摘発され捕まるまで // Police ゲームオーバー画面、警察署に連行される // Clear ステージクリア // Scene1がOpeningのとき、フォームに描画する文字列 const string StartField = "" + " \n" + " \n" + " \n" + " ■■■■■■■■■■■■■■■■■■■■ \n" + " ■ セ ○ ン イ レ ブ ン ■■■ \n" + " ■■■■■■■■■■■■■■■■■■■■ \n" + " ■ ┏━━┓ ┏━━┓ ┏━━┓ ■■■ \n" + " ■ ┃ ┃ ┃ ┃ ┃ ┃ ■■■ \n" + " ■ ┃ ┃ ┃ ┃ ┃ ┃ ■■■ \n" + " ■ ┃ ┃ ┃ ┃ ┃ ┃ ■■■ \n" + " ■ ┗━━┛ ┗━━┛ ┗━━┛ ■■■ \n" + " ■ ┏━━┓ ┏━━┓ ┏━━┓ ■■■ \n" + " アイテテ ヨカッタ!! ■ ┃ ┃ ┃ ┃ ┃ ┃ ■■■ \n" + " ■ ┃ ┃ ┃ ┃ ┃ ┃ ■■■ \n" + " ○ ○ ■ ┃ ┃ ┃ ┃ ┃ ┃ ■■■ \n" + " (■\ /■? ■ ┃ ┃ ┃ ┃ ┃ ┃ ■■■ \n" + " ? ? ? ? ■ ┗━━┛ ┗━━┛ ┗━━┛ ■■■ \n" + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ \n" + " \n" + " PUSH S KEY GAME START! \n" + " \n"; // Scene1がStore1のとき、フォームに描画する文字列の原形になる文字列 const string StoreField = "" + "┳┳━━━━┳━━┳━━━━┳━━┳━━━━┳━━┳━━━━┳━━┳━━━━┳┳\n" + "┗┛ ┗━━┛ ┗━━┛ ┗━━┛ ┗━━┛ ┗┛\n" + " \n" + " \n" + " \n" + "━┓ ┏━\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫\n" + "┣■ ■┫\n" + "┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + " \n"; int ColumMax = 41; // StoreFieldの一行の幅(改行含む) // Scene1がStore1のとき、少年の現在座標 int PositionBoyX = 10; int PositionBoyY = 12; // 少年が商品を盗むときの手の動きに関するフラグ bool StealLeftSucceeded = false; bool StealRightSucceeded = false; // Scene1がStore1のとき、警備員が移動する方向 List<bool> MoveRight = new List<bool>(); // 少年を捕まえる前の警備員のX座標(捕まえる前は横方向にしか動かない) int PositionK = 0; int LastPositionK = 0; // スコア int _score = 0; string Score = "SCORE 000000 残り時間 000"; // 残り時間 double _time = 300; // 残り時間の最大値 double MaxTime = 300; // 万引きすべき商品のリスト List<Position> LeftItems = new List<Position>(); List<Position> RightItems = new List<Position>(); // 捕まったときのシーン // Scene1がStore2のとき、警備員が移動するY方向(初期値は2) int PositionKY = 2; int KKoraUpdateCount = 0; // クリアのシーン const string ClearField = "" + " \n" + " \n" + " \n" + " ■■■■■■■■■■■■■■■■■■■■ \n" + " ■ セ ○ ン イ レ ブ ン ■■■ \n" + " ■■■■■■■■■■■■■■■■■■■■ \n" + " ■ ┏━━┓ ┏━━┓ ┏━━┓ ■■■ \n" + " ■ ┃ ┃ ┃ ┃ ┃ ┃ ■■■ \n" + " ■ ┃ ┃ ┃ ┃ ┃ ┃ ■■■ \n" + " ダイセイコウダ!! ■ ┃ ┃ ┃ ┃ ┃ ┃ ■■■ \n" + " ■ ┗━━┛ ┗━━┛ ┗━━┛ ■■■ \n" + " アジタモタノムゾ!! ■ ┏━━┓ ┏━━┓ ┏━━┓ ■■■ \n" + " ■ ┃ ┃ ┃ ┃ ┃ ┃ ■■■ \n" + " ■ ┃ ┃ ┃ ┃ ┃ ┃ ■■■ \n" + " \○/\○ ■ ┃ ┃ ┃ ┃ ┃ ┃ ■■■ \n" + " (■\/■? ■ ┃ ┃ ┃ ┃ ┃ ┃ ■■■ \n" + " ? ? ? ? ■ ┗━━┛ ┗━━┛ ┗━━┛ ■■■ \n" + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ \n" + " \n" + " \n" + " \n"; int GameClearUpdateCount = 0; // ゲームオーバーのシーン const string PoliceField = "" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " ┏━━━━━━━━━━┓ \n" + " ┃ POLICE ┃ \n" + " ■■■■■■■■■■━┛ \n" + " ■ ■ \n" + " ■ ■ \n" + " ■■■■■■■■■■━━━━━━━━━━━━━━━━━━━━━━━━\n" + " GAME OVER \n" + " YOUR SCORE 000000 \n" + " \n" + " PUSH S KEY GAME START! \n" + " \n" + " \n" + " \n"; int GameOverUpdateCount = 0; } |
文字によって幅に違いがあるとズレてしまうのでフォントは「MS ゴシック」を使用しています。ただ掲載したコードは別のフォントなのでズレが生じています。また記事を管理しているエディタの都合で機種依存文字が?に化けてしまっています。そこで画像としてキャプチャしたものも掲載します。
コンストラクタの処理
まずアプリケーションが開始されたときの処理を示します。Form1クラスのコンストラクタは以下のようになっています。
タイマーを使って各キャラクタを移動させるのでタイマーの初期化をして、Timer.Tickイベントを処理できるようにしています。そして最初は店に入るまえのシーンを表示させたいので、フィールド変数 _sceneにはScene.Openingを代入しています。あとはゲームらしく背景を黒にすることとちらつきをなくすためにダブルバッファーの設定をしています。
それ以外の初期化の設定はInitGameメソッドでおこなっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public partial class Form1 : Form { public Form1() { InitializeComponent(); Timer1.Interval = 500; Timer1.Tick += Timer1_Tick; Timer1.Start(); this.DoubleBuffered = true; this.BackColor = Color.Black; InitGame(); _scene = Scene.Opening; } } |
ゲームの初期化
InitGameメソッドにおける処理を示します。GameOverUpdateCountとKKoraUpdateCountはゲームオーバー処理のときだけ値がインクリメントされます。前回ゲームオーバーになったときに初期値とは違う値に変わっているので再度ゲームを開始するときは0にリセットします。
それから前回のゲームのスコアも0にリセットして、シーンをScene.Openingに変更します。
1 2 3 4 5 6 7 8 9 10 11 12 |
public partial class Form1 : Form { void InitGame() { GameOverUpdateCount = 0; KKoraUpdateCount = 0; PositionKY = 2; _score = 0; _scene = Scene.Opening; } } |
Timer.Tickイベントの処理
再描画の処理は基本的にタイマーをつかっておこなわます(キー操作によって再描画の処理がおこなわれることもある)。そこでTimer.Tickイベントが発生したときの処理を示します。シーンによってDrawStart、DrawStoreField1、DrawStoreField2、DrawPolice、DrawGameClearの各メソッドを呼び出して処理をしています。
(DrawStartメソッドの定義はすぐ下にあります。DrawStoreField1、DrawStoreField2、DrawPolice、DrawGameClearの各メソッドは次回の記事を参照してください)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public partial class Form1 : Form { private void Timer1_Tick(object sender, EventArgs e) { if (_scene == Scene.Opening) DrawStart(); else if (_scene == Scene.Store1) DrawStoreField1(); else if (_scene == Scene.Store2) DrawStoreField2(); else if (_scene == Scene.Police) DrawPolice(); else if (_scene == Scene.Clear) DrawGameClear(); } } |
文字をフォームに描画する
最初はDrawStartメソッドによって再描画がおこなわれます。ここではフィールド変数のStartFieldに格納されている文字列が表示されるだけです。StartFieldに格納されている文字列をフィールド変数FieldStringに格納してInvalidateメソッドを呼び出せばオーバーライドされたOnPaintメソッドによって描画処理がおこなわれます。
1 2 3 4 5 6 7 8 |
public partial class Form1 : Form { void DrawStart() { FieldString = StartField; Invalidate(); } } |
オーバーライドされたOnPaintメソッドを示します。一番上にはスコアや制限時間も表示させます。それ以外のシーンで制限時間を表示させてもあまり意味ないので表示はさせません。
1 2 3 4 5 6 7 8 9 10 11 |
public partial class Form1 : Form { protected override void OnPaint(PaintEventArgs e) { if(_scene == Scene.Store1 || _scene == Scene.Store2) e.Graphics.DrawString(Score, FieldFont, Brushes.Aqua, new Point(20, 10)); e.Graphics.DrawString(FieldString, FieldFont, Brushes.Aqua, new Point(20, 30)); base.OnPaint(e); } } |
オープニングの処理
オープニング画面からゲーム開始までの処理を示します。
Sキーを押したらゲーム開始
画面に「アイテテヨカッタ」と表示されているときに書かれているようにSキーを押したら店内に侵入して万引きミッションが始まるようにします。そこでOnKeyDownメソッドもオーバーライドします。
少年を移動させるときもキーを押すのですが、押しっぱなしで連続移動できてしまうと高速移動ができてしまうのでnoMoveフラグをつかってキーを移動したい距離だけ押さなければならないようにしています。キーを押したらnoMoveフラグによって移動処理ができないようにします。キーが離されたらnoMoveフラグをクリアします。
シーンがScene.OpeningのときにSキーを押せばInitStoreメソッドが実行され、シーンがScene.Store1に変更され、万引きミッションに移行します。
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 |
public partial class Form1 : Form { bool noMove = false; protected override void OnKeyDown(KeyEventArgs e) { if (noMove) return; noMove = true; bool done; // オープニングからのゲームスタート done = OnKeyDownGameStart(e); // OnKeyDownGameStartで処理がおこなわれた場合はなにもしない if(!done) done = OnKeyDownBoyMove(e); // 店内での少年の移動 if (!done) OnKeyDownGameRetry(e); // ゲームオーバー後の再挑戦 base.OnKeyDown(e); } bool OnKeyDownGameStart(KeyEventArgs e) { if (e.KeyCode == Keys.S) { if (_scene == Scene.Opening) { InitStore(); _scene = Scene.Store1; return true; } } return false; } protected override void OnKeyUp(KeyEventArgs e) { noMove = false; } } |
DrawClear関数(今はメソッドというのかな?)が無いようなんですが。
DrawClearではなくDrawGameClearの書き間違いでした。ゲーム開始後の処理 万引少年 レトロなゲームをアスキーアートでつくる(2)で定義しています。