ゲームを開発するためにオブジェクトの継承を可能な限り使用しないでください。


36

Unityでゲームを開発するときは、OOP機能が好きです。通常、基本クラス(ほとんどが抽象化された)を作成し、オブジェクトの継承を使用して、同じ機能を他のさまざまなオブジェクトと共有します。

ただし、最近、継承を使用することは避けなければならず、代わりにインターフェイスを使用する必要があると誰かから聞きました。だから私は理由を尋ねて、「オブジェクトの継承はもちろん重要ですが、拡張オブジェクトがたくさんあると、クラス間の関係は深く結合されます。

私は、のようなものを抽象基底クラスを使用していますWeaponBaseし、同様に、特定の武器クラスを作成しShotgunAssaultRifleMachinegunそのような何か。非常に多くの利点があり、私が本当に楽しんでいるパターンの1つはポリモーフィズムです。サブクラスによって作成されたオブジェクトを基本クラスであるかのように扱うことができるため、ロジックを大幅に削減し、再利用可能にすることができます。サブクラスのフィールドまたはメソッドにアクセスする必要がある場合、サブクラスにキャストし直します。

私は、一般的に異なるクラスで使用したのと同じフィールドを定義する必要が好きではありませんAudioSource/ Rigidbody/ Animatorそして私は次のように定義されたメンバフィールドをたくさんfireRatedamagerangespreadというように。また、場合によっては、一部のメソッドが上書きされる可能性があります。そのため、その場合は仮想メソッドを使用しているため、基本的には親クラスのメソッドを使用してそのメソッドを呼び出すことができますが、子でロジックが異なる場合は、手動でオーバーライドします。

それで、これらすべてを悪い習慣として捨てるべきですか?代わりにインターフェイスを使用する必要がありますか?


30
「非常に多くの利点があり、私が本当に楽しんでいるパターンの1つはポリモーフィズムです。」多態性はパターンではありません。文字通り、OOPの要点です。共通フィールドなどの他のことは、コードの重複や継承なしで簡単に実現できます。
キュービック

3
インターフェイスが継承よりも優れていると提案した人は、あなたに議論を与えましたか?私は興味がある。
Hawker65

1
@Cubicポリモーフィズムはパターンだとは言いませんでした。「...私が本当に楽しんでいるパターンの1つはポリモーフィズムです」と言いました。それは、私が本当に楽しんでいるパターンは「ポリモーフィズム」を使用していることを意味し、ポリモーフィズムはパターンではありません。
モダニエーター

4
開発のあらゆる形態で、継承よりもインターフェースを使用することを宣伝しています。ただし、多くのオーバーヘッド、認知的不協和を追加する傾向があり、クラスの爆発につながり、疑わしい価値を頻繁に追加し、通常、プロジェクトの全員がインターフェイスに焦点を当てない限り、システムでうまく相乗効果を発揮しません。そのため、まだ継承を放棄しないでください。ただし、ゲームにはいくつかの独自の特性があり、UnityのアーキテクチャではCES設計パラダイムを使用することを想定しているため、アプローチを微調整する必要があります。ゲームタイプの問題について説明しているEric LippertのWizards and Warriorsシリーズをお読みください。
ダンク

2
インターフェースは、基本クラスにデータフィールドが含まれない継承の特定のケースであると言えます。私は、各オブジェクトを分類する必要のある巨大なツリーを構築するのではなく、多重継承を利用できるインターフェースに基づいた多くの小さなツリーを構築することを提案すると思います。ポリモーフィズムはインターフェイスで失われません。
エマヌエーレパオリーニ

回答:


65

エンティティおよび在庫/アイテムシステムの継承よりも構成を優先します。このアドバイスはゲームロジック構造に適用される傾向があります。複雑なものを(実行時に)組み立てる方法が多くの異なる組み合わせにつながる場合があります。それが作曲を好むときです。

UIコンストラクトからサービスに至るまで、アプリケーションレベルのロジックの構成よりも継承を優先します。例えば、

Widget->Button->SpinnerButton または

ConnectionManager->TCPConnectionManager->UDPConnectionManager

...ここには、多数の潜在的な派生ではなく、明確に定義された派生の階層があるため、継承を使用する方が簡単です。

結論:可能な場合は継承を使用しますが、必要な場合は構成を使用します。PSエンティティシステムでの合成を好むもう1つの理由は、通常多くのエンティティが存在し、継承によってすべてのオブジェクトのメンバーにアクセスするためのパフォーマンスコストが発生する可能性があることです。vtablesを参照してください。


