Archive

Posts Tagged ‘Panda3D’

Iuna Capoeira

August 3rd, 2010

Nossa… faz mais de seis meses que terminei minha pós, e ainda não postei aqui no blog o resultado do trabalho!! Que vergonha… mas é que as coisas estão meio corridas.

Bem, o projeto era um jogo de capoeira, porém não um jogo de luta, e sim um jogo de ritmo. O demo do jogo ficou pronto, embora ainda longe do que esperávamos (eu e o Guilherme Muniz, grande cara e colega!).

Conseguimos cumprir bem certos pontos que julgávamos essenciais como fluidez nas animações, e dinamismo do gameplay, e uma trilha sonora moderna, sem muitos clichês.

Embora alguns pontos ficaram mais fracos. Não gostei da arte 3D (fui eu que fiz), e com relação ao elemento de interface que indica a marcação do ritmo, também deve ser melhorado!

Estou trabalhando para portar esse demo para o iPhone, usando o Unity3D, e enfim concluir o jogo por completo. E chamar um bom artista 3D para fazer uma arte melhor :)

O demo foi feito com o Panda3D. Que se mostrou uma ótima ferramenta! Especialmente para suprir as necessidades que antevimos como críticas. Uma delas é a variedade de soluções para se manipular e controlar as animações de personagens. Comparando o Panda3D e o Unity3D(versão 2.6) nesse requisito… o Panda3D dá de 10×0! Tanto na facilidade de uso, quanto nos resultados. Espero que na versão 3 o Unity3D melhore.

abraço,

Portifólio , , ,

Aula 5 - GameCultura - Panda3D

February 1st, 2009

Nessa quinta aula veremos como usar animações com o Panda3D.

O Panda3D possui um ótimo ferramental para se trabalhar com animações, porém iremos explorar apenas o básico.

Como já foi dito nas aulas anteriores, no Panda3D os modelos 3D e suas animações são armazenados separadamente em arquivos do tipo .egg, o .egg que guarda a malha (mesh), possui informações dos vertices, UVs, grupos de vertices e joints, e o arquivo que guarda as animações, possuem apenas as transformações que sofrem cada um dos vertices da malha durante a animação.

::: Blend de animações

O Panda possui um recurso para realizar Blend nas animações. O Blend serve para interpolar transições entre as animações. Infelizmente o Panda não tem uma função para criar esses blends automaticamente, ou seja… para fazê-lo você precisa criar a lógica e as funções. Mas a coisa não é tão complicada, durante essa aula você poderá ter uma idéia de como realizar isso.

Mas essa possibilidade de se usar um blend em uma animação permite um ‘approach’ especial na forma de lidar com animações, mesmo que não usemos o blend própriamente dito.

A idéia é a seguinte, vamos colocar todas as animações para rodar ao mesmo tempo, e a medida que o avatar realiza uma ação, configuramos o indice de atuação do blend para a animação relativa a ação.

Para isso precisaremos acrescentar algumas coisas na nossa classe Avatar:

  1. Algumas variáveis:  uma lista com valores para o blend; duas variáveis para saber se o avatar esta se movendo e/ou caindo.
  2. Uma função para realizar as alterações entre as animações.

::: Mãos na massa

Vamos para a primeira parte, criar as variáveis para manipular a animação - na função __init__() :

      #animacao
      self.blenAnim = [1.0, 0.0, 0.0] #idle run jump
      self.estaSeMovendo = False
      self.estaNoAr = True

Bem a lista self.blenAnim possui três valores, que serão os índices de influência para as animações, por uma escolha pessoal, os três valores serão destinados as animações ‘idle’, ‘run’ e ‘jump’, respectivamente. E observe que são valores entre 0 e 1, ou seja normalizados. As variáveis self.estaSeMovendo e self.estaNoAr, são booleanas, que serão usadas para testes de condição.

Agora na segunda parte, vamos pensar na lógica que teremos para a função de animação:

Se o avatar não estiver se movendo e não estiver no ar: animação = idle

Se o avatar estiver se movendo e não estiver no ar: animação = run

Se o avatar estiver no ar: animação = jump

A implementação dessa lógica em uma função ficaria assim:

   def animacao(self):
      if (self.estaSeMovendo == False) and (self.estaNoAr == False):
         self.blendAnim = [1.0, 0.0, 0.0]
      elif (self.estaSeMovendo == True) and (self.estaNoAr == False):
         self.blendAnim = [0.0, 1.0, 0.0]
      elif (self.estaNoAr == True):
         self.blendAnim = [0.0, 0.0, 1.0]
      else:
         self.blendAnim = [1.0, 0.0, 0.0]

      self.personaActor.setControlEffect( ‘idle’, self.blendAnim[0] )
      self.personaActor.setControlEffect( ‘run’, self.blendAnim[1] )
      self.personaActor.setControlEffect( ‘jump’, self.blendAnim[2] )

Essa função animacao() precisa ser chamada dentro da função mover(). E ainda na função mover(), em cada uma das condições, podemos atribuir valores a algumas daquelas variáveis iniciais:

   def mover(self, task):
      ‘tarefa responsavel pela movimentacao do personagem
      ’

      #DIREITA
      if (self.estados[‘direita’] != 0):
         self.persona.setX( self.persona.getX()+self.velocidade*globalClock.getDt() )
         self.persona.setH(90)
         self.estaSeMovendo = True
      #ESQUERDA
      if (self.estados[‘esquerda’] !=0):
         self.persona.setX( self.persona.getX()-self.velocidade*globalClock.getDt() )
         self.persona.setH(-90)
         self.estaSeMovendo = True

      #SALTO
      self.salto()

      #PARADO
      if (self.estados[‘esquerda’]==0) and (self.estados[‘direita’]==0):
         self.estaSeMovendo = False

      self.animacao()

      return Task.cont

Aqui nós só estamos usando a variável estaSeMovendo, mas lembra-se da variável estaNoAr? Então, pensando em termos lógicos, seus valores serão modificados sempre o avatar tocar o solo ou sair do solo, então advinha onde teremos que fazer as próximas alterações… função tocaSolo() e saiSolo().

   def tocaSolo(self, entrada):

      self.estados[‘pular’]=False

      self.estados[‘X’]=0

      self.estados[‘cair’]=0

      self.estaNoAr = False

   def saiSolo(self, entrada):
      self.pontoBase = entrada.getSurfacePoint(render).getZ()

      self.pontoAuge = self.pontoBase+self.alturaMaxima

      self.estados[‘X’]=0

      self.estados[‘cair’]=1

      self.estaNoAr = True

::: Finalizando

Neste exemplo, o blend apenas ‘regula’ qual animação será ‘exibida’, se você reparar no comando setControlEffect(), apenas colocamos o valor 1 ou 0. Para se fazer o blend de forma mais ‘correta’, deveríamos fazer com que o índice variasse aos poucos a cada frame, para que de fato haja um ‘cross fade’ entre as animações.

Aqui você encontra o arquivo final desta aula.

Games, Panda3D, Tutoriais , ,

Aula 4 - GameCultura - Panda3D

January 12th, 2009

Nesta aula vamos fazer com que nosso avatar pule, e construir uma interface (GUI)

O Salto

Vamos pensar sobre como deve ser o pulo, afinal existem muitas formas para isso, neste vou querer que seja um pulo até uma certa altura, e que essa altura seja se um valor X, durante o pulo o avatar poderá se mover para a direita e esquerda.

Veja o esquema abaixo:

esquema do salto

