Unityでタイトなスクリプトカップリングを回避するにはどうすればよいですか?


24

しばらく前に、Unityの使用を開始しましたが、依然として密結合スクリプトの問題に苦労しています。この問題を回避するためにコードをどのように構成できますか?

例えば:

健康と死のシステムを別々のスクリプトに入れたいです。また、プレイヤーキャラクターの移動方法を変更できるさまざまな交換可能なウォーキングスクリプトが必要です(マリオのような物理ベースの慣性コントロールと、スーパーミートボーイのようなタイトでぴくぴくしたコントロール)。ヘルススクリプトは、プレイヤーのヘルスが0に達したときにDie()メソッドをトリガーできるように、デススクリプトへの参照を保持する必要があります。デススクリプトは、使用中のウォーキングスクリプトへの参照を保持して、ゾンビにうんざりしている)。

私は考え、通常のように、インターフェースを作成しIWalkingIHealthそしてIDeath私は自分のコードの残りの部分を壊すことなく、気まぐれで、これらの要素を変更することができるように、。たとえば、プレーヤーオブジェクトの個別のスクリプトでセットアップしますPlayerScriptDependancyInjector。おそらく、そのスクリプトにはpublic IWalkingIHealthおよびIDeath属性があり、適切なスクリプトをドラッグアンドドロップすることで、レベルデザイナがインスペクタから依存関係を設定できます。

これにより、ゲームオブジェクトにビヘイビアを簡単に追加でき、ハードコードされた依存関係を心配する必要がなくなります。

Unityの問題

問題はユニティで、私はインスペクタでインタフェースを公開することができないということである、と私は私自身の検査官を書いた場合、参照が連載を取得文句を言わない、そしてそれは、不必要な多くの作業です。だからこそ、密結合されたコードを書くしかありません。私のDeathスクリプトは、スクリプトへの参照を公開していInertiveWalkingます。しかし、プレイヤーキャラクターにきめ細かく制御させたい場合、TightWalkingスクリプトをドラッグアンドドロップすることはできませんDeath。スクリプトを変更する必要があります。ひどい。対処できますが、このようなことをするたびに魂が泣きます。

Unityのインターフェイスの代替として望ましいものは何ですか?この問題を修正するにはどうすればよいですか?私はこれを見つけましたが、すでに知っていることを教えてくれ、Unityでそれを行う方法を教えてくれません!これも、どのように行うべきかを説明するものであり、方法ではなく、スクリプト間の密結合の問題に対処するものではありません。

全体として、ゲームデザインのバックグラウンドを持ってUnityに来た人のために書かれたものだと感じています。Unityには通常の開発者向けのリソースはほとんどありません。Unityでコードを構造化する標準的な方法はありますか、それとも独自の方法を見つけ出す必要がありますか?


1
The problem is that in Unity I can't expose interfaces in the inspector私が最初に考えたのは「なぜですか?」だったので、「インスペクタでインターフェイスを公開する」という意味が理解できないと思います。
-jhocking

@jhockingは団結開発者に尋ねます。インスペクターに表示されないだけです。パブリック属性として設定した場合は表示されず、他のすべての属性は表示されます。
KL

1
ああ、そのインターフェイスでオブジェクトを参照するだけでなく、変数の型としてインターフェイスを使用することを意味します。
ジョッキング

1
オリジナルのECS記事を読みましたか?cowboyprogramming.com/2007/01/05/evolve-your-heirachy SOLIDデザインはどうですか?en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29
jzx

1
@jzx私はSOLIDデザインを知っていますし、使用しています。私はRobert C. Martinsの本の大ファンですが、この質問はUnityでそれを行う方法についてのすべてです。いえ、その記事を読んでいませんでした。今すぐ読んで、ありがとう:)
KL

回答:


14

スクリプトの密結合を回避するために作業できる方法がいくつかあります。Unityの内部にあるSendMessage関数は、MonobehaviourのGameObjectをターゲットにすると、そのメッセージをゲームオブジェクト上のすべてに送信します。そのため、ヘルスオブジェクトに次のようなものがあるかもしれません。

