講義は休みでしたが今週の話題

ゲームソフト販売本数ランキング:今週1位は「METAL GEAR SOLID Δ: SNAKE EATER(PS5)」 GO!
『ブルアカ』『メイプルストーリー』減衰…ネクソンの日本事業は上期25%の減収で折り返し【ゲーム企業の決算を読む】GO!
プロが教える動画制作、中高生向け無料オンライン講座9-10月 GO!
【キャリアクエスト】FGOだけじゃない、ラセングルならではの挑戦を求めて。“若い会社”で活躍する、とあるゼネラリストの働き方事情 GO!
インディーゲームの祭典「INDIE Live Expo」開催日は11月29日に決定、100作品以上の紹介やアワードを予定 GO!

Roblox、ユーザーに対しての「年齢確認」取り組み強化へ―未成年と大人のコミュニケーションを大きく制限する機能も追加予定 GO!
R-18コンテンツを開いているブラウザタブを検知・スクショするマルウェア Webカムで撮影も 性的脅迫に使用か GO!
App Storeの審査が突如通らず──DMM傘下のスマホゲームでトラブル 「リリースから実装済みの全イラストが修正対象に」GO!
「みんなのGOLF」新作、発売日に異例声明 ユーザーからの“不具合報告”続出で 「現象の原因究明に努める」GO!

講義は休みでしたが今週の話題

ゲームソフト販売本数ランキング:今週1位は再び「マリオカート ワールド(Switch2)」 GO!
【イベント告知】アカマイが「Akamai Cloud Day 2025」を9月24日に開催。特別セッションに安野貴博氏が登壇、ソニーグループや時雨堂の事例も GO!
赤字スタートのセガ、逆襲の鍵は『ソニックレーシング』と待望の『Football Manager』か【ゲーム企業の決算を読む】GO!
業績は減益、株価は高値。スクエニに迫る「物言う株主」の影【ゲーム企業の決算を読む】GO!
グリーの2025年6月期決算は売上6.8%減―新作『まどドラ』投入もゲーム事業はQoQで減収【ゲーム企業の決算を読む】GO!
【キャリアクエスト】「これ、あったら面白い」を実際に作り出せる。セガ入社3年目の若手が語るプランナーという職種、そして自身の展望とは GO!

マイクロソフト「Windows 11高負荷時にSSDを破損する不具合再現できず」と声明。一方Phisonは「Windows側の問題」と調査結果公表 GO!
DDoS・Bot攻撃との“いたちごっこ”に終止符を──AkamaiとCygamesが語る、費用対効果と自動化を両立するセキュリティ戦略【CEDEC2025レポート】GO!

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

:キーボードの状態を得る、画像の左右移動、簡易アニメーション など

テーマ24 背景画像のスクロール【再掲載】

・背景画像を2枚用意して、1枚目の描画開始位置を少しずつずらし、空いた部分にもう1枚をつながるように描画すれば、
 背景画像のスクロールが可能
・なお、C#のGDI+では描画位置を示す座標を負の数や描画範囲外にできる。これにより、画像の一部を表示することが可能
・画像全体が描画範囲の外になったら、描画対象から外すか、描画位置を変えると良い
・背景画像の縦スクロールの場合、描画開始位置が下端を超えたら0に戻すと良い
・背景画像は2枚になるが、画像オブジェクトは1つで良く、2つの参照変数で扱えば良い

提出フォロー:演習24 背景画像の縦スクロール

・背景画像のつなぎ目のY座標を示す変数backyを0で初期化しておき、タイマーによってインクリメントする
 (背景画像.Heightを超えたら0に戻す)
・背景画像の描画を2回に増やし、(0, backy)からと、(0, backy - 背景画像.Height)から描画する
・タイマーのインターバルは10ミリ秒に変更すると良い
・プレイ画面の背景もシームレスにスクロールしたいので、タイマーのインクリメントはシーンに限らず行うように変更

作成例

//テーマ24 背景画像のスクロール
using System; //C#標準クラス用
using System.Windows.Forms; //Application、Formクラス用
using System.Drawing; //Size、Graphics、Image、Penクラス、Color構造体用
class Program : Form { //Formクラスの派生クラス
    enum mode { TITLE, PLAY, OVER }; //タイトル画面、プレイ画面、ゲームオーバー画面
    mode scene = mode.TITLE; //シーンをタイトル画面とする
    Image backi; //画像ファイル用変数
    Timer t = new Timer(); //タイマーを生成
    int sec = 0; //経過秒数
    int backy = 0; //【追加】背景画像のつなぎ目のY座標
    protected override void OnPaint(PaintEventArgs e) { //描画処理のオーバライド
        //全画面に共通の描画処理
        base.OnPaint(e); //元のメソッドの内容を呼び出す
        e.Graphics.DrawImage(backi, 0, backy); //【変更】背景画像①を描画
        e.Graphics.DrawImage(backi, 0, backy - backi.Height); //【追加】背景画像②を①の直上に描画
        Font fm24 = new Font("メイリオ", 24, FontStyle.Bold); //フォントを生成
        Brush bcyan = new SolidBrush(Color.Cyan); //シアン色のソリッドブラシを生成
        switch (scene) { //シーンにより分岐
        case mode.TITLE: //タイトル画面?
            e.Graphics.DrawString("GAME", fm24, bcyan, 250, 200); //文字列を描画
            e.Graphics.DrawString("Hit Enter Key", fm24, bcyan, 200, 400); //文字列を描画
            break;
        case mode.PLAY: //プレイ画面?
            e.Graphics.DrawString("START", fm24, bcyan, 250, 200); //文字列を描画
            e.Graphics.DrawString("" + sec , fm24, bcyan, 0, 0); //【移動】経過秒数を描画
            break;
        }
    }
    public Program() { //コンストラクタ
        DoubleBuffered = true; //ダブルバッファリングを行う
        try { //例外処理対象
            backi = Image.FromFile("backb2.bmp"); //背景画像ファイルを読み込む
        } catch (Exception e) { //例外処理内容
            MessageBox.Show(e.ToString()); //内容をメッセージボックスに表示
        }
        KeyDown += new KeyEventHandler(OnKeyDown); //キー押し下げ時のメソッドを登録
        t.Tick += new EventHandler(OnTimer); //タイマーイベント時のメソッドを登録
        t.Interval = 10; //【変更】タイマー動作間隔を10ミリ秒指定
        t.Start(); //【移動】タイマー開始
    }
    void OnKeyDown(object o, KeyEventArgs e){ //キー押し下げ時に呼ばれるメソッド
        if (e.KeyCode.ToString() == "Escape") { //押されたキーのコードを文字列化したらEscape?
            Close(); //フォームアプリケーションを終了
        }
        if (e.KeyCode.ToString() == "Return") { //押されたキーのコードを文字列化したらReturn?
            scene = mode.PLAY; //シーンをプレイ画面とする
            sec = 0; //経過秒数をクリア
        }
        Invalidate(); //画面再描画を依頼
    }
    void OnTimer(object o, EventArgs e){ //タイマーイベント時に呼ばれるメソッド
        backy = (backy < backi.Height) ? backy + 1 : 0; //【追加】背景画像のつなぎ目のY座標をインクリメント
        sec++; //経過秒数インクリメント
        Invalidate(); //画面再描画を依頼
    }
    static void Main() { //実行用メソッド(publicはなくてOK)
        Program f = new Program(); //自クラスのインスタンスを生成
        f.Text = "Game"; //Form名を設定
        f.StartPosition = FormStartPosition.Manual; //「手動設定」を設定
        Point p = new Point(300,0); //X座標とY座標のPointインスタンスを生成
        f.Location = p; //インスタンスプロパティで初期位置を設定
        f.FormBorderStyle = FormBorderStyle.FixedSingle; //フォームサイズの固定化
        f.ControlBox = false; //コントールボックスの非表示
        f.ClientSize = new Size(640, 480); //クライアントサイズの幅と高さを指定
        Application.Run(f); //生成済のインスタンスを実行
    }
}

テーマ25 キーボードの状態を得る

・キー入力イベントを用いる手法は「キーを押している間、〇〇する」には向かない
・代わりにタイマーイベントを用いてキーボードの状態を得る処理を呼び出してもらうと良い
・これを実現するには、WindowsAPIを提供するDLL(動的リンクライブラリ)の一つである「user32.dll」を直接インポートする
・インポートの書式: [System.Runtime.InteropServices.DllImport("user32.dll")] ※セミコロン不要
・外部定義指定の書式: private static extern short GetKeyState(int nVirtKey);
 ※インポートと外部定義指定はクラス定義の先頭で行うこと
