前回のコメント

・ゲームが面白くなってきて楽しかったです
・表示倍率の変化を実装できて面白かったです。

 何よりです。

・計算など頭を使う事が多く苦戦しましたが他は上手く理解できました!

 何よりです!
 応用では「調べて&考えて&試して」を繰り返してやっと数行のプログラムになることが少なくありません。
 全てを暗記する必要はありません。テキストや講義メモで思い出せるようにしておくと便利です。

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

:画像の拡大と縮小、自機の落下、敵機の落下

演習38 敵弾の移動・消去・自機との衝突判定(再掲載)

・敵弾は敵機の砲口からその時点の自機中央に向かって斜めに移動するようにしよう
・移動速度は敵機速度の1.5倍とする
・敵弾が画面端より外に出たら消そう
・全敵弾と自機との衝突判定を行い、衝突していたらゲームオーバーに遷移しよう
・衝突していたら、その敵弾も消そう

手順と仕様(再掲載)

① データメンバ:敵機移動速度をint型定数enemyvvで定義し値を5とする
② タイマーイベント処理:全敵機について繰返す中で自機が存在し衝突したら、ゲームオーバーのメソッドGameOver()を
 呼ぶようにする
 これに伴い、繰返しを抜けるbreakを除く各処理をメソッドGameOver()の中に移動する
③ タイマーイベント処理:全敵弾について敵弾と自機が存在し衝突していたら、メソッドGameOver()を呼び、
 その敵弾を消してから繰返しを抜ける処理を追加
④ タイマーイベント処理:敵機を出現させるときに与える下移動速度を敵機移動速度定数enemyvvで与えるように変更
⑤ タイマーイベント処理:敵弾を出現させるときに自機と敵弾の座標で角度を得て、X移動量とY移動量を設定する処理を追加
  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移動量
⑥ タイマーイベント処理:全敵弾について、敵弾が存在したらXY座標にXY移動量を加算し、画面端より外に出たら敵弾を消す処理を追加

作成例=途中段階

//演習38 敵弾の移動・消去・自機との衝突判定(途中段階)
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"); //敵機画像を読込む
    Image enemybi = Image.FromFile("ebullet.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クラスのインスタンスを生成
    const int enemybint = 75; //敵弾の出現間隔
    int waitenemyb = 200; //敵弾の出現待ち時間
    const int maxenemyb = 20; //敵弾の最大数
    Item[] enemyba = new Item[maxenemyb]; //敵弾の構造体配列
    const int enemyvv = 5; //【追加】敵機移動速度
    //各データをゲーム開始時の値にする
    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; //スコアクリア
        waitenemyb = 200; //敵弾の出現待ち時間
    }
    //中央座標を用いる画像描画処理
    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); //敵機を描画
                }
            }
            foreach (var enemyb in enemyba) { //全敵弾について繰返す
                if (enemyb.v == 1) { //敵弾がある?
                    DrawItem(e, enemyb); //敵弾を描画
                }
            }
        }
        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; //無しにする
            }
            for (int i = 0; i < maxenemyb; i++) { //全敵弾について繰返す
                enemyba[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); //スコア文字列を作る
                    //    }
                    GameOver(); //【追加】ゲームオーバー
                    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; //次の敵機へ進む
                    }
                }
            }
        }
        for (int i = 0; i < maxenemyb; i++) { //【以下追加】全敵弾について繰返す
            if (enemyba[i].v != 0 && player.v != 0 && isHit(enemyba[i], player)) { //敵弾と自機が衝突?
                GameOver(); //ゲームオーバー
                enemyba[i].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 = enemyi.Width / 2 + rnd.Next(backi.Width - enemyi.Width);
                    enemya[i].y = -enemyi.Height; //Y座標は上端の直上
                    enemya[i].vv = enemyvv; //【変更】下移動速度
                    waitenemy = enemyint; //敵機出現待ち時間をセット
                    break; //1機出現できればOK
                }
            }
        }
        if (waitenemyb <= 0) { //敵弾出現待ち時間がゼロ?
            int enemyi = -1; //発射敵機を-1(未確定)にしておく
            for (int i = 0; i < maxenemy; i++) { //全敵機について繰返す
                if (enemya[i].v == 1 && 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座標は発射敵機中央の上
                        //@@@@@次回ここから
                        waitenemyb = enemybint; //敵弾発射待ち時間をセット
                        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--; //カウントダウンする
        }
        if (waitenemyb > 0) { //敵弾出現待ち時間がセットされていたら
            waitenemyb--; //カウントダウンする
        }
        Invalidate(); //画面再描画を依頼
    }
    //【以下追加】ゲームオーバー処理
    private void GameOver() {
        player.v = 0; //自機を消す
        gamemode = 9; //ゲームオーバーにする
        if (score > hiscore) { //ハイスコア更新?
            hiscore = score; //ハイスコア更新
            hiscoremsg = "HiSCORE Update !"; //メッセージ
        } else {
            hiscoremsg = String.Format("HiSCORE is {0:000,000}", hiscore); //スコア文字列を作る
        }
    }
    //コンストラクタ
    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); //フォームを現出
    }
}

作成例(完全版)

