今週の話題

販売本数ランキング 今回トップは「スーパーマリオブラザーズ ワンダー(Switch)」GO!
PS公式ワイヤレスヘッドセット“PULSE Elite”レビュー。平面磁気ドライバー採用で粒立ちよく“聴きやすい”サウンド GO!
『ヘブンバーンズレッド』効果も長くは続かず…グリーは選択と集中が必要な時期?【ゲーム企業の決算を読む】GO!
学生からインディーゲームクリエイターになった人々は、どのように自らの行く道を決めたのか?『ElecHead』の生高橋氏らが鼎談【IDC2023】GO!
「おす」がテーマ!「第21回UE5ぷちコン」エントリー開始―サイドイベント「ぷちコン ゲームジャム」は東京・大阪・名古屋で開催 GO!
DeNA、エンジニア向け技術カンファレンス「TechCon 2024」をオンライン/オフラインで開催2/29 GO!

ヒューマンアカデミーが運営するCREST GAMING、ゲーミングデバイスブランド「STORIA」とスポンサー契約締結 GO!

販売中止となった『The Day Before』になりすますゲームがSteamに出現―パッチノートも公開し大型アップデートによる再出発装う GO!
バンダイナムコHDの株価が昨年来安値を更新…オンラインゲーム新作不調とタイトル編成の見直しなど第3四半期決算影響か GO!
バンダイナムコ、開発中だった5タイトル以上を開発中止に…オンラインゲームは可能性があるとして今後も取り組みを続ける GO!

参考:【2024年】スマホ向け無料ブラウザゲーム38選!おすすめ・新作Webゲーム紹介 GO!

前回のコメント

・型パラメータの制約では、4種類の制約があってそれぞれ条件によって
 使い方も変わるんだなとわかりました。それぞれのデータメンバやコンストラクタなどの
 扱う数が増えたら頭から煙があがりそうになりますね
 ゆっくりちゃんとよく見ると「なるほど」となるので使いこなせるよう頑張ります。

 テキストだけではわかりづらいテーマでしたね。
 必要でしたらサンプルをご提供しますので、是非、使いこなしてください。

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

:演習24、自機の表示と左右移動、自機のアニメーション など

テーマ28 キーボードの状態を得る(再掲載)

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

演習24 上矢印キーが押されていたらスコアアップ(再掲載)

・上矢印キーのKey列挙子はKeys.Up
・これを用いて、Playメソッド内で、上矢印キーが押されているかチェックし、押されていたらスコアをインクリメントしよう

作成例

//演習24 上矢印キーが押されていたらスコアアップ
using System; //フォームアプリケーションに必須
using System.Windows.Forms; //フォームアプリケーションに必須
using System.Drawing; //Image用
class Program : Form { //Formクラスを継承
    [System.Runtime.InteropServices.DllImport("user32.dll")] //【追加】外部DLLのインポート
    private static extern short GetKeyState(int nVirtKey); //【追加】メソッドの外部定義指定
    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; //スコア
    int backy = 0; //背景画像のつなぎ目のY座標
    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, backy); //背景画像を(0,つなぎ目)から描画
        e.Graphics.DrawImage(backb, 0, backy - backb.Height); //背景画像をその上に描画
        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++; //【削除】スコアアップ
        backy = (backy < backb.Height) ? backy + 1 : 0; //背景画像のつなぎ目を下げる
        if (GetKeyState((int)Keys.Up) < 0) { //【以下追加】↑キーが押されている?
            score++; //スコアアップ
        }
        Invalidate(); //画面再描画を依頼する
    }
    Program() { //コンストラクタ
        DoubleBuffered = true; //ダブルバッファリングの導入
        KeyDown += new KeyEventHandler(OnKeyDown); //キーボードが押された時に呼ばれるメソッドを登録
        timer.Tick += new EventHandler(Play); //タイマーから呼ばれるメソッドを登録
        timer.Interval = 10; //タイマーの間隔
    }
    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); //インスタンスを画面に出す
    }
}

演習25 自機を表示し左右に動かす

・自機の通常画像(player.gif)をダウンロードして配置
 
・自機通常画像を初期位置(300,390)から描画
・左矢印キー(Keys.Left)が押されていたら左へ10ドット、右(Keys.Right)なら右へ10ドット移動する
・赤い枠とスコアアップは削除

作成例

