Árvore para detectar colisões, objetos próximos etc

Escrevi o código em C++ de uma Interval KD Tree e adicionei na UGDK.
Ela está no arquivo util/intervalkdtree.h (e tem o forward declaration no util.h).

De forma resumida, esta árvore permite:

  • Adicionar itens associados a “caixas” (um hipercubo contendo o item);
  • Fazer uma busca por todos os itens cujas caixas intersectam a caixa passada como parâmetro.

A princípio esta árvore funciona em K dimensões (por isso o “K” no nome dela), mas obviamente só faz sentido utilizá-la em 2D na UGDK.

O nome da classe é ugdk::ikdtree::IntervalKDTree.
Ela recebe dois argumentos de template, o primeiro é a classe dos objetos que ela armazenará, o segundo é o número de dimensões (2 no caso da UGDK).
O construtor recebe o número máximo de elementos que uma folha da árvore pode armazenar. Algo por volta de 5 ou 10 deve estar bom, tem que ajustar dependendo do que for inserir na árvore.

Uma classe auxiliar é a ugdk::ikdtree::Box, que recebe o número de dimensões como argumento de template.
Você só precisa saber construir objetos dela, não é necessário usar nenhum método desta classe.
O construtor recebe dois ponteiros:

  • O primeiro é um vetor de K elementos para as menores coordenadas da caixa;

  • O segundo é análogo, mas para as maiores coordenadas da caixa.
    Ou seja, para construir uma caixa (neste caso um quadrado) que tem vértices (0,0) e (1,2), basta passar estes dois vértices para o construtor. Um jeito é criando dois Vector2D e passando o membro “val”.

  • Para adicionar um item na árvore, basta fazer árvore.Insert(bounding_box, item); A bounding_box é a caixa/quadrado que envolve o item.

  • O método Clear() remove todos os elementos da árvore.

  • O método Update() serve para atualizar a caixa que envolve um item, pois ela (provavelmente) mudou de posição.

  • O método Remove() remove um item da árvore.

  • O método getIntersectingItems(Box) é o mais interessante, pois ele recebe uma Box, e devolve um ponteiro para um vetor contendo todas os itens cujas caixas intersectam esta Box. Não se esqueça de fazer delete do vetor depois. (Seria melhor se o método recebesse o ponteiro para este vetor de resultados como parâmetro, mas depois eu vejo de mudar isso.)

Além disso, o header contém dois parâmetros:
#define COORD_NEG_INFINITY -10
#define COORD_INFINITY 1000
São as coordenadas máxima e mínima que qualquer Box passada para a árvore terá. (Depois eu vejo de colocar isso como argumento do construtor, para cada árvore poder ter os seus próprios parâmetros.)
A diferença entre estes dois números deve ser o menor possível para ficar mais eficiente.

Eu fiz umas gambiarras para o sistema de colisões utilizar esta árvore, mas como as colisões do Horus Eye são bem simples, nem dá para perceber diferença de performance (pelo menos não na minha máquina).
Além disso, eu tive que fazer o sistema de colisões ficar destruindo e reconstruindo as árvores do zero a cada frame, simplesmente porque eu não consegui encontrar um jeito simples de o sistema de colisões saber o seguinte:

  • Quando um objeto que colide muda de posição?
    Assim, eu não pude utilizar o método Update() da árvore quando um item se move. E precisei manter uma lista auxiliar de todos os itens da árvore para poder reconstruí-la.

Acredito que, para o Horus Eye, essa árvore traria mais benefícios se utilizada na parte gráfica. Se houvesse uma árvore dessas com todas as imagens e suas posições (finais) na tela, seria fácil descobrir quais imagens devem ser desenhadas na tela. No momento, acredito que ele desenhe todas as imagens, independente delas estarem ou não fora da tela.
E, assim como no sistema de colisões, seria bom poder chamar o método Update() quando uma imagem muda de posição, pois é mais eficiente do que ficar reconstruindo a árvore a cada frame.

