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

:自弾の複数化、敵機の出現

提出フォロー:演習29 自弾のアニメーション

・自弾を左右反転した画像と交互に表示することで、回転しているように見せよう
・画像は下記を利用可能
 https://rundog.org/si/bullet2.gif bullet2.gif 20x20

手順:

① 自弾画像2としてbullet2.gifを読込む
② OnPaintメソッドにおいて、自弾を描画を描画する前に、描画画像の入れ替えを行う
  条件演算子を用いると便利:
  pb.i = (pb.i == bulleti) ? bullet2i : bulleti; //画像を交互変更

作成例

//演習29 自弾のアニメーション
using System; //汎用的に利用
using System.Windows.Forms; //フォームアプリケーションに必須
using System.Drawing; //Size、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:プレイ画面,9:終了画面)
    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を読込む
    Pen pen1 = new Pen(Color.Red, 2); //赤色太さ2のペン
    Brush brush1 = new SolidBrush(Color.FromArgb(63, 255, 0, 0)); //透明赤いブラシ
    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); //黄色のブラシ
    Timer timer = new Timer(); //タイマーの生成
    int backy = 0; //1枚目の背景描画開始Y座標
    Item player; //自機の構造体オブジェクト
    Item pb; //自弾の構造体オブジェクト
    //中央座標を用いる画像描画処理
    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) { 
        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 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 = playerl; break; //左寄画像にする
                case 1:  player.i = playerr; break; //右寄画像にする
            }
            DrawItem(e, player); //自機を描画
            if (pb.v == 1) { //自弾がある?
                pb.i = (pb.i == bulleti) ? bullet2i : bulleti; //【追加】画像を交互変更
                DrawItem(e, pb); //自弾を描画
            }
        }

    }
    //キー入力時処理
    void OnKeyDown(object o, KeyEventArgs e) { 
        if (e.KeyCode.ToString() == "Escape") { //Escキーが押されていたら
            Close(); //フォーム終了
        }
        //タイトル画面でEnterキーが押されていたら
        if (gamemode == 0 && e.KeyCode.ToString() == "Return") { 
            gamemode = 1; //プレイ動画に遷移
            timer.Start(); //タイマー開始
        }
        Invalidate(); //画面再描画を依頼
    }
    //タイマーイベント処理
    void Play(object o, EventArgs e) { 
        backy = (backy + 1) % backi.Height; //1枚目の背景描画開始Y座標を下げる
        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 (pb.v == 0) { //自弾が非表示?
                pb.v = 1; //表示にする
                pb.i = bulleti; //画像
                pb.x = player.x; //X座標は自機と同じ
                pb.y = player.y - player.i.Height / 2 - pb.i.Height / 2; //Y座標は自機の直上
                pb.vv = -5; //上移動速度
            }
        }
        if (pb.v != 0) { //自弾が存在?
            pb.y += pb.vv; //上へ移動
            if (pb.y + pb.i.Height / 2 < 0) { //画面上端より上に出たら
                pb.v = 0; //自弾を消す
            }
        }
        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;  //自機の左右方向の速度
    }
    public static void Main() {
        Program f = new Program(); //自分のオブジェクトを生成
        f.Size = new Size(660, 520); //フォームのサイズを設定
        f.Text = "Game"; //フォーム名を設定
        f.ControlBox = false; //コントロールボックスを非表示に
        f.FormBorderStyle = FormBorderStyle.Fixed3D; //サイズ変更を抑止
        Application.Run(f); //フォームを現出
    }
}

演習30 自弾の複数化

・自弾を10個にしよう
・スペースキーを押すと順に発射され、画面上に10個ある時は、押されても無視する
・位置情報などは弾ごとに持ち、画面上部から消えたら、再度発射可能とする

手順:

① 自弾の最大数を定数maxpbで保持し初期値を10とする
② 自弾の構造体オブジェクトを、要素数maxpbの配列pbaにする
③ OnPaintメソッドにおいて、自弾を描画を描画する処理を、
 「自弾があれば描画する」を全自弾について繰返すように変更
 ※画像の交互変換はPlayメソッドに移そう
④ Playメソッドにおいて、スペースキーが押されているときの処理を、
 「非表示の自弾が見つかったら発射する」を全自弾について繰返すように変更
 なお、1つ見つかったら抜けること
⑤ また「自弾があれば画像を切り替え、上へ移動し、上端より上になったら非表示にする」を
 全自弾について繰返すように変更

作成例

