Come posso evitare l'accoppiamento di script stretto in Unity?


24

Qualche tempo fa ho iniziato a lavorare con Unity e continuo a lottare con il problema degli script strettamente associati. Come posso strutturare il mio codice per evitare questo problema?

Per esempio:

Voglio avere sistemi di salute e morte in script separati. Voglio anche avere diversi script di camminata intercambiabili che mi permettano di cambiare il modo in cui viene mosso il personaggio del giocatore (basato sulla fisica, controlli inerziali come in Mario contro controlli stretti e contorti come in Super Meat Boy). Lo script Health deve contenere un riferimento allo script Death, in modo che possa innescare il metodo Die () quando la salute dei giocatori raggiunge lo 0. Lo script Death dovrebbe contenere alcuni riferimenti allo script walking utilizzato, per disabilitare il camminare sulla morte (I sono stanco di zombi).

Avrei normalmente creare interfacce, come IWalking, IHealthe IDeath, in modo che posso cambiare questi elementi in un capriccio senza rompere il resto del mio codice. Vorrei che fossero impostati da uno script separato sull'oggetto giocatore, diciamo PlayerScriptDependancyInjector. Forse che lo script avrebbe pubblici IWalking, IHealthe IDeathgli attributi, in modo che le dipendenze possono essere impostati dal designer di livello dalla finestra di ispezione trascinando e rilasciando gli script appropriati.

Ciò mi consentirebbe semplicemente di aggiungere facilmente comportamenti agli oggetti di gioco e non preoccuparmi delle dipendenze codificate.

Il problema in Unity

Il problema è che in Unity non riesco a esporre interfacce nell'ispettore e, se scrivo i miei ispettori, i riferimenti non verranno serializzati ed è un sacco di lavoro inutile. Ecco perché sono rimasto con la scrittura di codice strettamente accoppiato. La mia Deathsceneggiatura espone un riferimento a una InertiveWalkingsceneggiatura. Ma se decido di voler controllare strettamente il personaggio del giocatore, non posso semplicemente trascinare e rilasciare la TightWalkingsceneggiatura, devo cambiarla Death. Che schifo Posso affrontarlo, ma la mia anima piange ogni volta che faccio qualcosa del genere.

Qual è l'alternativa preferita alle interfacce in Unity? Come posso risolvere questo problema? Ho trovato questo , ma mi dice quello che già so, e non mi dice come farlo in Unity! Anche questo discute cosa dovrebbe essere fatto, non come, non affronta il problema dello stretto accoppiamento tra script.

Tutto sommato, penso che quelli siano scritti per le persone che sono venute su Unity con un background di game design che hanno appena imparato a programmare e su Unity ci sono pochissime risorse per gli sviluppatori regolari. Esiste un modo standard per strutturare il codice in Unity o devo capire il mio metodo?


1
The problem is that in Unity I can't expose interfaces in the inspectorNon credo di capire cosa intendi con "esporre l'interfaccia nell'ispettore" perché il mio primo pensiero è stato "perché no?"
jhocking del

@jhocking chiedi agli sviluppatori di unità. Non lo vedi nell'ispettore. Non appare lì se lo si imposta come attributo pubblico e tutti gli altri attributi lo fanno.
KL,

1
ohhh intendi usare l'interfaccia come tipo di variabile, non solo facendo riferimento a un oggetto con quell'interfaccia.
jhocking del


1
@jzx Conosco e utilizzo il design SOLID e sono un grande fan dei libri di Robert C. Martins, ma questa domanda riguarda tutto come farlo in Unity. E no, non ho letto quell'articolo, leggendolo adesso, grazie :)
KL

Risposte:


14

Ci sono alcuni modi in cui puoi lavorare per evitare l'accoppiamento di script stretto. Internal to Unity è una funzione SendMessage che quando viene indirizzata a GameObject di Monobehaviour invia quel messaggio a tutto sull'oggetto di gioco. Quindi potresti avere qualcosa del genere nel tuo oggetto salute:

[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;
}

Questo è davvero semplificato, ma dovrebbe mostrare esattamente a cosa sto arrivando. La proprietà che lancia il messaggio come se fosse un evento. Il tuo script di morte intercetterebbe quindi questo messaggio (e se non c'è nulla per intercettarlo, SendMessageOptions.DontRequireReceiver ti garantirà di non ricevere un'eccezione) ed eseguirà azioni basate sul nuovo valore di integrità dopo che è stato impostato. Così:

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

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