//演習38 敵弾の移動・消去・自機との衝突判定
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"); //敵機画像を読込む
    Image enemybi = Image.FromFile("ebullet.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クラスのインスタンスを生成
    const int enemybint = 75; //敵弾の出現間隔
    int waitenemyb = 200; //敵弾の出現待ち時間
    const int maxenemyb = 20; //敵弾の最大数
    Item[] enemyba = new Item[maxenemyb]; //敵弾の構造体配列
    const int enemyvv = 5; //【追加】敵機移動速度
    //各データをゲーム開始時の値にする
    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; //スコアクリア
        waitenemyb = 200; //敵弾の出現待ち時間
    }
    //中央座標を用いる画像描画処理
    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); //敵機を描画
                }
            }
            foreach (var enemyb in enemyba) { //全敵弾について繰返す
                if (enemyb.v == 1) { //敵弾がある?
                    DrawItem(e, enemyb); //敵弾を描画
                }
            }
        }
        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; //無しにする
            }
            for (int i = 0; i < maxenemyb; i++) { //全敵弾について繰返す
                enemyba[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); //スコア文字列を作る
                    //    }
                    GameOver(); //【追加】ゲームオーバー
                    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; //次の敵機へ進む
                    }
                }
            }
        }
        for (int i = 0; i < maxenemyb; i++) { //【以下追加】全敵弾について繰返す
            if (enemyba[i].v != 0 && player.v != 0 && isHit(enemyba[i], player)) { //敵弾と自機が衝突?
                GameOver(); //ゲームオーバー
                enemyba[i].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 = enemyi.Width / 2 + rnd.Next(backi.Width - enemyi.Width);
                    enemya[i].y = -enemyi.Height; //Y座標は上端の直上
                    enemya[i].vv = enemyvv; //【変更】下移動速度
                    waitenemy = enemyint; //敵機出現待ち時間をセット
                    break; //1機出現できればOK
                }
            }
        }
        if (waitenemyb <= 0) { //敵弾出現待ち時間がゼロ?
            int enemyi = -1; //発射敵機を-1(未確定)にしておく
            for (int i = 0; i < maxenemy; i++) { //全敵機について繰返す
                if (enemya[i].v == 1 && 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座標は発射敵機中央の上
                        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
                    }
                }
            }
        }
        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; //敵機を消す
                }
            }
        }
        for (int i = 0; i < maxenemyb; i++) { //【以下追加】全敵弾について繰返す
            if (enemyba[i].v != 0) { //敵弾が存在?
                enemyba[i].x += enemyba[i].hv; //X移動量をX座標に加算
                enemyba[i].y += enemyba[i].vv; //Y移動量を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; //上下左右から外に出たら消す
                }
            }
        }
        if (waitpb > 0) { //自弾発射待ち時間がセットされていたら
            waitpb--; //カウントダウンする
        }
        if (waitenemy > 0) { //敵機出現待ち時間がセットされていたら
            waitenemy--; //カウントダウンする
        }
        if (waitenemyb > 0) { //敵弾出現待ち時間がセットされていたら
            waitenemyb--; //カウントダウンする
        }
        Invalidate(); //画面再描画を依頼
    }
    //【以下追加】ゲームオーバー処理
    private void GameOver() {
        player.v = 0; //自機を消す
        gamemode = 9; //ゲームオーバーにする
        if (score > hiscore) { //ハイスコア更新?
            hiscore = score; //ハイスコア更新
            hiscoremsg = "HiSCORE Update !"; //メッセージ
        } else {
            hiscoremsg = String.Format("HiSCORE is {0:000,000}", hiscore); //スコア文字列を作る
        }
    }
    //コンストラクタ
    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); //フォームを現出
    }
}

テーマ33 画像の拡大・縮小表示

・DrawImage()メソッドのオーバーロードで、描画幅と高さを指定できる
・書式:DrawImage(Image image, int x, int y, int width, int height)
・これを用いて、画像の拡大・縮小表示が可能
・中心に向かって縮小したい場合は、x、yを拡大縮小率に合わせて調整すれば良い
・画像を中心座標で扱っている場合:
 x ← 中心X座標 - 画像幅 × 拡大縮小率% ÷ 100 ÷ 2
 y ← 中心Y座標 - 画像高 × 拡大縮小率% ÷ 100 ÷ 2
 width  ← 画像幅 × 拡大縮小率% ÷ 100
 height ← 画像高 × 拡大縮小率% ÷ 100

演習39 自機の落下

・敵機が当たった自機はすぐに消えるのではなく、ゲームオーバー画面においてその位置でだんだん縮小するようにしよう(落下に見える)
・縮小中は移動、自弾の発射はできない。また、衝突判定もしない。
・DrawItem(e, player)メソッドのオーバーロードとしてDrawItem(e, player, 縮小率%)を作って用いると良い
・DrawItem(e, player, 縮小率%)では、縮小率%が100の場合にDrawItem(e, player)を呼ぶようにすることで効率低下を防ごう
・自機の表示(player.v)を0/1ではなく0~100(%)にしよう

手順

① データの初期化処理:自機の表示(player.v)の初期値は100(%)にする
② 画像描画処理のオーバーロードの追加:DrawItem(PaintEventArgs e, Item it, int 倍率)
 ・倍率が100なら、元のDrawItem(e, it)を呼ぶ
 ・でなければ、倍率で左上座標、幅、高さを計算して、DrawImage(it, X, Y, 幅, 高さ)を呼ぶ
  X ← it.x - it.i.Width  × 倍率 ÷ 100 ÷ 2
  Y ← it.y - it.i.Height × 倍率 ÷ 100 ÷ 2
  幅   ← it.i.Width  × 倍率 ÷ 100
  高さ ← it.i.Height × 倍率 ÷ 100
③ 描画処理:自機の描画を②を用いるように変更
④ タイマーイベント処理:敵機と自機の衝突判定処理は自機の表示(倍率)が100の時のみにする
⑤ タイマーイベント処理:敵弾と自機の衝突判定処理は自機の表示(倍率)が100の時のみにする
⑥ タイマーイベント処理:ゲームオーバーで自機の表示(倍率)が0超100未満なら、自機の表示(倍率)をカウントダウンする処理を追加
⑦ ゲームオーバー処理:自機の表示(倍率)を0から99に変更

作成例