Nesse diagrama eu tenho um ponto base e um ponto auge, o ponto auge será o limite máximo na altura do salto. O ponto base, é a altura de onde saí o avatar, afinal como esse é um jogo de plataforma, o ponto de partida do salto varia com frequência (agora sem trema!! :) ). Há também um sólido de colisão a mais, pela seguinte razão: estamos usando o ‘Floor’, e com ele não temos muitas formas para conseguir extrair informações dessa colisão. Para isso poderíamos usar o CollisionHandlerQueue, porém seria uma mudança que complicaria muito a essa altura.

TODAS AS ALTERAÇÕES SERÃO FEITAS NA CLASSE AVATAR()

A nossa lógica será algo assim:

Os elementos novos serão:

1- Teremos dois novos estados ‘pular’ e ‘X’

2 - Teremos algumas variáveis novas: pontoBase, pontoAuge, forcaEmpuxo

3 - Um novo nó de colisão, com uma esfera como sólido

4 - Dois novos eventos para detectar as colisões entre o novo sólido e a plataforma

5 - Uma função para executar o salto

A lógica:

1 - quando o estado: pular for verdadeiro o salto será executado

2 - ao ser executado, o avatar será levado até o ponto auge

3 - chegando ao ponto auge, o Floor fará o trabalho de trazê-lo à plataforma

4 - enquanto cai, o salto não pode ser executado

ALTERAÇÃO FORA DA CLASSE AVATAR:

1 - adicionar mais um CollisionHandlerEvent: collEvent2

Mãos a massa:

Adicionando as variáveis novas à classe Avatar:

class Avatar(DirectObject):
 def __init__(self):
  self.saude = 1
  self.estados = {‘direita’:0, ‘esquerda’:0,
  ‘pular’: False, ‘cair’:0,
  ‘X’: 0}

  self.velocidade=9
  self.pontoBase = 0
  self.pontoAuge = 0
  self.forcaEmpuxo=0.3
  self.alturaMaxima = 3

Agora criar o novo nó de colisão, na função criaCollider(), abaixo dos colliders anteriores:

self.cNodeEsferaPe = CollisionNode(‘avatarPe’)
 self.cNodeEsferaPe.addSolid( CollisionSphere(.5,0,0,  .1) )
 self.personaPe = self.persona.attachNewNode( self.cNodeEsferaPe )
 self.personaPe.show()

 base.cTrav.addCollider(self.personaPe, base.collHandEvent)

Agora a atualização da função capturaEventos():

def capturaEventos(self):
 self.accept( ‘arrow_right’, self.alteraEstado, [‘direita’,1] )
 self.accept( ‘arrow_left’, self.alteraEstado, [‘esquerda’,1] )
 self.accept( ‘arrow_up’, self.alteraEstado, [‘pular’,True] )
 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)
 self.accept( ‘into-plataforma’, self.tocaSolo )
 self.accept( ‘outof-plataforma’, self.saiSolo )

Novas funções para checar o status do salto. Elas controlam os estados, de acordo com a colisão entre o nó ‘avatarPe’ e a plataforma, e não permitindo que o avatar salte novamente enquanto ainda estiver no ‘ar’.

def tocaSolo(self, entrada):
 self.estados[‘pular’]=False
 self.estados[‘X’]=0
 self.estados[‘cai’]=0
 self.estaNoAr = False

def saiSolo(self, entrada):
 self.pontoBase = entrada.getSurfacePoint(render).getZ()
 self.pontoAuge = self.pontoBase+self.alturaMaxima
 self.estados[‘X’]=0
 self.estados[‘cai’]=1
 self.estaNoAr = True

A função salto(), que executa o salto. Essa função deve ser chamada dentro da função mover():

def salto(self):
 if(self.estados[‘pular’]):
  if (self.persona.getZ() <= self.pontoAuge) and ( self.estados[‘X’]==0 ):
   self.persona.setZ( self.persona.getZ()+self.forcaEmpuxo )
  else:
   self.estados[‘X’]=1

Agora a criação do novo CollisionHandlerEvent. O anterior não deve ser apagado, afinal não deixamos de usá-lo, só estamos criando um novo:

base.collEvent2 = CollisionHandlerEvent()
base.collEvent2.addInPattern(‘into-%in’)
base.collEvent2.addOutPattern(‘outof-%in’)

Se tudo der certo você já pode testar o código e ver resultado. Então vamos ver em detalhes esse processo. O salto é controlado prioritariamente por duas variáveis de estados, ‘pular’ e ‘X’. Quando a seta para cima, do teclado direcional é pressionada o estado ‘pular’ se torna True (verdadeiro), e o estado ‘pular’ só se torna False (Falso) quando o avatarPe toca o solo, liberando o avatar para um novo salto.

Quando o pulo se tonar True ( função salto() ), e desde que o Z do avatar, esteja abaixo do ponto Auge, e que ‘X’ seja igual a 0, o avatar começa a subir. E assim que o avatar chega ao ponto auge, o estado ‘X’, se torna 1, então o avatar pára de subir. E o Floor se encarrega de trazê-lo de volta para a plataforma.

Existem outros pormenores. Dois pontos chaves do salto são importantes, a saída ( função saiSolo() ) e a chegada ( função tocaSolo() ). Essas duas funções auxiliam na alteração de estados. Assim que o avatar toca o solo, o estado ‘pular’ é definido como 0, assim com o estado ‘X’. E quando o avatar sai do solo, é quando o ponto base é ‘extraído’, o ponto auge também é definido.

Mas ainda existe um bug… sim… experimente subir em uma plataforma, e ir andando até seu limite até cair naturalmente, e durante essa queda, experimente pular, o pior é que você vai conseguir… pular enquanto cai! Isso não é aceitável… então vamos a correção.

Esse problema acontece porque não ha uma lógica que contemple uma ação simples: cair.

Então para corrigir isso eu inseri uma pequena lógica; o avatar deveria cair ao sair do solo e durante a queda, o pular não pode ser ativado. Para isso, no dicionário estados, acrescentei ums chave “cair” com valor inicial 1: ‘cair’:0. A função tocaSolo(), deve setar o self.estados[’cair’]=0, e a função saiSolo(), deve setar o self.estados[’cair’]=1. Mas isso não é suficente, e para completar, precisamos então mudar a função alteraEstados(), com uma lógica simples: se a chave for ‘pular’, e o estado ‘cair’ estiver como 1, o estado ‘pular’ deve permanecer False. Veja só:

def alteraEstado(self, chave, valor):
 if chave == ‘pular’:
  if self.estados[‘cair’]==1:
   self.estados[‘pular’] = False
  else:
   self.estados[chave] = valor

GUI

Agora vamos trabalhar uma Interface Gráfica, algo bem simples, que apenas vai mostrar a saúde do avatar.

imagem da GUI

Nós podemos colocar os comando de GUI, na própria classe Avatar(), porém no futuro eu recomendo colocar esses comandos em uma classe Mundo(), que coordena elementos mais “universais”. Mas vamos lá:

Na classe Avatar vamos usar uma lista para armazenar algumas imagens para fazer o gráfico:

class Avatar(DirectObject):
 def __init__(self):
  self.pizzaImagem = [‘../res/texturas/Saude0.png’,
  ‘../res/texturas/Saude1.png’,
  ‘../res/texturas/Saude2.png’,
  ‘../res/texturas/Saude3.png’,
  ‘../res/texturas/Saude4.png’,
  ‘../res/texturas/Saude5.png’,
  ‘../res/texturas/Saude6.png’]

E vamos adicionar uma função para exibir a interface, e outra para atualizar. Ambas as funções são vão exibir uma imagem, e um número, que são relativos ao valor da saúde do avatar:

def exibeGUI(self):
 self.graficoSaude = OnscreenImage( image= self.pizzaImagem[self.saude], pos=(-1,0,-0.7), scale=(0.2) )
 self.graficoSaude.setTransparency( TransparencyAttrib.MAlpha )
 self.graficoNum = OnscreenText( text= str(self.saude), pos=(-1,-0.73,0), scale=(0.1), fg=(1,1,1,1) )