//演習30 自弾の複数化
using System; //汎用的に利用
using System.Windows.Forms; //フォームアプリケーションに必須
using System.Drawing; //Size、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:プレイ画面,9:終了画面)
    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を読込む
    Pen pen1 = new Pen(Color.Red, 2); //赤色太さ2のペン
    Brush brush1 = new SolidBrush(Color.FromArgb(63, 255, 0, 0)); //透明赤いブラシ
    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); //黄色のブラシ
    Timer timer = new Timer(); //タイマーの生成
    int backy = 0; //1枚目の背景描画開始Y座標
    Item player; //自機の構造体オブジェクト
    const int maxpb = 10; //【追加】自弾の最大数
    Item[] pba = new Item[maxpb]; //【変更】自弾の構造体オブジェクト配列
    //中央座標を用いる画像描画処理
    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) { 
        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 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 = playerl; break; //左寄画像にする
                case 1:  player.i = playerr; break; //右寄画像にする
            }
            DrawItem(e, player); //自機を描画
            foreach (var pb in pba) { //【追加】全自弾について繰返す
                if (pb.v == 1) { //自弾がある?
                    DrawItem(e, pb); //自弾を描画
                }
            }
        }

    }
    //キー入力時処理
    void OnKeyDown(object o, KeyEventArgs e) { 
        if (e.KeyCode.ToString() == "Escape") { //Escキーが押されていたら
            Close(); //フォーム終了
        }
        //タイトル画面でEnterキーが押されていたら
        if (gamemode == 0 && e.KeyCode.ToString() == "Return") { 
            gamemode = 1; //プレイ動画に遷移
            timer.Start(); //タイマー開始
        }
        Invalidate(); //画面再描画を依頼
    }
    //タイマーイベント処理
    void Play(object o, EventArgs e) { 
        backy = (backy + 1) % backi.Height; //1枚目の背景描画開始Y座標を下げる
        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) { //スペースキーが押されている?
            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; //【変更】上移動速度
                    break; //【追加】1発発射できればOK
                }
            }
        }
        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; //【変更】自弾を消す
                }
            }
        }
        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;  //自機の左右方向の速度
    }
    public static void Main() {
        Program f = new Program(); //自分のオブジェクトを生成
        f.Size = new Size(660, 520); //フォームのサイズを設定
        f.Text = "Game"; //フォーム名を設定
        f.ControlBox = false; //コントロールボックスを非表示に
        f.FormBorderStyle = FormBorderStyle.Fixed3D; //サイズ変更を抑止
        Application.Run(f); //フォームを現出
    }
}

テーマ29 冷却期間

・主にアクション系ゲームにおいて、プレイヤーの挙動に合わせて複数の物体を動かす場合「ボタンが押されている間、連続して処理」とすると、1挙動で必要以上の数の物体に影響してしまうことが多い。
・これは、プログラムの動作速度が1挙動より大幅に早いことが原因。
・特に、連射を可能にしている場合、このことへの配慮が必要
・これを防ぐには、1物体の動作後から一定時間の動作不能時間=冷却時間を持てば良い
・動作直後にタイミングを計る変数をセットしてカウントダウンを行い、ゼロになったら、次の動作を可能とすると良い
・ボタンによる連射も可能にしたい場合は、ボタンの状況を監視して手が離れたら、タイミングをはかる変数をゼロにすれば良い

演習31 自弾発射の改良

・スペースキーを押しっぱなしの場合は、次の弾の発射まで適当な冷却時間を置くことにしよう
・スペースキーが押されてなかったら、冷却時間なしで次の弾の発射を可能にしよう

手順:

① 冷却時間を定数coldで保持し初期値を10とする
② 自弾発射待ちを変数waitpbで表し初期値を0とする
③ スペースキーが押されたとき、waitpbが0なら自弾を発射する
④ 自弾を発射したら、waitpbにcoldを代入して冷却時間としよう
⑤ playメソッドにおいて、waitpbが0以上なら0までカウントダウンしよう
⑥ playメソッドにおいて、スペースキーが押されていなければ、waitpbを0にしよう

作成例

//演習31 自弾発射の改良
using System; //汎用的に利用
using System.Windows.Forms; //フォームアプリケーションに必須
using System.Drawing; //Size、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:プレイ画面,9:終了画面)
    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を読込む
    Pen pen1 = new Pen(Color.Red, 2); //赤色太さ2のペン
    Brush brush1 = new SolidBrush(Color.FromArgb(63, 255, 0, 0)); //透明赤いブラシ
    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); //黄色のブラシ
    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; //【追加】自弾発射の待ち時間
    //中央座標を用いる画像描画処理
    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) { 
        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 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 = playerl; break; //左寄画像にする
                case 1:  player.i = playerr; break; //右寄画像にする
            }
            DrawItem(e, player); //自機を描画
            foreach (var pb in pba) { //全自弾について繰返す
                if (pb.v == 1) { //自弾がある?
                    DrawItem(e, pb); //自弾を描画
                }
            }
        }

    }
    //キー入力時処理
    void OnKeyDown(object o, KeyEventArgs e) { 
        if (e.KeyCode.ToString() == "Escape") { //Escキーが押されていたら
            Close(); //フォーム終了
        }
        //タイトル画面でEnterキーが押されていたら
        if (gamemode == 0 && e.KeyCode.ToString() == "Return") { 
            gamemode = 1; //プレイ動画に遷移
            timer.Start(); //タイマー開始
        }
        Invalidate(); //画面再描画を依頼
    }
    //タイマーイベント処理
    void Play(object o, EventArgs e) { 
        backy = (backy + 1) % backi.Height; //1枚目の背景描画開始Y座標を下げる
        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) { //自弾が非表示?
                        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; //自弾発射待ち時間をゼロにして発射可能にする
        }
        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; //自弾を消す
                }
            }
        }
        if (waitpb > 0) { //【以下追加】自弾発射待ち時間がセットされていたら
            waitpb--; //カウントダウンする
        }
        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;  //自機の左右方向の速度
    }
    public static void Main() {
        Program f = new Program(); //自分のオブジェクトを生成
        f.Size = new Size(660, 520); //フォームのサイズを設定
        f.Text = "Game"; //フォーム名を設定
        f.ControlBox = false; //コントロールボックスを非表示に
        f.FormBorderStyle = FormBorderStyle.Fixed3D; //サイズ変更を抑止
        Application.Run(f); //フォームを現出
    }
}

提出:演習31 自弾発射の改良(未完成でもOK)

コメントを残す

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