前回のコメント

・finallyブロックの例外での活用方法が上手く理解できて楽しかったです!
・弾の発射について理解できてよかったです
・操作性を上げるための工夫が学べてよかった。

 何よりです。
 操作性を上げるための工夫はいろいろありますが、対象の特性に合わせて使い分けられるように、
 複数の手段を知っておく=引き出しを多めに持っておくと良いですね。

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

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

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

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

手順:

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

作成例

//演習29 自弾のアニメーション
using System; //汎用的に利用
using System.Windows.Forms; //フォームアプリケーションに必須
using System.Drawing; //Size、Image用
struct Item { //アイテムを表す構造体
    public Image i; //画像
    public int x;  //中心X座標
    public int y;  //中心Y座標
    public int hv; //左右方向の速度(左向きは負の数、右向きは正の数)
    public int vv; //上下方向の速度(上向きは負の数、下向きは正の数)
    public int v;  //表示状態(0:非表示、1以上:表示)
}
class Program : Form { //Formクラスの派生クラス
    [System.Runtime.InteropServices.DllImport("user32.dll")] //DLLインポート
    private static extern short GetKeyState(int nVirtKey); //外部定義指定
    int gamemode = 0; //モード(0:タイトル画面,1:プレイ画面,9:終了画面)
    int score = 0; //スコア
    Image backi = Image.FromFile("backb.bmp"); //背景画像を読込む
    Image playeri = Image.FromFile("player.gif"); //自機通常画像を読込む
    Image playerl = Image.FromFile("playerl.gif"); //自機左寄画像を読込む
    Image playerr = Image.FromFile("playerr.gif"); //自機右寄画像を読込む
    Image bulleti = Image.FromFile("bullet.gif"); //自弾画像を読込む
    Image bullet2i = Image.FromFile("bullet2.gif"); //【追加】自弾画像2を読込む
    Pen pen1 = new Pen(Color.Red, 2); //赤色太さ2のペン
    Brush brush1 = new SolidBrush(Color.FromArgb(63, 255, 0, 0)); //透明赤いブラシ
    Font font1 = new Font("メイリオ", 20, FontStyle.Bold); //フォントを生成
    Font fontt = new Font("メイリオ", 80, FontStyle.Bold); //フォントを生成
    Font fontm = new Font("メイリオ", 25, FontStyle.Bold); //フォントを生成
    Brush brushs = new SolidBrush(Color.Yellow); //黄色のブラシ
    Timer timer = new Timer(); //タイマーの生成
    int backy = 0; //1枚目の背景描画開始Y座標
    Item player; //自機の構造体オブジェクト
    Item pb; //自弾の構造体オブジェクト
    //中央座標を用いる画像描画処理
    private void DrawItem(PaintEventArgs e, Item it) {
        int xx = it.x - it.i.Width / 2; //左上X座標を得る
        int yy = it.y - it.i.Height / 2; //左上Y座標を得る
        e.Graphics.DrawImage(it.i, xx, yy);
    }
    //描画処理のオーバライド
    protected override void OnPaint(PaintEventArgs e) { 
        base.OnPaint(e); //基本クラスの描画処理を呼ぶ
        e.Graphics.DrawImage(backi, 0, backy); //背景画像を描画
        e.Graphics.DrawImage(backi, 0, backy - backi.Height); //背景画像を描画
        if (gamemode == 0) { //スタート画面?
            e.Graphics.DrawString("GAME1", fontt, brushs, 100, 150); //タイトル表示
            e.Graphics.DrawString("Hit Enter Key", fontm, brushs, 200, 300); //メッセージ表示
        } else if (gamemode == 1) { //プレイ画面?
            string s = String.Format("SCORE:{0:000,000}", score); //スコア文字列を作る
            e.Graphics.DrawString(s, font1, brushs, 400, 10); //スコア表示
            switch (player.hv) { //自機の向きによって分岐
                case 0:  player.i = playeri; break; //通常画像にする
                case -1: player.i = playerl; break; //左寄画像にする
                case 1:  player.i = playerr; break; //右寄画像にする
            }
            DrawItem(e, player); //自機を描画
            if (pb.v == 1) { //自弾がある?
                pb.i = (pb.i == bulleti) ? bullet2i : bulleti; //【追加】画像を交互変更
                DrawItem(e, pb); //自弾を描画
            }
        }

    }
    //キー入力時処理
    void OnKeyDown(object o, KeyEventArgs e) { 
        if (e.KeyCode.ToString() == "Escape") { //Escキーが押されていたら
            Close(); //フォーム終了
        }
        //タイトル画面でEnterキーが押されていたら
        if (gamemode == 0 && e.KeyCode.ToString() == "Return") { 
            gamemode = 1; //プレイ動画に遷移
            timer.Start(); //タイマー開始
        }
        Invalidate(); //画面再描画を依頼
    }
    //タイマーイベント処理
    void Play(object o, EventArgs e) { 
        backy = (backy + 1) % backi.Height; //1枚目の背景描画開始Y座標を下げる
        player.hv = 0; //自機の向きを無しにしておく
        if (player.x > playeri.Width / 2 && GetKeyState((int)Keys.Left) < 0) { //範囲内で←キーが押されている?
            player.x -= 10; //自機を左へ
            player.hv = -1; //左向き
        }
        if (player.x < backi.Width - playeri.Width / 2 && GetKeyState((int)Keys.Right) < 0) { //範囲内で→キーが押されている?
            player.x += 10; //自機を右へ
            player.hv = 1; //右向き
        }
        if (GetKeyState((int)Keys.Space) < 0) { //スペースキーが押されている?
            if (pb.v == 0) { //自弾が非表示?
                pb.v = 1; //表示にする
                pb.i = bulleti; //画像
                pb.x = player.x; //X座標は自機と同じ
                pb.y = player.y - player.i.Height / 2 - pb.i.Height / 2; //Y座標は自機の直上
                pb.vv = -5; //上移動速度
            }
        }
        if (pb.v != 0) { //自弾が存在?
            pb.y += pb.vv; //上へ移動
            if (pb.y + pb.i.Height / 2 < 0) { //画面上端より上に出たら
                pb.v = 0; //自弾を消す
            }
        }
        Invalidate(); //画面再描画を依頼
    }
    //コンストラクタ
    Program() { 
        DoubleBuffered = true; //ダブルバッファリングを有効化
        KeyDown += new KeyEventHandler(OnKeyDown); //キー入力イベント登録
        timer.Tick += new EventHandler(Play); //タイマーイベント登録
        timer.Interval = 10; //タイマーインターバル(ミリ秒)
        player.i = playeri;  //自機の画像
        player.x = 320; //自機の中心X座標
        player.y = 410; //自機の中心Y座標
        player.hv = 0;  //自機の左右方向の速度
    }
    public static void Main() {
        Program f = new Program(); //自分のオブジェクトを生成
        f.Size = new Size(660, 520); //フォームのサイズを設定
        f.Text = "Game"; //フォーム名を設定
        f.ControlBox = false; //コントロールボックスを非表示に
        f.FormBorderStyle = FormBorderStyle.Fixed3D; //サイズ変更を抑止
        Application.Run(f); //フォームを現出
    }
}

