Sobre el 3D, Un tutorial de panda3D

Panda3d, blender y otras herramientas para juegos en 3D

Sobre el 3D, Un tutorial de panda3D

Notapor Barajas » Mar Dic 27, 2011 2:24 am

Mi primer juego en 3D.

Antes de empezar.

Este tutorial-articulo esta destinado a ser una pequeña y fácil guía para hacer un pequeño juego 3D.
Antes que nada, unas consideraciones básicas.

Lo que necesitas tener:

Panda3D, saber tanto Python y programación estructurada. (en teoría si sabes la primera, se supone que sabes la segunda ;) )

Lo que (mas aparte) deberías tener:

Conocimientos de programación orientada a objetos y saber manejar un programa 3D (aun que no se vera en este tutorial eso, ayuda mucho al momento de hacer juegos 3D).

Sobre los modelos...

Puedes encontrarlos adjuntos al articulo. Para este caso los modelos están hechos utilizando Blender. No se habla de herramientas que exportan directamente a .egg (el formato por defecto de panda3D), ya que, me parece mucho más simple exportar desde blender a .x (el formato de DirectX) y, después trasformar a .egg usando los transformadores que vienen junto con panda3D. Si bien, es posible exportar a .egg usando blender, esta característica de exportación esta activada en Blender 2.4x no en superiores como 2.5 y 2.6, eso lo pondré en otro hilo en este foro.

Sobre el 3D (algo muy, muy, obvio)...

Una cosa que resulta obvia, al hablar de 3D es que trabajamos en un espacio tridimensional (sí, capitán obvio). Esto significa que, se agrega un nuevo eje al espacio. En otras palabras, para representar la posición en el espacio de un elemento, en lugar de ser la típica representación cartesiana de toda la vida (X,Y); pasa a ser: (X,Y,Z).

La instalación de panda 3D.

No se toca ese tema. Si se tienen problemas instalando, para eso esta el foro ;).

Ahora sí.

Mi primera ventana.


import direct.directbase.DirectStart
run()


Con esas dos simples lineas, creamos una ventana. Lo que sucede, en la primera linea, importamos todo lo mínimo necesario para que panda funcione (en realidad, es mas parecido eso a una plantilla), y en la segunda, la función "run" es la encargada de echar a andar los componentes, incluyendo el bucle principal del programa.

Creando un mundo.

import direct.directbase.DirectStart
class Mundo():
def __init__(self):
base.setBackgroundColor(0,0,0)
mundo = Mundo()
run()


Esta vez, el código es mucho mas largo. ¿Que hemos hecho?
/*si sabes que es orientado a objetos pasa de aquí...

si no:
lee esto primero: http://mundogeek.net/archivos/2008/03/05/python-orientacion-a-objetos/

*/hasta aquí...

Se creo en el ejemplo de arriba una clase llamada Mundo (podría tener cualquier otro nombre). En esa clase, introducimos todo lo que hagamos, para nuestro juego. Y para que lo escrito en la clase suceda en la ventana, es necesario, instanciar con "mundo = Mundo()". En este caso, la función de la linea "base.setBackgroundColor(0,0,0)" sera cambiar el color de fondo de gris a negro.

Un escenario.

Ahora cargaremos un modelo a nuestro programa, para eso se agregan las siguientes lineas:

import direct.directbase.DirectStart
class Mundo():
def __init__(self):
base.setBackgroundColor(0,0,0)
self.mundo = loader.loadModel("cilindro.egg")
self.mundo.reparentTo(render)
mundo = Mundo()
run()


En la linea:
self.mundo = loader.loadModel("cilindro.egg")

Creamos una variable donde almacenamos nuestro modelo, y lo cargamos, en este caso, el modelo esta en la misma carpeta que nuestro programa. De no ser así, para cargar el archivo, se toma como referencia la ruta CON RESPECTO A NUESTRO PROGRAMA.
La variable es ahora, un tipo de dato llamado nodePath

En la siguiente linea:
self.mundo.reparentTo(render)

Es lo que "dibuja" al modelo en la pantalla.

podemos modificar su posición usando los métodos setPos(x,y,z) -general-, o setX(x), setY(y), setZ(z) :


import direct.directbase.DirectStart
class Mundo():
def __init__(self):
base.setBackgroundColor(0,0,0)
self.mundo = loader.loadModel("cilindro.egg")
self.mundo.setPos(0, 0, 0) #recuerden (x,y,z)
self.mundo.reparentTo(render)
mundo = Mundo()
run()


