Armazenar todos os objetos do jogo em uma única lista é um design aceitável?


24

Para cada jogo que eu fiz, acabei colocando todos os meus objetos de jogo (balas, carros, jogadores) em uma única lista de arrays, pela qual percorro para desenhar e atualizar. O código de atualização para cada entidade é armazenado em sua classe.

Fiquei me perguntando, este é o caminho certo para fazer as coisas, ou é um caminho mais rápido e eficiente?


4
Percebo que você disse "lista de matriz", assumindo que seja .Net, você deve atualizar para uma Lista <T>. É quase idêntico, exceto que uma Lista <T> é do tipo forte e, por isso, você não precisará digitar a conversão toda vez que acessar um elemento. Aqui está o documento: msdn.microsoft.com/en-us/library/6sh2ey19.aspx
John McDonald

Respostas:


26

Não existe um caminho certo. O que você está descrevendo funciona e é presumivelmente rápido o suficiente para que isso não importe, pois você parece estar se perguntando porque está preocupado com o design e não porque isso causou problemas de desempenho. Por isso, é um caminho certo, com certeza.

Você certamente poderia fazer isso de maneira diferente, mantendo, por exemplo, uma lista homogênea para cada tipo de objeto ou assegurando que tudo em sua lista heterogênea seja agrupado, de modo a melhorar a coerência do código que está no cache ou permitir atualizações paralelas. É difícil dizer se você verá alguma melhoria significativa no desempenho fazendo isso, sem saber o escopo e a escala dos seus jogos.

Você também pode levar em consideração as responsabilidades de suas entidades - parece que as entidades se atualizam e se processam no momento - para seguir melhor o Princípio de responsabilidade única. Isso pode aumentar a capacidade de manutenção e a flexibilidade de suas interfaces individuais, dissociando-as umas das outras. Mas, novamente, é difícil dizer se é um benefício do trabalho extra que implicaria saber o que sei dos seus jogos (que basicamente não é nada).

Eu recomendaria não se estressar muito com isso e lembre-se de que essa pode ser uma área potencial de melhoria se você notar problemas de desempenho ou de manutenção.


16

Como outros já disseram, se é rápido o suficiente, então é rápido o suficiente. Se você está se perguntando se existe uma maneira melhor, a pergunta a ser feita é:

Eu me vejo repetindo todos os objetos, mas operando apenas em um subconjunto deles?

Se o fizer, é uma indicação de que você pode querer ter várias listas contendo apenas referências relevantes. Por exemplo, se você estiver percorrendo todos os objetos do jogo para renderizá-los, mas apenas um subconjunto de objetos precisar ser renderizado, pode valer a pena manter uma lista Renderizável separada apenas para os objetos que precisam ser renderizados.

Isso reduziria o número de objetos pelos quais você percorre e ignora as verificações desnecessárias porque você já sabe que todos os objetos na lista devem ser processados.

Você teria que criar um perfil para ver se está obtendo muito ganho de desempenho.


Eu concordo com isso, geralmente tenho subconjuntos da minha lista para objetos que precisam ser atualizados a cada quadro, e tenho pensado em fazer o mesmo para objetos que são renderizados. No entanto, quando essas propriedades podem mudar rapidamente, o gerenciamento de todas as listas pode ser complexo. Quando e objeto passa de renderizável para não renderizável ou atualizável para não atualizável, e agora você os está adicionando ou removendo de várias listas ao mesmo tempo.
Nic Foster

8

Depende quase inteiramente de suas necessidades específicas de jogo . Pode funcionar perfeitamente para um jogo simples, mas falha em um sistema mais complexo. Se estiver funcionando para o seu jogo, não se preocupe ou tente projetar demais até que seja necessário.

Quanto ao motivo pelo qual a abordagem simples pode falhar em algumas situações, é difícil resumir isso em um único post, pois as possibilidades são infinitas e dependem inteiramente dos próprios jogos. Outras respostas mencionaram, por exemplo, que você pode querer agrupar objetos que compartilham responsabilidades em uma lista separada.