//演習25 自機を表示し左右に動かす
using System; //フォームアプリケーションに必須
using System.Windows.Forms; //フォームアプリケーションに必須
using System.Drawing; //Image用
class Program : Form { //Formクラスを継承
    [System.Runtime.InteropServices.DllImport("user32.dll")] //外部DLLのインポート
    private static extern short GetKeyState(int nVirtKey); //メソッドの外部定義指定
    Image backb = Image.FromFile("backb.bmp"); //背景画像の読込
    Image enemy = Image.FromFile("enemy.gif"); //アイテム画像の読込
    Image burn = Image.FromFile("burn.gif"); //効果用画像の読込
    Image playeri = Image.FromFile("player.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; //スコア
    int backy = 0; //背景画像のつなぎ目のY座標
    int playerx = 300; //【追加】自機のX座標
    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, backy); //背景画像を(0,つなぎ目)から描画
        e.Graphics.DrawImage(backb, 0, backy - backb.Height); //背景画像をその上に描画
        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); //スコア文字列の描画
            e.Graphics.DrawImage(playeri, playerx, 390); //【追加】自機画像を描画
        }
    }
    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 < backb.Height) ? backy + 1 : 0; //背景画像のつなぎ目を下げる
        //if (GetKeyState((int)Keys.Up) < 0) { //【以下削除】↑キーが押されている?
        //    score++; //スコアアップ
        //}
        if (gamemode == 1) { //【以下追加】プレイ画面
            if (GetKeyState((int)Keys.Left) < 0) { //←キーが押されている?
                playerx -= 10; //自機を左へ
            }
            if (GetKeyState((int)Keys.Right) < 0) { //→キーが押されている?
                playerx += 10; //自機を右へ
            }
        }
        Invalidate(); //画面再描画を依頼する
    }
    Program() { //コンストラクタ
        DoubleBuffered = true; //ダブルバッファリングの導入
        KeyDown += new KeyEventHandler(OnKeyDown); //キーボードが押された時に呼ばれるメソッドを登録
        timer.Tick += new EventHandler(Play); //タイマーから呼ばれるメソッドを登録
        timer.Interval = 10; //タイマーの間隔
    }
    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); //インスタンスを画面に出す
    }
}

演習25の続き 自機移動範囲の制限と画像変更

・←が押されているとき、自機左寄画像 playerl.gif を表示しよう
 
・→が押されているとき、自機右寄画像 playerr.gif を表示しよう
 
・そして、自機が左端・右端から出ないように制限しよう
・なお、背景、自機の幅は.Widthを用いること
・つまり、左は0チェックで良いが、右はX座標が「背景.Width-自機.Width」を超えるとはみ出す可能性がある(マージンによって調整する)

ヒント
・←と→が押されているとき、違う画像を表示するには、自機がどちらに向かっているのかを示す変数playervを用いると良い
・playerv(0:停止,-1:左,1:右)を、playメソッドの先頭で0にしておき、左や右が押されていたら、対応する値にする
・そして、onPaintメソッドでplayervの値に応じて描画する画像を選べば良い

作成例

//演習25 自機を表示し左右に動かす
using System; //フォームアプリケーションに必須
using System.Windows.Forms; //フォームアプリケーションに必須
using System.Drawing; //Image用
class Program : Form { //Formクラスを継承
    [System.Runtime.InteropServices.DllImport("user32.dll")] //外部DLLのインポート
    private static extern short GetKeyState(int nVirtKey); //メソッドの外部定義指定
    Image backb = Image.FromFile("backb.bmp"); //背景画像の読込
    Image enemy = Image.FromFile("enemy.gif"); //アイテム画像の読込
    Image burn = Image.FromFile("burn.gif"); //効果用画像の読込
    Image playeri = Image.FromFile("player.gif"); //【追加】自機画像の読込
    Image playerl = Image.FromFile("playerl.gif"); //【追加】自機左寄画像の読込
    Image playerr = Image.FromFile("playerr.gif"); //【追加】自機右寄画像の読込
    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; //スコア
    int backy = 0; //背景画像のつなぎ目のY座標
    int playerx = 300; //【追加】自機のX座標
    int playerv = 0; //【追加】自機の左右移動(0:停止,-1:左,1:右)
    Timer timer = new Timer(); //タイマーを生成
    protected override void OnPaint(PaintEventArgs e) { //Formのメソッドをオーバーライド
        base.OnPaint(e); //まず、Formクラスにおけるメソッドの内容(基本再描画処理)を実行
        e.Graphics.DrawImage(backb, 0, backy); //背景画像を(0,つなぎ目)から描画
        e.Graphics.DrawImage(backb, 0, backy - backb.Height); //背景画像をその上に描画
        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) { //プレイ画面
            string s = String.Format("SCORE:{0:000,000}", score); //スコア文字列を編集
            e.Graphics.DrawString(s, f20, byel, 400, 10); //スコア文字列の描画
            switch (playerv) { //【以下追加】動きに合わせた自機画像を描画
                case  0: e.Graphics.DrawImage(playeri, playerx, 390); break;
                case  1: e.Graphics.DrawImage(playerr, playerx, 390); break; 
                case -1: e.Graphics.DrawImage(playerl, playerx, 390); break; 
            }
        }
    }
    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 < backb.Height) ? backy + 1 : 0; //背景画像のつなぎ目を下げる
        if (gamemode == 1) { //【以下追加】プレイ画面
            playerv = 0; //仮に自機は停止中とする
            if (playerx > 0 && GetKeyState((int)Keys.Left) < 0) { //左端ではなく←キーが押されている?
                playerx -= 10; //自機を左へ
                playerv = -1; //左に移動中
            }
            if (playerx < backb.Width - playeri.Width && GetKeyState((int)Keys.Right) < 0) { //右端ではなく→キーが押されている?
                playerx += 10; //自機を右へ
                playerv = 1; //右に移動中
            }
        }
        Invalidate(); //画面再描画を依頼する
    }
    Program() { //コンストラクタ
        DoubleBuffered = true; //ダブルバッファリングの導入
        KeyDown += new KeyEventHandler(OnKeyDown); //キーボードが押された時に呼ばれるメソッドを登録
        timer.Tick += new EventHandler(Play); //タイマーから呼ばれるメソッドを登録
        timer.Interval = 10; //タイマーの間隔
    }
    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); //インスタンスを画面に出す
    }
}