[SerializeField]
private int _currentHealth;
public int currentHealth
{
    get { return _currentHealth;}
    protected set
    {
        _currentHealth = value;
        gameObject.SendMessage("HealthChanged", value, SendMessageOptions.DontRequireReceiver);
    }
}

[SerializeField]
private int _maxHealth;
public int maxHealth
{
    get { return _maxHealth;}
    protected set
    {
        _maxHealth = value;
        if (currentHealth > _maxHealth)
        {
            currentHealth = _maxHealth;
        }
    }
}

public void ChangeHealth(int deltaChange)
{
    currentHealth += deltaChange;
}

これは本当に単純化されていますが、私が何を得ているかを正確に示す必要があります。イベントのようにメッセージをスローするプロパティ。デススクリプトは、このメッセージをインターセプトし(インターセプトするものがない場合、SendMessageOptions.DontRequireReceiverは例外を受け取らないことを保証します)、設定後に新しいヘルス値に基づいてアクションを実行します。そのようです:

void HealthChanged(int newHealth)
{
    if (newHealth <= 0)
    {
        Die();
    }
}

void Die ()
{
    Debug.Log ("I am undone!");
    DestroyObject (gameObject);
}

ただし、これには順序の保証はありません。私の経験では、常にGameObjectの一番上のスクリプトから一番下のスクリプトに移動しますが、あなたが自分で観察できないものに頼るつもりはありません。

コンポーネントを取得し、Typeではなく特定のインターフェイスを持つコンポーネントのみを返すカスタムGameObject関数を構築している他の調査可能なソリューションがあります。少し時間がかかりますが、目的を十分に果たすことができます。

さらに、実際に割り当てをコミットする前に、そのインターフェイスのエディターの位置に割り当てられているオブジェクトをチェックするカスタムエディターを作成できます(この場合、スクリプトを通常どおり割り当てて、シリアル化を許可しますが、エラーをスローします)正しいインターフェースを使用せず、割り当てを拒否した場合)。私はこれらの両方を以前に実行し、そのコードを生成できますが、最初にそれを見つけるのに時間が必要です。


5
SendMessage()はこの状況に対処し、多くの状況でそのコマンドを使用しますが、このようなターゲットのないメッセージシステムは、受信者への参照を直接持つよりも効率が劣ります。オブジェクト間のすべての日常的な通信について、このコマンドに依存することに注意する必要があります。
ジョッキング

2
私は、コンポーネントオブジェクトがより内部的なコアシステムについては知っているが、コアシステムはコンポーネントについて何も知らないイベントやデリゲートの使用の個人的なファンです。しかし、それが彼らが答えを設計するのを望んでいた方法だと私は100%確信していませんでした。そこで、スクリプトを完全に切り離す方法を答える何かを選びました。
ブレットサトル

1
また、Unity Inspectorでサポートされていないインターフェイスやその他のプログラミング構成要素を公開できるプラグインがUnity Asset Storeで利用可能であると信じています。
マシューピグラム

7

私は個人的にSendMessageを使用しません。SendMessageを使用したコンポーネント間にはまだ依存関係がありますが、表示は非常に悪く、簡単に壊れます。インターフェイスやデリゲートを使用すると、SendMessageを使用する必要がなくなります(低速になりますが、必要になるまで心配する必要はありません)。

http://forum.unity3d.com/threads/free-vfw-full-set-of-drawers-savesystem-serialize-interfaces-generics-auto-props-delegates.266165/

この男のものを使用してください。これは、公開されたインターフェースやデリゲートなどのエディターの山を提供します。このようなものは山ほどありますが、自分で作ることもできます。何をするにしても、Unityのスクリプトサポートが不足しているため、デザインを妥協しないでください。これらはデフォルトでUnityにあるはずです。