Essa é uma das mudanças mais comuns que você pode estar fazendo, dependendo do design do seu jogo. Aproveitarei a oportunidade para descrever alguns outros exemplos (mais complexos), mas lembre-se de que ainda existem muitas outras razões e soluções possíveis.

Para iniciantes, vou apontar que atualizar os objetos do jogo e renderizá-los pode ter requisitos diferentes em alguns jogos e, portanto, precisam ser manipulados separadamente. Alguns possíveis problemas com a atualização e renderização de objetos de jogo:

Atualizando objetos de jogo

Aqui está um trecho da Game Engine Architecture que eu recomendaria ler:

Na presença de dependências entre objetos, a técnica de atualizações em fases descrita acima deve ser ajustada um pouco. Isso ocorre porque as dependências entre objetos podem levar a regras conflitantes que governam a ordem da atualização.

Em outras palavras, em alguns jogos é possível que os objetos dependam um do outro e exijam uma ordem específica de atualização. Nesse cenário, pode ser necessário criar uma estrutura mais complexa do que uma lista para armazenar objetos e suas interdependências.

insira a descrição da imagem aqui

Renderização de objetos de jogo

Em alguns jogos, cenas e objetos criam uma hierarquia de nós pais-filhos e devem ser renderizados na ordem correta e com transformações em relação aos pais. Nesses casos, você pode precisar de uma estrutura mais complexa, como um gráfico de cena (uma estrutura em árvore), em vez de uma única lista:

insira a descrição da imagem aqui

Outros motivos podem ser, por exemplo, o uso de uma estrutura de dados de particionamento espacial para organizar seus objetos de uma maneira que melhore a eficiência da execução do descarte de exibição de exibição.


4

A resposta é "sim, está tudo bem". Quando trabalhei em grandes simulações de ferro em que o desempenho não era negociável, fiquei surpreso ao ver tudo em uma matriz global grande e gorda (BIG e FAT). Mais tarde, trabalhei em um sistema OO e pude comparar os dois. Embora tenha sido super feia, a versão "uma matriz para governar todos eles" no C simples simples de thread único compilado em depuração foi várias vezes mais rápida que o sistema OO multithread "bonito" compilado -O2 em C ++. Eu acho que um pouco disso tinha a ver com a excelente localidade do cache (no espaço e no tempo) na versão big-fat-array.

Se você quer desempenho, uma grande matriz C global será o limite superior (por experiência).


2

Basicamente, o que você quer fazer é criar listas conforme necessário. Se você redesenhar objetos do terreno de uma só vez, uma lista deles poderá ser útil. Mas, para que isso valha a pena, você precisaria de dezenas de milhares deles e de 10.000 coisas que não são terreno. Caso contrário, ler atentamente sua lista de tudo e usar um "se" para retirar objetos do terreno é rápido o suficiente.

Eu sempre precisei de uma lista principal, independentemente de quantas outras listas eu já tenha. Geralmente, faço de um tipo de mapa, tabela com chave ou dicionário, para que eu possa acessar itens individuais aleatoriamente. Mas uma lista simples é muito mais simples; se você não acessar objetos do jogo aleatoriamente, não precisará do mapa. E a pesquisa seqüencial ocasional por alguns milhares de entradas para encontrar a correta não levará muito tempo. Então, eu diria que, o que quer que você faça, planeje manter a lista principal. Considere usar um mapa se ele ficar grande. (Embora se você pode usar índices em vez de chaves, pode ficar com matrizes e ter algo muito melhor que um mapa. Eu sempre acabava com grandes lacunas entre os números de índice, então tive que desistir.)

Uma palavra sobre matrizes: elas são incrivelmente rápidas. Mas, quando eles capturam cerca de 100.000 elementos, começam a dar encaixes ao Garbage Collectors. Se você pode alocar o espaço uma vez e não tocá-lo depois, tudo bem. Mas se você está continuamente expandindo a matriz, está continuamente alocando e liberando grandes blocos de memória e está apto a travar o jogo por segundos por vez e até falhar com um erro de memória.

