前回のコメント

・今日やりました、プリプロセッサは前処理を行うプログラムということで
 テキストではササササッと説明が書かれていましたが、開発ではよく使われるのでしょうか?

 「#if」系はC/C++で開発したプログラムのC#への移植で用いることが多いようです。
 あるいは、C/C++経験の長いプログラマはよく使うかもしれません。
 C#独自のConditional属性の方が高機能で便利なのですが、記述されていない入門テキストが多いようで、
 存在を知らないという方も多いようです。

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

画像位置の管理、自弾の発射 など

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

・自機の位置を画像の中央座標で行うように変更しよう
・これに合わせて、画像、X座標、Y座標、左右方向の速度、表示状態を持つ構造体を定義する
  struct Item { 
    public Image i; //画像
    public int x;  //中心X座標
    public int y;  //中心Y座標
    public int hv; //左右方向の速度(左向きは負の数、右向きは正の数)
    public int v;  //表示状態(0:非表示、1以上:表示)
  }
・自機の情報は(今度登場する各アイテムの情報も)この構造体で表そう
・これに合わせて、中心座標を用いて画像を描画するDrawItemメソッドを作り、その内部でe.Graphics.DrawImageメソッドを実行するように
 しよう
  private void DrawItem(PaintEventArgs e, Item it) {
    xからit.i.Widthの半分を差し引いて左上X座標を得る
    yからit.i.Heightの半分を差し引いて左上Y座標を得る
    e.Graphics.DrawImage(it.i, 左上X座標, 左上Y座標);
  }
・合わせてデータメンバを整理しよう
    int gamemode = 0; //0:スタート画面、1:プレイ画面
    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"); //自機右寄画像の読込
    Font font1 = new Font("メイリオ", 20, FontStyle.Bold); //フォントを生成(メイリオ20P太字)
    Font fontt = new Font("メイリオ", 80, FontStyle.Bold); //フォントを生成(メイリオ80P太字)
    Brush brushs = new SolidBrush(Color.Yellow); //ブラシを生成(黄色)
    Timer timer = new Timer(); //タイマーを生成
    int backy = 0; //背景画像のつなぎ目のY座標
    //int playerx = 300; //【削除】自機のX座標
    //int playerv = 0; //【削除】自機の左右移動量(0:停止,-1:左,1:右)
・合わせてMainメソッドも整理しよう
    Program f = new Program(); //継承したフォームのインスタンスを生成
    f.Size = new Size(660, 520); //インスタンスのSizeプロパティに高さと幅を代入
    f.Text = "Game"; //インスタンスのTextプロパティにフォーム名を代入
    f.ControlBox = false; //ControlBoxを非表示にする
    f.FormBorderStyle = FormBorderStyle.Fixed3D; //フォームサイズ変更を禁止
    Application.Run(f); //インスタンスを画面に出す

作成例

