【ゲーム開発のためのC#入門講座・応用拡張編】ポリモーフィズムで自由度を高めよう【#5】
一番意味不明そうな単語がきたぜ
オブジェクト指向三大要素の最後はポリモーフィズム(多態性)です。
うん。言葉の響きからして嫌な予感がした人もいるかもしれないけど、継承と同じぐらい難しい機能になります。
継承もポリモーフィズムも、未経験者・初心者の人が学んでいきなり使う技術ではないので、正直もっとずっと後でもいいぐらい。ただ、Unityがこの「継承」と「ポリモーフィズム」を活用したフレームワークになっているので、「Unityってどういう風に動いているのかな」を理解してもらうのに、知っておいて欲しい知識になります。
あくまで知識です。慣れないうちは本当に無理して使う必要のない技術なので、まずはどういうものなのかの理解だけでも進めていきましょう。
また、応用編におけるオブジェクト指向の難しい話は基本的にこれで最後です。以降は基礎編と同じぐらいの難易度に落ち着く予定。
ここが最後の壁と思って、一緒に乗り越えてもらえれば幸いです。
継承すると、あることが約束される
継承で親クラスと子クラスというものが作れることを学びました。親クラスの要素を子に引き継いで、新しい機能を追加したり、親の機能をオーバーライドしたりできるんでしたね。
この継承を行うと、あることが約束されます。
とても大事な約束です。
例えば下記の子クラスHero
とEnemy
も、ある約束事があります。
何だと思いますか?
// ↓何が約束されるのか、少しだけ考えてみよう!
public class BattleCharacter
{
public string Name;
public int Level;
public int HP;
public int ATK;
public virtual int Attack()
{
return ATK + Level * 2 / 5;
}
}
public class Hero : BattleCharacter
{
public int Skill()
{
return ATK * 2;
}
}
public class Enemy : BattleCharacter
{
public override int Attack()
{
return ATK + 1;
}
}
いきなり言われても難しいですね。
皆さんの貴重な時間を奪ってしまうのは本意ではないので、答えを言います。
いいですか、いきますよ?
それは
「BattleCharacter」クラスを継承しているということ
です。
…………。
……。
「いや、継承してるんだから当たり前やろ」と思われた方、全くその通りです。
ですが、より詳細かつ汎用的に記述するなら、
親クラスで定義されたデータやメソッドは必ず存在することが約束されている
ということです。
Hero
クラスとEnemy
クラスは、同じ親クラスを継承してはいますが、それぞれ違うクラスですよね。片方は新しくメソッドを追加しているし、片方は親のメソッドを上書きしています。
ですが、少なくとも親クラスで定義されたメンバー変数は必ず持っています。また、オーバーライドの有無はあれど、引数なしでint型の戻り値を返すメソッドAttack
を必ず行うことができます。
つまりこのふたつのクラスは、少なくとも親クラスBattleCharacter
で定義された部分だけでいえば、全く同じように扱うことができるんですね。あたかも、イチゴもバナナも同じ果物ではあるから果物として扱えるよね、といった形で。
これは概念的な話だけではないです。
オブジェクト指向では具体的な実装でも、同じものとして扱うことができるのです。
子クラスを親クラスとして扱おう
色々難しい理屈を話しましたが、できることを簡潔にいいます。
子クラスのインスタンスは親クラスの型として扱うことができます。
例えば下記のプログラムを見てみましょう。
// ↓「Hero」クラスのインスタンスを生成してるよ!
Hero hero = new Hero();
// ↓「Enemy」クラスのインスタンスを生成してるよ!
Enemy enemy = new Enemy();
Console.WriteLine(hero.Attack());
Console.WriteLine(enemy.Attack());
public class BattleCharacter
{
public string Name;
public int Level;
public int HP;
public int ATK;
public virtual int Attack()
{
return ATK + Level * 2 / 5;
}
}
public class Hero : BattleCharacter
{
public int Skill()
{
return ATK * 2;
}
}
public class Enemy : BattleCharacter
{
public override int Attack()
{
return ATK + 1;
}
}
通常はこのように、変数の型とインスタンスの型は一致していますよね。
// ↓変数の型 ↓作成するインスタンスの型
Hero hero = new Hero();
ですがそのクラスが継承を行っている場合、格納する変数の型を親クラスにすることができます。そしてメンバー変数やプロパティ、メソッドなどを親クラスとして操作することができます。
// ↓「Hero」クラスのインスタンスを、
// 「BattleCharacter」型の変数に格納してるよ!
BattleCharacter hero = new Hero();
「えー、そんなのありなの!?」って感じですが、先程も言った通り、Hero
クラスはBattleCharacter
クラスの要素をすべて持っています。
それはつまり、「Hero」クラスは「BattleCharacter」クラスとして振る舞えることが約束されているということなのです。だからこんな芸当ができてしまうんですね。
同様のことはEnemy
クラスにも言えます。
// ↓「Hero」クラスのインスタンスも、
// 「BattleCharacter」でやれることは全部やれるから、
// 「BattleCharacter」クラスと見なすことができるよ!
BattleCharacter enemy = new Enemy();
試しに実行してみましょう。エラーにはなりません。
// ↓どちらも「BattleCharacter」型の変数に入れているよ!
BattleCharacter hero = new Hero();
BattleCharacter enemy = new Enemy();
Console.WriteLine(hero.Attack());
Console.WriteLine(enemy.Attack());
public class BattleCharacter
{
public string Name;
public int Level;
public int HP;
public int ATK;
public virtual int Attack()
{
return ATK + Level * 2 / 5;
}
}
public class Hero : BattleCharacter
{
public int Skill()
{
return ATK * 2;
}
}
public class Enemy : BattleCharacter
{
public override int Attack()
{
return ATK + 1;
}
}
コンソール画面0
1
ちゃんと、先程までの実装と同じ結果になりましたね。
このように、継承関係にあるインスタンスは親クラスと見なすこともできるし、子クラスと見なすこともできます。
捉え方によって姿形を変える様はまるでカメレオンのようですよね。
そのため、ポリモーフィズム(多態性)と呼ばれているのです。
このポリモーフィズムによって同一の型として見なすことができるようになると、実はこんなことができるようになります。
BattleCharacter[] battleCharacters = {
new Hero(),
new Enemy()
};
そう、同じ型として扱えるからこそ、配列に格納することができるんですね。
これまでは異なる型としてしか扱えなかったので、例えばHero
のインスタンス3体とEnemy
のインスタンス3体による戦闘システムを作ろうとした場合、それぞれループ処理を行うしかありませんでした。
// ↓それぞれの型で、
Hero[] heroes = {
new Hero(),
new Hero(),
new Hero()
};
for (int i = 0; i < heroes.Length; i++)
{
Console.WriteLine(heroes[i].Attack());
}
// ↓それぞれ処理をまとめるしかなかった
BattleCharacter[] enemies = {
new Enemy(),
new Enemy(),
new Enemy()
};
for (int i = 0; i < enemies.Length; i++)
{
Console.WriteLine(enemies[i].Attack());
}
ですが、ポリモーフィズムを活用すれば、ひとつの配列にまとめてループ処理ができてしまうのです。
// ↓厳密には異なる型のものを、同じ型と見なして、
BattleCharacter[] battleCharacters = {
new Hero(),
new Hero(),
new Hero(),
new Enemy(),
new Enemy(),
new Enemy()
};
// ↓まとめて操作することができるようになった!
for (int i = 0; i < battleCharacters.Length; i++)
{
Console.WriteLine(battleCharacters[i].Attack());
}
何かをまとめて操作したい時には非常に便利そうですね!
ただ、この場合はあくまでBattleCharacter
クラスとして扱っているため、子クラス独自の実装、例えばHero
クラスに新しく追加されたSkill
メソッドを実行することはできません。
BattleCharacter hero = new Hero();
// ↓「BattleCharacter」クラスとしては持っていない機能のためエラー
hero.Skill();
その場合はいつも通りHero
クラスとして扱ってあげる必要がある点に注意です。
Unityでの活躍ポイント
Unityで扱うゲームオブジェクト(画像や3Dキャラクターなど)に対してスクリプトを設定する場合、必ずMonoBehaviour
というクラスを継承しなければならないという説明を以前しましたね。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// ↓クラス「MonoBehaviour」を継承している
public class NewBehaviourScript : MonoBehaviour
{
void Start()
{
}
void Update()
{
}
}
Unityでは、すべてのオブジェクトがこのMonoBehaviour
を継承することで、キャラクターだろうとマップオブジェクトだろうと音楽プレイヤーだろうと、すべてをMonoBehaviour
という親クラスとして見なし、まとめて操作を行っています。
下記のようなイメージですね。
// あくまでイメージですが、
MonoBehaviour[] monoBehavioures = GetGameObjects();
// まとめて操作していることが伝われば嬉しいです
for (int i = 0; monoBehavioures.Length; i++)
{
monoBehavioures[i].Start();
}
このMonoBehaviour
を継承したゲームオブジェクトをまとめて操作する際の流れが、基礎学習編でご紹介したUnityのフローチャートになります。
懐かしいですね!
全体を把握するのは難しいですが、今の皆さんならポイントは理解できると思います。
フローチャートの最初を見てみてください。
UnityではAwake
というメソッドから始まり、OnEnable
というメソッドが実行され、Reset
というメソッドの後に、Start
というメソッドが呼ばれている……といった感じです。
これをイメージのコーディングに起こすと、下記のようになります。
// こちらもあくまでイメージですが、
while (true) // ゲームが終了するまでループ
{
// ↓最初にAwake!
for (int i = 0; monoBehavioures.Length; i++)
{
monoBehavioures[i].Awake();
}
// ↓次にOnEnable!
for (int i = 0; monoBehavioures.Length; i++)
{
monoBehavioures[i].OnEnable();
}
// ↓続いてReset!
for (int i = 0; monoBehavioures.Length; i++)
{
monoBehavioures[i].Reset();
}
// ↓そしてStart!
for (int i = 0; monoBehavioures.Length; i++)
{
monoBehavioures[i].Start();
}
...まだまだフローチャート通り処理は続くよ!……
}
このように、Unityでは定められた順番に全ゲームオブジェクトの特定のメソッドがまとめて実行されているんですね。
そして上記の通りStart
というメソッド実行まで到達すると、個々のゲームオブジェクトのStart
メソッドが呼び出され皆さんが実装した内容が実行される、という仕掛けになっているのです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NewBehaviourScript : MonoBehaviour
{
// ↓個々のゲームオブジェクトのこのメソッドが呼ばれる
void Start()
{
// ↓だからUnityではここに処理を実装しておくだけで、
// ゲーム起動時に自動で実行されるようになってるんだ!
// ここまで
}
void Update()
{
// ↓ちなみにこっちは毎フレーム単位で実行して欲しい処理を
// 実装しておくためのメソッドだよ!
// ここまで
}
}
このように、Unityでは個々のゲームオブジェクトの大枠の扱い方を「継承」と「ポリモーフィズム」を使ってまとめています。そして特定のタイミングで実行されるメソッドを提供しつつ、どんな実装を行うかはクリエイターの皆さんにお任せすることで、クリエイターが必要なポイントでだけ作業すれば済むように工夫しているんですね。
これがUnityというゲーム開発のフレームワークです。
本来は異なるクラスを親クラスにて同一視し、まとめて操作することで個々のクラスをさらに再利用しやすくする――これもまたオブジェクト指向の目的である「再利用しやすいプログラムを作る」を実現するための機能なんだということ、少しでも伝わっていれば幸いです。
なお、Start
やUpdate
は扱いだけでいうとぱっと見オーバーライドメソッドに見えますが、厳密に言うと違います(override
のキーワードもついてないですよね)。
これはもっと難しい話になるのと、「オーバーライドメソッド」として捉えても理解としては特に困らないので、気になる方だけ調べてみてください。
実践演習
それでは、実際にポリモーフィズムを体験してみましょう。
仕様下記のクラス
Priest
のインスタンスを、親クラスHealer
型の変数に格納し、メソッドHeal
の実行結果をコンソール画面に出力してください。
また、練習として、クラスPriest
で1スクリプトファイル、クラスHealer
で1スクリプトファイル、インスタンス生成やメソッド実行はProgram.cs
に実装してみてください。
テンプレートusing System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; // ↓ここはプロジェクト名なので任意 namespace Csharp5_5 { internal class Healer { protected int HealPower; public int MP { get; protected set; } public Healer() { Random random = new Random(); MP = random.Next(20) + 1; HealPower = random.Next(10) + 1; } public virtual int Heal() { MP -= 4; return HealPower * 2; } } }
// ↓ここはプロジェクト名なので任意 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Csharp5_5 { internal class Priest: Healer { public override int Heal() { MP -= 5; return HealPower * 3; } } }
// Program.cs // ↓ここはプロジェクト名なので任意 using Csharp5_5; // ↓ここにヒールを実行する処理を追加しよう! // ここまで
答え合わせ
// Program.cs
// ↓ここはプロジェクト名なので任意
using Csharp5_5;
// ↓ここにヒールを実行する処理を追加しよう!
Healer healer = new Priest();
Console.WriteLine(healer.Heal());
// ここまで
まとめ
- 継承すると、親クラスで定義されたデータやメソッドは必ず存在することが約束される
- そのため、子クラスのインスタンスは親クラスと同一の振る舞いができるものとして扱うことができる
- 捉え方によって型を変化させられることからポリモーフィズム(多態性)と呼ぶ
- 異なる子クラスを親クラスとしてまとめて操作することで、より再利用しやすいプログラムを作ることができる
- Unityのフレームワークはこの継承とポリモーフィズムを活用して作られている
オブジェクト指向の三大要素、いかがだったでしょうか?
カプセル化はともかく、継承とポリモーフィズムはすごく難しかったと思います。自分も最初見た時は「なるほど、わからん」となりました。
プログラムに慣れ親しんでいくと少しずつ扱い方がわかるようになってきますので、今はぽやんとしか理解できていなかったとしても、全く気に病む必要はありません。
楽しくゲーム開発をしているうちに気付いたら「あ、なんか、わかってきたかも」、そんな風になれたらいいなぐらいの気持ちでいきましょう。
それでは、次回は長い付き合いとなったおまじない「static」の謎を解き明かします。お楽しみに!
今回もお疲れ様でした!
また次の記事でお会いしましょう!