前回のコメント

・ジェネリックの活用方法が上手く理解できました!

 何よりです。

・ゲームらしくなってきて面白かったです
 
 次回も「使えそうなテク」をご紹介しますので、お楽しみに。

・Listが使いやすくて楽しかった。

 いろいろな場面で活用できますので、是非、使いこなしてください。

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

:ハイスコア処理、敵機出現のランダム化、他

演習35 スコアアップ(再掲載+α)

・敵機に自弾が当たったら+10点としよう
・スコアはゲームオーバー画面でEnterキーが押されたらゼロにしよう
・ハイスコアを保持しておき、GAME OVER画面でハイスコアを超えたら「HiSCORE Update !」と表示しよう
・ハイスコア以下なら「HiSCORE is 000,000(ハイスコアを埋め込む)」と表示しよう

手順・仕様例

① インスタンス変数:ハイスコア用のint変数hiscoreとハイスコア表示用の文字列変数hiscoremsgを追加
② 初期化処理:スコアの変数scoreを0に初期化する処理を追加
③ 描画処理:ゲームオーバー画面ならば、hiscoremsgを25ポイントで(150,200)に描画する処理を追加
④ タイマーイベント処理:ゲームオーバー時に、ハイスコアを更新していたら、
 スコアをハイスコアに代入し、hiscoremsgに「HiSCORE Update !」を代入
⑤ タイマーイベント処理:ゲームオーバー時に、ハイスコアを更新していなかったら、
 書式「HiSCORE is 000,000」にハイスコアを埋め込んでhiscoremsgに代入
⑥ タイマーイベント処理:自弾と敵機が衝突していたら、スコア10点加算

作成例

//演習35 スコアアップ
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; //スコア
    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"); //敵機画像を読込む
    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; //自弾発射の待ち時間
    const int maxenemy = 20; //敵機の最大数
    Item[] enemya = new Item[maxenemy]; //敵機の構造体オブジェクト配列
    int waitenemy = 0; //敵機の出現待ち時間
    int enemyint = 50; //敵機の出現間隔
    string hiscoremsg = "";  //【追加】ハイスコアメッセージ
    //各データをゲーム開始時の値にする
    void initData() {
        player.i = playeri;  //自機の画像
        player.x = 320; //自機の中心X座標
        player.y = 410; //自機の中心Y座標
        player.hv = 0;  //自機の左右方向の速度
        player.v = 1;  //自機を表示
        waitpb = 0; //自弾発射の待ち時間
        waitpb = 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);
    }
    //楕円形の衝突判定(アイテムaとアイテム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.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乗を比較
    }
    //描画処理のオーバライド
    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); //自機を描画
            }
            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(hiscoremsg, 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(); //フォーム終了
        }
        //タイトル画面でEnterキーが押されていたら
        if (gamemode == 0 && e.KeyCode.ToString() == "Return") {
            initData(); //各データをゲーム開始時の値にする
            gamemode = 1; //プレイ動画に遷移
            timer.Start(); //タイマー開始                  
        }
        //ゲームオーバー画面でEnterキーが押されていたら
        else if (gamemode == 9 && e.KeyCode.ToString() == "Return") {
            initData(); //各データをゲーム開始時の値にする
            for (int i = 0; i < maxenemy; i++) { //全敵機について繰返す
                enemya[i].v = 0; //無しにする
            }
            for (int i = 0; i < maxpb; i++) { //全自弾について繰返す
                pba[i].v = 0; //無しにする
            }
            gamemode = 1; //プレイ動画に遷移
        }
        Invalidate(); //画面再描画を依頼
    }
    //タイマーイベント処理
    void Play(object o, EventArgs e) { 
        backy = (backy + 1) % backi.Height; //1枚目の背景描画開始Y座標を下げる
        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) { //敵機が存在?
                if (player.v != 0 && isHit(enemya[i], player)) { //自機が存在し衝突? 
                    player.v = 0; //自機を消す
                    gamemode = 9; //ゲームオーバーにする
                    if (score > hiscore) { //【以下追加】ハイスコア更新?
                        hiscore = score; //ハイスコア更新
                        hiscoremsg = "HiSCORE Update !"; //メッセージ
                    } else {
                        hiscoremsg = String.Format("HiSCORE is {0:000,000}", hiscore); //スコア文字列を作る
                    }
                    break; //抜ける
                }
                for (int j = 0; j < maxpb; j++) { //全自弾について繰返す
                    if (pba[j].v != 0 && isHit(enemya[i], pba[j])) { //自弾が存在し衝突?
                        enemya[i].v = 0; //この敵機を消す
                        pba[j].v = 0;  //この自弾を消す
                        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) { //自弾が非表示?
                            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; //自弾発射待ち時間をゼロにして発射可能にする
            }
        }
        if (waitenemy <= 0) { //敵機出現待ち時間がゼロ?
            for (int i = 0; i < maxenemy; i++) { //全敵機について繰返す
                if (enemya[i].v == 0) { //敵機が非表示?
                    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機出現できれば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; //自弾を消す
                }
            }
        }
        for (int i = 0; i < maxenemy; i++) { //全敵機について繰返す
            if (enemya[i].v != 0) { //敵機が存在?
                enemya[i].y += enemya[i].vv; //下へ移動
                if (enemya[i].y - enemya[i].i.Height / 2 > backi.Height) { //画面下端より下に出たら
                    enemya[i].v = 0; //敵機を消す
                }
            }
        }
        if (waitpb > 0) { //自弾発射待ち時間がセットされていたら
            waitpb--; //カウントダウンする
        }
        if (waitenemy > 0) { //敵機出現待ち時間がセットされていたら
            waitenemy--; //カウントダウンする
        }
        Invalidate(); //画面再描画を依頼
    }
    //コンストラクタ
    Program() { 
        DoubleBuffered = true; //ダブルバッファリングを有効化
        KeyDown += new KeyEventHandler(OnKeyDown); //キー入力イベント登録
        timer.Tick += new EventHandler(Play); //タイマーイベント登録
        timer.Interval = 10; //タイマーインターバル(ミリ秒)
    }
    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 乱数