//演習26 画像の位置管理を中央座標に
using System; //フォームアプリケーションに必須
using System.Windows.Forms; //フォームアプリケーションに必須
using System.Drawing; //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:プレイ画面
    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"); //自機右寄画像の読込
    Font font1 = new Font("メイリオ", 20, FontStyle.Bold); //フォントを生成(メイリオ20P太字)
    Font fontt = new Font("メイリオ", 80, FontStyle.Bold); //フォントを生成(メイリオ80P太字)
    Brush brushs = new SolidBrush(Color.Yellow); //ブラシを生成(黄色)
    Timer timer = new Timer(); //タイマーを生成
    int backy = 0; //背景画像のつなぎ目のY座標
    Item player; //【追加】プレイヤーの構造体オブジェクト
    //int playerx = 300; //【削除】自機のX座標
    //int playerv = 0; //【削除】自機の左右移動量(0:停止,-1:左,1:右)
    //【以下追加】中心座標を用いて画像を描画
    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) { //Formのメソッドをオーバーライド
        base.OnPaint(e); //まず、Formクラスにおけるメソッドの内容(基本再描画処理)を実行
        e.Graphics.DrawImage(backi, 0, backy); //背景画像を(0,つなぎ目)から描画
        e.Graphics.DrawImage(backi, 0, backy - backi.Height); //背景画像をその上に描画
        if (gamemode == 0) { //スタート画面
            e.Graphics.DrawString("GAME1", fontt, brushs, 100, 100); //タイトルの描画
            e.Graphics.DrawString("Hit Enter to Start", font1, 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 = playerr; break; //【変更】右寄 
                case -1: player.i = playerl; break; //【変更】左寄
            }
            DrawItem(e, player); //【追加】自機を表示
        }
    }
    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) { //タイマーから呼ばれるメソッド
        backy = (backy < backi.Height) ? backy + 1 : 0; //背景画像のつなぎ目を下げる
        if (gamemode == 1) { //プレイ画面
            player.hv = 0; //【変更】自機の向きを通常にする
            if (player.x > playeri.Width / 2 && GetKeyState((int)Keys.Left) < 0)
            { //【変更】左端ではなく←キーが押されている?
                player.x -= 10; //【変更】自機を左へ
                player.hv = -1; //【変更】左に移動中
            }
            if (player.x < backi.Width - playeri.Width / 2 && GetKeyState((int)Keys.Right) < 0) { //【変更】右端ではなく→キーが押されている?
                player.x += 10; //【変更】自機を右へ
                player.hv = 1; //【変更】右に移動中
            }
        }
        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; //【追加】自機の左右方向の速度
    }
    static void Main() { //publicの指定は任意
        Program f = new Program(); //継承したフォームのインスタンスを生成
        f.Size = new Size(660, 520); //インスタンスのSizeプロパティに高さと幅を代入
        f.Text = "Game"; //インスタンスのTextプロパティにフォーム名を代入
        f.ControlBox = false; //ControlBoxを非表示にする
        f.FormBorderStyle = FormBorderStyle.Fixed3D; //フォームサイズ変更を禁止
        Application.Run(f); //インスタンスを画面に出す
    }
}

演習27 自弾の表示(単独バージョン)

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

手順と仕様

① 自弾画像をImage型変数bulletiに読込む
② 自弾の構造体オブジェクトpbを定義
② 描画処理:プレイ画面で自弾があれば自弾を描画
③ タイマーイベント処理:スペースキーが押されていて自弾が非表示なら表示にする。
  また、画像を設定し、X座標は自機と同じに、Y座標は直上に触れる位置にする

作成例

//演習27 自弾の表示(単独バージョン)
using System; //フォームアプリケーションに必須
using System.Windows.Forms; //フォームアプリケーションに必須
using System.Drawing; //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:プレイ画面
    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"); //【追加】自弾画像の読込
    Font font1 = new Font("メイリオ", 20, FontStyle.Bold); //フォントを生成(メイリオ20P太字)
    Font fontt = new Font("メイリオ", 80, FontStyle.Bold); //フォントを生成(メイリオ80P太字)
    Brush brushs = new SolidBrush(Color.Yellow); //ブラシを生成(黄色)
    Timer timer = new Timer(); //タイマーを生成
    int backy = 0; //背景画像のつなぎ目の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) { //Formのメソッドをオーバーライド
        base.OnPaint(e); //まず、Formクラスにおけるメソッドの内容(基本再描画処理)を実行
        e.Graphics.DrawImage(backi, 0, backy); //背景画像を(0,つなぎ目)から描画
        e.Graphics.DrawImage(backi, 0, backy - backi.Height); //背景画像をその上に描画
        if (gamemode == 0) { //スタート画面
            e.Graphics.DrawString("GAME1", fontt, brushs, 100, 100); //タイトルの描画
            e.Graphics.DrawString("Hit Enter to Start", font1, 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 = playerr; break; //右寄 
                case -1: player.i = playerl; break; //左寄
            }
            DrawItem(e, player); //自機を表示
            if (pb.v == 1) { //【以下追加】自弾がある? 
                DrawItem(e, pb); //自弾を表示
            }
        }
    }
    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) { //タイマーから呼ばれるメソッド
        backy = (backy < backi.Height) ? backy + 1 : 0; //背景画像のつなぎ目を下げる
        if (gamemode == 1) { //プレイ画面
            player.hv = 0; //自機の向きを通常にする
            if (player.x > playeri.Width / 2 && GetKeyState((int)Keys.Left) < 0)
            { //左端ではなく←キーが押されている?
                player.x -= 10; //自機を左へ
                player.hv = -1; //左に移動中
            }
            if (player.x < backi.Width - playeri.Width / 2 && GetKeyState((int)Keys.Right) < 0) { //右端ではなく→キーが押されている?
                player.x += 10; //自機を右へ
                player.hv = 1; //右に移動中
            }
        }
        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; //自機の左右方向の速度
    }
    static void Main() { //publicの指定は任意
        Program f = new Program(); //継承したフォームのインスタンスを生成
        f.Size = new Size(660, 520); //インスタンスのSizeプロパティに高さと幅を代入
        f.Text = "Game"; //インスタンスのTextプロパティにフォーム名を代入
        f.ControlBox = false; //ControlBoxを非表示にする
        f.FormBorderStyle = FormBorderStyle.Fixed3D; //フォームサイズ変更を禁止
        Application.Run(f); //インスタンスを画面に出す
    }
}

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