Tão rápido e mais eficiente? Certo. Um mapa para acesso aleatório. Para listas realmente grandes, uma Lista vinculada vencerá uma matriz se e quando você não precisar de acesso aleatório. Vários mapas / listas para organizar os objetos de várias maneiras, conforme necessário. Você pode multiencadear a atualização.

Mas é tudo muito trabalho. Essa matriz única é rápida e simples. Você pode adicionar outras listas e outras coisas somente quando necessário, e provavelmente não precisará delas. Esteja ciente do que pode dar errado se o seu jogo ficar grande. Você pode querer ter uma versão de teste com 10 vezes mais objetos do que na versão real para ter uma idéia de quando você está enfrentando problemas. (Faça isso apenas se o seu jogo estiver ficando grande.) (E não tente o multi-threading sem pensar muito. Todo o resto é fácil de começar e você pode fazer o quanto quiser e recue a qualquer momento. Com o multi-threading, você pagará um preço alto para entrar, as taxas de permanência são altas e é difícil voltar.)

Em outras palavras, continue fazendo o que está fazendo. Seu maior limite é provavelmente o seu próprio tempo, e você está fazendo o melhor uso possível.


Realmente não acho que uma lista vinculada supere uma matriz de qualquer tamanho, a menos que você adicione ou remova coisas do meio.
Zan Lynx

@ZanLynx: E mesmo assim. Acho que as novas otimizações de tempo de execução ajudam muito as matrizes - não que elas precisem. Mas se você alocar e liberar grandes pedaços de memória repetidamente e rapidamente, como pode acontecer com grandes matrizes, poderá amarrar o coletor de lixo em nós. (A quantidade total de memória também é um fator aqui. O problema é uma função do tamanho dos blocos, da frequência de liberação e alocação e da memória disponível.) A falha ocorre repentinamente, com o programa travando ou travando. Geralmente, há muita memória livre; o GC simplesmente não pode usá-lo. Listas vinculadas evitam esse problema.
RalphChapin

Por outro lado, as listas vinculadas têm uma probabilidade significativamente maior de armazenar em cache a falta na iteração devido ao fato de os nós não serem contíguos na memória. Não é tão fácil de cortar e secar. Você pode aliviar os problemas de realocação não fazendo isso (alocando antecipadamente, se possível, porque geralmente é) e pode aliviar os problemas de coerência do cache em uma lista vinculada, alocando os nós (embora você ainda tenha o custo de referência da referência , é relativamente trivial).
18712 Josh

Sim, mas mesmo em uma matriz, você não está apenas armazenando ponteiros para objetos? (A menos que seja uma matriz de vida curta para um sistema de partículas ou algo assim.) Nesse caso, ainda haverá muitas falhas de cache.
precisa saber é o seguinte

1

Eu usei um método um pouco semelhante para jogos simples. Armazenar tudo em uma matriz é muito útil quando as seguintes condições forem verdadeiras:

  1. Você tem um número pequeno ou máximo conhecido de objetos de jogo
  2. Você favorece a simplicidade à performance (por exemplo: estruturas de dados mais otimizadas, como árvores, não valem o trabalho)

De qualquer forma, implementei essa solução como uma matriz de tamanho fixo que contém uma gameObject_ptrestrutura. Essa estrutura continha um ponteiro para o próprio objeto do jogo e um valor booleano representando se o objeto estava ou não "vivo".

O objetivo do booleano é acelerar as operações de criação e exclusão. Em vez de destruir os objetos do jogo quando eles são removidos da simulação, eu simplesmente configuraria o sinalizador "vivo" para false.

Ao executar cálculos em objetos, o código percorrerá a matriz, executando cálculos em objetos marcados como "vivos" e somente objetos marcados como "vivos" serão renderizados.