・これで、GetKeyStateメソッドが利用可能になり、引数としてKeys列挙子をint型にキャストして与えると、そのキーが押されていれば
 負の数が返される
・Keys列挙型については ⇒ https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.keys

演習25 キーボードの状態を得る

・タイマーで変数secを無条件にインクリメントするのではなく、上矢印キーが押されていたらインクリメントするようにしよう
・なお、上矢印キーのKeys列挙子は Keys.Up

提出:演習25 キーボードの状態を得る

講義メモ

テキスト編:p.323「例外処理の基礎(tryブロック・catchブロック)」から
ゲーム開発演習:キーボードの状態を得る、画像の左右移動、簡易アニメーション など

p.321 例外処理の基礎

・プログラムによって異常状態が発生すると「実行時エラー」として、発生時点で異常終了する。
・この異常状態を「例外(Exception)」として、これを表すクラスのオブジェクトで扱うことができる
・このオブジェクトを受け取って処理する仕組みが例外処理で、例外処理を組み込むことで「実行時エラー」をコントロールできる。
・例えば、p.322 exception01.cs のように、double.Parse()メソッドに実数に変換できない文字列を渡すと、
 Exceptionクラスの派生クラスであるFormatExceptionクラスのオブジェクトが生成され、例外処理がないので異常終了する。
・以下が異常終了時のメッセージとその解説:
ハンドルされていない例外: System.FormatException: 入力文字列の形式が正しくありません。
 ⇒「ハンドルされていない」とは例外処理がないか例外処理対象になっていないことを示す
 ⇒「System.FormatException」は形式例外を表すクラス
 ⇒「入力文字列の形式が正しくありません。」はこのクラスのMessageプロパティにある文字列
   場所 System.Number.ParseDouble(String value, NumberStyles options, NumberFormatInfo numfmt)
   場所 System.Double.Parse(String s)
   場所 MyClass.Main() 場所 F:\ha241_AkibaC#\Project1\Project1\exception01.cs:行 9
 ⇒下から上に向かって例外発生の経緯を示す。C#で実際にどう扱われたかがわかる
 ⇒一番下の1行で「発生の起因となったクラス名.メソッド名 場所:ソースファイル名:行番号」が明示されている

p.323 例外処理の基礎(続き)try-catch

・例外処理対象はtryブロックの記述によって指定する
・tryブロック内で例外が発生したら、その直後にcatchブロックを記述することで、対処ができる
・catchブロックで対処が行われたら例外状態は解消され、処理が続行される
・ただし、tryブロック内に複数の記述があり、その途中で例外が発生した場合は、発生行の下の行は実行されない
・例:下記の場合、①④⑤の順に実行され、②③はスキップされる
try {
 ①例外が発生する処理
 ②処理
 ③例外が発生する処理
} catch {
 ④対処の処理
}
⑤通常処理

アレンジ演習:p.324 exception02.cs

・元のプログラムでは実数除算なのでゼロ除算の例外が発生しないので、形式例外(FormatException)のみになる
・そこで、変数a、bをint型にして、ゼロ除算の例外処理も記述しよう
・なお、実数除算で0.0以外の正の値を0.0で割ると∞、0.0以外の負の値を0.0で割ると-∞,0.0を0.0で割ると
 NaN(Not a Number=非数)となる。

作成例

//アレンジ演習:p.324 exception02.cs
using System;
class MyClass
{
    public static void Main() {
        int a = 0, b = 0; //【変更】
        Console.Write("割られる数--");
        string strA = Console.ReadLine();
        try { //例外処理対象範囲①
            a = int.Parse(strA); //【変更】形式例外発生の可能性
        }
        catch { //例外処理
            Console.WriteLine("不適切な入力です"); //形式例外発生時の処理
        }
        Console.Write("割る数---");
        string strB = Console.ReadLine();
        try { //例外処理対象範囲②
            b = int.Parse(strB); //【変更】形式例外発生の可能性
        }
        catch {
            Console.WriteLine("不適切な入力です"); //形式例外発生時の処理
        }
        try { //【以下追加】例外処理対象範囲③
            Console.WriteLine("{0} ÷ {1} = {2}", a, b, a / b); //ゼロ除算例外発生の可能性
        }
        catch {
            Console.WriteLine("除算はできません"); //ゼロ除算発生時の処理
        }
    }
}

作成例

//アレンジ演習:p.327 exception03.cs
using System;
class exception03 {
    public static void Main() {
        int[] arr = new int[5];
        try { //例外処理範囲
            //下2行でオーバーフロー例外、形式例外発生の可能性
            Console.Write("添字:"); int a = int.Parse(Console.ReadLine()); //【追加】
            Console.Write("代入値:"); int b = int.Parse(Console.ReadLine()); //【追加】 
            arr[a] = b; //【変更】添字範囲外例外発生の可能性
        }
        catch (IndexOutOfRangeException io) { //添字範囲外例外処理
            Console.WriteLine(io);
            Console.WriteLine("[io]---------");
            Console.WriteLine(io.Source);
            Console.WriteLine("[io.Source]---------");
            Console.WriteLine(io.Message);
            Console.WriteLine("[io.Message]---------");
            Console.WriteLine(io.ToString());
            Console.WriteLine("[io.ToString()]---------");
            Console.WriteLine(io.TargetSite);
            Console.WriteLine("[io.TargetSite]---------");
        }
    }
}

p.328 例外処理の基礎(続き)catchの複数指定

・try-catch構文は、if-elseif構文に近い位置づけでcatchを複数指定できる
・if-elseif構文と同様に、最初に行われたcatchのみが実行され、それ以外のcatchは実行されない
・この時、例外クラスの継承関係において、上位のクラスのcatchが前方にあると、後方のcatchが文法エラーになるので注意
・なお、例外クラスの継承関係において、上位のクラスのcatchを記述することで、この派生クラスの例外処理をまとめて行うことができる
※ チームルールにより、catch{…}が非推奨の場合、catch(Exception)、catch(SystemException)も非推奨とすることが多い

アレンジ演習:p.329 excepition04.cs

・このプログラムではDivideByZeroExceptionしか発生しないので、例外処理対象を書き換えよう
・そして、IndexOutOfRangeException、DivideByZeroException、これ以外の例外(例:FormatException)の3種が
 コンソールからの入力値によって発生するようにしよう

作成例

//アレンジ演習:p.329 excepition04.cs
using System;
class exception04 {
    public static void Main() {
        int x = 10, y = 0;
        try {
            //下2行でオーバーフロー例外、形式例外発生の可能性
            Console.Write("x:"); x = int.Parse(Console.ReadLine()); //【追加】
            Console.Write("y:"); y = int.Parse(Console.ReadLine()); //【追加】 
            Console.WriteLine("{0} / {1} = {2}", x, y, x / y); //ゼロ除算例外発生の可能性
            int[] a = new int[2]; //【追加】
            a[x] = y; //【追加】添字範囲外例外発生の可能性
            Console.WriteLine("a[{0}] = {1}", x, y); //【追加】
        }
        catch (IndexOutOfRangeException io) { //添字範囲外例外処理
            Console.WriteLine(io.Message);
        }
        catch (DivideByZeroException) { //ゼロ除算例外処理
            Console.WriteLine("0で割っちゃだめだよ");
        }
        catch (Exception ex) { //上記以外の例外処理
            Console.WriteLine("その他の例外:" + ex.Message); //【変更】
        }
    }
}

p.330 finallyブロック

・例外処理において、例外発生の有無にかかわらず実行したい処理がある場合、finallyブロックに記述できる
・例えば、データベースや通信などの外部リソースからの切断のような後始末を確実に行う場合に用いる
・書式例: try { ①例外処理対象 } catch(例外名) { ②例外処理 } finally { ③後始末処理 }
・動作は下記のように分岐する
 ・①で例外が発生しなければ、③を実行し、処理は続行
 ・①で例外が発生し、catchされたら、②を実行し、③を実行し、処理は続行
 ・①で例外が発生し、catchされなかったら、③を実行してから、異常終了
※後始末の処理内容によっては、③の中にさらに例外処理を記述する場合がある

アレンジ演習:p.331 exception05.cs

・例外が発生してcatchされなかった場合を試すために「catch (Exception e)」を「catch (FormatException e)」に変更しよう
・すると、整数に変換できない文字列の入力時の例外処理になり、オーバーフロー例外発生時にはcatchされなくなる
・よって「2200000000」等を入力すると、finallyブロックの内容を実行後に異常終了する

作成例

