講義メモ:ゲーム開発演習

:敵機の出現と移動、衝突判定、ゲームオーバー など

演習32 敵機の出現と移動

・一定間隔ごとに敵機が自機の直上の上端範囲外に出現し、下方向に移動するようにしよう
・敵機の最大数は20機とし、これ以上は出現させない
・出現間隔はタイマーインターバルの50倍とする
・下方向移動速は5とする
・自弾と同様に画面下部から先で見えなくなったら出現前の状態に戻すこと
・敵機の画像は下記を利用可能
  enemy.gif

手順と仕様

① 敵機画像の読み込みを追記。Image enemyi 等
② 敵機の最大数を初期化。int maxenemy 等
③ 敵機の構造体オブジェクト配列を生成 Item[] enemya 等
④ 敵機の出現待ち時間を初期化 waitenemy 等
⑤ 敵機の出現間隔を初期化 enemyint 等
⑥ 描画処理:全敵機について、表示状態であるものを表示する処理
⑦ タイマーイベント処理:敵機出現待ち時間がゼロであれば、非表示である敵機要素を表示状態にし、
 自機と同じX座標、上端の直上のY座標などを設定
 また、敵機の出現待ち時間に敵機の出現間隔をセット
⑧ タイマーイベント処理:全敵機について、下へ移動し、下端より下に出たら非表示にする
⑨ タイマーイベント処理:敵機出現待ち時間がセットされていたらカウントダウン

作成例

//演習32 敵機の出現と移動
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"); //【追加】敵機画像の読込
    Font font1 = new Font("メイリオ", 20, FontStyle.Bold); //フォントを生成(メイリオ20P太字)
    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 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);
    }
    //描画処理
    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 if (gamemode == 1) { //プレイ画面
            string s = String.Format("SCORE:{0:000,000}", score); //スコア文字列を編集
            e.Graphics.DrawString(s, font1, brushs, 400, 10); //スコア文字列の描画
            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); //敵機を表示
                }
            }
        }
    }
    //キー入力時処理
    void OnKeyDown(object o, KeyEventArgs e) { //キーボードが押された時に呼ばれるメソッド
        if(e.KeyCode.ToString() == "Escape") { //Escキーが押されたら
            Close(); //フォームアプリケーション終了
        }
        if(gamemode == 0 && e.KeyCode.ToString() == "Return") { //スタート画面でEnterキーが押されたら
            gamemode = 1; //プレイ画面に切り替える
            timer.Start(); //タイマー開始
            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; //右に移動中
            }
        }
        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); //インスタンスを画面に出す
    }
}

テーマ30 衝突判定

・矩形(四角形)の衝突判定は重なっている部分の有無でできるが、これを矩形以外に用いると、角にあたる部分で過度の判定になってしまう
・この場合は、楕円形の衝突判定を用いると良い
・楕円形の衝突判定では、双方の中心間の距離を得て、これが双方の内径の和を下回ったら衝突しているとみなす
・中心間の距離は三平方の定理で求められるが、比較するだけなら、平方根を得る必要はなく、
 「X座標の差の2乗+Y座標の差の2乗 < 内径の和の2乗」と比較すれば良い

・内径は画像の高さと画像の横幅の小さい方の半分とする

private static bool isHit(int x1, int y1, Imagei i1, int x2, int y2, Image i2) {
  int xc1 = x1 + i1.Height / 2, xc2 = x2 + i2.Height / 2; //中心X座標を得る
  int yc1 = y1 + i1.Width  / 2; yc2 = y2 + i2.Width  / 2; //中心Y座標を得る
  int dest =  (xc1 - xc2) * (xc1 - xc2) + (yc1 - yc2) * (yc1 - yc2); //距離の自乗和
  double r1 = (i1.Width < i1.Height) ? i1.Width / 2.0 : i1.Height / 2.0; //画像①の内径
  double r2 = (i2.Width < i2.Height) ? i2.Width / 2.0 : i2.Height / 2.0; //画像②の内径
  return dest < (r1 + r2) * (r1 + r2); //距離の自乗が内径の和の自乗より小さいかを返す
}

演習33 自弾と敵機の衝突判定

・タイマーイベント処理において、出現している全ての自弾と敵機の組合せについて、楕円形の衝突判定を行う
・衝突していたら、その自弾と敵機の両方を非表示にする
・タイマーで呼ばれる処理において、出現中の全ての敵機と全ての自弾の組み合わせにおいて、衝突判定を行い、
 衝突していたら、その敵機と自弾の両方を非表示にしよう

手順と仕様

① 衝突判定用のメソッド bool isHit(Item a, Item b)を作る
 ・aとbのX座標差の2乗とY座標差の2乗の和を得て距離の2乗とする
 ・aの幅が高さ未満なら幅の半分を、でなければ高さの半分を内径aとする
 ・bの幅が高さ未満なら幅の半分を、でなければ高さの半分を内径bとする
 ・距離の2乗が、内径aと内径bの和の2乗未満ならtrue、でなければfalseを返す
② タイマーイベント処理において存在中の全敵機について以下を繰り返す
 ・存在中の全自弾について、その敵機と衝突中かチェックし、衝突中なら双方を非表示にする
 ・そして、次の敵機に進む

作成例

//演習33 自弾と敵機の衝突判定
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 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 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 if (gamemode == 1) { //プレイ画面
            string s = String.Format("SCORE:{0:000,000}", score); //スコア文字列を編集
            e.Graphics.DrawString(s, font1, brushs, 400, 10); //スコア文字列の描画
            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); //敵機を表示
                }
            }
        }
    }
    //キー入力時処理
    void OnKeyDown(object o, KeyEventArgs e) { //キーボードが押された時に呼ばれるメソッド
        if(e.KeyCode.ToString() == "Escape") { //Escキーが押されたら
            Close(); //フォームアプリケーション終了
        }
        if(gamemode == 0 && e.KeyCode.ToString() == "Return") { //スタート画面でEnterキーが押されたら
            gamemode = 1; //プレイ画面に切り替える
            timer.Start(); //タイマー開始
            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]が存在?
                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 (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); //インスタンスを画面に出す
    }
}

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です