【ゲーム開発のためのC#入門講座・応用拡張編】最終課題にチャレンジしてみよう【#8】

5.0_C#応用拡張編

まずは一言

ここまでありがとうございました&お疲れ様でした!

このあとは応用強化編

応用学習編ではオブジェクト指向の三大要素を中心に、さらに再利用しやすくしたり自由度を広げたりしてくれる技術を学びました。自由過ぎて扱いが難しいのが難点ですけどね。

これでオブジェクト指向のお話はおしまいです。

次の応用強化編では、Unityでゲームを作る上で知っておきたいこと、例えばバグ調査の便利機能やデータを外部に保存する方法などを学習します。

それが終わると、いよいよUnityでのゲーム開発に突入します。

長かったですね。でも色んなこと学んできたからこそ、今の皆さんならきっと色んな実装ができるはず。ぜひ色々頭を悩ませて、色々ゲーム開発の醍醐味を楽しんでもらえればと思うので、この企画ももう少しだけ走り続けます。

というわけで、これまで学習してきたすべてをぶつける最終課題にチャレンジ……の前に、複数データをまとめて扱う上で便利な新しいループ処理と、継承時のコンストラクタの注意点をここで学習しておきましょう。

インデックスから解き放たれろ!

これまでずっと活用していたforループ。インデックスを持つ配列を使うには必須と言っても過言ではないですね。また、前回学習したリストもインデックスでアクセスできるので、こちらを扱うのにもforループは欠かせません。

が、インデックスは0から始まるというのがやはりネックですよね。どんなに慣れても、0から始まることを意識しなきゃいけないこと自体がそもそも煩わしいです。

また、表示上の順番は1から表示したい場合などはいちいち+1してないといけません。

int[] scores = {
    10,
    20,
    30
};
​
for (int i = 0; i < scores.Length; i++)
{
    // ↓1番から表示したい場合は+1しなきゃいけない!
    Console.Write((i + 1).ToString() + "番目");
    Console.WriteLine(scores[i]);
}

インデックスという呪縛から解き放たれたい……。

そんなあなたにとっておきのループ方法があります。

それがforeachループです。

foreachループって?

foreachループは、インデックスでアクセスしなくても順番に値を取り出してくれるループ機能です。

早速実例を見てみましょう。

int[] scores = {
    10,
    20,
    30
};
​
foreach (int score in scores)
{
    Console.WriteLine(score);
}
コンソール画面

10
20
30

きちんと格納したデータがインデックスの順番で出力されていますね。

foreachループではインデックスでアクセスしない代わりに、インデックスの順番で取り出した値がループ内専用の特別な変数に格納されます。

そのため、構文は下記のようになります。

foreach (データ型 特別な変数 in コレクション)
{
    ……ループ処理……
}

コレクションと記載してある通り、ListやDictioaryもループすることができます。

// Listでもループできるぜ!
List<int> scores = new List<int>() {
    10,
    20,
    30
};
scores.Add(40);
​
foreach (int score in scores)
{
    Console.WriteLine(score);
}
// Dictionaryでもループできるぜ!
Dictionary<string, int> fruits = new Dictionary<string, int>();
fruits.Add("リンゴ", 50);
fruits.Add("オレンジ", 100);
fruits.Add("バナナ", 150);
​
foreach (int value in fruits.Values)
{
    Console.WriteLine(value);
}

この方法でも決して悪くはないのですが、Dictionaryについては値よりもキーでループを回すのがおすすめです。なぜならキーでループ処理を行うと、キーと値の両方にアクセスできるようになってとても便利だからです。

Dictionary<string, int> fruits = new Dictionary<string, int>();
fruits.Add("リンゴ", 50);
fruits.Add("オレンジ", 100);
fruits.Add("バナナ", 150);
​
// ↓キーでループを回せば、
foreach (string key in fruits.Keys)
{
    // キーも、キーに紐づく値も、まとめて両方にアクセスできるよ!
    Console.WriteLine(key);
    Console.WriteLine(fruits[key]);
}
コンソール画面

リンゴ
50
オレンジ
100
バナナ
150

同じことをforループでやっていた時と比べると、すっきり度が一目瞭然ですね。

Dictionary<string, int> fruits = new Dictionary<string, int>();
fruits.Add("リンゴ", 50);
fruits.Add("オレンジ", 100);
fruits.Add("バナナ", 150);

