スポンサーリンク
設計・アーキテクチャ

【Unity C#】オブジェクト指向設計の基本|継承・ポリモーフィズム・インターフェースの違いと使い方を徹底解説

設計・アーキテクチャ

Unityでゲームを作っていると、こんな悩みにぶつかったことはありませんか?

「敵キャラを1体追加したいだけなのに、なぜか他の処理まで直さないと動かない」「コピペでスクリプトを増やしていたら、どこを直せばいいのか分からなくなった」。実はこの原因、多くの場合は設計の作り方にあります。

この記事では、Unity C#でよく耳にする「継承」「ポリモーフィズム」「インターフェース」について、それぞれが何を解決するための仕組みなのか、どう使い分ければいいのかを、実際のゲーム制作の場面に沿って解説していきます。

なぜUnity開発にOOP設計が必要なのか

結論からお伝えすると、オブジェクト指向設計(OOP)を意識してコードを書くと、機能を追加するたびに全体を壊してしまうリスクを減らせます。逆に言うと、これを意識せずに書き進めると、プロジェクトが育つほど修正がつらくなっていくんです。

たとえば、プレイヤーの動き・攻撃・体力管理・アイテム所持をぜんぶ1つのクラスに書いてしまったとします。最初のうちは「全部見えるから楽」に感じるかもしれません。でも敵キャラを追加しようとしたとき、プレイヤー用に書いたはずの処理まで巻き込んで修正することになり、テストする範囲もどんどん広がっていきます。

こうした「1つを直すと他も壊れる」状態を防ぐために使われるのが、クラスの役割を整理する考え方であるオブジェクト指向設計です。中でも今回扱うのは、次の3つの仕組みです。

  • 継承:似たクラス同士で共通の機能をまとめる仕組み
  • ポリモーフィズム:同じ命令でクラスごとに違う動きをさせる仕組み
  • インターフェース:「これができる」という約束だけを定義する仕組み

この3つは似ているようで役割がまったく違います。次の項目から、それぞれどんな場面で使うのが向いているのか、順番に見ていきましょう。

先にゲーム制作を題材にしたオブジェクト指向の入門書に目を通しておくと、この記事の内容もイメージしやすくなります。

ゲーム作りで楽しく学ぶ オブジェクト指向のきほん(森巧尚)
✅ Amazonでチェックする✅ 楽天でチェックする


継承とは?「is-a」で使う場面を判断する

継承とは、親クラスが持っている機能を、子クラスがそのまま引き継げる仕組みのことです。たとえば「Character」という親クラスに体力やダメージを受ける処理を書いておけば、そこから生まれた「Player」や「Enemy」は、同じ処理をわざわざ書き直さずに使えます。

ただ、継承を使うかどうかで迷ったときに便利な判断基準があります。それが「AはBである」と自然に言えるかどうかです。

  • ✅「FighterはCharacterである」→ 継承が向いている
  • ✅「EnemyはCharacterである」→ 継承が向いている
  • ❌「PlayerはWeaponを持っている」→ これは継承ではなく「所有」の関係

最後の例のように、「持っている」「使っている」という関係を継承で表現しようとすると、あとで無理が出てきます。「〜である」と言い切れる関係にだけ継承を使う、というのがまず押さえておきたいポイントです。

継承の欠点|階層が深くなると起きる問題

継承はとても便利な反面、使いすぎると別の問題が出てきます。それが「階層が深くなることで起きる硬直化」です。

たとえば、Characterを継承してHumanoidを作り、そこからさらにFighterやMageを継承する…というように階層を重ねていくと、一見きれいに整理できているように見えます。ですが実際には、親クラスの処理を1つ直しただけで、その下にぶら下がる子クラス全部の動きに影響が出てしまうことがあります。

特に、あるクラスにだけ例外的な動きをさせたいとき、これが顕著に表れます。「Mageだけは特定の処理をスキップしたい」となった場合、親クラス側に条件分岐を増やすことになり、結果としてどのクラスがどんな動きをするのか、コードを読まないと分からない状態になってしまうんです。

注意
継承の階層を深くすればするほど、後から仕様変更をしたときの影響範囲が読みにくくなります。「とりあえず継承しておく」を繰り返すと、後で身動きが取りにくい設計になりやすいので気をつけましょう。

この「継承が硬直化しやすい」という問題をどう解決するかについては、記事の後半で改めて取り上げます。

まずは、継承と対になって語られることが多い「ポリモーフィズム」から見ていきましょう。


ポリモーフィズムとは?同じ命令で違う動きをさせる仕組み

ポリモーフィズムとは、同じメソッド呼び出しなのに、クラスごとに違う処理が実行される仕組みのことです。日本語では「多態性」と訳されますが、言葉だけだとイメージしにくいので、具体的な流れで見てみましょう。