・Randomクラスは乱数を提供するもので、System名前空間にあるので「using System;」があればクラス名だけで利用可能
・Randomクラスのデフォルトコンストラクタを呼び出すと、内部的にシステムクロックを用いたシード処理(乱数系の初期設定)が行われて、
 乱数機能を提供するオブジェクトが生成される
 ※ C/C++が提供する疑似乱数と同等だが、自前のシード処理は不要なので便利
・Randomクラスのインスタンスメソッド「int Next(int)」は0から引数値-1までのどれかの整数を返す
例:
 Random rnd = new Random(); //乱数用のRandomクラスのインスタンスを生成
 int hp = 10 + rnd.Next(10); //10~19の間のランダムな整数を得てHPの初期値とする
・乱数オブジェクトは使いまわしが可能なので、クラスのデータメンバとして定義・初期化しておくと良い

演習36 敵機の出現位置をランダムに

・敵機の出現時のX座標を自機直上固定からランダムに変更しよう
・左端から右端までのどこかから出現するとする

手順・仕様

① ProgramクラスのデータメンバとしてRandomクラスのインスタンスを生成
② タイマーイベント処理:敵機出現待ち時間がゼロならば敵機を出現させるところで、X座標の値を下記の式で決めるように変更
 X座標 = 敵機の幅÷2+乱数(0から背景の幅-敵機の幅まで)

作成例

//演習36 敵機の出現位置をランダムに
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; //スコア
    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"); //敵機画像を読込む
    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; //自弾発射の待ち時間
    const int maxenemy = 20; //敵機の最大数
    Item[] enemya = new Item[maxenemy]; //敵機の構造体オブジェクト配列
    int waitenemy = 0; //敵機の出現待ち時間
    int enemyint = 50; //敵機の出現間隔
    string hiscoremsg = "";  //ハイスコアメッセージ
    Random rnd = new Random(); //【追加】乱数用のRandomクラスのインスタンスを生成
    //各データをゲーム開始時の値にする
    void initData() {
        player.i = playeri;  //自機の画像
        player.x = 320; //自機の中心X座標
        player.y = 410; //自機の中心Y座標
        player.hv = 0;  //自機の左右方向の速度
        player.v = 1;  //自機を表示
        waitpb = 0; //自弾発射の待ち時間
        waitpb = 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);
    }
    //楕円形の衝突判定(アイテムaとアイテム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.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乗を比較
    }
    //描画処理のオーバライド
    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); //自機を描画
            }
            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(hiscoremsg, 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(); //フォーム終了
        }
        //タイトル画面でEnterキーが押されていたら
        if (gamemode == 0 && e.KeyCode.ToString() == "Return") {
            initData(); //各データをゲーム開始時の値にする
            gamemode = 1; //プレイ動画に遷移
            timer.Start(); //タイマー開始                  
        }
        //ゲームオーバー画面でEnterキーが押されていたら
        else if (gamemode == 9 && e.KeyCode.ToString() == "Return") {
            initData(); //各データをゲーム開始時の値にする
            for (int i = 0; i < maxenemy; i++) { //全敵機について繰返す
                enemya[i].v = 0; //無しにする
            }
            for (int i = 0; i < maxpb; i++) { //全自弾について繰返す
                pba[i].v = 0; //無しにする
            }
            gamemode = 1; //プレイ動画に遷移
        }
        Invalidate(); //画面再描画を依頼
    }
    //タイマーイベント処理
    void Play(object o, EventArgs e) { 
        backy = (backy + 1) % backi.Height; //1枚目の背景描画開始Y座標を下げる
        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) { //敵機が存在?
                if (player.v != 0 && isHit(enemya[i], player)) { //自機が存在し衝突? 
                    player.v = 0; //自機を消す
                    gamemode = 9; //ゲームオーバーにする
                    if (score > hiscore) { //ハイスコア更新?
                        hiscore = score; //ハイスコア更新
                        hiscoremsg = "HiSCORE Update !"; //メッセージ
                    } else {
                        hiscoremsg = String.Format("HiSCORE is {0:000,000}", hiscore); //スコア文字列を作る
                    }
                    break; //抜ける
                }
                for (int j = 0; j < maxpb; j++) { //全自弾について繰返す
                    if (pba[j].v != 0 && isHit(enemya[i], pba[j])) { //自弾が存在し衝突?
                        enemya[i].v = 0; //この敵機を消す
                        pba[j].v = 0;  //この自弾を消す
                        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) { //自弾が非表示?
                            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; //自弾発射待ち時間をゼロにして発射可能にする
            }
        }
        if (waitenemy <= 0) { //敵機出現待ち時間がゼロ?
            for (int i = 0; i < maxenemy; i++) { //全敵機について繰返す
                if (enemya[i].v == 0) { //敵機が非表示?
                    enemya[i].v = 1; //表示にする
                    enemya[i].i = enemyi; //画像
                    enemya[i].x = enemyi.Width / 2 + rnd.Next(backi.Width - enemyi.Width); //【変更】
                    enemya[i].y = -enemyi.Height; //Y座標は上端の直上
                    enemya[i].vv = 5; //下移動速度
                    waitenemy = enemyint; //自弾発射待ち時間をセット
                    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; //自弾を消す
                }
            }
        }
        for (int i = 0; i < maxenemy; i++) { //全敵機について繰返す
            if (enemya[i].v != 0) { //敵機が存在?
                enemya[i].y += enemya[i].vv; //下へ移動
                if (enemya[i].y - enemya[i].i.Height / 2 > backi.Height) { //画面下端より下に出たら
                    enemya[i].v = 0; //敵機を消す
                }
            }
        }
        if (waitpb > 0) { //自弾発射待ち時間がセットされていたら
            waitpb--; //カウントダウンする
        }
        if (waitenemy > 0) { //敵機出現待ち時間がセットされていたら
            waitenemy--; //カウントダウンする
        }
        Invalidate(); //画面再描画を依頼
    }
    //コンストラクタ
    Program() { 
        DoubleBuffered = true; //ダブルバッファリングを有効化
        KeyDown += new KeyEventHandler(OnKeyDown); //キー入力イベント登録
        timer.Tick += new EventHandler(Play); //タイマーイベント登録
        timer.Interval = 10; //タイマーインターバル(ミリ秒)
    }
    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); //フォームを現出
    }
}