...o su tamaño con setScale(x,y,z) -general-, setSx(x), setSy(y), setSx(z)...

import direct.directbase.DirectStart
class Mundo():
def __init__(self):
base.setBackgroundColor(0,0,0)

self.mundo0 = loader.loadModel("cilindro.egg")
self.mundo0.setPos(0, -5, 0)
self.mundo0.setSz(10) #crece en el eje Z
self.mundo0.reparentTo(render)

self.mundo1 = loader.loadModel("cilindro.egg")
self.mundo1.setPos(0, 0, 0)
self.mundo1.setSy(10)#crece en el eje Y
self.mundo1.reparentTo(render)

self.mundo2 = loader.loadModel("cilindro.egg")
self.mundo2.setPos(0, 5, 0)
self.mundo2.setSx(10)#crece en el eje X
self.mundo2.reparentTo(render)
mundo = Mundo()
run()


Un tip es que escalen a la misma relación los modelos al cargarlos :)

...también, podemos girar los modelos de forma simple con los métodos

setH(h) -gira con respecto al eje z un ángulo h (la h viene de Heading rotates)-
setP(p) -gira con respecto al eje x un ángulo p (la p es de Pitch rotates)-
setR(r) -gira con respecto al eje y un ángulo r (y la r es por Roll rotates)-
setHpr(h,p,r) -una combinación de los anteriores-

en acción:

import direct.directbase.DirectStart
class Mundo():
def __init__(self):
base.setBackgroundColor(0,0,0)

self.mundo0 = loader.loadModel("cilindro.egg")
self.mundo0.setPos(0, -5, 0)
self.mundo0.setSz(10)
self.mundo0.setH(45) #gira con respecto al eje Z
self.mundo0.reparentTo(render)

self.mundo1 = loader.loadModel("cilindro.egg")
self.mundo1.setPos(0, 0, 0)
self.mundo1.setSy(10)
self.mundo1.setR(45) #gira con respecto al eje Y
self.mundo1.reparentTo(render)

self.mundo2 = loader.loadModel("cilindro.egg")
self.mundo2.setPos(0, 5, 0)
self.mundo2.setSx(10)
self.mundo2.setP(45) #gira con respecto al eje X
self.mundo2.reparentTo(render)
mundo = Mundo()
run()


Sabiendo esto, podemos acomodar nuestro modelo en la pantalla:

import direct.directbase.DirectStart
class Mundo():
def __init__(self):
base.setBackgroundColor(0,0,0)
self.mundo0 = loader.loadModel("cilindro.egg")
self.mundo0.setScale(2,2,2)
self.mundo0.setPos(0, 15, -5)
self.mundo0.setH(90)
self.mundo0.reparentTo(render)
mundo = Mundo()
run()


Funciones internas para mover los objetos con código....
El gestor de tareas (en ingles, task Manager)


El gestor de tareas, como dice su nombre, se encarga de gestionar las "tareas", las tareas son cosas a realizar por panda mientras se ejecuta el programa. El gestor de tareas ejecuta las tareas como si una cola se tratara, y siempre en orden.


import direct.directbase.DirectStart
class Mundo():
def __init__(self):
base.setBackgroundColor(0,0,0)
self.mundo0 = loader.loadModel("cilindro.egg")
self.mundo0.setScale(2,2,2)
self.mundo0.setPos(0, 15, -5)
self.mundo0.setH(90)
self.mundo0.reparentTo(render)
print taskMgr #esta linea, imprime las tareas en el gestor
mundo = Mundo()
run()


Como se habrán dado cuenta, no utilizamos un bucle, de eso se encarga la función run(), así que, ¿Como aplicamos una función sobre nuestro modelo?, facíl, creamos una nueva tarea.

import direct.directbase.DirectStart
class Mundo():
def __init__(self):
base.setBackgroundColor(0,0,0)
self.mundo0 = loader.loadModel("cilindro.egg")
self.mundo0.setScale(2,2,2)
self.mundo0.setPos(0, 15, -5)
self.mundo0.setH(90)
self.mundo0.reparentTo(render)
taskMgr.add(self.gira_cilindro, "gira_cilindro")
def gira_cilindro(self, task):
self.mundo0.setR(self.mundo0,-1)
return task.cont

