Archive

Posts Tagged ‘python’

Desligar terminais Ubuntu de uma LAN

December 12th, 2009

Olá,

Neste post vou relatar uma solução que encontrei para um problema que eu tinha. Eu atualmente estou trabalhando em um local que é basicamente uma LAN com 20 terminais com Ubuntu, e de quando em quando, eu tenho que desligar os computadores e …. puta preguiça de fazer isso um por um. Então pensei, isso é ridículo!! Deve haver uma forma de automatizar isso! Dei uma pesquisada pela net mas não achei nada… não procurei tanto assim… :)

Então tomei a decisão de fazer alguma coisa… tipo um script (em Python) que mandasse um sinal via rede, e do outro lado, um programa que desse a ordem de desligar. Depois de uma certa odisseia, cheguei a uma solução, e o melhor, aprendi uma coisa incrível: a possibilidade de editar os perfis de usuário do Linux, editando o arquivo sudoers.

Depois que eu tinha tudo pronto o Raidicar, me deu uma outra chance de solução bem legal no fórum do Ubuntu-Br, que se complementaria perfeitamente com parte desse tutorial. Veja o link.

Vou explicar a coisa toda de forma sucinta, para maiores detalhes visite os links sugeridos.

Idéia & Problema

Então a idéia inicial era a seguinte, um script em python que recebesse via rede um sinal e executasse o comando shutdown, ou init. O problema que apareceu logo de cara foi que, ambos os comandos shutdown e init, exigem por padrão a execução em modo de super usuário (sudo), e isso conseqüentemente exige a senha do usuário. E como se não bastasse, os micros em questão, rodam 90% do tempo em usuário sem privilégio administrativos, e dessa forma os comandos shutdown e init, por padrão, não funcionam! :S

Então depois de uma pesquisa nos fóruns do Ubuntu achei uma dica, a possibilidade de se personalizar as ações de usuário com perfis distintos editando o arquivo sudoers. Neste link, há uma explicação muito bacana sobre como se editar o arquivo sudoers (em inglês)

Seguindo as instruções do link, eu fiz uma configuração no arquivo sudoers, que não exige senha para a execução dos comandos shutdown, init e reboot nos usuários sem privilégios administrativos. Então segue abaixo o tutorial para isso:

O arquivo sudoers está localizado na pasta /etc, e para editá-lo você precisa fazê-lo como root, e MUITO CUIDADO! Esse arquivo, por padrão tem apenas permissão de leitura, inclusive para o root, e se você mudar a permissão, o Ubuntu não roda mais o comando sudo, e aí você precisará reinstalar o sistema, então siga as instruções detalhadamente para não dar erro. SE VOCÊ FIZER MERDA, E VIER POSTAR PEDIDOS DE AJUDA, PQ VC FEZ MERDA, E NÃO SEGUIU O MEU TUTORIAL, EU VOU DAR UM TAPA NA TUA CARA, E DIZER EU EU EU VC SE FU***, ENTÃO SIGA MEU TUTORIAL TIM-TIM POR TIM-TIM!

1. Como usuário administrador, abra o Terminal, e digite o comando: sudo nautilus

Entre com sua senha e navegue para a pasta /etc: Sistema de Arquivos > etc, e encontre o arquivo sudoers

2. Clique-direito do mouse, vá em Permissões, e habilite Leitura e Escrita para o usuário root. ATENÇÃO: NÃO FECHE O NAUTILUS! SE O NAUTILUS FOR ENCERRADO, COM O ARQUIVO SUDOERS COM PERMISSÃO DE ESCRITA HABILITADO, O COMANDO SUDO NÃO VAI MAIS FUNCIONAR, E VC PRECISARÁ REINSTALAR SEU SISTEMA! Então apenas habilite o modo escrita, clique em ‘Fechar’ (canto direito inferior da telinha de Propriedades)

3. Clique-direito do mouse no arquivo sudoers, e “Abrir com Editor de Texto”, agora vamos editar o arquivo. Por padrão vc vai ver isso (Ubuntu 9.04)