演習37 敵弾の発射

・一定時間(25ターン)が経過する都度、出現中の敵機のいずれかが敵弾を発射するようにしよう
・最初の1発目は開始後100ターン経過してからにしよう
・よって、敵弾は同時に複数出現可能なので最大20個とする
・発射位置は敵機中央のやや上にある発射口とする
・敵弾の画像は以下を利用可能
  ebullet.gif (20x20)
・敵弾の移動と衝突判定は後回し

手順・仕様の検討

① データメンバ:敵弾の画像を読み込んでImage型変数enemybiで扱う
② データメンバ:敵弾の出現間隔をint型定数enemybintで定義し値を25とする
③ データメンバ:敵弾の出現待ち時間をint型変数waitenemybで定義し初期値を100とする
④ データメンバ:敵弾の最大数をint型定数maxenemybで定義し値を20とする
⑤ データメンバ:敵弾の構造体オブジェクト配列Item[] enemybaを敵弾の最大数分定義
⑥ 初期化処理:敵弾の出現待ち時間waitenemybを100に戻す
⑦ 描画処理:全敵弾について、敵弾の配列enemybaの表示状態vが0以外なものを描画
⑧ :(以下次回)

提出:演習36(未完成でもOk)

講義メモ

テキスト編:p.367(ジェネリックとコレクションクラス)から
ゲーム開発演習:ハイスコア処理、敵機出現のランダム化、他

p.367(ジェネリックとコレクションクラス)

・ArrayListクラス(p.177)のように、データ構造を提供するクラスがコレクションクラス。
・コレクションクラスの中には、ジェネリッククラスもあり、その代表的な存在がC
・List<T>クラスは、格納したいオブジェクトの型を型パラメータで指定して用いる。
・すると、配列と同様に利用できて、かつ配列より柔軟なデータ構造となる
・なお、ArrayListクラスでは格納により型情報が失われてしまい、キャストが必要だが、List<T>クラスでは
 不必要になり、使いやすい。
・List<T>クラスには、ArrayListクラスと同様に、格納用のAddメソッド、要素数を返すCountプロパティ、
 整列するSortメソッドなどがある。
・利用には、using System.Collections.Generic; を指定すると便利

p.368 list01.cs

//p.368 list01.cs
using System;
using System.Collections.Generic; //List<T>用
class list01 {
    public static void Main() {
        bool bEnd = false; //終了フラグをオフ
        //ジェネリックコレクションクラスList<T>のオブジェクトを生成
        List<int> mylist = new List<int>(); //型パラメータで整数型を指定
        while (true) { //無限ループ
            Console.Write("Data = ");
            string strData = Console.ReadLine();
            if (!Char.IsDigit(strData[0])) { //先頭文字が数字ではない?
                bEnd = true; //終了フラグをオン
            } else {
                mylist.Add(int.Parse(strData)); //整数化してリストに格納
            }
            if (bEnd) { //終了フラグオン?}
                break; //ループを抜ける
            }
        }
        Console.WriteLine(); //改行
        for (int i = 0; i < mylist.Count; i++) { //全要素について繰返す
            Console.WriteLine("[{0}] {1}", i, mylist[i]);
        }
        Console.WriteLine(); //改行
        mylist.Sort(); //昇順に整列
        for (int i = 0; i < mylist.Count; i++) { //全要素について繰返す
            Console.WriteLine("[{0}] {1}", i, mylist[i]);
        }
    }
}

p.369 コレクションクラスの初期化

・ジェネリッククラスであるかどうかとは無関係で、コレクションクラスの初期化にはコレクション初期化子が利用可能
・通常のクラスの場合の書式: クラス名 参照変数 = new クラス名{値,…}
・ジェネリッククラスの場合の書式: クラス名<型> 参照変数 = new クラス名<型>{値,…}

p.369 list02.cs

//p.369 list02.cs
using System;
using System.Collections.Generic;
class list02 {
    public static void Main() {
        List<string> names = new List<string> { "一郎", "二郎", "三郎" };
        foreach (string s in names) { //格納されている全要素について繰返す
            Console.WriteLine(s);
        }
    }
}

アレンジ演習:p.369 list02.cs

・ジェネリッククラスではないArrayListクラスにしてみよう

作成例

//アレンジ演習:p.369 list02.cs
using System;
using System.Collections; //ArrayList用
class list02 {
    public static void Main() {
        ArrayList names = new ArrayList{ "一郎", "二郎", "三郎" }; //コレクション初期化子
        foreach (var s in names) { //格納されている全要素について繰返す
            Console.WriteLine(s);
        }
    }
}