提出:演習25 自機を表示し左右に動かす(追加分は含んでもOK)

講義メモ

テキスト篇:p.369「コレクションクラスの初期化」から
ゲーム開発演習:演習24、自機の表示と左右移動、自機のアニメーション など

p.369 コレクションクラスの初期化

・ジェネリッククラスであるかどうかとは無関係に、コレクション初期化子によるコレクションクラスの初期化が可能
 ※ テキストではジェネリックなコレクションクラスのみを掲載しているが…
・書式: クラス名 参照変数 = new クラス名{値,…};
・書式(ジェネリッククラスの場合): クラス名<型> 参照変数 = new クラス名<型>{値,…};
・例:
 ArrayList anames = new ArrayList { "一郎", "二郎", "三郎" };
 List<string> names = new List<string> { "一郎", "二郎", "三郎" };

p.369 list02.cs

//p.369 list02.cs
using System;
using System.Collections.Generic; //List<T>用
class list02 {
    public static void Main() {
        List<string> names = new List<string> { "一郎", "二郎", "三郎" }; //コレクション初期化子
        foreach (string s in names) { //全要素について繰返す
            Console.WriteLine(s);
        }
    }
}

アレンジ演習:p.369 list02.cs

・非ジェネリッククラスであるArrayListに置き換えよう

作成例

//アレンジ演習:p.369 list02.cs
using System;
using System.Collections; //ArrayList用
class list02 {
    public static void Main() {
        ArrayList anames = new ArrayList { "一郎", "二郎", "三郎" }; //コレクション初期化子
        foreach (var s in anames) { //全要素について繰返す
            Console.WriteLine(s);
        }
    }
}

p.370(オブジェクト初期化子)

・p.370 list03.csでは(特に説明がないが)オブジェクト初期化子を利用している
・オブジェクト初期化子は「データメンバ名 = 値,…」形式でオブジェクトの初期値を設定できる記述法
・ジェネリッククラスであるかどうかとは無関係
・書式: クラス名 参照変数 = new クラス名{ データメンバ名 = 値,…};
・例:
 class BOOK { public string title; public string author; public decimal price; }
  :
 BOOK b = new BOOK {title = "我が輩は猫である", author = "夏目漱石", price = 1050};
・なお、この形式を用いるには、データメンバがpublicであること(プロパティによる代用は可能)
・p.370 list03.csのように、オブジェクト初期化子で初期化したオブジェクトを、コレクション初期化子に渡すことも可能

p.370 list03.cs

//p.370 list03.cs
using System;
using System.Collections.Generic;
class BOOK {
    public string title;
    public string author;
    public decimal price;
}
class list03 {
    public static void Main() {
        List<BOOK> mybook = new List<BOOK> { //コレクション初期化子
            new BOOK {title = "我が輩は猫である",
                author = "夏目漱石",
                price = 1050}, //オブジェクト初期化子
            new BOOK {title = "雲の階段",
                author = "渡辺淳一",
                price = 1600}, //オブジェクト初期化子
            new BOOK {title = "こころ",
                author = "夏目漱石",
                price = 1200} //オブジェクト初期化子
        };
        Console.WriteLine("----蔵書一覧----");
        foreach (BOOK b in mybook) { //全要素について繰返す
             Console.WriteLine("{0}, {1}, {2}円", b.title, b.author, b.price);
        }
    }
}

アレンジ演習:p.370 list03.cs

・オブジェクト初期化子で予め初期化したオブジェクトをコレクション初期化子に渡すように書き換えよう

作成例

//アレンジ演習:p.370 list03.cs
using System;
using System.Collections.Generic;
class BOOK {
    public string title;
    public string author;
    public decimal price;
}
class list03 {
    public static void Main() {
        BOOK b1 = new BOOK {title = "我が輩は猫である", author = "夏目漱石", price = 1050}; //オブジェクト初期化子
        BOOK b2 = new BOOK {title = "雲の階段", author = "渡辺淳一", price = 1600}; //オブジェクト初期化子
        BOOK b3 = new BOOK {title = "こころ", author = "夏目漱石", price = 1200}; //オブジェクト初期化子
        List<BOOK> mybook = new List<BOOK> { b1, b2, b3 }; //コレクション初期化子
        Console.WriteLine("----蔵書一覧----");
        foreach (BOOK b in mybook) { //全要素について繰返す
             Console.WriteLine("{0}, {1}, {2}円", b.title, b.author, b.price);
        }
    }
}

p.371 ジェネリッククラスと継承

・ジェネリッククラスの継承には、型パラメータの指定方法による制限がある
・ジェネリッククラスの型パラメータに具体的な型を指定している状態にして、継承の基本クラスにできる
・書式: class 派生クラス : 基本ジェネリッククラス<型> {…}
・これをクローズ構築型といい、派生クラスは通常クラスでもジェネリッククラスでも良い
 例: class Slime : Monster {…}
