Colisões!! é o tema dessa aula!
Colisões é um tópico de fundamental importância em jogos. Na verdade eu nunca vi um jogo que não tivesse colisões… talvez fosse um bom tema para o desafio de game design do Gamasutra, jogo sem colisões… rsrsrs.
Os elementos básicos que veremos de colisão no Panda3D são: Geometrias, sólidos, CollisionNodes, CollisionHandlers, Traverser e entradas de colisão.
Geometrias e Sólidos
No Panda3D, da mesma forma que em qualquer game engine que se prese, é possível se testar colisões entre geometrias (mesh) e sólidos de colisão. Não entendeu? então vamos esclarecer…
Pegando como exemplo a cena do nosso demo (abaixo) temos dois modelos 3D principais, o avatar e a plataforma, se não me engano o avatar possui uns 200 polígonos, e a plataforma completa uns 350 polígonos. Para fazer o nosso avatar andar e pular pela plataforma, nós precisamos fazer teste de colisões entre esses dois modelos, para que o avatar não transpasse a plataforma, certo? Então para fazer os testes de colisão entre as geometrias mesh no nosso demo, a cada frame o Panda3D vai ter que fazer 70.000 teste de posicionamento, só para testar o avatar e a plataforma… é muito teste, e isso vai fazer nosso jogo muito lento. Bem, eu faltei um pouco com a verdade aqui, o Panda consegue otimizar esses teste, só testando polígonos que estejam próximos o suficiente… então os testes cairiam para uns 1.000 testes por frame… ainda sim é muito teste.
Ora, em alguns casos isso é interessante… imagine que vc tem um helicóptero, e que vc precisa saber onde os tiros o atingiram, na cauda, na cabine, na hélice, no motor… então se justifica essa necessidade. Mas ainda assim, existe como você melhorar esses testes, para que eles não sejam feitos a todo frame, e apenas quando for necessário. Mas isso é assunto para outro post.
Voltando para nosso demo, não é necessário que sejam realizados 70.000 testes/frame para que o avatar não traspasse a plataforma, e para isso é que existem os sólidos de colisão, que são formas geométricas simples, como uma esfera ou plano, que possuem estruturas internas no Panda3D, que otimizam os teste de colisão.
::: Geometrias
O tipo de arquivo para modelos 3D no Panda, é o arquivo .egg, que é uma arquivo de texto com uma estrutura parecida com XML, veja o exemplo:
<CoordinateSystem> { Z-up }
<Comment> { “Egg laid by Chicken for Blender v1.0″ }
<Texture> base05_Low_TEX_1024.j {
“../texturas/base05_Low_TEX_1024.jpg”
}
[…]
<Group> plataforma {
<Collide>{Polyset keep descend}
<Transform> {
<Matrix4> {
0.000000 -0.798657 0.000000 0.000000
0.798657 0.000000 0.000000 0.000000
0.000000 0.000000 0.798657 0.000000
-0.257677 0.000000 -2.429399 1.000000
}
}
<VertexPool> plataforma {
<Vertex> 0 {
-2.208247 0.271071 -2.336218
<UV> { 0.802243 0.147077 }
<Normal> { 0.000000 0.000000 1.000000 }
}
<Vertex> 1 {
-2.303914 0.402745 -2.336218
<UV> { 0.831482 0.168320 }
<Normal> { 0.000000 0.000000 1.000000 }
}
[…]
}
<Polygon> {
<TRef> { base_ponte_Low_TEX_10 }
<Normal> { 0.000000 1.000000 0.000000 }
<VertexRef> { 3261 3262 3263 3264 <Ref> { plataforma } }
}
<Polygon> {
<TRef> { base_ponte_Low_TEX_10 }
<Normal> { 0.000000 0.000000 -1.000000 }
<VertexRef> { 3265 3266 3267 3268 <Ref> { plataforma } }
}
}
Esse formato “nada mais é do que uma lista” dos vértices, polígonos, texturas e UVs, que formarão o modelo no mundo 3D. Mas o detalhe que mais nos interessa para as colisões está naquela linha logo abaixo do “<Group> plataforma {” ; o <Collide>{ Polyset keep descend }. Quando o arquivo é criado, ele não vem com essa linha, é necessário adicioná-la. É essa linha que torna esse objeto passível de ( ou ativo à ) colisão. Então sempre que quiser um modelo passível de colisão… <Collide>{ Polyset keep descend }.
::: Sólidos
Como dito antes os sólidos de colisão são forma geométricas simples, e a lista é:
Esfera : CollisionSphere(x,y,z , raio)
Esfera Invertida: CollisonInvSphere(x,y,z , raio)
Tudo : CollisionTube(x1,y1,z1 , x2,y2,z2 , raio)
Plano : CollisonPlane( Plane(Vec3(x,y,z)), Point3(x,y,z) )
Raio : CollisionRay( x,y,z , Dx,Dy,Dz ) > D=direcão
Linha: CollisionLine( x,z,y , Dx,Dy,Dz )
Segmento: CollisionSegment( x1,y1,z1 , x2,y2,z2 )
Para o demo usaremos apenas o raio e a esfera.
CollisionNodes (ou nós de colisão)
Na aula 2, para o avatar nós criamos um nó chamado ‘persona’, e um sub-nó chamado ‘personaActor’ que recedeu um modelo 3D, e por isso podemos chamá-lo de GeomNode (nó de geometria).
Para colisões acontece algo muito semelhante, quando usamos um sólido de colisão, temos que adicionar um CollisionNode, um nó que irá tratar das colisões, isso é necessário porque só um nó de colisão poderá reagir a comandos do CollisionHandler, que é o objeto que “define o comportamento das colisões”, dessa forma o esquema de hierarquia do nosso nó ‘persona’ ficará assim:

Lembrando que para cada sólido é necessário um CollisionNode distinto.
CollisionHandlers
Esse é o objeto que define como as colisões deve ser tratadas. Cada nó de colisão só pode ser associado a um Collision Handler, e para diferenciar esses comportamentos existe uma lista deles que podem ser usados como, CollisionHandlerQueue, CollisionHandlerEvent, PhysicsCollisonHandler …
Eu vou entrar em maiores detalhes de três deles que serão usados no nosso demo, o CollisionHandlerEvent, CollisionHandlerFloor e o CollisionHandlerPusher.
Na minha opinião eles são os mais simples de se usar quando estamos iniciando na ferramenta.
O CollisionHandlerFloor se preocupa apenas em manter o nó ”colado” ao chão, e caso o nó fique acima do chão, ele o traz de volta em uma velocidade constante. Pronto, só.
O CollisionHandlerPusher se preocupa apenas em não permitir que um nó traspasse outros sólidos, tipo uma parede… pronto, só.
O CollisionHandlerEvent, já é mais legal…
antes de começar a usá-lo, nós definimos alguns padrões de comportamento, tipo um sólido colide em uma geometria, um sólido colide com um sólido etc… existe uma listinha grande de possibilidades. Aí então ele vai começar a gerar evento a medida em que as coisas colidem, e essas eventos podem ser capturado e tratados da mesma forma que os eventos de input de teclado são tratados, com uma função de algum objeto, ou vários objetos ao mesmo tempo… sei lá.
Collision Traverser
O Traverser é o objeto que irá realizar todos os testes de colisão, e distribuir as tarefas aos respectivos handlers. Ele não tem muito mistério, quando trabalhamos em um nível mais iniciante, não se preocupem.
Entradas de colisão
Quando uma colisão é detectada, é gerada uma Collision Entry, que guarda consigo as informações pertinentes àquela colisão, tipo quem colidiu com quem, em que ponto relativo ao render, qual a normal da colisão, etc…
Essa entrada é gerada, e enviada até a função que trata a colisão. Outra coisa é que cada Handler possui uma forma diferente de tratar as colisões, então cada um tem uma particularidade na forma de se trabalhar com a entrada de colisão. Mas essa variável é sempre de grande importância para as colisões.
Mãos na massa:
Agora que vimos de forma rápida os elementos básicos de colisão, vamos ao código. O objetivo agora será, criar dois CollisionNode no avatar, um deles com uma esfera outro com um raio, e então definir que a esfera terá o trabalho de um Pusher (não permitindo que o avatar atravesse paredes) e que o raio terá o trabalho de um Floor (gerando uma gravidade). Na primeira parte vamos criar os sólidos, só depois adicionar o comportamento a eles.

Adicionar à classe Avatar uma função para fazer esse trabalho, a função criaColliders() - lembre-se de colocar a chamada da função no método construtor __init__(self): - Vamos lá:
def criaColliders
(self):
#ESFERA + PUSHER
self.
avatarEsferaCN = CollisionNode
(‘avatarEsfera’)
self.
avatarEsferaCN.
addSolid( CollisionSphere
(0,
0,
0, .
9) )
self.
personaEsfNP =
self.
persona.
attachNewNode(self.
avatarEsferaCN)
self.
personaEsfNP.
show()
#RAY + FLOOR
self.raio = CollisionRay(0,0,-1 , 0,0,-1)
self.avatarRaioCN = CollisionNode(‘avatarRaio’)
self.avatarRaioCN.addSolid( self.raio )
self.personaRaioNP = self.persona.attachNewNode( self.avatarRaioCN )
self.personaRaioNP.show()
Vamos a uma explicação. Primeiro Bloco:
self.avatarEsferaCN = CollisionNode(‘avatarEsfera’) >> cria um nó de colisão chamado ‘avatarEsfera’
self.avatarEsferaCN.addSolid( CollisionSphere(0,0,0 , .9) ) >> cria o sólido de colisão esfera
self.personaEsfNP = self.persona.attachNewNode(self.avatarEsferaCN) >> vincula o CollisionNode avatarEsferaCN ao nó persona, e guarda o caminho (do CollisionNode) no nodePath ‘personaEsfNP’
self.personaEsfNP.show() >> mando o Panda renderizar o sólido esfera
Segundo Bloco: faz a mesma coisa que o primeiro, porém de forma diferente
self.raio = CollisionRay(0,0,-1 , 0,0,-1) >> cria o sólido de colisão Ray
self.avatarRaioCN = CollisionNode(‘avatarRaio’) >> cria o CollisionNode avatarRaio
self.avatarRaioCN.addSolid( self.raio ) >> adiciona o sólido ao CollisionNode
self.personaRaioNP = self.persona.attachNewNode( self.avatarRaioCN ) >> vincula o CollisionNode avatarRaioCN ao nó persona, e guarda o caminho (do CollisionNode) no nodePath ‘personaRaioNP’
self.personaRaioNP.show() >> manda o Panda renderizar o sólido raio
Observação: Cuidado ao posicionar esses dois sólidos, NÃO DEIXE QUE ELES COLIDÃO, NÃO DEIXE UMA INTERSECÇÃO ENTRE ELES, porque como iremos utilizar o Pusher como um dos Handlers, se ele colidir com o raio, ele irá reagir e mandar o seu avatar para o alto.
Se vc testar o seu código verá algo assim:

para chegar neste ângulo, utilize o mouse. Observe que o raio e a esfera não colidem.
Agora chegou o momento de fazer com que esses sólidos funcionem! Adicionado o Traverser e os Handlers. Isso deve seguir uma ordem, o Traverser deve vir primeiro, depois os CollisionHandlers e então os sólidos, caso contrário surgirão erros. Dessa maneira você pode colocar as linhas relativas ao Traverser e Handlers acima das classes e abaixo dos imports; Ou ainda apenas antes de instanciar os objetos.
#Cria o Traverser, no "base"
base.
cTrav = CollisionTraverser
()
#Cria o Pusher, no "base"
base.pusher = CollisionHandlerPusher()
#Cria o Floor, "base" também
base.floor = CollisionHandlerFloor()
base.floor.setMaxVelocity(1)
Esse ‘base’ é um objeto do Panda3D, que guarda uma pandaca de outros objetos default do jogo, como a câmera por exemplo, então eu gosto de colocar os handlers nele, mas sinta-se livre em discordar. O setMaxVelocity() no objeto floor, seria uma espécie de “gravidade” fajuta, experimente mudar o valor!
Se você testar o programa, verá que nada mudou… porquê? Ainda temos que “setar” os CollisionNodes aos Handlers, caso contrário eles não trabalham. Em ambos os casos isso funciona em duas etapas:
1- temos que dizer ao Traverser, qual CollisionNode vai para qual Handler
2- dizer para o Handler qual NodePath está vinculado ao CollisionNode
Vejamos na prática, esses comandos serão adicionados na função criaColliders():
def criaColliders
(self):
#ESFERA + PUSHER
self.
avatarEsferaCN = CollisionNode
(‘avatarEsfera’)
self.
avatarEsferaCN.
addSolid( CollisionSphere
(0,
0,
1.5, .
8) )
self.
personaEsfNP =
self.
persona.
attachNewNode(self.
avatarEsferaCN)
self.
personaEsfNP.
show()
base.cTrav.addCollider( self.personaEsfNP, base.pusher )
base.pusher.addCollider( self.personaEsfNP, self.persona )
#RAY + FLOOR
self.raio = CollisionRay(0,0,0.5 , 0,0,-1)
self.avatarRaioCN = CollisionNode(‘avatarRaio’)
self.avatarRaioCN.addSolid( self.raio )
self.personaRaioNP = self.persona.attachNewNode( self.avatarRaioCN )
self.personaRaioNP.show()
base.cTrav.addCollider( self.personaRaioNP, base.floor )
base.floor.addCollider( self.personaRaioNP, self.persona )</code>
Agora se você testar o seu código, verá que o avatar começar a cair lentamente, até tocar a plataforma, e que o avatar não vai atravesar nenhuma parede. Faça alguns testes desabilitando um e outro dos Handlers, assim será mais fácil para você ver o papel de cada um.
Como esse momento é um ponto crucial do demo, aqui está o código para que quiser baixar.
Até aqui nós vimos como colidir o avatar com uma geometria (plataformaBase), agora vamos fazer ele colidir com um outro elemento de jogo, uma célula de energia. É tipo aquele elemento que o avatar toca para restaurar saúde. Aqui nós usaremos o HandlerEvent para gerar um evento na hora da colisão. Mas antes vamos pensar sobre como deve funcionar esse sisteminha, vou listar abaixo as reações que espero (caso você discorde, me manda um comentário):
Quando ocorre a colisão
1- a célula desparece
2- a saúde do avatar é incrementada com a ‘carga’ da célula
Isso é bem simples, mas para programar, dá um pouco mais de trabalho. Mas antes de executar isso vamos explorar isso aos poucos. Antes de mais nada criamos a classe CelulaEnergia:
class CelulaEnergia
(DirectObject
):
def __init__(self):
self.
carga =
1
self.
carregaModelo()
self.
criaCollider()
def carregaModelo(self):
self.celula = loader.loadModel(‘../res/eggs/celulaLOW’)
self.celula.reparentTo(render)
self.celula.setScale(.2)
self.celula.setPos(-3, 30, -4)
def criaCollider(self):
self.cNode = CollisionNode( <span>‘cellEsfera’</span> )
self.cNode.addSolid( CollisionSphere(0,0,0,3) )
self.cellCollider = self.celula.attachNewNode(self.cNode)
self.cellCollider.show()</code>
Agora se instanciamos nosso objeto (lá embaixo no código, antes do comando run() ):
objetoCelula = CelulaEnergia()
Agora execute seu código e veja se a célula aparece (se vc segui as mesmas coordenadas que eu, a célula deve aparecer à esquerda do avatar ). A célula deve aparecer cercada pela esfera de colisão, e o avatar não será capaz de transpassá-la.
Finalmente vamos criar o CollisionHandlerEvent, coloque essas linhas juntas com os demais handlers:
base.collEvent = CollisionHandlerEvent()
base.collEvent.addInPattern(‘%fn-into-%in’)
Antes que você pergunte, esse addInPattern(’%fn-into-%in’), é o padrão de colisão que estamos definindo para o nosso HandlerEvent.
::: CollisionHandlerEvent
Como dito antes o CollisionHandlerEvent gera evento quando colisões são detectadas, e existem três tipo de evento que podem ser gerados: o evento “in”, acontece quando um objeto colide com outro que não havia sido detectado antes ; o evento “out”, quando um objeto deixa de colidir com outro que havia sido detectado anteriormente; e o evento “again”, quando um objeto continua a colidir com outro objeto que havia sido detectado anteriormente. E esse padrões são definidos dessa forma:
handler.addInPattern(’%fn-into-%in’)
hanlder.addOutPattern(’%fn-out-%in’)
handler.addAgainPattern(’%fn-again-%in’)
E esse código ’%fn-into-%in’ significa: %fn = “from” node object ; e %in = “into” node object. No Panda podemos entender que o %fn é o objeto ativo da colisão, e o %in é o objeto passivo. Isso é bem relativo, mas serve bem para você que está iniciando.
Existem outras configurações possíveis, veja neste link.
::: Voltando ao código…
Então agora temos que dizar ao Traverser que o CollisionNode da célula deve ser tratado com o base.collEvent … isso será feito dentro da função criaColliders() da classe CelulaEnergia, após a criação do CollisionNode:
base.cTrav.addCollider( self.cellCollider, base.collEvent )
Muito bem, agora a cada colisão será gerado um evento! Mas… ainda não estamos tratando evento nenhum! É verdade, e para tratar um evento nós precisamos de uma accept, e de uma função para fazer o tratamento, certo? Eu vou colocar o accept na função criaColliders():
self.accept( ‘cellEsfera-into-avatarEsfera’, self.colisaoEvento )
Repare no nome do evento: ’cellEsfera-into-avatarEsfera’. Esse nome é o padrão “in” que definimos para nosso base.collEvent, só que no lugar de %fn e %in, estão os nomes dos CollisionNodes desejados.
E a função self.colisaoEvento é a que tratará o evento.
Neste primeiro momento, vamos criar a função colisaoEvento() e aprender sobre a entrada de colisão (CollisionEntry):
def colisaoEvento(self, entrada):
print entrada
Observação: Toda função que trate eventos de colisão DEVE RECEBER COMO ARGUMENTO A ENTRADA DE COLISÃO, SEMPRE.
Por hora tudo que vai acontecer é, assim que houver a colisão, a entrada de colisão será “impressa” terminal:

Na imagem acima você está vendo um resumo das informações contidas na variável ‘entrada’, nela estão as informações de que é o “from node” e o “into node”, o ponto da coisão(relativo ao ‘render’) a normal da colisão. Porém outras informações também pode ser obtidas… experimente o print dir(entrada) :

E ainda algumas dessas opções possui outras “sub-opções” …
e com essas informações você pode fazer objetos sumirem, aparecerem, mudarem de cor etc… faça o seguinte:
def colisaoEvento(self, entrada):
entrada.getFromNodePath().getParent().removeNode()
Execute e veja que ao se tocarem, a célula de energia desaparece. A razão é simples, veja os passos:
Com os valores da, eu ‘peguei’, o nó de colisão(’cellEsfera’), depois ‘peguei’ o pai dela(’celula’), e mandei ele ser removido do grafo de cena. Pronto ele sumiu do jogo!
Então já cumprimos com uma parte da missão: a célula já está sumindo. Agora temos que atualizar a saúde do avatar. Eu vejo duas possibilidades iniciais para fazer isso: 1 - fazer com que a classe Avatar também possa tratar esse evento (bastaria colocar um accept, como o mesmo evento lá); 2- fazer com que a célula envie uma mensagem para o avatar para ele atualizar sua vida. Dentre as opções vou escolher a segunda, porque assim teremos a chance de ver como fazer objetos trocarem informações entre sí, e outra razão é que se tivermos células com deferentes níveis de carga, a coisa fica mais fácil.
Para fazer isso acontecer temos que dar uma variável ’saude’ para o avatar, que deve ser colocada no método construtor(__init__):
class Avatar(DirectObject):
def __init__(self):
self.saude = 1
Pode colocar antes ou depois do self.estados, sem problema. Essa troca de mensagens é feita através de eventos, então iremos mandar um evento da célula para o avatar, com o valor da carga da célula junto, e o avatar vai tratar esse evento. Vamos mandar esse evento, lá da função colisãoEvento() na classe CelulaEnergia():
def colisaoEvento(self, entrada):
entrada.getFromNodePath().getParent().removeNode()
messenger.send(‘col-celula-avatar’, [self.carga])
Esse objeto ‘messenger’ é quem envia eventos usando o método ‘.send’. Entre os parênteses vão o nome do evento, neste caso ‘col-celula-avatar’, e um parâmetro, neste caso a carga da célula [self.carga]. Simples não? Agora temos que mexer no avatar.
No avatar a coisa continua bem simples, na função capturaEventos(), vou colocar um accept para o evento ‘col-celula-avatar’, e criar uma função saudeAvatar(), que fará o trabalho.
Na função capturaEventos(): veja a última linha
def capturaEventos
(self):
self.
accept( ‘arrow_right’,
self.
alteraEstado,
[‘direita’,
1] )
self.
accept( ‘arrow_left’,
self.
alteraEstado,
[‘esquerda’,
1] )
self.
accept( ‘arrow_right-up’,
self.
alteraEstado,
[‘direita’,
0] )
self.
accept( ‘arrow_left-up’,
self.
alteraEstado,
[‘esquerda’,
0] )
#Colisoes
self.accept(‘col-celula-avatar’, self.saudeAvatar)
E agora a função saudeAvatar() fica assim:
def saudeAvatar
(self, carga
):
print "Saude Avatar:"
print self.
saude
print "nova carga:"
print carga
self.saude = self.saude + carga
print "nova saude:"
print self.saude
Por enquanto, eu vou manter essa função com esses ‘prints’, mas quando chegarmos a GUI, essas informações ficarão na interface gráfica.

Observe que nessa função saudeAvatar(), existe o parâmetro ‘carga’. É aqui que chega o valor da carga da célula. Eu particularmente acho isso meio engraçado, porque a informação passou por uma outra função antes de chegar aqui, e a dita função anterior nem tomou conhecimento da ‘carga’, mas isso é assim mesmo, ao menos aqui no Panda3D.
E nessa função é que atualizamos o valor da saúde atual com o valor recebido da ‘carga’.
Basta que você teste o seu código para ver a coisa todo funcionando.
Aqui está o arquivo final dessa aula, espero que tenha gostado. Qualquer coisa manda um comentário.
Games, Panda3D
Panda3D, python, tutorial