今週の話題

販売本数ランキング 今回トップは「龍が如く8(PS5)」GO!
『アトリエ』シリーズ好調でコーエーテクモが2割増収―“月商20億円のスマートフォンゲーム”を生み出すことに成功【ゲーム企業の決算を読む】GO!
業績好調でも脱・中古ゲーム販売を進めるゲオが向かう先【ゲーム企業の決算を読む】GO!
『原神』が40カ月で売上50億ドルを達成―『クラッシュ・オブ・クラン』を抜き、モバイルゲーム最速記録 GO!
CESA、「CEDEC2024」開催を発表ーセッション講演者の公募を開始 GO!
マイクロソフトが2024年第2四半期決算を発表―アクティビジョン買収でゲーム部門が躍進 GO!
『いけにえと雪のセツナ』『鬼ノ哭ク邦』のTokyo RPG Factoryがスクエニに吸収合併、解散へ―権利義務一切はスクエニ側に引き継ぎ GO!
【決算】カプコンの3Q連結業績は4割超増益で着地―『スト6』などデジタル販売強化が奏功 GO!
【決算】コーエーテクモHDの3Q決算、経常利益100%増の大幅増益 GO!

AppleがEU圏でアプリのサイドローディングを許可するも大手アプリは反発―Xboxサラ・ボンド氏「間違った方向に進んでいる」GO!

前回のコメント

・ミニ演習では三平方の定理で距離を求めるプログラムを作りましたが
 三平方の役割を今日知りましたし、何気に中学高校で習った数学がポンポンでてきて(unity講座でも)
 血の気が引いてきましたが(゚Д゚;)、なんとかプログラムと数学も理解できるよう頑張ります。

 ある意味で数学公式もゲーム作りに使う道具ですし(呪文に近いかも)
 使い方のパターンが決まっていることがほとんどです。
 三平方の定理は面倒ですが、座標間の距離をサクっと出してくれる便利なものと割り切って活用してください。

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

:タイマー処理 など

テーマ24 タイマー処理

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

テーマ25 数値の形式指定文字列化

・WriteLineメソッドなどで使える{0}、{1}、…を用いた形式指定文字列化は、文字列の生成でも使える
・Stringクラスの静的メソッドFormatに、形式指定文字列を値や式を渡して、編集結果の文字列を得ることができる
・書式例: string s = String.Format("形式指定文字列", 値や式, …);
・例: string s = String.Format("{0}は{1}倍速い!", "シャア", 3); //文字列"シャアは3倍速い!"を得る

演習21 タイマーによるスコアアップ

・500ミリ秒ごとにスコアが1ずつ増えるようにしよう
・手順
 ① スコア用の変数 int score を0で初期化しておく
 ② スコア表示を変数scoreを用いるように変更する
 ③ タイマーから呼び出されるPlayメソッドを作成し、この中でscoreをインクリメントし、再描画する
 ④ タイマーを生成し、500ミリ秒ごとにPlayメソッドを呼び出すよう定義して、開始する
 ⑤ OnPaintメソッドにおけるスコアの表示に変数scoreを用いるように変更する(形式指定文字列を利用)
※ タイマーの生成はデータメンバの定義で行えばよい
※ タイマーのイベント登録とインターバルの設定はコンストラクタで行えばよい
※ タイマーのStartは、画面がプレイ画面に遷移する時に行えば良い
※ 画面のチラツキが発生するが、次の演習で解消する

作成例