def atualizaGUI(self):
 self.graficoSaude.setImage( self.pizzaImagem[self.saude] )
 self.graficoSaude.setTransparency(TransparencyAttrib.MAlpha)
 self.graficoNum.destroy()
 self.graficoNum = OnscreenText( text=str(self.saude), pos=(-1,-0.73,0), scale=(0.1), fg=(1,1,1,1) )

Agora as funções serão chamadas em momentos diferentes, a função exibeGUI(), será chamada na inicialização( __init__ ) e a função atualizaGUI(), será chamada quando houver a atualização da saúde, ou seja, na função saudeAvatar().

Se vc quiser aqui está o arquivo final.

Dúvidas é só mandar um comentário.

Games, Panda3D, Tutoriais , ,

Aula 3 - GameCultura - Panda3D

January 12th, 2009

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:

grafo para collisionNodes

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.

grafos para collisioNodes

 

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:

 avatar com solidos de colisao

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:

saída com as informações de entrada

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) :

opções de comandos para a entrada de colisão

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.

saida de atualiazação da saude do avatar

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 , ,

Aula 2 - GameCultura - Panda3D

January 11th, 2009

Na segunda aula do curso a idéia é começar a colocar interatividade nos objetos do game. Mas colocar interatividade nesses elementos a partir do código gerado na primeira aula, seria o fim do mundo, porque ficaria uma ‘zona’ o nosso código; Então decidi colocar tudo bonitinho em classes e seus objetos :)  …sim seria possível colocar tal interatividade sem uso de objetos, mas além de ficar uma ‘zona’, eu pergunto, não parece natural criar objetos para um jogo, uma vez que em um jogo, os objetos possuem suas ações? Para mim, a resposta é sim!

Um cara chamado Mark Guzdial, escreveu o seguinte (sobre POO): “Objetos possuem duas coisas: Objetos sabem coisas; Objetos podem fazer coisas”, isso me parece cair como uma luva para jogos :) Dessa forma começaremos a organizar nosso código como classes para nossos objetos do jogo.

 

Iniciantes: (caso você conheça orientação a objetos pule este trecho)

Você iniciante, que pode estar com medo de POO, digo que não se preocupe, a coisa não é tão feia, na verdade é muito mais simples do que se pode imaginar, e para ajudar você eu não usarei termos técnicos que não sejam extremamente necessários, e quando necessários farei de forma que vc os ache legal… espero :)

Para você entender isso melhor é simples, basta pensar naturalmente. Por exemplo, um cachorro late quando houve um barulho incomum, certo!? Então, para transformar isso em programação, nós temos que criar um tipo de objeto que ao ‘ouvir’ um evento incomum execute uma ação de latir. E essas ações nós iremos escrever em uma ‘classe’ que vai conter todas essas ‘instruções’, e só depois quando criamos o objeto é que elas irão acontecer… lembre-se disso e observe o que faremos adiante.

 

Mão na massa: (todos… :D )

Nós podemos começar com base no código da primeira aula. Que está assim:

arena = loader.loadModel(‘../res/eggs/plataformaBase’)
arena.setScale(4)
arena.reparentTo(render)
arena.setPos(20,30,5)

 

Nossa idéia é transformar os elementos do jogo em objetos, certo? Bem, a arena é um elemento importantíssimo do jogo, então vamos transformá-la em um objeto, para tal, teremos que criar uma classe, que terá apenas uma função, a de carregar a nossa arena no ‘render’ do Panda, e definir algumas propriedades. Vamos lá:

class Arena():
    def __init__(self):
        self.carregaModelo()

    def carregaModelo(self):
        self.arena = loader.loadModel(‘../res/eggs/plataformaBase’)
        self.arena.setScale(4)
        self.arena.reparentTo(render)
        self.arena.setPos(20,30,5)

 