//アレンジ演習:p.331 exception05.cs
using System;
class exception05 {
    public static void Main() {
        string strWarusu; //入力用
        int x; //変換結果
        bool bEnd = false; //終了フラグをオフにする
        while (true) { //無限ループ
            Console.Write("割る数--- ");
            strWarusu = Console.ReadLine();
            try { //例外処理対象
                x = int.Parse(strWarusu); //形式例外、オーバーフロー例外発生の可能性
                Console.WriteLine("10 / {0} = {1}", x, 10 / x); //ゼロ除算例外発生の可能性
            }
            catch (DivideByZeroException d) { //ゼロ除算例外処理
                Console.WriteLine(d.Message);
            }
            catch (FormatException e) { //【変更】形式例外処理
                Console.WriteLine("形式例外:" + e.Message); //【変更】
            }
            finally { //例外発生有無に関わらず実行する処理
                Console.Write("続けますか(Y/N)---");
                if (Console.ReadLine()[0] == 'N') { //入力文字列の先頭文字が'N'?
                    bEnd = true; //終了フラグをオンにする
                }
            } //※例外がcatchされていないときはここで異常終了
            if (bEnd) { //終了フラグがオン?※例外がcatchされていないときは実行されない
                break; //ループを抜ける
            }
        }
    }
}

p.333 throw文(例外のコールスタック)

・p.326のStackTraceプロパティの「意味」にある「コールスタック」は呼び出しの階層のことで、メソッドが他のメソッドを呼びだし、
 そのメソッドがさらに他のメソッドを呼ぶ場合、呼び出した順に戻ってくること。
・例:下記の場合、処理①、処理③、処理⑤、処理④、処理②の順に実行される
 public static void Main() { 処理①; f1(); 処理②; }
 void f1() { 処理③; f2(); 処理④; }
 void f2() { 処理⑤; }
・なお、呼び出されたメソッドで例外が発生した場合、そこでは異常終了せず、呼び出し元に戻戻っていく
・そして、最終的に例外処理が見つからなければ、もっとも上位の呼び出しにおいて異常終了する
・例:下記の場合、処理①、処理③、処理⑤の順に実行され、f2に例外処理がないので、f1に戻され、f1に例外処理がないのでMainに戻され、
 Mainにもないので異常終了する
 public static void Main() { 処理①; f1(); 処理②; }
 void f1() { 処理③; f2(); 処理④; }
 void f2() { 処理⑤で例外発生; }
・この仕組みを例外のコールスタックといい、呼び出し先での例外を呼び出し元でcatchできるので部品化に向く
・例えば、汎用的な処理を記述した部品はUI(ユーザインタフェース)を持たないので、UIを持つ処理から呼び出して、
 例外処理はUIで行えば良い

p.333 throw文

・プログラマが意図的に例外を発生させる=例外を投げるのがthrow文
・書式: throw 例外オブジェクト;
・例外オブジェクトは「new Exceptionクラスか派生クラス()」で生成できる
・あるいは、catch (例外クラス 引数){…} の引数を用いることもできる
・このことを利用して、呼び出されたメソッドの中で例外処理を行い(正常終了させずに)呼び出し元に例外を投げて、
 対処を任せることが多い
・なお、呼び出されたメソッドの中で例外処理を行うと、正常状態で呼び出し元に戻るので、不都合が発生する場合があるので注意
・例:下記の場合、処理①、処理③、処理⑤の順に実行され、例外が発生すると例外処理されてしまい、④②が実行される
 public static void Main() { 処理①; f1(); 処理②; }
 void f1() { 処理③; f2(); 処理④; }
 void f2() { try { 処理⑤; } catch(Exception e) {} } //throw無し
・例:下記の場合、処理①、処理③、処理⑤の順に実行され、例外が発生すると、f2に例外処理がないので、f1に戻され、
 f1に例外処理がないのでMainに戻され、Mainにもないので異常終了する
 public static void Main() { 処理①; f1(); 処理②; }
 void f1() { 処理③; f2(); 処理④; }
 void f2() { try { 処理⑤; } catch(Exception e) {throw e;} } //throw有

p.334 exception06.cs

using System;
class MyClassA {
    public void Calc() { //④かr呼ばれる
        int x = 10, y = 0; //⑤
        int[] arr = new int[5]{1, 2, 3, 4, 5}; //⑥
        try { //例外処理範囲
            Console.WriteLine("{0}, {1}", arr[x], x / y); //⑦添字範囲外例外発生
        }
        catch (IndexOutOfRangeException i) { //⑧添字範囲外例外処理
            Console.WriteLine(i.Message); //⑨
            DivideByZeroException d = new DivideByZeroException(); //⑩ゼロ除算例外オブジェクト生成
            Console.WriteLine("外側にthrowします"); //⑪
            throw d; //⑫ゼロ除算例外オブジェクトを投げる
        }
    }
}
class MyClassB {
    public void Calc() { //②から呼ばれる
        MyClassA a = new MyClassA(); //③
        try { //例外処理範囲
            a.Calc(); //④ ⑬ゼロ除算例外発生
        }
        catch (DivideByZeroException d) { //⑭ゼロ除算例外処理
            Console.WriteLine("外側のcatch節です"); //⑮
            Console.WriteLine(d.Message); //⑯
        }
    }
}
class exception06 {
    public static void Main() {
        MyClassB b = new MyClassB(); //①
        b.Calc(); //② ⑰
    }
}

アレンジ演習:p.334 exception06.cs

・このプログラムでは添字範囲外例外が発生しているのに、プログラマがゼロ除算例外に書き換えてしまっており、
 本来は避けるべき作りになっている
・正しい対処を行うように書き換えよう
・変数x、yをコンソールから与えることで、添字範囲外例外とゼロ除算例外の発生を切り替えると良い

作成例

//アレンジ演習:p.334 exception06.cs
using System;
class MyClassA {
    public void Calc() { //④かr呼ばれる
        int x = 10, y = 0; //⑤
        int[] arr = new int[5]{1, 2, 3, 4, 5}; //⑥
        try { //例外処理範囲
            //下2行でオーバーフロー例外、形式例外発生の可能性
            Console.Write("x:"); x = int.Parse(Console.ReadLine()); //【追加】
            Console.Write("y:"); y = int.Parse(Console.ReadLine()); //【追加】
            Console.WriteLine("{0}, {1}", arr[x], x / y); //⑦添字範囲外例外発生
        }
        catch (IndexOutOfRangeException i) { //⑧添字範囲外例外処理
            Console.WriteLine(i.Message); //⑨
            //DivideByZeroException d = new DivideByZeroException(); //【削除】⑩ロ除算例外オブジェクト生成
            Console.WriteLine("外側にthrowします"); //⑪
            throw i; //【変更】⑫例外オブジェクトを投げる
        }
    }
}
class MyClassB {
    public void Calc() { //②から呼ばれる
        MyClassA a = new MyClassA(); //③
        try { //例外処理範囲
            a.Calc(); //④ ⑬添字範囲外例外発生
        }
        catch (IndexOutOfRangeException d) { //【変更】⑭添字範囲外例外処理
            Console.WriteLine("外側のcatch節です"); //⑮
            Console.WriteLine(d.Message); //⑯
        }
    }
}
class exception06 {
    public static void Main() {
        MyClassB b = new MyClassB(); //①
        b.Calc(); //② ⑰
    }
}

p.336 throw文(例外をそのまま投げる)

・catchブロックの中で、引数で受け取った例外オブジェクトをそのまま投げる場合「throw;」のみで良い

アレンジ演習:p.336 exception07.cs

・変数x、yをコンソールから与えることで、ゼロ除算例外の以外の例外発生時の処理を確認しよう
 ⇒ ゼロ除算例外のみがthrowの対象になる

作成例

//アレンジ演習:p.336 exception07.cs
using System;
class MyClass {
    int x = 5, y = 0;
    public void Calc() {
        try { //例外処理範囲
            //下2行でオーバーフロー例外、形式例外発生の可能性
            Console.Write("x:"); x = int.Parse(Console.ReadLine()); //【追加】
            Console.Write("y:"); y = int.Parse(Console.ReadLine()); //【追加】
            Console.WriteLine("{0}", x / y); //ゼロ除算例外発生の可能性
        }
        catch (DivideByZeroException d) { //ゼロ除算例外処理
            Console.WriteLine(d.Message);
            throw; //引数で受け取った例外オブジェクトをそのまま投げる
        }
    }
}
class MyClassB {
    public static void Main() {
        MyClass mc = new MyClass();
        try { //例外処理範囲
            mc.Calc(); //ゼロ除算例外が投げられて来る
        }
        catch (DivideByZeroException d) { //ゼロ除算例外処理
            Console.WriteLine(d.TargetSite); //発生したメソッド名を表示
        }
    }
}

p.337 tryのネスト

・tryブロックの中に、さらにtry-catch構造を置くことができる
・これにより、例外処理内容の重複の整理などが可能になるが、可読性が落ちやすい
・よって、発生する例外の種類によって実行可否の制御をしたい場合などに用いることが多い