・ジェネリッククラスの型パラメータを型パラメータのままにして、継承の基本クラスにできる
・書式: class 派生クラス<型パラメータ> : 基本ジェネリッククラス<型パラメータ> {…}
・これをオープン構築型といい、派生クラスもジェネリッククラスにする
・これにより、型パラメータの引継ぎが行われる

p.371 generic03.cs

//p.371 generic03.cs
using System;
class MyClass<T> { //基本ジェネリッククラス
    public T x;
}
class MyClass2 : MyClass<int> { //クローズ構築型を継承
    //ここに public int x; があるとみなされる(ジェネリックは解決されている)
    public void show() {
        Console.WriteLine(x);
    }
}
class generic03 {
    public static void Main() {
        MyClass2 mc = new MyClass2();
        mc.x = 10; //クローズ構築型で指定したint型になっている
        mc.show();
    }
}

アレンジ演習:p.371 generic03.cs

・クローズ構築型では派生クラスは通常クラスでもジェネリッククラスでも良い
・MyClass2クラスをジェネリッククラスMyClass2<T>クラスにして、T型のデータメンバyを追記して動作を確認しよう
・「class MyClass<T> {…}」「class MyClass2<T> : MyClass<int> {…}」となるが、型パラメータは引継がれないので、
 2つのTは別のものとして扱われる

作成例

//アレンジ演習:p.371 generic03.cs
using System;
class MyClass<T> { //基本ジェネリッククラス
    public T x;
}
class MyClass2<T> : MyClass<int> { //クローズ構築型を継承するジェネリッククラス(型パラメータは引継がれない)
    //ここに public int x; があるとみなされる(ジェネリックは解決されている)
    public T y;
    public void show() {
        Console.WriteLine("x = {0} y = {1}", x, y);
    }
}
class generic03 {
    public static void Main() {
        MyClass2<string> mc = new MyClass2<string>(); //派生ジェネリッククラスのオブジェクト生成
        mc.x = 10; //クローズ構築型で指定したint型になっている
        mc.y = "Shar"; //ジェネリッククラスのオブジェクト生成で指定したstring型になっている
        mc.show();
    }
}

p.372 generic04.cs

//p.372 generic04.cs
using System;
class MyClass<T> { //基本ジェネリッククラス
    public T x;
}
class MyClass2<T> : MyClass<T> { //オープン構築型を継承 
    //ここに public T x; があるとみなされる(指定した型になる)
    public void show() {
        Console.WriteLine(x);
    }
}
class generic04 {
    public static void Main() {
        MyClass2<int> mc = new MyClass2<int>(); //定義のすべてのTがintになる
        mc.x = 2;
        mc.show();
        MyClass2<string> mc2 = new MyClass2<string>(); //定義のすべてのTがstringになる
        mc2.x = "test";
        mc2.show();
    }
}

アレンジ演習:p.372 generic04.cs

・MyClass2クラスをジェネリッククラスMyClass2<T,U>クラスにして、U型のデータメンバyを追記して動作を確認しよう
・「class MyClass<T> {…}」「class MyClass2<T,U> : MyClass<T> {…}」となり、型パラメータTは引継がれ、
 2つのTは同じものとして扱われる

作成例

//アレンジ演習:p.372 generic04.cs
using System;
class MyClass<T> { //基本ジェネリッククラス
    public T x;
}
class MyClass2<T, U> : MyClass<T> { //オープン構築型を継承 
    //ここに public T x; があるとみなされる(指定した型になる)
    public U y;
    public void show() {
        Console.WriteLine("x = {0} y = {1}", x, y);
    }
}
class generic04 {
    public static void Main() {
        MyClass2<int, string> mc = new MyClass2<int, string>(); //定義のすべてのTがintになる
        mc.x = 2; //型パラメータT経由でint型になる
        mc.y = "test"; //型パラメータU経由でstring型になる
        mc.show();
    }
}

p.373(複数の型パラメータを持つジェネリッククラスの継承)

・複数の型パラメータを持つジェネリッククラスを継承するとき、全て型パラメータに型を指定すれば、クローズ構築型にすることができる
・あるいは、同じ並びの型パラメータを持つジェネリッククラスで継承すれば、オープン構築型になる
・また、一部の型パラメータに型を指定することも可能で、こうするとオープン構築型になる
例: class Slime<T> : Monster<T, int> {…}

p.373 generic05.cs

//p.373 generic05.cs
using System;
class MyClass<T, U> { //2つの型パラメータを持つ基本ジェネリッククラス
    public T x;
    public U y;
}
class MyClass2<T> : MyClass<T, int> { //共有されていない型は明示するオープン構築型
    // ここに「public T x;」があるとみなされる
    // ここに「public int y;」があるとみなされる
    public void show() {
        Console.WriteLine("x = {0}, y = {1}", x, y);
    }
}
class generic05 {
    public static void Main() {
        MyClass2<string> mc = new MyClass2<string>(); //Tの型のみ指定すれば良い
        mc.x = "cat"; //生成時に指定したstringがTになる
        mc.y = 20; //継承時に指定したintがUになる
        mc.show();
    }
}

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

・ジェネリッククラスの定義において「どんな型が指定されてもOK」ということはあまりない
・データメンバと単純なプロパティのみを持つクラスであれば可能だが、なんらかのメソッドを持つ場合、
 指定できる型を制限する必要がある

