¿Cómo puedo evitar el apretado acoplamiento de scripts en Unity?

24

Hace algún tiempo comencé a trabajar con Unity y todavía lucho con el tema de los scripts estrechamente acoplados. ¿Cómo puedo estructurar mi código para evitar este problema?

Por ejemplo:

Quiero tener sistemas de salud y muerte en guiones separados. También quiero tener diferentes guiones de caminata intercambiables que me permitan cambiar la forma en que se mueve el personaje del jugador (controles inerciales basados ​​en la física como en Mario versus controles apretados y nerviosos como en Super Meat Boy). El script de Salud debe contener una referencia al script de Muerte, para que pueda activar el método Die () cuando la salud de los jugadores alcance 0. El script de Muerte debe contener alguna referencia al script de caminar utilizado, para deshabilitar caminar sobre la muerte (I Estoy cansado de zombies).

Yo normalmente crear interfaces, como IWalking, IHealthy IDeath, de modo que pueda cambiar estos elementos en un capricho sin romper el resto de mi código. Me gustaría tenerlos configurados por un guión separado en el objeto jugador, por ejemplo PlayerScriptDependancyInjector. Tal vez ese guión tendría públicas IWalking, IHealthy IDeathatributos, de manera que las dependencias pueden ser establecidas por el diseñador de niveles del inspector de arrastrar y soltar los scripts apropiados.

Eso me permitiría simplemente agregar comportamientos a los objetos del juego fácilmente y no preocuparme por las dependencias codificadas.

El problema en la unidad

El problema es que en Unity no puedo exponer interfaces en el inspector, y si escribo mis propios inspectores, las referencias no se serializarán, y es un trabajo innecesario. Es por eso que me queda escribir código estrechamente acoplado. Mi Deathguión expone una referencia a un InertiveWalkingguión. Pero si decido que quiero que el personaje del jugador controle estrictamente, no puedo simplemente arrastrar y soltar el TightWalkingguión, necesito cambiar el Deathguión. Eso apesta. Puedo lidiar con eso, pero mi alma llora cada vez que hago algo así.

¿Cuál es la alternativa preferida a las interfaces en Unity? ¿Cómo soluciono este problema? Encontré esto , pero me dice lo que ya sé, ¡y no me dice cómo hacerlo en Unity! Esto también analiza lo que debe hacerse, no cómo, no aborda el problema del acoplamiento estrecho entre scripts.

En general, creo que están escritos para personas que vinieron a Unity con experiencia en diseño de juegos y que simplemente aprenden a codificar, y hay muy pocos recursos en Unity para desarrolladores habituales. ¿Hay alguna forma estándar de estructurar su código en Unity, o tengo que descubrir mi propio método?

KL
fuente
1
The problem is that in Unity I can't expose interfaces in the inspectorNo creo entender lo que quieres decir con "exponer la interfaz en el inspector" porque mi primer pensamiento fue "¿por qué no?"
jhocking
@jhocking pregunta a los desarrolladores de la unidad. Simplemente no lo ves en el inspector. Simplemente no aparece allí si lo configura como un atributo público, y todos los demás atributos lo hacen.
KL
1
ohhh quieres decir usar la interfaz como el tipo de variable, no solo hacer referencia a un objeto con esa interfaz.
jhocking
1
¿Has leído el artículo original de ECS? cowboyprogramming.com/2007/01/05/evolve-your-heirachy ¿Qué pasa con el diseño SOLID? en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29
jzx
1
@jzx Sí conozco y uso el diseño SOLID y soy un gran admirador de los libros de Robert C. Martins, pero esta pregunta se trata de cómo hacerlo en Unity. Y no, no leí ese artículo, lo leí en este momento, gracias :)
KL

Respuestas:

14

Hay algunas formas en que puede trabajar para evitar el acoplamiento apretado de scripts. Internal to Unity es una función SendMessage que, cuando se dirige a un GameObject de Monobehaviour, envía ese mensaje a todo el objeto del juego. Entonces, podría tener algo como esto en su objeto de salud:

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

Esto está realmente simplificado, pero debería mostrar exactamente a qué me refiero. La propiedad arroja el mensaje como si fuera un evento. Su secuencia de comandos de muerte interceptaría este mensaje (y si no hay nada para interceptarlo, SendMessageOptions.DontRequireReceiver garantizará que no reciba una excepción) y realice acciones basadas en el nuevo valor de salud después de que se haya configurado. Al igual que:

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

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