//演習39 自機の落下
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"); //敵機画像を読込む
    Image enemybi = Image.FromFile("ebullet.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クラスのインスタンスを生成
    const int enemybint = 75; //敵弾の出現間隔
    int waitenemyb = 200; //敵弾の出現待ち時間
    const int maxenemyb = 20; //敵弾の最大数
    Item[] enemyba = new Item[maxenemyb]; //敵弾の構造体配列
    const int enemyvv = 5; //敵機移動速度
    //各データをゲーム開始時の値にする
    void initData() {
        player.i = playeri;  //自機の画像
        player.x = 320; //自機の中心X座標
        player.y = 410; //自機の中心Y座標
        player.hv = 0;  //自機の左右方向の速度
        player.v = 100;  //【変更】自機を100%表示
        waitpb = 0; //自弾発射の待ち時間
        waitpb = 0; //自弾発射の待ち時間
        score = 0; //スコアクリア
        waitenemyb = 200; //敵弾の出現待ち時間
    }
    //中央座標を用いる画像描画処理
    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);
    }
    //【以下追加】中央座標を用いる画像描画処理(倍率指定)
    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);
        }
    }
    //楕円形の衝突判定(アイテム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, player.v); //【変更】自機を倍率描画
            }
            foreach (var pb in pba) { //全自弾について繰返す
                if (pb.v == 1) { //自弾がある?
                    DrawItem(e, pb); //自弾を描画
                }
            }
            foreach (var enemy in enemya) { //全敵機について繰返す
                if (enemy.v == 1) { //敵機がある?
                    DrawItem(e, enemy); //敵機を描画
                }
            }
            foreach (var enemyb in enemyba) { //全敵弾について繰返す
                if (enemyb.v == 1) { //敵弾がある?
                    DrawItem(e, enemyb); //敵弾を描画
                }
            }
        }
        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; //無しにする
            }
            for (int i = 0; i < maxenemyb; i++) { //全敵弾について繰返す
                enemyba[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 == 100 && isHit(enemya[i], player)) { //【変更】自機が100%存在し衝突?
                    GameOver(); //ゲームオーバー
                    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; //次の敵機へ進む
                    }
                }
            }
        }
        for (int i = 0; i < maxenemyb; i++) { //全敵弾について繰返す
            if (enemyba[i].v != 0 && player.v == 100 && isHit(enemyba[i], player)) { //【変更】敵弾と自機が衝突?
                GameOver(); //ゲームオーバー
                enemyba[i].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 = enemyi.Width / 2 + rnd.Next(backi.Width - enemyi.Width);
                    enemya[i].y = -enemyi.Height; //Y座標は上端の直上
                    enemya[i].vv = enemyvv; //下移動速度
                    waitenemy = enemyint; //敵機出現待ち時間をセット
                    break; //1機出現できればOK
                }
            }
        }
        if (waitenemyb <= 0) { //敵弾出現待ち時間がゼロ?
            int enemyi = -1; //発射敵機を-1(未確定)にしておく
            for (int i = 0; i < maxenemy; i++) { //全敵機について繰返す
                if (enemya[i].v == 1 && 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座標は発射敵機中央の上
                        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
                    }
                }
            }
        }
        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; //敵機を消す
                }
            }
        }
        for (int i = 0; i < maxenemyb; i++) { //全敵弾について繰返す
            if (enemyba[i].v != 0) { //敵弾が存在?
                enemyba[i].x += enemyba[i].hv; //X移動量をX座標に加算
                enemyba[i].y += enemyba[i].vv; //Y移動量を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; //上下左右から外に出たら消す
                }
            }
        }
        if (waitpb > 0) { //自弾発射待ち時間がセットされていたら
            waitpb--; //カウントダウンする
        }
        if (waitenemy > 0) { //敵機出現待ち時間がセットされていたら
            waitenemy--; //カウントダウンする
        }
        if (waitenemyb > 0) { //敵弾出現待ち時間がセットされていたら
            waitenemyb--; //カウントダウンする
        }
        if (gamemode == 9 && player.v > 0 && player.v < 100) { //【以下追加】ゲームオーバーで自機が縮小中?
            player.v--; //表示倍率をダウンする
        }
        Invalidate(); //画面再描画を依頼
    }
    //ゲームオーバー処理
    private void GameOver() {
        player.v = 99; //【変更】自機の落下(縮小)を開始
        gamemode = 9; //ゲームオーバーにする
        if (score > hiscore) { //ハイスコア更新?
            hiscore = score; //ハイスコア更新
            hiscoremsg = "HiSCORE Update !"; //メッセージ
        } else {
            hiscoremsg = String.Format("HiSCORE is {0:000,000}", hiscore); //スコア文字列を作る
        }
    }
    //コンストラクタ
    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); //フォームを現出
    }
}

演習40 敵機の落下

・自弾が当たった敵機もすぐに消えるのではなく、落下(縮小)しながら移動するようにしよう
・落下(縮小)中の敵機は敵弾の発射はできない。また、衝突判定もしない。
・敵機の表示(enemya[i].v)も0/1ではなく0~100(%)の表示倍率にしよう

手順

① 描画処理:敵機の描画を表示倍率が0ではないときに、倍率付きで行うようにしよう
② タイマーイベント処理:敵機と自機の衝突判定処理は敵機の表示(倍率)も100の時のみにする。
  また、ゲームオーバー処理後、その敵機の表示(倍率)を99にする処理を追加
③ タイマーイベント処理:敵機と自弾の衝突判定処理は敵機の表示(倍率)も100の時のみにする。
  また、その敵機の表示(倍率)を0から99に変更
④ タイマーイベント処理:敵機の出現待ち時間がゼロになった時の敵機の表示を1ではなく100にする
⑤ タイマーイベント処理:敵弾の発射敵機を決める処理は100%表示の敵機のみからにする
⑥ タイマーイベント処理:敵機の移動処理の前に、その敵機の表示(倍率)が99以下(落下中)であればデクリメントする

提出:演習39または40(未完成でも可)

講義メモ

テキスト編:p.386「練習問題」から
ゲーム開発演習:画像の拡大と縮小、自機の落下、敵機の落下

p.386 練習問題 ヒント

・p.368 list01.csを基にすると良い
・整列は不要になる
・受験者数、合計点数、平均値の表示を追加する
・入力終了条件を「整数以外」から「負の数」に変更する

作成例

//p.386 練習問題
using System;
using System.Collections.Generic; //Listクラス用
class prac15 {
    public static void Main() {
        int num = 0; //【追加】受験者数
        int sum = 0; //【追加】合計点
        List<int> mylist = new List<int>(); //型パラメータを与えてListオブジェクトを生成
        while (true) { //無限ループ
            Console.Write("Data = ");
            int strData = int.Parse(Console.ReadLine()); //【変更】
            if (strData < 0) { //負の数なら
                break; //繰返しを抜ける
            }
            mylist.Add(strData); //【変更】リストに追加
            num++; //【追加】受験者数カウント
            sum += strData; //【追加】合計点に加算
        }
        Console.WriteLine("受験者数:{0}", num); //【追加】
        Console.WriteLine("合計点数:{0}", sum); //【追加】
        Console.WriteLine("平均値:{0}", (double)sum / num); //【追加】
    }
}

第16章 名前空間、プリプロセッサ、属性など

p.387 名前空間