Mas o arquivo deve ser editado assim (nesse caso vale ler a explicação detalhada abaixo da imagem)

OBS: ERRATA! Na linha 18, acima, está escrito ‘Cmnd_ALias’, o correto é ‘Cmnd_Alias’.

Aqui o que acontece na linha 13, é que primeiro criamos um grupo de usuários com perfis semelhantes, no caso, usuários sem privilégios administrativos (USER), que engloba apenas o usuario14. Poderiam haver mais usuários… bastaria colocá-los entre vírgulas, ex: User_Alias USER = usuario14, paulo, maira. Com isso os usuários paulo e maira, estariam no mesmo grupo.

Esse usuário14, é o nome do usuário no terminal 14, logo, em cada máquina da Lan, esse nome possivelmente será diferente.

Nas linhas 16, 17 e 18, são criados uma espécie de variáveis que representarão os comandos que eu quero configurar para o usuário14, no caso o comando shutdown, reboot e init. Os executáveis desses comandos estão na pasta /sbin do sistema, portanto devem ser declarados com o caminho completo.

Finalmente na linha 22, é onde o sudoers recebe a configuração desejada. Tentando traduzir aquilo para bom português, que é dito é mais ou menos o seguinte: O grupo USER, de qualquer terminal (leia-se console de comando), não mais precisará de senha para os comandos SHUTDOWN, REBOOT e INIT. Ou seja, a partir de agora os usuários sem privilégios administrativos, poderão executar os comandos de desligar ou reiniciar, vindos de algum console de comando sem solicitação de senhas, então o meu script passará a funcionar bonitinho! :) O script do Raidicar também! :)

4. Agora salve as alterações no arquivo e feche o Editor de Texto. E AGORA VOLTE ÀS PROPRIEDADES DO ARQUIVO sudoers E COLOQUE-O NOVAMENTE COM A PERMISSÃO DE “Apenas Leitura”.

Clique no botão ‘Fechar’.

E SÓ AGORA VOCÊ PODE FECHAR O NAUTILUS. NÃO FECHE O NAUTILUS SEM TER A CERTEZA DE QUE O ARQUIVO sudoers ESTÁ COM APENAS A PERMISSÃO PARA LEITURA CONFIGURADA PARA TODOS OS USUÁRIOS.

Agora essa etapa está encerrada, e falta apenas a instalação do programa.

SCRIPTS

Script para os Terminais

O sistema como um todo funciona com um pequeno script gravado nos terminais e configurado para ser iniciado com a sessão do usuário, e um outro que roda no computador central - no caso o meu computador :) - que dispara as ordens de desligar e reiniciar os terminais da LAN.

Começando pelo script dos terminais, você pode baixá-lo aqui. Não vou entrar em detalhes do script, mas usei como base para criá-lo uma receita do CookBook do comunidade Python Brasil, veja só.

A instalação e configuração do script é super simples. Primeiro eu recomendo que você crie uma pasta oculta dentro da pasta de usuário e colocar o script down.py nessa pasta. Para criar uma pasta oculta basta colocar um ponto(.) na frente do nome que vc quiser. Por exemplo, vou criar uma pasta oculta chamada ‘down’, abro o navegador de arquivos, vou até a Pasta de Usuário, clique-direito do mouse e “Criar nova Pasta”, e dar o nome de ‘.down’, e pronto, basta entrar na pasta e colocar o arquivo down.py dentro dela, e aqui vale uma observação importante, o arquivo down.py precisa ter permissão de execução como programa. Para fazer isso, clique-direito sobre o arquivo, vá em Propriedades >> Permissões >> Permitir execução do arquivo como programa.

Depois disso basta configurar a sessão de usuário para executar esse arquivo assim que a sessão for iniciada. Na sessão do usuário, vá em SISTEMA >> PREFERÊNCIAS >> APLICATIVOS DE SESSÃO (Ubuntu 9.04). Em Ubuntus mais antigos, veja isso em SISTEMA >> PREFERÊNCIAS >> SESSÕES.

