演習34 自機と敵機の衝突判定とゲームオーバー画面
・自機と敵機が衝突したら自機とその敵機を消そう ・そして、ゲームオーバーとし、ゲームオーバー画面に遷移しよう ・ゲームオーバー画面でも背景のスクロールや敵機の出現は続くものとする ・しかし、自機の移動や自弾の発射はできないようにする ・Enterキーが押されたら、リプレイできるようにしよう
手順と仕様
①各データをゲーム開始時の値にするメソッド void initData() を追加する
対象はコンストラクタにあった下記を移動:
player.i = playeri; //自機の画像
player.x = 320; //自機の中心X座標
player.y = 410; //自機の中心Y座標
player.hv = 0; //自機の左右方向の速度
加えて下記を追加:
player.v = 1; //自機を表示
waitpb = 0; //自弾発射の待ち時間
waitpb = 0; //自弾発射の待ち時間
②描画処理:プレイ画面において表示していたものはゲームオーバー画面でも表示する
③描画処理:自機の向きによって分岐する処理と、自機を描画する処理は、自機が表示状態の時のみにする
④描画処理:ゲームオーバー時に「GAME OVER」と(210,150)から25ポイントで表示し、
「Hit Enter Key」と(200,300)から25ポイントで表示
⑤キー入力時処理:タイトル画面でEnterキーが押されていたら、①を呼んでからゲームモードを1にする
⑥キー入力時処理:ゲームオーバー画面でEnterキーが押されていたら、①を呼び、全敵機と全自弾を非表示にし、
ゲームモードを1にする処理を追加
⑦タイマーイベント処理:スペースキーのチェックはプレイ画面の時のみにする
⑧タイマーイベント処理:全敵機について自弾との衝突判定をする前に、自機との衝突判定を行い、衝突していたら自機を消し、
ゲームモードを9にして抜ける処理を追加
作成例
//演習34 自機と敵機の衝突判定とゲームオーバー画面
using System; //フォームアプリケーションに必須
using System.Windows.Forms; //フォームアプリケーションに必須
using System.Drawing; //Image用
struct Item { //アイテムを表す構造体
public Image i; //画像
public int x; //中心X座標
public int y; //中心Y座標
public int hv; //左右方向の速度(左向きは負の数、右向きは正の数)
public int vv; //上下方向の速度(上向きは負の数、下向きは正の数)
public int v; //表示状態(0:非表示、1以上:表示)
}
class Program : Form { //Formクラスを継承
[System.Runtime.InteropServices.DllImport("user32.dll")] //外部DLLのインポート
private static extern short GetKeyState(int nVirtKey); //メソッドの外部定義指定
int gamemode = 0; //0:スタート画面、1:プレイ画面
int score = 0; //スコア
Image backi = Image.FromFile("backb.bmp"); //背景画像の読込
Image playeri = Image.FromFile("player.gif"); //自機画像の読込
Image playerl = Image.FromFile("playerl.gif"); //自機左寄画像の読込
Image playerr = Image.FromFile("playerr.gif"); //自機右寄画像の読込
Image bulleti = Image.FromFile("bullet.gif"); //自弾画像の読込
Image bullet2i = Image.FromFile("bullet2.gif"); //自弾画像2の読込
Image enemyi = Image.FromFile("enemy.gif"); //自弾画像2の読込
Font font1 = new Font("メイリオ", 20, FontStyle.Bold); //フォントを生成(メイリオ20P太字)
Font fontm = new Font("メイリオ", 25, FontStyle.Bold); //【追加】フォントを生成(メイリオ25P太字)
Font fontt = new Font("メイリオ", 80, FontStyle.Bold); //フォントを生成(メイリオ80P太字)
Brush brushs = new SolidBrush(Color.Yellow); //ブラシを生成(黄色)
Timer timer = new Timer(); //タイマーを生成
int backy = 0; //背景画像のつなぎ目のY座標
Item player; //プレイヤーの構造体オブジェクト
static int maxpb = 10; //自弾の最大数
static int maxenemy = 20; //敵機の最大数
Item[] pba = new Item[maxpb]; //自弾の構造体配列
Item[] enemya = new Item[maxenemy]; //敵機の構造体配列
const int cold = 10; //自弾の冷却時間
int waitpb = 0; //自弾発射待ち時間
int waitenemy = 0; //敵機出現待ち時間
int enemyint = 50; //敵機出現時間間隔
//【以下追加】初期化処理(ゲーム開始時とゲーム再開時に用いる)
private void initData() {
player.i = playeri; //自機の画像
player.x = 320; //自機の中心X座標
player.y = 410; //自機の中心Y座標
player.hv = 0; //自機の左右方向の速度
player.v = 1; //自機を表示
waitpb = 0; //自弾発射の待ち時間
waitenemy = 0; //敵機出現の待ち時間
}
//中心座標を用いて画像を描画
private void DrawItem(PaintEventArgs e, Item it) {
int xx = it.x - it.i.Width / 2; //幅の半分を差し引いて左上X座標を得る
int yy = it.y - it.i.Height / 2; //高さの半分を差し引いて左上Y座標を得る
e.Graphics.DrawImage(it.i, xx, yy);
}
//衝突判定(Item aとItem bが衝突しているかどうかを返す)
private bool isHit(Item a, Item b) {
int dest = (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y); //距離の2乗
double ra = (a.i.Width < a.i.Height) ? a.i.Width / 2 : a.i.Height / 2; //aの内径
double rb = (b.i.Width < b.i.Height) ? b.i.Width / 2 : b.i.Height / 2; //bの内径
return dest < (ra + rb) * (ra + rb); //距離の2乗が内径の和の2乗より小なら衝突
}
//描画処理
protected override void OnPaint(PaintEventArgs e) { //Formのメソッドをオーバーライド
base.OnPaint(e); //まず、Formクラスにおけるメソッドの内容(基本再描画処理)を実行
e.Graphics.DrawImage(backi, 0, backy); //背景画像を(0,つなぎ目)から描画
e.Graphics.DrawImage(backi, 0, backy - backi.Height); //背景画像をその上に描画
if (gamemode == 0) { //スタート画面
e.Graphics.DrawString("GAME1", fontt, brushs, 100, 100); //タイトルの描画
e.Graphics.DrawString("Hit Enter to Start", font1, brushs, 200, 300); //メッセージの描画
} else { //【変更】プレイ画面とゲームオーバー画面
string s = String.Format("SCORE:{0:000,000}", score); //スコア文字列を編集
e.Graphics.DrawString(s, font1, brushs, 400, 10); //スコア文字列の描画
if (gamemode == 1) { //【追加】プレイ画面のみ
switch (player.hv) { //自機の向きによって分岐
case 0: player.i = playeri; break; //通常
case 1: player.i = playerr; break; //右寄
case -1: player.i = playerl; break; //左寄
}
DrawItem(e, player); //自機を表示
}
foreach(var pb in pba) { //全自弾について繰返す
if (pb.v == 1) { //自弾がある?
DrawItem(e, pb); //自弾を表示
}
}
foreach(var enemy in enemya) { //全敵機について繰返す
if (enemy.v == 1) { //敵機がある?
DrawItem(e, enemy); //敵機を表示
}
}
}
if (gamemode == 9) { //【以下追加】ゲームオーバー画面
e.Graphics.DrawString("GAME OVER", fontm, brushs, 210, 150); //メッセージの描画
e.Graphics.DrawString("Hit Enter Key", fontm, brushs, 200, 300); //メッセージの描画
}
}
//キー入力時処理
void OnKeyDown(object o, KeyEventArgs e) { //キーボードが押された時に呼ばれるメソッド
if(e.KeyCode.ToString() == "Escape") { //Escキーが押されたら
Close(); //フォームアプリケーション終了
}
if(gamemode == 0 && e.KeyCode.ToString() == "Return") { //スタート画面でEnterキーが押されたら
initData(); //【追加】各データを開始時の値に
gamemode = 1; //プレイ画面に切り替える
timer.Start(); //タイマー開始
} else if (gamemode == 9 && e.KeyCode.ToString() == "Return") { //【以下追加】ゲームオーバー画面でEnterキーが押されたら
initData(); //各データを開始時の値に
for (int i = 0; i < maxenemy; i++) { //全敵機について繰返す
enemya[i].v = 0; //敵機[i]を消す
}
for (int i = 0; i < maxpb; i++) { //全自弾について繰返す
pba[i].v = 0; //自弾[i]を消す
}
gamemode = 1; //プレイ画面へ
}
Invalidate(); //【移動】画面再描画を依頼する
}
//タイマーイベント処理
void Play(object o, EventArgs e) { //タイマーから呼ばれるメソッド
backy = (backy < backi.Height) ? backy + 1 : 0; //背景画像のつなぎ目を下げる
if (gamemode == 1) { //プレイ画面
player.hv = 0; //自機の向きを通常にする
if (player.x > playeri.Width / 2 && GetKeyState((int)Keys.Left) < 0)
{ //左端ではなく←キーが押されている?
player.x -= 10; //自機を左へ
player.hv = -1; //左に移動中
}
if (player.x < backi.Width - playeri.Width / 2 && GetKeyState((int)Keys.Right) < 0) { //右端ではなく→キーが押されている?
player.x += 10; //自機を右へ
player.hv = 1; //右に移動中
}
}
for(int i = 0; i < maxenemy; i++) { //全敵機について繰返す
if (enemya[i].v != 0) { //敵機[i]が存在?
if (player.v != 0 && isHit(enemya[i], player)) { //【以下追加】自機があり衝突?
player.v = 0; //自機を消す
gamemode = 9; //ゲームオーバーにする
break; //繰返しを抜ける
}
for(int j = 0; j < maxpb; j++) { //全自弾について繰返す
if (pba[j].v == 1 && isHit(enemya[i], pba[j])) { //自弾[j]があり衝突?
enemya[i].v = 0; //敵機[i]を消す
pba[j].v = 0; //自弾[j]を消す
break; //次の敵機に進む
}
}
}
}
if (gamemode == 1) { //【追加】プレイ画面なら
if (GetKeyState((int)Keys.Space) < 0) { //スペースキーが押されている?
if (waitpb <= 0) { //自弾発射待ち時間ゼロ(発射可能)?
for(int i = 0; i < maxpb; i++) { //全自弾について繰返す
if (pba[i].v == 0) { //自弾[i]が非表示?
pba[i].v = 1; //表示にする
pba[i].i = bulleti; //画像を設定
pba[i].x = player.x; //X座標は自機の中心に合わせる
pba[i].y = player.y - player.i.Height / 2 - bulleti.Height / 2; //Y座標は自機の直上に合わせる
pba[i].vv = -5; //上移動速度を設定
waitpb = cold; //自弾発射待ち時間をセット
break; //1つ見つかったら抜ける
}
}
}
} else { //スペースキーが押されていない?
waitpb = 0; //自弾発射待ち時間をゼロ(発射可能)にする
}
}
if (waitenemy <= 0) { //敵機出現待ち時間ゼロ(出現発射可能)?
for(int i = 0; i < maxenemy; i++) { //全敵機について繰返す
if (enemya[i].v == 0) { //敵機[i]が非表示?
enemya[i].v = 1; //表示にする
enemya[i].i = enemyi; //画像を設定
enemya[i].x = player.x; //X座標は自機の中心に合わせる
enemya[i].y = -enemyi.Height; //Y座標は枠外
enemya[i].vv = 5; //下移動速度を設定
waitenemy = enemyint; //敵機出現待ち時間をセット
break; //1機出現したら抜ける
}
}
}
for(int i = 0; i < maxpb; i++) { //全自弾について繰返す
if (pba[i].v == 1) { //自弾[i]が存在したら
pba[i].i = (pba[i].i == bulleti) ? bullet2i : bulleti; //画像を交互変更
pba[i].y += pba[i].vv; //Y座標に上移動速度を反映することで上昇
if (pba[i].y + bulleti.Height / 2 < 0) { //画面上端より上に出たら
pba[i].v = 0; //自弾[i]を消す
}
}
}
for(int i = 0; i < maxenemy; i++) { //全敵機について繰返す
if (enemya[i].v == 1) { //敵機[i]が存在したら
enemya[i].y += enemya[i].vv; //Y座標に下移動速度を反映することで下降
if (enemya[i].y + bulleti.Height / 2 > backi.Height) { //画面下端より下に出たら
enemya[i].v = 0; //敵機[i]を消す
}
}
}
if (waitpb > 0) { //自弾発射待ち時間がセットされている(待ち状態)?
waitpb--; //カウントダウンする
}
if (waitenemy > 0) { //敵機出現待ち時間がセットされている(待ち状態)?
waitenemy--; //カウントダウンする
}
Invalidate(); //画面再描画を依頼する
}
//コンストラクタ
Program() {
DoubleBuffered = true; //ダブルバッファリングの導入
KeyDown += new KeyEventHandler(OnKeyDown); //キーボードが押された時に呼ばれるメソッドを登録
timer.Tick += new EventHandler(Play); //タイマーから呼ばれるメソッドを登録
timer.Interval = 10; //タイマーの間隔
//player.i = playeri; //【削除】自機の画像
//player.x = 320; //【削除】自機のX座標
//player.y = 410; //【削除】自機のY座標
//player.hv = 0; //【削除】自機の左右方向の速度
}
static void Main() { //publicの指定は任意
Program f = new Program(); //継承したフォームのインスタンスを生成
f.Size = new Size(660, 520); //インスタンスのSizeプロパティに高さと幅を代入
f.Text = "Game"; //インスタンスのTextプロパティにフォーム名を代入
f.ControlBox = false; //ControlBoxを非表示にする
f.FormBorderStyle = FormBorderStyle.Fixed3D; //フォームサイズ変更を禁止
Application.Run(f); //インスタンスを画面に出す
}
}
演習35 スコアアップ
・敵機に自弾が当たったら+10点としよう ・スコアはゲームオーバー画面でEnterキーが押されたらゼロにしよう ・ハイスコアを保持しておき、GAME OVER画面でハイスコアを超えたら「HiSCORE Update !!」と表示しよう ・ハイスコア以下なら「HiSCORE is 000,000(点数)」と表示しよう
手順と仕様
①データメンバ:ハイスコア用変数 int hiscoreを0で初期化
②データメンバ:ハイスコアメッセージ用変数 string hiscoremesを""で初期化
③初期化処理:スコア score を0に戻す処理を追加
④描画処理:ハイスコアメッセージ hiscoremesを150,200から描画する処理を追加
⑤タイマーイベント処理:敵機と自機が衝突したら、ハイスコア更新かをチェックする。
更新していたらハイスコアメッセージ hiscoremesに"HiSCORE Update !!"を代入。
していなければハイスコアメッセージ hiscoremesに"HiSCORE is {0:000,000}", hiscoreを代入。
⑥タイマーイベント処理:敵機と自弾が衝突したら、スコアに10加算
作成例
//演習35 スコアアップ
using System; //フォームアプリケーションに必須
using System.Windows.Forms; //フォームアプリケーションに必須
using System.Drawing; //Image用
struct Item { //アイテムを表す構造体
public Image i; //画像
public int x; //中心X座標
public int y; //中心Y座標
public int hv; //左右方向の速度(左向きは負の数、右向きは正の数)
public int vv; //上下方向の速度(上向きは負の数、下向きは正の数)
public int v; //表示状態(0:非表示、1以上:表示)
}
class Program : Form { //Formクラスを継承
[System.Runtime.InteropServices.DllImport("user32.dll")] //外部DLLのインポート
private static extern short GetKeyState(int nVirtKey); //メソッドの外部定義指定
int gamemode = 0; //0:スタート画面、1:プレイ画面
int score = 0; //スコア
int hiscore = 0; //【追加】ハイスコア
string hiscoremes = ""; //【追加】ハイスコアメッセージ
Image backi = Image.FromFile("backb.bmp"); //背景画像の読込
Image playeri = Image.FromFile("player.gif"); //自機画像の読込
Image playerl = Image.FromFile("playerl.gif"); //自機左寄画像の読込
Image playerr = Image.FromFile("playerr.gif"); //自機右寄画像の読込
Image bulleti = Image.FromFile("bullet.gif"); //自弾画像の読込
Image bullet2i = Image.FromFile("bullet2.gif"); //自弾画像2の読込
Image enemyi = Image.FromFile("enemy.gif"); //自弾画像2の読込
Font font1 = new Font("メイリオ", 20, FontStyle.Bold); //フォントを生成(メイリオ20P太字)
Font fontm = new Font("メイリオ", 25, FontStyle.Bold); //フォントを生成(メイリオ25P太字)
Font fontt = new Font("メイリオ", 80, FontStyle.Bold); //フォントを生成(メイリオ80P太字)
Brush brushs = new SolidBrush(Color.Yellow); //ブラシを生成(黄色)
Timer timer = new Timer(); //タイマーを生成
int backy = 0; //背景画像のつなぎ目のY座標
Item player; //プレイヤーの構造体オブジェクト
static int maxpb = 10; //自弾の最大数
static int maxenemy = 20; //敵機の最大数
Item[] pba = new Item[maxpb]; //自弾の構造体配列
Item[] enemya = new Item[maxenemy]; //敵機の構造体配列
const int cold = 10; //自弾の冷却時間
int waitpb = 0; //自弾発射待ち時間
int waitenemy = 0; //敵機出現待ち時間
int enemyint = 50; //敵機出現時間間隔
//初期化処理(ゲーム開始時とゲーム再開時に用いる)
private void initData() {
player.i = playeri; //自機の画像
player.x = 320; //自機の中心X座標
player.y = 410; //自機の中心Y座標
player.hv = 0; //自機の左右方向の速度
player.v = 1; //自機を表示
waitpb = 0; //自弾発射の待ち時間
waitenemy = 0; //敵機出現の待ち時間
score = 0; //【追加】スコア
}
//中心座標を用いて画像を描画
private void DrawItem(PaintEventArgs e, Item it) {
int xx = it.x - it.i.Width / 2; //幅の半分を差し引いて左上X座標を得る
int yy = it.y - it.i.Height / 2; //高さの半分を差し引いて左上Y座標を得る
e.Graphics.DrawImage(it.i, xx, yy);
}
//衝突判定(Item aとItem bが衝突しているかどうかを返す)
private bool isHit(Item a, Item b) {
int dest = (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y); //距離の2乗
double ra = (a.i.Width < a.i.Height) ? a.i.Width / 2 : a.i.Height / 2; //aの内径
double rb = (b.i.Width < b.i.Height) ? b.i.Width / 2 : b.i.Height / 2; //bの内径
return dest < (ra + rb) * (ra + rb); //距離の2乗が内径の和の2乗より小なら衝突
}
//描画処理
protected override void OnPaint(PaintEventArgs e) { //Formのメソッドをオーバーライド
base.OnPaint(e); //まず、Formクラスにおけるメソッドの内容(基本再描画処理)を実行
e.Graphics.DrawImage(backi, 0, backy); //背景画像を(0,つなぎ目)から描画
e.Graphics.DrawImage(backi, 0, backy - backi.Height); //背景画像をその上に描画
if (gamemode == 0) { //スタート画面
e.Graphics.DrawString("GAME1", fontt, brushs, 100, 100); //タイトルの描画
e.Graphics.DrawString("Hit Enter to Start", font1, brushs, 200, 300); //メッセージの描画
} else { //プレイ画面とゲームオーバー画面
string s = String.Format("SCORE:{0:000,000}", score); //スコア文字列を編集
e.Graphics.DrawString(s, font1, brushs, 400, 10); //スコア文字列の描画
if (gamemode == 1) { //【追加】プレイ画面のみ
switch (player.hv) { //自機の向きによって分岐
case 0: player.i = playeri; break; //通常
case 1: player.i = playerr; break; //右寄
case -1: player.i = playerl; break; //左寄
}
DrawItem(e, player); //自機を表示
}
foreach(var pb in pba) { //全自弾について繰返す
if (pb.v == 1) { //自弾がある?
DrawItem(e, pb); //自弾を表示
}
}
foreach(var enemy in enemya) { //全敵機について繰返す
if (enemy.v == 1) { //敵機がある?
DrawItem(e, enemy); //敵機を表示
}
}
}
if (gamemode == 9) { //ゲームオーバー画面
e.Graphics.DrawString("GAME OVER", fontm, brushs, 210, 150); //メッセージの描画
e.Graphics.DrawString(hiscoremes, fontm, brushs, 150, 200); //【追加】ハイスコアメッセージの描画
e.Graphics.DrawString("Hit Enter Key", fontm, brushs, 200, 300); //メッセージの描画
}
}
//キー入力時処理
void OnKeyDown(object o, KeyEventArgs e) { //キーボードが押された時に呼ばれるメソッド
if(e.KeyCode.ToString() == "Escape") { //Escキーが押されたら
Close(); //フォームアプリケーション終了
}
if(gamemode == 0 && e.KeyCode.ToString() == "Return") { //スタート画面でEnterキーが押されたら
initData(); //各データを開始時の値に
gamemode = 1; //プレイ画面に切り替える
timer.Start(); //タイマー開始
} else if (gamemode == 9 && e.KeyCode.ToString() == "Return") { //ゲームオーバー画面でEnterキーが押されたら
initData(); //各データを開始時の値に
for (int i = 0; i < maxenemy; i++) { //全敵機について繰返す
enemya[i].v = 0; //敵機[i]を消す
}
for (int i = 0; i < maxpb; i++) { //全自弾について繰返す
pba[i].v = 0; //自弾[i]を消す
}
gamemode = 1; //プレイ画面へ
}
Invalidate(); //画面再描画を依頼する
}
//タイマーイベント処理
void Play(object o, EventArgs e) { //タイマーから呼ばれるメソッド
backy = (backy < backi.Height) ? backy + 1 : 0; //背景画像のつなぎ目を下げる
if (gamemode == 1) { //プレイ画面
player.hv = 0; //自機の向きを通常にする
if (player.x > playeri.Width / 2 && GetKeyState((int)Keys.Left) < 0)
{ //左端ではなく←キーが押されている?
player.x -= 10; //自機を左へ
player.hv = -1; //左に移動中
}
if (player.x < backi.Width - playeri.Width / 2 && GetKeyState((int)Keys.Right) < 0) { //右端ではなく→キーが押されている?
player.x += 10; //自機を右へ
player.hv = 1; //右に移動中
}
}
for(int i = 0; i < maxenemy; i++) { //全敵機について繰返す
if (enemya[i].v != 0) { //敵機[i]が存在?
if (player.v != 0 && isHit(enemya[i], player)) { //自機があり衝突?
player.v = 0; //自機を消す
gamemode = 9; //ゲームオーバーにする
if (score > hiscore) { //【以下追加】ハイスコア更新?
hiscore = score; //ハイスコア更新
hiscoremes = "HiSCORE Update !!";
} else { //更新していなければ
hiscoremes = String.Format("HiSCORE is {0:000,000}", hiscore);
}
break; //繰返しを抜ける
}
for(int j = 0; j < maxpb; j++) { //全自弾について繰返す
if (pba[j].v == 1 && isHit(enemya[i], pba[j])) { //自弾[j]があり衝突?
enemya[i].v = 0; //敵機[i]を消す
pba[j].v = 0; //自弾[j]を消す
score += 10; //【追加】スコアアップ
break; //次の敵機に進む
}
}
}
}
if (gamemode == 1) { //プレイ画面なら
if (GetKeyState((int)Keys.Space) < 0) { //スペースキーが押されている?
if (waitpb <= 0) { //自弾発射待ち時間ゼロ(発射可能)?
for(int i = 0; i < maxpb; i++) { //全自弾について繰返す
if (pba[i].v == 0) { //自弾[i]が非表示?
pba[i].v = 1; //表示にする
pba[i].i = bulleti; //画像を設定
pba[i].x = player.x; //X座標は自機の中心に合わせる
pba[i].y = player.y - player.i.Height / 2 - bulleti.Height / 2; //Y座標は自機の直上に合わせる
pba[i].vv = -5; //上移動速度を設定
waitpb = cold; //自弾発射待ち時間をセット
break; //1つ見つかったら抜ける
}
}
}
} else { //スペースキーが押されていない?
waitpb = 0; //自弾発射待ち時間をゼロ(発射可能)にする
}
}
if (waitenemy <= 0) { //敵機出現待ち時間ゼロ(出現発射可能)?
for(int i = 0; i < maxenemy; i++) { //全敵機について繰返す
if (enemya[i].v == 0) { //敵機[i]が非表示?
enemya[i].v = 1; //表示にする
enemya[i].i = enemyi; //画像を設定
enemya[i].x = player.x; //X座標は自機の中心に合わせる
enemya[i].y = -enemyi.Height; //Y座標は枠外
enemya[i].vv = 5; //下移動速度を設定
waitenemy = enemyint; //敵機出現待ち時間をセット
break; //1機出現したら抜ける
}
}
}
for(int i = 0; i < maxpb; i++) { //全自弾について繰返す
if (pba[i].v == 1) { //自弾[i]が存在したら
pba[i].i = (pba[i].i == bulleti) ? bullet2i : bulleti; //画像を交互変更
pba[i].y += pba[i].vv; //Y座標に上移動速度を反映することで上昇
if (pba[i].y + bulleti.Height / 2 < 0) { //画面上端より上に出たら
pba[i].v = 0; //自弾[i]を消す
}
}
}
for(int i = 0; i < maxenemy; i++) { //全敵機について繰返す
if (enemya[i].v == 1) { //敵機[i]が存在したら
enemya[i].y += enemya[i].vv; //Y座標に下移動速度を反映することで下降
if (enemya[i].y + bulleti.Height / 2 > backi.Height) { //画面下端より下に出たら
enemya[i].v = 0; //敵機[i]を消す
}
}
}
if (waitpb > 0) { //自弾発射待ち時間がセットされている(待ち状態)?
waitpb--; //カウントダウンする
}
if (waitenemy > 0) { //敵機出現待ち時間がセットされている(待ち状態)?
waitenemy--; //カウントダウンする
}
Invalidate(); //画面再描画を依頼する
}
//コンストラクタ
Program() {
DoubleBuffered = true; //ダブルバッファリングの導入
KeyDown += new KeyEventHandler(OnKeyDown); //キーボードが押された時に呼ばれるメソッドを登録
timer.Tick += new EventHandler(Play); //タイマーから呼ばれるメソッドを登録
timer.Interval = 10; //タイマーの間隔
}
static void Main() { //publicの指定は任意
Program f = new Program(); //継承したフォームのインスタンスを生成
f.Size = new Size(660, 520); //インスタンスのSizeプロパティに高さと幅を代入
f.Text = "Game"; //インスタンスのTextプロパティにフォーム名を代入
f.ControlBox = false; //ControlBoxを非表示にする
f.FormBorderStyle = FormBorderStyle.Fixed3D; //フォームサイズ変更を禁止
Application.Run(f); //インスタンスを画面に出す
}
}
完成版
・下記の画像と音声をダウンロードしてくださいebullet.gif
burn.gif
meteo3.gif 開始音 終了音 爆散音 再爆散音 ※効果音著作者:MaouDamashii(https://maou.audio/) //シューティングゲーム開発演習 最終版 using System; //汎用的に利用 using System.Windows.Forms; //フォームアプリケーションに必須 using System.Drawing; //Size、Image用 using System.Media; //SoundPlayer用 //《アイテムを表す構造体》 struct Item { public Image i; //画像 public int x; //中心X座標 public int y; //中心Y座標 public int hv; //左右方向の速度(左向きは負の数、右向きは正の数) public int vv; //上下方向の速度(上向きは負の数、下向きは正の数) public int v; //表示状態(0:非表示、1~100:表示、101~200:爆散) } //《ゲームを表すクラス》 class Program : Form { //Formクラスの派生クラス //《1. DLLインポートと外部定義指定》 [System.Runtime.InteropServices.DllImport("user32.dll")] //DLLインポート private static extern short GetKeyState(int nVirtKey); //外部定義指定 //《2. データメンバ》 int gamemode = 0; //モード(0:タイトル画面,1:プレイ画面,9:終了画面) int score = 0; //スコア int hiscore = 0; //ハイスコア Image backi = Image.FromFile("backb.bmp"); //背景画像を読込む Image playeri = Image.FromFile("player.gif"); //自機通常画像を読込む Image playerl = Image.FromFile("playerl.gif"); //自機左寄画像を読込む Image playerr = Image.FromFile("playerr.gif"); //自機右寄画像を読込む Image bulleti = Image.FromFile("bullet.gif"); //自弾画像を読込む Image bullet2i = Image.FromFile("bullet2.gif"); //自弾画像2を読込む Image enemyi = Image.FromFile("enemy.gif"); //敵機画像を読込む Image enemybi = Image.FromFile("ebullet.gif"); //敵弾画像を読込む static Image burni = Image.FromFile("burn.gif"); //爆散画像を読込む static Image meteoi = Image.FromFile("meteo3.gif"); //炎上画像を読込む Font font1 = new Font("メイリオ", 20, FontStyle.Bold); //フォントを生成 Font fontt = new Font("メイリオ", 80, FontStyle.Bold); //フォントを生成 Font fontm = new Font("メイリオ", 25, FontStyle.Bold); //フォントを生成 Brush brushs = new SolidBrush(Color.Yellow); //黄色のブラシ Pen burnp = new Pen(new TextureBrush(burni), 15); //爆散用テクスチャブラシによる太さ15のペン Timer timer = new Timer(); //タイマーの生成 int backy = 0; //1枚目の背景描画開始Y座標 Item player; //自機の構造体オブジェクト const int maxpb = 10; //自弾の最大数 Item[] pba = new Item[maxpb]; //自弾の構造体オブジェクト配列 const int cold = 10; //自弾発射の冷却時間 int waitpb = 0; //自弾発射の待ち時間 const int maxenemy = 20; //敵機の最大数 Item[] enemya = new Item[maxenemy]; //敵機の構造体オブジェクト配列 int waitenemy = 0; //敵機の出現待ち時間 int enemyint = 50; //敵機の出現間隔 string hiscoremsg = ""; //ハイスコアメッセージ Random rnd = new Random(); //乱数用のRandomクラスのインスタンスを生成 int enemybint = 75; //敵弾の出現間隔 int waitenemyb = 200; //敵弾の出現待ち時間 const int maxenemyb = 20; //敵弾の最大数 Item[] enemyba = new Item[maxenemyb]; //敵弾の構造体オブジェクト配列 const int enemyvv = 5; //敵機移動速度 SoundPlayer begins = new SoundPlayer("maou_se_onepoint30.wav"); //開始音の準備 SoundPlayer ends = new SoundPlayer("maou_se_onepoint29.wav"); //終了音の準備 SoundPlayer mhits = new SoundPlayer("maou_se_battle12.wav"); //爆散音の準備 SoundPlayer hits = new SoundPlayer("maou_se_battle18.wav"); //再爆散音の準備 //《3. 初期化処理》各データをゲーム開始時の値にする void initData() { player.i = playeri; //自機の画像 player.x = 320; //自機の中心X座標 player.y = 410; //自機の中心Y座標 player.hv = 0; //自機の左右方向の速度 player.v = 100; //自機を表示 waitpb = 0; //自弾発射の待ち時間 waitpb = 0; //自弾発射の待ち時間 score = 0; //スコアクリア waitenemyb = 200; //敵弾の出現待ち時間 enemyint = 50; //敵機の出現間隔 enemybint = 75; //敵弾の出現間隔 } //《4. 中央座標を用いる画像描画処理》 private void DrawItem(PaintEventArgs e, Item it) { int xx = it.x - it.i.Width / 2; //左上X座標を得る int yy = it.y - it.i.Height / 2; //左上Y座標を得る e.Graphics.DrawImage(it.i, xx, yy); } //《5. 中央座標を用いる画像描画処理(縮小率%付き)》 private void DrawItem(PaintEventArgs e, Item it, int rate) { if (rate == 100) { //等倍? DrawItem(e, it); //通常描画する } else { int xx = it.x - it.i.Width * rate / 100 / 2; //左上X座標を得る int yy = it.y - it.i.Height * rate / 100 / 2; //左上Y座標を得る e.Graphics.DrawImage(it.i, xx, yy, it.i.Width * rate / 100, it.i.Height * rate / 100); } } //《6. 楕円形の衝突判定》 private bool isHit(Item a, Item b) { //アイテムaとアイテムbが衝突しているかどうかを返す int dest = (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y); //距離2乗 double ra = (a.i.Width < a.i.Height) ? a.i.Width / 2.0 : a.i.Height / 2.0; //内径a double rb = (b.i.Width < b.i.Height) ? b.i.Width / 2.0 : b.i.Height / 2.0; //内径b return dest < (ra + rb) * (ra + rb); //距離2乗と内径の和の2乗を比較 } //《7. 描画処理》オーバライド protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); //基本クラスの描画処理を呼ぶ e.Graphics.DrawImage(backi, 0, backy); //背景画像を描画 e.Graphics.DrawImage(backi, 0, backy - backi.Height); //背景画像を描画 if (gamemode == 0) { //スタート画面? e.Graphics.DrawString("GAME1", fontt, brushs, 100, 150); //タイトル表示 e.Graphics.DrawString("Hit Enter Key", fontm, brushs, 200, 300); //メッセージ表示 } else { //プレイ画面かゲームオーバー画面? string s = String.Format("SCORE:{0:000,000}", score); //スコア文字列を作る e.Graphics.DrawString(s, font1, brushs, 400, 10); //スコア表示 if (player.v != 0) { //自機が表示中? switch (player.hv) { //自機の向きによって分岐 case 0: player.i = playeri; break; //通常画像にする case -1: player.i = playerl; break; //左寄画像にする case 1: player.i = playerr; break; //右寄画像にする } DrawItem(e, player, player.v); //自機を倍率描画 } foreach (var pb in pba) { //全自弾について繰返す if (pb.v == 1) { //自弾がある? DrawItem(e, pb); //自弾を描画 } } foreach (var enemy in enemya) { //全敵機について繰返す if (enemy.v != 0) { //表示中? DrawItem(e, enemy, enemy.v); //敵機を倍率描画 } } foreach (var enemyb in enemyba) { //全敵弾について繰返す if (enemyb.v == 1) { //敵弾がある? DrawItem(e, enemyb); //敵弾を描画 } else if (enemyb.v >= 101) { //爆散中? int rr = (enemyb.v - 100) * 2; //爆散円の直径(2→200) e.Graphics.DrawEllipse(burnp, enemyb.x - (enemyb.v - 100), enemyb.y - (enemyb.v - 100), rr, rr); //爆散円を描く } } } if (gamemode == 9) { //ゲームオーバー画面? e.Graphics.DrawString("GAME OVER", fontm, brushs, 210, 150); //メッセージ表示 e.Graphics.DrawString(hiscoremsg, fontm, brushs, 150, 200); //ハイスコアメッセージ表示 e.Graphics.DrawString("Hit Enter Key", fontm, brushs, 200, 300); //メッセージ表示 } } //《8. キー入力時処理》 void OnKeyDown(object o, KeyEventArgs e) { if (e.KeyCode.ToString() == "Escape") { //Escキーが押されていたら Close(); //フォーム終了 } //タイトル画面でEnterキーが押されていたら if (gamemode == 0 && e.KeyCode.ToString() == "Return") { begins.Play(); //開始音を出力 initData(); //各データをゲーム開始時の値にする gamemode = 1; //プレイ動画に遷移 timer.Start(); //タイマー開始 } //ゲームオーバー画面でEnterキーが押されていたら else if (gamemode == 9 && e.KeyCode.ToString() == "Return") { begins.Play(); //開始音を出力 initData(); //各データをゲーム開始時の値にする for (int i = 0; i < maxenemy; i++) { //全敵機について繰返す enemya[i].v = 0; //無しにする } for (int i = 0; i < maxpb; i++) { //全自弾について繰返す pba[i].v = 0; //無しにする } for (int i = 0; i < maxenemyb; i++) { //全敵弾について繰返す enemyba[i].v = 0; //無しにする } gamemode = 1; //プレイ動画に遷移 } Invalidate(); //画面再描画を依頼 } //《9. タイマーイベント処理》 void Play(object o, EventArgs e) { //《9.1. 背景画面のスクロール backy = (backy + 1) % backi.Height; //1枚目の背景描画開始Y座標を下げる //《9.2. 自機の向きを決める if (gamemode == 1) { //プレイ画面? player.hv = 0; //自機の向きを無しにしておく if (player.x > playeri.Width / 2 && GetKeyState((int)Keys.Left) < 0) { //範囲内で←キーが押されている? player.x -= 10; //自機を左へ player.hv = -1; //左向き } if (player.x < backi.Width - playeri.Width / 2 && GetKeyState((int)Keys.Right) < 0) { //範囲内で→キーが押されている? player.x += 10; //自機を右へ player.hv = 1; //右向き } } //《9.3. 敵機と自機・自弾の衝突 for (int i = 0; i < maxenemy; i++) { //全敵機について繰返す if (enemya[i].v == 100) { //敵機が100%存在? if (player.v == 100 && isHit(enemya[i], player)) { //自機が100%存在し衝突? GameOver(); //ゲームオーバーにする enemya[i].v = 99; //敵機落下開始 enemya[i].i = meteoi; //破壊画像にする break; //抜ける } for (int j = 0; j < maxpb; j++) { //全自弾について繰返す if (pba[j].v != 0 && isHit(enemya[i], pba[j])) { //自弾が存在し衝突? enemya[i].v = 99; //敵機落下開始 enemya[i].i = meteoi; //破壊画像にする pba[j].v = 0; //この自弾を消す if (gamemode == 1) { //プレイ画面? score += 10; //スコアアップ } break; //次の敵機へ進む } } } } //《9. 4 敵弾と自機・自弾の衝突 for (int i = 0; i < maxenemyb; i++) { //全敵弾について繰返す if (enemyba[i].v == 1) { //敵弾が存在? if (player.v == 100 && isHit(enemyba[i], player)) { //自機が存在し敵弾と衝突? GameOver(); //ゲームオーバーにする enemyba[i].v = 0; //敵弾を消す break; //抜ける } for (int j = 0; j < maxpb; j++) { //全自弾について繰返す if (pba[j].v != 0 && isHit(enemyba[i], pba[j])) { //自弾が存在し敵弾と衝突? enemyba[i].v = 101; //この敵弾を爆散状態にする pba[j].v = 0; //この自弾を消す if (gamemode == 1) { //プレイ画面? mhits.Play(); //命中音を出力 score += 100; //スコアアップ } break; //次の敵弾へ進む } } } else if (enemyba[i].v >= 101) { //敵弾が爆散状態? for (int j = 0; j < maxenemy; j++) { //全敵機について繰返す if (enemya[j].v == 100) { //敵機が100%存在? double d1 = Math.Pow(enemyba[i].x - enemya[j].x, 2) + Math.Pow(enemyba[i].y - enemya[j].y, 2); //中心距離の2乗 double d2 = Math.Pow(enemyba[i].v - 100 + enemyi.Width / 2, 2); //爆散円と敵機半径の和の2乗 if (d1 < d2) { //敵機が爆散円内? enemya[j].v = 99; //敵機落下開始 enemya[j].i = meteoi; //破壊画像にする if (gamemode == 1) { //プレイ画面? score += 10; //スコアアップ } break; //次の敵機へ進む } } } for (int j = 0; j < maxenemyb; j++) { //全敵弾について繰返す if (enemyba[j].v == 1) { //敵弾が存在? double d1 = Math.Pow(enemyba[i].x - enemyba[j].x, 2) + Math.Pow(enemyba[i].y - enemyba[j].y, 2); //中心距離の2乗 double d2 = Math.Pow(enemyba[i].v - 100 + enemybi.Width / 2, 2); //爆散円と敵弾半径の和の2乗 if (d1 < d2) { //敵弾が爆散円内? enemyba[j].v = 101; //敵弾爆散開始 if (gamemode == 1) { //プレイ画面? hits.Play(); //命中音を出力 score += 10; //スコアアップ } break; //次の敵弾へ進む } } } } } //《9.5 自弾発射 if (gamemode == 1) { //プレイ画面? if (GetKeyState((int)Keys.Space) < 0) { //スペースキーが押されている? if (waitpb <= 0) { //自弾発射待ち時間がゼロ? for (int i = 0; i < maxpb; i++) { //全自弾について繰返す if (pba[i].v == 0) { //自弾が非表示? pba[i].v = 1; //表示にする pba[i].i = bulleti; //画像 pba[i].x = player.x; //X座標は自機と同じ pba[i].y = player.y - player.i.Height / 2 - pba[i].i.Height / 2; //Y座標は自機の直上 pba[i].vv = -5; //上移動速度 waitpb = cold; //自弾発射待ち時間をセット break; //1発発射できればOK } } } } else { //スペースキーが押されていない? waitpb = 0; //自弾発射待ち時間をゼロにして発射可能にする } } //《9.6 敵機出現 if (waitenemy <= 0) { //敵機出現待ち時間がゼロ? for (int i = 0; i < maxenemy; i++) { //全敵機について繰返す if (enemya[i].v == 0) { //敵機が非表示? enemya[i].v = 100; //表示100%にする enemya[i].i = enemyi; //画像 enemya[i].x = enemyi.Width / 2 + rnd.Next(backi.Width - enemyi.Width); //X座標はランダム enemya[i].y = -enemyi.Height; //Y座標は上端の直上 enemya[i].vv = enemyvv; //下移動速度 waitenemy = enemyint; //敵機出現待ち時間をセット break; //1機出現できればOK } } } //《9.7 敵弾発射 if (waitenemyb <= 0) { //敵弾出現待ち時間がゼロ? int enemyi = -1; //発射敵機 for (int i = 0; i < maxenemy; i++) { //全敵機について繰返す if (enemya[i].v == 100 && enemya[i].y > 0 && enemya[i].y < backi.Height / 2) { //敵機が画面上半分にいる? enemyi = i; //発射敵機決定 break; } } if (enemyi != -1) { //発射敵機決定済? for (int i = 0; i < maxenemyb; i++) { //全敵弾について繰返す if (enemyba[i].v == 0) { //敵弾が非表示? enemyba[i].v = 1; //表示にする enemyba[i].i = enemybi; //画像 enemyba[i].x = enemya[enemyi].x; //X座標は発射敵機と同じ enemyba[i].y = enemya[enemyi].y - 15; //Y座標は発射敵機の15ドット上 double rad = Math.Atan2(player.y - enemyba[i].y, player.x - enemyba[i].x); //角度を得る enemyba[i].hv = (int)(Math.Cos(rad) * enemyvv * 1.5); //X移動量 enemyba[i].vv = (int)(Math.Sin(rad) * enemyvv * 1.5); //Y移動量 waitenemyb = enemybint; //敵弾発射待ち時間をセット break; //1弾発射できればOK } } } } //《9.8 自弾移動 for (int i = 0; i < maxpb; i++) { //全自弾について繰返す if (pba[i].v != 0) { //自弾が存在? pba[i].i = (pba[i].i == bulleti) ? bullet2i : bulleti; //画像を交互変更 pba[i].y += pba[i].vv; //上へ移動 if (pba[i].y + pba[i].i.Height / 2 < 0) { //画面上端より上に出たら pba[i].v = 0; //自弾を消す } } } //《9.9 敵機移動 for (int i = 0; i < maxenemy; i++) { //全敵機について繰返す if (enemya[i].v != 0) { //敵機が存在? if (enemya[i].v <= 99) { //落下中? enemya[i].v--; //落下 } enemya[i].y += enemya[i].vv; //下へ移動 if (enemya[i].y - enemya[i].i.Height / 2 > backi.Height) { //画面下端より下に出たら enemya[i].v = 0; //敵機を消す } } } //《9.10 敵弾移動・爆散 for (int i = 0; i < maxenemyb; i++) { //全敵弾について繰返す if (enemyba[i].v == 1) { //敵弾が存在? enemyba[i].x += (int)enemyba[i].hv; //X座標を変更 enemyba[i].y += (int)enemyba[i].vv; //Y座標を変更 if (enemyba[i].x < -enemybi.Width / 2 || enemyba[i].x - enemybi.Width / 2 > backi.Width || enemyba[i].y < -enemybi.Height / 2 || enemyba[i].y - enemybi.Height / 2 > backi.Height) { //画面端より外に出たら enemyba[i].v = 0; //敵弾を消す } } else if (enemyba[i].v >= 101) { //爆散中? enemyba[i].v = (enemyba[i].v <= 200) ? enemyba[i].v + 1 : 0; //200までカウントアップし超えたら0にする } } //《9.11 待ち時間カウント・倍率・敵機敵弾の出現間隔のダウン・画面再描画 if (waitpb > 0) { //自弾発射待ち時間がセットされていたら waitpb--; //カウントダウンする } if (waitenemy > 0) { //敵機出現待ち時間がセットされていたら waitenemy--; //カウントダウンする } if (waitenemyb > 0) { //敵弾出現待ち時間がセットされていたら waitenemyb--; //カウントダウンする } if (gamemode == 9 && player.v > 0 && player.v < 100) { //ゲームオーバーで自機縮小中? player.v--; //自機描画倍率ダウン } if (enemyint > 10) { //敵機の出現間隔が10超なら enemyint = 50 - score / 100; //スコア÷100を敵機の出現間隔から差し引く } if (enemybint > 15) { //敵弾の出現間隔が15超なら enemybint = 75 - score / 100; //スコア÷100を敵弾の出現間隔から差し引く } Invalidate(); //画面再描画を依頼 } //《10. ゲームオーバーにする》 private void GameOver() { player.v = 99; //自機の落下を開始 gamemode = 9; //ゲームオーバーにする ends.Play(); //終了音を出力 if (score > hiscore) { //ハイスコア更新? hiscore = score; //ハイスコア更新 hiscoremsg = "HiSCORE Update !"; //メッセージ } else { hiscoremsg = String.Format("HiSCORE is {0:000,000}", hiscore); //スコア文字列を作る } } //《11. コンストラクタ》 Program() { ClientSize = new Size(640, 480); //ゲーム用描画エリアの大きさを指定 DoubleBuffered = true; //ダブルバッファリングを有効化 KeyDown += new KeyEventHandler(OnKeyDown); //キー入力イベント登録 timer.Tick += new EventHandler(Play); //タイマーイベント登録 timer.Interval = 10; //タイマーインターバル(ミリ秒) begins.Load(); //開始音のロード ends.Load(); //終了音のロード mhits.Load(); //爆散音のロード hits.Load(); //再爆散音のロード } //《12. Main》 public static void Main() { Program f = new Program(); //自分のオブジェクトを生成 f.Text = "Game"; //フォーム名を設定 f.ControlBox = false; //コントロールボックスを非表示に f.FormBorderStyle = FormBorderStyle.Fixed3D; //サイズ変更を抑止 Application.Run(f); //フォームを現出 } } 追加資料.zip
ebullet.gif
burn.gif
meteo3.gif