前回から万引少年 レトロなゲームをアスキーアートでつくるを始めましたが、今回はゲーム開始後の処理を実装します。
Contents
店内を初期化
ゲームが開始されたら店内の様子がフォームに表示されます。そのためにInitStoreメソッドを呼び出します。
このメソッドは店のなかの状態を初期化します。まず制限時間をフルの状態にセットします。それから商品をすべて置かれている状態にします。そして警備員と少年の位置にゲーム開始の初期座標をセットします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public partial class Form1 : Form { void InitStore() { // タイムをフルに _time = MaxTime; // すべての商品が置かれている状態に InitItems(); // 警備員と少年の位置を初期座標に PositionK = 0; LastPositionK = 0; PositionBoyX = 10; PositionBoyY = 12; Timer1.Interval = 500; } } |
商品を初期位置にセットする
InitItemsメソッドは商品を店の棚にセットします。前回ゲームオーバーになったときのデータが残っているので、最初にLeftItemsとRightItemsをクリアします。そして商品をあるべき位置にセットします。LeftItemsは少年の左に陳列されている商品の座標のリスト、RightItemsは少年の右側に陳列されているもののリストです。
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 |
public partial class Form1 : Form { // フィールド変数はこのように定義されている // List<Position> LeftItems = new List<Position>(); // List<Position> RightItems = new List<Position>(); void InitItems() { LeftItems.Clear(); RightItems.Clear(); for (int y=6; y <= 21; y++) LeftItems.Add(new Position(1, y)); for (int y = 6; y <= 11; y++) LeftItems.Add(new Position(9, y)); for (int y = 15; y <= 21; y++) LeftItems.Add(new Position(9, y)); for (int y = 6; y <= 11; y++) LeftItems.Add(new Position(17, y)); for (int y = 15; y <= 21; y++) LeftItems.Add(new Position(17, y)); for (int y = 6; y <= 11; y++) LeftItems.Add(new Position(25, y)); for (int y = 15; y <= 21; y++) LeftItems.Add(new Position(25, y)); for (int y = 6; y <= 11; y++) LeftItems.Add(new Position(33, y)); for (int y = 15; y <= 21; y++) LeftItems.Add(new Position(33, y)); for (int y = 6; y <= 11; y++) RightItems.Add(new Position(6, y)); for (int y = 15; y <= 21; y++) RightItems.Add(new Position(6, y)); for (int y = 6; y <= 11; y++) RightItems.Add(new Position(14, y)); for (int y = 15; y <= 21; y++) RightItems.Add(new Position(14, y)); for (int y = 6; y <= 11; y++) RightItems.Add(new Position(22, y)); for (int y = 15; y <= 21; y++) RightItems.Add(new Position(22, y)); for (int y = 6; y <= 11; y++) RightItems.Add(new Position(30, y)); for (int y = 15; y <= 21; y++) RightItems.Add(new Position(30, y)); for (int y = 6; y <= 21; y++) RightItems.Add(new Position(38, y)); } } |
DrawStoreField1メソッドで店内を描画
前回の記事の記事の最後の部分にあるようにSキーが押されることで、シーンがScene.Store1に切り替わります。するとこれ以降はTimer.Tickイベントが発生するたびにDrawStoreField1メソッドが呼び出されます。
警備員を移動させる
DrawStoreField1メソッドでやっていることは警備員の動きをランダムに決めて左右に移動させています。ただ本当にランダムにすると右へひとつ移動したかと思えば次は左に行ったりでいつまでたっても同じ位置から変わらないという問題がおきてしまいます。そこで乱数で左右どちらに移動するか、その方向に連続でいくつ移動するかの2つの要素を決めています。
警備員の動きはbool変数のリストとしてMoveRightに格納されます。リストから取り出した値がtrueであれば右へ移動して、falseであれば左に移動します。またリストのなかが空であれば乱数でリストに格納するbool変数を生成します。
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 |
public partial class Form1 : Form { // List<bool> MoveRight = new List<bool>(); void DrawStoreField1() { // リストのなかが空であれば乱数でリストに格納するbool変数を生成 if (MoveRight.Count == 0) { int r1 = _random.Next(2); int r2 = _random.Next(5) + 1; for (int i = 0; i < r2; i++) { bool isRight = r1 == 1 ? true : false; MoveRight.Add(isRight); if (!isRight && PositionK - i <= 0) break; if (isRight && PositionK + i >= ColumMax - 4) break; } } // リストのなかから値をひとつだけ取り出す bool moveRight = MoveRight[0]; MoveRight.RemoveAt(0); // 取り出した値によって警備員を左右に移動させるが、端の場合は移動できる方向に移動させる if (moveRight && (PositionK < ColumMax - 4)) KMoveRight(); else if (PositionK > 0) KMoveLeft(); else if (!(PositionK < ColumMax - 4)) KMoveLeft(); else KMoveRight(); // 少年のX座標がdangerXのどれかの場合、見つかる可能性があるし、 // 警備員のX座標がdangerXのどれかの場合、万引きを摘発できる可能性がある。 int[] dangerX = { 1, 2, 3, 4, 9, 10, 11, 12, 17, 18, 19, 20, 25, 26, 27, 28, 33, 34, 35, 36 }; bool danger = dangerX.Any(x => x == PositionBoyX); bool find = dangerX.Any(x => x == PositionK); // 両方の条件をみたしている場合で両者のX座標の差の絶対値が4未満のときは // 万引きが摘発されたことになる。この場合はScene.Store2(警備員が少年を捕まえる)に移行する if (danger && find && Math.Abs(PositionK - PositionBoyX) < 4) _scene = Scene.Store2; // 後述 店のなかを再描画する RedrawStoreField(); // Timer.Tickは0.5秒おきにくるので残り時間を0.5秒減らす // 残り時間とスコアを表示する文字列をセットする(後述) _time -= 0.5; SetNewScoreString(); } } |
警備員を右へ移動させる処理を示します。PositionKがColumMax – 4を超えている場合は右端まで移動しているということなので右には移動できません。
1 2 3 4 5 6 7 8 |
public partial class Form1 : Form { void KMoveRight() { if (PositionK < ColumMax - 4) PositionK++; } } |
これは警備員を左に移動させる処理です。
1 2 3 4 5 6 7 8 |
public partial class Form1 : Form { void KMoveLeft() { if (PositionK > 0) PositionK--; } } |
SetNewScoreStringメソッドはスコアと残り時間を上部に表示させるための文字列をフィールド変数 Scoreに格納します。レトロ感をだすために数字は全角を用いています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public partial class Form1 : Form { void SetNewScoreString() { string s = String.Format("SCORE {0:000000} 残り時間 {1:000}", _score, _time); s = s.Replace("0", "0"); s = s.Replace("1", "1"); s = s.Replace("2", "2"); s = s.Replace("3", "3"); s = s.Replace("4", "4"); s = s.Replace("5", "5"); s = s.Replace("6", "6"); s = s.Replace("7", "7"); s = s.Replace("8", "8"); Score = s.Replace("9", "9"); } } |
少年を移動させる
キー操作がおこなわれたら少年を移動させます。
前回の記事のなかでオーバーライドされたOnKeyDownメソッドのなかではOnKeyDownBoyMoveメソッドが呼び出され、少年を移動させる処理をおこなっています。
少年もフィールドの端から外や通路以外のところに移動されては困るので移動できるかどうか確認してからPositionBoyXとPositionBoyYを変更しています。そして移動処理をおこなったらRedrawStoreFieldメソッドを呼び出してフォームを再描画しています。
CanBoyMoveメソッドは引数として渡された場所に移動できるかどうかを調べるものです。移動できない場合はそこには棚があり、商品があるかもしれません。SteelRightItemIfCanメソッドは少年の右側に商品があるか、SteelLeftItemIfCanメソッドは左側にある商品を盗むことができるかを調べて、盗むことができるなら盗む処理をおこないます。
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 |
public partial class Form1 : Form { bool OnKeyDownBoyMove(KeyEventArgs e) { if (_scene == Scene.Store1) { if (e.KeyCode == Keys.Right) { if (CanBoyMove(PositionBoyX + 1, PositionBoyY)) PositionBoyX++; else SteelRightItemIfCan(); } if (e.KeyCode == Keys.Left) { if (CanBoyMove(PositionBoyX - 1, PositionBoyY)) PositionBoyX--; else SteelLeftItemIfCan(); } if (e.KeyCode == Keys.Up) { if (CanBoyMove(PositionBoyX, PositionBoyY - 1)) PositionBoyY--; } if (e.KeyCode == Keys.Down) { if (CanBoyMove(PositionBoyX, PositionBoyY + 1)) PositionBoyY++; } RedrawStoreField(); return true; } return false; } } |
CanBoyMoveメソッドを示します。移動先が通路であれば移動可能なのでtrueを返します。移動不可であればfalseを返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public partial class Form1 : Form { bool CanBoyMove(int newPositionBoyX, int newPositionBoyY) { bool b1 = newPositionBoyX == 2 || newPositionBoyX == 3 || newPositionBoyX == 10 || newPositionBoyX == 11 || newPositionBoyX == 18 || newPositionBoyX == 19 || newPositionBoyX == 26 || newPositionBoyX == 27 || newPositionBoyX == 34 || newPositionBoyX == 35; if (b1) { if (newPositionBoyY >= 5 && newPositionBoyY <= 20) return true; else return false; } if (newPositionBoyY == 12) { if (newPositionBoyX >= 2 && newPositionBoyX <= 35) return true; else return false; } return false; } } |
盗む動作の実装
SteelLeftItemIfCanメソッドにおける処理を示します。少年が移動しようとした場所に商品があればそれを盗むことができます。ただし棚には商品はそれぞれ1つずつしかおかれていません。同じ商品を何度も盗むことはできないのでLeftItemsに格納されている座標のなかから少年が移動しようとした座標と一致するものがあるのかを調べています。
そして盗むときの手の動きをそれっぽくするために成功したら手を商品棚の方向に移動させます。そのためのフラグがStealLeftSucceededです。一定時間(0.05秒)がきたらもとの状態に戻します。手を棚に伸ばすときも手をもとの状態に戻すときもすぐにRedrawStoreFieldメソッド(後述)を呼び出して再描画処理をおこないます。
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 |
public partial class Form1 : Form { void SteelLeftItemIfCan() { Position position = LeftItems.FirstOrDefault(item => item.X == PositionBoyX - 1 && item.Y == PositionBoyY + 1); if (position != null) { StealLeftSucceeded = true; // 手を商品がある方向に伸ばすフラグをセット LeftItems.Remove(position); // 盗んだ商品をリストから取り除く StealSucceeded(); // 盗んだあとの処理 // 0.05秒後に手を元に戻す Timer timer = new Timer(); timer.Interval = 50; timer.Tick += Timer_Tick; timer.Start(); // 0.05秒後に呼び出されるメソッド // フラグのクリアと再描画の処理、タイマーの破棄 void Timer_Tick(object sender, EventArgs e) { StealLeftSucceeded = false; RedrawStoreField(); Timer t = (Timer)sender; t.Stop(); t.Dispose(); } } } } |
商品を盗むことに成功したときにおこなわれる処理を示します。ここでは再描画の処理、効果音を鳴らす、スコアを増やす、ゲームクリア判定をおこなっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public partial class Form1 : Form { void StealSucceeded() { RedrawStoreField(); Sound1(); _score += 10; SetNewScoreString(); if(IsGameClear()) _scene = Scene.Clear; } } |
効果音を鳴らす処理を示します。WMPLib.WindowsMediaPlayerが使えるようにするには[参照の追加]でC:\windows\system32\wmp.dllを追加しておく必要があります。
1 2 3 4 5 6 7 8 9 10 |
public partial class Form1 : Form { WMPLib.WindowsMediaPlayer mediaPlayer = new WMPLib.WindowsMediaPlayer(); void Sound1() { mediaPlayer.settings.autoStart = true; mediaPlayer.URL = Application.StartupPath + "\\get.mp3"; } } |
右側の商品を盗む処理も同様に実装します。
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 |
public partial class Form1 : Form { void SteelRightItemIfCan() { // 少年のX座標と商品の座標の関係から、左にある商品を盗むときは調べる座標が違うので注意! Position position = RightItems.FirstOrDefault(item => item.X == PositionBoyX + 3 && item.Y == PositionBoyY + 1); if (position != null) { StealRightSucceeded = true; RightItems.Remove(position); StealSucceeded(); Timer timer = new Timer(); timer.Interval = 100; timer.Tick += Timer_Tick; timer.Start(); void Timer_Tick(object sender, EventArgs e) { StealRightSucceeded = false; RedrawStoreField(); Timer t = (Timer)sender; t.Stop(); t.Dispose(); } } } } |
描画に必要な文字列を取得する
RedrawStoreFieldメソッドはシーンがScene.Store1のときフォームに文字列を描画する処理をおこなうためのものです。GetStringDrawBoyメソッド、GetStringItemsメソッド、GetStringDrawKメソッドはこの次に解説します。
またStoreFieldは全角文字だけでできているのですが、足の部分は半角文字を使うので、文字列が取得されたあと「足」を半角文字2つに置換しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public partial class Form1 : Form { void RedrawStoreField() { if (StealLeftSucceeded) FieldString = GetStringDrawBoy(GetStringItems(GetStringDrawK()), true, false); else if (StealRightSucceeded) FieldString = GetStringDrawBoy(GetStringItems(GetStringDrawK()), false, true); else FieldString = GetStringDrawBoy(GetStringItems(GetStringDrawK()), false, false); FieldString = FieldString.Replace("足", "||▊"); FieldString = FieldString.Replace("▊ ", "▊ "); Invalidate(); } } |
警備員や商品、少年を描画するためにはStoreFieldの文字列を一部置き換える必要があります。最初にGetStringDrawKメソッドを実行します。
警備員はこんなアスキーアートになっています。
そこでStoreFieldのなかの文字列を以下のようにして置き換えます。警備員には左向きと右向きがあります。右向きの警備員の姿は「K」、左向きだと「K」を逆にしたような形をしていて、地味に笑えます。
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 |
public partial class Form1 : Form { string GetStringDrawK() { char[] vs = StoreField.ToCharArray(); int index1 = ColumMax * 2; int index2 = ColumMax * 3; int index3 = ColumMax * 4; if (LastPositionK - PositionK > 0) { vs[index1 + PositionK] = '\'; vs[index1 + 1 + PositionK] = '●'; vs[index1 + 2 + PositionK] = '〉'; vs[index2 + 1 + PositionK] = '■'; vs[index3 + PositionK] = '/'; vs[index3 + 1 + PositionK] = '〈'; } else { vs[index1 + PositionK] = '〈'; vs[index1 + 1 + PositionK] = '●'; vs[index1 + 2 + PositionK] = '/'; vs[index2 + 1 + PositionK] = '■'; vs[index3 + 1 + PositionK] = '〉'; vs[index3 + 2 + PositionK] = '\'; } LastPositionK = PositionK; return new string(vs); } } |
GetStringItemsメソッドはGetStringDrawKメソッドが返した文字列を引数にして商品を表示するためのものです。商品は$で表わされ、すでになくなっている部分には表示されません。その場合は■が表示されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public partial class Form1 : Form { string GetStringItems(string source) { char[] vs = source.ToCharArray(); foreach (Position position in LeftItems) { int index = position.Y * ColumMax + position.X; vs[index] = '$'; } foreach (Position position in RightItems) { int index = position.Y * ColumMax + position.X; vs[index] = '$'; } return new string(vs); } } |
GetStringDrawBoyメソッドはGetStringItemsメソッドが返した文字列を引数に少年も表示する文字列を返す処理をおこないます。足の部分は半角文字を使うのでここでは仮の文字「足」をいれています。leftとrightがtrueの場合は商品に手を伸ばしているような文字列を返します。
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 |
public partial class Form1 : Form { string GetStringDrawBoy(string source, bool left, bool right) { char[] vs = source.ToCharArray(); int index1 = (PositionBoyY) * ColumMax; int index2 = (PositionBoyY + 1) * ColumMax; int index3 = (PositionBoyY + 2) * ColumMax; vs[index1 + 1 + PositionBoyX] = '○'; if(left) vs[index2 + PositionBoyX] = '/'; else vs[index2 + PositionBoyX] = '('; vs[index2 + 1 + PositionBoyX] = '■'; if(right) vs[index2 + 2 + PositionBoyX] = '\'; else vs[index2 + 2 + PositionBoyX] = ')'; vs[index3 + 1 + PositionBoyX] = '足'; string str = new string(vs); return str; } } |
ステージクリアの処理
めでたく全部の商品を盗むことができたらステージクリアです。ステージクリアになるのかどうかはStealSucceededメソッドのなかでおこなっています。LeftItemsもRightItemsも空であればステージクリアです。そのときはシーンをScene.Clearに変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public partial class Form1 : Form { void StealSucceeded() { // 既出のメソッドなのでステージクリア判定の部分以外は省略 if(IsGameClear()) _scene = Scene.Clear; } bool IsGameClear() { return LeftItems.Count == 0 && RightItems.Count == 0; } } |
シーンがScene.Clearに変更されるとTimer.Tickイベントが発生するとDrawGameClearメソッドが呼び出されるようになります。DrawGameClearメソッドのなかではGetGameClearStringメソッドが呼び出されます。タイマーのIntervalが変更され、ClearFieldの文字列が描画されます。
ClearFieldの文字列は両手をあげてバンザイをしているのですが、手を挙げている状態と下ろしている状態をいっしょにしています。何回GetGameClearStringメソッドが呼び出されたかで処理を切り分け、速い速度で何度もバンザイを繰り替えているように見せかけています。
バンザイを何度か繰り返したら次のステージにすすみます。シーンをScene.Store1に切り替え、InitStoreメソッドで店内を初期化して悪友のために新たな万引きをしてもらいます。警備員に見つかって警察に連行されるまで・・・
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 |
public partial class Form1 : Form { void DrawGameClear() { FieldString = GetGameClearString(); Invalidate(); } string GetGameClearString() { GameClearUpdateCount++; Timer1.Interval = 150; char[] vs = ClearField.ToCharArray(); if (GameClearUpdateCount % 2 == 0) { // 挙げている手を消している int row = 14; vs[ColumMax * row + 6] = ' '; vs[ColumMax * row + 8] = ' '; vs[ColumMax * row + 9] = ' '; } else { // 下ろしている手を消している int row = 15; vs[ColumMax * row + 7] = ' '; vs[ColumMax * row + 9] = ' '; vs[ColumMax * row + 10] = ' '; } // 次のステージへ if (GameClearUpdateCount > 16) { GameClearUpdateCount = 0; _scene = Scene.Store1; InitStore(); } return new string(vs); } } |
さて不幸にして万引きがバレてしまった場合の処理ですが、これは次回に続きます。