これがオプションでない場合は、代わりにモノビヘイビアクラスをドラッグアンドドロップし、必要なインターフェイスに動的にキャストします。それはより多くの作業であり、エレガントではありませんが、仕事を終わらせます。


2
個人的には、このような低レベルのもののためにプラグインやライブラリに依存しすぎるのは嫌です。私はおそらく少し妄想的ですが、Unityの更新後にコードが壊れるので、プロジェクトが壊れるリスクが大きすぎると感じています。
ジョッキング

私はそのフレームワークを使用していましたが、@ jhockingが私の心の後ろでかじっているという理由があったので最終的に停止しました(そして、アップデートから別のものへの助けにはならず、エディターヘルパーからすべてを含むシステムに行きましたしかし、キッチンは、後方互換性を破りながら(そして、かなり便利な機能を1つまたは2つ削除しながら))
Selali Adob​​or

6

私に起こるUnity固有のアプローチは、最初GetComponents<MonoBehaviour>()にスクリプトのリストを取得し、それらのスクリプトを特定のインターフェイスのプライベート変数にキャストすることです。依存関係インジェクタースクリプトのStart()でこれを実行する必要があります。何かのようなもの:

MonoBehaviour[] list = GetComponents<MonoBehaviour>();
for (int i = 0; i < list.Length; i++) {
    if (list[i] is IHealth) {
        Debug.Log("Found it!");
    }
}

実際、このメソッドを使用すると、typeof()コマンドと 'Type' typeを使用して、個々のコンポーネントに依存関係注入スクリプトに依存関係を伝えることさえできます。つまり、コンポーネントは依存関係インジェクターに型のリストを提供し、依存関係インジェクターはそれらの型のオブジェクトのリストを返します。


または、インターフェイスの代わりに抽象クラスを使用することもできます。抽象クラスは、インターフェイスとほぼ同じ目的を達成でき、インスペクターでそれを参照できます。


複数のインターフェイスを実装できる一方で、複数のクラスから継承することはできません。これは問題になる可能性がありますが、何もないよりはましだと思います:)ご回答ありがとうございます
KL

編集に関しては-スクリプトのリストを取得したら、コードはどのスクリプトをどのインターフェイスに傾けるのかをどのように知るのですか?
KL

1
それを説明するために編集された
-jhocking

1
Unity 5ではGetComponent<IMyInterface>()GetComponents<IMyInterface>()直接使用することができ、それを実装するコンポーネントを返します。
S.タリクチェティン

5

私自身の経験から、ゲーム開発者は伝統的に産業開発者よりも実用的なアプローチを必要とします(抽象化レイヤーが少ない)。部分的には、保守性よりもパフォーマンスを優先したいため、また他のコンテキストでコードを再利用する可能性が低いためです(通常、グラフィックスとゲームロジックの間には強力な固有のカップリングがあります)

特に最近のデバイスではパフォーマンスの懸念はそれほど重要ではないため、私はあなたの懸念を完全に理解していますが、産業用OOPのベストプラクティスをUnityゲーム開発に適用したい場合は独自のソリューションを開発する必要があると感じています(依存性注入、等...)。


2

