Olá, Hackernoon! Hoje continuo a discutir o ECS (Entity-Component-System) no desenvolvimento do Unity. No , discuti o que é o ECS (Entity-Component-System), por que o ECS é necessário, como trabalhar com o ECS, as vantagens do ECS no Unity e os contras do ECS no . guia sobre ECS (Entity-Component-System) no Unity: part 1 Unity Nesta parte, vou focar nos erros de iniciantes e boas práticas de uso do ECS no jogo Unity. Também cobrirei um pouco as estruturas para Unity/C#. Erros de iniciante ao usar ECS no jogo Unity Nesta seção, vou falar sobre os erros que pessoas mais experientes notaram no meu código antigo e que me impediram de desenvolver. Também abordarei os erros que muitos iniciantes cometem quando começam a dominar o ECS. Espero que isso ajude você a evitar alguns dos bugs. Herança de componentes e interfaces Herdar componentes e usar interfaces é um erro comum cometido por iniciantes. Então, por que é ruim para o desenvolvimento do Unity? 4 razões: A abstração no nível do componente não oferece absolutamente nenhum benefício em comparação com a abstração do ECS por meio de dados Isso cria limitações para você: você achará mais difícil estender esses componentes e refinar a lógica associada Isso leva a uma situação em que um componente tem sua própria lógica, o que, como lembramos, é uma violação dos princípios do ECS, o que você não deve fazer. Isso leva a uma necessidade irracional de herdar sistemas ou fazer todos os tipos de casos de troca por tipos. Observarei imediatamente que herdar sistemas nem sempre é uma boa ideia, mas não causa nada de terrível, ao contrário da herança de componentes. Portanto, se você deseja herdar componentes, não faça isso. Pense novamente em como você pode resolver o problema de outra maneira. Incapacidade de usar corretamente a abstração do ECS A abstração do ECS é quando você simplesmente coloca dados comuns (que devem ser herdados em OOP) em um componente separado. Você cria um "herdeiro" desse componente simplesmente adicionando um novo componente com os dados necessários e filtrando as entidades com e . Tudo é elementar. Se você tiver alguns dados comuns entre componentes/entidades, quase sempre poderá colocá-los em um componente separado. Quanto mais cedo você fizer isso, melhor. BaseComponent InheritorComponent Ativar/desativar sistemas para alterar a lógica No ECS, o mundo e os sistemas que o processam são estáticos e sempre existem, mas as entidades e seus dados são muito dinâmicos. E se você precisar desabilitar alguma lógica, desabilitar um sistema não é a solução correta. Muitas vezes não há acesso a outros sistemas (e isso é bom). Uma opção muito mais prática é criar alguns componentes de marcadores. Ele dirá que a lógica do sistema não deve funcionar para uma entidade com um marcador. Muitos recém-chegados afirmam: "Mas se eu não tenho entidades para o sistema, por que o sistema deveria funcionar? Não seria melhor desligá-lo para fins de otimização?". Não, não é melhor. Se você admitir que pode não haver entidades, é mais fácil adicionar até o início do método principal do sistema. Na escala do , chamar uma função e comparar dois ints é uma gota no oceano que não afetará seu desempenho de forma alguma. if (entities.Length < 1) return; jogo Unity Os únicos casos legítimos de desabilitação de sistemas em tempo de execução são testes A/B e depuração/teste de sistemas específicos. No entanto, a maioria das estruturas fornece ferramentas para fazer isso não no código, mas na janela do editor. Fazendo da ECS um absoluto Vale lembrar que OOP não é proibido para adeptos do ECS :D Ao trabalhar com ECS, você não deve ficar completamente obcecado com o ECS e transferir tudo para ele porque pode ser contraproducente. Além disso, como mencionei nas : nem todas as estruturas de dados se encaixam bem no ECS. Portanto, é melhor criar uma classe OOP separada nesses casos. desvantagens do ECS Alguns vão além e criam alguns elementos do projeto (por exemplo, interface do usuário) não de acordo com o ECS, mas um pouco à parte de qualquer outra maneira conveniente e, em seguida, conectam-se ao ECS por alguma ponte. Além disso, toda a lógica adicional (carregar configurações, trabalhar diretamente com a rede, salvar em arquivo) é mais fácil de fazer OOP e trabalhar com ela diretamente do sistema desejado. desenvolvedores do Unity Você deve escolher o que fazer com base no bom senso. O ECS deve ajudar o desenvolvimento, não impedi-lo. Tentando portar o código existente para o ECS sem nenhuma alteração Muitas vezes, os iniciantes tentam transferir seu código existente literalmente para o ECS. Essa não é uma boa ideia porque as abordagens do ECS para escrever código diferem dos padrões de arquitetura tradicionais. O resultado dessa portabilidade geralmente é uma bagunça do ECS e um código final muito ruim. Se você precisar portar o código antigo para o ECS, a melhor opção é escrever a mesma lógica do zero no ECS. Tudo o que você precisa fazer é usar seu conhecimento e o código existente como um guia. Usando Delegados/Callbacks ou lógica reativa em sistemas No ECS, pode ser perigoso pegar alguma lógica dos sistemas e armazená-la em um componente para uso posterior ou reagir instantaneamente a alguma mudança (por exemplo, um sistema reagindo à adição de um componente em outro sistema). Ele não apenas adiciona interconectividade desnecessária aos sistemas (eles se tornam fortemente dependentes de chamadas externas). Ele também quebra nosso belo pipeline de processamento de dados adicionando lógica sobre a qual temos pouco controle sobre a chamada. Organizando arquivos em pastas por tipo Quando você começa a trabalhar com o ECS, primeiro deseja colocar novos arquivos por tipo: componentes na pasta Componentes e sistemas na pasta Sistemas. Mas com a experiência, percebo que essa forma de classificação está longe de ser eficiente. Não é fácil navegar com ele para entender quais componentes estão relacionados a quais sistemas. A melhor opção é classificar por recursos quando tudo relacionado a um determinado recurso estiver em uma pasta (talvez com uma hierarquia interna de componentes/sistemas). Ou seja, todos os componentes e sistemas relacionados à saúde e danos estarão na pasta Saúde. Isso permitirá que alguém olhe para a pasta para entender o contexto básico de dados dos sistemas nela contidos e facilitará a navegação no projeto. Práticas recomendadas para usar o ECS no jogo Unity Acima, você provavelmente leu sobre exatamente o que NÃO fazer ao desenvolver jogos Unity no ECS. Agora vamos falar sobre práticas úteis e dicas sobre o que você pode fazer. Marcando entidades com componentes de marcador No ECS, existe um conceito como componente de tag. É um componente sem campo que executa apenas o papel de marcação de entidade. Você pode pensar nisso como um sinalizador booleano em uma classe: ele está presente (verdadeiro) ou não está presente (falso). Por exemplo, temos mil unidades, uma das quais o jogador controla. Você pode marcá-lo com um componente vazio. Isso permitirá que você obtenha apenas as unidades do jogador no filtro, além de entender se está trabalhando com uma unidade regular ou controlada pelo jogador quando passar por todas as unidades de uma vez. PlayerMarker Minimizando os lugares onde o componente muda Quanto menos lugares onde um componente mudar, melhor. Em geral, isso é apenas seguir o princípio benéfico de Não se repita (recomendo a todos). Há muitas vantagens em praticar isso: Ele permite que você entenda melhor o processo de alteração de dados em seu projeto e simplifica a depuração se algo der errado Ao atualizar a lógica das alterações de dados, você precisará atualizar menos código, de preferência apenas um local Há simplesmente menos chance de permitir que um bug de dados aconteça na hora Por exemplo, ao invés de mudar o HealthComponent em todo sistema que tem dano, seria melhor criar um DamageSystem cujo objetivo é causar dano a entidades com um HealthComponent. Esquecendo o postfix do componente O postfix do componente é muito útil para iniciantes porque os lembra que "apenas os dados estão aqui". Mas, com o tempo, a necessidade de lembrar desaparece e o código permanece difícil de entender com o componente onipresente. É por isso que gostaria de aconselhar: você pode esquecer com segurança o postfix Component. Ele não fornece nada de útil, exceto, talvez, alguma simplificação da pesquisa no IntelliSense. É apenas uma dica e talvez até uma questão de gosto, então depende de você como lidar com isso :) Por exemplo, torna-se apenas e o código torna-se um pouco mais legível . Nesse caso, permanece inalterado porque o postfix carrega informações úteis nesse caso, observando que não é um componente simples. HealthComponent Health entity.Has<Health>() PlayerMarker Reatividade atrasada e componentes de quadro único A reatividade no ECS pode ser prejudicial. Mas o que fazer quando a reatividade é necessária? A resposta é a reatividade retardada. A reatividade atrasada é quando ao invés de chamar a lógica diretamente na hora do evento, você cria dados que o evento aconteceu, e todos irão simplesmente reagir ao evento na hora que quiserem. Posso fazer uma analogia com o Dirty Flag em OOP, onde qualquer um pode declarar um evento , mas a lógica reagirá a esse evento quando achar adequado. SetDirty(true) No ECS, você simplesmente cria um componente com ou sem dados (você pode apenas adicionar um sinalizador booleano a um componente existente) que o sistema processará quando chegar a hora. Não é incomum que tais componentes existam no mundo para apenas um quadro alertar todos os sistemas, mas não repetir a lógica no próximo quadro. A remoção pode ser realizada pelo sistema que gera o evento ou por algum sistema separado que removerá todos os componentes do tipo X onde você desejar. Por exemplo, você tem um . Para dizer quanto dano causar, você declara com a quantidade de dano e o adiciona à entidade que deve receber o dano. O percorre todas as entidades com e , danifica a entidade, remove o e cria um que informa todos os sistemas após o da entidade danificada. No final do quadro, o sistema individual exclui o para que os sistemas não processem esse marcador novamente no próximo quadro. DamageSystem MakeDamageComponent DamageSystem HealthComponent MakeDamageComponent MakeDamageComponent DamagedEntityMarker DamageSystem DamagedEntityMarker Requests/Events como API para sistemas Desenvolvendo a ideia de componentes de quadro único, podemos usá-los para expressar uma espécie de API para sistemas. Devemos distinguir componentes de solicitação para solicitações externas e componentes de evento para notificar a todos sobre o evento. O próprio sistema pode controlar o ciclo de vida de ambos os componentes. É possível excluir solicitações imediatamente após processar e limpar eventos antes de iniciar novos eventos. Cabe a você como exatamente nomeá-los e se deve adicionar o postfix Requests/Events. Por exemplo, você tem o do parágrafo anterior. Você pode expressar uma solicitação de dano a ele usando o componente e usar o componente para notificar outros sistemas. A lógica dentro do sistema é a seguinte: limpe todos do último quadro, danifique todas as entidades com uma solicitação, remova a solicitação e adicione o componente . DamageSystem MakeDamageRequest DamagedEntityEvent DamagedEntityEvent DamagedEntityEvent Armazenar uma referência a outra entidade dentro de um componente Você provavelmente tem uma pergunta "Como construir links entre entidades no ECS? É necessário marcar entidades com componentes e depois procurá-los em um loop?". Claro que não. Tudo é muito mais simples e corriqueiro: apenas salvamos a referência. A única diferença é que a referência não é a um componente de outra entidade na qual estamos interessados, mas à própria entidade. Então, você adiciona um campo com a entidade (ou qualquer forma que sua estrutura armazene entidades) ao componente. Antes de usá-lo, verifique se a entidade está viva e possui o componente desejado. Então você pega esse componente e trabalha com ele como quiser. Por exemplo, você pode fazer não diretamente na entidade, mas executá-lo como um evento de entidade separado com referência à entidade de destino. Para fazer isso, adicione o campo a e refaça o . Agora ele deve passar por todas as requisições, verificar se o alvo é uma entidade viva com um , pegar o e causar dano. MakeDamageRequest Entity target MakeDamageRequest DamageSystem HealthComponent HealthComponent A execução agora também parecerá diferente. Em vez de adicionar o componente diretamente à entidade, você cria uma nova entidade com e especifica o . Dessa forma, em detrimento da conveniência de filtragem, você pode acionar vários eventos de dano diferentes para uma única entidade . MakeDamageRequest MakeDamageRequest target target Movendo a lógica repetitiva para StaticUtils/Extensions Com o tempo, você começa a perceber que está executando a mesma lógica em sistemas diferentes. Normalmente, é sinal de que está na hora de fazer um novo sistema :D Mas acontece que a lógica repetida é secundária, relacionada a um ou dois componentes/entidades específicos, e seu resultado é utilizado para finalidades diferentes. Digamos, uma interpretação especial de dados em um componente. Alguns desenvolvedores permitem declarar essa lógica adicional diretamente no componente (na forma de getters, por exemplo). Mas para evitar violar o ECS, sugiro outra opção: utilitários estáticos (ou extensões em C#) que chamamos de sistemas. Por exemplo, temos um . Dentro dela, está localizada a cor do time. A verificação de que duas entidades pertencem à mesma equipe pode ser necessária em vários sistemas. Então criamos uma classe estática e um método nela , descrevendo a lógica recorrente de comparar equipes para duas entidades. InTeamComponent TeamUtils IsInSameTeam(Entity, Entity) Agrupamento de sistemas por momento de execução Como você já sabe, a ordem em que os sistemas são chamados é crítica no ECS. Portanto, é conveniente agrupar os sistemas no nível superior pela ordem em que são chamados em um quadro. Por exemplo, os primeiros sistemas a serem chamados em um quadro podem ser todos os sistemas relacionados à entrada. Eles coletarão informações do usuário e as prepararão no formato ECS. O segundo da fila será um grupo de sistemas com lógica de jogo, que interpretará os dados de entrada à sua maneira e atualizará o mundo ECS. E por último, podemos ter um grupo de sistemas responsáveis pela renderização ou apenas várias coisas adicionais que devem ser chamadas depois de toda a lógica do jogo. Separando os recursos principais em montagens separadas Essa abordagem separará os recursos uns dos outros e controlará suas dependências. Em um mundo ideal, eles não deveriam se sobrepor. A ordem entre os recursos não deve ser importante. Também deve haver um Core Assembly onde os componentes que todos os recursos precisam para funcionar devem estar localizados. Devo dividir componentes/sistemas em partes menores no ECS? Esta questão é muito interessante, pois os desenvolvedores do Unity com diferentes experiências têm opiniões diferentes sobre ela. Mas tentarei cobrir ambas as respostas para ajudá-lo a entender suas necessidades. Sim, é sempre necessário dividir os componentes Essa abordagem na organização do ECS pode ser chamada de atômica. O grau superior dessa abordagem é que cada componente tem apenas um campo. Isso nos permitirá atingir o apogeu da combinatória do projeto e eliminar a necessidade de refatoração em nome da abstração ECS. Não podemos mais pensar em “como combinar entidades com propriedade X”. Desvantagens de sempre dividir componentes no ECS: O número de classes e arquivos aumentará, o que pode causar confusão em grandes projetos se você não prestar a devida atenção à organização do projeto O número de componentes (em geral ou por entidade) pode afetar o desempenho do seu framework É mais difícil entender o que é uma entidade por um monte de propriedades (que podem ser resolvidas por um marcador com um nome normal) Agora vamos para a segunda opinião. Não, você deve dividir os componentes somente quando necessário Esse princípio é chamado de "Não divida antes do tempo". Este é o princípio que eu pessoalmente sigo. Quando você precisa dividir dados, a métrica é simples: esses dados são usados/planejados para serem usados em algum outro lugar além deste componente? Se não, não há razão para gastar tempo com isso. Você pode adotar uma abordagem semelhante para particionar a lógica em sistemas. Desvantagens de dividir componentes no ECS somente quando necessário: O tempo ainda terá que ser gasto na abstração do ECS Menos liberdade para projetar entidades Cabe a você escolher o seu lado :) Estruturas para Unity/C# Se você é iniciante, presumo que esteja pensando primeiro em aprender o Unity DOTS. Mas eu quero avisá-lo. O Unity DOTS não é uma boa opção para iniciantes porque é enorme e complexo. Não há tanta documentação e pessoas experientes quanto gostaríamos. Além disso, não é bem compatível com o código Unity antigo (tudo isso pode mudar após a publicação deste artigo). Se você gosta do Unity Editor e já está acostumado a pendurar componentes diretamente no GameObjects, o é sua melhor escolha. Ele oferece API simples, forte integração com o Unity Editor (pode funcionar fora do Unity) e trabalho conveniente com o MonoBehaviour. É simples e conveniente. Tudo o que você precisa para trabalhar com o Unity está nele. Basta instalá-lo e trabalhar com ele. Sua principal desvantagem é que requer um pago para trabalhar totalmente no Unity. Morpeh Odin Inspector Entitas e DOTS (Unity ECS): você deve dominá-los? E agora uma breve revisão das estruturas Unity/C# que conheci pessoalmente e quais prós/contras notei. Vale ressaltar que tudo descrito abaixo pode mudar desde a publicação deste artigo, então é melhor verificar você mesmo os frameworks. Entitas É a estrutura ECS mais antiga para Unity/C# e ainda é a mais popular. É o visto com mais frequência em anúncios de emprego na ECS. Entitas prós: Ótimo WorldViewer no editor Unity Estilo de código fluente (graças à geração de código) Boa documentação comunidade muito grande Muitos projetos de sucesso nele Suporte obrigatório do desenvolvedor (graças ao AssetStore) Pode ser usado fora do Unity em C# puro Entidades contras: Baixo desempenho em relação a outros frameworks ECS (mas ainda melhor que MonoBehaviour) Muitas alocações, que afetam negativamente o GC Requer geração de código para cada alteração na estrutura do componente Em grandes projetos, a API fica muito inchada devido à geração de código A versão Github não permite chamar a geração de código em erros de compilação, enquanto a versão AssetStore permite Você deve dominá-lo, pelo menos em um nível básico. DOTS (Unidade ECS) Acho que dispensa apresentações. O DOTS não é uma estrutura, mas uma plataforma completa (pilha de tecnologia) com o Unity ECS como uma estrutura interna. Gostaria de salientar que o seguinte é uma experiência um tanto desatualizada. Admito que um DOTS atualizado poderia eliminar os problemas descritos abaixo. Pontos positivos (Unity ECS): Uma plataforma completa para desenvolvimento em ECS Construído por desenvolvedores de mecanismos com a integração mais estreita possível com o editor Suporta Jobs e Burst Juntamente com Jobs e Burst, atinge desempenho máximo entre os frameworks ECS. Possui uma excelente biblioteca de rede com previsões NetCode Mecanismo de subcenas Contras do DOTS (Unity ECS): Trabalho em andamento, que leva à quebra de alterações de código antigas e novos bugs Documentação fraca, que muitas vezes não acompanha as mudanças, tem que ler o código fonte Requer escrever mais código técnico do que análogos O código é difícil de ler e longe de ser conciso Não funciona mais rápido do que soluções de código aberto sem Burst/Jobs (e, em alguns casos, mais lento) Não se encaixa bem com o código Unity antigo O DOTS é essencialmente um tempo de execução e nem todas as funcionalidades antigas foram transferidas para o DOTS. Isso limita as possibilidades de uso. Conclusão sobre ECS em Unity Como você deve ter notado pela grande lista de desvantagens, o ECS não é uma bala de prata. Essa solução arquitetônica, como qualquer outra, tem suas vantagens e desvantagens que você terá que enfrentar se optar por desenvolver usando esse padrão arquitetural. Portanto, a escolha de usar o ECS em seus projetos ou não é inteiramente sua. Eu recomendo fortemente que você pelo menos tente fazer um pequeno projeto no ECS para entender se você gosta dessa abordagem ou não. Também recomendo que você dê uma olhada neste , onde há respostas para muitas perguntas relacionadas ao ECS. Você encontrará links para relatórios, uma lista de frameworks para diferentes linguagens, além de exemplos de jogos e programas que usam ECS. repositório Do meu ponto de vista pessoal, o ECS parece uma ótima opção para criar jogos Unity. Desenvolver nele, para mim pessoalmente, é um prazer: você está desenvolvendo um jogo, não tentando descobrir como integrar um novo código a um sistema antigo sem quebrar nada. Vale lembrar que a usabilidade do desenvolvimento ECS é muito afetada pela escolha do framework, então experimente diferentes opções e escolha com cuidado. Com base na minha experiência, tendo a pensar que o ECS (ou sua variação) é o futuro do desenvolvimento de jogos interativos. E não apenas porque (e talvez até a Epic) o escolheu como seu foco principal, mas simplesmente porque o ECS tem vantagens vantajosas no contexto do desenvolvimento de jogos. a Unity Technologies E, em geral, é uma abordagem prática que parece estranha no início, mas compensa a longo prazo.