p.370(オブジェクト初期化子)

・テキストには説明されていないが、p.370 list03.csは、オブジェクト初期化子で初期化したオブジェクトを
 コレクション初期化子の要素としている。
・オブジェクト初期化子は「データメンバ名 = 値」の書式により、オブジェクトの初期値を設定できる仕掛け
・ジェネリッククラスであるかどうかとは無関係
・書式: クラス名 参照変数 = new クラス名{メンバ名 = 値; …};
・なお、オブジェクト初期化子として用いるデータメンバは基本的にpublicである必要がある。
・ただし、publicなプロパティを経由するなら問題ない。

p.370 list03.cs

//p.370 list03.cs
using System;
using System.Collections.Generic;
class BOOK {
    public string title; //コレクション初期化子で扱うデータメンバ
    public string author;
    public decimal price;
}
class list03 {
    public static void Main() {
        List<BOOK> mybook = new List<BOOK> { //全体はコレクション初期化子
            new BOOK {title = "我が輩は猫である",
                      author = "夏目漱石",
                      price = 1050}, //それぞれ扱うがオブジェクト初期化子
            new BOOK {title = "雲の階段",
                      author = "渡辺淳一",
                      price = 1600}, //それぞれがオブジェクト初期化子
            new BOOK {title = "こころ",
                      author = "夏目漱石",
                      price = 1200}  //それぞれがオブジェクト初期化子
        };
        Console.WriteLine("----蔵書一覧----");
        foreach (BOOK b in mybook) { //全要素について繰返す
            Console.WriteLine("{0}, {1}, {2}円", b.title, b.author, b.price);
        }
    }
}

アレンジ演習:p.370 lit03.cs


・3つあるBOOKオブジェクトを、予め、オブジェクト初期化子で初期化しておこう
・これらをコレクション初期化子でList<T>に格納しよう
・また、データメンバを全てprivateにし、これらの為のをpublicなプロパティを追加しよう
 (適当なルールを仕掛けておくと良い)

作成例

//アレンジ演習:p.370 list03.cs
using System;
using System.Collections.Generic;
class BOOK {
    string title; //コレクション初期化子で扱うデータメンバ
    string author;
    decimal price;
    public string T { set { title = value; } get { return title; } } //プロパティ
    public string A { set { author = value; } get { return author; } }
    public decimal P { set { if (value >= 0) price = value; } get { return price; } }
}
class list03 {
    public static void Main() {
        BOOK x = new BOOK {T = "我が輩は猫である", A = "夏目漱石", P = 1050}; //オブジェクト初期化子
        BOOK y = new BOOK {T = "雲の階段", A = "渡辺淳一", P = 1600}; //オブジェクト初期化子
        BOOK z = new BOOK {T = "こころ", A = "夏目漱石", P = 1200}; //オブジェクト初期化子
        List<BOOK> mybook = new List<BOOK> { x, y, z }; //全体はコレクション初期化子
        Console.WriteLine("----蔵書一覧----");
        foreach (var b in mybook) { //全要素について繰返す
            Console.WriteLine("{0}, {1}, {2}円", b.T, b.A, b.P); //プロパティ経由に
        }
    }
}

p.371 ジェネリッククラスと継承

・ジェネリッククラスの継承には、型パラメータの指定方法に伴う制限がある
・ジェネリッククラスの型パラメータに具体的な型を指定した状態で継承の基本クラスにできる
・書式: class 派生クラス : 基本ジェネリッククラス<型名> {…}
・これをクローズ構築型といい、派生クラスは通常クラスでもジェネリッククラスでも良い
・ジェネリッククラスの型パラメータが型パラメータのままの状態で継承の基本クラスにできる
・書式: class 派生クラス<型パラメータ> : 基本ジェネリッククラス<型パラメータ> {…}
・これをオープン構築型といい、派生クラスはジェネリッククラスでなければならない
・オープン構築型では型パラメータの基本クラスからの引継ぎが行われる

p.371 generic03.cs

//p.371 generic03.cs
using System;
class MyClass<T> { //ジェネリック基本クラス
    public T x;
}
class MyClass2 : MyClass<int> { //クローズ構築型を継承
    //ここに「public T x;」があり型が入って「public int x;」とみなされる
    public void show() {
        Console.WriteLine(x);
    }
}
class generic03 {
    public static void Main() {
        MyClass2 mc = new MyClass2(); //型パラメータは不要
        mc.x = 10; //クローズ構築型継承によりint型になっている
        mc.show();
    }
}

アレンジ演習:p.371 generic03.cs

・クローズ構築型では、派生クラスもジェネリッククラスで良いことを確認しよう
・MyClass2クラスを、MyClass2<T>ジェネリッククラスにして、T型のデータメンバyを追加する
・showメソッドでyも表示するようにしよう
・なお、MyClass<T>の型パラメータTは無関係になることも確認しよう

作成例

//アレンジ演習:p.371 generic03.cs
using System;
class MyClass<T> { //ジェネリック基本クラス
    public T x;
}
class MyClass2<T> : MyClass<int> { //【変更】クローズ構築型を継承するジェネリッククラス
    //ここに「public T x;」があり型が入って「public int x;」とみなされる
    public T y; //【追加】
    public void show() {
        Console.WriteLine("x = {0}, y = {1}", x, y); //【変更】
    }
}
class generic03 {
    public static void Main() {
        MyClass2<string> mc = new MyClass2<string>(); //【変更】型パラメータは不要
        mc.x = 10; //クローズ構築型継承によりint型になっている
        mc.y = "Shar"; //【追加】
        mc.show();
    }
}