IUnified(http://u3d.as/content/wounded-wolf/iunified/5H1)をご覧ください。これは、あなたが言及したように、エディターでインターフェースを公開できます。通常の変数宣言と比べて、もう少し配線が必要です。それだけです。

public interface IPinballValue{
   void Add(int value);
}

[System.Serializable]
public class IPinballValueContainer : IUnifiedContainer<IPinballValue> {}

public class MyClassThatExposesAnInterfaceProperty : MonoBehaviour {
   [SerializeField]
   IPinballValueContainer value;
}

また、別の回答で言及しているように、SendMessageを使用しても結合は削除されません。代わりに、コンパイル時にエラーが出ないようにします。非常に悪い-プロジェクトをスケールアップすると、維持するのが悪夢になる可能性があります。

エディターでインターフェイスを使用せずにコードをさらに分離したい場合は、厳密に型指定されたイベントベースのシステムを使用できます(実際には緩やかに型指定されたシステムでも、保守が難しくなります)。私はこの投稿に基づいていますが、これは出発点として非常に便利です。GCをトリガーする可能性があるため、多数のイベントを作成することに注意してください。代わりにオブジェクトプールで問題を回避しましたが、それは主にモバイルで作業しているためです。


1

ソース:http : //www.udellgames.com/posts/ultra-useful-unity-snippet-developers-use-interfaces/

[SerializeField]
private GameObject privateGameObjectName;

public GameObject PublicGameObjectName
{
    get { return privateGameObjectName; }
    set
    {
        //Make sure changes to privateGameObjectName ripple to privateInterfaceName too
        if (privateGameObjectName != value)
        {
            privateInterfaceName = null;
            privateGameObjectName = value;
        }
    }
}

private InterfaceType privateInterfaceName;
public InterfaceType PublicInterfaceName
{
    get
    {
        if (privateInterfaceName == null)
        {
            privateInterfaceName = PublicGameObjectName.GetComponent(typeof(InterfaceType)) as InterfaceType;
        }
        return privateInterfaceName;
    }
    set
    {
        //Make sure changes to PublicInterfaceName ripple to PublicGameObjectName too
        if (privateInterfaceName != value)
        {
            privateGameObjectName = (privateInterfaceName as Component).gameObject;
            privateInterfaceName = value;
        }

    }
}

0

あなたが言及した制限を回避するために、いくつかの異なる戦略を使用します。

言及されていないものの1つはMonoBehavior、特定のインターフェイスのさまざまな実装へのアクセスを公開するを使用していることです。たとえば、レイキャストの実装方法を定義するインターフェイスがありますIRaycastStrategy

public interface IRaycastStrategy 
{
    bool Cast(Ray ray, out RaycastHit raycastHit, float distance, LayerMask layerMask);
}

次にLinecastRaycastStrategySphereRaycastStrategyなどのクラスを使用して異なるクラスに実装し、RaycastStrategySelector各タイプの単一インスタンスを公開するMonoBehavior を実装します。のようなものでRaycastStrategySelectionBehavior、次のようなものが実装されています。

public class RaycastStrategySelectionBehavior : MonoBehavior
{

    private readonly Dictionary<RaycastStrategyType, IRaycastStrategy> _raycastStrategies
        = new Dictionary<RaycastStrategyType, IRaycastStrategy>
        {
            { RaycastStrategyType.Line, new LineRaycastStrategy() },
            { RaycastStrategyType.Sphere, new SphereRaycastStrategy() }
        };

    public RaycastStrategyType StrategyType;


    public IRaycastStrategy RaycastStrategy
    {
        get
        {
            IRaycastStrategy raycastStrategy;
            if (!_raycastStrategies.TryGetValue(StrategyType, out raycastStrategy))
            {
                throw new InvalidOperationException("There is no registered implementation for the selected strategy");
            }
            return raycastStrategy;
        }
    }
}

実装が構造内でUnity関数を参照する可能性が高い場合(または安全のために、この例を入力するときにこれを忘れました)Awake、コンストラクターの呼び出しまたはエディターまたはメインの外部でのUnity関数の参照に関する問題を回避するために使用します糸

この戦略には、特に急速に変化するさまざまな実装を管理する必要がある場合に、いくつかの欠点があります。列挙値は特別な作業なしにシリアル化できるため、カスタムエディターを使用すると、コンパイル時のチェックがないという問題を解決できます(ただし、実装はそれほど多くない傾向があるため、通常はこれを忘れます)。

MonoBehaviorsを使用してインターフェイスを実装することを実際に強制することなく、MonoBehaviorsに基づいて柔軟性を提供するため、この戦略を使用します。Selectorはを使用してアクセスできるため、セレクターGetComponentはすべてUnityによって処理され、Selectorは実行時にロジックを適用して特定の要因に基づいて実装を自動的に切り替えることができるため、これにより「両方の長所」が得られます。

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