演習30 自弾の複数化

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

手順:

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

作成例

//演習30 自弾の複数化
using System; //汎用的に利用
using System.Windows.Forms; //フォームアプリケーションに必須
using System.Drawing; //Size、Image用
struct Item { //アイテムを表す構造体
    public Image i; //画像
    public int x;  //中心X座標
    public int y;  //中心Y座標
    public int hv; //左右方向の速度(左向きは負の数、右向きは正の数)
    public int vv; //上下方向の速度(上向きは負の数、下向きは正の数)
    public int v;  //表示状態(0:非表示、1以上:表示)
}
class Program : Form { //Formクラスの派生クラス
    [System.Runtime.InteropServices.DllImport("user32.dll")] //DLLインポート
    private static extern short GetKeyState(int nVirtKey); //外部定義指定
    int gamemode = 0; //モード(0:タイトル画面,1:プレイ画面,9:終了画面)
    int score = 0; //スコア
    Image backi = Image.FromFile("backb.bmp"); //背景画像を読込む
    Image playeri = Image.FromFile("player.gif"); //自機通常画像を読込む
    Image playerl = Image.FromFile("playerl.gif"); //自機左寄画像を読込む
    Image playerr = Image.FromFile("playerr.gif"); //自機右寄画像を読込む
    Image bulleti = Image.FromFile("bullet.gif"); //自弾画像を読込む
    Image bullet2i = Image.FromFile("bullet2.gif"); //自弾画像2を読込む
    Pen pen1 = new Pen(Color.Red, 2); //赤色太さ2のペン
    Brush brush1 = new SolidBrush(Color.FromArgb(63, 255, 0, 0)); //透明赤いブラシ
    Font font1 = new Font("メイリオ", 20, FontStyle.Bold); //フォントを生成
    Font fontt = new Font("メイリオ", 80, FontStyle.Bold); //フォントを生成
    Font fontm = new Font("メイリオ", 25, FontStyle.Bold); //フォントを生成
    Brush brushs = new SolidBrush(Color.Yellow); //黄色のブラシ
    Timer timer = new Timer(); //タイマーの生成
    int backy = 0; //1枚目の背景描画開始Y座標
    Item player; //自機の構造体オブジェクト
    const int maxpb = 10; //【追加】自弾の最大数
    Item[] pba = new Item[maxpb]; //【変更】自弾の構造体オブジェクト配列
    //中央座標を用いる画像描画処理
    private void DrawItem(PaintEventArgs e, Item it) {
        int xx = it.x - it.i.Width / 2; //左上X座標を得る
        int yy = it.y - it.i.Height / 2; //左上Y座標を得る
        e.Graphics.DrawImage(it.i, xx, yy);
    }
    //描画処理のオーバライド
    protected override void OnPaint(PaintEventArgs e) { 
        base.OnPaint(e); //基本クラスの描画処理を呼ぶ
        e.Graphics.DrawImage(backi, 0, backy); //背景画像を描画
        e.Graphics.DrawImage(backi, 0, backy - backi.Height); //背景画像を描画
        if (gamemode == 0) { //スタート画面?
            e.Graphics.DrawString("GAME1", fontt, brushs, 100, 150); //タイトル表示
            e.Graphics.DrawString("Hit Enter Key", fontm, brushs, 200, 300); //メッセージ表示
        } else if (gamemode == 1) { //プレイ画面?
            string s = String.Format("SCORE:{0:000,000}", score); //スコア文字列を作る
            e.Graphics.DrawString(s, font1, brushs, 400, 10); //スコア表示
            switch (player.hv) { //自機の向きによって分岐
                case 0:  player.i = playeri; break; //通常画像にする
                case -1: player.i = playerl; break; //左寄画像にする
                case 1:  player.i = playerr; break; //右寄画像にする
            }
            DrawItem(e, player); //自機を描画
            foreach (var pb in pba) { //【追加】全自弾について繰返す
                if (pb.v == 1) { //自弾がある?
                    DrawItem(e, pb); //自弾を描画
                }
            }
        }

    }
    //キー入力時処理
    void OnKeyDown(object o, KeyEventArgs e) { 
        if (e.KeyCode.ToString() == "Escape") { //Escキーが押されていたら
            Close(); //フォーム終了
        }
        //タイトル画面でEnterキーが押されていたら
        if (gamemode == 0 && e.KeyCode.ToString() == "Return") { 
            gamemode = 1; //プレイ動画に遷移
            timer.Start(); //タイマー開始
        }
        Invalidate(); //画面再描画を依頼
    }
    //タイマーイベント処理
    void Play(object o, EventArgs e) { 
        backy = (backy + 1) % backi.Height; //1枚目の背景描画開始Y座標を下げる
        player.hv = 0; //自機の向きを無しにしておく
        if (player.x > playeri.Width / 2 && GetKeyState((int)Keys.Left) < 0) { //範囲内で←キーが押されている?
            player.x -= 10; //自機を左へ
            player.hv = -1; //左向き
        }
        if (player.x < backi.Width - playeri.Width / 2 && GetKeyState((int)Keys.Right) < 0) { //範囲内で→キーが押されている?
            player.x += 10; //自機を右へ
            player.hv = 1; //右向き
        }
        if (GetKeyState((int)Keys.Space) < 0) { //スペースキーが押されている?
            for (int i = 0; i < maxpb; i++) { //【追加】全自弾について繰返す
                if (pba[i].v == 0) { //【変更】自弾が非表示?
                    pba[i].v = 1; //【変更】表示にする
                    pba[i].i = bulleti; //【変更】画像
                    pba[i].x = player.x; //【変更】X座標は自機と同じ
                    pba[i].y = player.y - player.i.Height / 2 - pba[i].i.Height / 2; //【変更】Y座標は自機の直上
                    pba[i].vv = -5; //【変更】上移動速度
                    break; //【追加】1発発射できればOK
                }
            }
        }
        for (int i = 0; i < maxpb; i++) { //【追加】全自弾について繰返す
            if (pba[i].v != 0) { //【変更】自弾が存在?
                pba[i].i = (pba[i].i == bulleti) ? bullet2i : bulleti; //【移動&変更】画像を交互変更
                pba[i].y += pba[i].vv; //【変更】上へ移動
                if (pba[i].y + pba[i].i.Height / 2 < 0) { //【変更】画面上端より上に出たら
                    pba[i].v = 0; //【変更】自弾を消す
                }
            }
        }
        Invalidate(); //画面再描画を依頼
    }
    //コンストラクタ
    Program() { 
        DoubleBuffered = true; //ダブルバッファリングを有効化
        KeyDown += new KeyEventHandler(OnKeyDown); //キー入力イベント登録
        timer.Tick += new EventHandler(Play); //タイマーイベント登録
        timer.Interval = 10; //タイマーインターバル(ミリ秒)
        player.i = playeri;  //自機の画像
        player.x = 320; //自機の中心X座標
        player.y = 410; //自機の中心Y座標
        player.hv = 0;  //自機の左右方向の速度
    }
    public static void Main() {
        Program f = new Program(); //自分のオブジェクトを生成
        f.Size = new Size(660, 520); //フォームのサイズを設定
        f.Text = "Game"; //フォーム名を設定
        f.ControlBox = false; //コントロールボックスを非表示に
        f.FormBorderStyle = FormBorderStyle.Fixed3D; //サイズ変更を抑止
        Application.Run(f); //フォームを現出
    }
}