・ここまで作成したプログラムで「using」で指定してきた「System」「System.Collection.Generic」などは名前空間(Name Space)
・名前空間とは、定義の領域を定める仕掛けで、名前空間が異なれば同一名の定義が可能になり、名前の管理の効率が上がる
・異なる名前空間にあるものを用いるには「名前空間名.」を前置すれば良い

p.388 名前空間の定義

・プログラマが自前の名前空間を定義することもできる
・定義書式: namespace 名前空間名 {…}
・なお、名前空間が示されていない場合、無名の名前空間にあると見なされる

p.388 namespace01.cs

//p.388 namespace01.cs
using System;
namespace Cat { //名前空間「Cat」の定義
    class Animal { //外部からはCat.Animalでアクセスできるクラス
        public string name; //外部からはCat.Animalのnameでアクセスできるメンバ
        public void show(){ //外部からはCat.Animalのshow()でアクセスできるメソッド
            Console.WriteLine("猫の名前は{0}です", name);
        }
    }
}
namespace Dog {//名前空間「Dog」の定義
    class Animal { //外部からはDog.Animalでアクセスできるクラス
        public string name; //外部からはDog.Animalのnameでアクセスできるメンバ
        public void show(){ //外部からはDog.Animalのshow()でアクセスできるメソッド
            Console.WriteLine("犬の名前は{0}です", name);
        }
    }
}
class namespace01 { //無名の名前空間にあるクラス
    public static void Main() {
        Cat.Animal cat = new Cat.Animal(); //名前空間を指定してインスタンスを生成
        cat.name = "タマ";
        Dog.Animal dog = new Dog.Animal(); //名前空間を指定してインスタンスを生成
        dog.name = "ポチ";
        cat.show();
        dog.show();
    }
}

p.390 usingディレクティブ

・ソースファイルの冒頭または名前空間の冒頭に「using 名前空間;」を指定すると、その範囲内で「名前空間::」を省略できる
・ソースファイルの冒頭に指定すると全体で有効になるが、名前管理のトラブルに注意。
 ※プロジェクトによっては自前の名前空間をusingディレクティブに指定することを禁止する場合がある
・名前空間の冒頭に指定するとその名前空間の中でのみ有効になる
・定義書式: using 名前空間名;
・別名を付ける為に利用ことも可能
・定義書式: using 別名 = 名前空間名;

]
p.390 namespace02.cs

//p.390 namespace02.cs
using Cat; //usingディレクティブで「Cat.」の省略が可能
using D = Dog; //usingディレクティブで「Dog.」の別名「D.」を定義
namespace Cat { //名前空間「Cat」の定義
    using System;
    class Animal { //外部からはAnimalでアクセスできるクラス
        public string name; //外部からはAnimalのnameでアクセスできるメンバ
        public void show(){ //外部からはAnimalのshow()でアクセスできるメソッド
            Console.WriteLine("猫の名前は{0}です", name);
        }
    }
}
namespace Dog { //名前空間「Dog」の定義
    using System;
    class Animal { //外部からはD.Animalでアクセスできるクラス
        public string name; //外部からはD.Animalのnameでアクセスできるメンバ
        public void show(){ //外部からはD.Animalのshow()でアクセスできるメソッド
            Console.WriteLine("犬の名前は{0}です", name);
        }
    }
}
namespace MyNamespace {
    class Namespace01 {
        public static void Main() {
            Animal cat = new Animal(); //usingによりCat.Animalが用いられる
            cat.name = "タマ";
            D.Animal dog = new D.Animal(); //usingによりDog.Animalが用いられる
            dog.name = "ポチ";
            cat.show();
            dog.show();
        }
    }
}

p.392 名前空間のあいまいさ

・usingディレクティブの指定により、定義が重複し対象が特定できない場合、コンパイルエラーになる
・なお、usingディレクティブで別名を指定しても、元の名前を利用できる。
 ※ 可読性が低下するので、指定したら用いることで統一すると良い

p.393 名前空間のネスト

・名前空間の中に名前空間を定義できる
・外部から内側の名前空間にあるものにアクセスするには「外側の名前空間名.内側の名前空間名.」を用いる
・なお、名前空間の定義を複数個所で行うこと=分割定義が可能。
・テキストの「p.394 namespace03.cs」では、Cat名前空間内にCatクラスを定義しており、この重複は可能
 ※ プロジェクトによっては禁止される場合があるので、ここではHouseCatクラスに変更している

p.394 namespace03.cs

//p.394 namespace03.cs
namespace Animal { //外側の名前空間定義
    using System; //Animal名前空間内では「System.」は省略可
    namespace Mammal { //内側の名前空間定義(外からはAnimal.Mammal)
        namespace Cat { //さらに内側の名前空間定義(外からはAnimal.Mammal.Cat)
            class HouseCat { //外からはAnimal.Mammal.Cat.HouseCatクラス
                public string name;
                public void show() {
                    Console.WriteLine("猫の名前は{0}です", name);
                }
            }
        }
    }
    
}
namespace MyNamespace { //名前空間定義
    class Namespace03 {
        public static void Main() {
            //ネストした名前空間にあるクラスのインスタンスを生成
            Animal.Mammal.Cat.HouseCat mycat = new Animal.Mammal.Cat.HouseCat();
            mycat.name = "マイケル";
            mycat.show();
            //ネストしていない名前空間にあるクラスのインスタンスを生成
            Animal.Dog mydog = new Animal.Dog();
            mydog.name = "ポチ";
            mydog.show();
        }
    }
}
namespace Animal { //Animal名前空間の定義の続き
    class Dog { //外からはAnimal.Dogクラス
        public string name;
        public void show() {
            System.Console.WriteLine("犬の名前は{0}です", name);
        }
    }
}

p.395(名前空間のネストの定義の省略記法)

・内側の名前空間は「namespace 名前空間名.名前空間名」と定義することもできる
・例: namespace A { namespace B {…} } ⇒ namespace A.B {…}
・ただし、外側や中間の名前空間に属するものがある場合は、別途、定義する必要がある
・例: namespace A{●; namespace B{■}} ⇒ namespace A{●} namespace A.B{■}
・テキストの「p.396 namespace04.cs」では、Dog名前空間内に「using System;」を指定しているのに
 「System.Console.WriteLine」としており、この「System.」は不要