for (int i = 0; i < fruits.Keys.Count; i++)
{
    // ↓文字が圧倒的に密!
    Console.WriteLine(fruits.Keys.ElementAt(i));
    Console.WriteLine(fruits[fruits.Keys.ElementAt(i)]);
}

このように、foreachループを使うと簡潔にループ処理を書くことができます。

インデックスも参照したい場合はforループを使わざるを得ないですが、それ以外の場合は積極的にこちらを活用していくとよいでしょう。

継承コンストラクタの注意点

これまで色んなクラスを継承してもらった時のように、引数のないコンストラクタを持つクラスであれば問題ないです。が、もし引数のあるコンストラクタを持つクラスを継承する場合は、親クラスのコンストラクタを必ず実行しなければならないルールになっています。

そのため、明示的に子クラスのコンストラクタを実装し、そこで親クラスのコンストラクタを実行するよう記述するのですが、この書き方が結構独特です。

// ↓もしこのクラスを継承するなら……
internal class BattleCharacter
{
    public string Name;
    public int HP;
    public int ATK;
    public string VictoryDialogue;

    public BattleCharacter(int hp, int atk, string victoryDialogue)
    {
        HP = hp;
        ATK = atk;
        VictoryDialogue = victoryDialogue;
    }

    public BattleCharacter(string name, int hp, int atk, string victoryDialogue)
    {
        Name = name;
        HP = hp;
        ATK = atk;
        VictoryDialogue = victoryDialogue;
    }
}

早速実例を見てみましょう。

internal class Magician : BattleCharacter
{
    // ↓このように、コンストラクタを実装しなければならない!
    public Magician(int hp, int atk, string victoryDialogue) : base(hp, atk, victoryDialogue)
    {

    }
}

構文は下記の通りです。

アクセス修飾子 クラス名(引数) : base(親クラスコンストラクタの引数)
{
    ……何か処理があれば実装……
}

何だか長ったらしくて、正直面倒ですよね。

このように、コンストラクタは継承においても強い制約になりがちです。コンストラクタで初期値を実装するかどうかという点も、こういったことを考えながら決めた方がよさそうですね。

最終課題

それでは改めて、最終課題にチャレンジしてみましょう。

今回は応用学習編で作成した「3VS1」のボスバトル仕様の戦闘シミュレーションを、継承とポリモーフィズムを使って改良したいと思います。

基本的な枠組みはこちらで作成しました。仕様に記載のクラスを作成してください。