Pronto! Já temos a nossa classe pronta. Mas vamos dar uma olhada no que fizemos:

1- Primeiro criamos a classe com nome Arena

class Arena():

2- Definimos a iniciação da classe (ou também chamado método construtor), ele é sempre obrigatório quando trabalhamos com classes:

            def __init__(self):

3-  No __init__ nós chamamos a função que irá fazer o trabalho de carregar os modelos 3D e setar as propriedade que queremos:

                  self.carregaModelo()

4- Criamos a função que fará o trabalho:

            def carregaModelo(self):

5- O conteúdo da função você já conhece, com a exceção do uso do ’self’:

          self.arena = loader.loadModel(‘../res/eggs/plataformaBase’)
          self.arena.setScale(4)
          self.arena.reparentTo(render)
          self.arena.setPos(20,30,5)

O ’self’ é sempre algo que permite muitas conversas e debates, mas de forma sucinta: o ’self’ serve para que o objeto, busque as informações relativas, nele mesmo.

 

Agora resta instanciar o objeto, ou seja, criar o objeto:

objetoArena = Arena()

e então:

run()

obs.: Não esqueça dos ‘imports’, são os mesmos da primeira aula!

 

Execute o programa, e você terá a arena na tela.

arena

Ainda na classe Arena, nós podemos carregar o papel de parede, veja só:

class Arena(DirectObject):
    def __init__(self):
        self.carregaModelo()

    def carregaModelo(self):
        self.arena = loader.loadModel(‘../res/eggs/plataformaBase’)
        self.arena.setScale(4)
        self.arena.reparentTo(render)
        self.arena.setPos(20,30,5)

        self.parede = loader.loadModel(‘../res/eggs/papelParede’)
        self.parede.reparentTo(render)
        self.parede.setScale(1)
        self.parede.setPos(-5, 60, -2)

Isso vai colocar um céu no nosso jogo :)

Agora vamos pensar no objeto Avatar. Para esse pequeno demo, o avatar vai ter poucas ações mas o suficiente para aprender coisas importantes, eu montei uma relação de ações e propriedades (coisas que ele vai fazer, e coisas que ele vai saber) para esse avatar; Embora não vamos fazer tudo ao mesmo tempo, é bom termos uma panorâmica do todo:

Ações X Propriedades 
- se move para direita e esquerda                             

- pula

- colide com coisas(plataforma, Energia, obstáculos)

- sensível aos eventos de teclado

 

  - saúde                              

- velocidade

- altura do pulo

Além é claro de coisas básicas que são invisíveis ao jogador, tipo, carregar o próprio modelo. Então vamos começar por coisas básicas… vamos fazer nossa classe para o futuro ‘objetoAvatar’, começando por fazê-la carregar o modelo 3D:

class Avatar(DirectObject):
    def __init__(self):
        ‘Metodo construtor da classe Avatar.
        - Define os estados do Avatar: self.estados
        ‘

        self.estados = {‘direita’:0, ‘esquerda’:0, ‘pulo’:0}

        self.carregaAtor()

    def carregaAtor(self):
        ‘Carrega o modelo personagem.egg no NodePath ‘persona
        ‘

        self.persona = render.attachNewNode(‘persona’)
        self.personaActor = Actor(‘../res/eggs/personagem.egg’,
                             {‘idle’:‘../res/eggs/personagem-parado’,
                              ‘run’ :‘../res/eggs/personagem-correr’,
                              ‘jump’:‘../res/eggs/personagem-pular’}
                             )
        self.personaActor.setScale(.3)
        self.personaActor.loop(‘idle’)
        self.personaActor.reparentTo(self.persona)
        self.personaActor.setPos(0,0,0)
        self.persona.setPos(0,30,0)
        self.persona.setH(90)

 

