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

4.0_C#応用学習編

まずは一言

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

このあとは応用拡張編!

応用学習編では、現代プログラミングの真髄であるオブジェクト指向の基本について学んできました。

次の応用拡張編では、オブジェクト指向の三大要素である下記について学習します。

  • カプセル化
  • 継承
  • ポリモーフィズム(多態性)

もし考え方、理解の仕方に悩むことがあれば、オブジェクト指向の目的はあくまで「わかりやすくて再利用しやすいプログラムを作ること」という原点に立ち返りましょう。

それでは、これまで学習してきたすべてをぶつける最終課題にチャレンジ……の前に、コンストラクタとメソッドに関する便利な技術を学んでおきましょう。

同名メソッドは基本的にNG

基本的に同じクラスに同じ名前のメソッドを持つことはできません。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
​
namespace CSharp4_8
{
    internal class BattleCharacter
    {
        public string Name = "";
        public int Level = 0;
        public int HP = 0;
        public int ATK = 0;
​
        // ↓同じ名前のメソッドが、
        public int Attack()
        {
            return ATK + Level * 2 / 5;
        }
        // ↓ふたつあるのはあかんぜよ!
        public int Attack()
        {
            return ATK;
        }
    }
}
​

「BattleCharacterのインスタンス.Attack()」という命令があった時に、どっちのメソッドを実行すればいいのかわからなくなってしまいますよね。

これまで学習してきた通り、コンピュータは曖昧な命令には対応できません。数字と文字列の区別をできるように、あるいはクラスを名前空間で識別できるように、などの様々な工夫をしてきたのもすべては命令の意味を厳密にするためです。

逆にいえば、例え同じ名前であっても厳密に区別できるのならよいのです。

その性質を利用した機能がオーバーロードです。

オーバーロードって?

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
​
namespace CSharp4_8
{
    internal class BattleCharacter
    {
        public string Name = "";
        public int Level = 0;
        public int HP = 0;
        public int ATK = 0;
​
        // ↓Attackメソッド① 引数×0
        public int Attack()
        {
            return ATK + Level * 2 / 5;
        }
        
        // ↓Attackメソッド② int型引数×1
        public int Attack(int plusDamage)
        {
            return ATK + Level * 2 / 5 + plusDamage;
        }
​
        // ↓Attackメソッド③ float型引数×1
        public int Attack(float plusDamage)
        {
            return ATK + Level * 2 / 5 + int.Parse(plusDamage.ToString());
        }
​
        // ↓Attackメソッド④ int型引数×2
        public int Attack(int plusDamage, int minusDamage)
        {
            return ATK + Level * 2 / 5 + plusDamage - minusDamage;
        }
    }
}

実装されているメソッドはすべて「Attack」ですが、引数の型や数が違いますよね。

C#ではメソッドというものを「メソッド名」「引数の型」「引数の数」の3つの情報で捉えています。つまりこの「メソッド名」「引数の型」「引数の数」のいずれかが違うのであれば、コンピュータ君はちゃんとそれを区別することができるんですね。

この引数の型や数が異なる同名メソッドを定義できる機能のことをオーバーロードといいます

便利そうな雰囲気は感じますが、具体的なメリットを感じにくいかもしれません。

「同じメソッド名で、引数の型や数が違うものを作りたいことってあるの?」

と思われる方も多いんじゃないかなと思います。

ですが、実はこれまでもずっとオーバーロードの恩恵を預かっていました。

それは下記のメソッドです。

System.Console.WriteLine(引数);

いつもお世話になっているやつですね!

このメソッド、どうして「string型(文字列)を引数にしてもOK、int型を引数にしてもOK、float型を引数にしてもOK」だと思いますか?

// ↓仮にもしWriteLineが下記のように実装されているなら、
public static void WriteLine(string value)
{
    ……処理……
}
​
// ↓int型やfloat型を引数にしたらエラーになるはず。
//  でも実際にはどんな型を引数にしても動く。
//  よくよく考えてみると不思議だね!
System.Console.WriteLine("hoge");
System.Console.WriteLine(1);
System.Console.WriteLine(0f);

実際のメソッドの定義を列挙してみます(処理の記載はなし)。

public static void WriteLine(bool value)
public static void WriteLine(decimal value)
public static void WriteLine(double value)
public static void WriteLine(int value)
public static void WriteLine(long value)
public static void WriteLine(float value)
……

引数の型毎に「WriteLine」というメソッドが用意されていて、その型に応じた実装が行われているんですね。

仮にもしこのオーバーロードという機能がなく、同じメソッド名が使えないとなるとどうなるでしょう?

string型の引数しか扱えないなら、使う度にstring型に変換しなければなりません。

// ↓このメソッドしか用意されていなかったら、
public static void WriteLine(string value)
{
    ……処理……
}
​
// ↓必ずstring型に変換しないと使えなくなってしまう!
System.Console.WriteLine("hoge");
System.Console.WriteLine(1.ToString());
System.Console.WriteLine(1f.ToString());

もしくは、それぞれの型毎に異なる名前のメソッドを用意するとなると、型に合わせていちいちメソッド名を変えなければいけません。

// ↓こんな風に、型毎に違う名前でメソッドを用意しなければならないとすると、
public static void WriteLine(string value)
public static void WriteLineInt(int value)
public static void WriteLineFloat(float value)
……
    
// ↓使う時にいちいちメソッド名を変えないといけない!
System.Console.WriteLine("hoge");
System.Console.WriteLineInt(1);
System.Console.WriteLineFloat(1f); 

正直使い辛いですよね。

このように、目的が同じ処理を行うが状況に応じて型や数が異なる引数を扱いたい場合に、オーバーロードは非常に強力な機能となります。