講義メモ

テキスト篇:p.386「練習問題」から
ゲーム開発演習:画像位置の管理、自弾の発射 など

p.386 練習問題 ヒント

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

作成例

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

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

p.387 名前空間

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

p.388 名前空間の定義

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

p.388 namespace01.cs

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

p.390 usingディレクティブ

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

p.390 namespace02.cs

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

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

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

p.393 名前空間のネスト

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

p.394 namespace03.cs

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

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

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

p.396 namespace04.cs

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

p.398 プリプロセッサ

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

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

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

p.400 preprocess01.cs

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

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

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

作成例

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

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

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

p.402 Conditional属性

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

p.402 conditional01.cs

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

今週の話題

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

Respawn開発の「スター・ウォーズ」FPSも開発中止に…EA、全従業員の約5%を削減へ GO!
Epic Gamesがハッキングされた疑い―犯人グループが約200GBの内部情報をおさえたと主張 GO!

休講でしたが今週の話題

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

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

前回のコメント

・今日やった匿名型の、anonymous03では様々な型で値や文字を指定できて、
 便利そうだなーと感じました。
 実際、現場でもよくつかわれるのでしょうか?

 説明の通り、複数の定数をグループ化する場合に便利ですので、大量の定数が必要な場合に利用されることがあるようです。
 テキストには書かれていませんが、下記のような使い方も可能です。

        var m1 = new {x = 10, y = "ABC"}; //匿名型オブジェクトを生成してメンバx,yに初期値を与える
        var m2 = new {i = 3.14, j = 'C'}; //匿名型オブジェクトを生成してメンバi,jに初期値を与える
        var ma = new {p = m1, q = m2}; //匿名型オブジェクトを生成してメンバp,qに初期値を与える
        Console.WriteLine("ma = {0}", ma); //参照変数経由でメンバpとqの初期値を表示
        Console.WriteLine("ma.p = {0}", ma.p); //参照変数経由でメンバxとyの初期値を表示
        Console.WriteLine("ma.p.x = {0}", ma.p.x); //参照変数経由でメンバxの初期値を表示

講義メモ

テキスト篇:p.377「ジェネリックメソッド」から
ゲーム開発演習:画像位置の管理、自弾の発射 など

p.374 型パラメータの制約(補足① struct)

・制約として「struct」を指定すると、値型のみ型パラメータに指定できる
・例: class MyCalc<T> where T : struct {…}
 ⇒ MyCalc<int> m; //指定可能
・よって、値型であることを条件にしたい場合に用いる

例:

using System;
//ジェネリッククラスの定義(Tの制約:値型のみ)
class MyClass<T> where T : struct { 
    T x;
    public void show() {
        Console.WriteLine("x = {0}", x); 
    }
    public T X { set { x = value; } get { return x; } }
}
class generic06 {
    public static void Main() {
        MyClass<int> mc1 = new MyClass<int>(); //制約に反しないのでOK
        mc1.X = 100;
        mc1.show();
        // MyClass<string> mc2 = new MyClass<string>(); //制約に反するのでエラー
    }
}

p.374 型パラメータの制約(補足② struct)

・制約として「class」を指定すると、参照型のみ型パラメータに指定できる
・例: class MonsterHouse<T> where T : class {…}
 ⇒ MonsterHouse<Dragon> m; //指定可能
・よって、参照型であることを条件にしたい場合に用いる

例:

using System;
//ジェネリッククラスの定義(Tの制約:参照型のみ)
class MyClass<T> where T : class { 
    T x;
    public void show() {
        Console.WriteLine("x = {0}", x); 
    }
    public T X { set { x = value; } get { return x; } }
}
class generic06 {
    public static void Main() {
        MyClass<string> mc1 = new MyClass<string>(); //制約に反しないのでOK
        mc1.X = "S";
        mc1.show();
        //MyClass<int> mc2 = new MyClass<int>(); //制約に反するのでエラー
    }
}

p.374 型パラメータの制約(補足③ インターフェイス)

・制約としてインターフェイス名を指定すると、指定したインターフェイスを実装したクラスのみ型パラメータに指定できる
・よって、指定したインターフェイスに定義されているメソッドをジェネリッククラス内で記述可能になる

例:

using System;
//ジェネリッククラスの定義(Tの制約:インターフェイス指定)
interface Flyable { //「飛べる」ことを示す
    string howtofly(); //飛び方を返す
}
class Dragon : Flyable {
    string name;
    public Dragon() { name = "ドラゴン"; }
    public string howtofly() { return "翼で飛ぶ"; }
}
class Robot : Flyable {
    string name;
    public Robot() { name = "ロボット"; }
    public string howtofly() { return "ロケットで飛ぶ"; }
}
class Flyers<T> where T : Flyable, new() { //制約:インターフェイス名、デフォルトコンストラクタ有
    T x = new T();
    public void show() {
        Console.WriteLine(x.howtofly()); //Flyableインターフェイス実装が確定なのでこのメソッドが呼べる
    }
}
class generic06 {
    public static void Main() {
        Flyers<Dragon> mc1 = new Flyers<Dragon>(); //制約に反しないのでOK
        mc1.show();
        Flyers<Robot> mc2 = new Flyers<Robot>(); //制約に反しないのでOK
        mc2.show();
        //Flyers<int> mc3 = new Flyers<int>(); //制約に反するのでエラー
    }
}

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

・クラスと同様に、メソッド単体でも型パラメータの指定が可能で、これをジェネリックメソッドという
・書式: アクセス修飾子 戻り値型 メソッド名<型パラメータ,…>(引数リスト){…}
・インスタンスメソッドでも静的メソッドでも可能
・例:public static void MySet<T>(T x) { obj = (T)x; } //xはT型なのでキャスト可能

p.377 generic07.cs

//p.377 generic07.cs
using System;
class MyClass { //通常のクラス
    static object obj; //静的変数
    object ob; //インスタンス変数
    public static void myset<T>(T x) { //静的ジェネリックメソッド
        obj = (T)x; //型パラメータで受け取った型にキャストして代入
    }
    public static void show() { //静的メソッド
        Console.WriteLine(obj.ToString());
    }
    public void myset2<T>(T x) { //インスタンスメソッドでジェネリックメソッド
        ob = (T)x;//型パラメータで受け取った型にキャストして代入
    }
    public void show2() { //インスタンスメソッド
        Console.WriteLine(ob.ToString());
    }
}
class generic07 {
    public static void Main() {
        MyClass.myset<int>(12); //静的ジェネリックメソッドに型intを与えて呼ぶ
        MyClass.show(); //静的メソッドを呼んで表示
        MyClass.myset<string>("abc"); //静的ジェネリックメソッドに型intを与えて呼ぶ
        MyClass.show(); //静的メソッドを呼んで表示
        MyClass mc2 = new MyClass();
        mc2.myset2<int>(100); //インスタンスメソッドでジェネリックメソッドに型intを与えて呼ぶ
        mc2.show2(); //インスタンスメソッドを呼んで表示
    }
}

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