・型パラメータに指定できるクラスを制約として明示することができる
・書式: class ジェネリッククラス<型パラメータ> where 型パラメータ : クラス名 {…}
・こうすると、指定したクラス及びその派生クラスのみ型パラメータに指定できる
・例: class SlimeHouse<T> where T : Slime {…}
 ⇒ SlimeHouse<HoimiSlime> m; //指定可能
・よって、指定したクラスに定義されているメソッドをジェネリッククラス内で記述可能になる

・制約として「struct」を指定すると、値型のみ型パラメータに指定できる
・例: class MyCalc<T> where T : struct {…}
 ⇒ MyCalc<int> m; //指定可能
・よって、値型であることが必要な記述が可能になる

・制約として「class」を指定すると、参照型のみ型パラメータに指定できる
・例: class MonsterHouse<T> where T : class {…}
 ⇒ MonsterHouse<Dragon> m; //指定可能
・よって、参照型であることが必要な記述が可能になる

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

・制約として「class, new()」を指定すると、デフォルトコンストラクタを持つクラスのみ型パラメータに指定できる
・制約として「クラス名, new()」を指定すると、指定したクラス及びその派生クラスでデフォルトコンストラクタを持つもののみ
 型パラメータに指定できる
・例: class SlimeHouse<T> where T : Slime, new() {…}
 ⇒ SlimeHouse<HoimiSlime> m; //HoimiSlimeクラスにデフォルトコンストラクタがあれば指定可能
・よって、デフォルトコンストラクタを呼び出す処理をジェネリッククラス内で記述可能になる

p.375 generic06.cs

//p.375 generic06.cs
using System;
class MyClass {
    public int x;
    public string name;
    public MyClass() { //デフォルトコンストラクタ
        x = 0;
        name = "";
    }
}
//ジェネリッククラスの定義(Tの制約:MyClassまたはその派生クラスでデフォルトコンストラクタ有)
class MyClass2<T> where T : MyClass, new() { 
    T p = new T(); //デフォルトコンストラクタがある前提なのでOK
    public void show() {
        //MyClassまたはその派生クラスなのでxとnameがあるからOK
        Console.WriteLine("x = {0}, p.name = {1}", p.x, p.name); 
    }
    public void setxname(int n, string str) {
        p.x = n; //MyClassまたはその派生クラスなのでint型のxがあるからOK
        p.name = str; //MyClassまたはその派生クラスなのでstring型のnameがあるからOK
    }
}
class generic06 {
    public static void Main() {
        MyClass2<MyClass> mc2 = new MyClass2<MyClass>(); //制約に反しないのでOK
        mc2.setxname(100, "abc");
        mc2.show();
    }
}

アレンジ演習:p.375 generic06.cs

・MyClassの派生クラスYourClassを定義し「MyClass2<YourClass>」で利用可能なことを確認しよう

作成例

//アレンジ演習:p.375 generic06.cs
using System;
class MyClass {
    public int x;
    public string name;
    public MyClass() { //デフォルトコンストラクタ
        x = 0;
        name = "";
    }
}
class YourClass : MyClass { //【以下追加】MyClassの派生クラスの定義
    //ここに「public int x;」があるとみなされる
    //ここに「public string name;」があるとみなされる
    public int y;
    public YourClass() { //デフォルトコンストラクタ
        x = 0;
        y = 10;
        name = "";
    }
}
//ジェネリッククラスの定義(Tの制約:MyClassまたはその派生クラスでデフォルトコンストラクタ有)
class MyClass2<T> where T : MyClass, new() { 
    T p = new T(); //デフォルトコンストラクタがある前提なのでOK
    public void show() {
        //MyClassまたはその派生クラスなのでxとnameがあるからOK
        Console.WriteLine("x = {0}, p.name = {1}", p.x, p.name); //※派生クラスのyは利用不可
    }
    public void setxname(int n, string str) {
        p.x = n; //MyClassまたはその派生クラスなのでint型のxがあるからOK
        p.name = str; //MyClassまたはその派生クラスなのでstring型のnameがあるからOK
    }
}
class generic06 {
    public static void Main() {
        MyClass2<YourClass> mc2 = new MyClass2<YourClass>(); //【変更】制約に反しないのでOK
        mc2.setxname(100, "abc");
        mc2.show();
    }
}

今週の話題