Sin embargo, no hay garantía de orden en esto. En mi experiencia, siempre va desde el guión más alto en un GameObject hasta el guión más bajo, pero no confiaría en nada que no puedas observar por ti mismo.

Hay otras soluciones que podría investigar, que es construir una función GameObject personalizada que obtenga Componentes y solo devuelva aquellos componentes que tienen una interfaz específica en lugar de Tipo, tomaría un poco más de tiempo pero podría servir a sus propósitos.

Además, puede escribir un editor personalizado que verifique que un objeto se asigne a una posición en el editor para esa interfaz antes de comprometer realmente la asignación (en este caso, estaría asignando el script como normal permitiendo que se serialice, pero arrojando un error si no usó la interfaz correcta y denegó la asignación). He hecho ambos antes y puedo producir ese código, pero necesitaré algo de tiempo para encontrarlo primero.

Brett Satoru
fuente
55
SendMessage () aborda esta situación y uso ese comando en muchas situaciones, pero un sistema de mensajes no dirigido como este es menos eficiente que tener una referencia directa al receptor. Debe tener cuidado de confiar en este comando para toda comunicación de rutina entre objetos.
jhocking
2
Soy un fanático personal del uso de eventos y delegados donde los objetos componentes saben sobre los sistemas centrales más internos, pero los sistemas centrales no saben nada sobre las piezas componentes. Pero no estaba cien por ciento seguro de que esa fuera la forma en que querían que se diseñara su respuesta. Así que elegí algo que respondía cómo desacoplar completamente los scripts.
Brett Satoru
1
También creo que hay algunos complementos disponibles en la tienda de activos de la unidad que pueden exponer interfaces y otras construcciones de programación no compatibles con el Inspector de la Unidad, podría valer la pena hacer una búsqueda rápida.
Matthew Pigram
7

Yo personalmente NUNCA uso SendMessage. Todavía hay una dependencia entre sus componentes con SendMessage, se muestra muy mal y es fácil de romper. El uso de interfaces y / o delegados realmente elimina la necesidad de usar SendMessage (lo cual es más lento, aunque eso no debería ser una preocupación hasta que sea necesario).

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

Usa las cosas de este chico. Proporciona un montón de cosas de editor como interfaces expuestas Y delegados. Hay un montón de cosas como esta, o incluso puedes hacer la tuya. Hagas lo que hagas, NO comprometas tu diseño debido a la falta de soporte de script de Unity. Estas cosas deberían estar en Unity por defecto.

Si esto no es una opción, arrastre y suelte las clases de monoconductas en su lugar y vuélvalas dinámicamente a la interfaz requerida. Es más trabajo y menos elegante, pero hace el trabajo.

Ben
fuente
2
Personalmente, desconfío de depender demasiado de complementos y bibliotecas para cosas de bajo nivel como esta. Probablemente estoy siendo un poco paranoico, pero parece demasiado riesgo de que mi proyecto se rompa porque su código se rompe después de una actualización de Unity.
jhocking
Estaba usando ese marco y finalmente me detuve, ya que tenía la razón por la cual @jhocking menciona el roer en el fondo de mi mente (y no ayudó que de una actualización a otra, pasó de ser un asistente de editor a un sistema que incluía todo pero el fregadero de la cocina al romper la compatibilidad con versiones anteriores [y eliminar una característica bastante útil o dos])
Selali Adobor
6

Un enfoque específico de Unity que se me ocurre sería inicialmente GetComponents<MonoBehaviour>()obtener una lista de scripts y luego convertir esos scripts en variables privadas para las interfaces específicas. Desea hacer esto en Inicio () en la secuencia de comandos del inyector de dependencia. Algo como:

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

De hecho, con este método incluso podría hacer que los componentes individuales le digan al script de inyección de dependencia cuáles son sus dependencias, utilizando el comando typeof () y el tipo 'Tipo' . En otras palabras, los componentes le dan al inyector de dependencia una lista de tipos, y luego el inyector de dependencia devuelve una lista de objetos de esos tipos.


Alternativamente, podría usar clases abstractas en lugar de interfaces. Una clase abstracta puede lograr prácticamente el mismo propósito que una interfaz, y puede hacer referencia a eso en el Inspector.

