今回は自機をつくるとともにゲームとして完成させます。
Contents
自機をつくる
自機は円が繋がってできた蛇のような形をしていますが、敵を倒すたびに身体の長さがひとつずつ伸びていきます。そして自身の先頭だけでなく後ろに繋がっている「節」もいっしょに移動させなければなりません。
では自身の後ろに繋がっている節の座標をどのようにして移動させ描画するかですが、自身が移動した座標をリストにして保存していくことにします。このなかを探せば節の座標を求めることができます。
ではJikiクラスのコンストラクタを示します。コンストラクタでは引数の座標をCenterプロパティに格納しています。
自機の初期化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Jiki { // 自機の移動速度は3(敵の3倍)とする。 public int Speed = 3; public Jiki(Point center) { Center = center; } public Point Center { get; private set; } } |
自機の移動
自機の移動関連のメソッドを示します。
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 |
public class Jiki { // X座標とY座標を引数で指定しただけ増減させる public void Move(int dx, int dy) { int x = Center.X; int y = Center.Y; x += dx; y += dy; Center = new Point(x, y); } // X座標を引数で指定した値に変更する public void SetPosX(int posX) { int x = Center.X; int y = Center.Y; x = posX; Center = new Point(x, y); } // Y座標を引数で指定した値に変更する public void SetPosY(int posY) { int x = Center.X; int y = Center.Y; y = posY; Center = new Point(x, y); } } |
自機の描画
描画関連のメソッドを示します。描画するときと当たり判定をするときは先頭部分の中心座標だけでなく、後ろに繋がっている節の座標も取得しなければなりません。
そのときに必要なのが自機の軌跡です。これをフィールド変数 Trajectoryに保存します。ゲーム開始時の自機の位置と長さは決まっているのでInitメソッドで初期化します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Jiki { public int Length = 3; List<Point> Trajectory = new List<Point>(); Field Field = new Field(); public void Init() { // 初期の長さは3 Length = 3; // 初期の座標 Center = new Point(Field.BranchX[3], Field.BranchY[2]); // 初期の長さが3なのでこれを表示するために必要な座標をTrajectoryに格納しておく Trajectory.Clear(); for (int i = 0; i < 100; i++) Trajectory.Add(new Point(Center.X, Center.Y + Speed * i)); } } |
Trajectoryに自機の現在座標を追加する処理を示します。
前回格納した座標(この処理では先頭の要素)と同じ場合(=止まっている状態)はなにもしません。それ以外のときは現在座標をリストの先頭に追加します。
1 2 3 4 5 6 7 8 |
public class Jiki { public void AddTrajectory() { if (Trajectory.Count == 0 || Trajectory[0] != Center) Trajectory.Insert(0, Center); } } |
自機の節の中心座標のリストを取得する処理を示します。自機の速度はSpeedであり、TrajectoryにはX座標またはY座標がSpeedずつズレた値が格納されています。それでTrajectoryの要素を(節の直径 / Speed)で刻んでいけば各節の座標を取得することができます。
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 |
public class Jiki { public int Radius = 15; public List<Point> GetSubs() { List<Point> ret = new List<Point>(); int trajectoryCount = Trajectory.Count; int i = 0; while (true) { i++; if (Length <= i) break; int index = Radius * 2 / Speed; if (trajectoryCount > index * i + 1) ret.Add(new Point(Trajectory[index * i].X, Trajectory[index * i].Y)); else break; } return ret; } } |
自機を描画する処理を示します。先頭部分の中心座標と各節の中心座標を求めて円を描画します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class Jiki { public void Draw(Graphics graphics) { // 先頭部分の中心座標から円を描画 graphics.FillEllipse(Brushes.DarkCyan, new Rectangle(Center.X - Radius, Center.Y - Radius, Radius * 2, Radius * 2)); // 先頭部分以外の各節の中心座標を求めて円を描画 List<Point> points = GetSubs(); foreach (Point pt in points) graphics.FillEllipse(Brushes.DarkCyan, new Rectangle(pt.X - Radius, pt.Y - Radius, Radius * 2, Radius * 2)); } } |
当たり判定
当たり判定の処理を示します。
自分自身を噛んでいないか?
まず自分自身を噛んでしまうとミスです。CheckBiteSelfメソッドは先頭部分の中心座標と先頭部分以外の各中心座標の距離が節の半径よりも接近していないかを調べます。もし節の半径よりも接近している節が見つかった場合は自分自身を噛んでいます。その場合はPlayerDeadイベントを発生させます。イベントハンドラの引数のPlayerDeadArgsには噛んでしまった節の中心座標が入ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Jiki { public event PlayerDeadHandler PlayerDead; public delegate void PlayerDeadHandler(object sender, PlayerDeadArgs e); public void CheckBiteSelf() { List<Point> points = GetSubs(); foreach (Point point in points) { double x2 = Math.Pow(Center.X - point.X, 2); double y2 = Math.Pow(Center.Y - point.Y, 2); if (Math.Sqrt(x2 + y2) < Radius) { PlayerDead?.Invoke(this, new PlayerDeadArgs(point)); return; } } } } |
1 2 3 4 5 6 7 8 9 |
public class PlayerDeadArgs : EventArgs { public Point DeadPoint = Point.Empty; public PlayerDeadArgs(Point deadPoint) { DeadPoint = deadPoint; } } |
敵との接触
敵が自機の先頭部分以外の節に接触した場合もミスです。そこでEnemyクラスに当たり判定の処理とPlayerDeadイベントを追加します。
まず敵と自機の先頭部分が接触していないか調べます。この場合は敵を倒したことになるので敵を倒したイベント HitEnemyを発生させます。
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 |
public class Enemy { // 敵を倒したイベント static public event EventHandler HitEnemy; // ミスをしたときのイベント static public event PlayerDeadHandler PlayerDead; public delegate void PlayerDeadHandler(object sender, PlayerDeadArgs e); static public void HitJudge(Jiki jiki) { List<Point> subs = jiki.GetSubs(); Enemy hitedEnemy = null; Point deadPoint = Point.Empty; foreach (Enemy enemy in Enemies) { // まず敵と自機の先頭部分が接触していないか調べる double x2 = Math.Pow(enemy.Center.X - jiki.Center.X, 2); double y2 = Math.Pow(enemy.Center.Y - jiki.Center.Y, 2); if (Math.Sqrt(x2 + y2) < jiki.Radius + enemy.Radius) { hitedEnemy = enemy; break; } // つぎに敵と自機の先頭以外の節部分が接触していないか調べる foreach (Point sub in subs) { x2 = Math.Pow(enemy.Center.X - sub.X, 2); y2 = Math.Pow(enemy.Center.Y - sub.Y, 2); if (Math.Sqrt(x2 + y2) < jiki.Radius + enemy.Radius) { deadPoint = sub; break; } } } // hitedEnemy != null または deadPoint != Point.Emptyのときはイベントを発生させる if (hitedEnemy != null) { Enemies.Remove(hitedEnemy); HitEnemy?.Invoke(hitedEnemy, new EventArgs()); } if (deadPoint != Point.Empty) { PlayerDead?.Invoke(null, new PlayerDeadArgs(deadPoint)); } } } |
ゲームとして完成させる
最後にゲームとして完成させます。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 |
public partial class Form1 : Form { Jiki Jiki = new Jiki(new Point(0,0)); int Score = 0; Timer Timer = new Timer(); public Form1() { InitializeComponent(); this.DoubleBuffered = true; this.BackColor = Color.Black; Jiki.Init(); Enemy.InitEnemies(4); Enemy.HitEnemy += EnemyHit; // 敵を倒したときの加点処理 Enemy.PlayerDead += PlayerDead; // 自機死亡時の処理 Jiki.PlayerDead += PlayerDead; // 自機死亡時の処理 Timer.Interval = 1000 / 60; Timer.Tick += Timer_Tick; Timer.Start(); } } |
自機の移動
CanTurnHorizontallyメソッドとCanTurnVerticallyメソッドはそれぞれ自機が水平方向、垂直方向に移動できるかどうかを調べるためのものです。交差点から5ピクセル以内のズレであれば移動できるものとします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public partial class Form1 : Form { bool CanTurnHorizontally() { if (Field.BranchY.Any(y => Jiki.Center.Y - 5 <= y && y <= Jiki.Center.Y + 5)) return true; else return false; } bool CanTurnVertically() { if (Field.BranchX.Any(x => Jiki.Center.X - 5 <= x && x <= Jiki.Center.X + 5)) return true; else return false; } } |
自機が方向転換できる場合にその方向のキーが押されたら移動方向を変更します。このとき交差点から5ピクセル以内のズレであれば移動できるためズレが発生しています。そのズレはJiki.SetPosXメソッドとJiki.SetPosXメソッドで修正します。
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 |
public partial class Form1 : Form { Field Field = new Field(); bool MoveUp = false; bool MoveDown = false; bool MoveLeft = false; bool MoveRight = false; protected override void OnKeyDown(KeyEventArgs e) { if (e.KeyCode == Keys.Up && CanTurnVertically()) { // 上方向に移動できるようにする // このとき交差点と自機のX座標のズレを修正する // 以下、同様 MoveUp = true; int x = Field.GetNeerestBranchX(Jiki.Center.X); Jiki.SetPosX(x); } if (e.KeyCode == Keys.Down && CanTurnVertically()) { MoveDown = true; int x = Field.GetNeerestBranchX(Jiki.Center.X); Jiki.SetPosX(x); } if (e.KeyCode == Keys.Left && CanTurnHorizontally()) { MoveLeft = true; int y = Field.GetNeerestBranchY(Jiki.Center.Y); Jiki.SetPosY(y); } if (e.KeyCode == Keys.Right && CanTurnHorizontally()) { MoveRight = true; int y = Field.GetNeerestBranchY(Jiki.Center.Y); Jiki.SetPosY(y); } base.OnKeyDown(e); } } |
キーが離されたらその方向には移動できないようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public partial class Form1 : Form { protected override void OnKeyUp(KeyEventArgs e) { if (e.KeyCode == Keys.Up) MoveUp = false; if (e.KeyCode == Keys.Down) MoveDown = false; if (e.KeyCode == Keys.Left) MoveLeft = false; if (e.KeyCode == Keys.Right) MoveRight = false; if (e.KeyCode == Keys.S) RetryGame(); base.OnKeyUp(e); } } |
Timer.Tickイベントが発生したら移動できる場合は移動させます。また移動の軌跡を追加する必要がある場合はJiki.AddTrajectoryメソッドを呼び出して追加します。そのあと当たり判定とステージクリア判定をおこないます。そのあと敵を移動させてInvalidateメソッドを呼び出します。
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 |
public partial class Form1 : Form { private void Timer_Tick(object sender, EventArgs e) { if (MoveUp && Jiki.Center.Y > Field.BranchY.Min()) Jiki.Move(0, -Jiki.Speed); else if (MoveDown && Jiki.Center.Y < Field.BranchY.Max()) Jiki.Move(0, Jiki.Speed); else if (MoveLeft && Jiki.Center.X > Field.BranchX.Min()) Jiki.Move(-Jiki.Speed, 0); else if (MoveRight && Jiki.Center.X < Field.BranchX.Max()) Jiki.Move(Jiki.Speed, 0); Jiki.AddTrajectory(); Enemy.HitJudge(Jiki); Jiki.CheckBiteSelf(); if (Enemy.GetEnemiesCount() == 0) Clear(); Enemy.MoveAll(); Invalidate(); } } |
描画処理
描画処理をおこないます。
最初にフィールドを描画し、自機、敵の順に描画します。そのあとスコアとゲームオーバーの場合は「GAME OVER」の文字を描画します。
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 |
public partial class Form1 : Form { bool IsGameOver = false; Point DeadPoint = Point.Empty; Font ScoreFont = new Font("MS ゴシック", 20); SolidBrush ScoreBrush = new SolidBrush(Color.White); SolidBrush GameOverBrush = new SolidBrush(Color.Red); Pen DeadPointPen = new Pen(Color.Red, 3); protected override void OnPaint(PaintEventArgs e) { Field.Draw(e.Graphics); Jiki.Draw(e.Graphics); Enemy.DrawAll(e.Graphics); string scoreText = "SCORE " + Score.ToString(); e.Graphics.DrawString(scoreText, ScoreFont, ScoreBrush, new Point(30, 10)); if (IsGameOver) { e.Graphics.DrawString("GAME OVER Press S key to Retry", ScoreFont, GameOverBrush, new Point(220, 10)); // 敵に噛みつかれた節に×をつける e.Graphics.DrawLine(DeadPointPen, new Point(DeadPoint.X-15, DeadPoint.Y-15), new Point(DeadPoint.X+15, DeadPoint.Y+15)); e.Graphics.DrawLine(DeadPointPen, new Point(DeadPoint.X - 15, DeadPoint.Y + 15), new Point(DeadPoint.X + 15, DeadPoint.Y - 15)); } base.OnPaint(e); } } |
ステージクリア、ゲームオーバーなどの処理
敵を倒したときは加点の処理、ステージクリアのときはその処理をおこないます。
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 |
public partial class Form1 : Form { WMPLib.WindowsMediaPlayer mediaPlayer = new WMPLib.WindowsMediaPlayer(); // 敵を倒した場合。音を鳴らしてスコアを追加、体長が1伸びる private void EnemyHit(object sender, EventArgs e) { mediaPlayer.URL = Application.StartupPath + "\\get.mp3"; Score += 100; Jiki.Length++; } // ゲームクリアの場合。音を鳴らしてタイマーをいったん止める。 // 2秒後に敵を初期化してタイマースタート void Clear() { mediaPlayer.URL = Application.StartupPath + "\\clear.mp3"; Timer.Stop(); Timer timer = new Timer(); timer.Interval = 2000; timer.Tick += Timer_Tick1; timer.Start(); void Timer_Tick1(object sender, EventArgs e) { Timer timer1 = (Timer)sender; timer1.Stop(); timer1.Dispose(); Enemy.InitEnemies(6); Timer.Start(); } } } |
また自機死亡の場合はゲームオーバー処理を行ないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public partial class Form1 : Form { // 自機死亡の場合はミスした場所をイベントハンドラの引数から取得してフィールド変数に保存。 // そのあとゲームオーバー処理をする private void PlayerDead(object sender, PlayerDeadArgs e) { DeadPoint = e.DeadPoint; GameOver(); } // ゲームオーバーのときは音を鳴らしてタイマーを止める void GameOver() { mediaPlayer.URL = Application.StartupPath + "\\dead.mp3"; IsGameOver = true; Timer.Stop(); } } |
ゲームオーバーのあとSキーを押すとRetryGameメソッドが呼び出されゲームが再開されます。その処理を示します。
1 2 3 4 5 6 7 8 9 10 11 |
public partial class Form1 : Form { void RetryGame() { Score = 0; Enemy.InitEnemies(4); IsGameOver = false; Timer.Start(); Jiki.Init(); } } |