//演習21 タイマーによるスコアアップ
using System; //フォームアプリケーションに必須
using System.Windows.Forms; //フォームアプリケーションに必須
using System.Drawing; //Image用
class Program : Form { //Formクラスを継承
    Image backb = Image.FromFile("backb.bmp"); //背景画像の読込
    Image enemy = Image.FromFile("enemy.gif"); //アイテム画像の読込
    Image burn = Image.FromFile("burn.gif"); //効果用画像の読込
    Pen mypen = new Pen(Color.Red, 2); //ペンを生成(初期値は赤、2)
    Brush bred = new SolidBrush(Color.FromArgb(63, 255, 0, 0)); //ブラシを生成(赤の透明色)
    Brush byel = new SolidBrush(Color.Yellow); //ブラシを生成(黄色)
    Font f20 = new Font("メイリオ", 20, FontStyle.Bold); //フォントを生成(メイリオ20P太字)
    Font f80 = new Font("メイリオ", 80, FontStyle.Bold); //フォントを生成(メイリオ80P太字)
    int gamemode = 0; //0:スタート画面、1:プレイ画面
    int score = 0; //【追加】スコア
    Timer timer = new Timer(); //【追加】タイマーを生成
    void DrawCircle(PaintEventArgs e, Pen p, int x, int y, int r) { //ペン、中心座標、半径を指定して円を描画
        e.Graphics.DrawEllipse(p, x - r, y - r, 2 * r, 2 * r); //円の描画を呼ぶ
    }
    protected override void OnPaint(PaintEventArgs e) { //Formのメソッドをオーバーライド
        base.OnPaint(e); //まず、Formクラスにおけるメソッドの内容(基本再描画処理)を実行
        e.Graphics.DrawImage(backb, 0, 0); //背景画像を(0,0)から描画
        if (gamemode == 0) { //スタート画面
            e.Graphics.DrawString("GAME1", f80, byel, 100, 100); //タイトルの描画
            e.Graphics.DrawString("Hit Enter to Start", f20, byel, 200, 300); //メッセージの描画
        } else if (gamemode == 1) { //プレイ画面
            e.Graphics.FillRectangle(bred, 78, 411, 485, 64); //判定エリアを透明赤で塗りつぶす
            e.Graphics.DrawRectangle(mypen, 78, 411, 485, 64); //判定エリアの描画
            string s = String.Format("SCORE:{0:000,000}", score); //【追加】スコア文字列を編集
            e.Graphics.DrawString(s, f20, byel, 400, 10); //スコア文字列の描画
        }
    }
    void OnKeyDown(object o, KeyEventArgs e) { //キーボードが押された時に呼ばれるメソッド
        if(e.KeyCode.ToString() == "Escape") { //Escキーが押されたら
            Close(); //フォームアプリケーション終了
        }
        if(gamemode == 0 && e.KeyCode.ToString() == "Return") { //スタート画面でEnterキーが押されたら
            gamemode = 1; //プレイ画面に切り替える
            timer.Start(); //【追加】タイマー開始
            Invalidate(); //画面再描画を依頼する
        }
    }
    void Play(object o, EventArgs e) { //【以下追加】タイマーから呼ばれるメソッド
        score++; //スコアアップ
        Invalidate(); //画面再描画を依頼する
    }
    Program() { //コンストラクタ
        KeyDown += new KeyEventHandler(OnKeyDown); //キーボードが押された時に呼ばれるメソッドを登録
        timer.Tick += new EventHandler(Play); //【追加】タイマーから呼ばれるメソッドを登録
        timer.Interval = 500; //【追加】タイマーの間隔
    }
    static void Main() { //publicの指定は任意
        Program p = new Program(); //継承したフォームのインスタンスを生成
        p.Width = 660; //インスタンスのWidthプロパティに幅を代入
        p.Height = 520; //インスタンスのHeightプロパティに高さを代入
        p.Text = "Game"; //インスタンスのTextプロパティにフォーム名を代入
        p.ControlBox = false; //ControlBoxを非表示にする
        p.FormBorderStyle = FormBorderStyle.Fixed3D; //フォームサイズ変更を禁止
        Application.Run(p); //インスタンスを画面に出す
    }
}

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

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

演習22 ダブルバッファリングの導入

・コンストラクタでダブルバッファリングを導入しよう

作成例