Clique em Adicionar, então preencha as informações:

NOME: Down

COMANDO: /home/usuario14/.down/down.py

COMENTÁRIO: ah… qualquer coisa, se quiser.

No Comando, a pasta do usuário (usuário14 neste caso), fica dentro da pasta home, que geralmente está na raíz do sistema, mas isso pode variar. Dessa forma a cada sessão o programa que recebe o sinal via rede e executa o comando de desligar ou reiniciar, será automaticamente iniciado. :)

Script para a administração

O script para administração, ou seja, o micro que mandará as ordens para os terminais foi batizado de downComand.py, baixe-o aqui.

Há um detalhe importante para que você possa usar esse script e fazer algumas alterações para que ele te sirva perfeitamente. Como relatei no início tenho uma LAN com 20 máquinas, então nesse script, eu organizei uma tabela com os números dos computadores e seus respectivos ips dentro da LAN, e você pode adicionar, retirar, ou re-configurar esses ips, de acordo com a sua LAN.

O uso é super simples, você irá rodar o script escolhendo o micro e o que fazer, exemplo:

python downComand.py 10 desligar

Ou seja, micro 10: desligar

python downComand.py todos reiniciar

Ou seja, todos os micros: reiniciar

Há ainda uma pequena opção que fiz agrupando os micros, de acordo com sua posição ‘geográfica’. Na sala dessa LAN, temos duas mesas pequenas que chamamos de ilhas, e uma mesa maior que apelidamos de ferradura, e no script, também posso dizer: python downComand.py ferradura desligar ; e os micros da ferradura serão desligados.

Atualização: Aqui é possível baixar uma versão com interface gráfica, que só será interessante se vc souber editar scripts em Python, para personalizá-lo para seu caso.

Conclusão

Bem, o mais complicado mesmo é a configuração do usuário, o sudoers, e fazer isso micro por micro é um trabalhinho, mas depois, vale a pena. :) posso até sacanear alguém desligando/reiniciando o micro na surdina hahahaha. Mas o mais bacana nesse processo, para mim, foi aprender esse esquema de configuração do sudoers, que se você ler o link que indiquei anteriormente, vai ver que abre um monte de possibilidades bacanas, essa é só uma delas.

E Linux em geral é assim… pesquisando, lendo e aprendendo. Ele sempre exige uma postura ativa ante a tecnologia, que é um pouco menor quando se usa Mac, e quase nula quando se usa Windows. Um exemplo disso vi na minha pós-graduação (Produção de Games), meus colegas são, em sua maioria, formados em TI - eu sou formado em Desing Gráfico -, mas não usam Linux e em vários momentos do curso, eu tinha um conhecimento de várias bibliotecas e softwares livres para som ou gráficos, que eles não conheciam.

Tutoriais , , , ,

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 1 - GameCultura - Panda3D

January 10th, 2009

Nesta série de posts, vou transcrever o conteúdo das aulas que ministrei no evento GameCultura, SESC Pompéia, Jan/2009. Os arquivos para a aula você encontra aqui

Programação para jogos, é um recorte do universo da tecnologia, bem considerável, em termos de complexidade. Existem muitas camadas possíveis para ser tratar, mas como estamos tratando de uma iniciação, vamos explorar programação de script em uma game engine. De forma sintética, programar scripts, em uma game engine, significa que vamos focar toda nossa programação no gameplay, do jogo.

Game Engine: O que é?

Basicamente podemos dizer que um game engine é uma ferramenta para produção de jogos. Elas surgiram e ganharam importância devido a crescente complexidade de programação que os games começaram a exigir.

Usando um game engine, você não precisa se preocupar demasiadamente com a programação dos gráficos, ou áudio, ou acesso a recursos do sistema operacional, porque esses recursos são gerenciados pela engine, assim você foca a programação nas questões do gameplay do jogo.