p.372(オープン構築型)

・「class 派生クラス<型パラメータ> : 基本ジェネリッククラス<型パラメータ>」とすることで、型パラメータが引き継がれる

p.372 generic04.cs

//p.372 generic04.cs
using System;
class MyClass<T> { //ジェネリック基本クラス
    public T x;
}
class MyClass2<T> : MyClass<T> { //オープン構築型を継承するジェネリッククラス
    //ここに「public T x;」があるとみなされる
    public void show() {
        Console.WriteLine(x);
    }
}
class generic04 {
    public static void Main() {
        MyClass2<int> mc = new MyClass2<int>(); //型パラメータが必要
        mc.x = 2; //クローズ構築型継承によりint型になっている
        mc.show();
        MyClass2<string> mc2 = new MyClass2<string>(); //型パラメータが必要
        mc2.x = "test"; //クローズ構築型継承によりstring型になっている
        mc2.show();
    }
}

p.373(複数の型パラメータを持つジェネリッククラスの継承)

・複数の型パラメータについて、すべてに具体的な型を指定すればクローズ構築型になる
・1つでも型パラメータのままにすれば、オープン構築型になる
・この時、一部の型パラメータについて具体的な型を指定することもできる

作成例

//p.373 generic05.cs
using System;
class MyClass<T, U> { //複数の型パラメータを持つジェネリック基本クラス
    public T x;
    public U y;
}
class MyClass2<T> : MyClass<T, int> { //共有されていない型は明示:オープン構築型
    //ここに「public T x;」があるとみなされる
    //ここに「public U y;」があり、「public int y;」とみなされる
    public void show() {
        Console.WriteLine("x = {0}, y = {1}", x, y);
    }
}
class generic05 {
    public static void Main() {
        MyClass2<string> mc = new MyClass2<string>(); //型の指定が必要
        mc.x = "cat"; //生成時にstring型にしたもの
        mc.y = 20; //継承時にint型にしたもの
        mc.show();
    }
}

今週の話題

販売本数ランキング 今回トップは「スーパーマリオブラザーズ ワンダー(Switch)」GO!
PS公式ワイヤレスヘッドセット“PULSE Elite”レビュー。平面磁気ドライバー採用で粒立ちよく“聴きやすい”サウンド GO!
『ヘブンバーンズレッド』効果も長くは続かず…グリーは選択と集中が必要な時期?【ゲーム企業の決算を読む】GO!
学生からインディーゲームクリエイターになった人々は、どのように自らの行く道を決めたのか?『ElecHead』の生高橋氏らが鼎談【IDC2023】GO!
「おす」がテーマ!「第21回UE5ぷちコン」エントリー開始―サイドイベント「ぷちコン ゲームジャム」は東京・大阪・名古屋で開催 GO!
DeNA、エンジニア向け技術カンファレンス「TechCon 2024」をオンライン/オフラインで開催2/29 GO!

ヒューマンアカデミーが運営するCREST GAMING、ゲーミングデバイスブランド「STORIA」とスポンサー契約締結 GO!

販売中止となった『The Day Before』になりすますゲームがSteamに出現―パッチノートも公開し大型アップデートによる再出発装う GO!
バンダイナムコHDの株価が昨年来安値を更新…オンラインゲーム新作不調とタイトル編成の見直しなど第3四半期決算影響か GO!
バンダイナムコ、開発中だった5タイトル以上を開発中止に…オンラインゲームは可能性があるとして今後も取り組みを続ける GO!

参考:【2024年】スマホ向け無料ブラウザゲーム38選!おすすめ・新作Webゲーム紹介 GO!

前回のコメント

・開発演習でアレンジを加える様な考えなどができて楽しいです!
・とてもゲームらしくなってきて楽しいです。
・ゲームオーバ画面について理解できました

 何よりです。
 次回以降もいろいろなテクニックをご紹介しますので、いろいろと試してみてください。

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

:自機と敵機の衝突判定とゲームオーバー画面 他

演習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; //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を読込む
    Image enemyi = Image.FromFile("enemy.gif"); //敵機画像を読込む
    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; //自弾発射の待ち時間
    const int maxenemy = 20; //敵機の最大数
    Item[] enemya = new Item[maxenemy]; //敵機の構造体オブジェクト配列
    int waitenemy = 0; //敵機の出現待ち時間
    int enemyint = 50; //敵機の出現間隔
    //【以下追加】各データをゲーム開始時の値にする
    void initData() {
        player.i = playeri;  //自機の画像
        player.x = 320; //自機の中心X座標
        player.y = 410; //自機の中心Y座標
        player.hv = 0;  //自機の左右方向の速度
        player.v = 1;  //自機を表示
        waitpb = 0; //自弾発射の待ち時間
        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);
    }
    //楕円形の衝突判定(アイテムaとアイテム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.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乗を比較
    }
    //描画処理のオーバライド
    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); //自機を描画
            }
            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(); //フォーム終了
        }
        //タイトル画面でEnterキーが押されていたら
        if (gamemode == 0 && e.KeyCode.ToString() == "Return") {
            initData(); //【追加】各データをゲーム開始時の値にする
            gamemode = 1; //プレイ動画に遷移
            timer.Start(); //タイマー開始                  
        }
        //【以下追加】ゲームオーバー画面でEnterキーが押されていたら
        else if (gamemode == 9 && e.KeyCode.ToString() == "Return") {
            initData(); //各データをゲーム開始時の値にする
            for (int i = 0; i < maxenemy; i++) { //全敵機について繰返す
                enemya[i].v = 0; //無しにする
            }
            for (int i = 0; i < maxpb; i++) { //全自弾について繰返す
                pba[i].v = 0; //無しにする
            }
            gamemode = 1; //プレイ動画に遷移
        }
        Invalidate(); //画面再描画を依頼
    }
    //タイマーイベント処理
    void Play(object o, EventArgs e) { 
        backy = (backy + 1) % backi.Height; //1枚目の背景描画開始Y座標を下げる
        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) { //敵機が存在?
                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 != 0 && isHit(enemya[i], pba[j])) { //自弾が存在し衝突?
                        enemya[i].v = 0; //この敵機を消す
                        pba[j].v = 0;  //この自弾を消す
                        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) { //自弾が非表示?
                            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; //自弾発射待ち時間をゼロにして発射可能にする
            }
        }
        if (waitenemy <= 0) { //敵機出現待ち時間がゼロ?
            for (int i = 0; i < maxenemy; i++) { //全敵機について繰返す
                if (enemya[i].v == 0) { //敵機が非表示?
                    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機出現できれば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; //自弾を消す
                }
            }
        }
        for (int i = 0; i < maxenemy; i++) { //全敵機について繰返す
            if (enemya[i].v != 0) { //敵機が存在?
                enemya[i].y += enemya[i].vv; //下へ移動
                if (enemya[i].y - enemya[i].i.Height / 2 > backi.Height) { //画面下端より下に出たら
                    enemya[i].v = 0; //敵機を消す
                }
            }
        }
        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;  //【移動】自機の左右方向の速度
    }
    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); //フォームを現出
    }
}