テーマ29 冷却期間

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

演習31 自弾発射の改良

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

手順:

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

作成例

//演習31 自弾発射の改良
using System; //汎用的に利用
using System.Windows.Forms; //フォームアプリケーションに必須
using System.Drawing; //Size、Image用
struct Item { //アイテムを表す構造体
    public Image i; //画像
    public int x;  //中心X座標
    public int y;  //中心Y座標
    public int hv; //左右方向の速度(左向きは負の数、右向きは正の数)
    public int vv; //上下方向の速度(上向きは負の数、下向きは正の数)
    public int v;  //表示状態(0:非表示、1以上:表示)
}
class Program : Form { //Formクラスの派生クラス
    [System.Runtime.InteropServices.DllImport("user32.dll")] //DLLインポート
    private static extern short GetKeyState(int nVirtKey); //外部定義指定
    int gamemode = 0; //モード(0:タイトル画面,1:プレイ画面,9:終了画面)
    int score = 0; //スコア
    Image backi = Image.FromFile("backb.bmp"); //背景画像を読込む
    Image playeri = Image.FromFile("player.gif"); //自機通常画像を読込む
    Image playerl = Image.FromFile("playerl.gif"); //自機左寄画像を読込む
    Image playerr = Image.FromFile("playerr.gif"); //自機右寄画像を読込む
    Image bulleti = Image.FromFile("bullet.gif"); //自弾画像を読込む
    Image bullet2i = Image.FromFile("bullet2.gif"); //自弾画像2を読込む
    Pen pen1 = new Pen(Color.Red, 2); //赤色太さ2のペン
    Brush brush1 = new SolidBrush(Color.FromArgb(63, 255, 0, 0)); //透明赤いブラシ
    Font font1 = new Font("メイリオ", 20, FontStyle.Bold); //フォントを生成
    Font fontt = new Font("メイリオ", 80, FontStyle.Bold); //フォントを生成
    Font fontm = new Font("メイリオ", 25, FontStyle.Bold); //フォントを生成
    Brush brushs = new SolidBrush(Color.Yellow); //黄色のブラシ
    Timer timer = new Timer(); //タイマーの生成
    int backy = 0; //1枚目の背景描画開始Y座標
    Item player; //自機の構造体オブジェクト
    const int maxpb = 10; //自弾の最大数
    Item[] pba = new Item[maxpb]; //自弾の構造体オブジェクト配列
    const int cold = 10; //【追加】自弾発射の冷却時間
    int waitpb = 0; //【追加】自弾発射の待ち時間
    //中央座標を用いる画像描画処理
    private void DrawItem(PaintEventArgs e, Item it) {
        int xx = it.x - it.i.Width / 2; //左上X座標を得る
        int yy = it.y - it.i.Height / 2; //左上Y座標を得る
        e.Graphics.DrawImage(it.i, xx, yy);
    }
    //描画処理のオーバライド
    protected override void OnPaint(PaintEventArgs e) { 
        base.OnPaint(e); //基本クラスの描画処理を呼ぶ
        e.Graphics.DrawImage(backi, 0, backy); //背景画像を描画
        e.Graphics.DrawImage(backi, 0, backy - backi.Height); //背景画像を描画
        if (gamemode == 0) { //スタート画面?
            e.Graphics.DrawString("GAME1", fontt, brushs, 100, 150); //タイトル表示
            e.Graphics.DrawString("Hit Enter Key", fontm, brushs, 200, 300); //メッセージ表示
        } else if (gamemode == 1) { //プレイ画面?
            string s = String.Format("SCORE:{0:000,000}", score); //スコア文字列を作る
            e.Graphics.DrawString(s, font1, brushs, 400, 10); //スコア表示
            switch (player.hv) { //自機の向きによって分岐
                case 0:  player.i = playeri; break; //通常画像にする
                case -1: player.i = playerl; break; //左寄画像にする
                case 1:  player.i = playerr; break; //右寄画像にする
            }
            DrawItem(e, player); //自機を描画
            foreach (var pb in pba) { //全自弾について繰返す
                if (pb.v == 1) { //自弾がある?
                    DrawItem(e, pb); //自弾を描画
                }
            }
        }

    }
    //キー入力時処理
    void OnKeyDown(object o, KeyEventArgs e) { 
        if (e.KeyCode.ToString() == "Escape") { //Escキーが押されていたら
            Close(); //フォーム終了
        }
        //タイトル画面でEnterキーが押されていたら
        if (gamemode == 0 && e.KeyCode.ToString() == "Return") { 
            gamemode = 1; //プレイ動画に遷移
            timer.Start(); //タイマー開始
        }
        Invalidate(); //画面再描画を依頼
    }
    //タイマーイベント処理
    void Play(object o, EventArgs e) { 
        backy = (backy + 1) % backi.Height; //1枚目の背景描画開始Y座標を下げる
        player.hv = 0; //自機の向きを無しにしておく
        if (player.x > playeri.Width / 2 && GetKeyState((int)Keys.Left) < 0) { //範囲内で←キーが押されている?
            player.x -= 10; //自機を左へ
            player.hv = -1; //左向き
        }
        if (player.x < backi.Width - playeri.Width / 2 && GetKeyState((int)Keys.Right) < 0) { //範囲内で→キーが押されている?
            player.x += 10; //自機を右へ
            player.hv = 1; //右向き
        }
        if (GetKeyState((int)Keys.Space) < 0) { //スペースキーが押されている?
            if (waitpb <= 0) { //【追加】自弾発射待ち時間がゼロ?
                for (int i = 0; i < maxpb; i++) { //全自弾について繰返す
                    if (pba[i].v == 0) { //自弾が非表示?
                        pba[i].v = 1; //表示にする
                        pba[i].i = bulleti; //画像
                        pba[i].x = player.x; //X座標は自機と同じ
                        pba[i].y = player.y - player.i.Height / 2 - pba[i].i.Height / 2; //Y座標は自機の直上
                        pba[i].vv = -5; //上移動速度
                        waitpb = cold; //【追加】自弾発射待ち時間をセット
                        break; //1発発射できればOK
                    }
                }
            }
        } else { //【以下追加】スペースキーが押されていない?
            waitpb = 0; //自弾発射待ち時間をゼロにして発射可能にする
        }
        for (int i = 0; i < maxpb; i++) { //全自弾について繰返す
            if (pba[i].v != 0) { //自弾が存在?
                pba[i].i = (pba[i].i == bulleti) ? bullet2i : bulleti; //画像を交互変更
                pba[i].y += pba[i].vv; //上へ移動
                if (pba[i].y + pba[i].i.Height / 2 < 0) { //画面上端より上に出たら
                    pba[i].v = 0; //自弾を消す
                }
            }
        }
        if (waitpb > 0) { //【以下追加】自弾発射待ち時間がセットされていたら
            waitpb--; //カウントダウンする
        }
        Invalidate(); //画面再描画を依頼
    }
    //コンストラクタ
    Program() { 
        DoubleBuffered = true; //ダブルバッファリングを有効化
        KeyDown += new KeyEventHandler(OnKeyDown); //キー入力イベント登録
        timer.Tick += new EventHandler(Play); //タイマーイベント登録
        timer.Interval = 10; //タイマーインターバル(ミリ秒)
        player.i = playeri;  //自機の画像
        player.x = 320; //自機の中心X座標
        player.y = 410; //自機の中心Y座標
        player.hv = 0;  //自機の左右方向の速度
    }
    public static void Main() {
        Program f = new Program(); //自分のオブジェクトを生成
        f.Size = new Size(660, 520); //フォームのサイズを設定
        f.Text = "Game"; //フォーム名を設定
        f.ControlBox = false; //コントロールボックスを非表示に
        f.FormBorderStyle = FormBorderStyle.Fixed3D; //サイズ変更を抑止
        Application.Run(f); //フォームを現出
    }
}

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