Para a oficina elegi o Panda3D como game engine pelas seguintes razões:

    * é uma engine simples de se utilizar

    * é livre

    * é multiplataforma

    * tem uma comunidade voluntariosa

    * e uma documentação fácil de se seguir, apesar se ser em inglês

 

Antes de começar a ver o código script do Panda3D, vamos ver com mais detalhes um dos elemento básicos do Panda, que é o Grafo de Cena, e seus nós.

 

Grafo de cena

Um grafo de cena é uma representação hierárquica da cena, podemos entendê-lo melhor através desse gráfico.

grafo de cena

Nesta imagem temos uma representação gráfica de um Grafo, organizado para representar um cenário composto por cinco edifícios e um personagem.

Todo grafo de cena nasce a partir de um objeto central, o “caule central de uma árvore”, onde os demais objetos devem ser linkados. Então eles são chamados de nós do grafo.

No Panda3D, o grafo de cena sempre começa no objeto “render”, ele é o ‘Pai’ de toda cena no Panda3D. E a medida que objetos são adicionados à cena eles devem, ser filhos do render, ou ao menos ‘neto’ dele. No exemplo acima, temos um nó chamado “Edifícios”, este nó apenas contém referência à outros nós, os quais possuem as informações de cada ‘edifício’. Essa organização é interessante por vários motivos, mas vale destacar: ela permite que a engine crie gráficos com maior rapidez, e nos permite controlar efeitos, ou ações que podem ter alvos específicos na cena, com maior precisão.

Vale enfatizar que os nós, não são os objetos de cena em sí, e sim representações do mesmo, definindo apenas algumas características desses objetos, como posição, rotação, escala, iluminação etc…

Observe abaixo como seria a representação gráfica do grafo acima:

representação gráfica do Grafo de cena

 

Essa primeira explicação se faz necessária porque sem ela, muito provávelmente se você for iniciante, não vai entender nada do que está por vir.

Mão na massa

O Panda3D, é usado completamente via código, não possui uma interface gráfica. Isso é muito comum, acho até que uma constante, em game engines livres, pelo menos por enquanto. Dessa forma para se trabalhar com o Panda3D, nós só precisamos de um editor de texto. Mas o uso de IDEs é sempre bom, primeiro porque ele te ajuda em coisas básicas, e facilita seu trabalho.

Durante essa oficina nós iremos começar com códigos bem simples, e iremos aumentando a complexidade aos poucos. Neste primeiro código, vamos explorar coisas bem básicas, e fundamentais de se entender, como o Grafo de Cena, importar módulos, objetos 3D.

Importando Módulos

Módulos são outros arquivos python, que podem ser utilizados para viabilizar ou facilitar algumas tarefas que temos que executar.

Trabalhando no Panda3D, teremos uma série de módulos que temos que importar para utilizar as facilidades que a engine nos possibilita.

O primeiro deles é o DirectStart, ele possui as ferramentas básicas da game engine.
 

import direct.directbase.DirectStart

Outros módulos importantes:
 

from direct.actor.Actor import Actor
from direct.task.Task import Task
from pandac.PandaModules import *
from direct.showbase.DirectObject <span>import</span> DirectObject

Models e Actors

Vamos usar os primeiro objetos 3D do nosso jogo, nossa arena e nossa personagem.

Primeiro importamos o modelo 3D, para um nodePath, do nosso grafo de cena, que chamaremos de arena.

arena = loader.loadModel(‘eggs/plataformaBase’)

agora vamos configurar esse nosso nodePath.

arena.setScale(4)
arena.reparentTo( render )
arena.setPos(20,30,5)
arena.setHpr(0,0,0)

Primeiro, decidimos uma escala para o nosso node (setScale => 1:escala normal; 0.5:reduz 50%; 2:duplida ), na segunda linha “parenteamos” o node ao objeto render, que é o “pai de todos” no nosso grafo de cena. posteriormente definimos uma posição (setPos(x,y,z)) para o objeto no mundo, e uma rotação no eixo Z