p.396 namespace04.cs

//p.396 namespace04.cs
namespace Animal { //名前空間定義
    using System; //この名前空間定義内で有効(別途定義のネストの中では無効)
    class Dog {
        public string name;
        public void show() {
            Console.WriteLine("犬の名前は{0}です", name); //「System.」は不要
        }
    }
}
namespace Animal.Mammal.Cat { //ネストした名前空間定義
    using System; // Animal名前空間でusing System;とあるがここでもう一度
    class cat {
        public string name;
        public void show() {
            Console.WriteLine("猫の名前は{0}です", name); //「System.」は不要
        }
    }
}
namespace MyNamespace {
    class Namespace03 {
        public static void Main() {
            //ネストした名前空間にあるクラスのインスタンスを生成
            Animal.Mammal.Cat.cat mycat = new Animal.Mammal.Cat.cat();
            mycat.name = "マイケル";
            mycat.show();
            //ネストしていない名前空間にあるクラスのインスタンスを生成
            Animal.Dog mydog = new Animal.Dog();
            mydog.name = "ポチ";
            mydog.show();
        }
    }
}

p.398 プリプロセッサ

・C/C++から引き継がれた機能で、コンパイラ(ビルド)における前処理=プリプロセスを指示できる機能
・プリプロセッサの指示によってソースの書き換えを行い、書き換え結果がコンパイルされる
※ C/C++とは異なり、#defineによるマクロ定義や、#ifdefによるインクルードガードなどはC#では禁止
・C#では、プリプロセッサの指示として「#define シンボル」「#if シンボル」「#endif」「#undef シンボル」などが指定可能
・「#define シンボル」でシンボルを指定しておくと、「#if シンボル」と「#endif」で挟んだ行がコンパイル対象となる
・こうしておいてから「#define シンボル」を削除することで、上記をコンパイル対象から除外できる
・よって、コメントアウトによるコンパイル対象からの除外よりも可読性が高く、ミスを防止しやすい

p.399(プリプロセッサと論理演算子)

・「#if シンボル」において複数のシンボルを論理演算子を用いて指定できる
・「#if (シンボル① && シンボル②)」とすれば、両方のシンボルが指定時に対象になる
・「#if (シンボル① || シンボル②)」とすれば、どちらかのシンボルが指定時に対象になる
・また「#if (!シンボル)」とすれば、シンボルが指定されていない時に対象になる
・なお、シンボルは「#undef シンボル」の指定により無効化できる

p.400 preprocess01.cs

//p.400 preprocess01.cs
#define TEST //シンボルTESTの定義
using System;
class preprocess01
{
    public static void Main()
    {
#if TEST //シンボルTESTがあれば有効
        Console.WriteLine("テストです");
#endif //範囲はここまで
#if (TEST && TEST2) //シンボルTESTとTEST2が共にあれば有効
        Console.WriteLine("ここは、TESTとTEST2が定義されていないとコンパイルされません");
#endif //範囲はここまで
#if (TEST || TEST2) //シンボルTESTまたはTEST2があれば有効
        Console.WriteLine("ここは、TESTかTEST2が定義されていればコンパイルされます");
#endif //範囲はここまで
#if TEST2 //シンボルTEST2があれば有効
        Console.WriteLine("ここは、TEST2が定義されているとコンパイルされます");
#endif //範囲はここまで
    }
}

アレンジ演習:p.400 preprocess01.cs

・「#if シンボル」のネストにより記述を簡略化できるか試してみよう

作成例

//アレンジ演習:p.400 preprocess01.cs
#define TEST //シンボルTESTの定義
using System;
class preprocess01
{
    public static void Main()
    {
#if TEST //シンボルTESTがあれば有効
        Console.WriteLine("テストです");
    #if TEST2 //しかもシンボルTEST2があれば有効(ネスト)
        Console.WriteLine("ここは、TESTとTEST2が定義されていないとコンパイルされません");
    #endif //範囲はここまで
#endif //範囲はここまで
#if (TEST || TEST2) //シンボルTESTまたはTEST2があれば有効
        Console.WriteLine("ここは、TESTかTEST2が定義されていればコンパイルされます");
    #if (!TEST) //シンボルTESTがなければ有効(ネスト)
        Console.WriteLine("ここは、TEST2が定義されているとコンパイルされます");
    #endif //範囲はここまで
#endif //範囲はここまで
    }
}

p.401(その他のプリプロセッサディレクティブ)

・if文におけるelseにあたる「#else」ディレクティブを「#if シンボル」と「#endif」の間に1つだけ指定できる
・if文におけるelse ifにあたる「#elif シンボル」ディレクティブを「#if シンボル」と「#endif」の間または、
 「#if シンボル」と「#else」の値に複数指定できる

p.402 Conditional属性

・メソッドの直前に記述することで、そのメソッドをコンパイル対象にするかどうかの条件を記述できる仕掛けがConditional属性
・Conditional属性をつけたメソッドを条件付きメソッドという
・書式: [Conditional("シンボル")] ※必ず独立した1行にすること
・利用には「using System.Diagnostics;」が必要
・条件付きメソッドに指定したシンボルが無い場合、条件付きメソッドを呼び出している部分もコンパイル対象から自動的に除外される
・そのため、条件付きメソッドは戻り値型がvoidで、オーバーライドしていないこと
 ※ なお、Conditional属性は複数行定義可能で「または」の関係になる

p.402 conditional01.cs

//p.402 conditional01.cs
#define TEST //シンボルTESTの定義
using System;
using System.Diagnostics; //Conditional属性用
class MyClass {
    public string name;
    [Conditional("TEST")] //TESTシンボルがあればshowメソッドは有効
    public void show() { //条件付きメソッド
        Console.WriteLine(name);
    }
}
class conditional01 {
    public static void Main() {
        MyClass mc = new MyClass(); 
        mc.name = "マイケル";
        mc.show(); //条件付きメソッドを呼び出す(TESTシンボルがないと消える)
    }
}

今週の話題

