Se esta é a primeira vez que você faz essa pergunta, sugiro ler a parte de pré-atualização abaixo, depois essa parte. Aqui está uma síntese do problema, no entanto:
Basicamente, eu tenho um mecanismo de detecção e resolução de colisões com um sistema de particionamento espacial de grade onde os grupos de ordem e colisão são importantes. Um corpo de cada vez deve se mover, detectar colisões e resolver colisões. Se eu mover todos os corpos de uma só vez, gerar possíveis pares de colisão, é obviamente mais rápido, mas a resolução é interrompida porque a ordem de colisão não é respeitada. Se eu mover um corpo por vez, sou forçado a fazer com que os corpos verifiquem colisões, e isso se torna um problema ^ 2. Coloque grupos na mistura e você pode imaginar por que fica muito lento muito rápido com muitos corpos.
Atualização: trabalhei muito nisso, mas não consegui otimizar nada.
Também descobri um grande problema: meu mecanismo depende da ordem de colisão.
Eu tentei uma implementação de geração de pares de colisão exclusiva , que definitivamente acelera muito tudo, mas quebrei a ordem de colisão .
Deixe-me explicar:
no meu design original (sem gerar pares), isso acontece:
- um único corpo se move
- depois que se move, atualiza suas células e obtém os corpos contra os quais colide
- se sobrepuser um corpo ao qual precisa resolver, resolva a colisão
isso significa que, se um corpo se mover e atingir uma parede (ou qualquer outro corpo), apenas o corpo que se moveu resolverá sua colisão e o outro corpo não será afetado.
Esse é o comportamento que desejo .
Entendo que não é comum para motores de física, mas tem muitas vantagens para jogos em estilo retrô .
no design de grade usual (gerando pares únicos), isso acontece:
- todos os corpos se movem
- depois que todos os corpos se moverem, atualize todas as células
- gerar pares de colisão exclusivos
- para cada par, lide com a detecção e resolução de colisões
nesse caso, um movimento simultâneo pode resultar na sobreposição de dois corpos e eles serão resolvidos ao mesmo tempo - isso efetivamente faz com que os corpos "se empurrem" e rompe a estabilidade de colisão com vários corpos
Esse comportamento é comum para mecanismos de física, mas não é aceitável no meu caso .
Eu também encontrei outro problema, que é importante (mesmo que não seja provável que isso aconteça em uma situação do mundo real):
- considerar órgãos dos grupos A, B e W
- A colide e resolve contra W e A
- B colide e resolve contra W e B
- A não faz nada contra B
- B não faz nada contra A
pode haver uma situação em que muitos corpos A e B ocupam a mesma célula - nesse caso, há muita iteração desnecessária entre os corpos que não devem reagir entre si (ou apenas detectar colisão, mas não resolvê-los) .
Para 100 corpos ocupando a mesma célula, são 100 ^ 100 iterações! Isso acontece porque pares únicos não estão sendo gerados - mas eu não posso gerar pares únicos , caso contrário, obteria um comportamento que não desejo.
Existe uma maneira de otimizar esse tipo de mecanismo de colisão?
Estas são as diretrizes que devem ser respeitadas:
A ordem de colisão é extremamente importante!
- Os corpos devem se mover um de cada vez , depois verificar colisões, um de cada vez , e resolver após o movimento, um de cada vez .
Os corpos devem ter 3 conjuntos de bits de grupo
- Grupos : grupos aos quais o corpo pertence
- GroupsToCheck : agrupa o corpo deve detectar colisão contra
- GroupsNoResolve : agrupa o corpo não deve resolver colisões contra
- Pode haver situações em que eu só quero que uma colisão seja detectada, mas não resolvida
Pré-atualização:
Prefácio : sei que otimizar esse gargalo não é uma necessidade - o mecanismo já é muito rápido. Eu, no entanto, por motivos divertidos e educacionais, adoraria encontrar uma maneira de tornar o mecanismo ainda mais rápido.
Estou criando um mecanismo de detecção / resposta de colisão 2D C ++ de uso geral, com ênfase na flexibilidade e velocidade.
Aqui está um diagrama muito básico de sua arquitetura:
Basicamente, a classe principal é World
, que possui (gerencia a memória) de a ResolverBase*
, a SpatialBase*
e a vector<Body*>
.
SpatialBase
é uma classe virtual pura que lida com a detecção de colisão em fase ampla.
ResolverBase
é uma classe virtual pura que lida com a resolução de colisões.
Os corpos comunicar à World::SpatialBase*
com SpatialInfo
objetos, pertencentes aos próprios corpos.
Atualmente, existe uma classe espacial:, Grid : SpatialBase
que é uma grade 2D fixa básica. Tem sua própria classe de informação GridInfo : SpatialInfo
,.
Veja como fica sua arquitetura:
A Grid
classe possui uma matriz 2D de Cell*
. A Cell
classe contém uma coleção de (não pertencente) Body*
: a vector<Body*>
que contém todos os corpos que estão na célula.
GridInfo
os objetos também contêm ponteiros não proprietários para as células em que o corpo está.
Como eu disse anteriormente, o mecanismo é baseado em grupos.
Body::getGroups()
retorna umstd::bitset
de todos os grupos dos quais o corpo faz parte.Body::getGroupsToCheck()
retorna umstd::bitset
de todos os grupos contra os quais o corpo tem que verificar a colisão.
Os corpos podem ocupar mais de uma única célula. O GridInfo sempre armazena ponteiros não proprietários nas células ocupadas.
Depois que um único corpo se move, a detecção de colisão acontece. Suponho que todos os corpos sejam caixas delimitadoras alinhadas ao eixo.
Como a detecção de colisão de fase ampla funciona:
Parte 1: atualização de informações espaciais
Para cada um Body
body
:
- As células ocupadas no canto superior esquerdo e as células ocupadas no canto inferior direito são calculadas.
- Se eles diferirem das células anteriores,
body.gridInfo.cells
são limpos e preenchidos com todas as células que o corpo ocupa (2D para loop da célula superior esquerda para a célula inferior direita).
body
agora é garantido saber quais células ele ocupa.
Parte 2: verificações reais de colisão
Para cada um Body
body
:
body.gridInfo.handleCollisions
é chamado:
void GridInfo::handleCollisions(float mFrameTime)
{
static int paint{-1};
++paint;
for(const auto& c : cells)
for(const auto& b : c->getBodies())
{
if(b->paint == paint) continue;
base.handleCollision(mFrameTime, b);
b->paint = paint;
}
}
void Body::handleCollision(float mFrameTime, Body* mBody)
{
if(mBody == this || !mustCheck(*mBody) || !shape.isOverlapping(mBody->getShape())) return;
auto intersection(getMinIntersection(shape, mBody->getShape()));
onDetection({*mBody, mFrameTime, mBody->getUserData(), intersection});
mBody->onDetection({*this, mFrameTime, userData, -intersection});
if(!resolve || mustIgnoreResolution(*mBody)) return;
bodiesToResolve.push_back(mBody);
}
A colisão é então resolvida para cada corpo em
bodiesToResolve
.É isso aí.
Então, eu tenho tentado otimizar essa detecção de colisão de fase ampla por um bom tempo agora. Toda vez que tento algo diferente da arquitetura / configuração atual, algo não sai como o planejado ou assumo a simulação que mais tarde provou ser falsa.
Minha pergunta é: como posso otimizar a fase ampla do meu mecanismo de colisão ?
Existe algum tipo de otimização mágica de C ++ que pode ser aplicada aqui?
A arquitetura pode ser redesenhada para permitir mais desempenho?
- Implementação real: SSVSCollsion
- Body.h , Body.cpp
- World.h , World.cpp
- Grid.h , Grid.cpp
- Cell.h , Cell.cpp
- GridInfo.h , GridInfo.cpp
Saída do Callgrind para a versão mais recente: http://txtup.co/rLJgz
fonte
getBodiesToCheck()
foi chamada 5462334 vezes e ocupou 35,1% de todo o tempo de criação de perfil (tempo de acesso de leitura da instrução)Respostas:
getBodiesToCheck()
Pode haver dois problemas com a
getBodiesToCheck()
função; primeiro:Esta parte é O (n 2 ), não é?
Em vez de verificar se o corpo já está na lista, use a pintura .
Você está referenciando o ponteiro na fase de coleta, mas referenciando-o na fase de teste, portanto, se você tiver L1 suficiente, não é grande coisa. Você pode melhorar o desempenho adicionando dicas de pré-busca ao compilador também
__builtin_prefetch
, por exemplo , embora isso seja mais fácil comfor(int i=q->length; i-->0; )
loops clássicos e similares.Essa é uma alteração simples, mas meu segundo pensamento é que poderia haver uma maneira mais rápida de organizar isso:
No entanto, você pode usar bitmaps e evitar o
bodiesToCheck
vetor inteiro . Aqui está uma abordagem:Você já está usando chaves inteiras para corpos, mas depois as procura em mapas e outras coisas e mantém listas delas. Você pode mover para um alocador de slot, que é basicamente apenas uma matriz ou vetor. Por exemplo:
O que isso significa é que todo o material necessário para realizar as colisões reais está na memória linear compatível com o cache, e você só vai para o bit específico da implementação e o anexa a um desses slots se for necessário.
Para rastrear as alocações nesse vetor de corpos, você pode usar uma matriz de números inteiros como um bitmap e usar ajustes de bits ou
__builtin_ffs
etc. Isso é super eficiente para mover para os slots que estão ocupados no momento ou para encontrar um slot desocupado na matriz. Às vezes, você pode compactar a matriz, se ela crescer excessivamente grande e depois os lotes forem marcados como excluídos, movendo-os no final para preencher as lacunas.verifique apenas para cada colisão uma vez
Se você verificou se a colide com b , não precisa verificar se b colide com a também.
Resulta do uso de IDs inteiros que você evita essas verificações com uma simples instrução if. Se o ID de uma colisão em potencial for menor ou igual ao ID atual que está sendo verificado, ele poderá ser ignorado! Dessa forma, você só verificará cada par possível uma vez; isso representa mais da metade do número de verificações de colisão.
respeitar a ordem das colisões
Em vez de avaliar uma colisão assim que um par for encontrado, calcule a distância para atingi-la e armazená-la em uma pilha binária . Esses montes são como você normalmente faz filas prioritárias na localização de caminhos, assim como um código utilitário muito útil.
Marque cada nó com um número de sequência, para poder dizer:
Obviamente, depois de reunir todas as colisões, comece a saltar da fila de prioridade, o mais rápido primeiro. Portanto, o primeiro a ser obtido é A 10 e C 12 em 3. Você incrementa o número de sequência de cada objeto (o 10 bits), avalia a colisão, calcula seus novos caminhos e armazena suas novas colisões na mesma fila. A nova colisão é A 11 atinge B 12 em 7. A fila agora tem:
Então você sai da fila de prioridade e seu A 10 atinge B 12 em 6. Mas você vê que A 10 é obsoleto ; A está atualmente em 11. Portanto, você pode descartar essa colisão.
É importante não se preocupar em tentar excluir todas as colisões antigas da árvore; remover de um monte é caro. Simplesmente descarte-os quando você os estourar.
a grade
Você deve considerar usar um quadtree. É uma estrutura de dados muito simples de implementar. Muitas vezes, você vê implementações que armazenam pontos, mas eu prefiro armazenar rects e armazenar o elemento no nó que o contém. Isso significa que, para verificar colisões, você só precisa iterar sobre todos os corpos e, para cada um, verificar esses corpos no mesmo nó de quatro árvores (usando o truque de classificação descrito acima) e todos aqueles nos nós de quatro árvores principais. O quad-tree é ele próprio a lista de possíveis colisões.
Aqui está um Quadtree simples:
Armazenamos os objetos móveis separadamente porque não precisamos verificar se os objetos estáticos vão colidir com alguma coisa.
Estamos modelando todos os objetos como caixas delimitadoras alinhadas ao eixo (AABB) e os colocamos no menor QuadTreeNode que os contém. Quando um QuadTreeNode muitos filhos, você pode subdividi-lo ainda mais (se esses objetos se distribuírem bem nos filhos).
Cada tick do jogo, você precisa recuar para o quadtree e calcular o movimento - e as colisões - de cada objeto móvel. Ele deve ser verificado quanto a colisões com:
Isso irá gerar todas as colisões possíveis, sem ordem. Você então faz os movimentos. Você precisa priorizar esses movimentos à distância e 'quem se move primeiro' (qual é o seu requisito especial) e executá-los nessa ordem. Use um monte para isso.
Você pode otimizar este modelo quadtree; você não precisa realmente armazenar os limites e o ponto central; isso é totalmente derivável quando você anda na árvore. Você não precisa verificar se um modelo está dentro dos limites, apenas verifique de que lado está o ponto central (um teste de "eixo de separação").
Para modelar objetos voadores rápidos, como projéteis, em vez de movê-los a cada passo ou ter uma lista separada de 'marcadores' que você sempre verifica, basta colocá-los no quadtree com o reto do voo para várias etapas do jogo. Isso significa que eles se movem no quadtree muito mais raramente, mas você não está checando balas contra paredes distantes, por isso é uma boa escolha.
Objetos estáticos grandes devem ser divididos em partes componentes; um cubo grande deve ter cada face armazenada separadamente, por exemplo.
fonte
Aposto que você tem uma tonelada de erros de cache ao percorrer os corpos. Você está reunindo todos os seus corpos usando algum esquema de design orientado a dados? Com uma fase larga N ^ 2, posso simular centenas e centenas , enquanto grava com fraps, de corpos sem queda de taxa de quadros nas regiões inferiores (menos de 60), e tudo isso sem um alocador personalizado. Imagine o que pode ser feito com o uso adequado do cache.
A pista está aqui:
Isso imediatamente levanta uma enorme bandeira vermelha. Você está alocando esses corpos com novas chamadas brutas? Existe um alocador personalizado em uso? É mais importante que você tenha todos os seus corpos em uma enorme variedade na qual você atravessa linearmente . Se atravessar a memória linearmente não for algo que você consiga implementar, considere o uso de uma lista intrusivamente vinculada.
Além disso, você parece estar usando std :: map. Você sabe como a memória no std :: map é alocada? Você terá uma complexidade O (lg (N)) para cada consulta de mapa, e isso provavelmente pode ser aumentado para O (1) com uma tabela de hash. Além disso, a memória alocada pelo std :: map também afetará terrivelmente o seu cache.
Minha solução é usar uma tabela de hash intrusiva no lugar de std :: map. Um bom exemplo de listas intrusivamente vinculadas e tabelas de hash intrusivas está na base de Patrick Wyatt em seu projeto de coho: https://github.com/webcoyote/coho
Portanto, em resumo, você provavelmente precisará criar algumas ferramentas personalizadas para você, como um alocador e alguns contêineres intrusivos. É o melhor que posso fazer sem criar um perfil do código para mim.
fonte
new
ao enviar corpos para ogetBodiesToCheck
vetor - você quer dizer que isso está acontecendo internamente? Existe uma maneira de evitar isso enquanto ainda temos uma coleção de corpos de tamanho dinâmico?std::map
não é um gargalo - também me lembro de tentardense_hash_set
e não obter nenhum tipo de desempenho.getBodiesToCheck
chamadas por quadro. Suspeito que a constante limpeza / inserção do vetor seja o gargalo da própria função. Ocontains
método também faz parte da desaceleração, mas desde quebodiesToCheck
não tem mais de 8-10 corpos nele, ele deve ser tão lentoReduza a contagem de corpos para verificar cada quadro:
Apenas verifique os corpos que realmente podem se mover. Objetos estáticos só precisam ser atribuídos às células de colisão uma vez depois de serem criados. Agora verifique apenas colisões para grupos que contenham pelo menos um objeto dinâmico. Isso deve reduzir o número de verificações em cada quadro.
Use um quadtree. Veja minha resposta detalhada aqui
Remova todas as alocações do seu código de física. Você pode querer usar um criador de perfil para isso. Mas eu analisei apenas a alocação de memória em C #, então não posso ajudar com C ++.
Boa sorte!
fonte
Vejo dois candidatos a problemas em sua função de gargalo:
O primeiro é a parte "contém" - esta é provavelmente a principal razão do gargalo. Ele está interagindo através de corpos já encontrados para cada corpo. Talvez você deva usar algum tipo de hash_table / hash_map em vez de vetor. A inserção deve ser mais rápida (com busca de duplicidades). Mas não conheço números específicos - não tenho idéia de quantos corpos são iterados aqui.
O segundo problema pode ser vector :: clear e push_back. Clear pode ou não evocar realocação. Mas você pode evitá-lo. A solução pode ser uma matriz de sinalizadores. Mas você provavelmente pode ter muitos objetos; portanto, é ineficaz em termos de memória ter uma lista de todos os objetos para cada objeto. Outra abordagem poderia ser legal, mas não sei qual abordagem: /
fonte
Nota: Não sei nada de C ++, apenas Java, mas você deve conseguir descobrir o código. Física é linguagem universal, certo? Também percebo que este é um post de um ano, mas eu só queria compartilhar isso com todos.
Eu tenho um padrão de observador que, basicamente, depois que a entidade se move, ele retorna o objeto com o qual colidiu, incluindo um objeto NULL. Simplificando:
( Estou refazendo minecraft )
Então diga que você está vagando pelo seu mundo. sempre que você ligar
move(1)
, liguecollided()
. se você conseguir o bloco desejado, talvez as partículas voem e você possa se mover para a esquerda direita e para trás, mas não para frente.Usando isso de forma mais genérica do que apenas minecraft como exemplo:
Simplesmente, tenha uma matriz para apontar as coordenadas que, literalmente, como Java faz isso, usam ponteiros.
O uso desse método ainda requer algo diferente do método a priori de detecção de colisão. Você pode repetir isso, mas isso anula o objetivo. Você pode aplicar isso às técnicas de colisão ampla, média e estreita, mas por si só é uma fera, especialmente quando funciona muito bem para jogos 3D e 2D.
Agora, dando mais uma olhada, isso significa que, de acordo com o meu método minecraft collide (), eu terminarei dentro do bloco, então terei que mover o jogador para fora dele. Em vez de verificar o jogador, preciso adicionar uma caixa delimitadora que verifique qual bloco está atingindo cada lado da caixa. Problema resolvido.
o parágrafo acima pode não ser tão fácil com polígonos se você quiser precisão. Para maior precisão, sugiro definir uma caixa delimitadora de polígono que não seja um quadrado, mas não em mosaico. se não, então um retângulo está ótimo.
fonte