mundo = Mundo()
run()


En la linea:
taskMrg.add(self.gira_cilindro, "gira_cilindro")

Agregamos la tarea a nuestro gestor de tareas (taskMrg) con su método add(función_que queremos_agregar, nombre_que_queramos).

creamos el método con:
def gira_cilindro(self, task):


Es importante notar, que el primer argumento después de self, el siguiente argumento debe ser task, para que sea agregado al gestor. Todo lo que esta dentro del metodo es lo que realizara el gestor de tareas

Aquí, aparece la linea:
self.mundo0.setR(self.mundo0,-1)


La razón por la cual lo pasamos como propio argumento a su metodo, es para que sea "consiente" de que el giro es relativo a si mismo.

Y al final:

return task.cont


Esta linea, su trabajo es, hacer que la tarea se ejecute una vez más (cont de continue).

Ahora, veremos al cilindro rodando.

Una demostración de como funciona, (y un repaso por lo anterior)

import direct.directbase.DirectStart
class Mundo():
def __init__(self):
base.setBackgroundColor(0,0,0)

self.mundo0 = loader.loadModel("cilindro.egg")
self.mundo0.setPos(0, -15, 0)
self.mundo0.setSz(5)
self.mundo0.setH(45) #gira con respecto al eje Z
self.mundo0.reparentTo(render)

self.mundo1 = loader.loadModel("cilindro.egg")
self.mundo1.setPos(0, 0, 0)
self.mundo1.setSy(5)
self.mundo1.setR(45) #gira con respecto al eje Y
self.mundo1.reparentTo(render)

self.mundo2 = loader.loadModel("cilindro.egg")
self.mundo2.setPos(0, 15, 0)
self.mundo2.setSx(5)
self.mundo2.setP(45) #gira con respecto al eje X
self.mundo2.reparentTo(render)
taskMgr.add(self.gira_cilindros, "gira_cilindros")
def gira_cilindros(self, task):
self.mundo0.setH(self.mundo0,-1)
self.mundo1.setR(self.mundo1,-1)
self.mundo2.setP(self.mundo2,-1)
return task.cont
mundo = Mundo()
run()



Modificándolo un poco nuestro programa, podemos agregar un delay, es decir, que tarde un tiempo antes de iniciar la acción.

import direct.directbase.DirectStart
class Mundo():
def __init__(self):
base.setBackgroundColor(0,0,0)
self.mundo0 = loader.loadModel("cilindro.egg")
self.mundo0.setScale(2,2,2)
self.mundo0.setPos(0, 15, -5)
self.mundo0.setH(90)
self.mundo0.reparentTo(render)
taskMgr.doMethodLater(5,self.gira_cilindro, "gira_cilindro")
def gira_cilindro(self, task):
self.mundo0.setR(self.mundo0,-1)
return task.cont

mundo = Mundo()
run()


La linea:

taskMrg.add(self.gira_cilindro, "gira_cilindro")


se a remplazado por:

taskMgr.doMethodLater(5,self.gira_cilindro, "gira_cilindro")


doMethodLater, realiza la misma función que add, solo que, recibe como argumento, cuantos segundos esperar antes de comenzar a ejecutar la acción, en este caso, esperara 5 segundos antes de ponerlo a rodar. Si fuese ha hacerse una vez, y esperara nuevamente, 5 segundos antes de ejecutarse, nosotros tendríamos que cambiar return task.cont por return task.again (again = de nuevo, si no se te da mucho el ingles ;) )


import direct.directbase.DirectStart
class Mundo():
def __init__(self):
base.setBackgroundColor(0,0,0)
self.mundo0 = loader.loadModel("cilindro.egg")
self.mundo0.setScale(2,2,2)
self.mundo0.setPos(0, 15, -5)
self.mundo0.setH(90)
self.mundo0.reparentTo(render)
taskMgr.add(5,self.gira_cilindro, "gira_cilindro")
def gira_cilindro(self, task):
self.mundo0.setR(self.mundo0, -1)
return task.cont

mundo = Mundo()
run()


Utilizando diferencia de tiempo (delta t).

Nuestro cilindro rueda, pero, si la cantidad de FPS (Frames por segundo :) )del juego es grande, se vera mucho mas rápido. Imaginemos que por cada frame de nuestro juego, el tiempo es de 0.1 unidades, es decir, si corre a 60 FPS, el tiempo de un segundo seria 6 unidades, si va a 30 FSP, son 3 unidades. Ahora, imaginemos que esas unidades son el numero de veces que se realizan las tareas de nuestro gestor.