And this is Jeff. This if Jeff On Games. Sorry, não resisti.

Observações, sugestões e outros:

  1. Eu quero saber como funciona XD.

  2. Não entendi o problema do Update(). Se ele existe pra atualizar a posição, por que ele não consegue? Se o problema for realmente apenas “Não saber quando um objeto se move” então o que podemos fazer é que todos os WorldObjects podem mudar suas posições apenas usando um método Move(), e assim fica fácil saber quando é preciso atualizar a posição.

  3. Pelo que vi, parece que você está usando vetores do C. Não tem como mudar para contêineres STL?

  4. Sobre o getIntersectingItems(Box), um outro jeito de garantir que a memória seja liberada é fazer ele devolver um shared_ptr. Assim quem chama também tem que usar shared_ptr, e naturalmente a memória será liberada. Ah, e o nome desse método não está no padrão, mas os outros sim. Ou é typo?

  5. O sistema de colisões já faz uma verificação se os objetos estão próximos usando norma 1. Quais as vantagens da Interval KD Tree sobre isso? Ela evita inclusive essas comparações?

Eu realmente gostaria de ir dar uma olhada com mais calma, mas preciso acabar o relatório do Giuliano hoje >.<

EDIT: Esqueci de uma coisa importante! Código que lida com colisão nós estávamos a princípio colocando na PyramidWorks (que seria uma Engine intermediária entre a UGDK e o Horus, mas atualmente se resume a uma pasta dentro do Horus). Tratamento de colisão é meio que “muito alto nível” para a UGDK. Embora possamos discutir isso.