10
継承は、すべてのメソッドが仮想であることを意味するものではありません。したがって、パフォーマンスコストに自動的につながることはありません。VTable は、データメンバへのアクセスではなく、仮想呼び出しのディスパッチでのみ役割を果たします。
ルスラン

1
@Ruslanあなたのコメントに対応するために、単語「may」と「can」を追加しました。それ以外の場合、答えを簡潔にするために、あまり詳細に説明しませんでした。ありがとう。
エンジニア

7
P.S. The other reason we may favour composition in entity systems is ... performance:これは本当ですか?@LinaithショーがリンクしているWIkipediaページを見ると、インターフェースのオブジェクトを作成する必要があることがわかります。したがって、別のレベルのインダイレクションを導入すると、(まだまたはそれ以上の)仮想関数呼び出しとより多くのキャッシュミスが発生します。
炎火

1
@Flamefireはい、本当です。キャッシュの効率が最適であるようにデータを整理する方法がありますが、これらの余分なインダイレクションよりもはるかに最適です。これらはない継承としているより多くの/構造体オブアレイセンスの-構造体アレイ(キャッシュ線形配置でインターリーブ/非インターリーブデータ)、別々のアレイに分割し、時にはセットと一致するようにデータを複製するにもかかわらず、組成物パイプラインのさまざまな部分での操作。しかし、もう一度、ここに入ることは非常に大きな転換であり、簡潔にするために避けるのが最善です。
エンジニア

2
コメント保つために覚えておいてください民事明確化や批評のための要求をし、討論内容アイデアやないの性格や経験のにそれらを記載します。
ジョシュ

18

あなたはすでにいくつかの良い答えを得ましたが、あなたの質問の部屋にある巨大な象はこれです:

継承を使用することは避けなければならず、代わりにインターフェイスを使用する必要があると誰かから聞いた

経験則として、誰かがあなたに経験則を与えた場合、それを無視します。これは、「誰かが何かを言っている」だけでなく、インターネット上のものを読む場合にも当てはまります。理由がわからない(そして実際にその背後に立つことができる)場合を除き、このようなアドバイスは価値がなく、しばしば非常に有害です。

私の経験では、OOPで最も重要で有用な概念は「低結合」と「高凝集」です(クラス/オブジェクトは互いについての情報をできるだけ少なくし、各ユニットの責任はできるだけ少なくします)。

低カップリング