今回トップも「マリオvs.ドンキーコング(Switch)」GO!
『ダンクロ』不調で3期連続赤字のKLab、EAとの協業で逆転狙う【ゲーム企業の決算を読む】GO!
TGS2024でインディーゲームを無料出展―「Selected Indie 80」出展タイトルを募集中【TGS2024】GO!
『ラグナロク』好調も先細りが懸念材料?ガンホーは北米エリア開拓がカギ【ゲーム企業の決算を読む】GO!
約900名人員削減のSIE、新卒採用ページでメッセージを発信―新卒社員には影響なし GO!

Respawn開発の「スター・ウォーズ」FPSも開発中止に…EA、全従業員の約5%を削減へ GO!
Epic Gamesがハッキングされた疑い―犯人グループが約200GBの内部情報をおさえたと主張 GO!
『ティアキン』違法DLを助長した―ニンテンドースイッチエミュレーター「Yuzu」を任天堂が提訴 GO!

前回のコメント

・敵弾の処理を考えるのが楽しかったです。

 何よりです。

・敵弾の移動処理がとても難しかったです…
 理解できるように頑張ります!

 今回用いるアルゴリズムの中では最難度だと思います。
 とはいえ、定番でもありますので「こういうものなんだ」という理解でOKです。

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

:敵弾の発射、移動、衝突判定 他

演習37 敵弾の発射(修正)

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

手順と仕様

① データメンバ:敵弾の画像を読み込んでImage型変数enemybiで扱う
② データメンバ:敵弾の出現間隔をint型定数enemybintで定義し値を75とする
③ データメンバ:敵弾の出現待ち時間をint型変数waitenemybで定義し初期値を200とする
④ データメンバ:敵弾の最大数をint型定数maxenemybで定義し値を20とする
⑤ データメンバ:敵弾の構造体Item型配列enemybaを敵弾の最大数の分定義
⑥ 初期化処理:敵弾の出現待ち時間waitenemybを200に戻す
⑦ 描画処理:プレイ画面かゲームオーバー画面なら、全敵弾について、敵弾の配列enemybaの表示状態vが0以外なものを描画
⑧ キー入力時処理:ゲームオーバー画面でEnterキーが押されていたら、全敵弾の表示状態vを0にする
⑨ タイマーイベント処理:敵弾出現待ち時間がゼロであれば:
 ・発射敵機を-1(未確定)にしておく
 ・全敵機の中で出現中でY座標が画面上半分にいるものがあれば、発射敵機とする
 ・発射敵機確定していたら、全敵弾の中で未出現のものがあれば、出現させ、X座標は発射敵機と同じに、Y座標は発射敵機-15とする
  また、敵弾出現待ち時間を敵弾の出現間隔にする
⑩ タイマーイベント処理:敵弾出現待ち時間がセットされていたらカウントダウンする

作成例

//演習37 敵弾の発射
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"); //敵機画像を読込む
    Image enemybi = Image.FromFile("ebullet.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クラスのインスタンスを生成
    const int enemybint = 75; //【追加】敵弾の出現間隔
    int waitenemyb = 200; //【追加】敵弾の出現待ち時間
    const int maxenemyb = 20; //【追加】敵弾の最大数
    Item[] enemyba = new Item[maxenemyb]; //【追加】敵弾の構造体配列
    //各データをゲーム開始時の値にする
    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; //スコアクリア
        waitenemyb = 200; //【追加】敵弾の出現待ち時間
    }
    //中央座標を用いる画像描画処理
    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); //敵機を描画
                }
            }
            foreach (var enemyb in enemyba) { //【以下追加】全敵弾について繰返す
                if (enemyb.v == 1) { //敵弾がある?
                    DrawItem(e, enemyb); //敵弾を描画
                }
            }
        }
        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; //無しにする
            }
            for (int i = 0; i < maxenemyb; i++) { //【以下追加】全敵弾について繰返す
                enemyba[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
                }
            }
        }
        if (waitenemyb <= 0) { //【以下追加】敵弾出現待ち時間がゼロ?
            int enemyi = -1; //発射敵機を-1(未確定)にしておく
            for (int i = 0; i < maxenemy; i++) { //全敵機について繰返す
                if (enemya[i].v == 1 && 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座標は発射敵機中央の上
                        waitenemyb = enemybint; //敵弾発射待ち時間をセット
                        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--; //カウントダウンする
        }
        if (waitenemyb > 0) { //【以下追加】敵弾出現待ち時間がセットされていたら
            waitenemyb--; //カウントダウンする
        }
        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); //フォームを現出
    }
}

テーマ32 2座標の角度から移動量を得る

・座標(x0,y0)への(x1,y1)からの角度はMath.Atan2関数にそれぞれの差を渡すことでラジアン値として得られる。
 double rad = Math.Atan2(y0 - y1, x0 - x1);
・すると、物体の移動先の座標は、Cos(角度)、Sin(角度)、移動速度で得ることができる
 x座標移動量=Math.Cos(rad)×移動速度
 y座標移動量=Math.Sin(rad)×移動速度

演習38 敵弾の移動・消去・自機との衝突判定

・敵弾は敵機の砲口からその時点の自機中央に向かって斜めに移動するようにしよう
・移動速度は敵機速度の1.5倍とする
・敵弾が画面端より外に出たら消そう
・全敵弾と自機との衝突判定を行い、衝突していたらゲームオーバーに遷移しよう
・衝突していたら、その敵弾も消そう

手順と仕様

① データメンバ:敵機移動速度をint型定数enemyvvで定義し値を5とする
② タイマーイベント処理:全敵機について繰返す中で自機が存在し衝突したら、ゲームオーバーのメソッドGameOver()を
 呼ぶようにする
 これに伴い、繰返しを抜けるbreakを除く各処理をメソッドGameOver()の中に移動する
③ タイマーイベント処理:全敵弾について敵弾と自機が存在し衝突していたら、メソッドGameOver()を呼び、
 その敵弾を消してから繰返しを抜ける処理を追加
④ タイマーイベント処理:敵機を出現させるときに与える下移動速度を敵機移動速度定数enemyvvで与えるように変更
⑤ タイマーイベント処理:敵弾を出現させるときに自機と敵弾の座標で角度を得て、X移動量とY移動量を設定する処理を追加
⑥ タイマーイベント処理:全敵弾について、敵弾が存在したらXY座標にXY移動量を加算し、画面端より外に出たら敵弾を消す処理を追加

提出:演習37または38(未完成でもOK)

講義メモ