・ジェネリックメソッドでは内部や引数型のみならず、戻り値型にも型パラメータが指定できる
・例: public static List<T> makeList<T>(T x) {…}
・例えば、このメソッドは:
 ① T型のリストを生成
 ② 引数xで与えられたデータを①にAddメソッドで格納
 ③ ①を返す

ミニ演習: ジェネリックメソッドの戻り値型の型パラメータ mini377.cs

・上記の例を試すプログラムを作ろう

作成例

//ミニ演習: ジェネリックメソッドの戻り値型の型パラメータ mini377.cs
using System;
using System.Collections.Generic;
class MyClass { //通常のクラス
    public static List<T> makeList<T>(T x) { //リストの型を受け取るジェネリックメソッド
        List<T> mylist = new List<T>(); //受け取った型のリストを生成
        mylist.Add(x); //T型なので無条件に格納可能
        return mylist; //格納されたリストを返す
    }
}
class mini377 {
    public static void Main() {
        List<double> test = MyClass.makeList<double>(3.14); //実数型リストを作って返すメソッド
        Console.WriteLine(test[0]); //先頭要素を表示
    }
}

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

・ジェネリッククラスと同様の制約ジェネリクスメソッドにも指定できる
・書式:アクセス修飾子 戻り値型 メソッド名<型パラメータ,…>(引数リスト) where 型パラメータ:制約 {…}
・例:public List<T> makeList<T>(T x) : T:struct {…}

ミニ演習: mini377.cs・改

・ジェネリックメソッドの戻り値型の型パラメータのプログラム「mini377.cs」のジェネリクスメソッドに値型の制約を追記して
 動作を確認しよう

作成例

//ミニ演習: ジェネリックメソッドの戻り値型の型パラメータ mini377.cs・改
using System;
using System.Collections.Generic;
class MyClass { //通常のクラス
    public static List<T> makeList<T>(T x) where T:struct { //リストの型を受け取る制約付きジェネリックメソッド
        List<T> mylist = new List<T>(); //受け取った型のリストを生成
        mylist.Add(x); //T型なので無条件に格納可能
        return mylist; //格納されたリストを返す
    }
}
class mini377 {
    public static void Main() {
        List<double> test = MyClass.makeList<double>(3.14); //実数型リストを作って返すメソッド
        Console.WriteLine(test[0]); //先頭要素を表示
        //List<string> test1 = MyClass.makeList<string>("AB"); //制約のためにエラーになる
    }
}

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

・型パラメータはシグニチャの一部になるので、型パラメータの個数が異なるジェネリックメソッドによるオーバーロードが可能
・例:
 int Test(int){…}
 int Test<T>(int){…}
 int Test<T, U>(int){…}
・なお、戻り値型がシグニチャに含まれないのは、ジェネリックメソッドも同様

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

・静的メンバを除外してから「public void myset2(T x, U y){…}」を追加して動作を確認しよう

作成例