p.338 exception08.cs 013行目 正誤

・誤:"{0},{1}"
・正:"{0}"

アレンジ演習:p.338 exception08.cs

・ゼロ除算例外が固定で発生するのではなく、入力値により、ゼロ除算例外、添字範囲外例外、オーバフロー例外等が起こるようにして、
 例外ごとの動作を確認しよう
・例外処理範囲の末尾や外に実行文を置いて確認すると良い

作成例

//アレンジ演習:p.338 exception08.cs
using System;
class exception08 {
    public static void Main() {
        int x = 10, y = 0;
        try { //例外処理範囲(外)
            try { //例外処理範囲(内)
                //下2行でオーバーフロー例外、形式例外発生の可能性
                Console.Write("x:"); x = int.Parse(Console.ReadLine()); //【追加】
                Console.Write("y:"); y = int.Parse(Console.ReadLine()); //【追加】
                Console.WriteLine("{0}", x / y); //ゼロ除算例外発生の可能性
                int[] a = new int[2]; //【追加】
                a[x] = y; //【追加】添字範囲外例外発生の可能性
                Console.WriteLine("a[{0}] = {1}", x, y); //【追加】
            }
            catch (IndexOutOfRangeException i) { //添字範囲外例外処理
                Console.WriteLine("例外処理(内)" + i.Message); //【変更】
            }
            Console.WriteLine("例外処理範囲(内)終了"); //【追加】tryのネストによりこの記述が可能
        }
        catch (DivideByZeroException d) { //ゼロ除算例外処理
            Console.WriteLine("例外処理(外)" + d.Message); //【変更】
        }
    }
}

p.338 13.5 独自の例外を作る

・全例外クラスの基本クラスであるExceptionクラスは継承可能なので、プログラマが独自の例外クラスを派生クラスとして定義できる
・また、一部の例外クラスを除き、提供されているExceptionクラスの派生クラスも継承可能
 ※ 継承不可なsealedクラスの例: IndexOutOfRangeException
・よって、プログラマが独自の例外クラスを記述する場合、無条件にExceptionクラスを継承するのではなく、
 意味合いが近い派生クラスを用いる方が、継承関係を有効に活用できる
・例: ローカルルールで定めた形式に不一致なことを例外で扱うなら、FormatExceptionを継承
 ※ チームルールで定めることもある
・独自の例外クラスでは、p.326で示した主なメンバのうち、Message/HelpLinkプロパティ、ToStiringメソッドをオーバライドすることが
 推奨される
・なお、exception09.csでは、DivideByZeroExceptionのプロパティやメソッドをオーバライドすることを目的としている
 ※ チームルールによっては、この行為は推奨されないことがある

アレンジ演習:exception09.cs

・FormatExceptionを継承するMinusValExceptionを自前で定義し、負の数を例外化するために用いよう
・Messageプロパティ、ToStiringメソッドをオーバライドすること
・変数xの値が負の数であれば、MinusValExceptionを投げ、FormatExceptionでcatchしてMessageプロパティ、
 ToStiringメソッドを呼び出して確認しよう

作成例

//アレンジ演習:exception09.cs
using System;
class MyEx : DivideByZeroException { //ゼロ除算例外を継承する自前の例外クラス
    public new string Message = "0で割るエラーです"; //データメンバの上書き
    public new string HelpLink = "http://www.kumei.ne.jp/c_lang/"; //同上
    public override string ToString() { //メソッドのオーバライド
        return "0で割ってはいけません!!";
    }
}
class MinusValException : FormatException { //【以下追加】形式例外を継承する自前の例外クラス
    public new string Message = "負の数にはできません"; //データメンバの上書き
    public override string ToString() { //メソッドのオーバライド
        return "形式エラー(負の数は不可)";
    }
}
class exception09 {
    public static void Main() {
        int x;
        Console.Write("割る数(正の整数)--- "); //【変更】
        string strWaru = Console.ReadLine();
        try {
            x = int.Parse(strWaru);
            if (x == 0) { //除数が0であれば
                throw new MyEx(); //ゼロ除算例外を継承する自前の例外のオブジェクトを生成して投げる
            }
            if (x < 0) { //【以下追加】除数が負の数であれば
                throw new MinusValException(); //形式例外を継承する自前の例外のオブジェクトを生成して投げる
            }
            Console.WriteLine("12 / {0} = {1}",x, 12 / x); //ゼロ除算は起こらない
        }
        catch (MyEx me) { //ゼロ除算例外を継承する自前の例外処理
            Console.WriteLine(me.ToString()); //オーバライドしたメソッドを呼ぶ
            Console.WriteLine(me.Message); //上書きしたデータメンバを表示
            Console.WriteLine(me.HelpLink + "を参照"); //同上
        }
        catch (Exception e) { //上記以外の例外処理
            Console.WriteLine(e.ToString()); //【追加】
            Console.WriteLine(e.Message);
        }
    }
}

p.341 checked

・int.Parseメソッド(Int32.Parseメソッド)などでは、変換結果がint.MaxValueを超えたり、
 int.MinValueを下回るとオーバーフローとなり、オーバーフロー例外が投げられる
・しかし、計算の途中でオーバーフローが発生した場合、例外を投げない
・例: int a = int.MaxValue, b = a + 1; //bはint.MinValueになる
・このような場合にもオーバーフロー例外が投げられるようにするには、checkedブロックの中に置けば良い
・例: checked { int a = int.MaxValue, b = a + 1; } //オーバーフロー例外が投げられる
 ※ テキストでは「checked()」も紹介されているが推奨されない場合が多い

アレンジ演習:p.343 checked02.cs

・代入におけるオーバーフローを、初期化におけるオーバーフローに書き換えて、オーバーフロー例外が投げられることを確認しよう

作成例

//アレンジ演習:p.343 checked02.cs
using System;
class checked02 {
    public static void Main() {
        int x, y, z;
        try {
            checked { //このブロックでは計算の途中でオーバーフローが発生した場合、例外を投げる
                x = int.MaxValue;
                y = 1;
                int w = x + y; //【追加】初期化でもオーバーフロー例外が発生
                z = x + y;
                Console.WriteLine(z);
            }
        }
        catch (OverflowException o) { //オーバーフロー例外処理
            Console.WriteLine(o.Message);
            Console.WriteLine(o.StackTrace);
        }
    }
}

p.341 unchecked

・checkedブロックの中で、その一部において計算時のオーバーフローで例外を投げないようにしたい場合に用いる

アレンジ演習:p.343 checked02.cs・改

・uncheckedを用いて、初期化におけるオーバーフローでは例外を投げないようにできることを確認しよう

作成例

//アレンジ演習:p.343 checked02.cs・改
using System;
class checked02 {
    public static void Main() {
        int x, y, z;
        try {
            checked { //このブロックでは計算の途中でオーバーフローが発生した場合、例外を投げる
                x = int.MaxValue;
                y = 1;
                unchecked { //【追加】このブロックでは計算の途中でオーバーフローが発生した場合、例外を投げない
                    int w = x + y; //初期化でもオーバーフローになるが、例外を投げない
                    Console.WriteLine("初期化でもオーバーフローになるが、例外を投げないので続行"); //【追加】
                }
                z = x + y; //オーバーフロー例外が発生し、例外を投げる
                Console.WriteLine(z);
            }
        }
        catch (OverflowException o) { //オーバーフロー例外処理
            Console.WriteLine(o.Message);
            Console.WriteLine(o.StackTrace);
        }
    }
}

p.344 練習問題 ヒント ex13.cs

・「forループで行い」とあるが、継続/終了条件の記載がないので、これを省いた無限ループにする
・forループ内では動作が分かるように途中の値を表示すると良い
・forループ内にcatchブロックを置くなら、breakも記述することで、例外発生時にforループを抜けるようにしよう
・「byte型変数1 = byte型変数1 * byte型変数2」とすると暗黙的変換ができないというエラーになるので、byte型にキャストするか、
 複合代入演算子を用いると良い
 例: byte型変数1 = (byte)(byte型変数1 * byte型変数2)
 例: byte型変数1 *= byte型変数2

作成例

//p.344 練習問題
using System;
class ex13 {
    public static void Main() {
        byte x = 1;
        for (byte b = 1; ; b++) { //無限ループ
            try { //例外処理範囲
                checked { //このブロックでは計算の途中でオーバーフローが発生した場合、例外を投げる
                    x *= b; //オーバーフロー例外発生の可能性
                }
                Console.WriteLine(x);
            }
            catch (OverflowException) {
                Console.WriteLine("オーバーフロー発生により終了");
                break; //ループを抜ける
            }
        }
    }
}