Para lograr velocidad constante, se utiliza la técnica de la diferencia de tiempo (delta t).

en nuestra función que mueve el cilindro (girar_cilindro) creamos una variable donde almacenaremos la diferencia de tiempo.

dt = globalClock.getDt()


El secreto es, ahora, poner cuanto queremos que se mueva en un segundo y multiplicarlo por dt, en el programa seria:

self.mundo0.setR(self.mundo0, -20*dt)


es decir, que girara 20 grados cada segundo. Esto se aplica también para los métodos de cambio de posición.

El programa quedaría así:

import direct.directbase.DirectStart
class Mundo():
def __init__(self):
base.setBackgroundColor(0,0,0)
self.mundo0 = loader.loadModel("cilindro.egg")
self.mundo0.setScale(2,2,2)
self.mundo0.setPos(0, 15, -5)
self.mundo0.setH(90)
self.mundo0.reparentTo(render)
taskMgr.add(self.gira_cilindro, "gira_cilindro")
def gira_cilindro(self, task):
dt = globalClock.getDt()
self.mundo0.setR(self.mundo0, -20*dt)
return task.cont

mundo = Mundo()
run()


Pero, surge un problema. ¿Que sucede si la cantidad de FPS cambia?, si en un momento son muchos y al siguiente baja.

Esto se soluciona con dos lineas más de código:

if  dt > 0.20:
return task.cont


En el if, se revisa si esta comenzando a aumentar el numero de FPS, de pasar eso, hace que la tarea se agrege al gesto una vez más. Agregando estas lineas, queda así el programa.

import direct.directbase.DirectStart
class Mundo():
def __init__(self):
base.setBackgroundColor(0,0,0)
self.mundo0 = loader.loadModel("cilindro.egg")
self.mundo0.setScale(2,2,2)
self.mundo0.setPos(0, 15, -5)
self.mundo0.setH(90)
self.mundo0.reparentTo(render)
taskMgr.add(self.gira_cilindro, "gira_cilindro")
def gira_cilindro(self, task):
dt = globalClock.getDt()
if dt > 0.20:
return task.cont
self.mundo0.setR(self.mundo0, -20*dt)
return task.cont

mundo = Mundo()
run()



Agrego una nave al programa:

import direct.directbase.DirectStart
class Mundo():
def __init__(self):
base.setBackgroundColor(0,0,0)
self.mundo0 = loader.loadModel("cilindro.egg")
self.nave = loader.loadModel("nave_01.egg")

self.nave.setScale(1,1,1)
self.mundo0.setScale(2,2,2)

self.nave.setPos(0,15,0)
self.mundo0.setPos(0, 15, -5)

self.nave.setH(180)
self.mundo0.setH(90)

self.nave.reparentTo(render)
self.mundo0.reparentTo(render)

taskMgr.add(self.gira_cilindro, "gira_cilindro")

def gira_cilindro(self, task):
dt = globalClock.getDt()
if dt > 0.20:
return task.cont
self.mundo0.setR(self.mundo0, -20*dt)
return task.cont

mundo = Mundo()
run()


Eventos y entradas de usuario.

Nuestro programa se ve -considerando lo poco trabajado que esta- bastante bien. Pero no sirve de mucho si no podemos hacer nada mas que ver. Ahora, vamos a trabajar en lo necesario para que la nave añadida se mueva cuando presionemos una tecla. Para eso, agregamos al principio de nuestro archivo la siguiente linea:

from direct.showbase.DirectObject import DirectObject


DirectObject es una clase interna de panda3D que contiene utilidades para manejar los objetos y el mundo. Entre sus utilidades, se encuentra el método accept().

ahora, justo donde comienza nuestra clase Mundo(), heredamos los métodos de DirectObject para que ahora, sean nuestros:

class Mundo():


pasa a ser
class Mundo(DirectObject)


La sintaxis del método accept es accept("nombre_del_evento", función_a_hacer).


import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject #importamos DirectObject

class Mundo(DirectObject): #lo heredamos
def __init__(self):
base.setBackgroundColor(0,0,0)
self.mundo0 = loader.loadModel("cilindro.egg")
self.nave = loader.loadModel("nave_01.egg")