//演習22 ダブルバッファリングの導入
using System; //フォームアプリケーションに必須
using System.Windows.Forms; //フォームアプリケーションに必須
using System.Drawing; //Image用
class Program : Form { //Formクラスを継承
    Image backb = Image.FromFile("backb.bmp"); //背景画像の読込
    Image enemy = Image.FromFile("enemy.gif"); //アイテム画像の読込
    Image burn = Image.FromFile("burn.gif"); //効果用画像の読込
    Pen mypen = new Pen(Color.Red, 2); //ペンを生成(初期値は赤、2)
    Brush bred = new SolidBrush(Color.FromArgb(63, 255, 0, 0)); //ブラシを生成(赤の透明色)
    Brush byel = new SolidBrush(Color.Yellow); //ブラシを生成(黄色)
    Font f20 = new Font("メイリオ", 20, FontStyle.Bold); //フォントを生成(メイリオ20P太字)
    Font f80 = new Font("メイリオ", 80, FontStyle.Bold); //フォントを生成(メイリオ80P太字)
    int gamemode = 0; //0:スタート画面、1:プレイ画面
    int score = 0; //スコア
    Timer timer = new Timer(); //タイマーを生成
    void DrawCircle(PaintEventArgs e, Pen p, int x, int y, int r) { //ペン、中心座標、半径を指定して円を描画
        e.Graphics.DrawEllipse(p, x - r, y - r, 2 * r, 2 * r); //円の描画を呼ぶ
    }
    protected override void OnPaint(PaintEventArgs e) { //Formのメソッドをオーバーライド
        base.OnPaint(e); //まず、Formクラスにおけるメソッドの内容(基本再描画処理)を実行
        e.Graphics.DrawImage(backb, 0, 0); //背景画像を(0,0)から描画
        if (gamemode == 0) { //スタート画面
            e.Graphics.DrawString("GAME1", f80, byel, 100, 100); //タイトルの描画
            e.Graphics.DrawString("Hit Enter to Start", f20, byel, 200, 300); //メッセージの描画
        } else if (gamemode == 1) { //プレイ画面
            e.Graphics.FillRectangle(bred, 78, 411, 485, 64); //判定エリアを透明赤で塗りつぶす
            e.Graphics.DrawRectangle(mypen, 78, 411, 485, 64); //判定エリアの描画
            string s = String.Format("SCORE:{0:000,000}", score); //スコア文字列を編集
            e.Graphics.DrawString(s, f20, byel, 400, 10); //スコア文字列の描画
        }
    }
    void OnKeyDown(object o, KeyEventArgs e) { //キーボードが押された時に呼ばれるメソッド
        if(e.KeyCode.ToString() == "Escape") { //Escキーが押されたら
            Close(); //フォームアプリケーション終了
        }
        if(gamemode == 0 && e.KeyCode.ToString() == "Return") { //スタート画面でEnterキーが押されたら
            gamemode = 1; //プレイ画面に切り替える
            timer.Start(); //タイマー開始
            Invalidate(); //画面再描画を依頼する
        }
    }
    void Play(object o, EventArgs e) { //タイマーから呼ばれるメソッド
        score++; //スコアアップ
        Invalidate(); //画面再描画を依頼する
    }
    Program() { //コンストラクタ
        DoubleBuffered = true; //【追加】ダブルバッファリングの導入
        KeyDown += new KeyEventHandler(OnKeyDown); //キーボードが押された時に呼ばれるメソッドを登録
        timer.Tick += new EventHandler(Play); //タイマーから呼ばれるメソッドを登録
        timer.Interval = 500; //タイマーの間隔
    }
    static void Main() { //publicの指定は任意
        Program p = new Program(); //継承したフォームのインスタンスを生成
        p.Width = 660; //インスタンスのWidthプロパティに幅を代入
        p.Height = 520; //インスタンスのHeightプロパティに高さを代入
        p.Text = "Game"; //インスタンスのTextプロパティにフォーム名を代入
        p.ControlBox = false; //ControlBoxを非表示にする
        p.FormBorderStyle = FormBorderStyle.Fixed3D; //フォームサイズ変更を禁止
        Application.Run(p); //インスタンスを画面に出す
    }
}

提出:演習22 ダブルバッファリングの導入

講義メモ

・テキスト篇:p.352「2項演算子のオーバーロード」から
・ゲーム開発演習:タイマー処理 など

p.352 2項演算子のオーバーロード

・2項演算子のオーバーロードは2引数で定義すれば良い
・書式: public static 型 operator 演算子 (型 引数1, 型 引数2) {…; return 値や式; }
・単項演算子のオーバーロードは引数と同型を返すのが一般的だが、2項演算子のオーバーロードでは異なる型を返したり、
 異なる型の引数を指定することも多い
・例:座標クラス Map A(5, 3), B(4, 8) で、 A + B は「9, 11」、A * 2 は「10, 6」とできる
 また、A - Bを座標ではなく距離を返すと定義することも可能

ミニ演習:座標クラスMapにおける2項+演算子のオーバーロード Map02.cs

・Mapクラスはデータメンバ「double x, y; //X座標とY座標」と、デフォルトコンストラクタ、
 XY座標を指定するコンストラクタを持つとする
・2項+演算子のオーバーロードでは、X座標とY座標のそれぞれの和を持つ座標を返そう
・合わせて、座標の表示を行うToStringメソッドのオーバーロードも追加しよう

作成例

//座標クラスMapにおける2項+演算子のオーバーロード Map02.cs
using System;
class Map {
  double x; //X座標用のデータメンバ 
  double y; //Y座標用のデータメンバ
  public double X { //X座標用のプロパティ
    get { return x; } set { x = value; }
  }
  public double Y { //Y座標用のプロパティ
    get { return y; } set { y = value; }
  }
  public Map() { //デフォルトコンストラクタ
    x = 0.0;
    y = 0.0;
  }
  public Map(double x, double y) { //X座標、Y座標を指定するコンストラクタ
    this.x = x;
    this.y = y;
  }
  public override string ToString() { //座標表示用のオーバライド
      return "(" + x + ", " + y + ")";
  }
  public static Map operator +(Map a, Map b) { //2項+演算子のオーバーロード
    Map result = new Map(); //作業用のMapオブジェクトを生成
    result.x = a.x + b.x; //それぞれのX座標の和を格納
    result.y = a.y + b.y; //それぞれのY座標の和を格納 
    return result; //出来上がったオブジェクトを返す
  }
}
class Map02 {
  public static void Main() {
    Map A = new Map(5, 2), B = new Map(-3, 4);
    Console.WriteLine("A:{0}", A);
    Console.WriteLine("B:{0}", B);
    Map C = A + B; //2項+演算子のオーバーロード
    Console.WriteLine("C:{0}", C);
  }
}