Vou te perdoar pela primeira linha. :wink:

  1. Não sei se eu saberia explicar bem, mas o site da Wikipedia já dá uma idéia razoável de uma KD Tree (que serve apenas para pontos). Na Interval KD Tree, a idéia é que a raiz da árvore é uma caixa, e cada um dos dois filhos é uma parte desta caixa. Na implementação que eu fiz, cada filho é exatamente metade da caixa*. E a cada nível da árvore, essa divisão é feita em um eixo diferente (o eixo de divisão é modular/cíclico). Então alguns itens ficam no próprio nó, e quando o nó passa de N itens, ele tem dois filhos e as caixas/itens que estiverem totalmente contidas em algum dos filhos vão para o filho. As caixas que estiverem fazendo interseção com a divisão continuam no próprio nó, sem ir para os filhos. E quando um item é removido, um pai pode fazer Merge() com os filhos.
    Resumindo, ela não é necessariamente balanceada, mas se você passar parâmetros bons e inserir itens mais ou menos espalhados geometricamente, vai ficar boa para a grande maioria das buscas de colisões.
  • Mas depois dá para testar outras técnicas mais complexas, o problema é que agora não tem nada computacionalmente difícil para testarmos no Horus Eye e ver se realmente melhora a eficiência ou só complica.
  1. Existem alguns problemas. O primeiro é este que você citou, não dá para saber exatamente quando um objeto se moveu. No máximo existe um setter, que não significa necessariamente movimento: ele pode ter sido iniciado em uma posição padrão como 0,0 e depois foi setado para a correta. Sair colocando vários objetos em posições próximas no começo só para depois removê-los não seria muito bom para a árvore, mas isso é o de menos. O maior problema é que a posição final é composta por vários fatores. Tem vários Vector2D de posição e offset em alguns lugares, e o WorldObject é só o começo. E o que eu preciso saber mesmo é a posição da Shape, pois ela é que tem uma forma, e portanto pode me dar uma BoundingBox. O outro “problema” é que nem todos os WorldObjects colidem, ou possuem forma física. Então eles realmente não tem muito a ver com estas colisões, embora sua posição influencie no final. Enfim, é tudo bem confuso, e eu não entendi direito e acabei fazendo do jeito que deu para fazer.

  2. Talvez eu esteja enganado e falando besteira, mas se você estiver falando do que eu acho que está, eu não vejo vantagem alguma. Os vetores são de tamanho fixo, e o tamanho é passado como argumento do template. Eles não aumentam de tamanho, não diminuem, e servem apenas para guardar coordenadas. Nesse sentido, são como os Vector2D, com a vantagem que servem para qualquer número de dimensões >= 1. Aliás, se você for ver a implementação de Vector2D, ele também armazena coordenadas assim no membro val.

  3. Não sei se eu quero usar shared_ptr só para uma coisa simples dessa, eu queria manter a implementação disso o mais independente/modular possível, sem depender de nada externo. E o nome foi typo mesmo, mas está assim no código =(
    Mas você está falando só do nome não ser maiúsculo, ou tem mais alguma coisa? Ah, se você quiser arrumar o nome na UGDK e no Horus Eye, deve haver no máximo umas 4 ocorrências dele.

  4. Então, como eu disse, as colisões do Horus Eye estão tão simples no momento que nem dá para perceber diferença. O que acontece é que se houvessem MUITOS objetos “colidíveis” na tela, faria diferença. Do jeito que estava, era um algoritmo quadrático, comparando todos os objetos com todos e vendo se estavam colidindo. Tem otimizações para testar apenas “classes” de objetos, mas isto não muda a complexidade, apenas a constante, e esta otimização também pode ser usada junto com a árvore. A vantagem de usar uma árvore é que, em média, se você passar os parâmetros corretos para a árvore (depois eu percebi que aqueles #defines fazem muito mais diferença do que eu imaginava), ela ficará mais ou menos balanceada. Aí, cada busca é O(lg n), e como você testa N objetos, fica algo em torno de O(n lg n), ao invés de O(n^2). É claro que você pode dar muito azar e a árvore ficar bem desbalanceada, mas aí continua sendo O(n^2) como no algoritmo anterior, apenas com uma constante diferente.

Sobre seu EDIT) Esta árvore não trata necessariamente de colisões, e pode ser útil em outros contextos. O outro exemplo que eu tinha dado é uma árvore de imagens a serem desenhadas, para “colidir” com a tela e desenhar apenas as que podem ser vistas. E um outro exemplo é em um algoritmo de AI, para “colidir” com as Creatures próximas, fazendo com que a AI saiba/veja quem está por perto de forma mais eficiente.

Pelo que eu vi de um profile no Horus Eye, o que está deixando ele lento atualmente são funções de OpenGL. Então acho que esta deveria ser a prioridade para otimizações, embora não faça mal já ir utilizando coisas eficientes e relativamente simples em outros lugares (o problema são coisas eficientes mas complicadas de usar).

Obs.: A minha análise de complexidade da árvore foi bem superficial. Na verdade ela também depende da quantidade de itens que a busca deve retornar, mas ignorei isso porque normalmente ela retorna entre 0 e 2 itens na parte de colisões. Ou seja, se você fizer uma busca por uma caixa do tamanho do universo que deve retornar todos os itens, é claro que não será O(lg n), não tem como, mas estou supondo que você não usará a árvore para coisas assim.

Oba, fiz mais um post gigante! ;D

Here we go!

  1. Hummm… Interessante. Pelo que imagino, mesmo ela ficando “desbalanceada”, não significa necessariamente que fique ruim. Significa apenas que existe um lugar no mundo com muitos objetos aglomerados, e de qualquer jeito isso complicaria.

  2. Estou pensando soluções para isso. Aguarde a próxima temporada.

  3. Não, eu quis me referir ao fato da função getIntersectingItems devolver um vetor (do C) de itens. Fica mais uniforme com o resto do código se ela devolver um vector ou uma list (nem que seja uma referência para um buffer interno).

  4. Bom, vc pode implementar um smart ptr vc mesmo que garanta a liberação da memória. Quem sabe até usar um pool ou uma lista livre?

  5. Era o que eu imaginava mesmo.

EDIT) Eu acho que vai ser meio complicado usar a árvore pra detectar sprites fora da tela, devido ao modo como a renderização funciona atualmente. Basicamente, o render navega por um grafo e transformações compostas, então ele não tem uma noção mto boa do “todo”, até porque existem scales no meio do caminho. Você acha que dá?