講義メモ

テキスト編:p.321「例外処理の基礎」から
ゲーム開発演習:自弾の複数化、敵機の出現

第13章 例外

p.321 例外処理の基礎

・例えば、整数演算でゼロ除算になると実行時エラーとして異常終了する
・この実行時エラーを例外といい、例外の種類や状況によっては、異常終了して欲しくない場合がある
・この場合に用いる仕掛けが例外処理
・例外処理を用いると、例外発生の検知と対処を見やすく記述できる
・ただし、即時に異常終了すべき例外(例:メモリ不足や故障)もあるので、例外処理には配慮と知識が必要

p.322 exception01.cs

//p.322 exception01.cs
using System;
class MyClass {
    public static void Main() {
        Console.Write("割られる数--");
        string strA = Console.ReadLine();
        double a = double.Parse(strA); //形式例外の可能性有り
        Console.Write("割る数---");
        string strB = Console.ReadLine();
        double b = double.Parse(strB); //形式例外の可能性有り
        Console.WriteLine("{0} ÷ {1} = {2}", a, b, a / b);
    }
}

p.322 exception01.csの例外発生時の情報について

・p.322 exception01.csで「a」などの数値以外の文字を入力すると実行時エラーになる
・この時のメッセージについて分析しよう

ハンドルされていない例外: System.FormatException: 入力文字列の形式が正しくありません。
↑例外処理がないことを示す ↑例外を表すクラス(形式例外) ↑例外クラスの持つメッセージ
場所 System.Number.ParseDouble(String value, NumberStyles options, NumberFormatInfo numfmt)
  ↑実際に実行時エラー(例外)が発生した場所(※今回はC#が提供したクラス内)
場所 System.Double.Parse(String s)
  ↑実際に実行時エラー(例外)が発生したメソッドを呼んでいるメソッド名
場所 MyClass.Main() 場所 D:\ha232_C#_omiya\Chap11\Chap11\exception01.cs:行 7
  ↑例外が発生したメソッドを呼んでいるメソッドを呼んでいるプログラムと発生行番号

アレンジ演習:p.322 exception01.cs

・2変数をdouble型ではなくint型にすると、下記の3つの例外を発生させることができる
 ① 形式例外:整数に変換できない文字が入力された場合
 ② ゼロ除算例外:整数演算でゼロで除算した場合(※実数演算では発生しない)
 ③ オーバーフロー例外:変換結果がint型の範囲を超える場合(例:22億)
・以上が発生することを試そう

作成例

//アレンジ演習:p.322 exception01.cs
using System;
class MyClass {
    public static void Main() {
        Console.Write("割られる数--");
        string strA = Console.ReadLine();
        int a = int.Parse(strA); //形式例外、オーバーフロー例外の可能性有り
        Console.Write("割る数---");
        string strB = Console.ReadLine();
        int b = int.Parse(strB); //形式例外、オーバーフロー例外の可能性有り
        Console.WriteLine("{0} ÷ {1} = {2}", a, b, a / b); //ゼロ除算例外の可能性有り
    }
}

実行例(形式例外)

割られる数--a

ハンドルされていない例外: System.FormatException: 入力文字列の形式が正しくありません。
   場所 System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal)
   場所 System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info)
   場所 System.Int32.Parse(String s)
   場所 MyClass.Main() 場所 D:\ha232_C#_omiya\Chap11\Chap11\exception01.cs:行 7

実行例(オーバーフロー例外)

割られる数--2200000000

ハンドルされていない例外: System.OverflowException: Int32 型の値が大きすぎるか、または小さすぎます。
   場所 System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info)
   場所 System.Int32.Parse(String s)
   場所 MyClass.Main() 場所 D:\ha232_C#_omiya\Chap11\Chap11\exception01.cs:行 7

実行例(ゼロ除算例外)

割られる数--5
割る数---0

ハンドルされていない例外: System.DivideByZeroException: 0 で除算しようとしました。
   場所 MyClass.Main() 場所 D:\ha232_C#_omiya\Chap11\Chap11\exception01.cs:行 11

p.323(tryブロックとcatchブロック)

・例外処理の基本は処理対象の範囲を決めることからであり、決まった範囲をtryブロックにする
・書式: try { 例外処理対象 }
・tryブロックの中で例外が発生したら、即時に異常終了することはなくなり、システム側が例外処理定義のチェックと対処を行ってくれる
・なお、tryブロック内の途中で例外が発生したら、例外処理の有無に関わらず、発生場所からtryブロックの終わりまでの記述内容は
 実行されない
・tryブロックには1つ以上のcatchブロックが必要で、例外発生時の対処を記述する。
・対処が完了できたらプログラムは正常モードに戻り、catchブロックの次の行から実行を再開する。
・最も単純な書式: try { 例外処理対象 } catch { 例外処理 }

p.324 exception02.cs

//p.324 exception02.cs
using System;
class MyClass {
    public static void Main() {
        double a = 0.0, b = 0.0;
        Console.Write("割られる数--");
        string strA = Console.ReadLine();
        try { //例外処理対象①
            a = double.Parse(strA); //形式例外発生の可能性
        } catch { //①の例外処理
            Console.WriteLine("不適切な入力です"); //表示後、処理を続行
        }
        Console.Write("割る数---");
        string strB = Console.ReadLine();
        try { //例外処理対象②
            b = double.Parse(strB); //形式例外発生の可能性
        } catch { //②の例外処理
            Console.WriteLine("不適切な入力です");//表示後、処理を続行
        }
        Console.WriteLine("{0} ÷ {1} = {2}", a, b, a / b);
    }
}

p.325 例外クラス