これは、コード内の「ものの束」が周囲にできるだけ依存しないことを意味します。これはクラス(クラス設計)だけでなく、オブジェクト(実際の実装)、一般的な「ファイル」(つまり、#include単一.cppファイルあたりのs 数、import単一.javaファイルあたりの数など)にも当てはまります。

2つのエンティティが結合されているという兆候は、他のエンティティが何らかの方法で変更されたときに、一方が壊れる(または変更する必要がある)ということです。

継承は明らかに結合を増加させます。基本クラスを変更すると、すべてのサブクラスが変更されます。

インターフェイスは結合を減らします。明確なメソッドベースのコントラクトを定義することにより、コントラクトを変更しない限り、インターフェイスの両側について自由に変更できます。(「インターフェース」は一般的な概念であり、Java interfaceまたはC ++抽象クラスは単なる実装の詳細であることに注意してください)。

高い凝集力

これは、各クラス、オブジェクト、ファイルなどをできるだけ気にせず、責任を負わせないことを意味します。つまり、多くのことを行う大規模なクラスは避けてください。あなたの例では、武器が完全に独立した側面(弾薬、発射行動、グラフィック表現、インベントリ表現など)を持っている場合、それらの1つを正確に表すさまざまなクラスを持つことができます。メインの武器クラスは、これらの詳細の「ホルダー」に変換されます。その場合、武器オブジェクトはそれらの詳細へのいくつかのポインタにすぎません。

この例では、「発火行動」を表すクラスが、メインの武器クラスについて人間に可能な限りほとんど知らないようにします。最適には、何もありません。これは、たとえば、指のスナップだけで、世界のあらゆるオブジェクト(タレット、火山、NPCなど)に「発射動作」を与えることができることを意味します。ある時点で、インベントリで武器がどのように表現されるかを変更したい場合は、単純に変更できます。インベントリクラスのみがそれを知っています。

エンティティが凝集していないという兆候は、エンティティがますます大きくなり、同時に複数の方向に分岐する場合です。

継承は、凝集性を低下させます-結局のところ、武器クラスは、武器のあらゆる種類の無関係な側面を処理する大きな塊です。

インターフェースは、インターフェースの両側の責任を明確に分離することにより、間接的に結束力を高めます。

今何をする

厳格なルールはまだありません。これはすべて単なるガイドラインです。一般的に、ユーザーTKKが答えで述べたように、相続は学校や本でたくさん教えられています。それはOOPについての素晴らしいものです。インターフェイスはおそらく教えるのがより退屈であり、(些細な例を過ぎた場合)少し難しくなり、継承ほど明確ではない依存性注入の分野を開きます。

結局のところ、継承ベースのスキームは、明確なOOPデザインをまったく持たないよりも優れています。だから、それに固執すること自由に感じなさい。必要に応じて、低結合、高凝集について少し反論/グーグルし、その種の考え方を武器に追加したいかどうかを確認できます。必要に応じて、いつでもリファクタリングして試すことができます。または、次に大きなコードの新しいモジュールでインターフェイスベースのアプローチを試してください。


詳細な説明をありがとう。何が欠けているのかを理解することは本当に役立ちます。
モダニエーター

13

継承を避ける必要があるという考えは、単に間違っています。

Composition over Inheritanceと呼ばれるコーディング原則があります。合成で同じことを達成できると言われていますが、コードの一部を再利用できるので望ましいです。継承よりも合成を優先する理由をご覧ください

私はあなたの武器クラスが好きだと言わなければなりません、そしてそれは同じ方法をするでしょう。しかし、私は今までにゲームを作成していません...

James Trotterが指摘したように、合成にはいくつかの利点があります。特に、実行時の柔軟性が武器の動作を変更するという点で有利です。これは継承によって可能になりますが、より困難です。


14
ただし、武器のクラスを持つのではなく、単一の「武器」クラスと、発砲の動作、付属品、その機能、「弾薬庫」の機能を処理するためのコンポーネントを持つことができ、多くの構成を構築できますいくつかは「ショットガン」または「アサルトライフル」ですが、たとえば、アタッチメントを切り替えたり、マガジンの容量を変更したりするために、実行時に変更することもできます。元々考えもしなかった。はい、継承で同様のことを実現できますが、それほど簡単ではありません。
Trotski94

3
@JamesTrotterこの時点で、私はボーダーランドとその銃を思い浮かべ、これが相続だけでどんな怪物になるのだろうか...-
Baldrickk

1
@JamesTrotter私はそれが答えであり、コメントではないことを願っています。
ファラプ

6

問題は、継承がカップリングにつながることです。オブジェクトはお互いについてもっと知る必要があります。そのため、ルールは「常に継承よりも合成を優先する」です。これは決して継承を使用することを意味するものではなく、完全に適切な場所で使用することを意味しますが、そこに座って「私はこれを両方の方法で行うことができ、両方とも理にかなっている」と考えている場合は、構成にまっすぐ進んでください

継承も制限の一種です。AssultRifleになり得る「WeaponBase」があります。ダブルバレルショットガンを持ち、バレルを独立して発射できるようにしたい場合はどうなりますか-少し難しいが実行可能です、シングルバレルとダブルバレルクラスがありますが、2つのシングルバレルをマウントすることはできません同じ銃で、できますか?3バレルにするために1つを改造できますか、それとも新しいクラスが必要ですか?

下にGrenadeLauncherを備えたAssultRifleについてはどうですか?GrenadeLauncherを夜間の狩猟用の懐中電灯に置き換えることはできますか?

最後に、構成ファイルを編集して、ユーザーが前述の銃を作成できるようにする方法を教えてください。これは、データで構成および構成する方が適切な関係をハードコーディングしているため困難です。

多重継承は、これらの些細な例をいくつか修正することができますが、それ自身の問題のセットを追加します。

「継承よりも構成を優先する」などの一般的な言葉が出る前に、私は非常に複雑な継承ツリーを作成することでこれを見つけました(これは非常に楽しく、完璧に機能しました)、後で修正して維持することは本当に難しいことがわかりました。ことわざは、あなたが経験全体を経験することなく、そのような概念を学び、覚えるための代替のコンパクトな方法を持っているということです-しかし、継承を頻繁に使用したい場合は、心を開いてそれがどのように機能するかを評価することをお勧めします継承-継承を多用するとひどいことは起こりません。最悪の場合は素晴らしい学習体験になるかもしれません(せいぜいうまくいくかもしれません)


2
「設定ファイルを編集して、ユーザーに前述の銃を作成させるにはどうすればよいですか?」->私が一般的に行うパターンは、各武器の最終クラスを作成することです。たとえば、Glock17のように。最初にWeaponBaseクラスを作成します。このクラスは抽象化され、発射モード、発射速度、マガジンサイズ、最大弾薬、ペレットなどの一般的な値を定義します。また、mouse1 / mouse2 / reloadなどのユーザー入力を処理し、対応するメソッドを呼び出します。開発者が必要に応じて基本機能をオーバーライドできるように、それらはほとんど仮想であるか、より多くの機能に分割されています。そして、WeaponBaseを拡張した別のクラスを作成します
モダニエーター

2
SlideStoppableWeaponのようなもの。ほとんどのピストルにはこれがあり、これはスライド停止などの追加の動作を処理します。最後に、SlideStoppableWeaponから拡張されるGlock17クラスを作成します。Glock17は最終クラスであり、銃が持つ唯一の能力を持っている場合、最終的にここに記述されます。私は通常、銃に特別な発射機構またはリロード機構がある場合にこのパターンを使用します。クラスの階層の終わりにそのユニークな動作を実装し、親から可能な限り共通のロジックを組み合わせます。
モダニエーター

2
@modernator私が言ったように、それは単なるガイドラインです-もしあなたがそれを信用しないなら、遠慮なく無視して、時間の経過とともに結果に目を向けておいて、頻繁に利益対痛みを評価してください。私はつま先をスタブした場所を覚えて、他の人が同じことを避けるのを助けようとしますが、私のために働くものはあなたのために働かないかもしれません。
ビルK

7
@modernator MinecraftではMonster、サブクラスを作成しましたWalkingEntity。次に、スライムを追加しました。それはどれくらいうまくいったと思いますか?
user253751

1
@immibisその問題への参照または逸話的なリンクはありますか?グーグルのMinecraftのキーワードは、ビデオリンクで私をdrれさせます;-)
ピーター-モニカの復活