Agora na última linha colocamos o ‘Main Loop’ do Panda. É ele que faz tudo acontecer:

run()

Isso mesmo, apenas um comandinho: run() … :)

Se rodarmos agora nosso programa, teremos ao menos nossa arena sendo exibida. :) … Para rodar o programa… bem se vc estiver usando Linux ou Mac, use o terminal, vá até o diretório onde estão os seus arquivos, e digite: python nome_do_arquivo.py ; agora se vc estiver usando Windows, e se o Python estiver corretamente configurado, basta clicar duas vezes no ícone o arquivo. Você verá algo semelhante a isso: 

visão da primeira tela do demo

 
Agora vamos carregar nossa heroína, antes do comando run() (QUE SEMPRE DEVE SER A ÚLTIMA COISA DO SEU CÓDIGO):

persona = Actor( "eggs/personagem", {"idle": "eggs/personagem-parado"} )

 
Aqui já notamos uma diferença grande de como importamos a arena. Bem, isso acontesse porque no Panda temos duas formas de se importar objetos 3D: o Model, e o Actor.

Basicamente, a diferença maior está em que os Actors são objetos que possuem animações, e os Models não.

E na declaração acima, a animação está naquele dicionário. E agora vamos configurar o nosso nodePath persona:

persona.setScale(0.2)       #escala
persona.reparentTo(render)  #reparenteando para o render
persona.setPos(0,5,-0.5)    #definimos uma posição
persona.setH(90)            #definimos Rotação
persona.loop(‘idle’)        #definimos qual animação vai fazer loop

 

Vamos fazer uma breve pausa para falar sobre o formato de arquivo .egg, que é o formato nativo de modelos 3D do Panda. O formato .egg, é basicamente um arquivo de texto, estilo XML. Ele pode ser obtido usando plug-ins que são fornecidos pelo Panda, para converter modelos 3D criados em softwares de modelagem. Para animações, o arquivo .egg, separa as informações de animações das informações relativas ao mesh propriamente dito. Outra propriedade muito bacana, é que ele respeita a criação de grupos de vértices dos modelos, que podem ser usados dentro do jogo para algum tipo de ação criada por script.

Executando o programa até aqui, teríamos algo mais ou menos assim:

visualização do game

 

Continuação…

Bem nesta primeira aula, o objetivo é expor a galera ao código básico do Panda, e à idéia do grafo de cena. Agora é legal você ir alterando algumas propriedades do setPos(), setHpr(), setH() para ver o que acontece, e caso você perca o objeto de vista, uso o mouse para navegar pela cena. Usando o botão direito +  movimentando o mouse na vertical vc altera o ‘zoom’; usando o botão esquerdo, você movimenta a tela como em uma panorâmica; usando os dois botões do mouse você rotaciona sua visão no mundo.

Existem outros objetos que podemos adicionar a nossa cena (encontre os arquivos do curso aqui), entre eles um papel de parede(tipo sky-dome), uma célula de energia, e uns obstáculos. Tente adicioná-los sozinho, apenas com o que já vimos até aqui, mas se caso estiver com muita pressa ou preguiça, você encontra o arquivo final da aula aqui

Observações Importantes

Se quando você tentar rodar o programa, surgir um erro como esse:

imagem de erro no terminal

AttributeError: ‘NoneType’ object has no attribute ’setScale’

Isso pode estar sendo causado por um endereçamento errado de algum arquivo externo, como por exemplo um arquivo .egg, portando é sempre bom você manter seus arquivos organizados em pastas, por exemplo uma pasta ’src’ para o código, uma pasta ‘eggs’ para os modelos, com uma sub-pasta ‘texturas’.

Veja a organização que usei neste projeto:

organização do projeto: Game-Demo > src; Game-Demo > res > eggs, texturas, som

Games, Panda3D, Tutoriais , ,