Estou criando um jogo de tiro em primeira pessoa e conheço muitos tipos diferentes de contêineres, mas gostaria de encontrar o contêiner mais eficiente para armazenar objetos dinâmicos que serão adicionados e excluídos do jogo com bastante frequência. Balas EX.
Penso que nesse caso seria uma lista para que a memória não seja contígua e nunca haja redimensionamento. Mas também estou pensando em usar um mapa ou conjunto. Se alguém tiver alguma informação útil, eu agradeceria.
Estou escrevendo isso em c ++ pelo caminho.
Também vim com uma solução que acho que funcionará.
Para começar, vou alocar um vetor de tamanho grande ... digamos 1000 objetos. Vou acompanhar o último índice adicionado nesse vetor para saber onde está o final dos objetos. Então, também criarei uma fila que conterá os índices de todos os objetos que são "excluídos" do vetor. (Nenhuma exclusão real será feita, apenas saberei que ela está livre). Portanto, se a fila estiver vazia, adicionarei o último índice adicionado no vetor + 1; caso contrário, adicionarei o índice do vetor que estava na frente da fila.
fonte
Respostas:
A resposta é sempre usar uma matriz ou std :: vector. Tipos como uma lista vinculada ou um std :: map geralmente são absolutamente horríveis nos jogos, e isso definitivamente inclui casos como coleções de objetos do jogo.
Você deve armazenar os próprios objetos (não ponteiros para eles) na matriz / vetor.
Você quer memória contígua. Você realmente quer isso. A iteração sobre quaisquer dados na memória não contígua impõe muitas falhas de cache em geral e remove a capacidade do compilador e da CPU de realizar uma pré-busca de cache eficaz. Isso por si só pode prejudicar o desempenho.
Você também deseja evitar alocações e desalocações de memória. Eles são muito lentos, mesmo com um alocador de memória rápido. Eu já vi jogos obterem um aumento de 10x FPS ao remover algumas centenas de alocações de memória a cada quadro. Não parece que deve ser tão ruim, mas pode ser.
Por fim, a maioria das estruturas de dados de que você gosta para gerenciar objetos de jogo pode ser implementada com muito mais eficiência em uma matriz ou em um vetor do que com uma árvore ou uma lista.
Por exemplo, para remover objetos de jogo, você pode usar swap-and-pop. Facilmente implementado com algo como:
Você também pode marcar os objetos como excluídos e colocar o índice em uma lista gratuita para a próxima vez que precisar criar um novo objeto, mas fazer o swap-and-pop é melhor. Ele permite que você faça um loop for simples sobre todos os objetos ativos, sem ramificações além do próprio loop. Para integração da física de balas e similares, isso pode ser um aumento significativo no desempenho.
Mais importante, você pode encontrar objetos com um simples par de pesquisas de tabela a partir de um único estável usando a estrutura do mapa de slots.
Os objetos do seu jogo têm um índice em sua matriz principal. Eles podem ser pesquisados de maneira muito eficiente com apenas esse índice (muito mais rápido que um mapa ou mesmo uma tabela de hash). No entanto, o índice não é estável devido à troca e pop ao remover objetos.
Um mapa de slots requer duas camadas de indireção, mas ambas são simples pesquisas de matriz com índices constantes. Eles são rápidos . Muito depressa.
A idéia básica é que você tenha três matrizes: sua lista principal de objetos, sua lista indireta e uma lista grátis para a lista indireta. Sua lista de objetos principal contém seus objetos reais, onde cada objeto conhece seu próprio ID exclusivo. O ID exclusivo é composto por um índice e uma tag de versão. A lista indireta é simplesmente uma matriz de índices para a lista principal de objetos. A lista livre é uma pilha de índices na lista indireta.
Ao criar um objeto na lista principal, você encontra uma entrada não utilizada na lista indireta (usando a lista gratuita). A entrada na lista indireta aponta para uma entrada não utilizada na lista principal. Você inicializa seu objeto nesse local e define seu ID exclusivo para o índice da entrada da lista indireta escolhida e a tag de versão existente no elemento principal da lista, mais uma.
Quando você destrói um objeto, faz o swap e pop normalmente, mas também incrementa o número da versão. Você também adiciona o índice da lista indireta (parte do ID exclusivo do objeto) à lista gratuita. Ao mover um objeto como parte do swap-and-pop, você também atualiza sua entrada na lista indireta para seu novo local.
Exemplo de pseudocódigo:
A camada indireta permite que você tenha um identificador estável (o índice na camada indireta, para onde as entradas não se movem) para um recurso que pode se mover durante a compactação (a lista de objetos principal).
A tag version permite armazenar um ID em um objeto que pode ser excluído. Por exemplo, você tem o ID (10,1). O objeto com o índice 10 é excluído (digamos, seu marcador atinge um objeto e é destruído). O objeto nesse local da memória na lista principal de objetos tem seu número de versão aumentado, fornecendo-o (10,2). Se você tentar procurar (10,1) novamente a partir de um ID antigo, a pesquisa retornará esse objeto através do índice 10, mas poderá ver que o número da versão foi alterado, portanto o ID não é mais válido.
Essa é a estrutura de dados mais rápida e absoluta que você pode ter com um ID estável que permite que os objetos se movam na memória, o que é importante para a localidade dos dados e a coerência do cache. Isso é mais rápido do que qualquer implementação possível de uma tabela de hash; uma tabela de hash precisa, no mínimo, calcular um hash (mais instruções do que uma pesquisa de tabela) e seguir a cadeia de hash (uma lista vinculada no caso horrível de std :: unordered_map ou uma lista de endereços abertos em qualquer implementação não estúpida de uma tabela de hash) e, em seguida, faça uma comparação de valor em cada chave (não mais cara, mas possível menos cara do que a verificação da tag de versão). Uma tabela de hash muito boa (não a de qualquer implementação da STL, pois a STL exige uma tabela de hash otimizada para diferentes casos de uso do que você pensa em uma lista de objetos do jogo) pode economizar em um indireto,
Existem várias melhorias que você pode fazer no algoritmo base. Usando algo como um std :: deque para a lista principal de objetos, por exemplo; uma camada extra de indireção, mas permite que os objetos sejam inseridos em uma lista completa sem invalidar os ponteiros temporários que você adquiriu no mapa de slots.
Você também pode evitar o armazenamento do índice dentro do objeto, pois o índice pode ser calculado a partir do endereço de memória do objeto (this - objects) e, ainda melhor, só é necessário ao remover o objeto. Nesse caso, você já tem o ID do objeto (e, portanto, índice) como um parâmetro.
Desculpas pela redação; Não acho que seja a descrição mais clara possível. É tarde e é difícil de explicar sem gastar mais tempo do que eu tenho em amostras de código.
fonte
matriz de tamanho fixo (memória linear)
com lista livre interna (O (1) alocação / livre, indicações estáveis)
com chaves de referência fracas (a reutilização do slot invalida a chave)
zero dereferências de sobrecarga (quando conhecidas como válidas)
Lida com tudo, desde marcadores a monstros, texturas a partículas, etc. Essa é a melhor estrutura de dados para videogames. Eu acho que veio da Bungie (nos dias de maratona / mito), eu aprendi sobre isso na Blizzard e acho que estava em uma gema de programação de jogos na época. Provavelmente está em toda a indústria de jogos neste momento.
P: "Por que não usar uma matriz dinâmica?" R: Matrizes dinâmicas causam falhas. Exemplo simples:
Você pode imaginar casos com mais complicações (como pilhas de chamadas profundas). Isso é verdade para todos os array, como contêineres. Ao fazer jogos, temos compreensão suficiente do nosso problema para forçar tamanhos e orçamentos em tudo em troca de desempenho.
E não posso dizer o suficiente: sério, essa é a melhor coisa de todas. (Se você não concordar, poste sua solução melhor! Advertência - deve resolver os problemas listados na parte superior deste post: memória / iteração linear, O (1) alocação / livre, índices estáveis, referências fracas, zero derefs ou tem uma razão incrível pela qual você não precisa de um desses;)
fonte
DataArray
parece também alocar uma matriz dinamicamente no ctor. Portanto, pode ter algum significado diferente no meu entendimento.Não há resposta certa para isso. Tudo depende da implementação dos seus algoritmos. Basta ir com o que você achar melhor. Não tente otimizar nesta fase inicial.
Se você costuma excluir objetos e recriá-los, sugiro que você veja como os pools de objetos são implementados.
Edit: Por que complicar as coisas com slots e quais não. Por que não usar uma pilha, retirar o último item e reutilizá-lo? Então, quando você adiciona um, você faz ++, quando você abre um - para acompanhar o seu índice final.
fonte
Depende do seu jogo. Os contêineres são diferentes na rapidez com que o acesso a um elemento específico é, na rapidez com que um elemento é removido e na rapidez com que um elemento é adicionado.
Geralmente, você deseja usar uma lista se quiser que sua lista de objetos seja classificada de maneira diferente da cronocialmente e, portanto, precise inserir novos objetos em vez de anexá-los, caso contrário, deque. Um deque aumentou a flexibilidade em relação a um vetor, mas realmente não tem muita desvantagem.
Se você realmente possui muitas entidades, consulte o Particionamento de espaço.
fonte