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
, IHealth
e 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
, IHealth
e IDeath
atributos, 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 Death
script expõe uma referência a um InertiveWalking
script. Mas se eu decidir que quero que o personagem do jogador controle firmemente, não posso simplesmente arrastar e soltar o TightWalking
script, 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?
The problem is that in Unity I can't expose interfaces in the inspector
Acho que não entendo o que você quer dizer com "expor a interface no inspetor" porque meu primeiro pensamento foi "por que não?"Respostas:
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:
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:
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.
fonte
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.
fonte
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: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.
fonte
GetComponent<IMyInterface>()
eGetComponents<IMyInterface>()
diretamente, o que retorna os componentes que o implementam.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 ...).
fonte
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.
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.
fonte
Fonte: http://www.udellgames.com/posts/ultra-useful-unity-snippet-developers-use-interfaces/
fonte
Eu uso algumas estratégias diferentes para contornar as limitações mencionadas.
Um dos que não vejo mencionado é o uso de um
MonoBehavior
que 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
Em seguida, aplicá-lo em diferentes classes com classes de gosto
LinecastRaycastStrategy
eSphereRaycastStrategy
e implementar um MonoBehaviorRaycastStrategySelector
que expõe uma única instância de cada tipo. Algo comoRaycastStrategySelectionBehavior
, que é implementado algo como: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)
Awake
para evitar problemas com a chamada de construtores ou a referência de funções do Unity no editor ou fora do principal fioEssa 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.fonte