ミニ演習:座標クラスMapにおける2項-演算子、2項*演算子のオーバーロード Map03.cs

・2項-演算子のオーバーロードは、2座標の距離をdouble型で返そう
・2項*演算子のオーバーロードは(Map A, int d)として、AのX座標とY座標をd倍した結果の座標を返そう

作成例

//座標クラスMapにおける2項+演算子のオーバーロード Map02.cs
using System;
class Map {
  double x; //X座標用のデータメンバ 
  double y; //Y座標用のデータメンバ
  public double X { //X座標用のプロパティ
    get { return x; } set { x = value; }
  }
  public double Y { //Y座標用のプロパティ
    get { return y; } set { y = value; }
  }
  public Map() { //デフォルトコンストラクタ
    x = 0.0;
    y = 0.0;
  }
  public Map(double x, double y) { //X座標、Y座標を指定するコンストラクタ
    this.x = x;
    this.y = y;
  }
  public override string ToString() { //座標表示用のオーバライド
      return "(" + x + ", " + y + ")";
  }
  public static double operator -(Map a, Map b) { //距離を返す2項-演算子のオーバーロード
    return Math.Sqrt(Math.Pow((a.x - b.x), 2) + Math.Pow((a.y - b.y), 2)); //座標差の2乗の和の平方根
  }
  public static Map operator *(Map a, int n) { //2項*演算子のオーバーロード
    return new Map(a.x * n, a.y * n); //X座標,Y座標をn倍した座標を返す
  }
}
class Map02 {
  public static void Main() {
    Map A = new Map(5, 2), B = new Map(-3, 4);
    Console.WriteLine("A:{0}", A);
    Console.WriteLine("B:{0}", B);
    double d = A - B; //2項-演算子のオーバーロードで距離を得る
    Console.WriteLine("距離は{0}", d);
    Map C = A * 3; //2項*演算子のオーバーロードで3倍した座標を得る
    Console.WriteLine("A×3:{0}", C);
  }
}

p.356(等価演算子のオーバーロード)

・2項==演算子もオーバーロードが可能で、2つのオブジェクトの比較方法を定義できる
・通常、2項==演算子のオーバーロードでは、全データメンバの値が等しければ全体として等しいとすることが多い
・例: MAP A(3,2),B(3,2); ならば「A == B」はtrueを返す
・ルールとして、2項==演算子をオーバーロードしたら、2項!=演算子もオーバーロードする必要があるが中身はパターンが決まっている
 ① !(A == B) と、2項==演算子をオーバーロードを呼んだ結果を反転すれば良い
・また、public bool Equals(object o)メソッドのオーバライドが必要だが中身はパターンが決まっている
 ① 型が等しくなければfalseを返す:if(o.GetType() != this.GetType()) return false;
 ② 全データメンバの値が等しければtrue、でなければfalseを返す
・加えて、public int GetHashCode()メソッドのオーバライドが必要だが中身はパターンが決まっている
 ① データメンバの値の和のハッシュコードと差のハッシュコードの排他的論理和を返す
 ※ ハッシュコードはobjectクラスのGetHashCode()メソッドで得ると良い
 例: return (x + y).GetHashCode() ^ (x + y).GetHashCode();

ミニ演習:座標クラスMapにおける2項==演算子、2項!=演算子のオーバーロード Map04.cs

・2項==演算子のオーバーロードは、2座標の距離をdouble型で返そう

作成例

//座標クラスMapにおける2項+演算子のオーバーロード Map02.cs
using System;
class Map {
  double x; //X座標用のデータメンバ 
  double y; //Y座標用のデータメンバ
  public double X { //X座標用のプロパティ
    get { return x; } set { x = value; }
  }
  public double Y { //Y座標用のプロパティ
    get { return y; } set { y = value; }
  }
  public Map() { //デフォルトコンストラクタ
    x = 0.0;
    y = 0.0;
  }
  public Map(double x, double y) { //X座標、Y座標を指定するコンストラクタ
    this.x = x;
    this.y = y;
  }
  public override string ToString() { //座標表示用のオーバライド
      return "(" + x + ", " + y + ")";
  }
  public static bool operator ==(Map a, Map b) { //2項==演算子のオーバーロード
    return (a.x == b.x) && (a.y == b.y); //X座標,Y座標が等しければ等しい
  }
  public static bool operator !=(Map a, Map b) { //2項!=演算子のオーバーロード
    return !(a == b); //2項==演算子のオーバーロードを呼んだ結果を反転
  }
}
class Map03 {
  public static void Main() {
    Map A = new Map(5, 2), B = new Map(-3, 4);
    Console.WriteLine("A:{0}", A);
    Console.WriteLine("B:{0}", B);
    Console.WriteLine("A == Bは{0}", A == B);
    Console.WriteLine("A != Bは{0}", A != B);
  }
}