Mas de fato, existem duas coisas que atualmente deixam o Horus mais lento: desenhar coisas que não estão na tela, e derivados da função SpreadLight no World.

Fora isso, parece que seu código deu um erro no PC do coelho. Foi um assertion failed sobre “container_items_.count(element) != 0” na linha 150 do intervalkdtree.h.

E, por enquanto, parece que o FPS diminuiu no PC do Henrique, embora eu ache que seja por você estar construindo a árvore todo frame.

Para saber de onde veio tudo isso, entre no skype!

  1. Se objetos aglomerados desbalancearem a árvore, então é certeza, ela vai ficar desbalanceada. Em alguns mapas temos lugares com bem mais paredes que outros, mas isso acho que não é muito importante… O mais importante é: os mapas tendem a ter grandes grupos de mumias em um lugar, e as múmias tendem a se agrupar para seguir/atacar o herói.
    Ou seja, do jeito que o Horus Eye tá hoje, aglomerações são comuns de acontecer.
    Como já pensei antes com umas mudanças de AI (e umas outras AIs diferentes) parte disso poderia mudar, mas não acho que ia ficar muito diferente (se o gameplay não mudar muito do que é).

Do resto… Sei lá lol
Mas achei interessante isso ai Jeff (On Games? :P).
Porém bem complexo pra discutir pelo fórum, quem sabe numa reunião próxima falamos mais disso (eu principalmente para a parte de AI, mas imagino que pelo menos o Wil queira falar sobre outras coisas lol).

Ninja-doublepost!

Jeff, estava terminando aqui de mudar o Horus para usar o Update da KTree ao invés de ficar apagando e criando todo frame e cheguei num problema.

As colisões funcionam todas corretamente, o fps aumentou bastante mas aparentemente objetos removidos continuam existindo no último local que existiam.
Dei uma olhada no código da KTree, e suspeito fortemente que você deu uma esquecida de terminar o Remove.
Criei uma branch no horus_eye com as mudanças.

Opa!
É, suas suspeitas estavam corretas… Sorry! :-\

Achei que tinha terminado de implementar tudo, mas vendo agora, a função Remove realmente não estava completa.
Já a completei e também corrigi alguns bugs que surgiram quando fui testá-la.

Parece que agora está tudo funcionando, e realmente o fps aumentou. ;D

Err… double-post!

Removi aqueles #defines feios que eu tinha colocado na árvore, agora os limites máximos (e mínimos) das coordenadas dos objetos inseridos nela são passados no construtor, em um objeto Box.
Ou seja, todos os bounding boxes passados no método Insert e Update devem estar contidos neste Box passado no construtor da árvore. E para um Box estar contido neste do construtor, suas coordenadas mínimas devem ser estritamente maiores do que as coordenadas mínimas do box do construtor. Isso é porque a árvore divide o espaço em intervalos ]a, b] em todas as dimensões, mas os objetos inseridos possuem intervalos [a,b].

E com isso acabei encontrando mais um problema na (falta de) arquitetura do Horus Eye: por algum motivo que eu prefiro nem saber, os objetos de CollisionClass são inicializados junto com o jogo. Mesmo antes de começar a jogar, NO MENU, todos eles já foram instanciados e nunca mais são recriados, nem mesmo com uma mudança de fase. Eu tinha feito um código para eles inicializarem a árvore com o tamanho da fase, imaginando que eles seriam criados com o carregamento dela, mas vendo isso, o código se tornou inútil.

Eu fiquei realmente perplexo ao saber que o menu precisa de todo um sistema de colisões.

