Package pilas :: Module fisica
[hide private]
[frames] | no frames]

Source Code for Module pilas.fisica

  1  # -*- encoding: utf-8 -*- 
  2  # pilas engine - a video game framework. 
  3  # 
  4  # copyright 2010 - hugo ruscitti 
  5  # license: lgplv3 (see http://www.gnu.org/licenses/lgpl.html) 
  6  # 
  7  # website - http://www.pilas-engine.com.ar 
  8   
  9  PPM = 30 
 10   
 11  import math 
 12  import pilas 
 13   
 14  try: 
 15      import Box2D as box2d 
 16      contact_listener = box2d.b2ContactListener 
 17      __enabled__ = True 
 18  except ImportError: 
 19      __enabled__ = False 
20 - class Tmp:
21 pass
22 contact_listener = Tmp 23 24
25 -def convertir_a_metros(valor):
26 """Convierte una magnitid de pixels a metros.""" 27 return valor / float(PPM)
28
29 -def convertir_a_pixels(valor):
30 """Convierte una magnitud de metros a pixels.""" 31 return valor * PPM
32 33
34 -def crear_motor_fisica(area, gravedad):
35 """Genera el motor de física Box2D. 36 37 :param area: El area de juego. 38 :param gravedad: La gravedad del escenario. 39 """ 40 if __enabled__: 41 if obtener_version().startswith('2.0'): 42 print "Los siento, el soporte para Box2D version 2.0 se ha eliminado." 43 print "Por favor actualice Box2D a la version 2.1 (ver http://www.pilas-engine.com.ar)." 44 return FisicaDeshabilitada(area, gravedad) 45 else: 46 return Fisica(area, gravedad) 47 else: 48 print "No se pudo iniciar Box2D, se deshabilita el soporte de Fisica." 49 return FisicaDeshabilitada(area, gravedad)
50
51 -def obtener_version():
52 """Obtiene la versión de la biblioteca Box2D""" 53 return box2d.__version__
54 55
56 -class Fisica(object):
57 """Representa un simulador de mundo fisico, usando la biblioteca Box2D (version 2.1).""" 58
59 - def __init__(self, area, gravedad):
60 """Inicializa el motor de física. 61 62 :param area: El area del escenario, en forma de tupla. 63 :param gravedad: La aceleración del escenario. 64 """ 65 self.mundo = box2d.b2World(gravedad, False) 66 self.objetosContactListener = ObjetosContactListener() 67 self.mundo.contactListener = self.objetosContactListener 68 self.mundo.continuousPhysics = False 69 70 self.area = area 71 self.figuras_a_eliminar = [] 72 73 self.constante_mouse = None 74 self.crear_bordes_del_escenario() 75 76 self.velocidad = 1.0 77 self.timeStep = self.velocidad/120.0
78
80 """Genera las paredes, el techo y el suelo.""" 81 self.crear_techo(self.area) 82 self.crear_suelo(self.area) 83 self.crear_paredes(self.area)
84
85 - def reiniciar(self):
86 """Elimina todos los objetos físicos y vuelve a crear el entorno.""" 87 lista = list(self.mundo.bodies) 88 89 for x in lista: 90 self.mundo.DestroyBody(x) 91 92 self.crear_bordes_del_escenario()
93
94 - def capturar_figura_con_el_mouse(self, figura):
95 """Comienza a capturar una figura con el mouse. 96 97 :param figura: La figura a controlar con el mouse. 98 """ 99 if self.constante_mouse: 100 self.cuando_suelta_el_mouse() 101 102 self.constante_mouse = ConstanteDeMovimiento(figura)
103
104 - def cuando_mueve_el_mouse(self, x, y):
105 """Gestiona el evento de movimiento del mouse. 106 107 :param x: Coordenada horizontal del mouse. 108 :param y: Coordenada vertical del mouse. 109 """ 110 if self.constante_mouse: 111 self.constante_mouse.mover(x, y)
112
113 - def cuando_suelta_el_mouse(self):
114 """Se ejecuta cuando se suelta el botón de mouse.""" 115 if self.constante_mouse: 116 self.constante_mouse.eliminar() 117 self.constante_mouse = None
118
119 - def actualizar(self, velocidad=1.0):
120 """Realiza la actualización lógica del escenario. 121 """ 122 # TODO: eliminar el arguemnto velocidad que no se utiliza. 123 if self.mundo: 124 self.mundo.Step(self.timeStep, 6, 3) 125 self._procesar_figuras_a_eliminar() 126 self.mundo.ClearForces()
127
128 - def pausar_mundo(self):
129 """Detiene la simulación física.""" 130 if self.mundo: 131 self.timeStep = 0
132
133 - def reanudar_mundo(self):
134 """Restaura la simulación física.""" 135 if self.mundo: 136 self.timeStep = self.velocidad/120.0
137
139 "Elimina las figuras que han sido marcadas para quitar." 140 if self.figuras_a_eliminar: 141 for x in self.figuras_a_eliminar: 142 # Solo elimina las figuras que actualmente existen. 143 if x in self.mundo.bodies: 144 self.mundo.DestroyBody(x) 145 self.figuras_a_eliminar = []
146
147 - def dibujar_figuras_sobre_lienzo(self, motor, lienzo, grosor=1):
148 """Dibuja todas las figuras en una pizarra. Indicado para depuracion. 149 150 :param motor: Referencia al motor de pilas. 151 :param lienzo: Un actor lienzo sobre el que se dibujará. 152 :param grosor: El grosor de la linea medida en pixels. 153 """ 154 155 cuerpos = self.mundo.bodies 156 157 for cuerpo in cuerpos: 158 159 for fixture in cuerpo: 160 161 # cuerpo.type == 0 → estatico 162 # cuerpo.type == 1 → kinematico 163 # cuerpo.type == 2 → dinamico 164 165 shape = fixture.shape 166 167 if isinstance(shape, box2d.b2PolygonShape): 168 vertices = [cuerpo.transform * v * PPM for v in shape.vertices] 169 vertices = [pilas.escena_actual().camara.desplazar(v) for v in vertices] 170 lienzo.poligono(motor, vertices, color=pilas.colores.blanco, grosor=grosor, cerrado=True) 171 elif isinstance(shape, box2d.b2CircleShape): 172 (x, y) = pilas.escena_actual().camara.desplazar(cuerpo.transform * shape.pos * PPM) 173 174 lienzo.circulo(motor, x, y, shape.radius * PPM, pilas.colores.blanco, grosor=grosor) 175 else: 176 # TODO: implementar las figuras de tipo "edge" y "loop". 177 raise Exception("No puedo identificar el tipo de figura.")
178 179
180 - def crear_cuerpo(self, definicion_de_cuerpo):
181 """Genera un Body de box2d. 182 183 :param definicion_de_cuerpo: Los parámetros de configuración de un cuerpo para Box2d. 184 """ 185 return self.mundo.CreateBody(definicion_de_cuerpo)
186
187 - def crear_suelo(self, (ancho, alto), restitucion=0):
188 """Genera un suelo sólido para el escenario. 189 190 :param ancho: El ancho del suelo. 191 :param alto: Alto del suelo. 192 :param restitucion: El grado de conservación de energía ante una colisión. 193 """ 194 self.suelo = Rectangulo(0, -alto/2, ancho, 2, dinamica=False, fisica=self, restitucion=restitucion)
195
196 - def crear_techo(self, (ancho, alto), restitucion=0):
197 """Genera un techo sólido para el escenario. 198 199 :param ancho: El ancho del techo. 200 :param alto: Alto del techo. 201 :param restitucion: El grado de conservación de energía ante una colisión. 202 """ 203 self.techo = Rectangulo(0, alto/2, ancho, 2, dinamica=False, fisica=self, restitucion=restitucion)
204
205 - def crear_paredes(self, (ancho, alto), restitucion=0):
206 """Genera dos paredes para el escenario. 207 208 :param ancho: El ancho de las paredes. 209 :param alto: El alto de las paredes. 210 :param restitucion: El grado de conservación de energía ante una colisión. 211 """ 212 self.pared_izquierda = Rectangulo(-ancho/2, 0, 2, alto, dinamica=False, fisica=self, restitucion=restitucion) 213 self.pared_derecha = Rectangulo(ancho/2, 0, 2, alto, dinamica=False, fisica=self, restitucion=restitucion)
214
215 - def eliminar_suelo(self):
216 "Elimina el suelo del escenario." 217 if self.suelo: 218 self.suelo.eliminar() 219 self.suelo = None
220
221 - def eliminar_techo(self):
222 "Elimina el techo del escenario." 223 if self.techo: 224 self.techo.eliminar() 225 self.techo = None
226
227 - def eliminar_paredes(self):
228 "Elimina las dos paredes del escenario." 229 if self.pared_izquierda: 230 self.pared_derecha.eliminar() 231 self.pared_izquierda.eliminar() 232 self.pared_derecha = None 233 self.pared_izquierda = None
234
235 - def eliminar_figura(self, figura):
236 """Elimina una figura del escenario. 237 238 :param figura: Figura a eliminar. 239 """ 240 self.figuras_a_eliminar.append(figura)
241
242 - def obtener_distancia_al_suelo(self, x, y, dy):
243 """Obtiene la distancia hacia abajo desde el punto (x,y). 244 245 El valor de 'dy' tiene que ser positivo. 246 247 Si la funcion no encuentra obstaculos retornara 248 dy, pero en paso contrario retornara un valor menor 249 a dy. 250 251 :param x: posición horizontal del punto a analizar. 252 :param y: posición vertical del punto a analizar. 253 """ 254 255 if dy < 0: 256 raise Exception("El valor de 'dy' debe ser positivo, ahora vale '%f'." %(dy)) 257 258 delta = 0 259 260 while delta < dy: 261 262 if self.obtener_cuerpos_en(x, y-delta): 263 return delta 264 265 delta += 1 266 267 return delta
268
269 - def obtener_cuerpos_en(self, x, y):
270 """Retorna una lista de cuerpos que se encuentran en la posicion (x, y) o retorna una lista vacia []. 271 272 :param x: posición horizontal del punto a analizar. 273 :param y: posición vertical del punto a analizar. 274 """ 275 276 AABB = box2d.b2AABB() 277 f = 1 278 AABB.lowerBound = (x-f, y-f) 279 AABB.upperBound = (x+f, y+f) 280 281 cuantos, cuerpos = self.mundo.Query(AABB, 2) 282 283 if cuantos == 0: 284 return [] 285 286 lista_de_cuerpos = [] 287 288 for s in cuerpos: 289 cuerpo = s.GetBody() 290 291 if s.TestPoint(cuerpo.GetXForm(), (x, y)): 292 lista_de_cuerpos.append(cuerpo) 293 294 return lista_de_cuerpos
295
296 - def definir_gravedad(self, x, y):
297 """Define la gravedad del motor de física. 298 299 :param x: Aceleración horizontal. 300 :param y: Aceleración vertical. 301 """ 302 pilas.fisica.definir_gravedad(x, y)
303
304 -class FisicaDeshabilitada(object):
305 """Representa a un motor de física que no realiza acciones, y solo se habilita si box2d 306 no funciona en el equipo. 307 """ 308
309 - def __init__(self, area, gravedad=None):
310 pass
311
312 - def actualizar(self):
313 pass
314
315 - def dibujar_figuras_sobre_lienzo(self, motor, lienzo, grosor=1):
316 pass
318 pass
319
320 - def reiniciar(self):
321 pass
322
323 - def capturar_figura_con_el_mouse(self, figura):
324 pass
325
326 - def cuando_mueve_el_mouse(self, x, y):
327 pass
328
329 - def cuando_suelta_el_mouse(self):
330 pass
331
332 - def pausar_mundo(self):
333 pass
334
335 - def reanudar_mundo(self):
336 pass
337
338 - def dibujar_figuras_sobre_lienzo(self, motor, lienzo, grosor=1):
339 pass
340
341 - def crear_cuerpo(self, definicion_de_cuerpo):
342 pass
343
344 - def crear_suelo(self, (ancho, alto), restitucion=0):
345 pass
346
347 - def crear_techo(self, (ancho, alto), restitucion=0):
348 pass
349
350 - def crear_paredes(self, (ancho, alto), restitucion=0):
351 pass
352
353 - def eliminar_suelo(self):
354 pass
355
356 - def eliminar_techo(self):
357 pass
358
359 - def eliminar_paredes(self):
360 pass
361
362 - def obtener_distancia_al_suelo(self, x, y, dy):
363 pass
364
365 - def eliminar_figura(self, figura):
366 pass
367
368 - def obtener_cuerpos_en(self, x, y):
369 pass
370
371 - def definir_gravedad(self, x, y):
372 pass
373 374
375 -class Figura(object):
376 """Representa un figura que simula un cuerpo fisico. 377 378 Esta figura es abstracta, no está pensada para crear 379 objetos a partir de ella. Se usa como base para el resto 380 de las figuras cómo el Circulo o el Rectangulo simplemente.""" 381
382 - def __init__(self):
383 self.id = pilas.utils.obtener_uuid()
384
385 - def obtener_x(self):
386 "Retorna la posición horizontal del cuerpo." 387 return convertir_a_pixels(self._cuerpo.position.x)
388
389 - def definir_x(self, x):
390 """Define la posición horizontal del cuerpo. 391 392 :param x: El valor horizontal a definir. 393 """ 394 self._cuerpo.position.x = convertir_a_metros(x)
395
396 - def obtener_y(self):
397 "Retorna la posición vertical del cuerpo." 398 return convertir_a_pixels(self._cuerpo.position.y)
399
400 - def definir_y(self, y):
401 """Define la posición vertical del cuerpo. 402 403 :param y: El valor vertical a definir. 404 """ 405 self._cuerpo.position.y = convertir_a_metros(y)
406
407 - def definir_posicion(self, x, y):
408 """Define la posición para el cuerpo. 409 410 :param x: Posición horizontal que se asignará al cuerpo. 411 :param y: Posición vertical que se asignará al cuerpo. 412 """ 413 self.definir_x(x) 414 self.definir_y(y)
415
416 - def obtener_rotacion(self):
417 return - math.degrees(self._cuerpo.angle)
418
419 - def definir_rotacion(self, angulo):
420 # TODO: simplificar a la nueva api. 421 self._cuerpo.SetXForm((self.x, self.y), math.radians(-angulo))
422
423 - def impulsar(self, dx, dy):
424 # TODO: convertir los valores dx y dy a metros. 425 self._cuerpo.ApplyLinearImpulse((dx, dy), (0, 0))
426
427 - def obtener_velocidad_lineal(self):
428 # TODO: convertir a pixels 429 velocidad = self._cuerpo.linearVelocity 430 return (velocidad.x, velocidad.y)
431
432 - def detener(self):
433 """Hace que la figura regrese al reposo.""" 434 self.definir_velocidad_lineal(0, 0)
435
436 - def definir_velocidad_lineal(self, dx=None, dy=None):
437 # TODO: convertir a metros 438 anterior_dx, anterior_dy = self.obtener_velocidad_lineal() 439 440 if dx is None: 441 dx = anterior_dx 442 if dy is None: 443 dy = anterior_dy 444 445 b2vec = self._cuerpo.linearVelocity 446 b2vec.x = dx 447 b2vec.y = dy 448 449 # Añadimos eltry, porque aparece el siguiente error: 450 # TypeError: in method 'b2Vec2___call__', argument 2 of type 'int32' 451 try: 452 self._cuerpo.linearVelocity(b2vec) 453 except: 454 pass
455
456 - def empujar(self, dx=None, dy=None):
457 # TODO: convertir a metros??? 458 self.definir_velocidad_lineal(dx, dy)
459
460 - def eliminar(self):
461 """Quita una figura de la simulación.""" 462 pilas.escena_actual().fisica.eliminar_figura(self._cuerpo)
463 464 x = property(obtener_x, definir_x, doc="define la posición horizontal.") 465 y = property(obtener_y, definir_y, doc="define la posición vertical.") 466 rotacion = property(obtener_rotacion, definir_rotacion, doc="define la rotacion.")
467
468 -class Circulo(Figura):
469 """Representa un cuerpo de circulo. 470 471 Generalmente estas figuras se pueden construir independientes de un 472 actor, y luego asociar. 473 474 Por ejemplo, podríamos crear un círculo: 475 476 >>> circulo_dinamico = pilas.fisica.Circulo(10, 200, 50) 477 478 y luego tomar un actor cualquiera, y decirle que se comporte 479 cómo el circulo: 480 481 >>> mono = pilas.actores.Mono() 482 >>> mono.imitar(circulo_dinamico) 483 """ 484
485 - def __init__(self, x, y, radio, dinamica=True, densidad=1.0, 486 restitucion=0.56, friccion=10.5, amortiguacion=0.1, 487 fisica=None, sin_rotacion=False):
488 489 Figura.__init__(self) 490 491 x = convertir_a_metros(x) 492 y = convertir_a_metros(y) 493 radio = convertir_a_metros(radio) 494 495 if not fisica: 496 fisica = pilas.escena_actual().fisica 497 498 if not dinamica: 499 densidad = 0 500 501 fixture = box2d.b2FixtureDef(shape=box2d.b2CircleShape(radius=radio), 502 density=densidad, 503 linearDamping=amortiguacion, 504 friction=friccion, 505 restitution=restitucion) 506 507 # Agregamos un identificador para controlarlo posteriormente en las 508 # colisiones. 509 userData = { 'id' : self.id } 510 fixture.userData = userData 511 512 if dinamica: 513 self._cuerpo = fisica.mundo.CreateDynamicBody(position=(x, y), fixtures=fixture) 514 else: 515 self._cuerpo = fisica.mundo.CreateKinematicBody(position=(x, y), fixtures=fixture) 516 517 self._cuerpo.fixedRotation = sin_rotacion
518
519 -class Rectangulo(Figura):
520 """Representa un rectángulo que puede colisionar con otras figuras. 521 522 Se puede crear un rectángulo independiente y luego asociarlo 523 a un actor de la siguiente forma: 524 525 >>> rect = pilas.fisica.Rectangulo(50, 90, True) 526 >>> actor = pilas.actores.Pingu() 527 >>> actor.imitar(rect) 528 """ 529
530 - def __init__(self, x, y, ancho, alto, dinamica=True, densidad=1.0, 531 restitucion=0.5, friccion=.2, amortiguacion=0.1, 532 fisica=None, sin_rotacion=False):
533 534 Figura.__init__(self) 535 536 x = convertir_a_metros(x) 537 y = convertir_a_metros(y) 538 ancho = convertir_a_metros(ancho) 539 alto = convertir_a_metros(alto) 540 541 if not fisica: 542 fisica = pilas.escena_actual().fisica 543 544 if not dinamica: 545 densidad = 0 546 547 fixture = box2d.b2FixtureDef(shape=box2d.b2PolygonShape(box=(ancho/2, alto/2)), 548 density=densidad, 549 linearDamping=amortiguacion, 550 friction=friccion, 551 restitution=restitucion) 552 553 # Agregamos un identificador para controlarlo posteriormente en las 554 # colisiones. 555 userData = { 'id' : self.id } 556 fixture.userData = userData 557 558 if dinamica: 559 self._cuerpo = fisica.mundo.CreateDynamicBody(position=(x, y), fixtures=fixture) 560 else: 561 self._cuerpo = fisica.mundo.CreateKinematicBody(position=(x, y), fixtures=fixture) 562 563 self._cuerpo.fixedRotation = sin_rotacion
564 565
566 -class Poligono(Figura):
567 """Representa un cuerpo poligonal. 568 569 El poligono necesita al menos tres puntos para dibujarse, y cada 570 uno de los puntos se tienen que ir dando en orden de las agujas 571 del relog. 572 573 Por ejemplo: 574 575 >>> pilas.fisica.Poligono([(100, 2), (-50, 0), (-100, 100.0)]) 576 577 """ 578
579 - def __init__(self, x, y, puntos, dinamica=True, densidad=1.0, 580 restitucion=0.56, friccion=10.5, amortiguacion=0.1, 581 fisica=None, sin_rotacion=False):
582 583 Figura.__init__(self) 584 585 if not fisica: 586 fisica = pilas.escena_actual().fisica 587 588 vertices = [(convertir_a_metros(x1), convertir_a_metros(y1)) for (x1, y1) in puntos] 589 590 fixture = box2d.b2FixtureDef(shape=box2d.b2PolygonShape(vertices=vertices), 591 density=densidad, 592 linearDamping=amortiguacion, 593 friction=friccion, 594 restitution=restitucion) 595 596 userData = { 'id' : self.id } 597 fixture.userData = userData 598 599 if dinamica: 600 self._cuerpo = fisica.mundo.CreateDynamicBody(position=(0, 0), fixtures=fixture) 601 else: 602 self._cuerpo = fisica.mundo.CreateKinematicBody(position=(0, 0), fixtures=fixture) 603 604 self._cuerpo.fixedRotation = sin_rotacion
605 606
607 -class ConstanteDeMovimiento():
608 """Representa una constante de movimiento para el mouse.""" 609
610 - def __init__(self, figura):
611 """Inicializa la constante. 612 613 :param figura: Figura a controlar desde el mouse. 614 """ 615 mundo = pilas.escena_actual().fisica.mundo 616 punto_captura = convertir_a_metros(figura.x), convertir_a_metros(figura.y) 617 self.cuerpo_enlazado = mundo.CreateBody() 618 self.figura_cuerpo = figura 619 self.constante = mundo.CreateMouseJoint(bodyA=self.cuerpo_enlazado, 620 bodyB=figura._cuerpo, 621 target=punto_captura, 622 maxForce=1000.0*figura._cuerpo.mass) 623 624 figura._cuerpo.awake = True
625
626 - def mover(self, x, y):
627 """Realiza un movimiento de la figura. 628 629 :param x: Posición horizontal. 630 :param y: Posición vertical. 631 """ 632 self.constante.target = (convertir_a_metros(x), convertir_a_metros(y))
633
634 - def eliminar(self):
635 # Si se intenta destruir un Joint de un cuerpo que ya no existe, se cierra 636 # la aplicación. 637 #pilas.escena_actual().fisica.mundo.DestroyJoint(self.constante) 638 pilas.escena_actual().fisica.mundo.DestroyBody(self.cuerpo_enlazado)
639
640 -class ConstanteDeDistancia():
641 """Representa una distancia fija entre dos figuras. 642 643 Esta constante es útil para representar ejes o barras 644 que sostienen dos cuerpos. Por ejemplo, un eje entre dos 645 ruedas en un automóvil: 646 647 >>> circulo_1 = pilas.fisica.Circulo(-100, 0, 50) 648 >>> circulo_2 = pilas.fisica.Circulo(100, 50, 50) 649 >>> barra = pilas.fisica.ConstanteDeDistancia(circulo_1, circulo_2) 650 651 La distancia que tiene que respetarse en la misma que tienen 652 las figuras en el momento en que se establece la constante. 653 """ 654
655 - def __init__(self, figura_1, figura_2, fisica=None, con_colision=True):
656 """Inicializa la constante. 657 658 :param figura_1: Una de las figuras a conectar por la constante. 659 :param figura_2: La otra figura a conectar por la constante. 660 :param fisica: Referencia al motor de física. 661 :param con_colision: Indica si se permite colisión entre las dos figuras. 662 """ 663 if not fisica: 664 fisica = pilas.escena_actual().fisica 665 666 if not isinstance(figura_1, Figura) or not isinstance(figura_2, Figura): 667 raise Exception("Las dos figuras tienen que ser objetos de la clase Figura.") 668 669 constante = box2d.b2DistanceJointDef() 670 constante.Initialize(figura_1._cuerpo, figura_2._cuerpo, (0,0), (0,0)) 671 constante.collideConnected = con_colision 672 self.constante = fisica.mundo.CreateJoint(constante)
673
674 - def eliminar(self):
675 pilas.escena_actual().fisica.mundo.DestroyJoint(self.constante_mouse)
676
677 -def definir_gravedad(x, y):
678 """Define la gravedad del motor de física. 679 680 :param x: Aceleración horizontal. 681 :param y: Aceleración vertical. 682 """ 683 pilas.escena_actual().fisica.mundo.gravity = (x, y)
684
685 -class ObjetosContactListener(contact_listener):
686 """Gestiona las colisiones de los objetos para ejecutar funcionés.""" 687
688 - def __init__(self):
689 box2d.b2ContactListener.__init__(self)
690
691 - def BeginContact(self, *args, **kwargs):
692 objeto_colisionado_1 = args[0].fixtureA 693 objeto_colisionado_2 = args[0].fixtureB 694 695 if (not objeto_colisionado_1.userData == None) and (not objeto_colisionado_2.userData == None): 696 pilas.escena_actual().colisiones.verificar_colisiones_fisicas(objeto_colisionado_1.userData['id'], 697 objeto_colisionado_2.userData['id'])
698