今週の話題

販売本数ランキング 今回トップは「Marvel’s Spider-Man 2(PS5)」GO!
業績回復鮮明のシリコンスタジオ、ゲームエンジンの需要高まる自動車業界でも存在感を発揮【ゲーム企業の決算を読む】GO!
Apple、「GeForce NOW」などゲームストリーミングサービスの制限緩和―単一アプリでフル機能が提供可能に GO!
ブロックチェーンゲーム『キャプテン翼 -RIVALS-』、スマホアプリ版のサービス開始 事前登録は10万人突破 GO!
『FF16』が日本PS5部門でトップに!2023年のPS Store年間ダウンロードランキング公開 GO!
3DS/Wii Uのオンラインプレイサービスが2024年4月9日に終了へ GO!

『パルワールド』の“ポケモン”Modがコミュニティで波紋呼ぶ―動画等は任天堂により権利者削除、モデルの出処も怪しい GO!
ポケモン社が類似の他社ゲームに対して「いかなる利用も許諾しておりません」とコメントを発表。『パルワールド』を示唆か。知的財産権の侵害については「調査を行った上で、適切な対応を取っていく所存」GO!
マイクロソフト約1,900人解雇―アクティビジョン買収でゲーム部門全体の約8%にあたる人員整理 GO!
開発期間は6年以上…Blizzardレイオフで開発中止の未発表サバイバルゲームは社内で好評だった模様 GO!
リリースからたった4ヶ月での終了告知…ネクソンの無料大規模対戦アクション『Warhaven』4月にサービス終了へ GO!

前回のコメント

・演算子のオーバーロードでは、符号を反転させることができるということが分かりましたが、
 テキストのほうで複素数とか初めて聞く数学用語?がでてきて、今何を習ってるんだっけ(。´・ω・)?
 っとなってました笑 
 先生が座標で例を出してくれたのでなんとなく使い方みたいなのは理解できた気がします。

 何よりです。複素数を例にとって説明しているのは悪くないのですが、
 必須ではない数学的知識が要求されるのは楽しくないですね。
 本質をつかんでいただけるように工夫しますので、演算子のオーバーロードをしっかり理解してください。

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

:スタート画面の追加(つづき)

演習20 スタート画面の追加(再掲載+α)

・クラス変数gamemodeを0で初期化しておく(0:スタート画面、1:プレイ画面)
・gamemodeが0であれば、背景画像、タイトル「GAME1」、メッセージ「Hit Enter to Start」を表示
・gamemodeが1であれば、背景画像、判定エリア、スコアを表示
・タイトル「GAME1」はメイリオ、80ポイント、太字、黄色、(100,100)から
・メッセージ「Hit Enter to Start」はメイリオ、20ポイント、太字、黄色、(200,300)から
・スタート画面でEnterキー(キーコードは"Return")が押されたら、gamemodeを1にして、Invalidate()メソッドを呼ぶことで
 画面再描画を依頼する

作成例