今週の話題

ゲームソフト販売本数ランキング:今週1位も「マリオカート ワールド(Switch2)」 GO!
サイバーステップはオンクレ『トレバ』不振で5期連続の赤字、暗号資産事業で活路を模索【ゲーム企業の決算を読む】 GO!
「スイッチ2」全世界売上600万台突破。『マリカ ワールド』も563万本、初代スイッチを大きく上回る GO!
【キャリアクエスト】ツールが便利になる今、プログラマーに必要とされるものとは?アトラスで働くなかで見えてきた「プログラマーならではの仕事」GO!
「東京ゲームショウ2025」ビジネスデイ有料事前登録が開始!SIE社長・西野秀明氏による基調講演も決定 GO!
音楽はAIとの音声対話で作る時代に入った。Riffusion改めProducer.aiのAIプロデューサーと日本語で会話するだけで曲を作ってくれるのだ GO!
インディーゲームの祭典「BitSummit the 13th」なんと5万8,000人以上が来場。来年は開催時期5月に、猛暑は回避? GO!

成人向けゲームの規制に揺れるitch.io、寄付すら不可の「無料の成人向けゲーム」に限り表示再開を実施―決済代行業者を経由しないため GO!
「プラチナゲームズ」を名乗る不審なメールに注意喚起―求人募集を装って送付、フィッシング詐欺等の可能性も GO!

講義メモ ゲーム開発演習

ゲーム開発演習:画面遷移、タイマー処理 など

テーマ20 画面遷移【再掲載+α】

・タイトル画面とプレイ画面とゲームオーバー画面のように、画面が移り変わることを画面遷移という
・画面ごとに違うプログラムを用意して呼び出すこともあるが、プログラム内で「今はどの画面なのか」を変数で保持すればシンプルに記述できる
・この変数をゲームモードやシーン等と呼び、プログラム内で共有することで、画面遷移を見やすく記述できる
・ゲームモードやシーンに用いる値は列挙型(p.66)にすると意味がわかりやすくなる
・構造例:
class クラス名 : Form { //Formクラスを継承
  enum { TITLE, PLAY, OVER }; //タイトル画面、プレイ画面、ゲームオーバー画面
  int ゲームモード = TITLE;
  その他の初期化処理を記述
  protected override void OnPaint(PaintEventArgs e) { //描画処理のオーバライド
    全画面に共通の描画処理
    ゲームモードがTITLEならばタイトル画面を描画
    ゲームモードがPLAYならばプレイ画面を描画
    ゲームモードがOVERならばゲームオーバー画面を描画
  }
  public static void Main() { //実行用メソッド
    :
  }
  クラス名() { //コンストラクタ
    :
  }
  キー押し下げ時に実行されるメソッド {
    共通処理(Escキーが押し下げられたら終了)
    ゲームモードがTITLEの時にEnterやSpaceキーが押し下げられたらゲームモードをPLAYに
    ゲームモードがOVERの時にEnterキーが押し下げられたらゲームモードをTITLEやPLAYに
  }
}

テーマ21 画面再描画【再掲載】

・プログラム側で画面を書き換えても、そのままでは実画面には反映しない
・そのため、画面再描画をシステムに依頼する必要がある(OnPaintの直接実行は不可)
・画面再描画をシステムに依頼するにはContolクラスの静的メソッドInvalidateを引数なしで呼べばよい(「Contol.」は省略可)
・書式:Invalidate(); //画面再描画を依頼

演習21 タイトル画面の表示とプレイ画面への遷移

・テーマ20「画面遷移」の「構造例」を実装してタイトル画面の表示とプレイ画面への遷移を確認しよう
・タイトル画面は中央に「GAME」、中央下に「Hit Enter Key」と適当なフォントで表示
・プレイ画面は中央に「START」と適当なフォントで表示
 ※チェス盤、機体などの描画はいったん削除
・エンターキー(キーコード:return)が押されたら、プレイ画面に遷移する
・この時、画面がチラつくが、対処は次のテーマで扱う

作成例

//演習21 タイトル画面の表示とプレイ画面への遷移
using System; //C#標準クラス用
using System.Windows.Forms; //Application、Formクラス用
using System.Drawing; //Size、Graphics、Image、Penクラス、Color構造体用
class Program : Form { //Formクラスの派生クラス
    enum mode { TITLE, PLAY, OVER }; //タイトル画面、プレイ画面、ゲームオーバー画面
    mode scene = mode.TITLE; //シーンをタイトル画面とする
    Image backi; //画像ファイル用変数
    protected override void OnPaint(PaintEventArgs e) { //描画処理のオーバライド
        //全画面に共通の描画処理
        base.OnPaint(e); //元のメソッドの内容を呼び出す
        e.Graphics.DrawImage(backi, 0, 0); //画像をフォーム左上に配置
        Font fm24 = new Font("メイリオ", 24, FontStyle.Bold); //フォントを生成
        Brush bcyan = new SolidBrush(Color.Cyan); //シアン色のソリッドブラシを生成
        switch (scene) { //シーンにより分岐
        case mode.TITLE:  //タイトル画面?
            e.Graphics.DrawString("GAME", fm24, bcyan, 250, 200); //文字列を描画
            e.Graphics.DrawString("Hit Enter Key", fm24, bcyan, 200, 400); //文字列を描画
            break;
        case mode.PLAY:  //プレイ画面?
            e.Graphics.DrawString("START", fm24, bcyan, 250, 200); //文字列を描画
            break;
        }
    }
    public Program() { //コンストラクタ
        try { //例外処理対象
            backi = Image.FromFile("backb2.bmp"); //背景画像ファイルを読み込む
        } catch (Exception e) { //例外処理内容
            MessageBox.Show(e.ToString()); //内容をメッセージボックスに表示
        }
        KeyDown += new KeyEventHandler(OnKeyDown); //キー押し下げ時のメソッドを登録
    }
    void OnKeyDown(object o, KeyEventArgs e){ //キー押し下げ時に呼ばれるメソッド
        if (e.KeyCode.ToString() == "Escape") { //押されたキーのコードを文字列化したらEscape?
            Close(); //フォームアプリケーションを終了
        }
        if (e.KeyCode.ToString() == "Return") { //押されたキーのコードを文字列化したらReturn?
            scene = mode.PLAY; //シーンをプレイ画面とする
        }
        Invalidate(); //画面再描画を依頼
    }
    static void Main() { //実行用メソッド(publicはなくてOK)
        Program f = new Program(); //自クラスのインスタンスを生成
        f.Text = "Game"; //Form名を設定
        f.StartPosition = FormStartPosition.Manual; //「手動設定」を設定
        Point p = new Point(300,0); //X座標とY座標のPointインスタンスを生成
        f.Location = p; //インスタンスプロパティで初期位置を設定
        f.FormBorderStyle = FormBorderStyle.FixedSingle; //フォームサイズの固定化
        f.ControlBox = false; //コントールボックスの非表示
        f.ClientSize = new Size(640, 480); //クライアントサイズの幅と高さを指定
        Application.Run(f); //生成済のインスタンスを実行
    }
}

テーマ22 タイマー処理

・画面を自動的に変更したり、一定時間おきに何かを行いたい場合、タイマーを用いることが出来る
・タイマーは、System.Windows.Forms.Timerクラスのデフォルトコンストラクタで生成できる
・このTickイベントに、タイマーに呼び出して欲しいメソッドを登録する
・登録には、EventHandlerデリゲートを用いる
 例:
 Timer t = new Timer();
 t.Tick += new EventHandler(タイマーに呼び出して欲しいメソッド名);
・そして、インスタンスプロパティIntervalに、動作間隔をミリ秒で指定する
 例:t.Interval = 20;
・それから、インスタンスメソッドStart()を呼ぶことでタイマーが動作する
・なお、タイマーに呼び出して欲しいメソッドは、戻り値型はvoid、引数型は(object, EventArgs)とすること
 ※ 通常、メソッド内でこの2引数を用いることはないが指定は必須

作成例