Resumindo, 1: criamos uma classe chamada Avatar; 2: criamos um método de inicialização, onde chamamos a função que carrega o nosso modelo 3D; 3: criamos a função que carrega o modelo 3D.

Se você reparou bem na função carregaAtor(), reparou uma pequena mudança no procedimento que fizemos na aula 1. Na aula 1, nós criamos um nó chamado ‘persona’ ao grafo de cena (ligado ao render), e nesse nó estava no nosso modelo 3D, vejo o esquema abaixo:

esquema de grafo de cena

Mas agora a coisa mudou um pouquinho, criamos um nó chamado ‘persona’ ao render, e ao nó persona criamos um nó ‘personaActor’, que esse sim guarda o modelo 3D. Isso irá nos permitir maior flexibilidade no futuro, e uma estrutura mais organizada. Ah, aquela variável self.estados, que na verdade é um dicionário, servirá para fazermos nosso avatar se mover.

Outra pequena mudança: class Avatar(DirectObject):  reparou nisso? o que é esse DirectObject? Bem esse DirectObject é o que permitirá nosso avatar receber os inputs de teclado, e outros eventos! :) mas que por hora não está funcionando… mas, lembre-se dele.

Agora é criar o objeto baseado na classe Avatar, e testar; Então após o término da classe, e antes do comando run():

objetoAvatar = Avatar()

tela do jogo

 

Eventos e Tarefas (Task)

Agora vamos trabalhar muito na classe Avatar, implementando as funcionalidades principais.

A primeira das funcionalidades serão os inputs de teclado, e movimentação, e para tornar isso possível iremos falar sobre duas coisas super importantes no Panda3D, os Eventos e as tarefas (Tasks).

Eventos podem ser entendidos como mensagens que ‘circulam’ pelo sistema, e alguns objetos, ao receberem esses eventos podem reagir a ele, com alguma ação. Esses eventos podem ser gerados de diversas formas, via input de teclado, colisões, ou serem criados ao gosto do desenvolvedor.

Para o Panda3D, os inputs de teclado serão sempre eventos, sempre! É um padrão do sistema, sempre que uma tecla é pressionada gera um evento, basta que um objeto esteja apto a tratar esse evento. É exatamente o que temos que fazer para, por exemplo, tornar possível a movimentação do nosso personagem. Está lembrado do DirectObject? class Avatar(DirectObject): lembra que isso é o que permitirá a uma classe tratar eventos.

Tarefas por sua vez são funções que serão executadas todos os frames. Por exemplo, se um elemento do jogo tem que se mover constantemente, deve haver uma função que faça com que ele se mova, e tal função deve ser definida como tarefa, para que ela executada a cada frame.

A lógica da movimentação do nosso avatar será feita em duas partes, primeira: haverá uma tarefa que irá mover nosso personagem, dependendo dos valores do dicionário self.estados;  segunda: faremos com que a classe Avatar ao receber um evento das teclas direcionais do teclado, modifique os valores do dicionário self.estados.

esquema da lógica para mover o personagem

Para colocar isso em prática temos que criar algumas funções, uma para capturar os eventos, uma para alterar o self.estados, uma para mover o avatar:

 

