Como evitar o acoplamento rígido de scripts no Unity?

24

Há algum tempo, comecei a trabalhar com o Unity e ainda luto com a questão de scripts fortemente acoplados. Como posso estruturar meu código para evitar esse problema?

Por exemplo:

Eu quero ter sistemas de saúde e morte em scripts separados. Eu também quero ter diferentes scripts de caminhada intercambiáveis ​​que me permitam mudar a maneira como o personagem do jogador é movido (controles inerciais baseados em física, como em Mario, versus controles apertados e contorcidos, como em Super Meat Boy). O script Health precisa conter uma referência ao script Death, para que ele possa acionar o método Die () quando a saúde do jogador atingir 0. O script Death deve conter alguma referência ao script de caminhada usado, para desativar a caminhada na morte (I estou cansado de zumbis).

Normalmente , eu criaria interfaces como IWalking, IHealthe IDeath, para poder alterar esses elementos por um capricho, sem interromper o restante do meu código. Eu gostaria que eles fossem configurados por um script separado no objeto player, digamos PlayerScriptDependancyInjector. Talvez esse script teria públicas IWalking, IHealthe IDeathatributos, de modo que as dependências podem ser definidos pelo designer nível do inspetor, arrastando e soltando scripts apropriados.

Isso me permitiria simplesmente adicionar comportamentos aos objetos do jogo facilmente e não me preocupar com dependências codificadas.

O problema na unidade

O problema é que, no Unity, não posso expor interfaces no inspetor, e se eu escrever meus próprios inspetores, as referências não serão serializadas e é muito trabalho desnecessário. É por isso que fiquei escrevendo código fortemente acoplado. Meu Deathscript expõe uma referência a um InertiveWalkingscript. Mas se eu decidir que quero que o personagem do jogador controle firmemente, não posso simplesmente arrastar e soltar o TightWalkingscript, preciso alterá-lo Death. Isso é péssimo. Eu posso lidar com isso, mas minha alma chora toda vez que faço algo assim.

Qual é a alternativa preferida para interfaces no Unity? Como corrijo esse problema? Eu encontrei isso , mas ele me diz o que eu já sei, e não me diz como fazer isso no Unity! Isso também discute o que deve ser feito, não como, não aborda a questão do forte acoplamento entre os scripts.

Em suma, acho que eles foram escritos para pessoas que vieram para o Unity com experiência em design de jogos que apenas aprendem a codificar, e há muito poucos recursos no Unity para desenvolvedores regulares. Existe alguma maneira padrão de estruturar seu código no Unity ou preciso descobrir meu próprio método?

KL
fonte
11
The problem is that in Unity I can't expose interfaces in the inspectorAcho que não entendo o que você quer dizer com "expor a interface no inspetor" porque meu primeiro pensamento foi "por que não?"
Jhocking
@jhocking pergunte aos desenvolvedores da unidade. Você simplesmente não vê isso no inspetor. Ele simplesmente não aparece lá se você o definir como um atributo público, e todos os outros atributos o fazem.
KL
11
ohhh, você pretende usar a interface como o tipo da variável, não apenas referenciando um objeto com essa interface.
Jhocking
11
Você leu o artigo original do ECS? cowboyprogramming.com/2007/01/05/evolve-your-heirachy E o design do SOLID? pt.wikipedia.org/wiki/SOLID_%28object-oriented_design%29
jzx 12/02/15
11
@jzx Conheço e uso o design do SOLID e sou um grande fã dos livros de Robert C. Martins, mas essa questão é sobre como fazer isso no Unity. E não, eu não li esse artigo, lê-lo agora, graças :)
KL

Respostas:

14

Existem algumas maneiras de trabalhar para evitar um acoplamento rígido de scripts. Internal to Unity é uma função SendMessage que, quando direcionada para o GameObject de um Monobehaviour, envia essa mensagem para tudo no objeto do jogo. Portanto, você pode ter algo parecido com isto em seu objeto de saúde:

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

Isso é realmente simplificado, mas deve mostrar exatamente o que estou chegando. A propriedade que lança a mensagem como se fosse um evento. Seu script de morte interceptaria essa mensagem (e se não houver nada para interceptá-la, SendMessageOptions.DontRequireReceiver garantirá que você não receba uma exceção) e executará ações com base no novo valor de integridade depois que ela for definida. Igual a:

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

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

