Qual a melhor forma de estruturar / gerenciar centenas de personagens 'no jogo'

10

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.

paul p
fonte
9
Uma coisa que eu fiz no passado quando preciso de um subconjunto dos dados de um personagem após seu desaparecimento é criar um objeto "marca de exclusão" para esse personagem. A lápide pode conter as informações de que preciso procurar mais tarde, mas pode ser menor e iterada com menos frequência porque não precisa de simulação constante como um personagem vivo.
DMGregory
2
O jogo é parecido com o CK2, ou é apenas a parte de ter muitos personagens? Eu entendi como o jogo inteiro sendo como CK2. Nesse caso, muitas das respostas aqui não são incorretas e contêm um bom know-how, mas elas perdem o objetivo da pergunta. Não ajuda que você tenha chamado o CK2 de um jogo de estratégia em tempo real, quando na verdade é um grande jogo de estratégia . Isso pode parecer complicado, mas é muito relevante para os problemas que você está enfrentando.
Raphael Schmitz
1
Por exemplo, quando você menciona "milhares de caracteres", as pessoas pensam em milhares de modelos 3D ou sprites na tela ao mesmo tempo - portanto, no Unity, milhares de 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 um GameObject, exibindo o número "3000".
Raphael Schmitz
1
@ R.Schmitz Sim, eu deveria ter deixado essa parte clara, todos os personagens não têm objetos de jogo anexados a eles. Sempre que necessário, mova o personagem de um ponto para outro. É criada uma entidade separada, que contém todas as informações desse personagem com lógica Ai.
paul p

Respostas:

24

Há três coisas que você deve considerar:

  1. 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.

  2. 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.

  3. 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.

Jack Aidley
fonte
3
Entidade / Componente / Sistema funciona bem para isso. Crie cada "sistema" para o que você precisa, mantenha os milhares ou dezenas de milhares de caracteres (seus componentes) e forneça o "ID do caractere" ao sistema. Isso permite manter os diferentes modelos de dados separados e menores, além de remover caracteres mortos de sistemas que não precisam deles. (Você também pode descarregar completamente um sistema, se não o estiver usando no momento).
Der Kommissar
1
Ao reduzir a quantidade de caracteres de atualização que estão fora da tela, você pode aumentar drasticamente o tempo de processamento. Você não quer dizer diminuir?
Tejas Kale
1
@TejasKale: Sim, corrigido.
Jack Aidley
2
A mil caracteres não é muito, mas quando cada um deles está constantemente verificando para ver se eles podem castrar cada um dos outros que não começar a ter um grande impacto no desempenho geral ...
curiousdannii
1
Sempre é melhor checar de verdade, mas geralmente é uma suposição segura de que os romanos vão querer castrar um ao outro;)
curiousdannii
11

Nesta situação, eu sugeriria usar Composition :

O princípio de que as classes devem obter comportamento polimórfico e reutilização de código por sua composição (contendo instâncias de outras classes que implementam a funcionalidade desejada)


Nesse caso, parece que sua Characterclasse 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 Characterem subclasses, das quais ele Characterpossui 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,

  • Equipmentpode 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.

  • CharacterAIou CharacterControllerpode 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 AliveCharacterObjectque tem a CharacterController, CharacterEquipmente CharacterInfoscripts anexados. Para "matar" o personagem, basta remover as partes que não são mais relevantes (como a CharacterController) - agora não será desperdiçado memória ou tempo de processamento.

Observe como CharacterInfoprovavelmente 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.

Bilkokuya
fonte
8

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.

Philipp
fonte
Olá, obrigado pela resposta. Todos os cálculos são feitos por um único GameObject Manager. Estou atribuindo apenas objetos de jogo a atores individuais quando necessário (como mostrar movimentos do Army of Character de uma posição para outra).
paul p
1
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.
Morfildur 11/11/19
1

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,

Micheal Johnson
fonte
é um RTS. Devemos assumir que, a qualquer momento, um número considerável de unidades está realmente na tela.
Tom
Em um RTS, o mundo precisa continuar enquanto o jogador não está olhando. Uma grande atualização levaria o mesmo tempo, mas ocorreria muito quando você mover a câmera.
PSTag 11/1118
1

Esclarecimento da questão

Eu tenho feito um jogo RTS simples, que contém centenas de personagens como Crusader Kings 2 in Unity.

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.

CharacterClasses sem funcionalidade

Então, eu criei uma classe C # chamada "Personagem", que contém todos os dados.

Bom, porque um personagem no seu jogo é apenas dados. O que você vê na tela é apenas uma representação desses dados. Essas Characterclasses 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étodo GetFullName()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 classe Birthercom um método Character CreateCharacter(Character father, Character mother)será muito mais limpa do que ter essa funcionalidade na Characterclasse.

Não armazene dados no código

Para armazená-los, a opção mais fácil seria usar objetos programáveis

Não. Armazene-os no formato JSON, usando o JsonUtility do Unity. Com essas Characterclasses 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

Eu adicionei uma verificação simples para garantir que o personagem esteja "Alive" durante o processamento, [...] mas não consigo remover "Character" se ele estiver morto, porque preciso das informações dele ao criar a árvore genealógica.

É 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 ActingCharactere FamilyTreeEntry. Um personagem morto FamilyTreeEntrynão precisa ser atualizado e provavelmente precisa de muito menos dados do que um ativo ActingCharacter.

Raphael Schmitz
fonte
0

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.

  1. Entidade : é isso , o jogador, o animal, o NPC, o que for . É algo que precisa de componentes anexados a ele.
  2. Componente : este é o atributo ou propriedade , como "Nome" ou "Idade" ou "Pais", no seu caso.
  3. Sistema : essa é a lógica por trás de um componente ou comportamento . Normalmente, você cria um sistema por componente, mas isso nem sempre é possível. Além disso, às vezes os sistemas precisam influenciar outros sistemas.

Então, aqui é onde eu iria com isso:

Em primeiro lugar, crie um IDpara seus personagens. Um int, o Guidque 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 Pathfindersistema, você provavelmente desejará a Vector2 NextPosition(int entity). Dessa forma, você pode manter esses elementos em matrizes ou listas rigidamente controladas. Você pode usar structtipos 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, como Name.)

Mas, e não posso enfatizar isso o suficiente, agora um Entityé apenas um ID, 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 sistemas Spritee Movement(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 o Spritesistema pode controlar, ou o Paperdollsistema pode controlar.) Agora, NextPositionpodemos ser ligeiramente reescritos: Vector2? NextPosition(int entity)e pode retornar uma nullposição para as entidades que não interessam. Também aplicamos isso ao nosso NameSystem.GetName(int entity), ele retorna nullpara á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.)

Der Kommissar
fonte
Olá, Obrigado por uma resposta tão detalhada. Estou usando apenas o One GameObject Manager, que contém a referência a todos os outros personagens do jogo.
paul p
O desenvolvimento no Unity gira em torno de entidades chamadas GameObjectque não fazem muito, mas têm uma lista de Componentclasses 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 o GameObjectsistema deles é e sempre foi um ECS. O OP já está usando um ECS.
Raphael Schmitz
-1

Enquanto você faz isso no Unity, a abordagem mais fácil é a seguinte:

  • crie um objeto de jogo por personagem ou tipo de unidade
  • salve-os como prefabs
  • você pode instanciar uma pré-fabricada sempre que necessário
  • quando um personagem é morto, destrua o objeto do jogo e ele não ocupará mais CPU ou memória

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.

Tom
fonte