class Avatar(DirectObject):
    def __init__(self):
        ‘Metodo construtor da classe Avatar.
        - Define os estados do Avatar: self.estados
        ‘

        self.estados = {‘direita’:0, ‘esquerda’:0, ‘pulo’:0}

        self.carregaAtor()
        self.capturaEventos()

        taskMgr.add( self.mover, ‘moverPersona’ )

    def carregaAtor(self):
        ‘Carrega o modelo personagem.egg no NodePath ‘persona
        ‘

        self.persona = render.attachNewNode(‘persona’)
        self.personaActor = Actor(‘../res/eggs/personagem.egg’,
                             {‘idle’:‘../res/eggs/personagem-parado’,
                              ‘run’ :‘../res/eggs/personagem-correr’,
                              ‘jump’:‘../res/eggs/personagem-pular’}
                             )
        self.personaActor.setScale(.3)
        self.personaActor.loop(‘idle’)
        self.personaActor.reparentTo(self.persona)
        self.personaActor.setPos(0,0,0)
        self.persona.setPos(0,30,0)
        self.persona.setH(90)

    def capturaEventos(self):
        ‘Tratamento de eventos INPUT
        arrow_right = direita
        arrow_left = esquerda
        ‘

        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] )

    def alteraEstado(self, chave, valor):
        ‘Modifica os estados de acao, de acordo com os valores recebidos
        acao ->; estados: direita, esquerda
        valor ->; valor que sera atribuido a acao
        ‘

        self.estados[chave] = valor

    def mover(self, task):
        ‘tarefa responsavel pela movimentacao do personagem
        ‘

        #DIREITA
        if (self.estados[‘direita’] != 0):
            self.persona.setX( self.persona.getX()+0.1 )
        #ESQUERDA
        if (self.estados[‘esquerda’] !=0):
            self.persona.setX( self.persona.getX()-0.1 )

        return Task.cont

 

Vamos aos detalhes. Primeiro vamos observar aquela primeira parte da lógica, onde uma função (tipo task) fará nosso personagem se mover de acordo com valores do ‘estados’. Essa é a função mover()! Observe a chamada dessa função lá no __init__:

taskMgr.add( self.mover, ‘moverPersona’ )

O taskMgr, é um objeto ‘global’ no sistema Panda3D, que irá gerenciar todas as tarefas do jogo, afinal cada elemento do jogo pode ter sua tarefa… o que nós fazemos aqui foi dizer a esse gerenciador (taskMgr) para executar a função self.mover (do avatar) todos os frames. Agora na função mover(), propriamente dita, repare que ela está recebendo uma variável task -> def mover(self, task): <- ISSO É OBRIGATÓRIO! Sempre que uma função seja definida como tarefa, ela deve receber a variável de tarefa (no caso task), isso se explica, por exemplo na última linha da função -> return Task.cont <- isso quer dizer que essa tarefa deve ser novamente executada no próximo frame (significa: tarefa.continue  :) ). Sim, a execução dessas tarefas podem ser largamente personalizadas, dê uma olhada aqui.

Bem, a lógica interna da função mover(), está bem simples. Os valores do self.estado, começam todos com 0(zero), e dentro da função se o estado direita for diferente de 0(zero), ele altera a coordenada X em 0.1 positivo, o que dará a sensação do personagem se mover para a direita. E logo depois a mesma lógica é aplicada para o mover para a esquerda.

Agora a segunda parte da lógica, onde o evento do teclado será capturado, e acionará uma alteração nos valores do dicionário. A captura dos eventos, é feita na função capturaEventos() … a estrutura para isso é muito simples, é o comando accept():

self.accept( ‘nome_do_evento’, funçao_que_trata_Evento, [parametros] )

No caso selecionamos os eventos ‘arrow_left’, ‘arrow_right’ que são disparados quando as teclas são pressionadas, e os eventos ‘arrow_left-up’ e ‘arrow_right-up’ que acontecem quando as mesmas teclas são ’soltas’.

self.accept( ‘arrow_right’, self.alteraEstado, [‘direita’, 1] )

Então o que fizemos foi, quando ocorrem o evento ‘arrow_right’, disparamos a função alteraEstado() para trabalhar com os parâmetros ‘direita’, e 1.

A função alteraEstado(), apenas tem o trabalho de alterar os valores do dicionário (ou…),  recebendo a informação de qual chave alterar, e o valor a ser atualizado, só.

Simples não!? Agora você pode testar o programa e ver ele funcionar com o nosso avatar se movendo para os lados.

Na próxima aula vamos falar de colisões, e de como tratar os eventos gerados por elas.

Aqui está o arquivo final desta aula.

Games, Panda3D , ,