演習35 スコアアップ

・敵機に自弾が当たったら+10点としよう
・スコアはゲームオーバー画面でEnterキーが押されたらゼロにしよう
・ハイスコアを保持しておき、GAME OVER画面でハイスコアを超えたら「HiSCORE!」と表示しよう
・ハイスコア以下なら「HiSCORE is 000,000」と表示しよう

提出:演習35 スコアアップ(演習34や未完成でもOK)

講義メモ

テキスト編:p.362 練習問題2(演算子のオーバーロードの複数化) から
ゲーム開発演習:自機と敵機の衝突判定とゲームオーバー画面 他

p.362 練習問題2(演算子のオーバーロードの複数化)

・p.358 Map05.cs:2項==演算子、2項!=演算子のオーバーロード (opov03.cs改)を用いよう
・「座標 * 実数」である2項*演算子のオーバーロードを加えよう
・X座標、Y座標のそれぞれを実数倍して整数化した座標を返すとする
・小数点以下切捨てで良い
・「座標 / 実数」である2項/演算子のオーバーロードを上記を利用して加えよう
・オーバーフローや、ゼロ除算は対処しない(例外が投げられるに任せて良い)
・「座標 - 座標」である2項-演算子のオーバーロードを他の演算子のオーバーロードを利用して加えよう
・ついでに文字列表現を返すstring ToString()メソッドのオーバーライドも記述し「(X座標, Y座標)」形式で戻すようにしよう

作成例

//p.362 練習問題2(演算子のオーバーロードの複数化)
using System;
class Map { //座標クラス
    int x; //X座標のデータメンバ 
    int y; //Y座標のデータメンバ 
    public int X { //X座標用のプロパティ
        get { return x; }
        set { x = value; }
    }
    public int Y { //Y座標用のプロパティ
        get { return y; }
        set { y = value; }
    }
    public Map() { //デフォルトコンストラクタ
        x = y = 0;
    }
    public Map(int x, int y) { //X座標、Y座標を指定するコンストラクタ
        this.x = x;
        this.y = y;
    }
    public static Map operator -(Map w) { //単項-演算子のオーバーロード
        return new Map(-w.x, -w.y); //X座標とY座標の符号を反転した結果を返す
    }
    public static double operator +(Map w) { //単項+演算子のオーバーロード
        return Math.Sqrt(w.x * w.x + w.y * w.y); //各座標の2乗の和の平方根を返す
    }
    public static Map operator +(Map a, Map b) { //2項+演算子のオーバーロード
        return new Map(a.x + b.x, a.y + b.y); //各座標の和をもつオブジェクトを返す
    }
    public static bool operator ==(Map a, Map b) { //2項==演算子のオーバーロード
        return a.x == b.x && a.y == b.y; //各座標の和をもつオブジェクトを返す
    }
    public static bool operator !=(Map a, Map b) { //2項!=演算子のオーバーロード
        return !(a == b); //2項==演算子のオーバーロードの結果を反転して返す
    }
    public static Map operator *(Map a, double d) { //2項*演算子のオーバーロード
        return new Map((int)(a.x * d), (int)(a.y * d)); //各座標の積をもつオブジェクトを返す
    }
    public static Map operator /(Map a, double d) { //2項/演算子のオーバーロード
        return a * (1 / d); //2項*演算子のオーバーロードを利用
    }
    public static Map operator -(Map a, Map b) { //2項-演算子のオーバーロード
        return a + (-b); //2項+演算子と単項-演算子のオーバーロードを利用
    }
    public override string ToString() { //文字列表現を返すオーバーライド
        return "(" + x + ", " + y + ")";
    }
}
class Map01 {
    public static void Main() {
        Map A = new Map(-3, -4); //座標Aを生成
        Map B = new Map(4, 2); //座標Bを生成
        Console.WriteLine("A{0} * 3 は {1}", A, A * 3.0);
        Console.WriteLine("B{0} / 2 は {1}", B, B / 2.0);
        Console.WriteLine("A{0} - B{1} は {2}", A, B, A - B);
    }
}

第15章 ジェネリック
p.363 ジェネリックとは