self.nave.setScale(1,1,1)
self.mundo0.setScale(2,2,2)

self.nave.setPos(0,15,0)
self.mundo0.setPos(0, 15, -5)

self.nave.setH(180)
self.mundo0.setH(90)

self.nave.reparentTo(render)
self.mundo0.reparentTo(render)

self.accept("h", self.di_hola)
# en esta parte usamos accept
# el nombre del evento es "h"
# la función es di_hola

taskMgr.add(self.gira_cilindro, "gira_cilindro")

def gira_cilindro(self, task):
dt = globalClock.getDt()
if dt > 0.20:
return task.cont
self.mundo0.setR(self.mundo0, -20*dt)
return task.cont

def di_hola(self):
print "Hola a todos"

mundo = Mundo()
run()


El programa de arriba contiene la implementación del método accept, en particular, al apretar la letra h, mandara a llamar a la función "di_hola" y realizara su contenido, en este caso, imprimir un "Hola a todos".

La pregunta que debes estar haciendo (o deberías ) es ¿cuales son los nombres de eventos del teclado?.

Panda3D contiene un nombre de evento para casi todas las teclas, por ejemplo:

"a", "d", "2", "{"
... etc.

todos esos, son nombres validos.

Los que no lo son:

"A", "B", "*", "#"


De los que no funcionan, lo que tienen en común, es que son en mayúsculas, o el segundo carácter de una tecla. Esto puede parecer bueno en principio, pero en forma personal, es molesto, ya que, en un teclado ingles, el primer carácter es "[", pero el segundo "{" Así que hay que tener cuidado con eso.

El evento para que al dejar de presionar una tecla se llame a la función es agregando un "-up" al nombre:

"a-up", "2-up", "{-up"


Y si queremos que el evento ocurra mientras mantenemos presionada la tecla es agregando "-repeat" al nombre:

"a-repeat", "2-repeat", "{-repeat"


Los nombres de eventos para teclas que no tienen un carácter imprimible, están por su nombre en ingles:

"escape", "f1", "f2", "f12", "print_screen", "scroll_lock", "num_lock", "backspace", "insert", "home", "page_up", "delete", "end", "page_down", "caps_lock", "enter", "space", "tab", "pause", "arrow_left", "arrow_up", "arrow_down", "arrow_right", "shift", "lshift", "rshift", "control", "alt", "lcontrol", "lalt", "ralt", "rcontrol"


Algunas teclas especiales (shift, control, alt), pueden llamarse en combinaciones:

"shift-a", "control-a", "shift-control-a", "shift-control-alt-a"


También se pueden gestionar las entradas del ratón con este método. Los nombres de los eventos para el ratón son:

"mouse1": clic izquierdo
"mouse2": botón medio
"mouse3": clic derecho



Creando un keyMap (mapa de teclas) y extendiendo la funcionalidad de accept()


Un keyMap (usare el nombre en ingles) es solo un diccionario en el que meteremos las teclas y su valor binario,
en este caso:

self.keyMap = {"a":False, "s":False, "d":False, "w":False}


Comente que extenderíamos la funcionalidad de accept. Bien, accept no solo recibe como argumentos el nombre del evento y la función a llamar, también, puede tomar y en su momento pasar los argumentos de la función a llamar. ¿Como?.

accept("nombre_evento", función, [lista,de,argumentos,a,función])

Esto nos servirá para hacer que nuestro modelo se mueva.

En donde llamábamos a accept, lo remplazamos por lo siguiente:

self.keyMap = {"a":False, "s":False, "d":False, "w":False}
self.accept("a", self.di_tecla, ["a",True])
self.accept("s", self.di_tecla, ["s",True])
self.accept("d", self.di_tecla, ["d",True])
self.accept("w", self.di_tecla, ["w",True])
self.accept("a-up", self.di_tecla, ["a",False])
self.accept("s-up", self.di_tecla, ["s",False])
self.accept("d-up", self.di_tecla, ["d",False])
self.accept("w-up", self.di_tecla, ["w",False])


También cambiamos la función di_hola por di_tecla:


def di_tecla(self, tecla, valor):
self.keyMap[tecla] = valor


Ahora, esas partes del código sirve como un interruptor, y no solo eso, renombramos "gira_cilindro" a "actualiza" y le agregamos las siguientes lineas:

if(self.keyMap["w"] == True):
self.nave.setY(self.nave, -10 * dt)
if(self.keyMap["d"] == True):
self.nave.setX(self.nave, -5 * dt)
if(self.keyMap["a"] == True):
self.nave.setX(self.nave, 5 * dt)
if(self.keyMap["s"] == True):
self.nave.setY(self.nave, 10 * dt)


Estas ultimas lineas realizan la magia de mover la nave.

Nuestro programa queda así:

import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject #importamos DirectObject

class Mundo(DirectObject):
def __init__(self):

base.setBackgroundColor(0,0,0)
self.mundo0 = loader.loadModel("cilindro.egg")
self.nave = loader.loadModel("nave_01.egg")

self.nave.setScale(1,1,1)
self.mundo0.setScale(2,2,2)

self.nave.setPos(0,15,0)
self.mundo0.setPos(0, 15, -5)

self.nave.setH(180)
self.mundo0.setH(90)

self.nave.reparentTo(render)
self.mundo0.reparentTo(render)

base.camera.reparentTo(self.nave)
base.camera.setY(base.camera, -5)


self.keyMap = {"a":False, "s":False, "d":False, "w":False}
self.accept("a", self.di_tecla, ["a",True])
self.accept("s", self.di_tecla, ["s",True])
self.accept("d", self.di_tecla, ["d",True])
self.accept("w", self.di_tecla, ["w",True])
self.accept("a-up", self.di_tecla, ["a",False])
self.accept("s-up", self.di_tecla, ["s",False])
self.accept("d-up", self.di_tecla, ["d",False])
self.accept("w-up", self.di_tecla, ["w",False])

#no olviden cambiar el nombre del gestor de tareas
taskMgr.add(self.actualiza, "actualiza")

def actualiza(self, task):
dt = globalClock.getDt()
if dt > 0.20:
return task.cont
self.mundo0.setR(self.mundo0, -20*dt)
base.camera.reparentTo(self.nave)
base.camera.setY(base.camera, -5)
if(self.keyMap["w"] == True):
self.nave.setY(self.nave, -10 * dt)
if(self.keyMap["d"] == True):
self.nave.setX(self.nave, -5 * dt)
if(self.keyMap["a"] == True):
self.nave.setX(self.nave, 5 * dt)
if(self.keyMap["s"] == True):
self.nave.setY(self.nave, 10 * dt)
return task.cont

def di_tecla(self, tecla, valor):
self.keyMap[tecla] = valor

mundo = Mundo()
run()


Haciendo que la cámara nos siga:


Para esto, hay que desactivar todos los métodos relacionados por default con el ratón. para eso, agregamos en __init__ la siguiente linea:

base.disableMouse()


ahora, en nuestro método actualiza, debajo de donde se revisa el estado de las teclas, ponemos esto:


if(base.mouseWatcherNode.hasMouse() == True):
mpos = base.mouseWatcherNode.getMouse()
base.camera.setP(mpos.getY() * 30)
base.camera.setH(mpos.getX() * -30)


Si, se ve un poco extraño, lo que hace el programa es que la cámara gira basada en la posición del ratón en la ventana. Para ser específicos, la cantidad que gira, es un porcentaje de 30° de acuerdo con que tan lejos este el ratón del centro de la ventana.

El código luce así:


import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject #importamos DirectObject

class Mundo(DirectObject):
def __init__(self):
base.disableMouse() #desactivamos las acciones por defecto del raton
base.setBackgroundColor(0,0,0)
self.mundo0 = loader.loadModel("cilindro.egg")
self.nave = loader.loadModel("nave_01.egg")

self.nave.setScale(1,1,1)
self.mundo0.setScale(2,2,2)

self.nave.setPos(0,15,0)
self.mundo0.setPos(0, 15, -5)

self.nave.setH(180)
self.mundo0.setH(90)

self.nave.reparentTo(render)
self.mundo0.reparentTo(render)

self.keyMap = {"a":False, "s":False, "d":False, "w":False}
self.accept("a", self.di_tecla, ["a",True])
self.accept("s", self.di_tecla, ["s",True])
self.accept("d", self.di_tecla, ["d",True])
self.accept("w", self.di_tecla, ["w",True])
self.accept("a-up", self.di_tecla, ["a",False])
self.accept("s-up", self.di_tecla, ["s",False])
self.accept("d-up", self.di_tecla, ["d",False])
self.accept("w-up", self.di_tecla, ["w",False])