//演習22 画面に秒数を表示
using System; //C#標準クラス用
using System.Windows.Forms; //Application、Formクラス用
using System.Drawing; //Size、Graphics、Image、Penクラス、Color構造体用
class Program : Form { //Formクラスの派生クラス
    enum mode { TITLE, PLAY, OVER }; //タイトル画面、プレイ画面、ゲームオーバー画面
    mode scene = mode.TITLE; //シーンをタイトル画面とする
    Image backi; //画像ファイル用変数
    Timer t = new Timer(); //【追加】タイマーを生成
    int sec = 0; //【追加】経過秒数
    protected override void OnPaint(PaintEventArgs e) { //描画処理のオーバライド
        //全画面に共通の描画処理
        base.OnPaint(e); //元のメソッドの内容を呼び出す
        e.Graphics.DrawImage(backi, 0, 0); //画像をフォーム左上に配置
        Font fm24 = new Font("メイリオ", 24, FontStyle.Bold); //フォントを生成
        Brush bcyan = new SolidBrush(Color.Cyan); //シアン色のソリッドブラシを生成
        switch (scene) { //シーンにより分岐
        case mode.TITLE:  //タイトル画面?
            e.Graphics.DrawString("GAME", fm24, bcyan, 250, 200); //文字列を描画
            e.Graphics.DrawString("Hit Enter Key", fm24, bcyan, 200, 400); //文字列を描画
            break;
        case mode.PLAY:  //タイトル画面?
            e.Graphics.DrawString("START", fm24, bcyan, 250, 200); //文字列を描画
            break;
        }
        e.Graphics.DrawString("" + sec , fm24, bcyan, 0, 0); //【追加】経過秒数を描画
    }
    public Program() { //コンストラクタ
        try { //例外処理対象
            backi = Image.FromFile("backb2.bmp"); //背景画像ファイルを読み込む
        } catch (Exception e) { //例外処理内容
            MessageBox.Show(e.ToString()); //内容をメッセージボックスに表示
        }
        KeyDown += new KeyEventHandler(OnKeyDown); //キー押し下げ時のメソッドを登録
        t.Tick += new EventHandler(OnTimer); //【追加】タイマーイベント時のメソッドを登録
        t.Interval = 1000; //【追加】タイマー動作間隔を1000ミリ秒指定
        t.Start(); //【追加】タイマー開始
    }
    void OnKeyDown(object o, KeyEventArgs e){ //キー押し下げ時に呼ばれるメソッド
        if (e.KeyCode.ToString() == "Escape") { //押されたキーのコードを文字列化したらEscape?
            Close(); //フォームアプリケーションを終了
        }
        if (e.KeyCode.ToString() == "Return") { //押されたキーのコードを文字列化したらReturn?
            scene = mode.PLAY; //シーンをプレイ画面とする
        }
        Invalidate(); //画面再描画を依頼
    }
    void OnTimer(object o, EventArgs e){ //【追加】タイマーイベント時に呼ばれるメソッド
        sec++; //【追加】経過秒数インクリメント
        Invalidate(); //【追加】画面再描画を依頼
    }
    static void Main() { //実行用メソッド(publicはなくてOK)
        Program f = new Program(); //自クラスのインスタンスを生成
        f.Text = "Game"; //Form名を設定
        f.StartPosition = FormStartPosition.Manual; //「手動設定」を設定
        Point p = new Point(300,0); //X座標とY座標のPointインスタンスを生成
        f.Location = p; //インスタンスプロパティで初期位置を設定
        f.FormBorderStyle = FormBorderStyle.FixedSingle; //フォームサイズの固定化
        f.ControlBox = false; //コントールボックスの非表示
        f.ClientSize = new Size(640, 480); //クライアントサイズの幅と高さを指定
        Application.Run(f); //生成済のインスタンスを実行
    }
}

テーマ23 ダブルバッファリング

・画面に動きがあるアプリケーションでは、画面の書き換えのタイミングのずれによりチラつきが起きる
・これを防ぐには、画像メモリを2重化し、描画を終えた画像メモリを表示用の画像メモリに一気に高速転送する
・この仕組みをダブルバッファリングという
・C#の.NETフレームワーク4.6以降のGDI+では(現行のVisualStudioでは)、
 System.Windows.Forms.FormクラスのDoubleBufferedプロパティをtrueにすれば良い

演習23 プレイ画面に秒数を表示

・起動と同時ではなく、プレイ画面への遷移時にタイマーを起動し、画面左上に経過秒数を表示しよう
・ダブルバッファリングで画面のちらつきに対処しよう

作成例

//演習23 プレイ画面に秒数を表示
using System; //C#標準クラス用
using System.Windows.Forms; //Application、Formクラス用
using System.Drawing; //Size、Graphics、Image、Penクラス、Color構造体用
class Program : Form { //Formクラスの派生クラス
    enum mode { TITLE, PLAY, OVER }; //タイトル画面、プレイ画面、ゲームオーバー画面
    mode scene = mode.TITLE; //シーンをタイトル画面とする
    Image backi; //画像ファイル用変数
    Timer t = new Timer(); //タイマーを生成
    int sec = 0; //経過秒数
    protected override void OnPaint(PaintEventArgs e) { //描画処理のオーバライド
        //全画面に共通の描画処理
        base.OnPaint(e); //元のメソッドの内容を呼び出す
        e.Graphics.DrawImage(backi, 0, 0); //画像をフォーム左上に配置
        Font fm24 = new Font("メイリオ", 24, FontStyle.Bold); //フォントを生成
        Brush bcyan = new SolidBrush(Color.Cyan); //シアン色のソリッドブラシを生成
        switch (scene) { //シーンにより分岐
        case mode.TITLE: //タイトル画面?
            e.Graphics.DrawString("GAME", fm24, bcyan, 250, 200); //文字列を描画
            e.Graphics.DrawString("Hit Enter Key", fm24, bcyan, 200, 400); //文字列を描画
            break;
        case mode.PLAY: //プレイ画面?
            e.Graphics.DrawString("START", fm24, bcyan, 250, 200); //文字列を描画
            e.Graphics.DrawString("" + sec , fm24, bcyan, 0, 0); //【移動】経過秒数を描画
            break;
        }
    }
    public Program() { //コンストラクタ
        DoubleBuffered = true; //【追加】ダブルバッファリングを行う
        try { //例外処理対象
            backi = Image.FromFile("backb2.bmp"); //背景画像ファイルを読み込む
        } catch (Exception e) { //例外処理内容
            MessageBox.Show(e.ToString()); //内容をメッセージボックスに表示
        }
        KeyDown += new KeyEventHandler(OnKeyDown); //キー押し下げ時のメソッドを登録
        t.Tick += new EventHandler(OnTimer); //タイマーイベント時のメソッドを登録
        t.Interval = 1000; //タイマー動作間隔を1000ミリ秒指定
    }
    void OnKeyDown(object o, KeyEventArgs e){ //キー押し下げ時に呼ばれるメソッド
        if (e.KeyCode.ToString() == "Escape") { //押されたキーのコードを文字列化したらEscape?
            Close(); //フォームアプリケーションを終了
        }
        if (e.KeyCode.ToString() == "Return") { //押されたキーのコードを文字列化したらReturn?
            scene = mode.PLAY; //シーンをプレイ画面とする
            sec = 0; //【追加】経過秒数をクリア
            t.Start(); //【移動】タイマー開始
        }
        Invalidate(); //画面再描画を依頼
    }
    void OnTimer(object o, EventArgs e){ //タイマーイベント時に呼ばれるメソッド
        sec++; //経過秒数インクリメント
        Invalidate(); //画面再描画を依頼
    }
    static void Main() { //実行用メソッド(publicはなくてOK)
        Program f = new Program(); //自クラスのインスタンスを生成
        f.Text = "Game"; //Form名を設定
        f.StartPosition = FormStartPosition.Manual; //「手動設定」を設定
        Point p = new Point(300,0); //X座標とY座標のPointインスタンスを生成
        f.Location = p; //インスタンスプロパティで初期位置を設定
        f.FormBorderStyle = FormBorderStyle.FixedSingle; //フォームサイズの固定化
        f.ControlBox = false; //コントールボックスの非表示
        f.ClientSize = new Size(640, 480); //クライアントサイズの幅と高さを指定
        Application.Run(f); //生成済のインスタンスを実行
    }
}

テーマ24 背景画像のスクロール

・背景画像を2枚用意して、1枚目の描画開始位置を少しずつずらし、空いた部分にもう1枚をつながるように描画すれば、
 背景画像のスクロールが可能
・なお、C#のGDI+では描画位置を示す座標を負の数や描画範囲外にできる。これにより、画像の一部を表示することが可能
・画像全体が描画範囲の外になったら、描画対象から外すか、描画位置を変えると良い
・背景画像の縦スクロールの場合、描画開始位置が下端を超えたら0に戻すと良い
・背景画像は2枚になるが、画像オブジェクトは1つで良く、2つの参照変数で扱えば良い

提出:演習24 背景画像の縦スクロール

講義メモ

テキスト編:p.310「12.5 イベント」から
ゲーム開発演習:画面遷移、タイマー処理 など

p.310 12.5 イベント

・コンソールアプリケーションにおける Console.ReadLine()メソッドのように、起動したプログラムが動的に情報を得る
 手段がある