//演習20 スタート画面の追加
using System; //フォームアプリケーションに必須
using System.Windows.Forms; //フォームアプリケーションに必須
using System.Drawing; //Image用
class Program : Form { //Formクラスを継承
    Image backb = Image.FromFile("backb.bmp"); //背景画像の読込
    Image enemy = Image.FromFile("enemy.gif"); //アイテム画像の読込
    Image burn = Image.FromFile("burn.gif"); //効果用画像の読込
    Pen mypen = new Pen(Color.Red, 2); //ペンを生成(初期値は赤、2)
    Brush bred = new SolidBrush(Color.FromArgb(63, 255, 0, 0)); //ブラシを生成(赤の透明色)
    Brush byel = new SolidBrush(Color.Yellow); //ブラシを生成(黄色)
    Font f20 = new Font("メイリオ", 20, FontStyle.Bold); //フォントを生成(メイリオ20P太字)
    Font f80 = new Font("メイリオ", 80, FontStyle.Bold); //【追加】フォントを生成(メイリオ80P太字)
    int gamemode = 0; //【追加】0:スタート画面、1:プレイ画面
    void DrawCircle(PaintEventArgs e, Pen p, int x, int y, int r) { //ペン、中心座標、半径を指定して円を描画
        e.Graphics.DrawEllipse(p, x - r, y - r, 2 * r, 2 * r); //円の描画を呼ぶ
    }
    protected override void OnPaint(PaintEventArgs e) { //Formのメソッドをオーバーライド
        base.OnPaint(e); //まず、Formクラスにおけるメソッドの内容(基本再描画処理)を実行
        e.Graphics.DrawImage(backb, 0, 0); //背景画像を(0,0)から描画
        if (gamemode == 0) { //【以下追加】スタート画面
            e.Graphics.DrawString("GAME1", f80, byel, 100, 100); //タイトルの描画
            e.Graphics.DrawString("Hit Enter to Start", f20, byel, 200, 300); //メッセージの描画
        } else if (gamemode == 1) { //【追加】プレイ画面
            e.Graphics.FillRectangle(bred, 78, 411, 485, 64); //判定エリアを透明赤で塗りつぶす
            e.Graphics.DrawRectangle(mypen, 78, 411, 485, 64); //判定エリアの描画
            e.Graphics.DrawString("SCORE:000,000", f20, byel, 400, 10); //スコアの描画
        }
    }
    void OnKeyDown(object o, KeyEventArgs e) { //キーボードが押された時に呼ばれるメソッド
        if(e.KeyCode.ToString() == "Escape") { //Escキーが押されたら
            Close(); //フォームアプリケーション終了
        }
        if(gamemode == 0 && e.KeyCode.ToString() == "Return") { //【以下追加】スタート画面でEnterキーが押されたら
            gamemode = 1; //プレイ画面に切り替える
            Invalidate(); //画面再描画を依頼する
        }
    }
    Program() { //コンストラクタ
        KeyDown += new KeyEventHandler(OnKeyDown); //キーボードが押された時に呼ばれるメソッドを登録
    }
    static void Main() { //publicの指定は任意
        Program p = new Program(); //継承したフォームのインスタンスを生成
        p.Width = 660; //インスタンスのWidthプロパティに幅を代入
        p.Height = 520; //インスタンスのHeightプロパティに高さを代入
        p.Text = "Game"; //インスタンスのTextプロパティにフォーム名を代入
        p.ControlBox = false; //ControlBoxを非表示にする
        p.FormBorderStyle = FormBorderStyle.Fixed3D; //フォームサイズ変更を禁止
        Application.Run(p); //インスタンスを画面に出す
    }
}

提出:演習20 スタート画面の追加

講義メモ

テキスト篇:p.341「checkedとunchecked」から
ゲーム開発演習:タイマー処理 など
p.341 checked

・int.Parseメソッド等では、変換結果がint.MaxValueを超えるか、int.MinValueを下回るとオーバーフローとなり、
 OverFlowExceptionを投げる
・しかし、計算処理の途中でオーバーフローとなった場合、例外を投げない
 例: int a = int.MaxValue, b = a + 1; //bはint.MinValueになる(例外を投げない)
・このような場合にも、OverFlowExceptionを投げるようにするのがcheckedの仕組み
・checkedブロックの中に記述しておくことで、上記を可能にする
 例: checked { int a = int.MaxValue, b = a + 1; } //bはint.MinValueにならず例外を投げる
※ テキストでは「checked(…)」形式も紹介しているが、ブロック形式に統一する場合がある

p.341 checked02.cs

//p.341 checked02.cs
using System;
class checked02 {
    public static void Main() {
        int x, y, z;
        try {
            checked { //オーバーフローを例外とする範囲
                x = int.MaxValue;
                y = 1;
                z = x + y; //checkedがある場合はオーバーフロー例外発生
                Console.WriteLine(z); //checkedがなければint.MinValueになっている
            }
        } catch (OverflowException o) { //オーバーフロー例外処理
            Console.WriteLine(o.Message);
            Console.WriteLine(o.StackTrace);
        }
    }
}

p.341 unchecked

・checkedブロックの中で、その効果を無効化したい範囲がある時に用いる

//p.341 uncheckedの例
using System;
class checked02 {
    public static void Main() {
        int x, y, z;
        x = int.MaxValue;
        y = 1;
        checked { //オーバーフローを例外とする範囲
            unchecked {
                Console.WriteLine("これ以降はオーバフローをチェックしない");
                z = x + y; //uncheckedなのでオーバーフロー例外は発生しない
            }
            Console.WriteLine("これ以降はオーバフローをチェックしする");
            z = x + y; //checkedなのでオーバーフロー例外発生
            Console.WriteLine(z);
        }
    }
}

p.344 練習問題 ヒント

・「forループで行い」とあるが、終了条件が無いので「for(byte b = 1; ; b++)」と無限ループにすると良い
・動作がわかるように、途中の値を表示しよう
・「catch」でオーバーフロー例外処理を記述し、発生した旨を表示してから、breakで抜けよう