#no olviden cambiar el nombre del gestor de tareas
taskMgr.add(self.actualiza, "actualiza")

def actualiza(self, task):
dt = globalClock.getDt()
if dt > 0.20:
return task.cont
self.mundo0.setR(self.mundo0, -20*dt)
if(self.keyMap["w"] == True):
self.nave.setY(self.nave, -10 * dt)
if(self.keyMap["d"] == True):
self.nave.setX(self.nave, -20 * dt)
self.nave.setH(self.nave, -10 * dt)
if(self.keyMap["a"] == True):
self.nave.setX(self.nave, 20 * dt)
self.nave.setH(self.nave, 10 * dt)
if(self.keyMap["s"] == True):
self.nave.setY(self.nave, 20 * dt)

#aqui hacemos que la camara siga al raton
if(base.mouseWatcherNode.hasMouse() == True):
mpos = base.mouseWatcherNode.getMouse()
base.camera.setP(mpos.getY() * 30)
base.camera.setH(mpos.getX() * -30)
return task.cont

def di_tecla(self, tecla, valor):
self.keyMap[tecla] = valor

mundo = Mundo()
run()


Para seguirlo, hay que pensar en que tiene que ir atrás de el (para este ejemplo), en ese caso, su posición es menor. En el siguiente ejemplo, se modifica el programa.

import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject

class Mundo(DirectObject):
def __init__(self):
base.disableMouse() #desactivamos las acciones por defecto del raton
base.setBackgroundColor(0,0,0)
self.mundo0 = loader.loadModel("cilindro.egg")
self.nave = loader.loadModel("nave_01.egg")

self.nave.setScale(1,1,1)
self.mundo0.setScale(2,2,2)

self.nave.setPos(0,15,0)
self.mundo0.setPos(0, 15, -5)

self.nave.setH(180)
self.mundo0.setH(90)

self.nave.reparentTo(render)
self.mundo0.reparentTo(render)

self.keyMap = {"a":False, "s":False, "d":False, "w":False}
self.accept("a", self.di_tecla, ["a",True])
self.accept("s", self.di_tecla, ["s",True])
self.accept("d", self.di_tecla, ["d",True])
self.accept("w", self.di_tecla, ["w",True])
self.accept("a-up", self.di_tecla, ["a",False])
self.accept("s-up", self.di_tecla, ["s",False])
self.accept("d-up", self.di_tecla, ["d",False])
self.accept("w-up", self.di_tecla, ["w",False])

#no olviden cambiar el nombre del gestor de tareas
taskMgr.add(self.actualiza, "actualiza")

def actualiza(self, task):
dt = globalClock.getDt()
if dt > 0.20:
return task.cont
self.mundo0.setR(self.mundo0, -20*dt)
if(self.keyMap["w"] == True):
self.nave.setY(self.nave, -30 * dt)
if(self.keyMap["d"] == True):
self.nave.setH(self.nave, -20 * dt)
if(self.keyMap["a"] == True):
self.nave.setH(self.nave, 45 * dt)#ahora, nuestra nave gira
if(self.keyMap["s"] == True):
self.nave.setY(self.nave, 45 * dt)#tambien aqui...

#tomamos la posicion de la nave
pos = self.nave.getPos()
#hacemos que la camara este detras de la nave...
base.camera.setPos(pos[0],pos[1]-15,pos[2]+2)
#aqui hacemos que la camara siga al raton
if(base.mouseWatcherNode.hasMouse() == True):
mpos = base.mouseWatcherNode.getMouse()
#y que cabie la altitud de nuestra nave conforme movemos el raton.
self.nave.setP(mpos.getY() * -30)


return task.cont

def di_tecla(self, tecla, valor):
self.keyMap[tecla] = valor

mundo = Mundo()
run()


Bueno, eso a sido todo, aquí adjunto el programa final, con los modelos utilizados. Recuerden, dudas al final.

(Este es el primer tutorial de otros que vienen ;))
Vi veri universum vivus vici
Avatar de Usuario
Barajas
 
Mensajes: 209
Registrado: Mar Nov 16, 2010 12:06 am

Volver a 3D

¿Quién está conectado?

Usuarios navegando por este Foro: No hay usuarios registrados visitando el Foro y 1 invitado

cron