3

他の答えとは反対に、これは継承と合成とは関係ありません。継承と構成は、クラスの実装方法に関する決定です。インターフェイスとクラスは、その前に決定されます。

インターフェイスは、あらゆるOOP言語の第一級市民です。クラスは、二次実装の詳細です。新しいプログラマーの心は、インターフェイスやクラスの前にクラスや継承を導入する教師や本によって厳しく歪められています。

重要な原則は、可能な限り、すべてのメソッドパラメーターと戻り値の型は、クラスではなくインターフェイスにすることです。これにより、APIがより柔軟で強力になります。ほとんどの場合、メソッドとその呼び出し元は、それらが処理しているオブジェクトの実際のクラスを知ったり、気にする必要はありません。APIが実装の詳細にとらわれない場合、何も壊さずに構成と継承を自由に切り替えることができます。


インターフェイスがOOP言語の第一級市民である場合、Python、RubyなどはOOP言語ではないかと思います。
ブラックジャック

@BlackJack:ではRubyは、インターフェースは(あなたがする場合はダックタイピング)暗黙的であり、組成物の代わりに、継承を用いて表現(また、それは、どこかの間であるミックスインを持っている限り、私は心配していたように)...
AnoE

@BlackJack「インターフェース」(概念)はinterface(言語キーワード)を必要としません。
カレス

2
@Calethしかし、彼らを第一級市民と呼ぶことは私見をします。言語記述が何かが第一級市民であると主張するとき、それは通常、それがその言語で型と価値を持ち、受け渡すことができる本物であることを意味します。同様のクラスは、関数、メソッド、およびモジュールと同様に、Pythonのファーストクラスの市民です。概念について説明している場合、インターフェイスはすべての非OOP言語の一部でもあります。
ブラックジャック

2

ある特定のアプローチがすべての場合に最適であると誰かがあなたに言うときはいつでも、同じ薬がすべての病気を治すとあなたに言うことと同じです。

継承vs構成はis-a vs has-aの質問です。インターフェイスは、別の(3番目の)方法であり、状況によっては適切です。

継承、またはis-aロジック:新しいクラスが動作し、新しいクラスがパブリックに公開される場合、派生元の古いクラスのように(外部で)完全に使用される場合に使用します古いクラスが持っていたすべての機能を...継承します。

構成、またはhas-aロジック:古いクラスの機能を公開せずに、新しいクラスが古いクラスを内部で使用する必要がある場合、構成を使用します。つまり、古いクラスのインスタンスをメンバープロパティまたは新しい変数。(これは、ユースケースに応じて、プライベートプロパティ、保護、またはその他のいずれかになります)。ここでの重要な点は、これが元のクラス機能を公開して使用したくないユースケースであり、内部で使用するだけで、継承の場合はそれらを公開することです新しいクラス。1つのアプローチが必要な場合もあれば、別のアプローチが必要な場合もあります。