Não há garantia de ordem nisso, no entanto. Na minha experiência, ele sempre vai do script mais alto de um GameObject para a maioria dos scripts inferiores, mas eu não apostaria em nada que você não possa observar por si mesmo.

Existem outras soluções que você pode investigar, que está criando uma função GameObject personalizada que obtém Componentes e retorna apenas os componentes que possuem uma interface específica em vez de Tipo, levaria um pouco mais de tempo, mas poderia servir bem a seus propósitos.

Além disso, você pode escrever um editor personalizado que verifique se um objeto está sendo atribuído a uma posição no editor para essa interface antes de efetivamente confirmar a atribuição (nesse caso, você atribuiria o script normalmente, permitindo que ele fosse serializado, mas gerando um erro se não usasse a interface correta e negasse a atribuição). Eu já fiz os dois antes e posso produzir esse código, mas precisará de algum tempo para encontrá-lo primeiro.

Brett Satoru
fonte
5
SendMessage () resolve essa situação e eu uso esse comando em muitas situações, mas um sistema de mensagens não segmentado como esse é menos eficiente do que ter uma referência direta ao destinatário. Você deve ter cuidado com esse comando para toda a comunicação de rotina entre objetos.
Jhocking
2
Sou um fã pessoal do uso de eventos e delegados onde os objetos Component conhecem os sistemas principais mais internos, mas os sistemas principais não sabem nada sobre os componentes. Mas eu não tinha cem por cento de certeza de que era assim que eles queriam que suas respostas fossem elaboradas. Então, eu fui com algo que respondia como obter scripts desacoplados completamente.
Brett Satoru
11
Também acredito que existem alguns plug-ins disponíveis no repositório de ativos da unidade que podem expor interfaces e outras construções de programação não suportadas pelo Unity Inspector; talvez valha a pena fazer uma pesquisa rápida.
Matthew Pigram
7

Eu pessoalmente NUNCA uso o SendMessage. Ainda existe uma dependência entre seus componentes com o SendMessage, é apenas muito pouco exibido e fácil de quebrar. O uso de interfaces e / ou delegados realmente elimina a necessidade de usar o SendMessage (o que é mais lento, embora isso não deva ser uma preocupação até que seja necessário).

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

Use as coisas desse cara. Ele fornece um monte de coisas de editor, como interfaces expostas e representantes. Existem montes de coisas por aí assim, ou você pode até fazer o seu próprio. Faça o que fizer, NÃO comprometa seu design devido à falta de suporte ao script do Unity. Essas coisas devem estar no Unity por padrão.

Se essa opção não for uma opção, arraste e solte classes de comportamento individual e as converta dinamicamente na interface necessária. É mais trabalho e menos elegante, mas faz o trabalho.

Ben
fonte
2
Pessoalmente, desconfio de ficar muito dependente de plugins e bibliotecas para coisas de baixo nível como essa. Provavelmente estou sendo um pouco paranóico, mas parece muito risco de que meu projeto seja interrompido porque o código deles é interrompido após uma atualização do Unity.
Jhocking
Eu estava usando essa estrutura e finalmente parei, porque eu tinha o motivo pelo qual o @jhocking menciona o roer na minha mente (e não ajudou que, de uma atualização para outra, passou de um auxiliar de editor para um sistema que incluía tudo mas a pia da cozinha ao quebrar compatibilidade com versões anteriores [e retirar um recurso bastante útil ou dois])
Selali Adobor
6

Uma abordagem específica do Unity que me ocorre seria obter inicialmente GetComponents<MonoBehaviour>()uma lista de scripts e depois converter esses scripts em variáveis ​​privadas para as interfaces específicas. Você deseja fazer isso em Start () no script do injetor de dependência. Algo como:

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

De fato, com esse método, você pode até fazer com que os componentes individuais digam ao script de injeção de dependência quais são suas dependências, usando o comando typeof () e o tipo 'Type' . Em outras palavras, os componentes fornecem ao injetor de dependência uma lista de tipos e, em seguida, o injetor de dependência retorna uma lista de objetos desses tipos.