・しかし、Console.ReadLine()メソッドは単なる入力待ちであり、プログラム側からの制御はできない
・そこで、あらかじめ「これが起きたらあれをする」という定義をしておき、必要時に動的に呼び出される仕掛として、
 イベント機能が提供されている。
・キー入力、マウスの操作、通信の受信、時間の経過などの「何かが起きた」ことをイベントの発生という。
・イベントに対して処理を実行するものをイベントハンドラという。
 ※Java言語などではイベントリスナといい、C#の書籍においてもイベントリスナとされていることがある。
・C#では主にフォームアプリケーションでイベントを扱うが、コンソールアプリケーションでもデリゲートを用いることで、
 イベント処理の実装が可能。
・例えば、Enterキー以外のキーの入力を即時に受け取るプログラムを記述できる。

p.312 イベントのプログラミング

・コンソールアプリケーションでもデリゲートを用いてイベント処理を実装する場合の例(クラス側):
 ① 戻り値なし、引数なしのデリゲートを宣言する
 ② イベントの発生を担うクラスを定義する
 ③ ②の中に①を用いるイベントフィールドを定義する
  書式: public event デリゲート名 イベント名;
 ④ ②の中にイベントを発生させるメソッドを定義(「Onイベント名」というメソッド名を推奨)
 ⑤ ④の中で「イベント名();」を実行することでイベントを発生させる。
  ただし、イベントフィールドの準備ができていないときに動作するのを避けるために、イベント名がnullではないか
  をチェックすること
・コンソールアプリケーションでもデリゲートを用いてイベント処理を実装する場合の例(Main()側):
 ① イベントの発生により動作させたいメソッドを記述しておく。
 ② main()の中で、イベントの発生を担うクラスのインスタンスを生成。
 ③ main()の中で「②.イベントフィールド」に対して、マルチキャストデリゲートの書式で①を登録
 ④ main()の中で「②.Onイベント名()」メソッドを呼び出すことで、イベントを発生させる
・なお、上記は説明用に、無理矢理イベントを起こさせる構成になっている

p.313 event01.cs

//p.313 event01.cs
using System;
delegate void MyDelegate(); //イベントで用いるデリゲートを宣言
class MyEventClass { //イベントの発生を担うクラス
    public event MyDelegate eventname; //イベントフィールドを定義(デリゲートを利用)
    public void OnEventname() { //イベントを発生させるメソッド
        if (eventname != null) { //イベントフィールドが準備済ならば
            eventname(); //イベントを発生させる
        }
    }
}
class MyClass { //イベントで呼び出されるメソッド①を持つクラス
    public void show() { //イベントで呼び出されるメソッド①
        Console.WriteLine("show");
    }
}
class MyClass2 { //イベントで呼び出されるメソッド②を持つクラス
    public void show2(){ //イベントで呼び出されるメソッド②
        Console.WriteLine("show2!!");
    }
}
class event01 {
    public static void Main() {
        MyClass mc = new MyClass(); //イベントで呼び出されるメソッド①を持つクラス
        MyClass2 mc2 = new MyClass2(); //イベントで呼び出されるメソッド②を持つクラス
        MyEventClass myevent = new MyEventClass(); //イベントの発生を担うクラス
        myevent.eventname += new MyDelegate(mc.show); //イベントで呼び出されるメソッド①を登録
        myevent.eventname += new MyDelegate(mc2.show2); //イベントで呼び出されるメソッド②も登録
        myevent.OnEventname(); //イベントを発生させる
    }
}

アレンジ演習:p.313 event01.cs

・mc.showメソッドと、mc2.show2メソッドを匿名メソッドにして、ラムダ式で記述しよう。
・これにより、MyClass、MyClass2は不要になる。

作成例

//アレンジ演習:p.313 event01.cs
using System;
delegate void MyDelegate(); //イベントで用いるデリゲートを宣言
class MyEventClass { //イベントの発生を担うクラス
    public event MyDelegate eventname; //イベントフィールドを定義(デリゲートを利用)
    public void OnEventname() { //イベントを発生させるメソッド
        if (eventname != null) { //イベントフィールドが準備済ならば
            eventname(); //イベントを発生させる
        }
    }
}
//class MyClass { //【以下削除】イベントで呼び出されるメソッド①を持つクラス
//    public void show() { //イベントで呼び出されるメソッド①
//        Console.WriteLine("show");
//    }
//}
//class MyClass2 { //【以下削除】イベントで呼び出されるメソッド②を持つクラス
//    public void show2(){ //イベントで呼び出されるメソッド②
//        Console.WriteLine("show2!!");
//    }
//}
class event01 {
    public static void Main() {
        //MyClass mc = new MyClass(); //【削除】イベントで呼び出されるメソッド①を持つクラス
        //MyClass2 mc2 = new MyClass2(); //【削除】イベントで呼び出されるメソッド②を持つクラス
        MyEventClass myevent = new MyEventClass(); //イベントの発生を担うクラス
        myevent.eventname += () => Console.WriteLine("show"); //【変更】イベントで呼び出されるメソッド①を登録
        myevent.eventname += () => Console.WriteLine("show2!!"); //【変更】イベントで呼び出されるメソッド②を登録
        myevent.OnEventname(); //イベントを発生させる
    }
}

p.315 超簡単1桁加算器 event02.cs(データ型の構造体とそのメンバ)

・C#の値型は構造体で定義されている(p.45)
・解釈としては、通常の型名を.NET型の型名(p.42)に内部的に変換し、この名の構造体として用いている
・例えば、int型はSystem.Int32型に変換され、System.Int32構造体として扱われる。
・よって、int.Parse()メソッド(p.45)、System.Int32構造体のメソッドであり、値型の各構造体にParse()メソッドが存在している。
・なお、データ型.MaxValue、データ型.MinValue(p.45)は構造体に含まれる定数。
・そして、event02.csで用いる Char.IsDigit(char)メソッドは、char型が変換されるSystem.Char構造体に含まれるメソッドであり、
 .NET型で表現すると Char.IsDigit(Char)メソッド となる。
・Char.GetNumericValue(char)メソッドも同様で、Char.GetNumericValue(Char)。
・なお、GetNumericValue(char)メソッドは、double.Parse("" + Char)で代用できるが、double.Parseは変換不能時に異常終了する。
 GetNumericValue(char)は-1.0を返す。

p.315 超簡単1桁加算器 event02.cs(ConsoleKeyInfo構造体とそのメンバ)

・System.ConsoleKeyInfoは、押されたキーの情報を返すためのデータ構造と、便利なプロパティを持つ構造体。
・Console.Readkey()メソッドの戻り値型がConsoleKeyInfo構造体で、Readkey()メソッドのbool型引数としてtrueを渡すとキー情報を
 コンソールに非表示にできる。
・p.293のConsole.KeyAvailableプロパティがキー入力があればtrueを返すので、この時にConsole.Readkey()メソッドを実行すると良い
・そして、押されたキーの情報は、ConsoleKeyInfo構造体のKeyCharプロパティで得られる。戻り値はchar型なので、
 文字キーであればchar型リテラルで比較可能。

p.315 event02.cs

//p.315 event02.cs
using System;
delegate void Handler(char ch); //デリゲートの宣言(戻り値無し、引数は文字)
class EventClass { //イベントの発生を担うクラス
    public event Handler KeyHit; //イベントフィールドをイベント名KeyHitで定義
    public void OnKeyHit(char ch) { //KeyHitイベントを発生させるメソッド
        if (KeyHit != null) { //イベントフィールドがnullでなければ
            KeyHit(ch); //イベントを発生させる
        }
    }
}
class Show { //イベントの発生により実行したいメソッドを持つクラス
    int sum = 0; //合計値
    public void keyshow(char ch) { //イベントの発生により実行したいメソッド
        if (Char.IsDigit(ch)) { //文字が10進数字(0~9)のキーが押されていたら
            int a = (int)char.GetNumericValue(ch); //文字を実数化、整数化する
            sum += a; //変換結果を合計値に足しこむ
            Console.WriteLine("+ {0}", a); //変換結果を表示
            Console.WriteLine("= {0}", sum); //合計値を表示
        } else if (ch == 'c') { //「c」キーが押されていたら
            sum = 0; //合計値をクリア
            Console.WriteLine("合計がクリアされました");
        } else { //数字キーでもcキーでもなければ
            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 { //xキー以外であれば
                    ec.OnKeyHit(cki.KeyChar); //イベントを発生させる
                }
            }
        }
    }
}

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

・p.319にあるとおり、ラムダ式で書き換えよう

作成例