・ジェネリックは「総称」という意味で「型を変数で扱えば、型だけが異なり同一内容のクラスは1個あれば済む」ことを実現する仕組み
・よって、メソッドのオーバーロードや、引数の可変個化(p.201)などを更に進化させた位置づけ。

p.363 ジェネリッククラス

・利用時に型を指定できるクラス
・定義書式: class クラス名<型パラメータ> {…}
・C#が提供するジェネリッククラスも多数あり、それらの定義に合わせて、型パラメータは大文字1文字で表すことが多い(例:T,U,V,…)
・ジェネリッククラスを通常のクラスから利用するには、宣言と同時に型パラメータに型を指定する。
・宣言書式例: ジェネリッククラス名<型> 変数名;
・生成書式例: 変数名 = new ジェネリッククラス名<型>();
・宣言&生成: ジェネリッククラス名<型> 変数名 = new ジェネリッククラス名<型>();
・例: class Monster<T> {public T hp;} //変数hpの型は不定で宣言時に決まる
    class RPG { Monster<int> rimuru = new Monster<int>(); } //HPを整数で扱う
                Monster<double> gobuta = new Monster<double>(); } //HPを実数で扱う
・つまり、実行時に内部的に型パラメータ部分の解釈(埋め込み)が行われる

p.364 generic01.cs

//p.364 generic01.cs
using System;
class MyClass<T> { //型パラメータTを持つジェネリッククラス
    public T name; //宣言時に型が決まるデータメンバ
    public T GetVal() { //宣言時に戻り値が決まるデータメンバ
        return name; //戻り値の型は決まっていない
    }
}
class generic01 {
    public static void Main() {
        MyClass<int> mca = new MyClass<int>(); //型にintを指定してオブジェクト生成
        mca.name = 10; //よってint型になり整数の代入が可能
        Console.WriteLine(mca.GetVal()); //メソッドの戻り値型もint
        MyClass<string> mcb = new MyClass<string>(); //型にstringを指定してオブジェクト生成
        mcb.name = "猫"; //よってstring型になり文字列の代入が可能
        Console.WriteLine(mcb.GetVal()); //メソッドの戻り値型もstring型
    }
}

アレンジ演習:p.364 generic01.cs

・mca.GetVal()の戻り値型はintなので、戻り値 + 10 とすると加算になることを確認しよう
・mcb.GetVal()の戻り値型はstringなので、戻り値 + 10 とすると連結になることを確認しよう

作成例

//アレンジ演習:p.364 generic01.cs
using System;
class MyClass<T> { //型パラメータTを持つジェネリッククラス
    public T name; //宣言時に型が決まるデータメンバ
    public T GetVal() { //宣言時に戻り値が決まるデータメンバ
        return name; //戻り値の型は決まっていない
    }
}
class generic01 {
    public static void Main() {
        MyClass<int> mca = new MyClass<int>(); //型にintを指定してオブジェクト生成
        mca.name = 10; //よってint型になり整数の代入が可能
        Console.WriteLine(mca.GetVal() + 10); //メソッドの戻り値型もintなので加算
        MyClass<string> mcb = new MyClass<string>(); //型にstringを指定してオブジェクト生成
        mcb.name = "猫"; //よってstring型になり文字列の代入が可能
        Console.WriteLine(mcb.GetVal() + 10); //メソッドの戻り値型もstring型なので連結
    }
}

p.365(複数の型パラメータを持つジェネリッククラス)

・ジェネリッククラスの型パラメータの数には制限はない
・2つ以上を用いる場合はカンマで区切る
・定義書式例: class ジェネリッククラス名<型パラメータ①, 型パラメータ②> {…}
・複数の型パラメータを持つジェネリッククラスを通常のクラスから利用するには、宣言と同時に全ての型パラメータに型を指定する。
・宣言書式例: ジェネリッククラス名<型①, 型②> 変数名;
・生成書式例: 変数名 = new ジェネリッククラス名<型①, 型②>();
・宣言&生成: ジェネリッククラス名<型①, 型②> 変数名 = new ジェネリッククラス名<型①, 型②>();
・例: class Monster<T, U> {public T hp; public U mp;} //2変数の型は不定で宣言時に決まる
    class RPG { Monster<int, double> rimuru = new Monster<int, double>(); }

p.365(配列の型パラメータを持つジェネリッククラス)

・データメンバとして配列を持ち、その型をジェネリックで指定することも可能
・宣言例: 型パラメータ[] 配列名;
・生成例: 配列名 = new 型パラメータ[要素数];
・型が生成時に決まらないので、初期化はできないが、宣言+生成は可能。
 型パラメータ[] 配列名 = new 型パラメータ[要素数];
 例: T[] monsters = new T[3];

p.365 generic02.cs

//p.365 generic02.cs
using System;
class MyClass<T, U> { //2個の型パラメータTを持つジェネリッククラス
    public T[] x; //T型の配列xの宣言のみ
    public U[] y; //U型の配列yの宣言のみ
    public MyClass(int n) { //コンストラクタ(要素数)
        x = new T[n]; //要素数nのT型の配列を生成しxとする
        y = new U[n]; //要素数nのU型の配列を生成しyとする
    }
}
class generic02 {
    public static void Main() {
        int n; //配列の要素数
        Console.Write("n = ");
        string strN = Console.ReadLine();
        if (!Char.IsDigit(strN[0])) { //先頭文字が数字ではない?
            Console.WriteLine("入力が不適切です");
            return; //終了
        }
        n = int.Parse(strN);
        MyClass<int, string> mc = new MyClass<int, string>(n); //型指定で生成
        for (int i = 0; i < n; i++) { //全要素について繰返す
            Console.Write("番号--- ");
            string strNo = Console.ReadLine();
            if (!Char.IsDigit(strNo[0])) { //先頭文字が数字ではない?
                Console.WriteLine("不適切な番号です");
                break; //繰返し終了
            }
            mc.x[i] = int.Parse(strNo);
            Console.Write("氏名--- ");
            string strName = Console.ReadLine();
            mc.y[i] = strName;
        }
        Console.WriteLine(); //改行
        for (int i = 0; i < n; i++) { //全要素について繰返す
            Console.WriteLine("[{0}] {1}", mc.x[i], mc.y[i]);
        }
    }
}