Bem, como não tinha essa necessidade não fazia sentido recriar as classes de colisão. Em particular vc só fala os nome de uma classe e o pai dela e o sistema se preocupa em criar e tudo mais.
O World usava essas classes como uma forma de fazer as colisões, mas era algo completamente desvinculado.

Agora pelo visto você quer que as CollisionClass sejam algo que um World possui, pois depende do tamanho deste. É simplesmente uma mudança de necessidades e a arquitetura vai mudar um pouco, tornando a inicialização destas um pouco mais elaborada.

Não acho que está errado ou ruim a forma atual: era muito simples de configurar e o utilizar!

Discordo, do jeito que está, parece que todos os objetos do CollisionClass são variáveis globais: são instanciados assim que o programa abre, e destruídos quando o programa fecha.

Era muito “simples de configurar e o utilizar”, assumindo que você quisesse que o sistema continuasse fazendo exatamente o que ele já estava fazendo. Mesmo que não houvesse necessidade de serem recriados a cada fase, é algo lógico considerando seu papel: armazenar os objetos de colisão de uma “classe”, objetos estes que mudam a cada fase do jogo.

Uma boa arquitetura não só deve prever mudanças nas necessidades, mas deve abraçar estas mudanças e torná-las simples de serem feitas.

E não entendi o que você quis dizer com “O World usava as classes, mas era algo completamente desvinculado”. Se o World usava, como que era desvinculado?

A impressão que eu tenho é que muita coisa no Horus Eye foi feita com um pensamento mais ou menos assim: vamos fazer o que for mais fácil para nós fazermos agora, de acordo com o que nós precisamos agora, e já otimizando de acordo como as coisas funcionam agora. É um tipo de pensamento muito comum entre programadores (e imagino que em outras áreas também), mas que leva a diversos problemas. Ninguém pensa que as necessidades estão sempre mudando, que aquele código precisará ser modificado para fazer algo bem diferente, que outras pessoas precisarão entender como as coisas funcionam e, principalmente, ninguém pensa que alguém que não sabe e nem quer saber nada de programação pode querer usar e talvez até customizar aquele programa. Por isso que temos essa arquitetura completamente engessada, que a cada mudança de necessidades precisa ser modificada. É claro que isso está começando a mudar e discutimos bastante uma nova arquitetura para o Horus Eye, mas é bom ter em mente o que levou à arquitetura antiga e o tipo de pensamento que precisa mudar.

Hummmm…

Eu entendo que vocês talvez queiram discutir isso mais a fundo, mas cuidado pra não ficar muito offtopic aqui. Como o tópico se destina ao assunto da IKDTree, foquemo-nos em como resolver o problema. Depois, vocês podem discutir o quanto quiserem sobre isso em outro tópico.

Não acho que esteja tão off-topic assim.

Para resolver o problema, precisamos mudar a arquitetura. E ao mudar a arquitetura, é bom fazer de uma forma que a nova arquitetura não tenha os problemas da arquitetura atual. Se nos focarmos apenas no problema atual e em como resolvê-lo agora, estaremos perpetuando exatamente o tipo de pensamento que eu fui explicitamente contra no meu último post.

Jeff, você sabe muito bem que não foi isso que eu quis dizer. Todos nós sabemos o que uma boa arquitetura deve apresentar. Estou apenas tentando otimizar a discussão pois o modo como você a estava levando não estava ajudando. O foco dessa thread é a IKDTree (e como ela pode ser usada/melhorada), e não os problemas arquiteturais do Horus. Se existe um problema atrapalhando a IKDTree, discuta apenas como resolvê-lo da melhor maneira possível.

edit2: Meu post como estava antes também estava offtopic. Desculpem. Sem mais desse assunto. Sabemos que a arquitetura do Horus precisa e vai ser melhorada. Issa já está marcado pra próxima reunião. Bolaremos um jeito que use melhor a IKDTree, e talvez seja bom postarmos aqui o que foi decido sobre ela e o porquê.