Spacewar!の特徴として慣性力が働くため進行方向をすぐに変更できないことと、太陽の存在があります。画面中央にある太陽に近づきすぎると吸い込まれてしまうのです。今回は太陽の引力をプレイヤーに作用させる処理を追加します。
太陽を描画する
太陽を描画するクラスを作成します。太陽からは光がランダムに放たれています。この部分はDrawメソッドでおこないます。
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 class Sun { public Point Center = new Point(0, 0); Pen Pen = new Pen(Color.White, 2); Random Random = new Random(); public Sun(int x, int y) { Center = new Point(x, y); } public void Draw(Graphics graphics) { for (int i = 0; i < 10; i++) { double rad = Random.NextDouble() * Math.PI * 2; double r = 25 + Random.NextDouble() * 25; double x = r * Math.Cos(rad) + Center.X; double y = r * Math.Sin(rad) + Center.Y; graphics.DrawLine(Pen, new Point(Center.X, Center.Y), new Point((int)x, (int)y)); } } } |
太陽の引力
次に太陽が宇宙船を引き込む処理を示します。万有引力は距離の2乗に反比例します。ただここまで厳密にやる必要があるかは疑問ですが・・・。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class Sun { public void PullPlayer(Player player) { // プレイヤーと太陽の距離から重力を計算する // 重力は距離の2乗に反比例する double distance = Math.Sqrt(Math.Pow(player.CenterX - Center.X, 2) + Math.Pow(player.CenterY - Center.Y, 2)); double gravity = 512 / Math.Pow(distance, 2); // 重力でプレイヤーを太陽に引きずり込む double angle = Math.Atan2(player.CenterY - Center.Y, player.CenterX - Center.X); player.VX -= gravity * Math.Cos(angle); player.VY -= gravity * Math.Sin(angle); } } |
Form1クラスの処理
それでは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 |
public partial class Form1 : Form { Timer Timer = new Timer(); Player Player; Player Enemy; Sun Sun; public Form1() { InitializeComponent(); FormSize = ClientSize; Player = new Player(Properties.Resources.wedge); Enemy = new Player(Properties.Resources.needle); ResetPlayer(); ResetEnemy(); Sun = new Sun(ClientSize.Width / 2, ClientSize.Height / 2); Timer.Interval = 1000 / 60; Timer.Tick += Timer_Tick; Timer.Start(); this.DoubleBuffered = true; this.BackColor = Color.Black; } public static Size FormSize { get; private set; } protected override void OnResize(EventArgs e) { // フォームのサイズが変更されたら // 外部からフォームの大きさを取得するためのFormSizeプロパティを変更する FormSize = ClientSize; // 太陽の位置をフォームの中心になるように変更する if(Sun != null) Sun.Center = new Point(ClientSize.Width / 2, ClientSize.Height / 2); base.OnResize(e); } } |
各オブジェクトの移動
Timer.Tickイベントが発生したら自機と敵機、弾丸を移動させるとともに太陽の重力も作用させます。そのあと死亡判定をおこないます。
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 Timer_Tick(object sender, EventArgs e) { JikiMove(); EnemyMove(); Burret.MoveAll(); Spark.MoveAll(); Sun.PullPlayer(Player); Sun.PullPlayer(Enemy); CheckDead(); Invalidate(); } } |
自機の移動はどのキーが押されているかでPlayer.AngleとPlayer.Accelerateメソッドを実行してPlayer.VXとPlayer.VYを変化させます。そのあとPlayer.Moveメソッドで座標を変化させます。敵の動作はEnemyMethodメソッド(後述)でおこないます。そのあと当たり判定をします。
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 { WMPLib.WindowsMediaPlayer MediaPlayer3 = new WMPLib.WindowsMediaPlayer(); bool IsAccelerate = false; void JikiMove() { if (MoveLeft) Player.Angle -= 4; if (MoveRight) Player.Angle += 4; bool oldIsAccelerate = IsAccelerate; if (MoveUp) { IsAccelerate = true; Player.Accelerate(true); } else { IsAccelerate = false; Player.Accelerate(false); } // 加速していない状態から加速する状態へ変化したときだけ加速音を鳴らす if (!oldIsAccelerate && IsAccelerate) { MediaPlayer3.URL = Application.StartupPath + "\\speedup.mp3"; } Player.Move(); } void EnemyMove() { EnemyMethod(); // 敵の動作(後述) Enemy.Move(); } void EnemyMethod() { } } |
スコア表示をする
自機と敵機、どちらか片方が死亡した場合、生き残ったほうに点数を加算します。衝突して両方が死亡した場合はノーカウントです。まずはそれぞれのスコアを記憶しておくためのフィールド変数をつくります。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public partial class Form1 : Form { int MyScore = 0; int EnemyScore = 0; Player DiedfirstPlayer = null; // 先に死亡したのはどちらか? IsCollapseTogether = false; // 共倒れではないのか? void ResetScore() { MyScore = 0; EnemyScore = 0; } } |
当たり判定
当たり判定では自機と敵機が衝突したかを調べたあと自機と敵機が弾丸に接触していないかを調べます。
1 2 3 4 5 6 7 8 9 |
public partial class Form1 : Form { void CheckDead() { CheckCollisionBetweenPlayers(); CheckDead(Player); CheckDead(Enemy); } } |
自機と敵機が衝突する前提は、判定前はPlayer.IsDeadがともにfalseであることです。この前提が存在する場合、Player.DoesRectangleOverlapメソッドをつかって敵機を衝突しているか調べます。衝突している場合は自機、敵機ともに死亡なのでそれぞれを引数にしてOnDeadメソッドを呼びます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public partial class Form1 : Form { public void CheckCollisionBetweenPlayers() { if (Player.IsDead || Enemy.IsDead) return; if (Player.DoesRectangleOverlap(Enemy)) { IsCollapseTogether = true; OnDead(Player); OnDead(Enemy); } } } |
自機と敵機が衝突していないのであればそれぞれが太陽に衝突していないか、弾丸に当たっていないかを調べます。少なくとも片方が死亡している場合はOnDeadメソッドが呼び出されます。
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 |
public partial class Form1 : Form { public void CheckDead(Player player) { if (player.IsDead) return; Double distance = Math.Sqrt(Math.Pow(Sun.Center.X - player.CenterX, 2) + Math.Pow(Sun.Center.Y - player.CenterY, 2)); if (distance < 20) { OnDead(player); return; } foreach (Burret burret in Burret.Burrets) { int burretX = (int)burret.X; int burretY = (int)burret.Y; if (!burret.IsDead && player.IsPointInside(new Point(burretX, burretY))) { burret.IsDead = true; OnDead(player); return; } } } } |
ゲームの再開
自機または敵機死亡の場合は爆発の描画をして3秒後にリセットしてゲームを再開します。
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 |
public partial class Form1 : Form { // 爆発音を鳴らす WMPLib.WindowsMediaPlayer MediaPlayer2 = new WMPLib.WindowsMediaPlayer(); public void OnDead(Player player) { Spark.Explosion((int)player.CenterX, (int)player.CenterY); player.IsDead = true; MediaPlayer2.URL = Application.StartupPath + "\\explosion.mp3"; if (DiedfirstPlayer == null || IsCollapseTogether) { DiedfirstPlayer = player; Timer timer = new Timer(); timer.Interval = 3000; timer.Tick += Reset; timer.Start(); } // 両者が衝突した場合はどちらにも点は入らない if (IsCollapseTogether) return; // 片方が死亡してそのあと他方が死亡した場合、先に死亡した側には点は入らないようにする if (player == Player && DiedfirstPlayer == Player) EnemyScore++; if (player == Enemy && DiedfirstPlayer == Enemy) MyScore++; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public partial class Form1 : Form { void Reset(object sender, EventArgs e) { Timer t = (Timer)sender; t.Stop(); t.Dispose(); // スコアに関するフラグはクリアする DiedfirstPlayer = null; IsCollapseTogether = false; ResetPlayer(); ResetEnemy(); IsAccelerate = false; } } |
描画処理
描画のためのOnPaintメソッドを示します。自機、敵機(死んでいなければ)、太陽、弾丸、爆発、スコアの描画をしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public partial class Form1 : Form { protected override void OnPaint(PaintEventArgs e) { Player.Draw(e.Graphics); Enemy.Draw(e.Graphics); Sun.Draw(e.Graphics); Burret.DrawAll(e.Graphics); Spark.DrawAll(e.Graphics); ShowScore(e.Graphics); base.OnPaint(e); } void ShowScore(Graphics graphics) { string str = String.Format("{0} - {1}", MyScore, EnemyScore); graphics.DrawString(str, new Font("MS ゴシック", 20), Brushes.White, new Point(100, 10)); } } |