Non vi è alcuna garanzia di ordine in questo, tuttavia. Nella mia esperienza va sempre dalla cima della maggior parte degli script su un GameObject alla fine della maggior parte degli script, ma non vorrei contare su nulla che non si possa osservare da soli.

Esistono altre soluzioni che potresti esaminare che stanno creando una funzione GameObject personalizzata che ottiene componenti e restituisce solo quei componenti che hanno un'interfaccia specifica anziché Tipo, richiederebbe un po 'più di tempo ma potrebbe servire bene ai tuoi scopi.

Inoltre, puoi scrivere un editor personalizzato che controlla un oggetto assegnato a una posizione nell'editor per quell'interfaccia prima di eseguire effettivamente l'assegnazione (in questo caso assegneresti lo script normalmente permettendo di serializzarlo, ma generando un errore se non utilizzava l'interfaccia corretta e negava l'assegnazione). Ho già fatto entrambe queste cose prima e posso produrre quel codice ma avrò bisogno di un po 'di tempo per trovarlo prima.


5
SendMessage () risolve questa situazione e io uso quel comando in molte situazioni, ma un sistema di messaggi non mirato come questo è meno efficiente di avere direttamente un riferimento al destinatario. Dovresti fare attenzione a fare affidamento su questo comando per tutte le comunicazioni di routine tra gli oggetti.
jhocking del

2
Sono un fan personale dell'uso di eventi e delegati in cui gli oggetti Component conoscono i sistemi core più interni ma i sistemi core non sanno nulla dei componenti. Ma non ero sicuro al cento per cento che fosse il modo in cui volevano che la loro risposta fosse progettata. Quindi sono andato con qualcosa che ha risposto a come scollegare completamente gli script.
Brett Satoru,

1
Credo anche che ci siano alcuni plugin disponibili nell'archivio risorse di unità che possono esporre interfacce e altri costrutti di programmazione non supportati da Unity Inspector, potrebbe valere la pena di fare una rapida ricerca.
Matthew Pigram,

7

Personalmente NON uso MAI SendMessage. C'è ancora una dipendenza tra i tuoi componenti con SendMessage, è solo molto poco mostrato e facile da rompere. L'uso di interfacce e / o delegati elimina davvero la necessità di utilizzare SendMessage (che è più lento, anche se non dovrebbe essere un problema fino a quando non lo sarà).

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

Usa le cose di questo ragazzo. Fornisce un mucchio di cose dell'editor come interfacce esposte e delegati AND. Ci sono un sacco di cose là fuori in questo modo, o puoi persino crearne uno tuo. Qualunque cosa tu faccia, NON compromettere il tuo design a causa della mancanza di supporto per gli script di Unity. Queste cose dovrebbero essere in Unity per impostazione predefinita.

Se questa non è un'opzione, trascina invece le classi monobehaviour e le cast dinamicamente sull'interfaccia richiesta. È più lavoro e meno elegante, ma fa il lavoro.


2
Personalmente sono sospettoso di diventare troppo dipendente da plugin e librerie per cose di basso livello come questa. Probabilmente sono un po 'paranoico, ma mi sembra troppo il rischio di interrompere il mio progetto perché il loro codice si rompe dopo un aggiornamento di Unity.
jhocking

Stavo usando quel framework e alla fine mi sono fermato dal momento che avevo il motivo per cui @jhocking menziona il rosicchiare nella parte posteriore della mia mente (e non ha aiutato che da un aggiornamento all'altro, è passato da un editor di editor a un sistema che includeva tutto ma il lavello della cucina infrange la compatibilità all'indietro [e rimuove una o due funzionalità piuttosto utili])
Selali Adobor,

6

Un approccio specifico di Unity che mi viene in mente sarebbe inizialmente GetComponents<MonoBehaviour>()quello di ottenere un elenco di script, e quindi trasmettere tali script in variabili private per le interfacce specifiche. Dovresti farlo in Start () sullo script dell'iniettore di dipendenza. Qualcosa di simile a:

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

In effetti, con questo metodo potresti persino avere i singoli componenti che dicono allo script di iniezione delle dipendenze quali sono le loro dipendenze, usando il comando typeof () e il tipo 'Type' . In altre parole, i componenti forniscono all'iniettore di dipendenza un elenco di tipi, quindi l'iniettore di dipendenza restituisce un elenco di oggetti di tali tipi.


In alternativa, è possibile utilizzare solo classi astratte anziché interfacce. Una classe astratta può raggiungere praticamente lo stesso scopo di un'interfaccia e puoi fare riferimento a quello nell'Inspector.