RPGで「攻撃ボタンを押したら、キャラごとに違うアクションが出る」場面を例にすると、次のような手順で実現できます。

  1. 基底となる抽象クラス(例:Character)を作り、中身のない攻撃メソッド(例:Attack())を定義する
  2. そのクラスを継承した具体的なクラス(例:FighterMonkWitch)を作り、それぞれでAttack()を独自の内容にオーバーライドする
  3. 管理側のクラス(例:GameManager)でCharacter型のリストを用意し、各キャラのインスタンスをまとめて入れる
  4. そのリストをループしながらAttack()を呼び出すと、Fighterなら剣攻撃、Witchなら魔法攻撃というように、それぞれのクラスに書いた処理が自動的に実行される

ここで注目したいのは、呼び出す側は「Attack()を呼んでいる」ということしか意識していない点です。誰がどんな攻撃をするかの判断を、呼び出す側ではなく各クラス自身に任せられるため、キャラクターの種類が増えても、管理側のコードを書き換える必要がありません。

オーバーライドとオーバーロードの違い

ポリモーフィズムを調べていると、よく混同されがちなのが「オーバーライド」と「オーバーロード」です。名前が似ているだけに、初めのうちは区別がつきにくいところだと思います。

オーバーライドは、親クラスで定義したメソッドを、子クラス側で上書きして中身を変えることです。先ほどのAttack()の例がまさにこれにあたります。

一方オーバーロードは、同じクラスの中に、同じ名前だけど引数の数や型が違うメソッドを複数用意することです。たとえばAttack()Attack(int power)を同じクラス内に共存させるような使い方で、こちらは継承やクラスをまたいだ多態性とは関係のない、別の仕組みになります。

この2つは名前こそ紛らわしいものの、「クラスをまたいで動きを変える」のがオーバーライド、「1つのクラスの中で引数違いのバリエーションを作る」のがオーバーロード、と役割で覚えておくと迷いにくくなります。


インターフェースとは?「〜できる」を定義する仕組み

インターフェースとは、「このメソッドを必ず実装してください」という約束だけを定義する仕組みです。継承のように処理の中身を引き継ぐわけではなく、あくまで「こういう機能を持っていること」を保証するためのものだとイメージすると分かりやすいと思います。

継承が「AはBである」で判断するのに対して、インターフェースを使うかどうかは「このクラスは〜できる」という機能面で捉えられるかどうかが判断基準になります。

  • ✅「プレイヤーはダメージを受けられる」→ IDamageableのようなインターフェースが向いている
  • ✅「宝箱もダメージを受けられる」→ 敵と同じ親クラスを持たなくても、同じインターフェースを実装すればOK
  • ❌「敵は移動する処理の中身も丸ごと引き継ぎたい」→ これは中身を共有したいので継承向き

ここで面白いのが、プレイヤーと宝箱のように、継承関係で言えば全く別のクラス同士でも、同じインターフェースさえ実装していれば「ダメージを受けられるもの」として同じ扱いができる点です。

なぜわざわざクラスに直接書かず、インターフェースとして切り出すのかというと、理由は大きく2つあります。1つは、機能ごとに実装を切り離せるので、後から似た機能を持つクラスを追加するときに、既存のコードを触らずに済むこと。もう1つは、呼び出す側が「相手が具体的に何のクラスか」を知らなくても処理を書けるようになり、クラス同士のつながりが緩やかになることです。

この「つながりが緩やかになる」という感覚は、文章だけだと少しつかみにくいかもしれません。具体的にどう書けば疎結合な設計になるのかは、後ほど実践例として詳しく紹介します。

まずは次に、似たような役割に見える抽象クラスとの違いを整理していきましょう。


インターフェースと抽象クラス、どちらを使うべきか

継承とインターフェース、それぞれの役割が見えてきたところで、次に迷いやすいのが「インターフェースと抽象クラス、結局どっちを使えばいいの?」という点だと思います。どちらも「実装を子クラスに強制する」という点では似ているため、混同しやすいところです。

この2つの違いを整理すると、次のようになります。

インターフェース抽象クラス
継承できる数複数実装できる1つの親クラスしか継承できない
変数(フィールド)持てない持てる
主な用途「〜できる」という役割の定義共通処理・状態を持たせた基盤づくり

この表から見えてくる判断基準はシンプルです。共通の処理や状態(変数)を複数のクラスで持たせたいなら抽象クラス、役割だけを定義して具体的な中身は各クラスに任せたいならインターフェースを選ぶ、という考え方になります。