テキスト編:p.374「型パラメータの制約」から
ゲーム開発演習:敵弾の発射、移動、衝突判定 他

p.374 型パラメータの制約

・ジェネリッククラスの定義において「どんな型が指定されてもOK」ということは考えづらい
・データメンバと単純なプロパティのみを持つクラスであれば可能だが、なんらかのメソッドを持つ場合、指定できる型を制限する必要が出てくる
・そこで、型パラメータに指定できるクラスを制約として明示できる
・書式: class ジェネリッククラス名<型パラメータ> where 型パラメータ : クラス名 {…}
・こうすると、指定したクラスおよびその派生クラスのみを型パラメータに指定できる
・例: class SlimeHouse<T> where T : Slime {…}
 ⇒ SlimeHouse<Slime> sh1; SlimeHouse<HoimiSlime> sh2; などが可能
・よって、指定したクラスに定義されているメソッドをジェネリッククラス内で利用可能になる
・また、「where T : new()」を指定すると、指定したクラスにデフォルトコンストラクタがあることを制約にできる
・よって、指定したクラスのインスタンスをジェネリッククラス内でnew演算子で生成可能になる
・上記は併用が可能で、クラス名の後ろに「, new()」を指定すると良い

p.375 generic06

//p.375 generic06
using System;
class MyClass {
    public int x;
    public string name;
    public MyClass() { //デフォルトコンストラクタ
        x = 0;
        name = "";
    }
}
class MyClass2<T> where T : MyClass, new() { //クラス名とデフォルトコンストラクタ制約
    T p = new T(); //new()制約があるのでデフォルトコンストラクタの呼出が可能
    public void show() {
        //クラス名制約があるのでMyClassのpublicデータメンバが利用可能
        Console.WriteLine("x = {0}, p.name = {1}", p.x, p.name); 
    }
    public void setxname(int n, string str) {
        p.x = n; //同上
        p.name = str; //同上
    }
}
class generic06 {
    public static void Main() {
        MyClass2<MyClass> mc2 = new MyClass2<MyClass>(); //制約に合致する
        mc2.setxname(100, "abc");
        mc2.show();
    }
}

アレンジ演習:p.375 generic06

・MyClassの派生クラスとしてYourClassを追記してコンストラクタ等を記述する
・MyClass2<YourClass> mc2 = new MyClass2<YourClass>(); とできることを確認しよう

作成例

//アレンジ演習:p.375 generic06
using System;
class MyClass {
    public int x;
    public string name;
    public MyClass() { //デフォルトコンストラクタ
        x = 0;
        name = "";
    }
}
class YourClass : MyClass { //【以下追加】派生クラス
    public YourClass() { //デフォルトコンストラクタ
        x = 10;
        name = "None";
    }
}
class MyClass2<T> where T : MyClass, new() { //クラス名とデフォルトコンストラクタ制約
    T p = new T(); //new()制約があるのでデフォルトコンストラクタの呼出が可能
    public void show() {
        //クラス名制約があるのでMyClassのpublicデータメンバが利用可能
        Console.WriteLine("x = {0}, p.name = {1}", p.x, p.name); 
    }
    public void setxname(int n, string str) {
        p.x = n; //同上
        p.name = str; //同上
    }
}
class generic06 {
    public static void Main() {
        MyClass2<YourClass> mc2 = new MyClass2<YourClass>(); //【変更】制約に合致する
        mc2.show();
        mc2.setxname(100, "abc");
        mc2.show();
    }
}

p.374 型パラメータの制約(続き)

・インターフェイスを制約として明示できる
・書式: class ジェネリッククラス名<型パラメータ> where 型パラメータ : インターフェイス名 {…}
・こうすると、指定したインターフェイスを実装したクラスのみを型パラメータに指定できる
・例: class Flyers<T> where T : Flyable {…}
・よって、指定したインターフェイスに定義されている抽象メソッド等をジェネリッククラス内で利用可能になる
・また、値型のみを可能にしたい場合は「where T : struct」、参照型のみを可能にしたい場合は「where T : class」と指定できる

アレンジ演習:p.375 generic06

・抽象メソッドshow()を持つインタフェースMyIFを追記しよう
・MyClass、YourClassでMyIFを実装しよう
・MyClass2ジェネリッククラスの制約をインタフェースMyIFにして、メソッドshow()が利用可能なことを確認しよう
・mainメソッドの内容を上記に合わせて改変しよう

作成例

//アレンジ演習:p.375 generic06
using System;
interface MyIF { //インタフェース
    void show(); //抽象メソッド
}
class MyClass : MyIF { //インタフェースを実装
    public int x;
    public string name;
    public MyClass() { //デフォルトコンストラクタ
        x = 0;
        name = "";
    }
    public void show() { //抽象メソッドをオーバーライド
        Console.WriteLine("MyClass");
    }
}
class YourClass : MyIF { //インタフェースを実装
    public void show() { //抽象メソッドをオーバーライド
        Console.WriteLine("YourClass");
    }
}
class MyClass2<T> where T : MyIF, new() { //インタフェースとデフォルトコンストラクタ制約
    T p = new T(); //new()制約があるのでデフォルトコンストラクタの呼出が可能
    public void showme() {
        //インタフェース名制約があるのでMyIFの抽象メソッドが利用可能
        p.show();
    }
}
class generic06 {
    public static void Main() {
        MyClass2<MyClass> mc1 = new MyClass2<MyClass>(); //制約に合致する
        mc1.showme();
        MyClass2<YourClass> mc2 = new MyClass2<YourClass>(); //制約に合致する
        mc2.showme();
    }
}

p.377 ジェネリックメソッド

・クラスと同様に、メソッド単体でも型パラメータの指定が可能で、これをジェネリックメソッドという
・書式: アクセス修飾子 戻り値型 メソッド名<型パラメータ,…> (引数リスト) {…}
・ジェネリックメソッドがあるクラスはジェネリッククラスでなくても良い
・ジェネリックメソッドは静的メソッドでもインスタンスメソッドでも良い

p.377 generic07.cs