//アレンジ演習:p.377 generic07.cs
using System;
class MyClass { //通常のクラス
    static object obj; //静的変数
    object ob; //インスタンス変数
    object ob2; //インスタンス変数
    public void myset2<T>(T x) { //インスタンスメソッドでジェネリックメソッド
        ob = (T)x;//型パラメータで受け取った型にキャストして代入
    }
    public void myset2<T,U>(T x, U y) { //インスタンスメソッドでジェネリックメソッド
        ob = (T)x;//型パラメータで受け取った型にキャストして代入
        ob2 = (U)y;//型パラメータで受け取った型にキャストして代入
    }
    public void show2() { //インスタンスメソッド
        Console.WriteLine(ob.ToString());
    }
    public void show2a() { //インスタンスメソッド
        Console.WriteLine("{0} {1}", ob.ToString(), ob2.ToString());
    }
}
class generic07 {
    public static void Main() {
        MyClass mc2 = new MyClass();
        mc2.myset2<int>(100); //インスタンスメソッドでジェネリックメソッドに型intを与えて呼ぶ
        mc2.show2(); //インスタンスメソッドを呼んで表示
        mc2.myset2<int, double>(100, 3.14); //インスタンスメソッドでジェネリックメソッドに型intを与えて呼ぶ
        mc2.show2a(); //インスタンスメソッドを呼んで表示
    }
}

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

・クラスと同様にインターフェイスにおいても型パラメータの指定が可能で、ジェネリックインターフェイスという
・書式: interface インターフェイス名<型パラメータ,…>{…}
・ジェネリックインターフェイスを実装するクラスにおいて型パラメータを与えて用いる

例:

using System;
//ジェネリッククラスの定義(Tの制約:インターフェイス指定)
interface Flyable<T> { //「飛べる」ことを示す
    T howtofly(); //飛び方を返すメソッドを求めるが戻り値型は実装側で決めて良い
}
class Dragon : Flyable<string> { //string型に指定できる
    string name;
    public Dragon() { name = "ドラゴン"; }
    public string howtofly() { return name + "は翼で飛ぶ"; } //string型に指定できる
}
class Robot : Flyable<int> { //intに指定できる
    string name;
    public Robot() { name = "ロボット"; }
    public int howtofly() { return 3; } //intに指定できる
}
class generic06 {
    public static void Main() {
        Dragon Veldra = new Dragon();
        Console.WriteLine(Veldra.howtofly());
        Robot Atom = new Robot(); 
        Console.WriteLine(Atom.howtofly());
    }
}

p.380(プロパティの自動実装)

・p.381 anonymous02.csにおいて、プロパティの自動実装が用いられている
・内容が「get {return データメンバ;} set {データメンバ = value;}」のみのプロパティであれば、publicなデータメンバに続けて「{get; set;}」と記述することでプロパティの自動実装になる
・しかし、このプログラムにおいてはプロパティの自動実装を用いる必要性はなく、{get; set;}を記述しない場合と同じ結果になる
※ プロパティの自動実装はC#の簡易クラス機能などにおいて用いられる(今回は割愛)
※ また、C#の名前付けルールでは自動実装プロパティのプロパティ名の先頭文字は大文字と規定されている
※ なお、p.381 anonymous02.csの「int z;」は不用

//p.381 anonymous02.cs
using System;
class MyClass
{
    public int x; //自動実装プロパティは必要ではない
}
class anonymous01
{
    public static void Main()
    {
        MyClass mc = new MyClass();
        mc.x = 10;
        Console.WriteLine("x = {0}", mc.x);
    }
}

p.380 匿名型

・varキーワードを用いて匿名型の参照変数を定義したとき、オブジェクト初期化子で生成したオブジェクトへの参照を代入できる
・こうすると、名前のないクラスが用意され、その中にオブジェクト初期化子で指定したメンバが定義され、初期値が与えられる
・このメンバの型は初期値により自動決定され、任意指定はできない
・書式: var 参照変数 = new {メンバ名 = 値, … };
・例: var v = new {a = 10, b = 20};
・なお、このメンバは「参照変数.メンバ名」で扱えるが読込専用で、初期値は変更できない
・複数の定数をグループ化する場合に便利
・例: var player = new {name = "Shar", age = 24, x = 10, y = 20};

p.381 anonymous03.cs

//p.381 anonymous03.cs
using System;
class anonymous03 {
    public static void Main() {
        var mc = new {x = 10}; //匿名型のオブジェクトを生成してメンバxに初期値を与える
        Console.WriteLine("x = {0}", mc.x); //参照変数経由でメンバxの初期値を表示
    }
}

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

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