Olá, Hackernoon! O próximo tópico que escolhi é ECS (Entity-Component-System) no . Dividi em duas partes para te ajudar a perceber todas as informações de forma mais acessível. desenvolvimento do Unity Vou contar tudo o que sei sobre o Sistema de Componentes de Entidade e tentar dissipar vários preconceitos sobre essa abordagem. Você encontrará muitas palavras sobre as vantagens e desvantagens do ECS, as peculiaridades dessa abordagem, como fazer amizade com ele, possíveis armadilhas e práticas úteis. Também examinarei brevemente as estruturas ECS para Unity/C#. Este artigo será bom para quem quer/começa a se familiarizar com o ECS. As pessoas que experimentaram o ECS, espero, também serão capazes de enfatizar algo novo para si mesmas. Se você criar jogos em qualquer linguagem diferente de C#, ainda poderá achar este artigo útil. Não haverá amostras de código e histórico do padrão, apenas minha experiência, raciocínio e observações :) O que é ECS (Sistema-Componente-Entidade)? Entity-Component-System é um padrão de arquitetura criado especificamente para o desenvolvimento de jogos. É perfeito para descrever um mundo virtual dinâmico. Por suas peculiaridades, alguns o consideram quase um novo paradigma de programação. ECS é um princípio absoluto de Composição Sobre Herança. Pode ser um exemplo particular de Design Orientado a Dados (DOD), mas depende da interpretação do padrão por uma implementação específica. Vamos decifrar o nome desse padrão: - um objeto maximamente abstrato. É um container condicional para propriedades que definem o que será esta Entidade. Muitas vezes é representado como um identificador para acessar dados. Entidade - uma propriedade com dados de objeto. Os componentes no ECS devem conter apenas dados puros sem uma única gota de lógica. No entanto, alguns desenvolvedores permitem vários getters e setters em componentes. Ainda assim, acho que os utilitários estáticos são mais adequados para esses propósitos. Componente - a lógica do processamento de dados. Os sistemas no ECS não devem conter dados, apenas lógica de processamento de dados. Mas, novamente, alguns desenvolvedores permitem definir algum comportamento auxiliar do sistema, por exemplo, constantes ou vários serviços auxiliares. Sistema Como você já percebeu acima: o ECS separa estritamente os dados da lógica. O comportamento de um objeto é determinado não por interfaces/contratos/API pública, como estamos acostumados na programação clássica orientada a objetos (OOP), mas por propriedades atribuídas ao objeto com dados + lógica de processamento existentes separadamente. No ECS, os dados definem tudo. Esta é a principal propriedade que o distingue de outras abordagens de desenvolvimento: tudo são dados. Propriedades, características e eventos de objetos são apenas dados no mundo ECS. A lógica é simplesmente o processamento do pipeline de todos esses dados. Por que o ECS é necessário? Você provavelmente já tem uma pergunta: "Por que preciso do ECS? Para que serve?". E para ajudá-lo a decidir se deseja continuar lendo este artigo, direi por que gosto do ECS. Pessoalmente, adoro o ECS porque: Com o ECS, você apenas senta e em vez de brigar com a arquitetura do projeto. Não há necessidade de construir hierarquias grandes e bonitas, pensar em muitas conexões e se preocupar com "X não deveria saber sobre Y". Ao mesmo tempo, os princípios do ECS protegem você (não 100%, é claro) da situação desesperadora causada por uma arquitetura ruim quando o desenvolvimento do projeto se torna muito doloroso. E mesmo que algo dê errado, a refatoração no ECS não é um problema. E isso , na minha opinião, é o que há de melhor no ECS. faz um jogo no Unity O código no ECS é simples e claro. Você não precisa rastrear as chamadas entre as classes para entender o que um determinado sistema faz. Você pode ver tudo de uma vez, especialmente se dividir um recurso em sistemas, sistemas em métodos e não complicar demais o código. Além disso, o ECS simplifica muito a criação de perfis. Você pode ver imediatamente qual lógica (sistema) leva quanto tempo de quadro. Você não precisa procurar a origem dos atrasos na profundidade das chamadas. É fácil manipular a lógica. Adicionar nova lógica é praticamente indolor. Você apenas insere um novo sistema no lugar certo sem medo de afetar diretamente o restante do código (deve-se notar que a influência indireta através de dados é possível). Você pode usar lógica comum (sistemas) entre cliente e servidor sem problemas, mantendo os dados (componentes) usados. Você pode facilmente reescrever sistemas, substituindo sistemas antigos por refatorados sem impactar o resto do código. Se você não gostar do resultado, basta ligar o sistema antigo novamente. O mesmo mecanismo pode facilmente organizar testes A/B. Tudo gira em torno de dados. Acontece que é extremamente conveniente. Ao manipular dados diretamente sobre entidades, as possibilidades de combinatória são enormes. Você pode usar dados para moldar uma entidade em qualquer coisa. E suponha que a estrutura ofereça ferramentas para visualizar dados em entidades. Nesse caso, você pode examinar os dados e sua dinâmica em qualquer entidade sem executar um depurador para procurar na memória. Agora você me entende? Como trabalhar com ECS? Aqui vou descrever em palavras simples como funciona o processo de desenvolvimento com ECS no exemplo mais simples. Farei isso da forma mais abstrata possível, sem fazer referência a uma linguagem de programação. Se você já tem alguma experiência com ECS, pode ir direto para a próxima seção :) criar um objeto que se move na direção de um determinado vetor de movimento. Tarefa: Primeiro, vamos definir os dados que precisamos para o nosso trabalho. Para nossa tarefa, precisaremos da posição do objeto e do vetor de movimento fornecido. Na linguagem ECS, serão: PositionComponent para armazenar o vetor de posição MovementComponent para o vetor de movimento O próximo passo é descrever a lógica. Vamos criar um . No método principal do sistema, dependendo da implementação, pode ser ou outro. Você obtém todas as entidades no ECS que possuem e . Como exatamente isso pode ser feito depende da estrutura, mas geralmente parece um tipo de consulta SQL como . MovementSystem Run()/Execute()/Update() PositionComponent MovementComponent GetAllEntities().With<PositionComponent>().With<MovementComponent>() E, finalmente, você simplesmente cria uma entidade (mesmo dez peças) com nossos dois componentes e define o vetor de movimento diferente de zero. Agora, a cada chamada de (independentemente de onde e quando o chamamos), nosso objeto mudará de posição na direção do vetor de movimento dado. Tarefa cumprida! :) MovementSystem Freqüentemente, os sistemas são de alguma forma incorporados ao GameLoop do projeto e contraem cada quadro pelo próprio mecanismo. Mas você pode fazer isso manualmente, e de qualquer outra forma, porque é apenas uma chamada de método. Vamos ver quais possibilidades adicionais de desenvolvimento obtivemos além de resolver o problema principal: Qualquer um de nossos outros sistemas pode determinar se um objeto está se movendo simplesmente verificando a presença da propriedade MovementComponent Qualquer outro sistema pode obter o vetor de movimento para suas necessidades Qualquer um de nossos outros sistemas poderá especificar um vetor de movimento para qualquer uma de nossas entidades à vontade Se quisermos, também podemos fazer qualquer outra entidade se mover simplesmente colocando e nela. Isso é muito útil ao . PositionComponent MovementComponent criar jogos Unity Prós do ECS no Unity Nesta seção, discutiremos o que é bom e o que é ruim no ECS. Algumas das características descritas abaixo têm dois lados da moeda. Ambos são benéficos para o desenvolvimento e desconfortáveis, criando limitações que às vezes devem ser contornadas. Primeiro, vamos discutir as vantagens do ECS no Unity. Coesão de código fraca Esta é uma propriedade benéfica para . Ele nos permite refatorar e estender a base de código com relativa facilidade e sem quebrar partes antigas do código. Sempre podemos adicionar um novo comportamento usando dados antigos ao lado sem interferir na lógica antiga de forma alguma. O ECS consegue esse efeito porque os dados expressam todas as interações lógicas na Entidade. Este é um objeto maximamente abstrato sem quaisquer garantias, como alguns objetos em C#/Java. os desenvolvedores de jogos Unity No entanto, você deve se lembrar que no ECS, a ordem das alterações de dados desempenha um papel importante. Eventualmente, isso pode afetar a complexidade da refatoração e quebrar sua lógica antiga ou até mesmo criar bugs de efeito colateral desagradáveis. Modularidade perfeita e testabilidade da lógica Se toda interação for expressa em dados puros, nossa lógica estará sempre completamente desacoplada da fonte de dados. Isso nos permite mover a lógica de projeto para projeto e reutilizá-la (preservando o formato dos dados, é claro), bem como executar a lógica em qualquer dado de entrada para testar sua operação. É mais difícil escrever um código ruim O ECS exige menos da arquitetura porque define a estrutura com a qual é mais difícil criar um design de código realmente ruim. Ao mesmo tempo, como foi dito acima, podemos corrigir o problema de forma relativamente indolor e com impacto mínimo no restante do código, mesmo que ocorra um design de código ruim. O ECS nos permite pensar menos em "como encaixar essa lógica em nossa arquitetura sem quebrar nada" e adicionar novos recursos. Combinatória de propriedades Essa vantagem torna o ECS uma excelente opção para descrever mundos dinâmicos. Imagine só: você pode dar qualquer propriedade (e, portanto, lógica) para qualquer uma de suas entidades sem problemas! Se você deseja que a câmera tenha saúde, pode colocar um na câmera. Ele sofrerá danos (se houver tal sistema). Coloque um em uma entidade e ela imediatamente começará a receber dano de queima se tiver um . Você quer que a casa se mova sob o controle do jogador? Sem problemas, basta usar . HealthComponent InFireComponent HealthComponent PlayerInputListenerComponent Um desenvolvedor experiente dirá: "Hah, a maioria dos padrões de composição sobre herança pode lidar com isso. Como o ECS é melhor?". Minha resposta é: "ECS permite combinar propriedades não apenas em termos de formação de entidade, mas também para criar uma lógica específica ao combinar várias propriedades (componentes) na mesma entidade." Eu nem mencionei a capacidade de adicionar uma lógica totalmente nova para dados antigos sem tocar nos componentes da entidade! É mais fácil impor uma Responsabilidade Única Quando temos a lógica completamente separada dos dados e não vinculada a nenhum objeto/entidade, fica mais fácil controlar o particionamento da lógica por seu propósito, em vez de seu lugar na hierarquia. Cada sistema simplesmente executa alguma tarefa específica exclusiva para ele. Freqüentemente, o código do sistema se parece com uma única chamada de método para muitos componentes do mesmo tipo. Como resultado, o código é mais fácil de ler e perceber. Perfil mais claro Ao criar o perfil, podemos ver qual lógica e quanto tempo de quadro leva. Isso é possível graças a sistemas separados com sua lógica responsável pelo processamento. Não precisamos nos aprofundar na pilha de chamadas para entender o que leva mais tempo. Podemos ver imediatamente o CharMovementSystem culpado. Deve-se observar que essa vantagem depende do dispositivo do framework ECS, pois o próprio framework pode ter sua pilha de chamadas. ECS pode dar um bom aumento de desempenho Muitas pessoas pensam que o bom desempenho é a principal vantagem do ECS (graças à propaganda do Unity). Isso não é bem verdade. A velocidade de execução do código é apenas um bom bônus resultante dos princípios do padrão: dados em um lugar - lógica em outro + SIMD (instrução única, dados múltiplos). E se a estrutura seguir o DOD ao implementar o ECS e atingir uma boa localidade de dados, também obteremos um código mais amigável ao cache, o que deixará seu processador satisfeito. O desempenho final do ECS depende de muitos fatores: como exatamente a estrutura armazena dados, como a estrutura filtra entidades, com que rapidez os sistemas acessam os dados e com que rapidez o código dentro de seus sistemas funciona. No entanto, , o ECS sempre será mais rápido do que a abordagem MonoBehaviour usual, especialmente em grandes quantidades de dados. Mas não se esqueça de que o que importa no desempenho do seu jogo não é tanto o padrão arquitetônico, mas a complexidade algorítmica e o desempenho do código que você escreve. no contexto do desenvolvimento do Unity Paralelização mais fácil do processamento de dados Como a lógica é separada em um processador de dados separado e os dados são, na verdade, uma sequência linear, podemos paralelizar o processamento dentro de um sistema sem problemas. Isso é muito importante se o sistema processar um grande número de entidades simultaneamente e elas não se cruzarem de forma alguma. Você pode ir ainda mais longe e enviar para diferentes threads a lógica que não se sobrepõe aos dados alterados. No entanto, é muito mais difícil de controlar e monitorar. Ainda assim, haverá um gargalo na sincronização com a thread principal para preparar os dados. Além disso, pode acontecer que a sobrecarga de preparação e distribuição de dados entre threads seja maior do que o tempo de execução do código em seus sistemas. Portanto, você precisa avaliar se vale a pena. Dados limpos são muito fáceis de trabalhar Em quase todos os jogos Unity, devemos salvar, carregar ou serializar algo para enviar pela rede. Isso é muito mais fácil quando os dados são separados da lógica. Não há necessidade de pensar: "Como isso deve entrar em dados privados ..." e chamar alguns métodos especiais para serialização adequada. Você apenas salva/carrega os componentes necessários na entidade. Em seguida, o sistema irá completá-lo para o estado desejado, se julgar necessário. Você pode alterar as estruturas do ECS com a frequência que desejar As estruturas do ECS são semelhantes entre si porque os princípios são os mesmos. Um desenvolvedor que reconstruiu seu cérebro para ECS e entendeu bem uma estrutura uma vez pode trabalhar com outra estrutura ECS sem problemas. Aprender a API e as peculiaridades de uma estrutura específica levará apenas tempo. Mas não haverá necessidade de reconstruir sua cabeça para a nova abordagem. Contras do ECS no Unity Como você pode ver, o ECS no Unity tem muitas vantagens valiosas sobre outros padrões. Agora vamos discutir as desvantagens do ECS no Unity. Um limite alto para desenvolvedores Unity experientes Embora o conceito ECS possa ser descrito em uma frase, aprender a usá-lo corretamente pode exigir muita prática. O ECS exige que você esqueça tudo o que sabia sobre design antes: todas as suas hierarquias de herança vertical, que o comportamento de um objeto é determinado por sua interface, que um objeto é algo concreto e imutável, que um objeto pode ter um espaço privado e que a lógica pode ser chamado onde quiser. No ECS, nem tudo é assim. É o contrário do que foi descrito acima. Aqui todos os dados são abertos, todas as entidades são abstratas e muito dinâmicas, suas propriedades estão em um plano e acessíveis a todos, a lógica funciona com base no princípio do transportador e o comportamento das entidades em geral muda instantaneamente com base nos dados. Coesão de código fraca pode ser um problema Suponha que de repente você precise de uma interação próxima entre 2 entidades concretas (por exemplo, um corpo de lagarta e uma torre de tanque). Nesse caso, você enfrenta o problema de que as entidades são abstratas e não pode garantir no nível do compilador que o corpo da lagarta estará na outra extremidade. Isso vai atrapalhar porque os jogos Unity são um lugar onde há muitas interações próximas e você sempre quer ter uma referência direta com garantia de propriedades e comportamento. Você terá que verificar a presença do componente e de alguma forma lidar com sua ausência, acessar o componente da entidade para começar a interagir com ele, etc. Acesse qualquer dado de qualquer lugar O mundo ECS é uma caixa aberta de entidades com dados disponíveis para todos os componentes. Como a fraca coesão de código acima, isso é um pró e um contra do ECS. Por um lado, é extremamente conveniente. Você não precisa descobrir como contornar a estrutura autolimitada criada anteriormente no processo de design ("X não deve saber sobre Y") e divulgar dados anteriormente ocultos para o público para resolver algum problema imediato. Por outro lado, qualquer programador inexperiente tentará alterar os dados de onde não deveriam estar. Mas geralmente o trabalho em equipe envolve confiar no trabalho dos outros, então confie, mas verifique ;) Os sistemas funcionam exclusivamente em fluxo, um após o outro Ao seguir os princípios ECS corretamente, você não deve chamar a lógica de um sistema dentro de outro sistema. Os sistemas não devem estar cientes da existência uns dos outros. Caso contrário, causará coesão de código desnecessária e poderá prejudicar seu projeto. No entanto, essa limitação pode ser inconveniente e, às vezes, levar a várias soluções alternativas que não violam os princípios do ECS. Se você ainda precisa chamar algum código aqui e agora, apenas faça um objeto regular com métodos e coloque-o em um componente, não se torture. Não funciona bem com lógica recursiva Esta desvantagem é consequência da anterior. Devido à falta de capacidade de chamar o código do sistema fora do encadeamento e onde quisermos, o ECS torna quase impossível criar código recursivo fora de qualquer sistema específico. Como solução para essa deficiência (também conhecida como solução alternativa para cumprir os princípios do ECS), só posso propor que você crie uma estrutura/sistema especializado que chamará uma lista específica de sistemas em um loop infinito, desde que uma condição específica seja atendida. Quero dizer, desde que existam entidades com um DoActionComponent. Se você tiver soluções alternativas mais elegantes, ficarei feliz em ler sobre elas nos comentários :) A ordem de execução dos sistemas é crítica No ECS, é crucial entender e controlar como os sistemas alteram seus dados. Muitas vezes, é possível perder o efeito de algum sistema nos dados com os quais estamos trabalhando e acabar com vários efeitos colaterais não planejados. A propósito, eles podem ser complicados de rastrear (que é a próxima desvantagem). No entanto, muitas vezes é possível, ao escrever sistemas, projetá-los de forma que não importe em que ordem os sistemas são invocados. Mais difícil de depurar Este é um ponto bastante controverso, especialmente com IDEs inteligentes modernos. Devido à falta de StackTrace profundo (temos uma lógica em nossos sistemas que não está vinculada à entidade) e à impossibilidade de rastrear como e por quem os dados e o estado da entidade foram alterados, pode ser um desafio encontrar o motivo pelo qual seu sistema de repente não funciona do jeito que foi planejado. Não é fácil entender o que levou a uma determinada chamada, embora alguém tenha adicionado um componente à entidade ou feito um ++ extra. Para resumir, no ECS, sem ferramentas de depuração, é difícil rastrear por que e como os dados nos componentes foram alterados, especialmente quando você tem milhares de entidades e apenas uma problemática. Isso pode ser remediado por ferramentas de depuração que as estruturas podem fornecer. Mas eles podem não estar disponíveis imediatamente e você mesmo terá que escrevê-los ou sofrerá. Péssima opção para estruturas de dados, principalmente hierárquicas Implementar estruturas de dados com ECS é difícil, inconveniente e, na minha opinião, não faz sentido. Não estou dizendo que é impossível (se você se esforçar o suficiente, tudo é possível), mas será um caminho espinhoso sem muitos benefícios no final da estrada, então seja racional em sua escolha. Vou listar alguns problemas que irão interferir ao tentar realizar alguma estrutura de dados no ECS: No ECS, todos os dados são acessíveis de qualquer lugar. Isso pode ser extremamente perigoso para estruturas de dados em que a consistência máxima é necessária. Qualquer "crocodilo" de passagem pode alterar quaisquer dados internos para ignorar sua lógica, quebrando completamente sua estrutura de dados. Se seguirmos honestamente os princípios do ECS, não poderemos invocar a lógica de nossa estrutura de dados aqui e agora, como geralmente é necessário ao trabalhar com eles. No entanto, esse ponto pode ser combatido com extensões/utilizadores estáticos. ECS é representativo de arquiteturas horizontais. Todos os dados nele estão em um plano, quase sempre apenas matrizes unidimensionais de componentes. Isso dificulta se sua estrutura de dados exigir verticalidade/hierarquia. Não é incomum que estruturas de dados exijam referências cruzadas entre elementos (hierarquia). Mas, como você deve se lembrar, tudo gira em torno de uma Entidade maximamente abstrata no ECS. Isso dificulta o trabalho porque não há garantia de um elemento do tipo que precisamos na outra ponta. Como resultado, terá que ser tratado separadamente. A estrutura de dados e seus elementos geralmente não precisam alterar o formato dos dados em tempo de execução, nem precisam de combinatória. Eles são bem rígidos. Cada entidade de estrutura de dados pode acabar tendo apenas um componente. Suponha que você ainda precise de uma estrutura de dados. Nesse caso, recomendo que você o crie como um objeto separado com métodos e, em seguida, coloque esse objeto em seu componente e apenas trabalhe com ele nos sistemas como de costume. Mais arquivos e classes Na , o número de arquivos em um projeto cresce mais rapidamente do que no caso de código semelhante nas abordagens clássicas. Pelo menos porque em vez de 1 classe com dados e lógica, você tem duas classes: componente e sistema (você ainda pode escondê-los em um arquivo). No máximo, se você tornar todos os componentes atômicos (1 componente - 1 campo), haverá muitos, muitos arquivos... abordagem ECS código clichê Essa desvantagem depende fortemente da implementação específica da estrutura ECS. Em alguns frameworks, você precisa escrever muito código técnico. Em outros, o desenvolvedor tentou tornar a API mais simples possível e minimizar o clichê. Mas, se você comparar com outras abordagens, quase sempre há pelo menos uma pequena quantidade de código adicional que você precisa escrever. Quero dizer, declarar componentes, obter um filtro com os componentes necessários, obter entidades dele, obter um componente de uma entidade, etc. Pequena conclusão Este é o fim da 1 parte. Na parte 2, discutirei: Erros de iniciante no ECS Boas práticas em ECS Frameworks para trabalhar com ECS em Unity/C# Se você tiver alguma dúvida, deixe nos comentários!