講義メモ オブジェクト指向演習:3人の魔導士の続き(オブジェクト配列、乱数の改良) ゲーム開発演習:メッセージボックス、システム側からの必要に応じた再描画の確認、背景画像の描画 RPG演習7 3人の魔導士 ver.2 ・3人の魔導士にしたいので、魔導士オブジェクトを長さ3の配列で扱うようにしよう ・実行イメージ 魔道士1(HP=14 MP=0)が現れた。 魔道士2(HP=14 MP=0)が現れた。 魔道士3(HP=14 MP=0)が現れた。 残りHPは20です。どれを攻撃しますか?(1/2/3:魔導士を攻撃 0:やめる):1 あなたの攻撃! 魔道士1(HP=14 MP=0)に8のダメージを与えた! 魔道士1(HP=6 MP=0)の攻撃! あなたは4のダメージを受けた! 魔道士2(HP=14 MP=0)の攻撃! あなたは3のダメージを受けた! 魔道士3(HP=14 MP=0)の攻撃! あなたは2のダメージを受けた! 残りHPは11です。どれを攻撃しますか?(1/2/3:魔導士を攻撃 0:やめる):1 : ・まずは、最低限の仕様を実装し、追加すべきことを洗い出そう ・「魔道士[0]の情報を表示する」を3人分に ・ループの条件「あなたのHPがあり、魔導士[0]のHPがある間」を「あなたのHPがあり、どれかの魔導士のHPがある間」に ・「攻撃しますか?(1:はい 0:やめる)」を「どれを攻撃しますか?(1/2/3:魔導士を攻撃 0:やめる)」に ・「魔道士[0]を攻撃する」を「魔道士[入力値を整数化して-1した値]を攻撃する」に ・「魔道士[0]は死んでない?」を「どれかの魔導士のHPがある?」に ・「魔道士[0]の攻撃!あなたのHPをマイナス」を3人分に ・「逃げられない!」の後の「魔道士[0]の攻撃!あなたのHPをマイナス」も3人分に 作成例 //オブジェクト指向演習 RPG演習7 3人の魔導士 ver.2 using System; class Madoshi { //魔導士を表すクラス(部品) int mp; //MP int hp; //HP int id; //魔導士番号 Random rnd = new Random(); //乱数用のRandomクラスのインスタンスを生成 public Madoshi (int id) { //コンストラクタ mp = rnd.Next(10); //0~9の間のランダムな整数を得てMPの初期値とする hp = 10 + rnd.Next(10); //10~19の間のランダムな整数を得てHPの初期値とする this.id = id; //生成時に指定した魔導士番号を設定 } public int HP { //HPを返すプロパティ get { return hp; } //getのみなので外部からのhpの書換を禁止 } public void DispInfo() { //魔導士の情報を表示する Console.Write("魔道士{0}(HP={1} MP={2})", id, hp, mp); //表示(改行しない) } public int Fight() { //魔道士があなたを攻撃し、あなたのダメージ値を返す if (rnd.Next(2) == 1) { //確率1/2で魔法攻撃かどうかを選ぶ int j = rnd.Next(5) + 1; //消費MPは1~5の乱数 DispInfo(); // 魔導士の情報を表示する Console.WriteLine("は消費MP" + j + "の魔法を唱えた!"); if (mp < j) { // MP不足? Console.WriteLine("MPが足りない!"); return 0; // あなたのダメージ=0を返す(攻撃終了) } else { // MP充足? Console.WriteLine("あなたは" + j * 4 + "のダメージを受けた!"); mp -= j; //MP消費 return j * 4; //あなたのダメージ=消費MPの4倍を返す(攻撃終了) } } else { //通常攻撃? int k = rnd.Next(5) + 1; //攻撃力は1~5の乱数 DispInfo(); //魔導士の情報を表示する Console.WriteLine("の攻撃!"); Console.WriteLine("あなたは" + k + "のダメージを受けた!"); return k; //あなたのダメージ=1~5を返す(攻撃終了) } } public void Damage() { //魔道士があなたからダメージをくらう int k = 5 + rnd.Next(5); //あなたの攻撃力は5~9の乱数 Console.WriteLine("あなたの攻撃!"); DispInfo(); //魔導士の情報を表示する(攻撃を受けた後の魔道士のHPを表示) Console.WriteLine("に" + k + "のダメージを与えた!"); hp -= k; //魔道士のHPをダウン } } class minimadou { //ゲーム本体のクラス public static void Main() { //ゲームを進行するメソッド const int N = 3; //【追加】魔導士の数 Random rnd = new Random(); //乱数用のRandomクラスのインスタンスを生成 string ans; //入力用 // 戦闘開始 int myhp = 20; // あなたのHP Madoshi[] m = new Madoshi[N]; //【変更】魔導士の配列を生成 for (int i = 0; i < N; i++) { //【追加】魔導士の数だけ繰返す m[i] = new Madoshi(i + 1); //【変更】魔道士(i + 1)を魔導士[i]に生成 m[i].DispInfo(); //【変更】魔道士[i]の情報を表示する Console.WriteLine("が現れた。"); } // 戦闘中(あなたが死ぬか,魔道士が死ぬまで繰り返す) while (myhp > 0 && (m[0].HP > 0 || m[1].HP > 0 || m[2].HP > 0)) { //【変更】あなたのHPがあり、どれかの魔導士のHPがある間 Console.Write("残りHPは{0}です。どれを攻撃しますか?(1/2/3:魔導士を攻撃 0:やめる):", myhp); //【変更】 ans = Console.ReadLine(); // 攻撃するかどうかを得る if (ans != "0") { //【変更】攻撃する? int num = int.Parse(ans) - 1; //【追加】攻撃対象の魔導士の添字を得る m[num].Damage(); //【変更】魔道士[num]を攻撃する if (m[0].HP > 0 || m[1].HP > 0 || m[2].HP > 0) { //【変更】どれかの魔導士のHPがある? for (int i = 0; i < N; i++) { //【追加】魔導士の数だけ繰返す myhp -= m[i].Fight(); //【変更】魔道士[i]の攻撃!あなたのHPをマイナス } } else { //魔道士のHPがもうない? Console.WriteLine("魔道士を倒した!"); } } else { // やめる? if (rnd.Next(2) == 0) { //確率50%で Console.WriteLine("逃げられない!"); for (int i = 0; i < N; i++) { //【追加】魔導士の数だけ繰返す myhp -= m[i].Fight(); //【変更】魔道士[i]の攻撃!あなたのHPをマイナス } } else { Console.WriteLine("逃げることができた"); break; // 繰り返し終了 } } if (myhp <= 0) { //あなたのHPがもうない? Console.WriteLine("あなたは死にました。"); } } } } RPG演習8 3人の魔導士 ver.3 ・Madoshiクラスに下記の静的メンバを追加して効率化しよう  ① static int mnum = 0; //魔導士が何人生成されたか  ② static int surv = 0; //生きている魔導士は何人いるか  ⇒p.202 静的メンバ  ・静的メンバはクラスに所属するので、インスタンスではなくクラスで持ちたい情報は静的データメンバにすると良い ・そして、Madoshiクラスのコンストラクタで①をカウントアップし、②に代入しておこう ・これに伴って、コンストラクタの引数が不要になる ・また、Damage()において、HPが尽きた場合、②をデクリメントしよう ・これで、Main()における「どれかの魔導士のHPがある?」を「生きている魔導士がいる?」に変更できる ・しかし、生きている魔導士は何人いるかを静的データメンバで持つと、publicにする必要があり、安全性に懸念がある ・そこで、生きている魔導士いるかどうかをbool型で返す静的プロパティをMadoshiクラスに追加しよう 作成例 //オブジェクト指向演習 RPG演習8 3人の魔導士 ver.3 using System; class Madoshi { //魔導士を表すクラス(部品) int mp; //MP int hp; //HP int id; //魔導士番号 static int mnum = 0; //【追加】魔導士が何人生成されたか static int surv = 0; //【追加】生きている魔導士は何人いるか Random rnd = new Random(); //乱数用のRandomクラスのインスタンスを生成 public Madoshi () { //【変更】コンストラクタ mp = rnd.Next(10); //0~9の間のランダムな整数を得てMPの初期値とする hp = 10 + rnd.Next(10); //10~19の間のランダムな整数を得てHPの初期値とする mnum++; //【追加】魔導士が何人生成されたかをカウントアップ this.id = mnum; //【変更】何人目の魔導士かを得て魔導士番号を設定 surv = mnum; //【追加】生きている魔導士の数に格納 } public int HP { //HPを返すプロパティ get { return hp; } //getのみなので外部からのhpの書換を禁止 } public static bool isSurv { //【以下追加】生きている魔導士がいるかを返すプロパティ get { return surv > 0; } //生きている魔導士がいればtrue、いなければfalseを返す } public void DispInfo() { //魔導士の情報を表示する Console.Write("魔道士{0}(HP={1} MP={2})", id, hp, mp); //表示(改行しない) } public int Fight() { //魔道士があなたを攻撃し、あなたのダメージ値を返す if (rnd.Next(2) == 1) { //確率1/2で魔法攻撃かどうかを選ぶ int j = rnd.Next(5) + 1; //消費MPは1~5の乱数 DispInfo(); // 魔導士の情報を表示する Console.WriteLine("は消費MP" + j + "の魔法を唱えた!"); if (mp < j) { // MP不足? Console.WriteLine("MPが足りない!"); return 0; // あなたのダメージ=0を返す(攻撃終了) } else { // MP充足? Console.WriteLine("あなたは" + j * 4 + "のダメージを受けた!"); mp -= j; //MP消費 return j * 4; //あなたのダメージ=消費MPの4倍を返す(攻撃終了) } } else { //通常攻撃? int k = rnd.Next(5) + 1; //攻撃力は1~5の乱数 DispInfo(); //魔導士の情報を表示する Console.WriteLine("の攻撃!"); Console.WriteLine("あなたは" + k + "のダメージを受けた!"); return k; //あなたのダメージ=1~5を返す(攻撃終了) } } public void Damage() { //魔道士があなたからダメージをくらう int k = 5 + rnd.Next(5); //あなたの攻撃力は5~9の乱数 Console.WriteLine("あなたの攻撃!"); DispInfo(); //魔導士の情報を表示する(攻撃を受けた後の魔道士のHPを表示) Console.WriteLine("に" + k + "のダメージを与えた!"); hp -= k; //魔道士のHPをダウン if (hp <= 0) { //【以下追加】倒された? surv--; //生きている魔導士の数を減らす } } } class minimadou { //ゲーム本体のクラス public static void Main() { //ゲームを進行するメソッド const int N = 3; //魔導士の数 Random rnd = new Random(); //乱数用のRandomクラスのインスタンスを生成 string ans; //入力用 // 戦闘開始 int myhp = 20; // あなたのHP Madoshi[] m = new Madoshi[N]; //魔導士の配列を生成 for (int i = 0; i < N; i++) { //魔導士の数だけ繰返す m[i] = new Madoshi(); //【変更】魔導士[i]を生成 m[i].DispInfo(); //魔道士[i]の情報を表示する Console.WriteLine("が現れた。"); } // 戦闘中(あなたが死ぬか,魔道士が死ぬまで繰り返す) while (myhp > 0 && Madoshi.isSurv) { //【変更】あなたのHPがあり、生きている魔導士がいる間 Console.Write("残りHPは{0}です。どれを攻撃しますか?(1/2/3:魔導士を攻撃 0:やめる):", myhp); //【変更】 ans = Console.ReadLine(); // 攻撃するかどうかを得る if (ans != "0") { //攻撃する? int num = int.Parse(ans) - 1; //攻撃対象の魔導士の添字を得る m[num].Damage(); //魔道士[num]を攻撃する if (Madoshi.isSurv) { //【変更】生きている魔導士がいる? for (int i = 0; i < N; i++) { //魔導士の数だけ繰返す myhp -= m[i].Fight(); //魔道士[i]の攻撃!あなたのHPをマイナス } } else { //魔道士のHPがもうない? Console.WriteLine("魔道士を倒した!"); } } else { // やめる? if (rnd.Next(2) == 0) { //確率50%で Console.WriteLine("逃げられない!"); for (int i = 0; i < N; i++) { //魔導士の数だけ繰返す myhp -= m[i].Fight(); //魔道士[i]の攻撃!あなたのHPをマイナス } } else { Console.WriteLine("逃げることができた"); break; // 繰り返し終了 } } if (myhp <= 0) { //あなたのHPがもうない? Console.WriteLine("あなたは死にました。"); } } } } RPG演習9 3人の魔導士 ver.4 ・問題点を1つずつ解決しよう ・乱数オブジェクトを各オブジェクトで持っているので無駄であり、しかも、生成タイミングが一致して同じ乱数系列になりやすい。 ・よって、乱数を持つ変数も静的データメンバにしてクラスで1個にし、各オブジェクトで使いまわそう。 ・また、せっかく魔導士の数を定数化したのに、表示部分「どれを攻撃しますか?(1/2/3:魔導士を攻撃 0:やめる)」が3人固定になっているのも改良しよう ・これは魔導士の数に応じて「1」「1/2」「1/2/3」「1/2/3/4」…の文字列を生成して返すメソッドを作ると良い ・ついでに、プレイヤーの初期HPを魔導士の数だけ増やそう ・また、倒されている魔導士が攻撃に参加してしまうゾンビ状態をなしにしよう ・また「魔道士を倒した!」を「魔道士●を倒した!」と「全ての魔道士を倒した!」にしよう ・そして、すでに倒されている魔導士を攻撃してしまったら「魔道士●はもういない」と表示して先に進むようにしよう 作成例 //オブジェクト指向演習 RPG演習9 3人の魔導士 ver.4 using System; class Madoshi { //魔導士を表すクラス(部品) int mp; //MP int hp; //HP int id; //魔導士番号 static int mnum = 0; //魔導士が何人生成されたか static int surv = 0; //生きている魔導士は何人いるか static Random rnd = new Random(); //【変更】乱数用のRandomクラスのインスタンスを生成 public Madoshi () { //コンストラクタ mp = rnd.Next(10); //0~9の間のランダムな整数を得てMPの初期値とする hp = 10 + rnd.Next(10); //10~19の間のランダムな整数を得てHPの初期値とする mnum++; //魔導士が何人生成されたかをカウントアップ this.id = mnum; //何人目の魔導士かを得て魔導士番号を設定 surv = mnum; //生きている魔導士の数に格納 } public int HP { //HPを返すプロパティ get { return hp; } //getのみなので外部からのhpの書換を禁止 } public static bool isSurv { //生きている魔導士がいるかを返すプロパティ get { return surv > 0; } //生きている魔導士がいればtrue、いなければfalseを返す } public void DispInfo() { //魔導士の情報を表示する Console.Write("魔道士{0}(HP={1} MP={2})", id, hp, mp); //表示(改行しない) } public int Fight() { //魔道士があなたを攻撃し、あなたのダメージ値を返す if (hp <= 0) { //【以下追加】すでに倒されていたら return 0; //0を返す } if (rnd.Next(2) == 1) { //確率1/2で魔法攻撃かどうかを選ぶ int j = rnd.Next(5) + 1; //消費MPは1~5の乱数 DispInfo(); // 魔導士の情報を表示する Console.WriteLine("は消費MP" + j + "の魔法を唱えた!"); if (mp < j) { // MP不足? Console.WriteLine("MPが足りない!"); return 0; // あなたのダメージ=0を返す(攻撃終了) } else { // MP充足? Console.WriteLine("あなたは" + j * 4 + "のダメージを受けた!"); mp -= j; //MP消費 return j * 4; //あなたのダメージ=消費MPの4倍を返す(攻撃終了) } } else { //通常攻撃? int k = rnd.Next(5) + 1; //攻撃力は1~5の乱数 DispInfo(); //魔導士の情報を表示する Console.WriteLine("の攻撃!"); Console.WriteLine("あなたは" + k + "のダメージを受けた!"); return k; //あなたのダメージ=1~5を返す(攻撃終了) } } public void Damage() { //魔道士があなたからダメージをくらう if (hp <= 0) { //【以下追加】すでに倒されている? Console.WriteLine("魔道士{0}はもういない", id); return; //抜ける } int k = 5 + rnd.Next(5); //あなたの攻撃力は5~9の乱数 Console.WriteLine("あなたの攻撃!"); DispInfo(); //魔導士の情報を表示する(攻撃を受けた後の魔道士のHPを表示) Console.WriteLine("に" + k + "のダメージを与えた!"); hp -= k; //魔道士のHPをダウン if (hp <= 0) { //倒された? Console.WriteLine("魔道士{0}を倒した!", id); //【追加】 surv--; //生きている魔導士の数を減らす } } } class minimadou { //ゲーム本体のクラス const int N = 3; //【移動】魔導士の数 static string selection() { //【以下追加】魔導士の数に応じて「1」「1/2」「1/2/3」「1/2/3/4」…の文字列を返す string ans = "1"; //返す文字列 for (int i = 1; i < N; i++) { //魔導士の数だけ繰返す ans += "/" + (i + 1); } return ans; } public static void Main() { //ゲームを進行するメソッド Random rnd = new Random(); //乱数用のRandomクラスのインスタンスを生成 string ans; //入力用 // 戦闘開始 int myhp = 20 * N; // あなたのHP(20×魔導士の数) Madoshi[] m = new Madoshi[N]; //魔導士の配列を生成 for (int i = 0; i < N; i++) { //魔導士の数だけ繰返す m[i] = new Madoshi(); //魔導士[i]を生成 m[i].DispInfo(); //魔道士[i]の情報を表示する Console.WriteLine("が現れた。"); } // 戦闘中(あなたが死ぬか,魔道士が死ぬまで繰り返す) while (myhp > 0 && Madoshi.isSurv) { //あなたのHPがあり、生きている魔導士がいる間 Console.Write("残りHPは{0}です。どれを攻撃しますか?({1}:魔導士を攻撃 0:やめる):", myhp, selection()); //【変更】 ans = Console.ReadLine(); // 攻撃するかどうかを得る if (ans != "0") { //攻撃する? int num = int.Parse(ans) - 1; //攻撃対象の魔導士の添字を得る m[num].Damage(); //魔道士[num]を攻撃する if (Madoshi.isSurv) { //生きている魔導士がいる? for (int i = 0; i < N; i++) { //魔導士の数だけ繰返す myhp -= m[i].Fight(); //魔道士[i]の攻撃!あなたのHPをマイナス } } else { //魔道士のHPがもうない? Console.WriteLine("全ての魔道士を倒した!"); //【変更】 } } else { // やめる? if (rnd.Next(2) == 0) { //確率50%で Console.WriteLine("逃げられない!"); for (int i = 0; i < N; i++) { //魔導士の数だけ繰返す myhp -= m[i].Fight(); //魔道士[i]の攻撃!あなたのHPをマイナス } } else { Console.WriteLine("逃げることができた"); break; // 繰り返し終了 } } if (myhp <= 0) { //あなたのHPがもうない? Console.WriteLine("あなたは死にました。"); } } } } オブジェクト指向演習次回予告:仕上げ(継承によるラスボスの生成と登場) ゲーム開発演習:メッセージボックス、システム側からの必要に応じた再描画の確認、背景画像の描画 テーマ7 メッセージボックス ・フォームアプリケーションにおいてテスト用に短い文字列を表示させたい場合などに便利なのがメッセージボックス ・System.Windows.Forms.MessageBoxクラスの静的メソッドShow(string)に引数として表示させたい文字列を渡すだけで良い ・「OK」ボタンの表示により閉じることができる 演習7 システム側からの必要に応じた再描画の確認 ・描画処理のオーバライド(override void OnPaint())において、メッセージボックスを表示しよう ・これにより、システム側からの必要に応じた再描画が行われ、自動的にOnPaint()が呼び出されることを確認しよう ・フォームの一部を画面外に移動させてから、戻す時にシステム側からの必要に応じた再描画が起きて、OnPaint()が呼び出されることがわかる 作成例 //演習7 システム側からの必要に応じた再描画の確認 using System; //C#標準クラス用 using System.Windows.Forms; //C#が提供するApplication、Formクラス用 using System.Drawing; //Sizeクラス用 class Program : Form { //Formクラスの派生クラス protected override void OnPaint(PaintEventArgs e) { //描画処理のオーバライド base.OnPaint(e); //元のメソッドの内容を呼び出す MessageBox.Show("OnPaintが呼ばれた"); //【追加】 } 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); //生成済のインスタンスを実行 } } テーマ8 フォーム上への画像の描画 ・画像ファイルをプロジェクトに配置することで、フォーム上に描画できる ・画像ファイルの形式としてはjpeg、bmp、gifなどに対応している ・描画の手順 ① System.Drawing.Imageクラスの静的メソッドFromFile(string)に、画像ファイル名を渡すと、Imageクラスのオブジェクトが生成される ② 描画処理の中で、System.Drawing.Graphicsクラスの静的メソッドDrawImage(Image, int, int)に、①とX座標、Y座標を渡すと、指定した位置に画像が描画される ・なお、X座標、Y座標はフォームの左上隅を(0,0)とする座標になる ・②で用いるGraphicsクラスのオブジェクトはOnPaintメソッドの引数「PaintEventArgs e」で得られるPaintEventArgsオブジェクトのGraphicsプロパティの戻り値で得る ・よって「e.Graphics.DrawImage(Image, int, int)」と呼び出せば良い 演習8 背景画像の描画 ・下記のテスト用画像をプロジェクトの\bin\debugフォルダにダウンロード   http://ha242.rundog.org/wp-content/uploads/2025/05/back.bmp ・この画像をフォームの座標(0,0)に配置しよう 作成例 //演習8 背景画像の描画 using System; //C#標準クラス用 using System.Windows.Forms; //C#が提供するApplication、Formクラス用 using System.Drawing; //Size、Graphics、Imageクラス用 class Program : Form { //Formクラスの派生クラス Image backi = Image.FromFile("back.bmp"); //【追加】画像ファイルを読み込む protected override void OnPaint(PaintEventArgs e) { //描画処理のオーバライド base.OnPaint(e); //元のメソッドの内容を呼び出す e.Graphics.DrawImage(backi, 0, 0); //【変更】画像をフォーム左上に配置 } 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); //生成済のインスタンスを実行 } } テーマ9 画像ファイル読込のトラブルへの対応 ・画像ファイルの読込においては、エラーの発生時に何も表示されないため、処置が難しい ・そこで、p.323のtry-catchを仕込んでおくと良い ・書式例:  try {   変数 = Image.FromFile(ファイル名);  } catch {   エラー発生時処理  } 演習9 背景画像の描画(エラー対処付き) ・画像ファイルの読み込みをコンストラクタで行うようにしよう ・そして、画像ファイル読込のトラブルへの対応を盛り込み、エラー発生時はメッセージボックスを表示しよう ・この時、catchを「catch (exception e)」とすることで、エラー発生オブジェクトを得ると良い ・このオブジェクトの文字列表現を「e.ToString()」で得てメッセージボックスに表示しよう ゲーム開発演習:次回予告:画像のプロパティ、画像の重ね合わせ、キーが押された情報を得る など