【補足】ジェネリックプロパティ

・プロパティの戻り値型を型パラメータにできる
・例: public T HP { get { return hp; } set { hp = value; } }

アレンジ演習:p.364 generic01.cs

・GetValメソッドをジェネリックプロパティNAME(getのみ)にしよう

作成例

//アレンジ演習:p.364 generic01.cs
using System;
class MyClass<T> { //型パラメータTを持つジェネリッククラス
    public T name; //宣言時に型が決まるデータメンバ
    public T NAME { //宣言時に戻り値が決まるプロパティ
        get { return name; } //getのみ
    }
}
class generic01 {
    public static void Main() {
        MyClass<int> mca = new MyClass<int>(); //型にintを指定してオブジェクト生成
        mca.name = 10; //よってint型になり整数の代入が可能
        Console.WriteLine(mca.NAME); //プロパティの戻り値型もint型
        MyClass<string> mcb = new MyClass<string>(); //型にstringを指定してオブジェクト生成
        mcb.name = "猫"; //よってstring型になり文字列の代入が可能
        Console.WriteLine(mcb.NAME); //プロパティの戻り値型もstring型
    }
}

【補足】ジェネリックインデクサ

・インデクサの型を型パラメータにできる
・例: public T this[int i] { get { return a[i]; } set { a[i] = value; } }

アレンジ演習:p.365 generic02.cs

・配列xをジェネリックインデクサで扱うようににしよう

作成例

//アレンジ演習:p.365 generic02.cs
using System;
class MyClass<T, U> { //2個の型パラメータTを持つジェネリッククラス
    public T[] x; //T型の配列xの宣言のみ
    public U[] y; //U型の配列yの宣言のみ
    public MyClass(int n) { //コンストラクタ(要素数)
        x = new T[n]; //要素数nのT型の配列を生成しxとする
        y = new U[n]; //要素数nのU型の配列を生成しyとする
    }
    public T this[int i] { //ジェネリックインデクサ(配列x用)
        get { return x[i]; }
        set { x[i] = value; }
    }
}
class generic02 {
    public static void Main() {
        int n; //配列の要素数
        Console.Write("n = ");
        string strN = Console.ReadLine();
        if (!Char.IsDigit(strN[0])) { //先頭文字が数字ではない?
            Console.WriteLine("入力が不適切です");
            return; //終了
        }
        n = int.Parse(strN);
        MyClass<int, string> mc = new MyClass<int, string>(n); //型指定で生成
        for (int i = 0; i < n; i++) { //全要素について繰返す
            Console.Write("番号--- ");
            string strNo = Console.ReadLine();
            if (!Char.IsDigit(strNo[0])) { //先頭文字が数字ではない?
                Console.WriteLine("不適切な番号です");
                break; //繰返し終了
            }
            mc[i] = int.Parse(strNo); //インデクサ経由で代入
            Console.Write("氏名--- ");
            string strName = Console.ReadLine();
            mc.y[i] = strName;
        }
        Console.WriteLine(); //改行
        for (int i = 0; i < n; i++) { //全要素について繰返す
            Console.WriteLine("[{0}] {1}", mc[i], mc.y[i]); //インデクサ経由で参照
        }
    }
}

今週の話題

販売本数ランキング 今回トップは「ペルソナ3 リロード(PS5)」GO!
ドット絵によるアニメーション制作の味方『Aseprite』がSteamなどで50%オフの1025円に(2/14まで)。入門~本格的なゲーム制作まで幅広く活用可能 GO!
『GTA5』累計販売本数が1億9,500万本を突破―『GTA6』トレイラーや12月のアップデートでプレイヤーが増加傾向など決算報告で明らかに GO!
ゲームオーディオのグローバルリーダーAudiokinetic主催「Wwise Tour 2023 Tokyo」イベントレポート&『Hi-Fi RUSH』を手掛けたTango Gameworksインタビュー GO!
ネクソン、過去最高売上も業績予想には届かず―『THE FINALS』は“課金額が予想を上回る”好調スタート、日本では『ブルアカ』が成長 GO!
『The Elder Scrolls II: Daggerfall』有志Unity移植版の制作者が独自作品の開発に着手 GO!
KADOKAWA、アクワイアを100%子会社化―フロム・ソフトウェア、スパイク・チュンソフトとの連携を推進 GO!
ニンテンドースイッチの国内販売台数がニンテンドーDSを超える…気になる次世代機については言及避ける GO!
『ファンパレ』『FF7EC』『プロセカ』のエンジニア・クリエイターが登壇―サイバーエージェント主催の技術カンファレンス3/7開催 GO!
【決算】コロプラの第1四半期は2割の減収、4億1400万の営業赤字で着地 GO!
Unity Japan、大前広樹氏が社長を退任し、再びゲーム開発へ―新社長には産業営業本部長 松本靖麿氏 GO!

『原神』チートツール開発者に対し「可能な限り最高規模の」代償を、カナダで5万ドル規模以上の訴訟が進行中か―海外メディア報道 GO!