Como alternativa, você pode apenas usar classes abstratas em vez de interfaces. Uma classe abstrata pode realizar praticamente o mesmo objetivo que uma interface, e você pode fazer referência a isso no Inspetor.

jhocking
fonte
Não posso herdar de várias classes enquanto posso implementar várias interfaces. Isso pode ser um problema, mas acho que é muito melhor que nada :) Obrigado pela resposta!
194 KL
quanto à edição - depois de ter a lista de scripts, como meu código sabe qual script deve ser cantado para qual interface?
KL
11
editado para explicar isso também
jhocking
11
No Unity 5, você pode usar GetComponent<IMyInterface>()e GetComponents<IMyInterface>()diretamente, o que retorna os componentes que o implementam.
S. Tarık Çetin
5

Pela minha própria experiência, o game dev tradicionalmente envolve uma abordagem mais pragmática do que o industrial (com menos camadas de abstração). Em parte porque você deseja favorecer o desempenho em vez da capacidade de manutenção e também porque é menos provável que você reutilize seu código em outros contextos (geralmente há um forte acoplamento intrínseco entre os gráficos e a lógica do jogo)

Entendo perfeitamente sua preocupação, principalmente porque a preocupação com o desempenho é menos crítica em dispositivos recentes, mas sinto que você precisará desenvolver suas próprias soluções se quiser aplicar as melhores práticas industriais de POO ao desenvolvimento de jogos do Unity (como injeção de dependência, etc ...).

sdabet
fonte
2

Dê uma olhada no IUnified ( http://u3d.as/content/wounded-wolf/iunified/5H1 ), que permite expor interfaces no editor, como você mencionou. Há um pouco mais de conexão a fazer em comparação com a declaração de variável normal, isso é tudo.

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

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

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

Além disso, como outras respostas mencionam, o uso do SendMessage não remove o acoplamento. Em vez disso, não faz erros em tempo de compilação. Muito ruim - à medida que você aumenta seu projeto, pode se tornar um pesadelo para manter.

Se você ainda deseja desacoplar seu código sem usar a interface no editor, pode usar um sistema baseado em evento com forte tipagem (ou mesmo um com baixa digitação, na verdade, mas novamente, mais difícil de manter). Baseei o meu neste post , que é super conveniente como ponto de partida. Lembre-se de criar vários eventos, pois isso pode acionar o GC. Em vez disso, resolvi o problema com um pool de objetos, mas isso ocorre principalmente porque trabalho com dispositivos móveis.

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

    }
}
Louis Hong
fonte
0

Eu uso algumas estratégias diferentes para contornar as limitações mencionadas.

Um dos que não vejo mencionado é o uso de um MonoBehaviorque expõe o acesso às diferentes implementações de uma determinada interface. Por exemplo, eu tenho uma interface que define como o Raycasts é implementado chamadoIRaycastStrategy

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

Em seguida, aplicá-lo em diferentes classes com classes de gosto LinecastRaycastStrategye SphereRaycastStrategye implementar um MonoBehavior RaycastStrategySelectorque expõe uma única instância de cada tipo. Algo como RaycastStrategySelectionBehavior, que é implementado 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;
        }
    }
}

Se é provável que as implementações façam referência às funções do Unity em suas construções (ou apenas por segurança, eu esqueci isso ao digitar este exemplo) Awakepara evitar problemas com a chamada de construtores ou a referência de funções do Unity no editor ou fora do principal fio

Essa estratégia tem algumas desvantagens, especialmente quando você precisa gerenciar muitas implementações diferentes que estão mudando rapidamente. Problemas com a falta de verificação em tempo de compilação podem ser ajudados usando um editor personalizado, já que o valor de enum pode ser serializado sem nenhum trabalho especial (embora eu geralmente renuncie a isso, pois minhas implementações não costumam ser tão numerosas).

Eu uso essa estratégia devido à flexibilidade que ela oferece, baseando-se no MonoBehaviors, sem forçá-lo a implementar as interfaces usando o MonoBehaviors. Isso fornece a você "o ​​melhor dos dois mundos", já que o Selector pode ser acessado usando GetComponent, a serialização é toda gerenciada pelo Unity e o Selector pode aplicar lógica em tempo de execução para alternar automaticamente as implementações com base em certos fatores.

Selali Adobor
fonte