・例外発生時に用いられるのがC#が定義している例外を表すクラスで、Exceptionクラスを基本クラスとする派生クラスになっている
・また、各種の例外のグループ化が例外クラスの継承関係によって表現されているので、複数の派生クラスを共通の基本クラスで扱うこともできる
・catchブロックに引数として例外クラスを記述でき、その例外発生時にのみ対処させることが可能
・この指定により「どんな例外でもcatchしてしまう」ことによる弊害が防止できる
・最も単純な書式: try { 例外処理対象 } catch(例外クラス名) { 例外処理 }
・なお、例外処理の中で、発生した例外に関する情報を例外クラスのオブジェクト経由で得ることが可能
・これは、例外発生時に発生情報を例外クラスのオブジェクトとしてシステムが生成して投げて(送って)くれるからで、
 catchブロックに引数として例外クラスと引数名を指定することで利用できる
・単純な書式: try { 例外処理対象 } catch(例外クラス名 引数名) { 例外処理 }

p.325 主な例外クラス

・すでに説明した例外クラス
 System.FormatException: 形式例外(整数変換や実数変換などが失敗した)
 System.OverflowException: オーバーフロー例外(データがその型の最大値・最小値を超えた)
 System.DivideByZeroException: ゼロ除算例外(整数演算で0で除算)
・頻出の例外クラス
 System.IndexOutOfRangeException: 配列添字範囲外例外(添字が要素数以上または0未満)
・例外の詳細やツリー構造は公式ドキュメントで閲覧可能
 例: https://learn.microsoft.com/ja-jp/dotnet/api/system.formatexception

p.326 例外クラスの主なメンバ

・catchブロックに引数として例外クラスと引数名を指定することで得られる例外オブジェクトが持つ情報は、
 例外クラスに共通なメンバ(メソッドとプロパティ)を用いて得られる
・詳細は公式ドキュメントで閲覧可能
 https://learn.microsoft.com/ja-jp/dotnet/api/system.exception?view=net-8.0
・なお、テキストに記載のToString()メソッドは、例外クラスに限ったものではなく、すべてのクラスにおいて、
 その情報を返すメソッドとして記述する(オーバーライドする)ことが推奨されている。

p.326 System.Objectクラス

・Object型はInt32などと同様の.NETフレームワーク型であり、.NETフレームワーク型にはすべて対応するクラスまたは構造体が提供されている
・int型などと同様に、object型を用いると、.NETフレームワークのObject型として扱われる
・object/Object型に対応するクラスがSystem.Objectクラスで、全てのクラスの暗黙の基本クラス(ルートクラス)として定義されており、
 継承を指定しなくても、自動的に用いられる
・このSystem.Objectクラスの持っているメンバの一つがToString()メソッドなので、すべてのクラスにおいてToString()メソッドの
 オーバーライドが可能。
・このことを利用して、オブジェクト名(参照変数)をConsole.Writeなどに指定すると、自動的に「参照変数.ToString()」が
 実行されるようになっている。
例: p.327 exception03.csで「Console.Write(io)」と「Console.Write(io.ToString())」を実行しているが同じ結果になる

今週の話題

販売本数ランキング 今回トップは「スーパーマリオブラザーズ ワンダー(Switch)」GO!
『フォートナイト』でプログラミングやデジタル技術を習得―茨城県で「FORTNITE UEFN クリエイティブ講座」開催 GO!
第24回「GDC Awards」ノミネート作品発表!『バルダーズ・ゲート3』と『ゼルダの伝説 ティアキン』が最多7部門で選出 GO!
ユービーアイソフト、サブスクは「今後大きな成長が見込める」と自信―PC向けサブスクプランをリブランディング GO!
「理想を追い求めたゲームが存在する余地が無くなる」…『バルダーズ・ゲート3』開発者がゲームのサブスクリプションに否定的な見解示す GO!
吉田直樹氏が『FF17』に対する胸の内を語る―「同じおじさんがやっていくよりは、若い世代にチャレンジしてほしい」GO!
『プロセカ』クリエイターがLive2D制作フローや演出のノウハウを惜しみなく紹介―Colorful Palette講演レポート【alive 2023】GO!

『LOST ARK』サービス終了告知―満足できるサービスの提供が困難であると判断 GO!
『GTA5』マイケル役俳優が自身の声を無許可使用したAIチャットボットに猛反発 GO!

前回のコメント

・練習問題など苦戦しましたが上手く理解できました!

 何よりです。

・コンソールでキー入力ができることを知れてよかったです。

 p.291 clock01.csで行っているとおり「なにかキーが押されたら行う処理」は簡単に記述できます。
 これをイベントを用いて扱うことで、いろいろと応用できるのがポイントです。

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

:自弾の発射と上移動、自弾の複数化、敵機の出現

演習26 画像の位置管理を中央座標に・改

・private void DrawImage(PaintEventArgs e, Image i, int x, int y)が冗長なので、
 private void DrawItem(PaintEventArgs e, Item it)としよう

作成例

//演習26 画像の位置管理を中央座標に・改
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 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"); //自機右寄画像を読込む
    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; //【変更】プレイヤーの構造体オブジェクト
    //【以下追加⇒変更】中央座標を用いる画像描画処理
    private void DrawItem(PaintEventArgs e, Item it) {
        int xx = it.x - it.i.Width / 2; //左上X座標を得る
        int yy = it.y - it.i.Height / 2; //左上Y座標を得る
        e.Graphics.DrawImage(it.i, xx, yy);
    }
    //描画処理のオーバライド
    protected override void OnPaint(PaintEventArgs e) { 
        base.OnPaint(e); //基本クラスの描画処理を呼ぶ
        e.Graphics.DrawImage(backi, 0, backy); //背景画像を描画
        e.Graphics.DrawImage(backi, 0, backy - backi.Height); //背景画像を描画
        if (gamemode == 0) { //スタート画面?
            e.Graphics.DrawString("GAME1", fontt, brushs, 100, 150); //タイトル表示
            e.Graphics.DrawString("Hit Enter Key", fontm, brushs, 200, 300); //メッセージ表示
        } else if (gamemode == 1) { //プレイ画面?
            string s = String.Format("SCORE:{0:000,000}", score); //スコア文字列を作る
            e.Graphics.DrawString(s, font1, brushs, 400, 10); //スコア表示
            switch (player.hv) { //【変更】自機の向きによって分岐
                case 0:  player.i = playeri; break; //【変更】通常画像にする
                case -1: player.i = playerl; break; //【変更】左寄画像にする
                case 1:  player.i = playerr; break; //【変更】右寄画像にする
            }
            DrawItem(e, player); //自機を描画
        }

    }
    //キー入力時処理
    void OnKeyDown(object o, KeyEventArgs e) { 
        if (e.KeyCode.ToString() == "Escape") { //Escキーが押されていたら
            Close(); //フォーム終了
        }
        //タイトル画面でEnterキーが押されていたら
        if (gamemode == 0 && e.KeyCode.ToString() == "Return") { 
            gamemode = 1; //プレイ動画に遷移
            timer.Start(); //タイマー開始
        }
        Invalidate(); //画面再描画を依頼
    }
    //タイマーイベント処理
    void Play(object o, EventArgs e) { 
        backy = (backy + 1) % backi.Height; //1枚目の背景描画開始Y座標を下げる
        player.hv = 0; //【変更】自機の向きを無しにしておく
        if (player.x > playeri.Width / 2 && GetKeyState((int)Keys.Left) < 0) { //【変更】範囲内で←キーが押されている?
            player.x -= 10; //【変更】自機を左へ
            player.hv = -1; //【変更】左向き
        }
        if (player.x < backi.Width - playeri.Width / 2 && GetKeyState((int)Keys.Right) < 0) { //【変更】範囲内で→キーが押されている?
            player.x += 10; //【変更】自機を右へ
            player.hv = 1; //【変更】右向き
        }
        if (GetKeyState((int)Keys.Up) < 0) { //↑キーが押されている?
            score++; //スコアアップ
        }
        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); //フォームを現出
    }
}