Outra otimização simples é organizar a matriz por tipo de objeto. Todos os marcadores, por exemplo, poderiam viver nas últimas n células da matriz. Em seguida, armazenando um int com o valor do índice ou um ponteiro para o elemento da matriz, tipos específicos podem ser referenciados a um custo reduzido.

Não é necessário dizer que esta solução não é boa para jogos com grandes quantidades de dados, pois a matriz será inaceitavelmente grande. Também não é adequado para jogos com restrições de memória que proíbem que objetos não utilizados ocupem memória. A conclusão é que, se você tiver um limite superior conhecido do número de objetos do jogo que terá em um determinado momento, não há nada de errado em armazená-los em uma matriz estática.

Este é o meu primeiro post! Espero que responda sua pergunta!


primeiro post! yay!
Tili

0

É aceitável, embora incomum. Termos como "mais rápido" e "mais eficiente" são relativos; normalmente você organizaria os objetos do jogo em categorias separadas (uma lista de marcadores, uma lista de inimigos etc.) para tornar a programação mais organizada (e, portanto, mais eficiente), mas não é como se o computador passasse por todos os objetos mais rapidamente .


2
É verdade o suficiente na maioria dos jogos de XNA, mas a organização de seus objetos pode tornar a iteração mais rápida: objetos do mesmo tipo têm mais probabilidade de atingir as instruções e o cache de dados da sua CPU. As falhas de cache são caras, especialmente em consoles. No Xbox 360, por exemplo, uma falta de cache L2 custará ~ 600 ciclos. Isso está deixando muito desempenho em cima da mesa. Este é um local em que o código gerenciado tem uma vantagem sobre o código nativo: os objetos alocados próximos no tempo também serão fechados na memória e a situação melhora com a coleta de lixo (compactação).
Programador de Jogo Crônico

0

Você diz matriz e objetos únicos.

Então (sem o seu código para analisar), acho que todos estão relacionados ao 'uberGameObject'.

Então, talvez dois problemas.

1) Você pode ter um objeto "deus" - faz toneladas de coisas, não realmente segmentadas pelo que faz ou é.

2) Você percorre esta lista e transmite para digitar? ou desembalar cada. Isso tem uma grande penalidade de desempenho.

considere dividir tipos diferentes (marcadores, bandidos etc.) em suas próprias listas fortemente tipadas.

List<BadGuyObj> BadGuys = new List<BadGuyObj>();
List<BulletsObj> Bullets = new List<BulletObj>();

 //Game 
OnUpdate(gameticks gt)
{  foreach (BulletObj thisbullet in Bullets) {thisbullet.Update();}  // much quicker.

Não há necessidade de executar a conversão de tipos se houver algum tipo de classe base ou interface comum que possua todos os métodos que serão chamados no loop de atualização (por exemplo, update()).
bummzack

0

Eu posso propor um compromisso entre lista única geral e listas separadas para cada tipo de objeto.

Mantenha a referência a um objeto em várias listas. Essas listas são definidas pelos comportamentos base. Por exemplo, MarineSoldierobjecto será referenciado em PhysicalObjList, GraphicalObjList, UserControlObjList,HumanObjList . Então, quando você processa a física, itera PhysicalObjList, quando processa os danos causados ​​pelo veneno - HumanObjListse o Soldier morre - remova-o, HumanObjListmas mantenha-o PhysicalObjList. Para que esse mecanismo funcione, você precisa organizar a inserção / remoção automática de objetos quando ele é criado / excluído.

Vantagens da abordagem:

  • organização de objetos digitada (não AllObjectsListcom conversão na atualização)
  • listas são baseadas em lógicas, não em classes de objetos
  • sem problemas com objetos com habilidades combinadas ( MegaAirHumanUnderwaterObjectseriam inseridos nas listas correspondentes em vez de criar uma nova lista para seu tipo)

PS: Quanto à atualização automática, você encontrará problemas quando vários objetos estiverem interagindo e cada um deles depende de outros. Para resolver isso, precisamos afetar o objeto externamente, não dentro da auto-atualização.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.