Neste tutorial, iremos trabalhar com simulações simples de física. O Panda3D possui uma pequena engine de física, que serve para simulações simples. Usaremos ela para melhorar o deslocamento do nosso personagem, e implementar um salto. Também veremos o uso de Intervals no Panda3D.
Esse tutorial tem por objetivo mostrar como usar, de maneira simplificada, uma engine de física. Caso você tenha interesse em utilizar o ODE no Panda, veja este pequeno tutorial, também lhe será útil: http://paulobarbeiro.com.br/blog/?p=77
Para começar com uma má notícia… boa parte do que foi feito até o momento nos tutoriais anteriores, principalmente o que foi feito para movimentar o avatar, não servirá para as simulações de física, logo vamos jogar, no lixo! 🙂 Isso lhe servirá como uma lição: Não se apegue a seu código! 😀 E pense bem, se irá usar simulações de física ou não, porque os códigos não serão reaproveitáveis.
O trabalho será meio longo. E antes de começarmos com códigos vamos explorar um pouco de teoria, e para ter uma “visão panorâmica” do que iremos fazer.
Engines de Física ——————-
Engines de física, são softwares dedicados a realizar cálculos de física, geralmente física Newtoniana, para diversos tipos de aplicações, sendo games uma das possibilidades.
Essas engines de física tem um único trabalho: calcular física. Sim, só isso! Engines de física não fazem renderizações, não capturam inputs de usuário, não fazem síntese sonora, elas apenas calculam valores, com base em fórmulas de física.
Dentro dessas fórmulas, encontra-se muitas opções, entre elas: as três leis de Newton, molas, juntas, colisões… etc. Cada engine de física, possui suas implementações de fórmulas. Cabe a você escolher qual engine usar, baseando-se no que cada uma oferece.
Bem, sendo que uma engine de física, apenas faz cálculos, talvez você esteja se perguntando, como elas são usadas em games?
Até o presente momento, nessa série de tutoriais, nós realizamos as movimentações de nosso personagem, modificando sua posição de coordenadas a cada frame do jogo. Ou seja, para que o avatar se desloca-se para a direita, nós incrementamos sua coordenada X a cada frame, e isso é o suficiente para dar uma ilusão de deslocamento. Essa é uma abordagem plenamente possível, e utilizada em muito jogos! Não há nada de errado nisso! Porém essa movimentação, feita dessa forma, ignora, por exemplo, uma reação natural a certas forças da natureza, como a gravidade. Qualquer um que olhar nosso jogo até o presente momento, irá reparar que o avatar cai a uma velocidade constante, e isso não é um comportamento natural em nosso universo.
E para muitos tipos de jogos, buscar essa naturalidade faz parte da diversão!
Usar simulações de física é um primeiro passo para buscar realismo nos jogos. Isso é uma busca que muda de jogo para jogo. Em alguns casos o realismo pode estragar completamente o jogo!
Então, quando utilizamos engines de física, nós deixamos de manipular as coordenadas os objetos! Quando se usa simulação de física, temos que pensar os movimentos, como reação a aplicação de forças sobre os objetos. Ou seja, para que um objeto se mova para a direita, ao invés de modificar suas coordenadas, temos que aplicar uma força nesse objeto, para fazer com que ele se mova para a direita. Todo o resto fica por conta da engine de física. O que irá determinar a forma do movimento, serão os resultados dos cálculos de simulação de física. Tudo que temos de fazer, é calibrar a aplicação das forças ao nosso gosto!
Pensar o movimento sobre a óptica da aplicação de forças físicas, não é complicado. Mas nos exige um pequeno conhecimento sobre forças e sua representação matemática, os vetores. Todos já estudaram isso nas aulas de física na escola, basta relembrar, e utilizar de uma forma muito mais divertida! Aqui tem um link que pode ajudar um pouco.
Há uma quantidade bem grande de explicações sobre vetores, cálculos vetorias, forças e forças vetoriais pela internet como um todo. Caso o link não lhe ajude, busque pela internet.
Agora, sabemos conceitualmente como as engines de física funcionam. Agora vamos pensar conceitualmente como isso se aplicaria ao nosso game.
Até o presente momento, temos movimentos em três direções, à direita, à esquerda, e para baixo. Então, para que nosso avatar se desloque temos que aplicar forças nele, ou seja:
– para baixo: teremos a força da gravidade;
– para direita: uma força que o empurre para direita;
– para esquerda: uma força que o empurre para a esquerda.
E caso queiramos um salto, teremos uma força que empurre nosso avatar para o alto! Veja o diagrama abaixo:
Nesse diagrama também temos os valores vetoriais para cada uma dessas forças. Esses valores vetoriais, representam a direção e intensidade da força. Por exemplo, a gravidade: Vec3(0,0,-9.81).
Esses três valores que forma o vetor, são coordenadas x,y,z. No caso da gravidade, x=0, y=0, e z=-9.81, ou seja, ela é uma força que aponta para baixo, com intensidade 9.81. Neste tutorial todas as forças são bem simples: apontam para direções nos eixos x, y, ou z, veja as outras:
– pushRight: Vec3(10,0,0): aponta para a “direita” (x positivo), intensidade 10
– pushLeft: Vec3(-10,0,0): aponta para a “esquerda” (x negativo), intensidade 10
– pushUp: Vec3(0,0,20): aponta para cima (z positivo), intensidade 20
O que nós faremos no nosso código, é uma implementação a engine de física do Panda3D, que irá aplicar essas forças em nosso avatar, de acordo com os inputs do usuário. Durante o processo, veremos a implementação dos elementos necessários, e problemas de lógica que teremos de achar soluções.
Essa engine de física que usaremos, é “embutida” no Panda3D, ela é muito simples, e com poucos recursos se comparada a outras engines de física, mas possui o que precisamos para nosso jogo. Devido a essas limitação, os desenvolvedores do Panda3D, também implementaram uma outra engine de física, a ODE (Open Dynamics Engine) um software já consagrado. Mas nada impede que outras engines sejam usadas. Outra possibilidade, é a PhysX, também implementada por alguns usuários!
Vamos aos passos iniciais. No arquivo Panda-tut-00.py, não teremos grandes modificações. Nesse arquivo principal, temos que importar e iniciar o PhysicsCollisionHandler, que será o grande responsável pela detecção de colisões, nesse caso, utilizando a simulação de física do Panda3D.
[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.cTrav.setRespectPrevTransform(True)
base.pusher = CollisionHandlerPusher()
base.pusher.addInPattern(“%fn-into-%in”)
base.pusher.addOutPattern(“%fn-out-%in”)
base.physics = PhysicsCollisionHandler()
base.physics.addInPattern(“%fn-into-%in”)
base.physics.addOutPattern(“%fn-out-%in”)
base.enableParticles()
#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]
Observe que nesse código, apenas adicionamos as linhas 24, 25, 26 e 27. O PhysicsCollisionHandler funciona de forma muito semelhante aos outros “collision handlers”, a maior diferença é a obrigatoriedade de se ativar o sistema de partículas (linha 27).
As linhas relativas ao CollisionHandlerPusher, podem ser apagadas, elas não possuem mais uso, só as mantenho lá, para que você possa comparar.
Agora na classe Avatar, teremos grandes modificações! Vou colocar todo o código abaixo, e depois seguir nas explicações de cada alteração.
código 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 *
from direct.interval.IntervalGlobal import *
#modulo da classe Avatar
class Avatar(DirectObject):
def __init__(self):
self.persona = render.attachNewNode(‘persona’)
self.personaActorNode = ActorNode(‘personaActorNode’)
self.personaActorNode.getPhysicsObject().setMass(35)
self.personaActorNP = self.persona.attachNewNode(self.personaActorNode)
base.physicsMgr.attachPhysicalNode(self.personaActorNode)
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.personaActorNP)
self.persona.setPos(0,10,10)
self.persona.setScale(.15)
self.persona.setH(90)
self.state = { ‘left’ :False,
‘right’ :False,
‘ground’:False,
‘canJump’ :False
}
taskMgr.add(self.movement, “Avatar Movement”)
#timer for jump
self.startCallforJump = 0
self.timer = LerpFunc(self.performJump,
fromData = 0,
toData =1,
duration = 0.25,
blendType=’easeIn’,
extraArgs=[],
name=None)
#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(‘space’, self.changeState,[‘jump’,True])
self.accept(‘space-up’, self.changeState,[‘jump’,False])
#capture collision events
self.accept(‘bola0CN-into-plataforma’, self.changeState, [‘ground’,True ])
self.accept(‘bola0CN-out-plataforma’, self.changeState , [‘ground’,False])
#Persona physics forces
self.pushUp=LinearVectorForce(0,0,20)
self.pushUpFN = ForceNode(‘pushup-force’)
self.pushUpFNP = render.attachNewNode(self.pushUpFN)
self.pushUpFN.addForce(self.pushUp)
self.pushLeft=LinearVectorForce(-10,0,0)
self.pushLeftFN = ForceNode(‘pushleft-force’)
self.pushLeftFNP = render.attachNewNode(self.pushLeftFN)
self.pushLeftFN.addForce(self.pushLeft)
self.pushRight=LinearVectorForce(10,0,0)
self.pushRightFN = ForceNode(‘pushright-force’)
self.pushRightFNP = render.attachNewNode(self.pushRightFN)
self.pushRightFN.addForce(self.pushRight)
self.gravityFN=ForceNode(‘world-forces’)
self.gravityFNP=render.attachNewNode(self.gravityFN)
self.gravityForce=LinearVectorForce(0,0,-9.81) #gravity acceleration
self.gravityFN.addForce(self.gravityForce)
base.physicsMgr.addLinearForce(self.gravityForce)
def setCollision(self, trav):
#colision nodes and solids
self.ball0 = CollisionSphere(0,0,1.5,1.5)
self.ball0NP = self.personaActorNP.attachNewNode(CollisionNode(‘bola0CN’))
self.ball0NP.node().addSolid(self.ball0)
self.ball0NP.show()
base.physics.addCollider(self.ball0NP, self.personaActorNP)
base.cTrav.addCollider(self.ball0NP, base.physics)
def movement(self, task):
#print self.personaActorNode.getPhysical(0).getLinearForces()
if(self.state[‘left’] == True):
if(len(self.personaActorNode.getPhysical(0).getLinearForces()) == 0):
self.personaActorNode.getPhysical(0).addLinearForce(self.pushLeft)
else:
self.personaActorNode.getPhysical(0).removeLinearForce(self.pushLeft)
if(self.state[‘right’] == True):
if(len(self.personaActorNode.getPhysical(0).getLinearForces()) == 0):
self.personaActorNode.getPhysical(0).addLinearForce(self.pushRight)
else:
self.personaActorNode.getPhysical(0).removeLinearForce(self.pushRight)
return Task.cont
def changeState(self, key, value, col=None):
if(self.state[‘canJump’] == True and key == ‘jump’):
if(value==True):
self.startCallForJump = globalClock.getRealTime()
if(value == False):
preparationTime = globalClock.getRealTime() – self.startCallForJump
if (preparationTime > 0.4): preparationTime = 0.4
elif(preparationTime < 0.2): preparationTime = 0.2
upforce = 1000*preparationTime
self.pushUp.setVector(0,0,upforce)
self.timer.start()
self.startCallForJump = 0
else:
self.state[key] = value
if(self.state[‘ground’] == True):
self.state[‘canJump’]=True
elif(self.state[‘ground’] == False):
self.state[‘canJump’]=False
def performJump(self, data):
if(data == 0):
self.personaActorNode.getPhysical(0).addLinearForce(self.pushUp)
elif(data == 1):
self.personaActorNode.getPhysical(0).removeLinearForce(self.pushUp)
[/code]
Vamos começar pela função __init__(self):
A primeira alteração é adicionar aos nodes e nodepaths que compõe nosso avatar, um ActorNode. O ActorNode é uma classe para um nó que interage com o sistema de física. Não confunda com a classe Actor, destinada à utilização de modelos “riggados”. Primeiro nós criamos o ActorNode (linha 5), configuramos a massa do nosso objeto; Adicionamos o ActorNode ao persona node; e o nodepath do ActorNode, dentro do persona node, é adicionado ao handler physics.
Observe outro detalhe importante na linha 17: O node personaActor, com o modelo do personagem, passa a ser ligado ao ActorNode. É essa ligação que fará com que o modelo, se movimente pela tela com as simulações de física.
Nas linhas 32 e 32, temos uma pequena lógica para realizar um salto. Veremos esse processo com mais detalhes em breve.
Nas linhas 46 e 47, temos uma captura de input de usuário com a tecla de espaço.
Das linhas 52 à 71, temos a configurações das forças que atuarão sobre nosso personagem, que discutimos anteriormente. E na linha 71, em especial vemos como fazer a aplicação da força ao ActorNode, no caso, a única força que será onipresente, a gravidade!
A criação dessas forças, é feita coma criação de um vetor de força, no caso, LinearVectorForce, que contém a direção e intensidade da força em questão. Com o LinearVectorForce criado, temos que criar um ForceNode, que será anexado ao render. Nesse momento, as forças ficam disponíveis para uso, basta aplicá-las.
A função setCollision:
Neste função temos poucas alterações, a única diferença é a necessidade, óbvia, de ligar os collisionNodes ao PhysicsCollisionHanlder.
A função movement:
Na função movement, temos a lógica que realiza o movimento do personagem aplicando as forçar apropriadas. Basicamente quando o estado do personagem muda para a direita ou esquerda, a função aplica e remove as forças, para que a simulação represente o que desejamos. Os comando addLinearForce() e removeLinearForce(), são bem auto-explicativos, não?
A função changeState:
Na função changeState temos uma lógica especial para a realização do salto do avatar.
Primeiro, o objetivo dessa função é filtrar o input de usuário pela tecla de espaço, para a realização do salto. O “filtro” consiste em realizar um salto mais forte, ou mais fraco, dependendo do tempo que o jogador pressiona a tecla de espaço.
Quando a barra de espaço é pressionada, a função avalia se o avatar pode saltar(canJump), ou seja, se ele está tocando a plataforma. Então ele avalia se a tecla de espaço foi pressionada ou desapertada; Se foi apertada, a função guarda o tempo em que ela foi apertada; Quando a tecla for solta, a função calcula o tempo em que ela ficou pressionada, e divide esse valor de 1000, o resultado é aplicado ao vetor de forca linear self.pushUp, e um timer é ativado, que executa a aplicação e remoção da força.
A função performJump:
Essa função é uma LerpFunction, declarada na linha 40, na função __init__. Essa função ira realizar uma espécie de timer, que usaremos para aplicar a força do salto, e bem pouco tempo depois, remover essa força. Um salto de uma humano, ou outro animal terrestre, é basicamente um único impulso bem forte, e não uma aplicação constante de força. Logo, para simular um salto humano, temos que fazer isso, uma aplicação de força em um curtíssimo período de tempo.