それでは、Let’s Rock!(ギルティギア風

仕様

・クラス「BattleCharacter」に下記のメソッドを実装する
 メソッド名 :Attack
 アクセス  :public
  引数    :なし
  戻り値  :int
  内容    :メンバー変数「ATK」の値を返す
  その他   :オーバーライド可能とすること

・新規のスクリプトファイルを作成し、下記のクラスを定義する
 クラス名  :Magician
 継承     :クラス「BattleCharacter」を継承する
 役割     :MPがある限り、高火力の攻撃を繰り出すBattleCharacter
 メンバー変数:private int MP = 30;(初期値)
 メソッド名  :Attack(オーバーライド)
  内容    :MPが4以上だったら4引いた上で、ATK * 2の値を返す。
         逆にMPが4未満だったらATK / 2の値を返す。
   
・新規のスクリプトファイルを作成し、下記のクラスを定義する
 クラス名  :Boss
 継承     :クラス「BattleCharacter」を継承する
 役割     :ランダムに強力な攻撃を放つBattleCharacter
 メソッド名  :Attack(オーバーライド)
  内容    :「Random」クラスのメソッド「Next」に引数2を渡し、
         戻り値が0の場合は親クラスの同名メソッドを実行する。
         上記以外の場合はメンバー変数「ATK」 * 2の値を返す。

テンプレート
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// ↓名前空間はご自由に!
namespace CSharp5_8
{
    internal class BattleCharacter
    {
        public string Name {get; private set; }
        public int HP {get; private set; }
        public int ATK {get; private set; }
        public string VictoryDialogue {get; private set; }

        public void SayVictoryDialogue()
        {
            Console.WriteLine(VictoryDialogue);
        }
        
        // ↓オーバーライド可能なメソッドを実装しよう!
        
        // ここまで
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// ↓名前空間はご自由に!
namespace CSharp5_8
{
    internal class BattleController
    {
        public void PlayersTurn(BattleCharacter[] players, BattleCharacter enemy)
        {
            for (int i = 0; i < players.Length; i++)
            {
                if (players[i].HP <= 0)
                {
                    continue;
                }
                // ↓Attackメソッドを実行するよう変更してるよ!
                enemy.HP -= players[i].Attack();
            }
        }

        public void EnemyTurn(BattleCharacter[] players, BattleCharacter enemy)
        {
            for (int i = 0; i < players.Length; i++)
            {
                if (players[i].HP <= 0)
                {
                    continue;
                }
                // ↓Attackメソッドを実行するよう変更してるよ!
                players[i].HP -= enemy.Attack();
                break;
            }
        }

        public bool IsGameOver(BattleCharacter[] players)
        {
            for (int i = 0; i < players.Length; i++)
            {
                if (players[i].HP > 0)
                {
                    return false;
                }
            }
            return true;
        }
    }
}
// ↓名前空間は定義したものを設定してね!
using CSharp5_8;

BattleCharacter[] players = new BattleCharacter[] {
    new BattleCharacter(25, 10, "ちょろいぜ!"),
    new BattleCharacter(30, 12, "あまいぜ!"),
    new Magician(21, 6, "ちょろあまですね!") // <=クラスをMagicianに変えてるよ!
};

for (int i = 0; i < players.Length; i++)
{
    Console.Write("Player" + (i + 1).ToString() + "の名前を入力してください。 > ");
    players[i].Name = Console.ReadLine();
}

// ↓クラスをBossに変えてるよ!
BattleCharacter enemy = new Boss("ボス", 200, 8, "貴様らの負けだよーん");

BattleController battleController = new BattleController();
while (true)
{
    battleController.PlayersTurn(players, enemy);
    Console.WriteLine("enemyHP:" + enemy.HP.ToString());
    if (enemy.HP <= 0)
    {
        break;
    }

    battleController.EnemyTurn(players, enemy);
    for (int i = 0; i < players.Length; i++)
    {
        string target = players[i].Name + "HP";
        System.Console.WriteLine(players[i].Name + "HP:" + players[i].HP.ToString());
    }
    if (battleController.IsGameOver(players))
    {
        break;
    }
}

if (enemy.HP <= 0)
{
    for (int i = 0;i < players.Length; i++)
    {
        players[i].SayVictoryDialogue();
    }
}
else
{
    enemy.SayVictoryDialogue();
}

答え合わせ

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CSharp5_8
{
    internal class Magician : BattleCharacter
    {
        private int MP = 30;

        public Magician(int hp, int atk, string victoryDialogue) : base(hp, atk, victoryDialogue)
        {
        }

        public override int Attack()
        {
            if (MP >= 4)
            {
                MP -= 4;
                return ATK * 2;
            }
            else
            {
                return ATK / 2;
            }
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CSharp5_8
{
    internal class Boss : BattleCharacter
    {
        public Boss(string name, int hp, int atk, string victoryDialogue) : base(name, hp, atk, victoryDialogue)
        {
        }

        public override int Attack()
        {
            Random random = new Random();
            if (random.Next(2) == 0)
            {
                return base.Attack();
            }
            else
            {
                return ATK * 2;
            }
        }
    }
}

ボスが強くなったので、実行すると負けちゃいますね。

有終の美、体力を2倍にいじって、ボスを倒せるようにしてみましょう。

BattleCharacter[] players = new BattleCharacter[] {
    new BattleCharacter(50, 10, "ちょろいぜ!"),
    new BattleCharacter(60, 12, "あまいぜ!"),
    new Magician(42, 6, "ちょろあまですね!")
};
コンソール画面

ティトレイHP:-6
マオHP:44
アニーHP:42
enemyHP:-18
ちょろいぜ!
あまいぜ!
ちょろあまですね!

最後に

改めて、お疲れ様でした!

基礎学習編で作っていたプログラムと見比べてみると、随分と色んなことが変わりましたね。また、最初はおまじないといっていた「public」や「class」や「static」も、ようやくすべて理解できるところまで来られました。

ひとつひとつ丁寧にやってきたからこそ、辿り着いた風景ですね。

技術的には今回の応用拡張編までで十分過ぎるぐらいバッチリOK!

応用強化編はUnityゲーム開発で欠かせない技術を補足として埋めていきます。

引き続きお付き合いいただければ幸いです。

それでは、また次の記事でお会いしましょう!

Posted by 夕目紅