作成例

//p.344 練習問題
using System;
class ex13 {
    public static void Main() {
        byte x = 1;
        for (byte b = 1; ; b++) { //checkedがなければ無限ループになる
            try {
                checked { //オーバーフローを例外とする範囲
                    x *= b; //720でオーバーフロー発生(checkedがなければ例外は投げられない)
                    Console.WriteLine("x = {0}", x);
                }
            } catch (OverflowException) { //checkedがあれば例外が投げられるので捕捉する
                Console.WriteLine("オーバーフロー例外発生");
                break; //ループを抜ける
            }
        }
        Console.WriteLine(x); //checkedがなければ実行されない
    }
}

第14章 演算子のオーバーロード

p.345 演算子のオーバーロード

・2項+演算子のように、オペランドの型により作用が異なるものがあり、これを演算子のオーバーロードという
・組み込み型(p.38)に対する演算子のオーバーロードはシステム側の定義によるので、変更できない
・しかし、ユーザ定義型に対する演算子のオーバーロードは、ユーザ定義型を表すクラスの中に定義できる
・例:座標クラスで「座標(1,2) + 座標(3,4) ⇒ 座標(4,6)」を実装できる
・テキストでは、複素数を表すクラスを例にとって、複素数の計算を演算子のオーバーロードで実装している
・複素数とは実数と虚数を合わせたもので、虚数とは-1の平方根を含む値で、-1の平方根は通常「i」で表す。
・よって、複素数は実数a、bと-1の平方根iの式で「a + bi」で表現できる。bが0であれば実数、でなければ虚数。
 例: 虚数「2i」は2乗すると 2√(-1) × 2√(-1) = 2 × 2 × √(-1) × √(-1) = -4 と実数になる
・なお「a + bi」のaを実部、bを虚部といい、どちらもdouble型で扱っている
・複素数どうしの加減乗除の公式(p.346)が提示されているので、これをそのままクラス内に記述すれば良い

p.347 complex01.cs

//p.347 complex01.cs
using System;
class MyComplex { 
  double real; //実部用のデータメンバ 
  double imaginary; //虚部用のデータメンバ
  public double a { //実部用のプロパティ
    get { return real; } set { real = value; }
  }
  public double i { //虚部用のプロパティ
    get { return imaginary; } set { imaginary = value; }
  }
  public MyComplex() { //デフォルトコンストラクタ
    real = 0.0;
    imaginary = 0.0;
  }
  public MyComplex(double x, double y) { //実部、虚部を指定するコンストラクタ
    real = x;
    imaginary = y;
  }
}
class Complex01 {
  public static void Main() {
    MyComplex A = new MyComplex(); //複素数「0 + 0i」を生成
    A.a = 10;
    A.i = 5; //複素数「10 + 5i」になる
    MyComplex B = new MyComplex(2, 3); //複素数「2 + 3i」を生成 
    Console.WriteLine("複素数Aの実部は{0}で虚部は{1}です。", A.a, A.i);
    Console.WriteLine("複素数Bの実部は{0}で虚部は{1}です。", B.a, B.i);
  }
}

p.349 単項演算子のオーバーロード

・クラス内で、operatorキーワードを用いたメソッドに近い記述をすることで演算子のオーバーロードが可能
・演算子のオーバーロードでは、戻り値を自由に指定できるが、元の演算子の意味から外れることは推奨されない
・例えば、単項-演算子のオーバーロードであれば「符号を反転した結果の同型オブジェクト」を返すのが適切
・複素数の場合、-(1+2i)は(-1)+(-2i)なので、これを処理として記述する
・書式例: public static クラス名 operator 演算子(クラス名 引数) { …; return クラス型オブジェクト; }
・複素数Complexクラスの単項-演算子のオーバーロードの場合:
 public static Complex operator -(Complex c) {…; return Complexオブジェクト;}

補足:p.350 opov01.csについて

・このプログラムでは、p.347 complex01.csで定義したComplexクラスをMyComplexとリネームして、
 これを継承するComplexクラスを記述して、その中で単項-演算子のオーバーロードを行っている
・しかし、継承する必要は特にないので、継承を省いて理解しよう

アレンジ演習:p.350 opov01.cs

・MyComplexクラスをComplexクラスに変更し、継承を省いた形式にしよう

作成例