//アレンジ演習:p.315 event02.cs
using System;
delegate void Handler(char ch); //デリゲートの宣言(戻り値無し、引数は文字)
class EventClass { //イベントの発生を担うクラス
    public event Handler KeyHit; //イベントフィールドをイベント名KeyHitで定義
    public void OnKeyHit(char ch) { //KeyHitイベントを発生させるメソッド
        if (KeyHit != null) { //イベントフィールドがnullでなければ
            KeyHit(ch); //イベントを発生させる
        }
    }
}
class Show { //イベントの発生により実行したいメソッドを持つクラス
    int sum = 0; //合計値
    public void keyshow(char ch) { //イベントの発生により実行したいメソッド
        if (Char.IsDigit(ch)) { //文字が10進数字(0~9)のキーが押されていたら
            int a = (int)char.GetNumericValue(ch); //文字を実数化、整数化する
            sum += a; //変換結果を合計値に足しこむ
            Console.WriteLine("+ {0}", a); //変換結果を表示
            Console.WriteLine("= {0}", sum); //合計値を表示
        } else if (ch == 'c') { //「c」キーが押されていたら
            sum = 0; //合計値をクリア
            Console.WriteLine("合計がクリアされました");
        } else { //数字キーでもcキーでもなければ
            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 { //xキー以外であれば
                    ec.OnKeyHit(cki.KeyChar); //イベントを発生させる
                }
            }
        }
    }
}

p.320 練習問題 1 ex1201.cs

・p.308 lambda02.cs と同じ内容
・よって、アレンジ演習として、double型の2引数をより、それらの差を返すように書き換えよう
・なお、差は「大きい方 - 小さい方」で得ること

作成例

//アレンジ演習:p.320 練習問題 1 ex1201.cs
using System;
delegate double MyDelegate(double x, double y); //デリゲートの宣言(戻り値有り、2引数)
class lambda02 {
    public static void Main() {
        MyDelegate md = (x, y) => { return (x > y) ? x - y : y - x; }; //引数x、yの型は指定不要
        Console.WriteLine("2と3の差は{0}", md(2, 3)); //デリゲート経由で呼び出す
        Console.WriteLine("3.14と2.2の差は{0}", md(3.14, 2.2)); //デリゲート経由で呼び出す
    }
}

p.320 練習問題 2 ex1202.cs ヒント

・p.315 event02.csを基に下記のアレンジを行おう
① イベントを実行した結果をreturnできるように、イベントを発生させるメソッドの戻り値型をintに、引数型をint,intにする
② よって、イベントを発生させると共に、結果を返すように「return KeyHit(int,int);」とする
③ イベントフィールドがnullの時に何も返さないと文法エラーになるので「-1」を返すと良い
④ これに伴い、デリゲートの宣言を「戻り値あり(int)、引数あり(int,int)」にしよう
⑤ イベントの発生により実行したいことは「return x + y;」にできるので、Showクラスは不要になる
⑥ keyshow()メソッドで行っていた「文字が10進数字(0~9)のキーが押されていたら」「文字を実数化、整数化する」は、
 Main()メソッドで行うと良い
⑦ Main()メソッドでは、キー入力1回目の値を保持しておき、2回目のキー入力で2数を渡す形でイベントを発生させる
⑧ そして、結果を表示したら、ループを抜けて終了すると良い

作成例

//p.320 練習問題 2 ex1202.cs
using System;
delegate int Handler(int x, int y); //【変更】デリゲートの宣言(戻り値有り、引数は2整数)
class EventClass { //イベントの発生を担うクラス
    public event Handler KeyHit; //イベントフィールドをイベント名KeyHitで定義
    public int OnKeyHit(int x, int y) { //【変更】KeyHitイベントを発生させるメソッド
        if (KeyHit != null) { //イベントフィールドがnullでなければ
            return KeyHit(x, y); //【変更】イベントを発生させる
        } else { //【以下追加】nullであれば
            return -1; //ダミーの値として-1を返す
        }
    }
}
class event02 { 
    public static void Main() {
        ConsoleKeyInfo cki; //キー情報のデータ構造などを持つ構造体
        EventClass ec = new EventClass(); //イベントの発生を担うクラス
        //Show s = new Show(); //イベントの発生により実行したいメソッドを持つクラス
        ec.KeyHit += (x, y) => { return x + y; }; //【変更】イベントにデリゲートと処理を登録
        int left = -1; //【追加】キー入力1回目の値を保持する変数
        while (true) { //無限ループ
            if (Console.KeyAvailable) { //何かキーが押されていたら
                cki = Console.ReadKey(true); //そのキーを得る
                char ch = cki.KeyChar; //【追加】押されたキー名を取り出す
                if (Char.IsDigit(ch)) { //【移動】文字が10進数字(0~9)のキーが押されていたら
                    int a = (int)(char.GetNumericValue(ch)); //【移動】文字を実数化、整数化する
                    if (left == -1) { //【以下追加】キー入力1回目?
                        left = a; //入力値を保持
                    }
                    else { //キー入力2回目?
                        int sum = ec.OnKeyHit(left, a); //2入力値を渡してイベントを発生させる
                        Console.WriteLine("{0} + {1} = {2}", left, a, sum);
                        break; //無限ループを抜ける
                    }
                }
            }
        }
    }
}

第13章 例外

p.321 例外処理の基礎

・プログラムによって異常状態が発生すると「実行時エラー」として、発生時点で異常終了する。
・この異常状態を「例外(Exception)」として、これを表すクラスのオブジェクトで扱うことができる
・このオブジェクトを受け取って処理する仕組みが例外処理で、例外処理を組み込むことで「実行時エラー」をコントロールできる。
・例えば、p.322 exception01.cs のように、double.Parse()メソッドに実数に変換できない文字列を渡すと、
 Exceptionクラスの派生クラスであるFormatExceptionクラスのオブジェクトが生成され、例外処理がないので異常終了する。
・以下が異常終了時のメッセージとその解説:
 ハンドルされていない例外: System.FormatException: 入力文字列の形式が正しくありません。
 ⇒「ハンドルされていない」とは例外処理がないか例外処理対象になっていないことを示す
 ⇒「System.FormatException」は形式例外を表すクラス
 ⇒「入力文字列の形式が正しくありません。」はこのクラスのMessageプロパティにある文字列
   場所 System.Number.ParseDouble(String value, NumberStyles options, NumberFormatInfo numfmt)
   場所 System.Double.Parse(String s)
   場所 MyClass.Main() 場所 F:\ha241_AkibaC#\Project1\Project1\exception01.cs:行 9
 ⇒下から上に向かって例外発生の経緯を示す。C#で実際にどう扱われたかがわかる
 ⇒一番下の1行で「発生の起因となったクラス名.メソッド名 場所:ソースファイル名:行番号」が明示されている

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

・変数a、bをint型に変更しよう
・すると、System.FormatException(形式例外)以外の例外を試すことができる
 ①「割られる数」や「割る数」にint型の最大値を超える値や、最小値を下回る値を入力した場合
 ②「割る数」に 0 を入力した場合(double型の場合は「∞」と表示され異常終了しない)

作成例

//アレンジ演習:p.322 exception01.cs
using System;
class MyClass
{
    public static void Main()
    {
        Console.Write("割られる数--");
        string strA = Console.ReadLine();
        int a = int.Parse(strA); //【変更】FormatException、OverflowException発生可能性
        Console.Write("割る数---");
        string strB = Console.ReadLine();
        int b = int.Parse(strB); //【変更】FormatException、OverflowException発生可能性
        Console.WriteLine("{0} ÷ {1} = {2}", a, b, a / b); //DevideByZeroException発生可能性
    }
}

今週の話題

ゲームソフト販売本数ランキング:今週1位も「マリオカート ワールド(Switch2)」 GO!
ゲーム領域で停滞も…高収益体質を築いたSHIFTの戦略的人材強化【ゲーム企業の決算を読む】GO!
AI時代のゲームインフラとセキュリティ―ゲーム開発者が知るべき最新テクノロジーをアカマイが解説【CEDEC 2025】GO!
モノリスソフト東京オフィスが人材を積極募集中―「ゼノブレイド」シリーズ総監督の「新作RPG」に向けた動き? GO!

トランプ政権で“デジタル遺産の危機”に直面…保管プロジェクトや教育ゲームも支援打ち切りで資金難に GO!
スイッチ海賊版サイト「Nsw2u」をFBIが摘発。現在はアクセス不可に GO!
中古スイッチソフト使用でスイッチ2が遠隔BAN!?迅速対応で一安心も復旧にゲームのパッケージ写真等の証明必要に GO!
PC全年齢向け人気タイトルがSteamではコンソール版相当の内容に、なぜ?―お詫び文掲載の事態に…国内メーカー悩ます、ユーザーとプラットフォームの板挟み GO!