インターフェース:インターフェースは、さらに別のユースケースです-新しいクラスを部分的に実装し、完全には実装せず、古いクラスの機能を公開します。これにより、新しいクラス、つまり古いクラスとはまったく異なる階層のクラスを作成し、一部の面でのみ古いクラスとして動作させることができます。

クラスによって表されるさまざまなクリーチャーがあり、関数によって表される機能を持っているとしましょう。たとえば、鳥にはTalk()、Fly()、Poop()があります。DuckクラスはBirdクラスを継承し、すべての機能を実装します。

クラスフォックスは、明らかに、飛べません。したがって、すべての機能を持つように祖先を定義すると、子孫を適切に導出できませんでした。

ただし、Takeoff()、FlapWings()、およびLand()を含むインターフェイス(IFlyなど)で呼び出しの各グループを表すグループに機能を分割した場合、クラスFoxはITalkおよびIPoopから関数を実装できますIFlyではありません。次に、特定のインターフェイスを実装するオブジェクトを受け入れるように変数とパラメーターを定義すると、それらを操作するコードは何を呼び出すことができるかを知り、他の機能も確認する必要がある場合は常に他のインターフェイスを照会できます現在のオブジェクトに実装されます。

これらのアプローチにはそれぞれ、最適なユースケースがありますが、すべてのケースに最適なソリューションはありません。


1

エンティティコンポーネントアーキテクチャで動作するゲーム、特にUnityでは、ゲームコンポーネントの継承よりも合成を優先する必要があります。はるかに柔軟性があり、ゲームエンティティが継承ツリーの異なるリーフにある2つのものに「なり」たいという状況に陥ることを防ぎます。

したがって、たとえば、継承ツリーの1つのブランチにTalkingAIがあり、別の別のブランチにVolumetricCloudがあり、話しているクラウドが必要だとします。これは、深い継承ツリーでは困難です。エンティティコンポーネントを使用すると、TalkingAIコンポーネントとクラウドコンポーネントを持つエンティティを作成するだけで、準備完了です。

しかし、それは、たとえば、ボリュームクラウドの実装では、継承を使用すべきではないという意味ではありません。そのためのコードはかなり多く、いくつかのクラスで構成されている可能性があり、必要に応じてOOPを使用できます。しかし、それはすべて単一のゲームコンポーネントになります。

サイドノートとして、私はこれに関していくつかの問題を取ります:

通常、基本クラス(ほとんどが抽象化された)を作成し、オブジェクトの継承を使用して、同じ機能を他のさまざまなオブジェクトと共有します。

継承はコードの再利用ではありません。is-a関係を確立するためです。多くの場合、これらは密接に関連していますが、注意する必要があります。2つのモジュールが同じコードを使用する必要があるからといって、一方が他方と同じタイプであることを意味するわけではありません。

たとえば、List<IWeapon>さまざまな種類の武器を持ちたい場合は、インターフェイスを使用できます。このようにして、IWeaponインターフェイスとすべての武器のMonoBehaviourサブクラスの両方から継承し、多重継承がないことによる問題を回避できます。


-3

インターフェイスが何であるか、または何をするのか理解していないようです。オブジェクト指向について考えるのに10年以上かかり、本当に頭の良い人に質問をするのに約10年かかりました。お役に立てれば。

最善の方法は、それらを組み合わせて使用​​することです。私のオブジェクト指向の世界では、世界と人々をモデリングして、いくつかの深い例があります。魂は、心を通して身体と結びついています。心は、魂(一部の人は知性を考慮しています)と魂が身体に発行するコマンドとの間のインターフェースです。心と体のインターフェースは、機能的に言​​えば、すべての人にとって同じです。

同じアナロジーを、世界と接する人(身体と魂)の間でも使用できます。身体は、心と世界の間のインターフェースです。誰もが同じように世界とインターフェースをとります。唯一の唯一性は、私たちがどのように世界とやり取りするかです。

インターフェースは、オブジェクト指向構造を別のオブジェクト指向構造と関連付けるためのメカニズムにすぎません。各サイドは、異なるメディア/環境で機能的な出力を生成するために互いにネゴシエート/マッピングする必要がある異なる属性を持っています。

そのため、オープンハンド関数を呼び出すには、オープンハンドを呼び出す前に必要なすべての要件を実装する方法を備えた独自のAPIを持つインターフェイスnervous_command_systemを実装する必要があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.