コンストラクタもオーバーロードできる

インスタンス生成時に必ず実行されるコンストラクタもオーバーロードが可能です。

これを活用すると、「初期設定で必ずレベルとHPとATKは設定しなければならないが、名前は同じタイミングでも後からでも設定できる」といった実装ができます。

public class BattleCharacter
{
    public string Name = "";
    public int Level = 0;
    public int HP = 0;
    public int ATK = 0;
    
    // ↓名前は引数に含まないコンストラクタ
    public BattleCharacter(int level, int hp, int atk)
    {
        Level = level;
        HP = hp;
        ATK = atk;
    }
    
    // ↓名前も引数に含むコンストラクタ
    public BattleCharacter(string name, int level, int hp, int atk)
    {
        Name = name;
        Level = level;
        HP = hp;
        ATK = atk;
    }
​
    public int Attack()
    {
        return ATK + Level * 2 / 5;
    }
}

これなら以前コンストラクタを学習した際に話していた、下記のような問題にも対処できます。

例えば名前だけはユーザが入力して決められるようにしたいとか、間に別の処理を行ってから設定したいとか、生成と設定が同時だと困る場面があるんですね。

// ↓主人公は後から名前を決めるので、名前なしのコンストラクタを使う
BattleCharacter hero = new BattleCharacter(10, 25, 10);
System.Console.Write("名前を入力してください。 > ");
hero.Name = System.Console.ReadLine();
​
// ↓ゴブリンは最初から名前が決まっているので、名前ありのコンストラクタを使う
BattleCharacter goblin = new BattleCharacter("ゴブリン", 5, 20, 7);

オーバーロードを上手に活用すると、さらにクラスを再利用しやすくすることができるんですね。

最終課題

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

今回は基礎編で作成した下記の「3VS1」のボスバトル仕様の戦闘シミュレーションを、オブジェクト指向による実装に置き換えます。

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

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

準備

下記のクラス追加、および「Program.cs」にコードをコピペしてください。

プロジェクト名が違う場合は名前空間のみ異なる名前になるので、そこだけ修正をお願いします。

BattleControllerクラス(追加)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
​
// ↓プロジェクト名に合わせて変更してね!
namespace CSharp4_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;
                }
                enemy.HP -= players[i].ATK;
            }
        }
​
        public void EnemyTurn(BattleCharacter[] players, BattleCharacter enemy)
        {
            for (int i = 0; i < players.Length; i++)
            {
                if (players[i].HP <= 0)
                {
                    continue;
                }
                players[i].HP -= enemy.ATK;
                break;
            }
        }
​
        public bool IsGameOver(BattleCharacter[] players)
        {
            for (int i = 0; i < players.Length; i++)
            {
                if (players[i].HP > 0)
                {
                    return false;
                }
            }
            return true;
        }
    }
}
Program.csには下記コードをコピペしてね!
// ↓プロジェクト名に合わせて変更してね!
using CSharp4_8;
​
BattleCharacter[] players = new BattleCharacter[] {
    new BattleCharacter(25, 10, "ちょろいぜ!"),
    new BattleCharacter(30, 12, "あまいぜ!"),
    new BattleCharacter(21, 6, "ちょろあまですね!")
};
​
for (int i = 0; i < players.Length; i++)
{
    Console.Write("Player" + (i + 1).ToString() + "の名前を入力してください。 > ");
    players[i].Name = Console.ReadLine();
}
​
BattleCharacter enemy = new BattleCharacter("ボス", 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();
}

課題

仕様
・新規のスクリプトファイルを作成し、下記のクラスを定義する
 クラス名   :BattleCharacter
 役割          :戦闘パラメータと、勝利時に台詞を言う処理をまとめる
 メンバー変数 :string Name
                int    HP
                int    ATK
                string VictoryDialogue
 コンストラクタ①:
        引数  :int    hp
                  int    atk
                  string victoryDialogue
        内容  :メンバー変数に引数の値を設定する
 コンストラクタ②:
        引数  :string name
                  int    hp
                  int    atk
                  string victoryDialogue
        内容  :メンバー変数に引数の値を設定する
 メソッド    :
        メソッド名:SayVictoryDialogue
        戻り値  :なし
        引数   :なし
        内容   :メンバー変数「VictoryDialogue」を
                  コンソール画面に出力する

答え合わせ

BattleCharacterクラス
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
​
namespace CSharp4_8
{
    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;
        }
​
        public void SayVictoryDialogue()
        {
            Console.WriteLine(VictoryDialogue);
        }
    }
}

実行してみましょう。

下記のように出力されていれば成功です。名前の部分は入力した名前によって変化するので、差異が出ていてもOK。

また、興味があればコピペしたコードの流れも見てみてください。自分が作成したクラスがどんな風に活用されているのか、確認してみるとよいでしょう。

コンソール画面

ティトレイHP:-7
マオHP:-2
アニーHP:5
enemyHP:-2
ちょろいぜ!
あまいぜ!
ちょろあまですね!

最後に

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

オブジェクト指向によるプログラミングはいかがだったでしょうか?

クラス、インスタンス、コンストラクタ、メンバー変数、そしてオーバーロード。新しい要素が目白押しでしたね。非常に自由度が増えた半面、自由過ぎてどうやって実装したらいいのか迷ってしまう人もいたのではないでしょうか?

慣れないうちは色々苦戦すると思いますが、ゲームを作っていくうちに自然と上手になります。大事なのはそのために手を動かせるだけの知識を持っておくこと。

そういう意味では、皆さんはもうゲーム開発の準備がほとんどできてます。

Unityを使ったゲーム開発まであと少し!

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

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

Posted by 夕目紅