演習27 自弾の出現(単独バージョン)

・ゲームモードが1(プレイ画面)の時に、スペースキーが押されたら自弾を出そう
・ただし、出現していないときに限る(まずは1個のみとする)
・出現位置は自機の位置で決まり、自機の直上とする
・自機はItem構造体で表す
・画像は下記を利用可能
  bullet.gif 20x20

作成例

//演習27 自弾の出現(単独バージョン)
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 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"); //【追加】自弾画像を読込む
    Pen pen1 = new Pen(Color.Red, 2); //赤色太さ2のペン
    Brush brush1 = new SolidBrush(Color.FromArgb(63, 255, 0, 0)); //透明赤いブラシ
    Font font1 = new Font("メイリオ", 20, FontStyle.Bold); //フォントを生成
    Font fontt = new Font("メイリオ", 80, FontStyle.Bold); //フォントを生成
    Font fontm = new Font("メイリオ", 25, FontStyle.Bold); //フォントを生成
    Brush brushs = new SolidBrush(Color.Yellow); //黄色のブラシ
    Timer timer = new Timer(); //タイマーの生成
    int backy = 0; //1枚目の背景描画開始Y座標
    Item player; //自機の構造体オブジェクト
    Item pb; //【追加】自弾の構造体オブジェクト
    //中央座標を用いる画像描画処理
    private void DrawItem(PaintEventArgs e, Item it) {
        int xx = it.x - it.i.Width / 2; //左上X座標を得る
        int yy = it.y - it.i.Height / 2; //左上Y座標を得る
        e.Graphics.DrawImage(it.i, xx, yy);
    }
    //描画処理のオーバライド
    protected override void OnPaint(PaintEventArgs e) { 
        base.OnPaint(e); //基本クラスの描画処理を呼ぶ
        e.Graphics.DrawImage(backi, 0, backy); //背景画像を描画
        e.Graphics.DrawImage(backi, 0, backy - backi.Height); //背景画像を描画
        if (gamemode == 0) { //スタート画面?
            e.Graphics.DrawString("GAME1", fontt, brushs, 100, 150); //タイトル表示
            e.Graphics.DrawString("Hit Enter Key", fontm, brushs, 200, 300); //メッセージ表示
        } else if (gamemode == 1) { //プレイ画面?
            string s = String.Format("SCORE:{0:000,000}", score); //スコア文字列を作る
            e.Graphics.DrawString(s, font1, brushs, 400, 10); //スコア表示
            switch (player.hv) { //自機の向きによって分岐
                case 0:  player.i = playeri; break; //通常画像にする
                case -1: player.i = playerl; break; //左寄画像にする
                case 1:  player.i = playerr; break; //右寄画像にする
            }
            DrawItem(e, player); //自機を描画
            if (pb.v == 1) { //【以下追加】自弾がある?
                DrawItem(e, pb); //自機を描画
            }
        }

    }
    //キー入力時処理
    void OnKeyDown(object o, KeyEventArgs e) { 
        if (e.KeyCode.ToString() == "Escape") { //Escキーが押されていたら
            Close(); //フォーム終了
        }
        //タイトル画面でEnterキーが押されていたら
        if (gamemode == 0 && e.KeyCode.ToString() == "Return") { 
            gamemode = 1; //プレイ動画に遷移
            timer.Start(); //タイマー開始
        }
        Invalidate(); //画面再描画を依頼
    }
    //タイマーイベント処理
    void Play(object o, EventArgs e) { 
        backy = (backy + 1) % backi.Height; //1枚目の背景描画開始Y座標を下げる
        player.hv = 0; //自機の向きを無しにしておく
        if (player.x > playeri.Width / 2 && GetKeyState((int)Keys.Left) < 0) { //範囲内で←キーが押されている?
            player.x -= 10; //自機を左へ
            player.hv = -1; //左向き
        }
        if (player.x < backi.Width - playeri.Width / 2 && GetKeyState((int)Keys.Right) < 0) { //範囲内で→キーが押されている?
            player.x += 10; //自機を右へ
            player.hv = 1; //右向き
        }
        if (GetKeyState((int)Keys.Space) < 0) { //【以下追加】スペースキーが押されている?
            if (pb.v == 0) { //自弾が非表示?
                pb.v = 1; //表示にする
                pb.i = bulleti; //画像
                pb.x = player.x; //X座標は自機と同じ
                pb.y = player.y - player.i.Height / 2 - pb.i.Height / 2; //Y座標は上
            }
        }
        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); //フォームを現出
    }
}

演習28 自弾の上移動(単独バージョン)

・自弾が出現状態であれば、上へ移動しよう
・完全に見えなくなったら、出現状態を無に戻して再発射可能にしよう
・上下方向の移動速度vvをItem構造体に追加しよう(値は適当に)

作成例

