Agora precisamos definir colisões para nosso personagem!
Colisões entre objetos em jogos, são um ponto absurdamente importante. Importante por se tratar de algo fundamental para a mecânica do jogo, e também por ser um tópico delicado em termos computacionais. E esse fator técnico nos obriga a algumas reflexões.
Calcular colisões entre objetos, com eficiência, envolve uma série de algoritmos complexos – os quais não vou entrar em detalhes – e que consomem um processamento considerável do computador, e para permitir que essas colisões sejam possíveis em tempo real, é necessário realizar algumas simplificações na descrição dos nossos elementos. Por exemplo: Se temos um modelo de uma cabeça, colisões não serão calculadas para cada depressão ou volume facial, mas simplificamos o formato da cabeça para uma esfera, ou um tubo, uma forma geométrica mais simples. Dessa forma as colisões conseguem ser calculadas de forma a não travar o bom desempenho da renderização.
Essas formas geométricas simples (esferas, cubos, tubos) são tratadas nas game engines como sólidos de colisão, pois serão esses objetos usados para cálculos de colisão.
Cada game engine possui seus próprios sólidos de colisão, e precisamos usá-los para criar uma descrição simplificada de nossos objetos.
No caso de nosso avatar, fiz a escolha de utilizar uma séria de esferas para suas colisões. Veja abaixo:
Porém, essa imagem acima é meramente ilustrativa, no nosso caso, não precisaremos de tanta definição, e vamos colocar esses sólidos de colisão na medida que forem necessários, começaremos apenas com uma esfera aos pés do avatar, para detectar colisões com a plataforma.
Criar um sólido de colisão, não é tudo. É necessário configurar os cálculos de colisões. No Panda3D, temos que adicionar outros dois objetos, um “Collision Traverser”, e um “collision handler”. Vamos adicionar esses objetos todos, e nos preocupar com os problemas depois 🙂 Sim teremos problemas, mas todos solucionáveis. Primeiro vamos criar o “Collision Traverser” e o “collision handler”, no arquivo Panda-tut-00.py:
[code lang=”python”]
from pandac.PandaModules import loadPrcFileData
loadPrcFileData(”,’show-frame-rate-meter 1′)
import direct.directbase.DirectStart
from pandac.PandaModules import *
from direct.task.Task import Task
from direct.showbase.DirectObject import DirectObject
from avatar import Avatar
#game class
class Game(DirectObject):
def __init__(self):
#init collider system
traverser = CollisionTraverser()
base.cTrav = traverser
base.pusher = CollisionHandlerPusher()
base.pusher.addInPattern(“%fn-into-%in”)
base.pusher.addOutPattern(“%fn-out-%in”)
#load platform model
self.plataforma = loader.loadModel(“../assets/eggs/plataformaBase”)
self.plataforma.reparentTo(render)
self.plataforma.setPos(0,10,0)
#load background sky
self.wall = loader.loadModel(‘../assets/eggs/papelParede’)
self.wall.reparentTo(render)
self.wall.setScale(1)
self.wall.setPos(-5, 30, -2)
#avatar
avatar = Avatar()
avatar.setCollision(traverser)
game = Game()
run()
[/code]
O que temos de novo:
– logo no inicio da função __init__(self), temo a criação dos elementos traverser e do base.pusher, que são respectivamente, o “Collision traverser” e o “collision handler”
– logo abaixo da construção do Avatar, temos a linha: avatar.setCollision(traverser). Aqui é o ponto onde criamos no avatar as configurações necessárias para que o mesmo colida com outros objetos.
Vamos a classe Avatar:
[code lang=”python”]
from direct.showbase.DirectObject import DirectObject
from direct.actor.Actor import Actor
from direct.task import Task
from pandac.PandaModules import *
#modulo da classe Avatar
class Avatar(DirectObject):
def __init__(self):
self.persona = render.attachNewNode(‘persona’)
self.personaActor = Actor(‘../assets/eggs/personagem.egg’,
{‘idle’:’../assets/eggs/personagem-parado’,
‘run’ :’../assets/eggs/personagem-correr’,
‘jump’:’../assets/eggs/personagem-pular’}
)
self.personaActor.setScale(1)
self.personaActor.setPos(0,0,5)
self.personaActor.reparentTo(self.persona)
self.persona.setPos(0,10,0)
self.persona.setScale(.15)
self.persona.setH(90)
self.state = { ‘flying’:True,
‘left’ :False,
‘right’ :False
}
taskMgr.add(self.movement, “Avatar Movement”)
#capture keyboard events
self.accept(‘arrow_left’, self.changeState, [‘left’,True])
self.accept(‘arrow_left-up’, self.changeState, [‘left’,False])
self.accept(‘arrow_right’, self.changeState, [‘right’,True])
self.accept(‘arrow_right-up’, self.changeState, [‘right’,False])
def setCollision(self, trav):
#colision nodes and solids
self.ball0 = CollisionSphere(0,0,1.5,1.5)
self.ball0NP = self.persona.attachNewNode(CollisionNode(‘ball0CN’))
self.ball0NP.node().addSolid(self.ball0)
self.ball0NP.show()
trav.addCollider(self.ball0NP, base.pusher)
base.pusher.addCollider(self.ball0NP, self.persona)
def movement(self, task):
if(self.state[‘flying’] == True):
self.persona.setZ( self.persona.getZ() – 0.1 )
if(self.persona.getZ() < -2.3):
self.state[‘flying’]=False
#move sideways
else:
if(self.state[‘left’]):
self.persona.setX( self.persona.getX() – 0.1 )
elif(self.state[‘right’]):
self.persona.setX( self.persona.getX() + 0.1 )
return Task.cont
def changeState(self, key, value):
self.state[key] = value
[/code]
O que há de novo nessa classe:
– logo nas primeiras linhas: from pandac.PandaModules import * . Nesta linha importamos alguns módulos necessários para realizar as configurações de colisão.
– abaixo da função __init__ e antes da função movement, inserimos a função setCollision.
– na função setCollision criamos dois sub nós ao no persona.
Após essas alterações será fácil perceber que o o avatar irá de deslocar de maneira estranha pela plataforma, porém ficará claro que ele está “esbarrando” na plataforma. Esse é o comportamento normal do Collision Handler Pusher. Ele impede que os objetos atravessem uns aos outros.
Outro detalhe importantíssimo! ESPECIALMENTE QUANDO VOCÊ FOR UTILIZAR SEUS PRÓPRIOS MODELOS 3D!!!
Até aqui, foi configurado os objetos traverser e handler, e também a configuração de colisão para o avatar. NÃO FOI FEITA NENHUMA CONFIGURAÇÃO DE COLISÃO PARA A PLATAFORMA!!
Na verdade verdadeira, o arquivo da plataforma já está configurado para colisões, por isso não foi necessário fazê-lo. Vou explicar como foi feito, para que este tutorial não fique incompleto, e que quando você for utilizar seus próprios modelos 3D, possa fazer o mesmo.
Para a plataforma, poder-se-ia (rsrsrsrs) usar sólidos de colisão, mas optei por usar Collision Mesh. Collision Mesh, é utilizar o próprio modelo 3D como sólido de colisão. A limitação é: Seu modelo deve ser simples, e não côncavo!
Quando você converte seus modelos 3D para o formato do Panda3D( .egg), o arquivo final, é essencialmente um arquivo de texto, que você pode abrir em qualquer editor de texto simples, e modificá-lo. Para fazer com que um modelo, simples e não côncavo, seja sensível à colisão, precisamos adicionar uma linha a cada “group”: <Collide>{Polyset keep descend}
Os arquivos .egg são um texto estruturado tal como um XML. Inicialmente, no .egg, é configurado os materiais existentes na geometria. Então iniciam-se os grupos. Um “Group”, é iniciado com sua matriz (informação de posicionamento, e rotação); Logo após, inicia-se uma lista (VertexPool) com informações de todos os vértices que compõe aquela geometria; Finalmente, temos uma lista com informações dos polígonos que compõe a geometria.
Logo, a linha <Collide>{Polyset keep descend}, deve ser colocada no inicio de cada Group. Abra o arquivo plataforma.egg e veja como ele funciona.
[code lang=”xml”]
piso {
{Polyset keep descend}
{
{
1.000000 0.000000 0.000000 0.000000
0.000000 11.226257 0.000000 0.000000
0.000000 0.000000 0.182287 0.000000
0.000000 0.000000 0.000000 1.000000
}
}
piso {
0 {
1.000000 11.226256 -0.182287
}
1 {
1.000000 -11.226257 -0.182287
}
[…]
22 {
-1.000000 11.226261 -0.182287
}
23 {
-1.000000 11.226257 0.182287
}
}
{
{ Material }
{ 0.000000 0.000000 -1.000000 }
{ 0 1 2 3 { piso } }
}
[ … ]
{
{ Material }
{ 0.000003 1.000000 0.000012 }
{ 20 21 22 23 { piso } }
}
}
[/code]
Antes da implementação, vamos entender do que se trata esse tal de CollisionHandlerPusher, e como as colisões serão encaixadas no nosso jogo.
O CollisionHandlerPusher, é um “tratador” de colisões no Panda3D, que não permite que os objetos atravessem uns os outros. Ele desloca o objeto que está se movendo para trás, ou para o lado, desviando-o da intersecção dos objetos.
Outra possibilidade do Collision Handler Pusher, é gerar eventos! Ou seja, quando objetos colidem, ele pode gerar eventos notificando o sistema da colisão. Essa é uma funcionalidade do Collision Handler Event, donde o Collision Handler Pusher é derivado. Logo ele herda essa funcionalidade do Handler Event. A geração de eventos do Collision Handler Pusher, é habilitada nas linhas 20 e 21, do arquivo Panda-tut-00.py.
Implementação —————
Agora que temos nossa colisão básica configurada, falta trabalhar nos detalhes de implementação.
A primeira colisão que temos nessa cena, é quando o avatar cai na plataforma. Até o presente momento, nós fazemos esse cálculo baseado em uma posição no eixo Z. Mas para deixar nosso jogo mais correto, vamos fazer isso com colisão, entre a esfera e a plataforma.
Antes de realizar a implementação
Para começar, vamos apagar as linhas 49 e 50:
[code lang=”python”]
if(self.persona.getZ() < -2.3):
self.state[‘flying’]=False
[/code]
A partir de agora o que irá mudar o state[‘flying’] para False, será a colisão entre a esfera e a plataforma. Vamos criar um novo accept no nosso avatar, para capturar o evento gerado pela colisão (‘ball0CN-into-plataforma’):
[code lang=”python”]
self.accept(‘ball0CN-into-plataforma’, self.changeState, [‘flying’,False])
[/code]
Nesta linha acima, definimos que a colisão entre a esfera e a plataforma, será tratada pela mesma função self.changeState. Observe com detalhe o nome do evento: ‘ball0CN-into-plataforma’. Observe a linha 20 do Panda-tut-00.py, base.pusher.addInPattern(“%fn-into-%in”).
Quando configuramos a geração de eventos pelo pusher, e usamos essa combinação “%fn-into-%in”, o pusher irá gerar eventos com os nomes dos colliders, no nosso caso, a “bolla0CN”, e a “plataforma”.
Porém temos um problema. Toda função que trata uma colisão, obrigatoriamente, tem que receber as informações da colisão. Essa informação é transmitida à função como um parâmetro, logo a função tem que estar preparada para recebê-lo. O que não acontece com nossa função self.changeState. Bem, você pode pensar que: “ah… blz, vamos colocar uma variável ali nos argumentos da função, que fica tudo certo!”. Não… Isso não é tão simples… e como fica a chamada da função nos eventos de teclado, que não enviam um terceiro parâmetro? Esqueceu disso? Se uma função for chamada, com mais ou menos parâmetros os quais ela possui, acontece um erro, e o programa é encerrado.
Felizmente, tem uma manhã em python para lidar com isso. Inserimos um parâmetro ‘col’ (diminutivo de ‘colisão’ rsrsrs) e dizemos que ele é igual a ‘None’ (nulo em python), caso a função não receba esse argumento!! Engenhoso não? Veja:
[code lang=”python”]
def changeState(self, key, value, col=None):
self.state[key] = value
[/code]
Ao final desse processo, teremos nosso avatar, parando de cair ao tocar a plataforma, porém ainda teremos um problema, veja que ele parece patinar sobre a plataforma! Isso acontece pq o pusher quer repelir o avatar da plataforma! Podemos solucionar esse problema, forçando o personagem na posição X e Y que queremos! Veja a solução no código completo abaixo:
[code lang=”python”]
from direct.showbase.DirectObject import DirectObject
from direct.actor.Actor import Actor
from direct.task import Task
from pandac.PandaModules import *
#modulo da classe Avatar
class Avatar(DirectObject):
def __init__(self):
self.persona = render.attachNewNode(‘persona’)
self.personaActor = Actor(‘../assets/eggs/personagem.egg’,
{‘idle’:’../assets/eggs/personagem-parado’,
‘run’ :’../assets/eggs/personagem-correr’,
‘jump’:’../assets/eggs/personagem-pular’}
)
self.personaActor.setScale(1)
self.personaActor.setPos(0,0,5)
self.personaActor.reparentTo(self.persona)
self.persona.setPos(0,10,0)
self.persona.setScale(.15)
self.persona.setH(90)
self.state = { ‘flying’:True,
‘left’ :False,
‘right’ :False
}
self.lastXpos = 0;
taskMgr.add(self.movement, “Avatar Movement”)
#capture keyboard events
self.accept(‘arrow_left’, self.changeState, [‘left’,True])
self.accept(‘arrow_left-up’, self.changeState, [‘left’,False])
self.accept(‘arrow_right’, self.changeState, [‘right’,True])
self.accept(‘arrow_right-up’, self.changeState, [‘right’,False])
self.accept(‘ball0CN-into-plataforma’, self.changeState, [‘flying’,False])
self.accept(‘ball0CN-out-plataforma’, self.changeState, [‘flying’,True])
def setCollision(self, trav):
#colision nodes and solids
self.ball0 = CollisionSphere(0,0,1.5,1.5)
self.ball0NP = self.persona.attachNewNode(CollisionNode(‘ball0CN’))
self.ball0NP.node().addSolid(self.ball0)
self.ball0NP.show()
trav.addCollider(self.ball0NP, base.pusher)
base.pusher.addCollider(self.ball0NP, self.persona)
def movement(self, task):
if(self.state[‘flying’] == True):
self.persona.setZ( self.persona.getZ() – 0.06 )
#move sideways
else:
if(self.state[‘left’]):
self.persona.setX( self.persona.getX() – 0.1 )
elif(self.state[‘right’]):
self.persona.setX( self.persona.getX() + 0.1 )
else:
self.persona.setX(self.lastXpos)
if(self.persona.getY() != 10):
self.persona.setY(10)
return Task.cont
def changeState(self, key, value, col=None):
self.state[key] = value
if(value == 0):
self.lastXpos = self.persona.getX()
[/code]
Basicamente, na função movement, checamos se a posição Y do avatar não for igual a 10, então ela é corrigida para 10. Isso resolve a “patinação” do avatar para frente e para tráz. Mas ele ainda patina para os lados. Para resolver isso, criamos uma variável chamada lastXpos. Essa variável irá fazer o trabalho de posicionar o avatar sempre no ponto X correto, e não permitir de ele patine para os lados. Observe na função movement, que quando o avatar estiver tocando na plataforma, e não estiver se movendo para a direita ou esquerda, ele será posicionado sempre na posição lastXpos. Essa variável tem seu valor atualizado, sempre que o avatar para de se mover: Veja na função self.changeState, sempre que o ‘value’ for igual a zero, atualizamos a variável lastXpos.
Otimo blog , por favor nao encerre esta serie de tutoriais … eu e um amigo estamos criando um jogo em panda graças a seus artigos (na verdade eu so modelo , ele programa). Voce so precisa de divulgação , de parceiros , porque conteudo pra manter audiencia voce ja tem
Olá João!
Obrigado pela mensagem! É muito legal saber que tem gente lendo o blog e tirando proveito dele!! Éssa é minha maior motivação! Valeu mesmo!
Eu não vou parar de escrever essa série de tutoriais, não! O problema é que nesse momento, estou com muito trabalho, e não estou com tempo para continuar… tenho uns dois rascunhos de posts, no gatilho… mas tá difícil continuar no trabalho.
abs,
Obrigado pelos posts estou começando no panda agora e este tutoriais são otimos para que eu começe super bemnos meus projetos.
Valeu
muitissimo obrigado e não deixae de postar mais alguns tutoriais…
Muito bom estes tutorias do panda, por favor não deixe de continua-los! Abraços.