Non posso ereditare da più classi mentre posso implementare più interfacce. Questo potrebbe essere un problema, ma immagino sia molto meglio di niente :) Grazie per la tua risposta!
KL,

per quanto riguarda la modifica - una volta che ho l'elenco degli script, come fa il mio codice a sapere quale script non può accedere a quale interfaccia?
KL,

1
modificato per spiegare anche questo
jhocking del

1
In Unity 5, è possibile utilizzare GetComponent<IMyInterface>()e GetComponents<IMyInterface>()direttamente, che restituisce i componenti che lo implementano.
S. Tarık Çetin,

5

Dalla mia esperienza, lo sviluppatore di giochi prevede tradizionalmente un approccio più pragmatico rispetto agli sviluppatori industriali (con livelli di astrazione inferiori). In parte perché vuoi favorire le prestazioni piuttosto che la manutenibilità, e anche perché hai meno probabilità di riutilizzare il tuo codice in altri contesti (di solito c'è un forte accoppiamento intrinseco tra la grafica e la logica del gioco)

Capisco perfettamente la tua preoccupazione, soprattutto perché la preoccupazione relativa alle prestazioni è meno critica con i dispositivi recenti, ma la mia sensazione è che dovrai sviluppare le tue soluzioni se desideri applicare le migliori pratiche di OOP industriale allo sviluppo di giochi di Unity (come iniezione di dipendenza, eccetera...).


2

Dai un'occhiata a IUnified ( http://u3d.as/content/wounded-wolf/iunified/5H1 ), che ti consente di esporre le interfacce nell'editor, proprio come hai detto. C'è un po 'più di cablaggio da fare rispetto alla normale dichiarazione delle variabili, tutto qui.

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

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

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

Inoltre, come menzionato da un'altra risposta, l'utilizzo di SendMessage non rimuove l'accoppiamento. Invece non lo rende errore al momento della compilazione. Molto male - man mano che ingrandisci il tuo progetto, può diventare un incubo da mantenere.

Se stai cercando di separare ulteriormente il codice senza utilizzare l'interfaccia nell'editor, puoi utilizzare un sistema basato su eventi fortemente tipizzato (o addirittura uno di tipo vagamente effettivamente, ma ancora più difficile da mantenere). Ho basato il mio su questo post , che è super conveniente come punto di partenza. Fare attenzione a creare un mucchio di eventi in quanto potrebbe innescare il GC. Ho invece risolto il problema con un pool di oggetti, ma principalmente perché lavoro con i dispositivi mobili.


1

Fonte: 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

Uso alcune strategie diverse per aggirare i limiti menzionati.

Uno di quelli che non vedo menzionati sta usando un MonoBehaviorche espone l'accesso alle diverse implementazioni di una data interfaccia. Ad esempio, ho un'interfaccia che definisce come vengono implementati i RaycastIRaycastStrategy

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

Ho poi attuare in diverse classi con classi piace LinecastRaycastStrategyed SphereRaycastStrategye implementare un MonoBehavior RaycastStrategySelectorche espone una singola istanza di ogni tipo. Qualcosa del genere RaycastStrategySelectionBehavior, che è implementato qualcosa del tipo:

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;
        }
    }
}

Se è probabile che le implementazioni facciano riferimento alle funzioni di Unity nelle loro costruzioni (o semplicemente per essere al sicuro, ho semplicemente dimenticato questo durante la digitazione di questo esempio) Awakeper evitare problemi con i costruttori che chiamano o facendo riferimento alle funzioni di Unity nell'editor o all'esterno del principale filo

Questa strategia presenta alcuni inconvenienti, soprattutto quando devi gestire molte implementazioni diverse che cambiano rapidamente. I problemi con la mancanza di controllo in fase di compilazione possono essere aiutati utilizzando un editor personalizzato, poiché il valore enum può essere serializzato senza alcun lavoro speciale (anche se di solito lo rinuncio, poiché le mie implementazioni non tendono ad essere così numerose).

Uso questa strategia a causa della flessibilità che ti offre essendo basato su MonoBehaviors senza obbligarti a implementare le interfacce usando MonoBehaviors. Questo ti dà "il meglio di entrambi i mondi" poiché è possibile accedere al Selettore utilizzando GetComponent, la serializzazione è gestita da Unity e il Selettore può applicare la logica in fase di esecuzione per cambiare automaticamente le implementazioni in base a determinati fattori.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.