販売本数ランキング 今回トップは「ペルソナ3 リロード(PS5)」GO!
ドット絵によるアニメーション制作の味方『Aseprite』がSteamなどで50%オフの1025円に(2/14まで)。入門~本格的なゲーム制作まで幅広く活用可能 GO!
『GTA5』累計販売本数が1億9,500万本を突破―『GTA6』トレイラーや12月のアップデートでプレイヤーが増加傾向など決算報告で明らかに GO!
ゲームオーディオのグローバルリーダーAudiokinetic主催「Wwise Tour 2023 Tokyo」イベントレポート&『Hi-Fi RUSH』を手掛けたTango Gameworksインタビュー GO!
ネクソン、過去最高売上も業績予想には届かず―『THE FINALS』は“課金額が予想を上回る”好調スタート、日本では『ブルアカ』が成長 GO!
『The Elder Scrolls II: Daggerfall』有志Unity移植版の制作者が独自作品の開発に着手 GO!
KADOKAWA、アクワイアを100%子会社化―フロム・ソフトウェア、スパイク・チュンソフトとの連携を推進 GO!
ニンテンドースイッチの国内販売台数がニンテンドーDSを超える…気になる次世代機については言及避ける GO!
『ファンパレ』『FF7EC』『プロセカ』のエンジニア・クリエイターが登壇―サイバーエージェント主催の技術カンファレンス3/7開催 GO!
【決算】コロプラの第1四半期は2割の減収、4億1400万の営業赤字で着地 GO!
Unity Japan、大前広樹氏が社長を退任し、再びゲーム開発へ―新社長には産業営業本部長 松本靖麿氏 GO!

『原神』チートツール開発者に対し「可能な限り最高規模の」代償を、カナダで5万ドル規模以上の訴訟が進行中か―海外メディア報道 GO!

前回のコメント

・今日習ったジェネリックでは、使い方次第でとても便利なクラスだと思ったと同時に
 unityでも使い時はあったりするのかなと思いました。

 はい、ありますので、しっかり理解しておくと良いですよ。
 Unityオフィシャルのガイド:
 https://docs.unity3d.com/ja/2019.4/Manual/GenericFunctions.html
 ジェネリックジョブ(やや難しいので参考程度に)
 https://docs.unity3d.com/ja/Packages/com.unity.burst@1.8/manual/compilation-generic-jobs.html

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

:背景画面のスクロール、キーボードの状態を知る など

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

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

演習23 背景画像の縦スクロール

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

作成例

//演習23 背景画像の縦スクロール
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; //スコア
    int backy = 0; //【追加】背景画像のつなぎ目のY座標
    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, backy); //【変更】背景画像を(0,つなぎ目)から描画
        e.Graphics.DrawImage(backb, 0, backy - backb.Height); //【追加】背景画像をその上に描画
        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++; //スコアアップ
        backy = (backy < backb.Height) ? backy + 1 : 0; //【追加】背景画像のつなぎ目を下げる
        Invalidate(); //画面再描画を依頼する
    }
    Program() { //コンストラクタ
        DoubleBuffered = true; //ダブルバッファリングの導入
        KeyDown += new KeyEventHandler(OnKeyDown); //キーボードが押された時に呼ばれるメソッドを登録
        timer.Tick += new EventHandler(Play); //タイマーから呼ばれるメソッドを登録
        timer.Interval = 10; //【変更】タイマーの間隔
    }
    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); //インスタンスを画面に出す
    }
}

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

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

演習24 上矢印キーが押されていたらスコアアップ

・上矢印キーのKey列挙子はKeys.Up
・これを用いて、Playメソッド内で、上矢印キーが押されているかチェックし、押されていたらスコアをインクリメントしよう

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

講義メモ

テキスト篇:p.363「ジェネリックとは」から
ゲーム開発演習:背景画面のスクロール、キーボードの状態を知る など

p.363 ジェネリックとは

・ジェネリックとは「総称」という意味で「型を変数で扱えば、型だけが異なるクラスやメソッドを1つにできる」仕組み
・つまり、メソッドのオーバーロードや引数の可変個化(p.201)などをさらに進化させた位置づけ

p.363 ジェネリッククラス