jhocking
fuente
No puedo heredar de varias clases mientras puedo implementar múltiples interfaces. Esto podría ser un problema, pero supongo que es mucho mejor que nada :) ¡Gracias por tu respuesta!
KL
en cuanto a la edición: una vez que tengo la lista de secuencias de comandos, ¿cómo sabe mi código qué secuencia de comandos no puedo usar en qué interfaz?
KL
1
editado para explicar eso también
jhocking
1
En Unity 5, puede usar GetComponent<IMyInterface>()y GetComponents<IMyInterface>()directamente, lo que devuelve los componentes que lo implementan.
S. Tarık Çetin
5

Desde mi propia experiencia, el desarrollo de juegos tradicionalmente implica un enfoque más pragmático que el desarrollo industrial (con menos capas de abstracción). En parte porque desea favorecer el rendimiento sobre la capacidad de mantenimiento, y también porque es menos probable que reutilice su código en otros contextos (generalmente hay un fuerte acoplamiento intrínseco entre los gráficos y la lógica del juego)

Entiendo totalmente su preocupación, especialmente porque la preocupación por el rendimiento es menos crítica con los dispositivos recientes, pero creo que tendrá que desarrollar sus propias soluciones si desea aplicar las mejores prácticas industriales de OOP al desarrollo de juegos de Unity (como la inyección de dependencia, etc ...)

sdabet
fuente
2

Eche un vistazo a IUnified ( http://u3d.as/content/wounded-wolf/iunified/5H1 ), que le permite exponer interfaces en el editor, tal como lo menciona. Hay un poco más de cableado que hacer en comparación con la declaración de variable normal, eso es todo.

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

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

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

Además, como mencionan otras respuestas, el uso de SendMessage no elimina el acoplamiento. En cambio, hace que no se produzca un error en tiempo de compilación. Muy malo: a medida que amplías tu proyecto, puede convertirse en una pesadilla para mantener.

Si desea desacoplar su código sin usar la interfaz en el editor, puede usar un sistema basado en eventos fuertemente tipado (o incluso uno tipado en realidad, pero de nuevo, más difícil de mantener). He basado el mío en esta publicación , que es muy conveniente como punto de partida. Tenga en cuenta la creación de un montón de eventos, ya que podría desencadenar el GC. En cambio, solucioné el problema con un grupo de objetos, pero eso es principalmente porque trabajo con dispositivos móviles.

ADB
fuente
1

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

    }
}
Louis Hong
fuente
0

Utilizo algunas estrategias diferentes para sortear las limitaciones que mencionas.

Uno de los que no veo mencionado es usar un MonoBehaviorque expone el acceso a las diferentes implementaciones de una interfaz determinada. Por ejemplo, tengo una interfaz que define cómo se implementan los Raycasts llamadaIRaycastStrategy

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

Luego lo implemento en diferentes clases con clases como LinecastRaycastStrategyy SphereRaycastStrategye implemento un MonoBehavior RaycastStrategySelectorque expone una sola instancia de cada tipo. Algo así RaycastStrategySelectionBehavior, que se implementa algo como:

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

Si es probable que las implementaciones hagan referencia a las funciones de Unity en sus construcciones (o simplemente para estar del lado seguro, me olvidé de esto al escribir este ejemplo), use Awakepara evitar problemas al llamar a los constructores o hacer referencia a las funciones de Unity en el editor o fuera de la página principal hilo

Esta estrategia tiene algunos inconvenientes, especialmente cuando tiene que administrar muchas implementaciones diferentes que están cambiando rápidamente. Los problemas con la falta de verificación en tiempo de compilación pueden solucionarse utilizando un editor personalizado, ya que el valor de enumeración se puede serializar sin ningún trabajo especial (aunque generalmente lo olvido, ya que mis implementaciones no tienden a ser tan numerosas).

Utilizo esta estrategia debido a la flexibilidad que le brinda al estar basado en MonoBehaviors sin obligarlo a implementar las interfaces usando MonoBehaviors. Esto le brinda "lo mejor de ambos mundos", ya que se puede acceder al Selector utilizando GetComponent, la serialización es manejada por Unity y el Selector puede aplicar la lógica en tiempo de ejecución para cambiar automáticamente las implementaciones en función de ciertos factores.

Selali Adobor
fuente