たとえば、敵キャラたちに共通する移動速度や体力といった「状態」を持たせつつ、攻撃処理の中身だけは子クラスごとに変えたい場合は、抽象クラスが向いています。逆に、プレイヤーも宝箱も置物も、見た目や継承関係はバラバラだけど「ダメージを受けられる」という機能だけ共通で持たせたい場合は、インターフェースの出番です。

また、C#の言語仕様上インターフェースはフィールドを持てないため、「状態を保持したいのにインターフェースを使おうとして詰まる」というのは初心者がよくつまずくポイントでもあります。迷ったときは、この表の「変数を持たせたいかどうか」を最初のチェックポイントにしてみてください。

こうしたクラス設計の使い分けは、コードの依存関係を整理するSOLID原則の考え方とも深く関わってきます。より体系的に設計を見直したい場合は、あわせて読んでみてください。


継承よりコンポジションを選ぶ理由

先ほど、継承には「階層が深くなるほど硬直化する」という欠点があるとお伝えしました。この問題を解決する考え方として広く使われているのが、コンポジションです。

コンポジションとは、クラスの中に別のクラスを「部品」として持たせ、それらを組み合わせて動作を作る設計のことです。継承が「AはBである」という関係だったのに対して、コンポジションは「AはBを持っている」という関係で表現します。

継承(is-a)コンポジション(has-a)
関係性「〜である」「〜を持っている」
クラス間の結びつき強い(親に依存する)弱い(部品を差し替えやすい)
向いている場面共通の性質をまとめたいとき機能を柔軟に組み替えたいとき

たとえば、キャラクターに「移動する」「攻撃する」という機能を持たせたい場合、継承だとMovableCharacterやAttackableCharacterのようにクラスを増やしていくことになりがちです。組み合わせのパターンが増えるほど、クラスの数が爆発的に膨らんでしまいます。

一方コンポジションなら、「移動を担当するクラス」「攻撃を担当するクラス」をそれぞれ部品として用意し、必要なキャラクターにその部品を持たせるだけで済みます。移動方法だけを差し替えたいときも、部品を入れ替えるだけで対応できるため、継承のように親クラス全体に影響が及ぶこともありません。

ここで大切なのは、継承が悪いわけではなく、「〜である」と言い切れる関係にだけ使う道具だということです。何でも継承でまとめようとせず、「これは性質の共有なのか、それとも機能の組み合わせなのか」を都度見極める意識を持つと、後から破綻しにくい設計に近づきます。

クラスに機能を詰め込みすぎない考え方は、責務分離という視点からも整理できます。あわせて確認しておくと、設計の判断基準がより明確になると思います。

継承とコンポジションの考え方は、ここで紹介した内容だけでも実務では十分役立ちますが、体系立てて理解を深めたい場合は専門書を読んでみるのもおすすめです。

オブジェクト指向でなぜつくるのか 第3版(平澤章)
✅ Amazonでチェックする✅ 楽天でチェックする


実践:ダメージ処理をインターフェースで疎結合に作る

ここまでの内容を踏まえて、実際に「ダメージ処理」をインターフェースで作ってみましょう。ダメージ処理をキャラクターのクラスに直接書いてしまうと、動かない箱や壊せるオブジェクトなど、キャラクター以外にも同じ処理を使いたくなったときに困ってしまいます。そこで役立つのがインターフェースです。

手順は次のとおりです。

  1. IDamageableというインターフェースを作り、Damage()Death()など、中身のないメソッドを定義する
  2. ダメージを受けさせたいオブジェクトのクラスにIDamageableを実装し、そのクラスの中で具体的なダメージ処理・死亡処理を書く
  3. 攻撃する側は、相手のオブジェクトからGetComponent<IDamageable>()でインターフェースを取得し、取得できた場合だけDamage()を呼び出す

このやり方のポイントは、攻撃する側が「相手が敵なのかプレイヤーなのか、それとも壊せる箱なのか」を一切気にしなくていい点です。IDamageableを持っているかどうかだけを見て処理を呼び出すため、新しくダメージを受けるオブジェクトを追加したいときも、攻撃側のコードを一切書き換える必要がありません。

インスペクター上で直接扱いたい場合(Odin Inspector)

ここで少し実務的な壁にぶつかることがあります。Unity標準の機能では、インターフェース型のフィールドをインスペクター上に表示して、オブジェクトをドラッグ&ドロップで割り当てる、ということができません。

この制約を解決したい場合、Odin Inspectorというアセットを使う方法があります。対象のスクリプトで通常のMonoBehaviourの代わりにSerializedMonoBehaviourを継承させると、インスペクター上にインターフェース型のフィールドが表示されるようになり、該当インターフェースを実装したコンポーネントを直接割り当てられるようになります。