・利用時に型を指定できるクラス
・定義書式: class クラス名<型パラメータ> {…}
・通常、型パラメータには大文字1文字を用いることが多い(C#が提供するジェネリッククラスがそうなので)
・例: class Monster<T> { public T power; } //型パラメータで指定した型のデータメンバを持つクラス
・ジェネリッククラスを通常のクラスから利用するには、宣言時と生成時に型を指定する
・宣言書式: クラス名<型> 参照変数;
・生成書式: 参照変数 = new クラス名<型>(…);
・宣言と生成を同時に行う場合、型を2箇所指定できる: クラス名<型> 参照変数 = new クラス名<型>(…);
・例: Monster<int> rimuru = new Monster<int>(); //①powerを整数で扱うモンスターrimuruの初期化
・例: Monster<double> gobuta = new Monster<double>(); //②powerを実数で扱うモンスターgobutaの初期化
・例: Monster<string> shuna = new Monster<string>(); //③powerを文字列で扱うモンスターshunaの初期化
・つまり、内部的に型パラメータの置き換えが行われ、その結果のオブジェクトが生成される
・例: class Monster<T> { public T power; } ⇒ class Monster { public int power; } //①の場合

p.364 generic01.cs

//p.364 generic01.cs
using System;
class MyClass<T> { //型パラメータTを持つジェネリッククラス
    public T name; //宣言時に型が決まるデータメンバ
    public T GetVal() { //宣言時戻り値が決まるメソッド(アクセッサ)
        return name; //nameの型が戻り値型に一致するのでどんな型でもOK
    }
}
class generic01 {
    public static void Main() {
        MyClass<int> mca = new MyClass<int>(); //型にintを指定してインスタンスを生成
        mca.name = 10; //インスタンス変数nameの型はintなので整数を代入できる
        Console.WriteLine(mca.GetVal()); //GetValメソッドの戻り値型はintなので整数が返る
        MyClass<string> mcb = new MyClass<string>(); //型にstringを指定してインスタンスを生成
        mcb.name = "猫"; //インスタンス変数nameの型はstringなので文字列を代入できる
        Console.WriteLine(mcb.GetVal()); //GetValメソッドの戻り値型はstringなので文字列が返る
    }
}

アレンジ演習:p.364 generic01.cs

・mca.GetVal()は戻り値型がintなので、これに+10すると加算になる
・mcb.GetVal()は戻り値型がstringなので、これに+10すると連結になる
・以上を追記して確認しよう

作成例

//アレンジ演習:p.364 generic01.cs
using System;
class MyClass<T> { //型パラメータTを持つジェネリッククラス
    public T name; //宣言時に型が決まるデータメンバ
    public T GetVal() { //宣言時戻り値が決まるメソッド(アクセッサ)
        return name; //nameの型が戻り値型に一致するのでどんな型でもOK
    }
}
class generic01 {
    public static void Main() {
        MyClass<int> mca = new MyClass<int>(); //型にintを指定してインスタンスを生成
        mca.name = 10; //インスタンス変数nameの型はintなので整数を代入できる
        Console.WriteLine(mca.GetVal() + 10); //GetValメソッドの戻り値型はintなので整数が返り加算
        MyClass<string> mcb = new MyClass<string>(); //型にstringを指定してインスタンスを生成
        mcb.name = "猫"; //インスタンス変数nameの型はstringなので文字列を代入できる
        Console.WriteLine(mcb.GetVal() + 10); //GetValメソッドの戻り値型はstringなので文字列が返り連結
    }
}

p.365(複数の型パラメータを持つジェネリッククラス)

・ジェネリッククラスの型パラメータ数には制限はない
・2つ以上を用いる場合は、カンマで区切る
・定義書式例: class クラス名<型パラメータ①, 型パラメータ②> {…}
・例: class Monster<T, U> { public T hp; public U mp; } //型パラメータで指定した型のデータメンバを持つクラス
・複数の型パラメータを持つジェネリッククラスを通常のクラスから利用するには、宣言時と生成時に全ての型パラメータに型を指定する
・宣言書式: クラス名<型①,型②> 参照変数;
・生成書式: 参照変数 = new クラス名<型①,型②>(…);
・宣言と生成を同時に行う場合: クラス名<型①,型②> 参照変数 = new クラス名<型①,型②>(…);
・例: Monster<int, double> rimuru = new Monster<int, double>(); //hpを整数、mpを実数で扱うモンスターrimuruの初期化

p.365(配列の型パラメータを持つジェネリッククラス)

・データメンバとして配列を持ち、その型をジェネリックで指定することも可能
・宣言書式: 型パラメータ[] 配列名;
・生成書式: 配列名 = new 型パラメータ[要素数];
・型が宣言時に決まらないので、初期化はできないが、要素数指定の宣言+生成は可能
 例: public T[] a = new T[10];

p.365 generic02.cs

//p.365 generic02.cs
using System;
class MyClass<T, U> { //2個の型パラメータT,Uを持つジェネリッククラス
    public T[] x; //T型の配列xの宣言のみ
    public U[] y; //U型の配列yの宣言のみ
    public MyClass(int n) { //コンストラクタ(引数は要素数)
        x = new T[n]; //要素数nでT型の配列xの要素を生成
        y = new U[n]; //要素数nでU型の配列yの要素を生成
    }
}
class generic02 {
    public static void Main() {
        int n; //配列の要素数       
        Console.Write("n = ");
        string strN = Console.ReadLine();
        if (!Char.IsDigit(strN[0])) { //先頭文字が数字ではない?
            Console.WriteLine("入力が不適切です");
            return; //終了
        }
        n = int.Parse(strN); //整数化
        //2引数を指定してインスタンス生成、コンストラクタが動作し2配列の要素を各n個生成
        MyClass<int, string> mc = new MyClass<int, string>(n); 
        for (int i = 0; i < n; i++) { //全要素について繰返す
            Console.Write("番号--- ");
            string strNo = Console.ReadLine();
            if (!Char.IsDigit(strNo[0])) { //先頭文字が数字ではない?
                Console.WriteLine("不適切な番号です");
                break; //終了
            }
            mc.x[i] = int.Parse(strNo); //整数化して配列xの要素[i]に代入
            Console.Write("氏名--- ");
            string strName = Console.ReadLine();
            mc.y[i] = strName; //そのまま配列yの要素[i]に代入
        }
        Console.WriteLine(); //改行
        for (int i = 0; i < n; i++) { //全要素について繰返す
            Console.WriteLine("[{0}] {1}", mc.x[i], mc.y[i]);
        }
    }
}

【補足】ジェネリックプロパティ

・プロパティの戻り値型を型パラメータにできる
・定義書式: public 型パラメータ プロパティ名 { get {…} set {…} }
・例: public T HP { get { return hp; } set { hp = value; } }
・valueの内部的な型は、自動的に型パラメータと同じ型になる

アレンジ演習:p.364 generic01.cs

・GatValメソッドをプロパティ(getのみ)にしよう

作成例

//アレンジ演習:p.364 generic01.cs
using System;
class MyClass<T> { //型パラメータTを持つジェネリッククラス
    public T name; //宣言時に型が決まるデータメンバ
    public T GetVal { //宣言時戻り値が決まるプロパティ
        get { return name; } //nameの型が戻り値型に一致するのでどんな型でもOK
    }
}
class generic01 {
    public static void Main() {
        MyClass<int> mca = new MyClass<int>(); //型にintを指定してインスタンスを生成
        mca.name = 10; //インスタンス変数nameの型はintなので整数を代入できる
        Console.WriteLine(mca.GetVal); //GetValプロパティの戻り値型はintなので整数が返る
        MyClass<string> mcb = new MyClass<string>(); //型にstringを指定してインスタンスを生成
        mcb.name = "猫"; //インスタンス変数nameの型はstringなので文字列を代入できる
        Console.WriteLine(mcb.GetVal); //GetValプロパティの戻り値型はstringなので文字列が返る
    }
}

【補足】ジェネリックインデクサ

・インデクサの型を型パラメータにできる
・定義書式: public 型パラメータ this[型 インデックス] { get {…} set {…} }
・例: public T this[int i] { get { return a[i]; } set { a[i] = value; } }

アレンジ演習:p.365 generic02.cs

・配列xをジェネリックインデクサで扱うようにしよう

作成例

//アレンジ演習:p.365 generic02.cs
using System;
class MyClass<T, U> { //2個の型パラメータT,Uを持つジェネリッククラス
    public T[] x; //T型の配列xの宣言のみ
    public U[] y; //U型の配列yの宣言のみ
    public MyClass(int n) { //コンストラクタ(引数は要素数)
        x = new T[n]; //要素数nでT型の配列xの要素を生成
        y = new U[n]; //要素数nでU型の配列yの要素を生成
    }
    public T this[int i] { //【以下追加】T型の配列xによるインデクサ
        get { return x[i]; } set { x[i] = value; }
    }
}
class generic02 {
    public static void Main() {
        int n; //配列の要素数       
        Console.Write("n = ");
        string strN = Console.ReadLine();
        if (!Char.IsDigit(strN[0])) { //先頭文字が数字ではない?
            Console.WriteLine("入力が不適切です");
            return; //終了
        }
        n = int.Parse(strN); //整数化
        //2引数を指定してインスタンス生成、コンストラクタが動作し2配列の要素を各n個生成
        MyClass<int, string> mc = new MyClass<int, string>(n); 
        for (int i = 0; i < n; i++) { //全要素について繰返す
            Console.Write("番号--- ");
            string strNo = Console.ReadLine();
            if (!Char.IsDigit(strNo[0])) { //先頭文字が数字ではない?
                Console.WriteLine("不適切な番号です");
                break; //終了
            }
            mc[i] = int.Parse(strNo); //【変更】整数化して配列xの要素[i]にインデクサ経由で代入
            Console.Write("氏名--- ");
            string strName = Console.ReadLine();
            mc.y[i] = strName; //そのまま配列yの要素[i]に代入
        }
        Console.WriteLine(); //改行
        for (int i = 0; i < n; i++) { //全要素について繰返す
            Console.WriteLine("[{0}] {1}", mc[i], mc.y[i]); //【変更】配列xはインデクサ経由で利用
        }
    }
}

p.367(ジェネリックとコレクションクラス)

・ArrayListクラスのようにデータ構造を提供するクラスの中には、ジェネリッククラスもあり、その代表例がList<T>クラス
・List<T>クラスはデータ構造に格納したい要素の型を型パラメータで与えて用いる
・すると、配列と同様に使えて配列より柔軟なデータ構造を型指定で利用できる
・ArrayListクラスでは格納すると型情報が失われ、取り出し時にキャストで与える必要があるが、
 List<T>クラスでは型情報を与えられるのでその必要はなく、ミスを防止できる
・List<T>クラスにも、要素を格納するAddメソッド、要素数を返すCountプロパティ、整列するSortメソッドなどがある
・using System.Collections.Generic; を指定すると良い

p.368 list01.cs

// p.368 list01.cs
using System;
using System.Collections.Generic; //List用
class list01 {
    public static void Main() {
        bool bEnd = false; //終了フラグをオフに
        List<int> mylist = new List<int>(); //int型を指定してジェネリックコレクションクラスを利用
        while (true) { //無限ループ
            Console.Write("Data = ");
            string strData = Console.ReadLine();
            if (!Char.IsDigit(strData[0])) { //先頭文字が数字ではない?
                bEnd = true; //終了フラグをオンに
            } else {
                mylist.Add(int.Parse(strData)); //整数化してリストに格納(いくつでもOK)
            }
            if (bEnd) { //終了フラグ:オン?
                break; //繰返し終了
            }
        }
        Console.WriteLine(); //改行
        for (int i = 0; i < mylist.Count; i++) { //全要素について繰返す
            Console.WriteLine("[{0}] {1}", i, mylist[i]);
        }
        Console.WriteLine();
        mylist.Sort(); //昇順に整列
        for (int i = 0; i < mylist.Count; i++) { //全要素について繰返す
            Console.WriteLine("[{0}] {1}", i, mylist[i]);
        }        
    }
}