// p.377 generic07.cs
using System;
class MyClass { //通常クラス
    static object obj;
    object ob;
    public static void myset<T>(T x) { //ジェネリック静的メソッド
        obj = (T)x; //型がTなのでキャスト可能
    }
    public static void show() {
        Console.WriteLine(obj.ToString());
    }
    public void myset2<T>(T x) { //ジェネリックインスタンスメソッド
        ob = (T)x; //型がTなのでキャスト可能
    }
    public void show2() {
        Console.WriteLine(ob.ToString());
    }
}
class generic07 {
    public static void Main() {
        MyClass.myset<int>(12); //型指定で静的メソッドを呼び出す
        MyClass.show();
        MyClass.myset<string>("abc"); //型指定で静的メソッドを呼び出す
        MyClass.show();
        MyClass mc2 = new MyClass();
        mc2.myset2<int>(100); //型指定でインスタンスメソッドを呼び出す
        mc2.show2();
    }
}

【補足】ジェネリックメソッドの戻り値型と型パラメータ

・ジェネリックメソッドでは引数型や内容の記述に加えて、戻り値型の中でも型パラメータが指定可能
・例: List<T> makelist<T>(T x) {…} //T型の引数を受け取り、T型用のリストを生成して格納、T型用のリスト返す

p.378(ジェネリックメソッドと制約)

・ジェネリッククラスの同様に制約をジェネリックメソッドにも指定可能
・書式: アクセス修飾子 戻り値型 メソッド名<型パラメータ,…> (引数リスト) where 型パラメータ : 制約 {…}

アレンジ演習:p.377 generic07.cs

・ジェネリックメソッドにstruct制約やclass制約をつけて動作を確認しよう

作成例

//アレンジ演習: p.377 generic07.cs
using System;
class MyClass { //通常クラス
    static object obj;
    object ob;
    public static void myset<T>(T x) where T : class { //ジェネリック静的メソッド+制約
        obj = (T)x; //型がTなのでキャスト可能
    }
    public static void show() {
        Console.WriteLine(obj.ToString());
    }
    public void myset2<T>(T x) { //ジェネリックインスタンスメソッド
        ob = (T)x; //型がTなのでキャスト可能
    }
    public void show2() {
        Console.WriteLine(ob.ToString());
    }
}
class generic07 {
    public static void Main() {
        //MyClass.myset<int>(12); //型指定で静的メソッドを呼び出す ※制約でエラー
        //MyClass.show();
        MyClass.myset<string>("abc"); //型指定で静的メソッドを呼び出す
        MyClass.show();
        MyClass mc2 = new MyClass();
        mc2.myset2<int>(100); //型指定でインスタンスメソッドを呼び出す
        mc2.show2();
    }
}

p.379(ジェネリックメソッドとオーバーロード)

・型パラメータはシグニチャの一部になるので、型パラメータの数が異なるジェネリックメソッドによるオーバーロードが可能
・なお、戻り値は通常のメソッドと同様にシグニチャには含まれない

p.379 ジェネリックインタフェース

・クラスと同様にインターフェイスにおいても型パラメータの指定が可能で、これをジェネリックインターフェイスという
・書式: interface インターフェイス名<型パラメータ,…> {…}
・例:
 interface FlyAble<T> { //「飛べる」ことを示すインターフェイス
  T howtofly(); //飛び方をT型で返す抽象メソッド
 }
 class Dragon : FlyAble<string> {…} //飛び方は文字列で返すことを示す

p.381 anonymous02.csについて

・p.380下3行において「anonymous01.cs を自動実装プロパティを用いて書き換えたのが anonymous02.cs」というような説明があるが誤り。
・自動実装プロパティは、主に簡易クラス機能(ここでは割愛)などに用いられるもので、プロパティ名が変数名になる
・よって、anonymous02.csの「int z;」は不要
・また「public int x {get; set;}」は「public int x;」とすればよく、自動実装プロパティを用いる意味はない

p.380 匿名型

・varキーワードを用いて匿名型の参照変数を定義し、オブジェクト初期化子で生成したオブジェクトへの参照を代入できる
・こうすると名前のないクラスが用意され、その中にオブジェクト初期化子で指定したプロパティが匿名型で定義され、
 匿名のデータメンバに初期値が与えられる。
・書式: var 参照変数 = new { プロパティ = 値; … }
・なお、プロパティの型を明示的に指定することや、値を変更することはできない
・データメンバの値は、参照変数.プロパティ で利用可能
・よって、これは複数の定数をグループ化する場合などに用いる
 例: var player1 = new {name = "Shar", age = 24, x = 10, y = 20}

p.381 anonymous03.cs

//p.381 anonymous03.cs
using System;
class anonymous03 {
    public static void Main() {
        var mc = new { x = 10 }; //匿名型のプロパティmc.xに10を与える
        Console.WriteLine("x = {0}", mc.x); //定数のように利用可能
    }
}

p.382(複数の匿名型)

・複数の匿名型では、プロパティの数、型、順序が同じ場合に同じ型とみなされる
・同じ型であれば、複数の匿名型の間で代入が可能
・なお、var型の変数に匿名型の参照変数を代入すると、var型の変数の型が匿名型の型になる

アレンジ演習:p.381 anonymous03.cs

・同じ匿名型の参照変数m2をxの値20で生成する
・mcの値とm2の値を交換してみよう

作成例

//アレンジ演習:p.381 anonymous03.cs
using System;
class anonymous03 {
    public static void Main() {
        var mc = new { x = 10 }; //匿名型のプロパティmc.xに10を与える
        var m2 = new { x = 20 }; //同じ匿名型のプロパティm2.xに20を与える
        var mw = mc; //var型の変数にmcを退避
        mc = m2; //mcとm2を交換
        m2 = mc; //同上
        Console.WriteLine("x = {0}", mc.x); //20になっている
    }
}

p.382「ジェネリックの共変性・反変性」は割愛します

今週の話題

今回トップは「マリオvs.ドンキーコング(Switch)」GO!
Aiming、コロプラとの資本業務提携で16.4億円の資金調達―新規オンラインゲーム共同開発など目指す GO!
「INDIE Live Expo」2024年5月25日に開催決定ー出展エントリーは3月12日まで GO!
「PlayStation VR2(PS VR2)」が2024年内にPCでも利用可能になるとソニーがひっそり発表 GO!

「子供がプロeスポーツチームにスカウトされた」との問い合わせ相次ぐ…チームは“詐欺行為”の可能性があると注意喚起 GO!