//アレンジ演習:p.350 opov01.cs
using System;
class Complex {
  double real; //実部用のデータメンバ 
  double imaginary; //虚部用のデータメンバ
  public double a { //実部用のプロパティ
    get { return real; } set { real = value; }
  }
  public double i { //虚部用のプロパティ
    get { return imaginary; } set { imaginary = value; }
  }
  public Complex() { //デフォルトコンストラクタ
    real = 0.0;
    imaginary = 0.0;
  }
  public Complex(double x, double y) { //実部、虚部を指定するコンストラクタ
    real = x;
    imaginary = y;
  }
  public static Complex operator -(Complex c) { //単項-演算子のオーバーロード
    Complex result = new Complex(); //作業用のComplexオブジェクトを生成
    result.a = -1.0 * c.a; //実部の符号を反転した結果を格納
    result.i = -1.0 * c.i; //虚部の符号を反転した結果を格納 
    return result; //出来上がったオブジェクトを返す
  }
}
class Opov01 {
  public static void Main() {
    Complex A, B; 
    A = new Complex(5, 2);  //Aは「5 +2i」
    B = new Complex(6, -3); //Bは「6 -3i」
    Console.WriteLine("-Aの実部は{0}で虚部は{1}です。", -A.a, -A.i); //-Aは「-5 -2i」
    Console.WriteLine("-Bの実部は{0}で虚部は{1}です。", -B.a, -B.i); //-Bは「-6 +3i」
  }
}

ミニ演習:座標クラスMapにおける単項-演算子のオーバーロード Map01.cs

・p.350 opov01.csのComplexクラスをMapクラスにしよう
・Mapクラスはデータメンバ「double x, y; //X座標とY座標」と、デフォルトコンストラクタ、
 XY座標を指定するコンストラクタを持つとする
・単項-演算子のオーバーロードでは、X座標とY座標の反転としよう

作成例

//座標クラスMapにおける単項-演算子のオーバーロード Map01.cs
using System;
class Map {
  double x; //X座標用のデータメンバ 
  double y; //Y座標用のデータメンバ
  public double X { //X座標用のプロパティ
    get { return x; } set { x = value; }
  }
  public double Y { //Y座標用のプロパティ
    get { return y; } set { y = value; }
  }
  public Map() { //デフォルトコンストラクタ
    x = 0.0;
    y = 0.0;
  }
  public Map(double x, double y) { //X座標、Y座標を指定するコンストラクタ
    this.x = x;
    this.y = y;
  }
  public static Map operator -(Map c) { //単項-演算子のオーバーロード
    Map result = new Map(); //作業用のMapオブジェクトを生成
    result.x = -1.0 * c.x; //X座標の符号を反転した結果を格納
    result.y = -1.0 * c.y; //Y座標の符号を反転した結果を格納 
    return result; //出来上がったオブジェクトを返す
  }
}
class Opov01 {
  public static void Main() {
    Map A = new Map(5, 2), B = -A; //BはAの反転座標
    Console.WriteLine("Aは({0},{1})", A.X, A.Y);
    Console.WriteLine("Bは({0},{1})", B.X, B.Y);
  }
}

補足:演算子のオーバーロードをしない場合

・演算子のオーバーロードと同一内容の通常のメソッドを記述することもできる
・しかし、演算子のオーバーロードを用いる方が直感的に理解でき、可読性も上がる

//座標クラスMapにおける単項-演算子のオーバーロード Map01.cs
using System;
class Map {
  double x; //X座標用のデータメンバ 
  double y; //Y座標用のデータメンバ
  public double X { //X座標用のプロパティ
    get { return x; } set { x = value; }
  }
  public double Y { //Y座標用のプロパティ
    get { return y; } set { y = value; }
  }
  public Map() { //デフォルトコンストラクタ
    x = 0.0;
    y = 0.0;
  }
  public Map(double x, double y) { //X座標、Y座標を指定するコンストラクタ
    this.x = x;
    this.y = y;
  }
  public static Map operator -(Map c) { //単項-演算子のオーバーロード
    Map result = new Map(); //作業用のMapオブジェクトを生成
    result.x = -1.0 * c.x; //X座標の符号を反転した結果を格納
    result.y = -1.0 * c.y; //Y座標の符号を反転した結果を格納 
    return result; //出来上がったオブジェクトを返す
  }
  public Map reverse(Map c) { //【参考】単項-演算子のオーバーロードではなく通常のメソッドで実装することもできるが
    Map result = new Map(); //作業用のMapオブジェクトを生成
    result.x = -1.0 * c.x; //X座標の符号を反転した結果を格納
    result.y = -1.0 * c.y; //Y座標の符号を反転した結果を格納 
    return result; //出来上がったオブジェクトを返す
  }
}
class Opov01 {
  public static void Main() {
    Map A = new Map(5, 2), B = -A; //BはAの反転座標
    Console.WriteLine("Aは({0},{1})", A.X, A.Y);
    Console.WriteLine("Bは({0},{1})", B.X, B.Y);
    B = A.reverse(A); //これでもBはAの反転座標にできるがわかりづらくなる
    Console.WriteLine("Bは({0},{1})", B.X, B.Y);
  }
}