//演習28 自弾の上移動(単独バージョン)
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"); //自弾画像を読込む
    Pen pen1 = new Pen(Color.Red, 2); //赤色太さ2のペン
    Brush brush1 = new SolidBrush(Color.FromArgb(63, 255, 0, 0)); //透明赤いブラシ
    Font font1 = new Font("メイリオ", 20, FontStyle.Bold); //フォントを生成
    Font fontt = new Font("メイリオ", 80, FontStyle.Bold); //フォントを生成
    Font fontm = new Font("メイリオ", 25, FontStyle.Bold); //フォントを生成
    Brush brushs = new SolidBrush(Color.Yellow); //黄色のブラシ
    Timer timer = new Timer(); //タイマーの生成
    int backy = 0; //1枚目の背景描画開始Y座標
    Item player; //自機の構造体オブジェクト
    Item pb; //自弾の構造体オブジェクト
    //中央座標を用いる画像描画処理
    private void DrawItem(PaintEventArgs e, Item it) {
        int xx = it.x - it.i.Width / 2; //左上X座標を得る
        int yy = it.y - it.i.Height / 2; //左上Y座標を得る
        e.Graphics.DrawImage(it.i, xx, yy);
    }
    //描画処理のオーバライド
    protected override void OnPaint(PaintEventArgs e) { 
        base.OnPaint(e); //基本クラスの描画処理を呼ぶ
        e.Graphics.DrawImage(backi, 0, backy); //背景画像を描画
        e.Graphics.DrawImage(backi, 0, backy - backi.Height); //背景画像を描画
        if (gamemode == 0) { //スタート画面?
            e.Graphics.DrawString("GAME1", fontt, brushs, 100, 150); //タイトル表示
            e.Graphics.DrawString("Hit Enter Key", fontm, brushs, 200, 300); //メッセージ表示
        } else if (gamemode == 1) { //プレイ画面?
            string s = String.Format("SCORE:{0:000,000}", score); //スコア文字列を作る
            e.Graphics.DrawString(s, font1, brushs, 400, 10); //スコア表示
            switch (player.hv) { //自機の向きによって分岐
                case 0:  player.i = playeri; break; //通常画像にする
                case -1: player.i = playerl; break; //左寄画像にする
                case 1:  player.i = playerr; break; //右寄画像にする
            }
            DrawItem(e, player); //自機を描画
            if (pb.v == 1) { //自弾がある?
                DrawItem(e, pb); //自機を描画
            }
        }

    }
    //キー入力時処理
    void OnKeyDown(object o, KeyEventArgs e) { 
        if (e.KeyCode.ToString() == "Escape") { //Escキーが押されていたら
            Close(); //フォーム終了
        }
        //タイトル画面でEnterキーが押されていたら
        if (gamemode == 0 && e.KeyCode.ToString() == "Return") { 
            gamemode = 1; //プレイ動画に遷移
            timer.Start(); //タイマー開始
        }
        Invalidate(); //画面再描画を依頼
    }
    //タイマーイベント処理
    void Play(object o, EventArgs e) { 
        backy = (backy + 1) % backi.Height; //1枚目の背景描画開始Y座標を下げる
        player.hv = 0; //自機の向きを無しにしておく
        if (player.x > playeri.Width / 2 && GetKeyState((int)Keys.Left) < 0) { //範囲内で←キーが押されている?
            player.x -= 10; //自機を左へ
            player.hv = -1; //左向き
        }
        if (player.x < backi.Width - playeri.Width / 2 && GetKeyState((int)Keys.Right) < 0) { //範囲内で→キーが押されている?
            player.x += 10; //自機を右へ
            player.hv = 1; //右向き
        }
        if (GetKeyState((int)Keys.Space) < 0) { //スペースキーが押されている?
            if (pb.v == 0) { //自弾が非表示?
                pb.v = 1; //表示にする
                pb.i = bulleti; //画像
                pb.x = player.x; //X座標は自機と同じ
                pb.y = player.y - player.i.Height / 2 - pb.i.Height / 2; //Y座標は自機の直上
                pb.vv = -5; //【追加】上移動速度
            }
        }
        if (pb.v != 0) { //【以下追加】自弾が存在?
            pb.y += pb.vv; //上へ移動
            if (pb.y + pb.i.Height / 2 < 0) { //画面上端より上に出たら
                pb.v = 0; //自弾を消す
            }
        }
        Invalidate(); //画面再描画を依頼
    }
    //コンストラクタ
    Program() { 
        DoubleBuffered = true; //ダブルバッファリングを有効化
        KeyDown += new KeyEventHandler(OnKeyDown); //キー入力イベント登録
        timer.Tick += new EventHandler(Play); //タイマーイベント登録
        timer.Interval = 10; //タイマーインターバル(ミリ秒)
        player.i = playeri;  //自機の画像
        player.x = 320; //自機の中心X座標
        player.y = 410; //自機の中心Y座標
        player.hv = 0;  //自機の左右方向の速度
    }
    public static void Main() {
        Program f = new Program(); //自分のオブジェクトを生成
        f.Size = new Size(660, 520); //フォームのサイズを設定
        f.Text = "Game"; //フォーム名を設定
        f.ControlBox = false; //コントロールボックスを非表示に
        f.FormBorderStyle = FormBorderStyle.Fixed3D; //サイズ変更を抑止
        Application.Run(f); //フォームを現出
    }
}

演習29 自弾のアニメーション

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

提出:演習29(未完成、演習28でも可)

講義メモ

テキスト編:p.315「超簡単1桁加算器(Char構造体と静的メソッド、ConsoleKeyInfo構造体とプロパティ)」から
ゲーム開発演習:自弾の発射と上移動、自弾の複数化、敵機の出現

p.315 超簡単1桁加算器(Char構造体と静的メソッド)

・C#のデータ型(p.41)は、内部的には.NET型(p.42)で表現されている
・この.NET型は、構造体として実装されている。
・よって「数値型のデータ型」.Parse(文字列)メソッド(p.45)は、.NET型の構造体のメソッドになっている
 例: int.Parse()は、Int32構造体のParse()メソッド
・char型の.NET型であるChar構造体には、文字を扱うのに便利な下記のような静的メソッドが含まれている
・public static bool IsDigit(char 文字)メソッド:文字が10進数の数字かどうかを返す
・public static double GetNumericValue(char 文字)メソッド:文字を10進数の実数に変換して返す(変換できない場合は-1を返す)
※ double.Parse(文字列)と似た動作なので、double.Parse("" + 文字)と同じ動作になる。
  しかし、double.Parseは変換できない場合は例外を投げる

p.315 超簡単1桁加算器(ConsoleKeyInfo構造体とプロパティ)

・p.293のbool KeyAvailableプロパティは、Consoleクラスの静的プロパティで、キー入力があったかどうかを返す
・これを受けて、押されたキーの情報を得られるのが、ConsoleKeyInfo構造体。
・この構造体オブジェクトを生成して押されたキーの情報を格納するのが、Consoleクラスの静的メソッドであるReadKey
・public static ConsoleKeyInfo ReadKey(bool 表示){…}
・表示がtrueの場合、押されたキーの文字は表示せず、falseだと表示する
 例: ConsoleKeyInfo cki = Console.ReadKey(true); //押されたキーを得る
・ConsoleKeyInfo構造体にあるpublic char KeyCharプロパティを用いると、押されたキーの文字が得られる

p.315 超簡単1桁加算器(ConsoleKeyInfo構造体とプロパティ)

・p.293のbool KeyAvailableプロパティは、Consoleクラスの静的プロパティで、キー入力があったかどうかを返す
・これを受けて、押されたキーの情報を得られるのが、ConsoleKeyInfo構造体。
・この構造体オブジェクトを生成して押されたキーの情報を格納するのが、Consoleクラスの静的メソッドであるReadKey
・public static ConsoleKeyInfo ReadKey(bool 表示){…}
・表示がtrueの場合、押されたキーの文字は表示せず、falseだと表示する
 例: ConsoleKeyInfo cki = Console.ReadKey(true); //押されたキーを得る
・ConsoleKeyInfo構造体にあるpublic char KeyCharプロパティを用いると、押されたキーの文字が得られる

p.317 event02.cs

