Eu tenho feito um jogo RTS simples, que contém centenas de personagens como Crusader Kings 2, no Unity. Para armazená-los, a opção mais fácil seria usar objetos programáveis, mas essa não é uma boa solução, pois você não pode criar novos em tempo de execução.
Então, eu criei uma classe C # chamada "Personagem", que contém todos os dados. Tudo está funcionando bem, mas à medida que o jogo simula, ele cria constantemente novos personagens e mata alguns personagens (conforme os eventos do jogo acontecem). À medida que o jogo simula continuamente, cria milhares de caracteres. Eu adicionei uma verificação simples para garantir que um personagem esteja "vivo" durante o processamento de sua função. Portanto, ajuda no desempenho, mas não consigo remover o "Personagem" se ele estiver morto, porque preciso das informações dele enquanto cria a árvore genealógica.
Uma lista é a melhor maneira de salvar dados para o meu jogo? Ou isso dará problemas quando houver 10000s de um personagem criado? Uma solução possível é fazer outra lista quando a lista atingir uma certa quantidade e mover todos os caracteres mortos neles.
GameObjects
. Na CK2, a quantidade máxima de personagens que eu vi ao mesmo tempo foi quando olhei para a minha quadra e vi de 10 a 15 pessoas lá (apesar de não ter jogado muito longe). Da mesma forma, um exército com 3000 soldados é apenas umGameObject
, exibindo o número "3000".Respostas:
Há três coisas que você deve considerar:
Isso realmente causa um problema de desempenho? 1000s é, bem, não muitos, na verdade. Os computadores modernos são extremamente rápidos e podem lidar com muitas coisas. Monitore quanto tempo o processamento de caracteres está demorando e verifique se realmente causará um problema antes de se preocupar muito com isso.
Fidelidade de caracteres atualmente minimamente ativos. Um erro frequente dos programadores de jogos iniciantes é ficar obcecado em atualizar com precisão caracteres fora da tela da mesma maneira que na tela. Isso é um erro, ninguém se importa. Em vez disso, você precisa criar a impressão de que caracteres fora da tela ainda estão agindo. Ao reduzir a quantidade de caracteres de atualização que estão fora da tela, você pode diminuir drasticamente o tempo de processamento.
Considere Design Orientado a Dados. Em vez de ter objetos de 1000 caracteres e chamar a mesma função para cada um, tenha uma matriz de dados para os 1000 caracteres e tenha um loop de função sobre os 1000 caracteres, atualizando cada um deles. Esse tipo de otimização pode melhorar drasticamente o desempenho.
fonte
Nesta situação, eu sugeriria usar Composition :
Nesse caso, parece que sua
Character
classe se tornou divina e contém todos os detalhes de como um personagem opera em todos os estágios do seu ciclo de vida.Por exemplo, observe que os caracteres mortos ainda são necessários - como são usados nas árvores genealógicas. No entanto, é improvável que todas as informações e funcionalidades de seus personagens vivos ainda sejam necessárias apenas para exibi-las em uma árvore genealógica. Eles podem, por exemplo, simplesmente precisar de nomes, data de nascimento e um ícone de retrato.
A solução é dividir as partes separadas do seu
Character
em subclasses, das quais eleCharacter
possui uma instância. Por exemplo:CharacterInfo
pode ser uma estrutura de dados simples com o nome, data de nascimento, data da morte e facção,Equipment
pode ter todos os itens que seu personagem possui ou seus ativos atuais. Também pode ter a lógica que os gerencia em funções.CharacterAI
ouCharacterController
pode ter todas as informações necessárias sobre o objetivo atual do personagem, suas funções utilitárias, etc. E também pode ter a lógica de atualização real que coordena a tomada de decisão / interação entre suas partes individuais.Depois de dividir o personagem, você não precisa mais verificar um sinalizador Alive / Dead no loop de atualização.
Em vez disso, você simplesmente fazer um
AliveCharacterObject
que tem aCharacterController
,CharacterEquipment
eCharacterInfo
scripts anexados. Para "matar" o personagem, basta remover as partes que não são mais relevantes (como aCharacterController
) - agora não será desperdiçado memória ou tempo de processamento.Observe como
CharacterInfo
provavelmente são os únicos dados realmente necessários para a árvore genealógica. Ao decompor suas classes em partes menores de funcionalidade - você pode manter mais facilmente esse pequeno objeto de dados após a morte, sem precisar manter todo o personagem orientado pela IA.Vale ressaltar que esse paradigma é o que o Unity foi construído para usar - e é por isso que lida com coisas com muitos scripts separados. Construir objetos-deus grandes raramente é a melhor maneira de lidar com seus dados no Unity.
fonte
Quando você tem uma grande quantidade de dados para manipular e nem todos os pontos de dados são representados por um objeto de jogo real, geralmente não é uma má idéia abandonar as classes específicas do Unity e apenas usar objetos C # antigos simples. Dessa forma, você minimiza a sobrecarga. Então você parece estar no caminho certo aqui.
Armazenar todos os caracteres, vivos ou mortos, em uma Lista ( ou matriz ) pode ser útil porque o índice nessa lista pode servir como um ID de caractere canônico. Acessar uma posição da lista por índice é uma operação muito rápida. Mas pode ser útil manter uma lista separada dos IDs de todos os personagens vivos, porque você provavelmente precisará iterá-los com muito mais frequência do que os caracteres mortos.
À medida que a implementação da sua mecânica de jogo progride, você também pode querer observar que outro tipo de pesquisa realiza mais. Como "todos os personagens vivos em um local específico" ou "todos os ancestrais vivos ou mortos de um personagem específico". Pode ser benéfico criar algumas estruturas de dados secundárias otimizadas para esses tipos de consultas. Lembre-se de que cada um deles deve ser atualizado. Isso requer programação adicional e será uma fonte de erros adicionais. Portanto, faça isso apenas se você espera um aumento notável no desempenho.
CKII " ameixas " personagens de seu banco de dados quando julgar-los como sem importância para economizar recursos. Se sua pilha de personagens mortos consome muitos recursos em um jogo de longa duração, você pode querer fazer algo semelhante (não quero chamar isso de "coleta de lixo". Talvez "respeitoso incremator"?).
Se você realmente tem um objeto de jogo para todos os personagens do jogo, o novo sistema Unity ECS e Jobs pode ser útil para você. Ele é otimizado para lidar com um grande número de objetos de jogo muito semelhantes de maneira eficiente. Mas isso força sua arquitetura de software a alguns padrões muito rígidos.
A propósito, eu realmente gosto de CKII e como ele simula um mundo com milhares de personagens únicos controlados por IA, então estou ansioso para interpretar sua opinião sobre o gênero.
fonte
Like "all living characters in a specific location" or "all living or dead ancestors of a specific character". It might be beneficial to create some more secondary data-structures optimized for these kinds of queries.
Da minha experiência com o CK2 modding, isso é parecido com o modo como o CK2 lida com os dados. O CK2 parece usar índices que são essencialmente índices básicos de banco de dados, o que torna mais rápido a localização de caracteres para uma situação específica. Em vez de ter uma lista de caracteres, parece ter um banco de dados interno de caracteres, com todos os inconvenientes e benefícios que isso implica.Você não precisa simular / atualizar milhares de caracteres quando apenas alguns estão próximos do player. Você só precisa atualizar o que o jogador pode realmente ver no momento atual, para que os personagens mais distantes do jogador sejam suspensos até que o jogador esteja mais perto deles.
Se isso não funcionar, porque sua mecânica de jogo exige que personagens distantes mostrem a passagem do tempo, você pode atualizá-los em uma "grande" atualização quando o jogador se aproximar. Se a sua mecânica de jogo exigir que cada personagem responda aos eventos do jogo à medida que eles acontecem, não importa onde o personagem esteja em relação ao jogador ou ao evento, poderá funcionar para reduzir a frequência com que os personagens mais distantes do jogador estão. atualizado (ou seja, eles ainda são atualizados em sincronia com o resto do jogo, mas não com tanta frequência, portanto haverá um pequeno atraso antes que os personagens distantes respondam a um evento, mas é improvável que cause um problema ou até seja notado pelo jogador ) Como alternativa, você pode querer usar uma abordagem híbrida,
fonte
Esclarecimento da questão
Nesta resposta, estou assumindo que todo o jogo deve ser como CK2, em vez de apenas a parte de ter muitos personagens. Tudo o que você vê na tela do CK2 é fácil de fazer e não comprometerá seu desempenho nem será complicado de implementar no Unity. Os dados por trás disso é onde fica complexo.
Character
Classes sem funcionalidadeBom, porque um personagem no seu jogo é apenas dados. O que você vê na tela é apenas uma representação desses dados. Essas
Character
classes são o coração do jogo e, como tal, correm o risco de se tornar " objetos divinos ". Portanto, aconselho medidas extremas contra isso: Remova todas as funcionalidades dessas classes. Um métodoGetFullName()
que combina o nome e o sobrenome, OK, mas nenhum código que realmente "faça alguma coisa". Coloque esse código em classes dedicadas que executam uma ação; por exemplo, uma classeBirther
com um métodoCharacter CreateCharacter(Character father, Character mother)
será muito mais limpa do que ter essa funcionalidade naCharacter
classe.Não armazene dados no código
Não. Armazene-os no formato JSON, usando o JsonUtility do Unity. Com essas
Character
classes sem funcionalidade , deve ser trivial. Isso funcionará para a configuração inicial do jogo, bem como para armazená-lo em jogos de salvamento. No entanto, ainda é uma coisa chata de se fazer, então eu apenas dei a opção mais fácil na sua situação. Você também pode usar XML ou YAML ou qualquer outro formato, desde que ainda possa ser lido por humanos quando armazenado em um arquivo de texto. CK2 faz o mesmo, na maioria dos jogos. Também é excelente configuração para permitir que as pessoas modifiquem seu jogo, mas isso é um pensamento para muito mais tarde.Pense abstrato
É mais fácil falar do que fazer, porque muitas vezes colide com a maneira natural de pensar. Você está pensando de uma maneira "natural", de um "personagem". No entanto, em termos do seu jogo, parece que existem pelo menos 2 tipos diferentes de dados que "são um personagem": eu vou chamá-lo
ActingCharacter
eFamilyTreeEntry
. Um personagem mortoFamilyTreeEntry
não precisa ser atualizado e provavelmente precisa de muito menos dados do que um ativoActingCharacter
.fonte
Vou falar com um pouco de experiência, passando de um design rígido de OO para um design de sistema de componentes de entidades (ECS).
Um tempo atrás eu era como você , eu tinha um monte de tipos diferentes de coisas que tinham propriedades semelhantes e construí vários objetos e tentei usar a herança para resolvê-lo. Uma pessoa muito inteligente me disse para não fazer isso e, em vez disso, use o Entity-Component-System.
Agora, o ECS é um grande conceito e é difícil de acertar. Há muito trabalho nisso, criando adequadamente entidades, componentes e sistemas. Antes de podermos fazer isso, porém, precisamos definir os termos.
Então, aqui é onde eu iria com isso:
Em primeiro lugar, crie um
ID
para seus personagens. Umint
, oGuid
que você quiser. Esta é a "entidade".Segundo, comece a pensar nos diferentes comportamentos que você está tendo. Coisas como a "Árvore Familiar" - isso é um comportamento. Em vez de modelar isso como atributos na entidade, construa um sistema que mantenha todas essas informações . O sistema pode então decidir o que fazer com ele.
Da mesma forma, queremos criar um sistema para "O personagem está vivo ou morto?" Este é um dos sistemas mais importantes no seu design, porque influencia todos os outros. Alguns sistemas podem excluir os caracteres "mortos" (como o sistema "sprite"), outros sistemas podem reorganizar internamente as coisas para melhor suportar o novo status.
Você criará um sistema "Sprite" ou "Desenho" ou "Renderização", por exemplo. Este sistema terá a responsabilidade de determinar com qual sprite o personagem precisa ser exibido e como exibi-lo. Então, quando um personagem morrer, remova-o.
Além disso, um sistema de "IA" que pode dizer a um personagem o que fazer, para onde ir etc. Isso deve interagir com muitos dos outros sistemas e tomar decisões com base neles. Novamente, os personagens mortos provavelmente podem ser removidos deste sistema, pois não estão mais fazendo nada.
Seu sistema "Nome" e o sistema "Árvore Familiar" provavelmente devem manter o personagem (vivo ou morto) na memória. Esse sistema precisa recuperar essas informações, independentemente do estado do personagem. (Jim ainda é Jim, mesmo depois de enterrá-lo.)
Isso também oferece o benefício de alterar quando um sistema reage com mais eficiência: o sistema possui seu próprio temporizador. Alguns sistemas precisam disparar rapidamente, outros não. É aqui que começamos a entender o que faz um jogo funcionar com eficiência. Não precisamos recalcular o clima a cada milissegundo, provavelmente podemos fazer isso a cada 5 ou mais.
Também oferece uma alavancagem mais criativa: você pode criar um sistema "Pathfinder" que pode lidar com o cálculo de um caminho de A para B e pode atualizar conforme necessário, permitindo que o sistema Movement diga "onde eu preciso Próximo?" Agora podemos separar completamente essas preocupações e argumentar sobre elas de maneira mais eficaz. O movimento não precisa encontrar o caminho, apenas o leva até lá.
Você deseja expor algumas partes de um sistema para o exterior. No seu
Pathfinder
sistema, você provavelmente desejará aVector2 NextPosition(int entity)
. Dessa forma, você pode manter esses elementos em matrizes ou listas rigidamente controladas. Você pode usarstruct
tipos menores, que podem ajudá-lo a manter os componentes em blocos de memória menores e contíguos, o que pode tornar as atualizações do sistema muito mais rápidas. (Especialmente se as influências externas a um sistema são mínimas, agora ele só precisa se preocupar com seu estado interno, comoName
.)Mas, e não posso enfatizar isso o suficiente, agora um
Entity
é apenas umID
, incluindo blocos, objetos etc. Se uma entidade não pertencer a um sistema, o sistema não o rastreará. Isso significa que podemos criar nossos objetos "Árvore", armazená-los nos sistemasSprite
eMovement
(as árvores não se moverão, mas elas têm um componente "Posição") e mantê-los fora dos outros sistemas. Não precisamos mais de uma lista especial de árvores, pois renderizar uma árvore não é diferente de um personagem, além da boneca de papel. (Que oSprite
sistema pode controlar, ou oPaperdoll
sistema pode controlar.) Agora,NextPosition
podemos ser ligeiramente reescritos:Vector2? NextPosition(int entity)
e pode retornar umanull
posição para as entidades que não interessam. Também aplicamos isso ao nossoNameSystem.GetName(int entity)
, ele retornanull
para árvores e rochas.Vou encerrar isso, mas a idéia aqui é fornecer alguns conhecimentos sobre o ECS e como você pode realmente aproveitá-lo para oferecer um design melhor no seu jogo. Você pode aumentar o desempenho, dissociar elementos não relacionados e manter as coisas de maneira mais organizada. (Isso também combina bem com linguagens / configurações funcionais, como F # e LINQ, que eu recomendo verificar o F # se você ainda não o fez, ele combina muito bem com o C # quando você os usa em conjunto.)
fonte
GameObject
que não fazem muito, mas têm uma lista deComponent
classes que fazem o trabalho real. Houve uma mudança de paradigma em relação à rotatória dos ECSs há uma década , pois colocar o código de atuação em classes de sistema separadas é mais limpo. A Unity recentemente implementou esse sistema também, mas oGameObject
sistema deles é e sempre foi um ECS. O OP já está usando um ECS.Enquanto você faz isso no Unity, a abordagem mais fácil é a seguinte:
No seu código, você pode manter referências aos objetos em algo como uma Lista para evitar que você use Find () e suas variações o tempo todo. Você está trocando ciclos de CPU por memória, mas uma lista de ponteiros é muito pequena, portanto, mesmo com alguns milhares de objetos, isso não deve ser um problema.
À medida que avança no jogo, você descobrirá que ter objetos de jogo individuais oferece muitas vantagens, incluindo navegação e IA.
fonte