ワンポイント
インスペクターでの割り当てが不要で、スクリプト内だけで完結する場合は、ここまで紹介したGetComponentでの取得方法だけで十分です。Odin Inspectorは「デザイナーやプランナーがインスペクター上で設定したい」といった、チーム開発の場面で特に力を発揮します。

Odin Inspector
✅アセットストアでチェックする

さらに疎結合を進めたい場合(DIという選択肢)

今回紹介したGetComponentを使う方法は、シンプルで理解しやすい反面、「取得するタイミング」や「取得できなかったときの処理」を自分で書く必要があります。プロジェクトが大きくなり、クラス同士の依存関係をもっと整理したくなってきたら、DI(依存性注入)という考え方を取り入れる選択肢も出てきます。

DIについてはこの記事の範囲を超える内容になるため、詳しい実装方法は別記事にまとめています。今回の設計に物足りなさを感じたら、あわせて読んでみてください。


よくある誤解・注意点まとめ

ここまで継承・ポリモーフィズム・インターフェースの使い方を見てきましたが、実際にコードを書き始めると混同しやすいポイントがいくつかあります。最後に、本文の中では扱いきれなかった誤解を整理しておきます。

まず多いのが、「カプセル化はprivateにすることだ」という捉え方です。たしかにアクセス修飾子で外部からのアクセスを制限するのはカプセル化の一部ですが、本質は「外部に必要な情報だけを公開し、内部の実装は隠す」という設計の考え方そのものにあります。privateにするかどうかは、その手段の1つに過ぎません。

次によくあるのが、「抽象クラスを使えるなら、インターフェースはいらないのでは」という誤解です。抽象クラスは1つしか継承できないため、複数の異なる種類のクラスに横断的な機能を持たせたい場合には対応できません。プレイヤーも宝箱も敵も、継承関係はバラバラだけど同じ機能を持たせたい、という場面ではインターフェースでなければ実現できないケースが出てきます。

また、設計に慣れてくると「とりあえずシングルトンにしておけば楽」と考えてしまうこともあります。ですが、シングルトンは便利な反面、使い方を誤るとクラス同士の依存関係が見えにくくなり、テストや修正がしづらくなることがあります。

注意
「1つのクラスに便利な機能を集めておけば楽そう」という発想は、後から見返すとどこで何が起きているか追いにくいコードにつながりがちです。今回紹介した継承・ポリモーフィズム・インターフェースの使い分けを思い出しながら、機能ごとに役割を分けることを意識してみてください。

シングルトンを使う場面そのものについては、正しい実装方法と注意点を以下の記事でまとめています。

今回紹介した考え方以外にも、Unity開発ではさまざまな設計パターンが使われています。他の選択肢も知っておきたい場合は、こちらの比較記事もあわせて参考にしてみてください。


よくある質問

Q
継承とインターフェースは同時に使ってもいいですか?
A
はい、問題ありません。C#では、1つの親クラスを継承しながら、複数のインターフェースを実装することができます。たとえば「Characterクラスを継承しつつ、IDamageableとIInteractableという2つのインターフェースも実装する」といった書き方が可能です。継承で共通の性質をまとめつつ、インターフェースで個別の機能を追加する、という組み合わせ方はUnity開発でもよく使われています。
Q
ポリモーフィズムを多用するとパフォーマンスに影響しますか?
A
通常の使い方であれば、パフォーマンスを気にする必要はほとんどありません。キャラクターの攻撃処理や状態管理など、一般的なゲームロジックの範囲で使う分には問題にならないケースがほとんどです。ただし、大量のオブジェクトを毎フレーム処理するような場面(たとえば数千体規模の弾やパーティクルなど)では、負荷が気になることもあります。その場合は、まず実際に処理が重くなっているかをプロファイラーで確認したうえで、必要に応じて設計を見直すという順番で考えるのがおすすめです。
Q
UnityのMonoBehaviourを継承したクラスにもabstract classは使えますか?
A
はい、使えます。MonoBehaviourを継承したabstractクラスを作り、そこからさらに具体的な敵キャラやプレイヤーのクラスを継承させる、という構成は一般的によく使われています。ただし、abstractクラス自体はそのままシーン上のオブジェクトにアタッチしてインスタンス化することはできない点に注意してください。あくまで「共通の土台」として使い、実際にアタッチするのは、それを継承した具体的なクラス側になります。

※当サイトはアフィリエイト広告を利用しています。リンクを経由して商品を購入された場合、当サイトに報酬が発生することがあります。

※本記事に記載しているAmazon商品情報(価格、在庫状況、割引、配送条件など)は、執筆時点のAmazon.co.jp上の情報に基づいています。
最新の価格・在庫・配送条件などの詳細は、Amazonの商品ページをご確認ください。

スポンサーリンク