//p.317 event02.cs
using System;
delegate void Handler(char ch); //デリゲートの宣言(引数有、戻り値型無し)
class EventClass { //イベント発生を担うクラス
    public event Handler KeyHit; //イベントフィールド
    public void OnKeyHit(char ch) { //イベントを発生させるメソッド
        if (KeyHit != null) //イベントフィールドがヌルでなければ
            KeyHit(ch); //デリゲート経由で呼び出す
    }
}
class Show { //イベントで呼び出されるメソッドを持つクラス
    int sum = 0;
    public void keyshow(char ch) { //イベントで呼び出されるメソッド
        if (Char.IsDigit(ch)) { //10進数字か?
            int a = (int)char.GetNumericValue(ch); //実数に変換しintにキャスト
            sum += a; //合計に足し込む
            Console.WriteLine("+ {0}", a); //入力値を表示
            Console.WriteLine("= {0}", sum); //合計を表示
        } else if (ch == 'c') {
            sum = 0; //合計をゼロクリアする
            Console.WriteLine("合計がクリアされました");
        } else { //10進数字でもクリアでもなければ
            return; //何もしないで戻る
        }
    }
}
class event02 {
    public static void Main() {
        ConsoleKeyInfo cki; //キー情報構造体
        EventClass ec = new EventClass();  //イベント発生を担うクラスのインスタンス生成
        Show s = new Show();//イベントで呼び出されるメソッドを持つクラスのインスタンス生成
        ec.KeyHit += new Handler(s.keyshow); //デリゲートにメソッドを登録。
        while (true) { //無限ループ
            if (Console.KeyAvailable) { //何かキーが押されている?
                cki = Console.ReadKey(true); //そのキーを得る
                if (cki.KeyChar == 'x') { //xキーならば
                    break; //抜ける=プログラム終了
                } else {
                    ec.OnKeyHit(cki.KeyChar); //キー情報を渡してイベントを発生させる
                }
            }
        }
    }
}

アレンジ演習:p.317 event02.cs

・テキストp.319の通り、ラムダ式に書き換えよう

作成例

//アレンジ演習:p.317 event02.cs
using System;
delegate void Handler(char ch); //デリゲートの宣言(引数有、戻り値型無し)
class EventClass { //イベント発生を担うクラス
    public event Handler KeyHit; //イベントフィールド
    public void OnKeyHit(char ch) { //イベントを発生させるメソッド
        if (KeyHit != null) //イベントフィールドがヌルでなければ
            KeyHit(ch); //デリゲート経由で呼び出す
    }
}
class Show { //イベントで呼び出されるメソッドを持つクラス
    int sum = 0;
    public void keyshow(char ch) { //イベントで呼び出されるメソッド
        if (Char.IsDigit(ch)) { //10進数字か?
            int a = (int)char.GetNumericValue(ch); //実数に変換しintにキャスト
            sum += a; //合計に足し込む
            Console.WriteLine("+ {0}", a); //入力値を表示
            Console.WriteLine("= {0}", sum); //合計を表示
        } else if (ch == 'c') {
            sum = 0; //合計をゼロクリアする
            Console.WriteLine("合計がクリアされました");
        } else { //10進数字でもクリアでもなければ
            return; //何もしないで戻る
        }
    }
}
class event02 {
    public static void Main() {
        ConsoleKeyInfo cki; //キー情報構造体
        EventClass ec = new EventClass();  //イベント発生を担うクラスのインスタンス生成
        Show s = new Show();//イベントで呼び出されるメソッドを持つクラスのインスタンス生成
        ec.KeyHit += (c) => s.keyshow(c); //デリゲートにメソッドを登録。
        while (true) { //無限ループ
            if (Console.KeyAvailable) { //何かキーが押されている?
                cki = Console.ReadKey(true); //そのキーを得る
                if (cki.KeyChar == 'x') { //xキーならば
                    break; //抜ける=プログラム終了
                } else {
                    ec.OnKeyHit(cki.KeyChar); //キー情報を渡してイベントを発生させる
                }
            }
        }
    }
}

p.320 練習問題 ヒント

・練習問題1は、p.308 lambda02.csをそのまま用いると良い
・練習問題2はevent02.csを基にしてアレンジしよう
① デリゲートの定義はlambda02.csからコピー
② すると、イベントフィールドがnullの場合もreturnが必須になるので、-1を返すとしよう
③ Mainメソッドにおける整数の受け取りを2回にして、その和を表示したら終了とする
④ デリゲートに登録するメソッドを「(x, y) => { return x + y; }」にできるので、keyshowメソッドはShowクラスを含めて不要になる
⑤「10進数字か」のチェックと「実数に変換しintにキャスト」はMainメソッドに移すと良い

作成例

//p.320 練習問題2
using System;
delegate int Handler(int x, int y); //【変更】デリゲートの宣言(引数有、戻り値型有)
class EventClass { //イベント発生を担うクラス
    public event Handler KeyHit; //イベントフィールド
    public int OnKeyHit(int x, int y) { //【変更】イベントを発生させるメソッド
        if (KeyHit != null) { //イベントフィールドがヌルでなければ
            return KeyHit(x, y); //【変更】デリゲート経由で呼び出す
        } else { //【以下追加】
            return -1;
        }
    }
}
class ex1202 {
    public static void Main() {
        ConsoleKeyInfo cki; //キー情報構造体
        EventClass ec = new EventClass();  //イベント発生を担うクラスのインスタンス生成
        ec.KeyHit += (x, y) => { return x + y; }; //【変更】デリゲートに匿名メソッドを登録。
        int temp = -1; //【追加】1値目
        while (true) { //無限ループ
            if (Console.KeyAvailable) { //何かキーが押されている?
                cki = Console.ReadKey(true); //そのキーを得る
                char ch = cki.KeyChar; //その文字を得る
                if (Char.IsDigit(ch)) { //【以下追加・変更】10進数字か?
                    int a = (int)char.GetNumericValue(ch); //実数に変換しintにキャスト
                    if (temp == -1) { //1値目?
                        temp = a;
                    } else { //2値目?
                        int sum = ec.OnKeyHit(temp, a); //キー情報を渡してイベントを発生させる
                        Console.WriteLine("{0} + {1} = {2}", temp, a, sum);
                        break;
                    }
                }
            }
        }
    }
}

今週の話題

販売本数ランキング 今回トップは「桃太郎電鉄ワールド ~地球は希望でまわってる!~(Switch)」GO!
『アスタタ』不発のgumi、SBIが株を追加取得しても距離を取るのはなぜか?【ゲーム企業の決算を読む】GO!
「龍が如くスタジオ」のコアメンバーとなる人材を広く募集―セガ、中途採用オンラインカジュアル面談会を1月25日・26日・27日に開催 GO!
任天堂が時価総額10兆円を超える…DS・Wiiが絶好調だった2007年以来の高値、スイッチ新型に期待高まる GO!
カヤック、2023年世界市場アプリDL数で日本企業1位獲得を報告―新規ハイカジタイトル13本リリース GO!
新作ゲームのUIが複雑すぎる!?『スーサイド・スクワッド』皮切りにユーザー間でスマートなUIと煩雑なUIの議論勃発 GO!
Live2Dデザイナー向けに充実の研修を用意! f4samuraiが明かす採用ポイントと研修カリキュラム【alive 2023セッションレポート】GO!

SteamがAI利用タイトル容認に舵を切る―違法なコンテンツなど問題には適切なチェックで対処狙う GO!
Twitchがスタッフの約35%を解雇予定、昨年のレイオフを合わせると900人程の削減に―海外メディア報道 GO!