En este capítulo vamos el manejo sprites. Los sprites son los elementos fundamentales de un videojuego. Son las imágenes que vemos en pantalla. Son los jugadores, los enemigos, las balas, etc. Vamos a escribir una clase base llamada CSprite de la cual heredarán todos los objetos visibles y móviles del juego. Luego de eso, escribiremos código para manejar los movimientos avanzados de los objetos, que ya nos quedarán disponibles para que cualquier elemento en el juego los use.
¿Qué es un Sprite?
Un sprite es simplemente algo que se mueve por la pantalla. El término sprite viene desde los orígenes de la programación de videojuegos y se usa como sinónimo de una representación en 2D que se muestra en pantalla. Originalmente se le llamaba sprite a una imagen, aunque luego, con el paso del tiempo, se usó el término sprite para denominar a un objeto en el juego. Un sprite entonces es cualquier objeto que se muestra en el juego, como por ejemplo el jugador, las naves espaciales, las balas que éstos disparan, las explosiones, etc.
Figura 8-1: Ejemplo de sprites en un juego.
¿Cuál es la diferencia entre un Sprite y un Game Object?
Esta es una pregunta que frecuentemente discuten los programadores de videojuegos. Hay muchas formas de hacer (programar) las cosas, pero básicamente el código que hace determinadas cosas como por ejemplo dibujar o mover un objeto, en alguna parte del programa tiene que estar. Estas discusiones generalmente giran en torno a cómo organizar el código, y básicamente es un tema de gustos de cada programador. En nuestro caso se hará de manera simple, como se explica a continuación.
El sprite irá en una clase CSprite y está relacionado con cómo se ve el objeto, o sea, su forma. Generalmente esta clase se encarga de la imagen o de la animación, mientras que la clase CGameObject se encarga del movimiento. En general se piensa en CSprite como una imagen (o secuencia de imágenes) y en CGameObject como un elemento abstracto.
En nuestros juegos, si utilizamos este motor que estamos construyendo, todos los elementos que se mueven en el juego van a estar en su propia clase, la cual va a heredar de CSprite (que a su vez hereda de CGameObject). De esta forma, cuando heredamos de CSprite, obtendremos mucho código que ya está escrito en las clases superiores (en las clases base), lo cual hace que sea mucho más sencillo programar los juegos. A continuación definiremos y luego escribiremos la clase CSprite.
¿Qué información tiene un Sprite?
Todo sprite tiene una representación visual (una imagen o una animación) que es lo que se ve en la pantalla. Tiene una posición (un par de coordenadas (x,y) que define dónde está ubicado el sprite) y el tamaño (el ancho y el alto de la imagen que define su tamaño, y ésto es lo que se utiliza para chequear las colisiones).
Los sprites luego tienen varios comportamientos, algunos son genéricos para todos los sprites del juego y otros son específicos para cada objeto. Por ejemplo, los sprites se mueven por el mundo del juego y tienen la capacidad de saber si chocan con otro sprite (una bala, el jugador, etc.). A esto último se le llama manejo de colisiones y se verá más adelante cuando veamos el manejador de sprites. El manejador de sprites se encarga de la creación de sprites, eliminación de sprites y el chequeo de colisiones, entre otras cosas que hace.
Por supuesto, siempre habrá características y comportamientos especiales para cada sprite. Por ejemplo, la nave del jugador es controlada por el teclado, un enemigo se mueve autónomo y puede perseguir al jugador, una bala sigue derecho hasta que choca con algo, etc. Los sprites (los objetos en el juego) son autocontenidos, esto significa que cada uno maneja su propia posición (en la función update()), según las reglas que tiene el objeto en el juego, o dicho de otro modo, según su comportamiento o lógica.
Comenzaremos por hacer un sprite básico que sirva como base para todos y más adelante implementaremos el manejador de sprites.
La clase CSprite
Ahora construiremos la clase CSprite. Una vez que la tengamos funcionando, podremos usarla para cualquier cosa que se mueva en el juego. Veamos el ejemplo que se encuentra en la carpeta: capitulo_08\001_clase_sprite.
Si ejecutamos este ejemplo, veremos que lo que hace es exactamente lo mismo que lo que hace el ejemplo anterior, salvo que se han reorganizado las clases. Ahora las clases CNave y CPlayer heredan de CSprite y a su vez CSprite hereda de CGameObject.
La clase CSprite se encarga de cargar la imagen y de dibujarla en la pantalla. El movimiento general del objeto se encuentra en la clase CGameObject de la cual hereda. La clase CSprite irá en la carpeta api, porque es una clase que utilizaremos en todos nuestros juegos.
El siguiente es el código de la clase CSprite:
# -*- coding: utf-8 -*-
#--------------------------------------------------------------------
# Clase CSprite.
# Simple sprite con una imagen. Hereda de CGameObject.
#
# Autor: Fernando Sansberro - Batovi Games Studio
# Proyecto: Hacete tu Videojuego.
# Licencia: Creative Commons. BY-NC-SA.
#--------------------------------------------------------------------
# Importar Pygame.
import pygame
# Importar la clase CGameObject.
from api.CGameObject import *
class CSprite(CGameObject):
# Constructor:
# Parámetros: Nombre de la imagen a cargar.
def __init__(self, aImgFile):
CGameObject.__init__(self)
# Imagen (superficie).
self.mImg = None
# Cargar la imagen.
self.mImg = pygame.image.load(aImgFile)
self.mImg = self.mImg.convert_alpha()
# Guardar el ancho y el alto.
self.mWidth = self.mImg.get_width()
self.mHeight = self.mImg.get_height()
# Mover el objeto.
def update(self):
# Invocar a update() de la clase base para el movimiento.
CGameObject.update(self)
# Dibuja el objeto en la pantalla.
# Parámetros: La superficie de la pantalla donde dibujar la imagen.
def render(self, aScreen):
aScreen.blit(self.mImg, (self.getX(), self.getY()))
# Obtener el ancho del sprite.
def getWidth(self):
return self.mWidth
# Obtener el alto del sprite.
def getHeight(self):
return self.mHeight
# Liberar lo que haya creado el objeto.
def destroy(self):
CGameObject.destroy(self)
self.mImg = None
Como vemos, CSprite es una clase que en este momento tiene el código de la carga de la imagen y el código para dibujar la imagen en la pantalla que tenían las clases CNave y CPlayer.
Ahora, luego de implementada la clase CSprite, no existirá código duplicado del manejo de la imagen como había antes (en las clases CNave y CPlayer) y cada clase sólamente se encarga de las tareas que le son pertinentes.
La función render(), hace acceso a las variables (x,y) que se encuentran en CGameObject. Estas variables, en CGameObject, se llaman mX y mY respectivamente. Como no es buena práctica de programación el tener que acordarse de cómo se llaman las variables internas de un objeto, hemos escrito dos funciones getter para obtener estos valores: getX() y getY(). En CGameObject se han agregados estas dos funciones:
...
class CGameObject(object):
def __init__(self):
...
# Obtener la coordenada X.
def getX(self):
return self.mX
# Obtener la coordenada Y.
def getY(self):
return self.mY
...
Una vez que tenemos implementada la clase CSprite, heredamos CNave y CPlayer de CSprite. Todos los objetos móviles que hagamos en el juego, heredarán de la clase CSprite. El código de CPlayer, por ejemplo, quedará mucho más simple ahora, dado que el código de manejo de la imagen y dibujar la imagen en pantalla, que compartía con la clase CNave, se ha puesto ahora en la clase CSprite.
La clase CNave, ahora no hace nada específico o nada propio (por ahora). El código de CNave ahora queda así:
# -*- coding: utf-8 -*-
# -------------------------------------------------------------------
# Clase CNave.
# Nave enemiga que se mueve por la pantalla chequeando los bordes.
#
# Autor: Fernando Sansberro - Batovi Games.
# Proyecto: Hacete tu Videojuego.
# Licencia: Creative Commons. BY-NC-SA.
# -------------------------------------------------------------------
# Importar Pygame.
import pygame
# Importar la clase CSprite.
from api.CSprite import *
# La clase CNave hereda de CSprite.
class CNave(CSprite):
# Tipos de naves.
TYPE_PLATINUM = 0
TYPE_GOLD = 1
TYPE_RED = 2
# ----------------------------------------------------------------
# Constructor. Recibe el tipo de nave (TYPE_PLATINUM o TYPE_GOLD).
# ----------------------------------------------------------------
def __init__(self, aType):
# Segun el tipo de la nave, la imagen que se carga.
self.mType = aType
if self.mType == CNave.TYPE_PLATINUM:
imgFile = "assets/images/grey_ufo.png"
elif self.mType == CNave.TYPE_GOLD:
imgFile = "assets/images/yellow_ufo.png"
elif self.mType == CNave.TYPE_RED:
imgFile = "assets/images/red_ufo.png"
# Invocar al constructor de CSprite con la imagen a cargar.
CSprite.__init__(self, imgFile)
# Mover el objeto.
def update(self):
# Invocar update() de CSprite.
CSprite.update(self)
# Dibuja el objeto en la pantalla.
# Parámetros:
# aScreen: La superficie de la pantalla en donde dibujar.
def render(self, aScreen):
# Invocar render() de CSprite.
CSprite.render(self, aScreen)
# Liberar lo que haya creado el objeto.
def destroy(self):
# Invocar destroy() de CSprite.
CSprite.destroy(self)
self.mImg = None
Por su parte, la clase CPlayer, ahora solamente contiene el código que controla el jugador con las teclas. El resto del código que manejaba la imagen y la mostraba en pantalla ya no se encuentra ahí, sino que se ha puesto en la clase base CSprite. Hacemos con esta clase lo mismo que hicimos con la otra. El código de CPlayer queda así:
# -*- coding: utf-8 -*-
#--------------------------------------------------------------------
# Clase CPlayer.
# Nave que controla el jugador.
#
# Autor: Fernando Sansberro - Batovi Games Studio
# Proyecto: Hacete tu Videojuego.
# Licencia: Creative Commons. BY-NC-SA.
#--------------------------------------------------------------------
# Importar Pygame.
import pygame
# Importar la clase CSprite.
from api.CSprite import *
# Importar la clase CKeyboard
from api.CKeyboard import *
# La clase CPlayer hereda de CSprite.
class CPlayer(CSprite):
# Tipos de jugador.
TYPE_PLAYER_1 = 0
TYPE_PLAYER_2 = 1
# ----------------------------------------------------------------
# Constructor. Recibe el tipo de nave (TYPE_PLAYER_1 o TYPE_PLAYER_2).
# ----------------------------------------------------------------
def __init__(self, aType):
# Segun el tipo de la nave, la imagen que se carga.
self.mType = aType
if self.mType == CPlayer.TYPE_PLAYER_1:
imgFile = "assets/images/player00.png"
# Invocar al constructor de CSprite con la imagen a cargar.
CSprite.__init__(self, imgFile)
# Mover el objeto.
def update(self):
# Mover el jugador con el teclado.
if not CKeyboard.inst().leftPressed() and not CKeyboard.inst().rightPressed():
self.setVelX(0)
else:
if CKeyboard.inst().leftPressed():
self.setVelX(-4)
elif CKeyboard.inst().rightPressed():
self.setVelX(4)
# Invocar update() de CSprite.
CSprite.update(self)
# Dibuja el objeto en la pantalla.
# Parámetros:
# aScreen: La superficie de la pantalla en donde dibujar.
def render(self, aScreen):
# Invocar render() de CSprite.
CSprite.render(self, aScreen)
# Liberar lo que haya creado el objeto.
def destroy(self):
# Invocar destroy() de CSprite.
CSprite.destroy(self)
self.mImg = None
Como vemos, si comparamos las clases con como estaban antes, ahora están más simples porque el código que tenían para cargar la imagen y dibujarla se ha sacado de estas clases y se ha puesto en la clase base CSprite. Además, también hemos eliminado el código duplicado.
Chequeo de los Bordes de la Pantalla
Cuando un sprite se pasa de los bordes de la pantalla (o en general, cuando se pasa de los bordes del mundo), debemos determinar qué hacer. Los comportamientos pueden ser muchos, y depende del juego que estemos haciendo. Por ejemplo, podemos hacer que una nave aparezca por el otro lado, que rebote, que se quede detenida, etc.
Vamos a implementar los comportamientos básicos que se utilizan en la mayoría de los juegos. Como este comportamiento es general, lo escribiremos en la clase CGameObject, y todos los objetos del juego tendrán este comportamiento disponible (porque lo heredarán, recordemos que cualquier objeto del juego será una clase heredada de CGameObject). A cada comportamiento le daremos un nombre (definido por una constante). Los comportamientos básicos que implementaremos serán los siguientes:
NONE: Cuando el sprite llega al borde, le permitimos al sprite seguir su camino. De esta forma el sprite se puede ir de la pantalla. Este va a ser el comportamiento por defecto (si no le indicamos otro comportamiento al sprite, éste es el comportamiento que tendrá el sprite).
STOP: Si el sprite alcanza un borde, no se lo deja seguir y quedará detenido (queda como pegado al borde, y no puede pasar).
WRAP: Si el sprite alcanza el borde, aparecerá desde el otro lado como hacen los asteroides en el juego Asteroids. Los sprites entonces aparecen por el lado contrario al borde por el cual salieron.
BOUNCE: Cuando el sprite toca el borde, rebota, saliendo en sentido contrario, como por ejemplo una pelota que rebota por las paredes.
DIE: El objeto se muere al tocar el borde. Esto ocurre típicamente con las balas por ejemplo, que cuando tocan un borde se destruyen.
Figura 8-2: Diagrama con los comportamientos de bordes.
.
Es importante recordar que los objetos del juego (la clase CGameObject) tienen una función setBounds() que define los límites de movimiento. Esto lo hemos hecho desde que hicimos nuestro primer cuadrado y usamos estos valores cuando controlamos los bordes.
Estos límites pueden ser la pantalla o un área mayor que la pantalla (por ejemplo un mundo más grande que una pantalla, donde la pantalla es la cámara). Los comportamientos de borde se dan cuando el objeto se encuentra con uno de estos bordes que han sido definidos con la función setBounds(). Usamos estos valores y no el tamaño de la pantalla, así más adelante podemos tener objetos que se vayan de la pantalla y tengan sus bordes en el mundo, o sea, en un área mayor que la pantalla.
Ahora vamos a agregar estos comportamientos a los sprites. Colocaremos en el juego cinco naves espaciales y cada una tendrá uno de estos comportamientos cuando toque el borde.
Al programa que ahora tiene tres naves, le colocamos dos naves más, creando las constantes de tipo de nave y las variables necesarias para eso e inicializando correctamente las naves.
Veamos el ejemplo que se encuentra en la carpeta: capitulo_08\002_comportamiento_de_borde.
En este ejemplo tenemos cinco naves, las tres que teníamos más dos que hemos agregado ahora. Una nave celeste (cyan) y la otra verde (green). Los comportamientos que tienen las naves cuando llegan a los bordes son:
Nave plateada (CNave.TYPE_PLATINUM): Al llegar al borde continúa su movimiento normalmente (CGameObject.NONE).
Nave dorada (CNave.TYPE_GOLD): Se detiene al llegar al borde (CGameObject.STOP).
Nave celeste (CNave.TYPE_RED). Al llegar al borde aparece por el lado contrario (CGameObject.WRAP).
Nave verde (CNave.TYPE_GREEN). Al llegar al borde rebota (CGameObject.BOUNCE).
Nave roja (CNave.TYPE_CYAN): Se elimina al llegar al borde (CGameObject.DIE).
Nota: En este ejemplo se han removido los cuadrados porque ya no los vamos a necesitar.
En el programa principal main.py, agregamos las líneas correspondientes a las dos naves que agregamos y en la función init(), cuando inicializamos las naves, usamos la función setBoundAction() para definir en cada nave un comportamiento diferente cuando alcance el borde:
# -*- coding: utf-8 -*-
#--------------------------------------------------------------------
# Ejemplo de los comportamientos de borde de los sprites.
#
# Autor: Fernando Sansberro - Batovi Games Studio
# Proyecto: Hacete tu Videojuego.
# Licencia: Creative Commons. BY-NC-SA.
#--------------------------------------------------------------------
# Importar Pygame.
import pygame
# Importar la clase CNave.
from game.CNave import *
# Importar la clase Cuadrado.
from game.CCuadrado import *
# Importar la clase CPlayer.
from game.CPlayer import *
# Importar la clase CKeyboard
from api.CKeyboard import *
# Definir ancho y alto de la pantalla.
SCREEN_WIDTH = 640
SCREEN_HEIGHT = 360
RESOLUTION = (SCREEN_WIDTH, SCREEN_HEIGHT)
# Control del modo ventana o fullscreen.
isFullscreen = False
screen = None
imgBackground = None
imgSpace = None
clock = None
n1 = None
n2 = None
n3 = None
n4 = None
n5 = None
player = None
# Inicializar la variable de control del game loop.
salir = False
# Función de inicialización.
def init():
global screen
global imgBackground
global imgSpace
global n1
global n2
global n3
global n4
global n5
global clock
global player
# Inicializar Pygame.
pygame.init()
# Poner el modo de video en ventana e indicar la resolución.
screen = pygame.display.set_mode(RESOLUTION)
# Poner el título de la ventana.
pygame.display.set_caption("Mi Juego")
# Crear la superficie del fondo o background.
imgBackground = pygame.Surface(screen.get_size())
imgBackground = imgBackground.convert()
# Cargar la imagen del fondo. La imagen es de 640 x 360.
imgSpace = pygame.image.load("assets/images/space_640x360.jpg")
imgSpace = imgSpace.convert()
# Dibujar la imagen cargada en la imagen de background.
imgBackground.blit(imgSpace, (0, 0))
# Crear las naves: se le pasa como parámetro la imagen de la nave.
n1 = CNave(CNave.TYPE_PLATINUM)
n2 = CNave(CNave.TYPE_GOLD)
n3 = CNave(CNave.TYPE_RED)
n4 = CNave(CNave.TYPE_GREEN)
n5 = CNave(CNave.TYPE_CYAN)
# Colocar las naves en su posición inicial.
n1.setXY(0, 100)
n2.setXY(0, 150)
n3.setXY(0, 200)
n4.setXY(0, 250)
n5.setXY(0, 300)
# Las naves comienzan detenidas.
n1.setVelX(2)
n1.setVelY(0)
n2.setVelX(2)
n2.setVelY(0)
n3.setVelX(2)
n3.setVelY(0)
n4.setVelX(2)
n4.setVelY(2)
n5.setVelX(2)
n5.setVelY(0)
# Acelerar las naves.
n1.setAccelX(0)
n1.setAccelY(0)
n2.setAccelX(0)
n2.setAccelY(0)
n3.setAccelX(0)
n3.setAccelY(0)
n4.setAccelX(0)
n4.setAccelY(0)
n5.setAccelX(0)
n5.setAccelY(0)
# Marcar los límites del mundo.
n1.setBounds(-n1.getWidth(), -n1.getHeight(), SCREEN_WIDTH, SCREEN_HEIGHT)
n2.setBounds(-n2.getWidth(), -n2.getHeight(), SCREEN_WIDTH, SCREEN_HEIGHT)
n3.setBounds(-n3.getWidth(), -n3.getHeight(), SCREEN_WIDTH, SCREEN_HEIGHT)
n4.setBounds(-n3.getWidth(), -n3.getHeight(), SCREEN_WIDTH, SCREEN_HEIGHT)
n5.setBounds(-n3.getWidth(), -n3.getHeight(), SCREEN_WIDTH, SCREEN_HEIGHT)
# Poner los comportamientos con los bordes.
n1.setBoundAction(CGameObject.NONE)
n2.setBoundAction(CGameObject.STOP)
n3.setBoundAction(CGameObject.WRAP)
n4.setBoundAction(CGameObject.BOUNCE)
n5.setBoundAction(CGameObject.DIE)
# Crear el jugador.
player = CPlayer(CPlayer.TYPE_PLAYER_1)
player.setXY(SCREEN_WIDTH / 2 - player.getWidth() / 2, SCREEN_HEIGHT - player.getHeight())
# Establecer los límites de la pantalla.
player.setBounds(0, 0, SCREEN_WIDTH - player.getWidth(), SCREEN_HEIGHT)
# Inicializar el reloj.
clock = pygame.time.Clock()
# Correr la lógica del juego.
def update():
global salir
global screen
global isFullscreen
# Timer que controla el frame rate.
clock.tick(60)
# Procesar los eventos que llegan a la aplicación.
for event in pygame.event.get():
# Si se cierra la ventana se sale del programa.
if event.type == pygame.QUIT:
salir = True
# Si se pulsa la tecla [Esc] se sale del programa.
if event.type == pygame.KEYUP:
if event.key == pygame.K_ESCAPE:
salir = True
# Si se pulsa la tecla [F], se cambia entre ventana y fullscreen.
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_f:
isFullscreen = not isFullscreen
if isFullscreen:
screen = pygame.display.set_mode(RESOLUTION, pygame.FULLSCREEN)
else:
screen = pygame.display.set_mode(RESOLUTION)
# Registrar cuando se apreta o suelta una tecla.
if event.type == pygame.KEYDOWN:
CKeyboard.inst().keyDown(event.key)
if event.type == pygame.KEYUP:
CKeyboard.inst().keyUp(event.key)
# Lógica de las naves.
n1.update()
n2.update()
n3.update()
n4.update()
n5.update()
# Lógica del jugador.
player.update()
# Dibujar el frame y actualizar la pantalla.
def render():
# Dibujar el fondo.
screen.blit(imgBackground, (0, 0))
# Dibujar las naves en la nueva posición.
n1.render(screen)
n2.render(screen)
n3.render(screen)
n4.render(screen)
n5.render(screen)
# Dibujar el jugador.
player.render(screen)
# Actualizar la pantalla.
pygame.display.flip()
# Función de destrucción.
def destroy():
global n1
global n2
global n3
global n4
global n5
global player
# Destruir los objetos.
n1.destroy()
n1 = None
n2.destroy()
n2 = None
n3.destroy()
n3 = None
n4.destroy()
n4 = None
n5.destroy()
n5 = None
# Destruir la nave del jugador.
player.destroy()
player = None
# Cerrar Pygame y liberar los recursos que pidió el programa.
pygame.quit()
# ============= Punto de entrada del programa. =============
# Inicializar los elementos necesarios del juego.
init()
# Loop principal del juego.
while not salir:
# Actualizar los objetos.
update()
# Dibujar la pantalla.
render()
# Liberar los recursos al final.
destroy()
En la clase CNave, agregamos la parte que corresponde a tener dos tipos nuevos de nave. Para eso ponemos las imágenes correspondientes a las nuevas naves (la verde y la celeste) en la carpeta de imágenes, y definimos las constantes para las nuevas naves. Copia las imágenes del programa de ejemplo a tu propio ejemplo.
# -*- coding: utf-8 -*-
# -------------------------------------------------------------------
# Clase CNave.
# Nave enemiga que se mueve por la pantalla chequeando los bordes.
#
# Autor: Fernando Sansberro - Batovi Games.
# Proyecto: Hacete tu Videojuego.
# Licencia: Creative Commons. BY-NC-SA.
# -------------------------------------------------------------------
# Importar Pygame.
import pygame
# Importar la clase CSprite.
from api.CSprite import *
# La clase CNave hereda de CSprite.
class CNave(CSprite):
# Tipos de naves.
TYPE_PLATINUM = 0
TYPE_GOLD = 1
TYPE_RED = 2
TYPE_GREEN = 3
TYPE_CYAN = 4
# ----------------------------------------------------------------
# Constructor. Recibe el tipo de nave (TYPE_PLATINUM o TYPE_GOLD).
# ----------------------------------------------------------------
def __init__(self, aType):
# Invocar al constructor de la clase base.
CGameObject.__init__(self)
# Segun el tipo de la nave, la imagen que se carga.
self.mType = aType
if self.mType == CNave.TYPE_PLATINUM:
imgFile = "assets/images/grey_ufo.png"
elif self.mType == CNave.TYPE_GOLD:
imgFile = "assets/images/yellow_ufo.png"
elif self.mType == CNave.TYPE_RED:
imgFile = "assets/images/red_ufo.png"
elif self.mType == CNave.TYPE_GREEN:
imgFile = "assets/images/green_ufo.png"
elif self.mType == CNave.TYPE_CYAN:
imgFile = "assets/images/cyan_ufo.png"
# Invocar al constructor de CSprite con la imagen a cargar.
CSprite.__init__(self, imgFile)
# Mover el objeto.
def update(self):
# Invocar update() de CSprite.
CSprite.update(self)
# Dibuja el objeto en la pantalla.
# Parámetros:
# aScreen: La superficie de la pantalla en donde dibujar.
def render(self, aScreen):
# Invocar render() de CSprite.
CSprite.render(self, aScreen)
# Liberar lo que haya creado el objeto.
def destroy(self):
# Invocar destroy() de CSprite.
CSprite.destroy(self)
self.mImg = None
En la clase CGameObject, es donde implementamos el comportamiento que tendrá el objeto cuando alcanza uno de los bordes definidos para la pantalla o el mundo. Recordemos que los límites del mundo por el cual se mueve un objeto lo establecemos con la función setBounds().
La clase CGameObject, queda de esta manera:
# -*- coding: utf-8 -*-
#--------------------------------------------------------------------
# Clase CGameObject.
# Clase base de todos los objetos que se mueven en el juego.
#
# Autor: Fernando Sansberro - Batovi Games Studio
# Proyecto: Hacete tu Videojuego.
# Licencia: Creative Commons. BY-NC-SA.
#--------------------------------------------------------------------
class CGameObject(object):
# Comportamientos del objeto al llegar a un borde.
NONE = 0 # No tiene ninguno, el objeto sigue de largo.
STOP = 1 # El objeto se detiene al alcanzar un borde.
WRAP = 2 # El objeto aparece por el lado contrario.
BOUNCE = 3 # El objeto rebota en el borde.
DIE = 4 # El objeto se marca para ser eliminado.
def __init__(self):
# Coordenadas del objeto.
self.mX = 0
self.mY = 0
# Velocidad.
self.mVelX = 0
self.mVelY = 0
# Aceleración.
self.mAccelX = 0
self.mAccelY = 0
# Variables para controlar los bordes.
self.mMinX = -float("inf")
self.mMaxX = float("inf")
self.mMinY = -float("inf")
self.mMaxY = float("inf")
# Comportamiento de borde del objeto. Ponemos que no tenga ninguno
# por defecto, y el objeto seguirá de largo en los bordes.
self.mBoundAction = CGameObject.NONE
# Obtener la coordenada X.
def getX(self):
return self.mX
# Obtener la coordenada Y.
def getY(self):
return self.mY
# Establece la posición del objeto.
# Parámetros:
# aX, aY: Coordenadas x e y del objeto.
def setXY(self, aX, aY):
self.mX = aX
self.mY = aY
# Establece la velocidad X del objeto.
def setVelX(self, aVelX):
self.mVelX = aVelX
# Establece la velocidad Y del objeto.
def setVelY(self, aVelY):
self.mVelY = aVelY
# Establece la aceleración x del objeto.
def setAccelX(self, aAccelX):
self.mAccelX = aAccelX
# Establece la aceleración y del objeto.
def setAccelY(self, aAccelY):
self.mAccelY = aAccelY
# Define los límites del movimiento del objeto.
# Parámetros:
# aMinX, aMinY: Coordenadas x e y mínimas del mundo.
# aMaxX, aMaxY: Coordenadas x e y máximas del mundo.
def setBounds(self, aMinX, aMinY, aMaxX, aMaxY):
self.mMinX = aMinX
self.mMaxX = aMaxX
self.mMinY = aMinY
self.mMaxY = aMaxY
# Define el comportamiento al alcanzar los bordes del mundo.
def setBoundAction(self, aBoundAction):
self.mBoundAction = aBoundAction
# Update mueve el objecto según su velocidad.
def update(self):
# Modificar la velocidad según la aceleración.
self.mVelX += self.mAccelX
self.mVelY += self.mAccelY
# Mover el objeto.
self.mX += self.mVelX
self.mY += self.mVelY
# Comportamiento con el borde.
self.checkBounds()
# Realiza el chequeo con los bordes y aplica el comportamiento que
# corresponda si el objeto toca alguno de los bordes.
# Esta función es invocada en update().
# La variable self.boundAction contiene el comportamiento:
# NONE: No tiene ninguno, el objeto sigue de largo.
# STOP: El objeto se detiene al alcanzar un borde.
# WRAP: El objeto aparece por el lado contrario.
# BOUNCE: El objeto rebota en el borde.
# DIE: El objeto se marca para ser eliminado.
def checkBounds(self):
# Si el comportamiento es NONE no se chequea el borde.
if self.mBoundAction == CGameObject.NONE:
return
# Saber qué bordes está tocando el objeto.
left = (self.mX < self.mMinX)
right = (self.mX > self.mMaxX)
up = (self.mY < self.mMinY)
down = (self.mY > self.mMaxY)
# Si no toca ningún borde no hacemos nada.
if not (left or right or up or down):
return
# Al llegar a este punto, el objeto está tocando un borde.
# Hay que corregir la posición del objeto y luego modificar
# su velocidad según el comportamiento que tenga.
# Corregir la posición del objeto.
# Si es WRAP, el objeto aparece desde el lado contrario.
if (self.mBoundAction == CGameObject.WRAP):
if (left):
self.mX = self.mMaxX
if (right):
self.mX = self.mMinX
if (up):
self.mY = self.mMaxY
if (down):
self.mY = self.mMinY
# Si es STOP, BOUNCE o DIE corregimos la posición porque sino el
# objeto queda con parte fuera de los límites.
else:
if (left):
self.mX = self.mMinX
if (right):
self.mX = self.mMaxX
if (up):
self.mY = self.mMinY
if (down):
self.mY = self.mMaxY
# Si el comportamiento es STOP o DIE, el objeto se detiene.
if (self.mBoundAction == CGameObject.STOP or self.mBoundAction == CGameObject.DIE):
self.mVelX = 0
self.mVelY = 0
self.mAccelX = 0
self.mAccelY = 0
elif (self.mBoundAction == CGameObject.BOUNCE):
if (right or left):
self.mVelX *= -1
if (up or down):
self.mVelY *= -1
# Si el comportamiento es que muera, no se hace más nada.
# Esto se va a implementar más adelante.
if (self.mBoundAction == CGameObject.DIE):
print("Objeto debe morir")
return
# Liberar lo que haya creado el objeto.
def destroy(self):
pass
Como vemos en CGameObject, en la función update(), luego de mover el objeto, chequeamos si el mismo ha salido de los bordes definidos como los límites del mundo por el cual se puede mover. Si esto es así, se corrigen sus coordenadas según el comportamiento de borde que tenga definido el objeto.
Intenta seguir la lógica en la función checkBounds(), leyendo los comentarios, hasta entender completamente el funcionamiento de este código. Una vez programados los comportamientos de borde en CGameObject, cualquier objeto del juego, al heredar de esta clase, tendrá disponible esta funcionalidad.
Manejo de Colisiones con los Bordes
El ejemplo anterior funciona bien, pero con una pequeña falla. Las tres primeras naves se van de la pantalla y no se ven más. El comportamiento de la nave dorada es CGameObject.STOP, y esta nave se debería ver en pantalla cuando se detiene. No se ve, porque queda fuera de la pantalla, cosa que corregiremos a continuación.
Miremos la siguiente imagen, que muestre el momento en el que la nave dorada (que tiene establecido el comportamiento CGameObject.STOP) se pasa de los límites:
Figura 8-3: La nave con comportamiento STOP se detiene cuando se va de la pantalla.
Como vemos en el ejemplo anterior, la nave dorada desaparece de la pantalla y se detiene, cuando lo que queremos es que se detenga al llegar al borde (no al irse completamente y ya no verse más). Esto se debe a que o bien estamos chequeando incorrectamente los bordes, o bien no estamos tomando en cuenta el ancho y el alto de la nave.
La posición (x,y) de un sprite, está relacionada con cómo se dibuja el sprite. Por ahora, la posición (x,y) de un sprite es la posición de la coordenada superior izquierda del sprite, porque es en ese punto donde se dibuja la imagen. A ese punto de referencia, se le llama pivot, o punto de registro. Mira la siguiente figura.
Figura 8-4: La posición (x,y) del sprite es la esquina superior izquierda del dibujo.
Entonces, para saber si los bordes de la nave se pasan de los límites del mundo, debemos tener en cuenta el ancho y el alto de la imagen para establecer los valores de bordes.
Veamos el ejemplo que se encuentra en la carpeta capitulo_08\003_comportamiento_de_borde_corregido.
Al ejecutar el ejemplo, vemos que la nave verde (que tiene comportamiento CGameObject.BOUNCE) rebota sin salirse de los bordes, y la nave roja (que tiene comportamiento CGameObject.WRAP) se va completamente de la pantalla antes de salir por el lado contrario. La clave para esto (y de hecho la única diferencia entre este ejemplo y el anterior), son los valores que hemos puesto en los límites de borde en la función init() en main.py:
# Función de inicialización.
def init():
...
# Marcar los límites del mundo.
n1.setBounds(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)
n2.setBounds(0, 0, SCREEN_WIDTH - n2.getWidth(), SCREEN_HEIGHT - n2.getHeight())
n3.setBounds(-n3.getWidth(), -n3.getHeight(), SCREEN_WIDTH, SCREEN_HEIGHT)
n4.setBounds(0, 0, SCREEN_WIDTH - n4.getWidth(), SCREEN_HEIGHT - n4.getHeight())
n5.setBounds(0, 0, SCREEN_WIDTH - n5.getWidth(), SCREEN_HEIGHT - n5.getHeight())
Cuando un objeto choca con una pared (como es el caso al chocar con el borde de la pantalla o del mundo), debemos asegurarnos de realizar dos cosas:
1o). Corregir la posición del objeto (para que el objeto no quede colisionando).
2o). Cambiar su velocidad si corresponde, según cómo se comporte con el borde (si rebota o no).
Observa la siguiente figura que muestra estos pasos a realizar cuando la nave se va del rango máximo (el límite derecho) y rebota contra el borde derecho:
Figura 8-5: Rebote en el borde derecho de la pantalla.
Se invierte la velocidad horizontalmente.
Si observamos en el código de CGameObject, en la función checkBounds(), podemos ver cómo corregimos la posición del objeto cuando éste se va de los límites. Colocamos el objeto en el lado contrario si el comportamiento es CGameObject.WRAP, y colocamos el objeto contra el borde, e invertimos su velocidad si el comportamiento es CGameObject.BOUNCE.
Como hemos dicho, al estar estos comportamientos programados en la clase CGameObject, se encontrarán disponibles para todos los objetos que se muevan en el juego (los cuales heredan de CGameObject). De esta forma, la primera vez cuesta un poco implementar algo del estilo de los los rebotes, pero luego acelera muchísimo el desarrollo de videojuegos, al tener clases con funcionalidades que podemos reutilizar en nuestros próximos videojuegos.
Con esto hemos finalizado el manejo básico de los sprites del juego. ¡En el siguiente capítulo veremos el manejador de sprites y cómo disparar!
Comments