5. Estructura de un Videojuego
- Fernando Sansberro
- 28 oct 2021
- 16 Min. de lectura
Actualizado: 16 ene 2022
En este capítulo veremos cómo es la estructura de un videojuego a nivel de programación, e iremos armando la estructura del programa del juego en varios pasos. Haremos una versión del juego Space Invaders, en el cual manejamos una nave que dispara a los invasores extraterrestres. Durante este libro, haremos una versión de este juego clásico, que es un buen caso de un juego sencillo, para aprender a programar.

Figura 5-1: Space Invaders (c) 1978 - Taito.
Primero comenzaremos viendo el ciclo del programa, el cual se compone de las funciones init(), update() - render() y destroy(). Luego veremos el Game Object, que será el objeto base de todos los objetos que se mueven en el juego. Por último veremos las carpetas que usaremos en el proyecto y la división en carpetas api / game, y finalmente veremos cómo deben ser los objetos para que su comportamiento quede encapsulado, o contenido en sí mismo.
Para cuando terminemos este capítulo, tendremos visualmente en la pantalla el mismo ejemplo que teníamos en el capítulo anterior, pero con la estructura de base armada para poder hacer videojuegos de calidad.
El Ciclo init() / update() - render() / destroy()
Como vimos en el capítulo 1, el programa se divide en básicamente cuatro funciones:
init(): Inicializa el sistema, los objetos del juego y carga los assets necesarios.
update(): Obtiene la entrada del jugador, actualiza todos los objetos del juego corriendo la lógica de cada uno y chequea las colisiones.
render(): Dibuja los objetos del juego y actualiza la pantalla.
destroy(): Libera toda la memoria utilizada por los objetos.
La estructura de un videojuego, ya se puede ver en los ejemplos que hemos realizado hasta ahora, y es la siguiente:
init()
while not salir:
update()
render()
destroy()
El programa comienza con su inicialización en la función init(), luego entra en el game loop, el cual se compone de las funciones update() y render(), y el programa permanece en ese loop hasta que el usuario decida salir del juego. En ese momento se sale del loop y finalmente se invoca a la función destroy() para liberar la memoria utilizada.
Veamos el ejemplo de la carpeta capitulo_05\001_init_update_render_destroy. Al ejecutarlo podemos ver que hace lo mismo que el ejemplo del capítulo anterior. La diferencia es que el código se encuentra organizado en las funciones init(), update(), render() y destroy(). A continuación se muestra completo el programa principal (main.py):
# -*- coding: utf-8 -*-
#------------------------------------------------------------------------
# Naves enemigas moviéndose por la pantalla.
# Estructura init(), update() / render(), destroy().
#
# 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 CNave 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
# Ancho y alto de la imagen de las naves.
NAVE_WIDTH = 60
NAVE_HEIGHT = 27
# 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 clock
# 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("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("grey_ufo.png")
n2 = CNave("yellow_ufo.png")
# Colocar las naves en su posición inicial.
n1.setXY(0, 180)
n2.setXY(SCREEN_WIDTH - NAVE_WIDTH, 212)
# Marcar los límites del mundo.
n1.setBounds(-NAVE_WIDTH, -NAVE_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT)
n2.setBounds(-NAVE_WIDTH, -NAVE_HEIGHT, SCREEN_WIDTH, 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(30)
# 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)
# Mover las naves y controlar los bordes.
n1.update(2, 1)
n2.update(-2, -1)
# 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)
# Actualizar la pantalla.
pygame.display.flip()
# Función de destrucción.
def destroy():
global n1
global n2
# Destruir los objetos.
n1.destroy()
n1 = None
n2.destroy()
n2 = 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()
Si miramos el bloque principal del programa, el cual se encuentra al final, vemos muy claramente que se invoca la función init() al principio, luego se entra al game loop invocando en cada frame a las funciones update() y render(), y al salir del game loop se llama a la función destroy(). El programa es el mismo que antes, pero lo hemos reestructurado en estas funciones.
Nota: Observa en el programa principal que hemos puesto las variables al comienzo, inicializándolas con None cuando se tratan de objetos.
Dentro de las funciones necesitamos declarar las variables con la palabra global para referirnos a la variable global (la variable definida al inicio del programa). Si no lo hiciéramos así, no podríamos acceder a la variable global desde dentro de una función y el programa no funcionaría.
Destrucción de Objetos
Para destruir un objeto en Python, debemos poner su referencia en None, que significa “nada”, o que ya no apunta a ningún objeto. Cuando hay un objeto en memoria que no tiene ninguna variable apuntándole (se dice que no tiene referencias), el mismo es borrado definitivamente. Los lenguajes como Python implementan un sistema denominado Garbage Collector que realiza esta tarea de eliminar objetos que ya no son usados.
En general, tendremos una función destroy() en cada uno de nuestros objetos en el juego que se encargará de liberar toda la memoria que se haya utilizado en el objeto. Por ejemplo, como vemos en la función destroy() del programa principal, se llama a la función destroy() de las naves antes de eliminar el objeto.
En el caso de la nave (en la clase CNave.py), la función destroy() debe liberar la memoria pedida para la imagen que ha sido cargada. La función queda así:
# Liberar lo que haya creado el objeto.
def destroy(self):
self.mImg = None
Nota: En general es necesario eliminar un objeto poniendo su referencia en None, cuando se trata de un objeto creado por nosotros, una imagen cargada en memoria, un array, etc.
Estructura de los Objetos del Juego
De la misma forma que el programa principal tiene una inicialización (la función init()), un game loop compuesto por la actualización y el dibujado (las funciones update() y render()) y un destructor (la función destroy()), todos los objetos que hagamos en el juego también tendrán esta cuatro funciones. Repasemos las funciones:
init() o el constructor: Inicializa el objeto, carga las imágenes necesarias, etc.
update(): Es invocada desde el game loop. Se encarga de mover el objeto y actualizar la animación de acuerdo a la lógica del objeto (lo que hace el objeto). Corre la lógica.
render(): Es invocada desde el game loop. Dibuja el objeto en su nueva posición en la pantalla.
destroy(): Destruye la memoria utilizada por el objeto. Se invoca al final de todo cuando se va a destruir el objeto.
Parece que toda esta reestructura hace al código más complicado, pero como vemos en el programa, el game loop queda muchísimo más chico y más claro. Tener esta estructura en el game loop y en los objetos con las funciones init() / update() - render() / destroy() nos permitirá una flexibilidad que necesitaremos más adelante para hacer juegos grandes.
El Game Object
Siguiendo con la estructura del código, lo que vamos a hacer ahora es tener un objeto base que será común a todos los objetos que se mueven en el juego. De esta forma, toda la programación del comportamiento base del objeto estará centralizada en un solo lugar, y todos los objetos del juego heredarán de él ese comportamiento. A este objeto base de todos los objetos que se mueven en el juego lo llamaremos game object.
En otras palabras, el game object es donde pondremos por ejemplo, el código de movimiento que ahora tenemos escrito en la clase de la nave. Imagina si ponemos en el juego otro objeto, aparte de la nave, y queremos que controle los bordes de la pantalla al igual que lo hace la nave. ¡Deberíamos duplicar el código de control de bordes en ambas clases! Eso sería una muy mala práctica de programación y no lo haremos.
Lo que haremos es tener un objeto de base llamado game object con el comportamiento de movimiento teniendo en cuenta los bordes de la pantalla y cuando creamos los otros objetos del juego, heredamos de ese game object y por lo tanto heredarán ese comportamiento. De esta forma, escribimos en el objeto base el comportamiento que reutilizaremos (heredamos) en los objetos que heredan.
Nota: La herencia es una de las principales propiedades de la Programación Orientada a Objetos. Cuando un objeto hereda de otro, automáticamente adquiere (hereda) sus variables y sus métodos, o sea, sus propiedades y su comportamiento.
Vamos a hacer una clase CGameObject básica, a la cual también se le denomina una entidad (o game entity). Este objeto no tendrá forma ninguna (por ejemplo, no tiene una imagen). Simplemente es un objeto abstracto que tendrá las cosas que comparten todos los objetos móviles del juego (por ejemplo: posición, velocidad, aceleración, etc). Todos los objetos móviles del juego heredarán de CGameObject y por lo tanto, adquirirán estas propiedades y sus métodos.
Por ahora, sólo haremos el objeto muy básico, y en los capítulos venideros le iremos agregando cosas, por ejemplo cuando hagamos movimientos con aceleración y que queremos que todos los objetos móviles lo tengan disponible por si lo queremos usar.
En el ejemplo que se encuentra en la carpeta capitulo_05\002_game_object, podemos ver los cambios en las clases CNave y CGameObject. En la clase CNave tendremos el código relacionado a la carga de la imagen de la nave y en la clase CGameObject tendremos el código del movimiento chequeando los bordes, comportamiento que heredarán todos los objetos del juego.
Cuando tenemos varias clases, unas heredando de otras, tenemos un diseño orientado a objetos. La relación de padres e hijos entre las clases se denomina jerarquía de clases, y se dibuja utilizando un diagrama de clases. El diagrama de clases del ejemplo se muestra en la figura 5-1.

Figura 5-2: Diagrama de clases con CGameObject y CNave.
En este diagrama de clases, se ve que la clase CNave hereda de CGameObject. Esto significa que todos los atributos de CGameObject (propiedades o variables) y su comportamiento (funciones o métodos) serán heredados en la clase CNave.
Nota: Cuanto más arriba en la estructura de clases, más genérica es la clase. Cuanto más abajo en la estructura, más específica es la clase.
Nota que la clase CGameObject no tiene función render(). Esto es porque CGameObject es una clase que solamente lleva control de la posición y del movimiento del objeto, y no tiene forma ninguna (no tiene imagen). Todos los objetos del juego heredarán de la clase CGameObject y la imagen que se muestra será responsabilidad de cada clase. En este caso, cuando implementamos la clase CNave, esta clase se encarga de mostrar su propia imagen.
Al usar clases separadas, el código en cada una queda más simple y claro. Cada clase se encarga solamente de lo que le compete, y hereda el resto de su clase padre. Por ejemplo, veamos cómo queda la clase CGameObject, la cual se encarga del movimiento del objeto:
# -*- 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):
def __init__(self):
# Coordenadas del objeto.
self.mX = 0
self.mY = 0
# Variables para controlar los bordes.
self.mMinX = -float("inf")
self.mMaxX = float("inf")
self.mMinY = -float("inf")
self.mMaxY = float("inf")
# 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
# 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
# Mover el objeto.
# Parámetros:
# aIncX: Cantidad de píxeles que se mueve en la horizontal.
# aIncY: Cantidad de píxeles que se mueve en la vertical.
def update(self, aIncX, aIncY):
# Mover el objeto.
self.mX += aIncX
self.mY += aIncY
# Controlar los bordes haciendo wrap.
if self.mX > self.mMaxX:
self.mX = self.mMinX
if self.mX < self.mMinX:
self.mX = self.mMaxX
if self.mY > self.mMaxY:
self.mY = self.mMinY
if self.mY < self.mMinY:
self.mY = self.mMaxY
# Liberar lo que haya creado el objeto.
def destroy(self):
pass
Como vemos, la clase CGameObject tiene como propiedades las coordenadas (x, y) de su posición, y las variables con los límites de movimiento. Luego sus métodos son setXY(), setBounds() y update(), relacionados al movimiento del objeto (de la misma forma en que se movían las naves antes). Por último, la función destroy(), que no hace nada, dado que no tiene nada que eliminar (nada ha sido creado por ahora), pero la implementamos porque es una buena práctica de programación y la vamos a invocar desde la clase CNave.
La clase CNave, heredará de CGameObject, y por lo tanto, heredará sus propiedades y el comportamiento de movimiento. El código de la clase CNave se muestra a continuación.
# -*- 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 CGameObject.
from CGameObject import *
# La clase CNave hereda de CGameObject.
class CNave(CGameObject):
# ----------------------------------------------------------------
# Constructor.
# Parámetros:
# aImgFile: Nombre de la imagen a cargar.
# ----------------------------------------------------------------
def __init__(self, aImgFile):
# Invocar al constructor de la clase base.
CGameObject.__init__(self)
# 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.
# Parámetros:
# aIncX: Cantidad de píxeles que se mueve en la horizontal.
# aIncY: Cantidad de píxeles que se mueve en la vertical.
def update(self, aIncX, aIncY):
# Invocar a update() de la clase base para el movimiento.
CGameObject.update(self, aIncX, aIncY)
# Dibuja el objeto en la pantalla.
# Parámetros:
# aScreen: La superficie de la pantalla en donde dibujar.
def render(self, aScreen):
aScreen.blit(self.mImg, (self.mX, self.mY))
# Liberar lo que haya creado el objeto.
def destroy(self):
# Invocar a destroy() de la clase base.
CGameObject.destroy(self)
self.mImg = None
Nota: La línea class CNave(CGameObject): significa que estamos definiendo la clase CNave, la cual hereda de CGameObject (se pone entre paréntesis la clase base, de la cual hereda).
La clase CNave se encarga de cargar la imagen de la nave enemiga y de liberarla al final. Como el movimiento lo hereda de CGameObject, la función update() lo único que hace es invocar a la función update() de la clase base (CGameObject), que es donde se encuentra el código del movimiento del objeto. Cuando hagamos objetos en el juego que tengan un comportamiento propio, la función update() tendrá más sentencias. Por ahora, como el movimiento de la nave usa el movimiento que está en la clase base, la función update() solamente invoca a la función update() en CGameObject.
Nota: En la función update(), vemos la siguiente línea: CGameObject.update(self, aInc, aIncY). Esto significa invocar a la función update() en la clase base, la cual es CGameObject.
La función render() de CNave, lo que hace es dibujar la imagen en la pantalla y por último la función destroy() elimina la imagen utilizada e invoca a la función destroy() de CGameObject.
Nota: La idea de la Programación Orientada a Objetos es reutilizar en otros proyectos las clases que programemos. La clase CGameObject es la primera clase que estaremos reusando en varios de nuestros propios juegos.

Figura 5-3: El juego ahora tiene naves y cuadrados heredando de CGameObject.
Packages api / game
Cuando programamos un videojuego (o cuando programamos cualquier otra cosa), generalmente resolvemos cosas que podremos reusar en futuros juegos. Por ejemplo, la clase CGameObject que estamos escribiendo, es una clase de base que sirve para todos los juegos que hagamos en el futuro.
Cuando comenzamos un nuevo juego, sería bueno ya poder contar con todas las clases que tenemos hechas y que nos puedan servir. De esta forma, desarrollaremos bastante más rápido y tendremos mucha funcionalidad disponible para usar en los juegos.
Para hacer esto, las clases deben estar bien organizadas, de modo de saber cuales son las clases genéricas (las que sirven para todos los juegos que hagamos) y cuales son las clases propias del juego actual (las que no sirven en otro juego directamente, sino que hay que adaptarlas para que funcionen).
Por otra parte, cuando un juego crece en complejidad, es muy conveniente organizar los archivos en carpetas y darle una estructura al proyecto, de modo de saber dónde se encuentra cada archivo sin tener que andar buscando mucho. Aprovecharemos que estamos creando la estructura del juego para hacer también carpetas para las imágenes que tenemos del juego. En la figura 5-3 se muestra la estructura de carpetas que utilizaremos.

Figura 5-4: Estructura de carpetas y archivos del proyecto.
La estructura del proyecto se compone de las siguientes carpetas:
api: En esta carpeta irán las clases que se pueden utilizar en otros juegos.
game: En esta carpeta irán las clases que son propias del juego actual que estamos haciendo.
assets
images: Donde colocamos las imágenes utilizadas en el juego.
audio: Donde se encuentran los archivos de audio utilizados.
fonts: Donde colocamos los archivos de las fuentes.
data: Donde se encuentran los archivos con datos del juego como niveles, textos, etc.
En la raíz (en la base) del proyecto, tenemos el archivo main.py, que es el programa principal que se ejecuta.
Ahora veamos el ejemplo en la carpeta: capitulo_05\003_estructura_api_game. En este ejemplo hemos dividido el proyecto en carpetas como se muestra en la figura 5-3.
Luego de creadas las carpetas api y game, colocamos las clase CGameObject en la carpeta api, dado que es una clase que reusaremos entre juego y juego. Las clases CNave y CCuadrado son clases del juego que estamos haciendo en este momento, por lo que irá en la carpeta game.
Cuando las clases del programa se encuentran en una carpeta, a la carpeta se le denomina package (o paquete). En este caso, tendremos dos packages: api y game.
Nota: Para que Python trate a una carpeta como un package y reconozca las clases que hay dentro como clases, hay que poner un archivo __init__.py. Este archivo estará vacío, pero es obligatorio que exista en la carpeta, de lo contrario Python nos dará error al importar una clase desde esa carpeta. Por esta razón es que las carpetas api y game tienen este archivo dentro.
Cuando usamos carpetas, lo que tenemos que hacer es indicar su ruta cuando hacemos los import de las clases. Por ejemplo, en main.py, importamos la clase CNave y CCuadrado desde el package game de la siguiente manera:
# Importar la clase CNave.
from game.CNave import *
# Importar la clase Cuadrado.
from game.CCuadrado import *
En CNave.py y en CCuadrado.py, importamos la clase CGameObject del package api de la siguiente manera:
# Importar la clase CGameObject.
from api.CGameObject import *
Las imágenes las colocaremos en la carpeta assets/images. Luego, al momento de cargar las imágenes que hemos reubicado, debemos indicar su ruta cuando indicamos el nombre del archivo. Esto lo hacemos en main.py:
...
imgSpace = pygame.image.load("assets/images/space_640x360.jpg")
...
n1 = CNave("assets/images/grey_ufo.png")
n2 = CNave("assets/images/yellow_ufo.png")
...
Encapsulamiento de los Objetos
Para terminar con la estructuración del código, vamos a hacer una mejora relacionada con la nave enemiga que tenemos en el juego.
Si nos fijamos en el código del ejemplo anterior, en main.py, cuando se crean las naves, se les pasa como parámetro las imágenes a usar. Luego de esto se establecen los límites del movimiento. Esto se hace con las siguientes sentencias:
# Crear las naves: se le pasa como parámetro la imagen de la nave.
n1 = CNave("assets/images/grey_ufo.png")
n2 = CNave("assets/images/yellow_ufo.png")
# Colocar las naves en su posición inicial.
n1.setXY(0, 180)
n2.setXY(SCREEN_WIDTH - NAVE_WIDTH, 212)
# Marcar los límites del mundo.
n1.setBounds(-NAVE_WIDTH, -NAVE_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT)
n2.setBounds(-NAVE_WIDTH, -NAVE_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT)
Pues bien, no tienen nada de incorrecto esto, pero es mejor si creamos una nave y ésta misma toma la imagen y maneja su ancho y alto, de forma tal que la clase CNave sea bastante más simple de usar, y también para que la clase quede mucho más reusable.
Lo que haremos, es que que la clase CNave tenga un parámetro que será el tipo de nave del que se trate (dorada o plateada), y según el tipo de nave se cargará la imagen correspondiente, establecerá los límites según el tamaño de la imagen y de esta forma todas las naves enemigas del juego harán eso.
Abre y ejecuta el ejemplo que se encuentra en la carpeta capitulo_05\004_clase_nave_encapsulada. En este ejemplo, en main.py las naves se inicializan de la siguiente manera:
# Crear las naves: se le pasa como parámetro la imagen de la nave.
n1 = CNave(CNave.TYPE_PLATINUM)
n2 = CNave(CNave.TYPE_GOLD)
# Colocar las naves en su posición inicial.
n1.setXY(0, 180)
n2.setXY(SCREEN_WIDTH - n2.getWidth(), 212)
# 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)
De esta manera, cuando creamos una nave, le pasamos el tipo de nave a crear y este tipo se guarda en la variable self.mType de la clase CNave. El tipo de nave es simplemente un número, (una constante), que se usará para diferenciar a las naves por su tipo.
En la clase CNave, en el constructor, se cargará la imagen correspondiente según se trate de la nave plateada o la dorada. En el código que se muestra arriba, también podemos observar que ya no tenemos constantes de ancho y alto de la nave. Las mismas se encuentran dentro de la clase CNave.
La clase CNave se modifica para tener un atributo con su tipo, y según el tipo, cargar la imagen correspondiente. A continuación se muestra la clase CNave y su constructor..
...
class CNave(CGameObject):
# Tipos de naves.
TYPE_PLATINUM = 0
TYPE_GOLD = 1
# ----------------------------------------------------------------
# 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"
# Cargar la imagen.
self.mImg = pygame.image.load(imgFile)
self.mImg = self.mImg.convert_alpha()
# Guardar el ancho y el alto.
self.mWidth = self.mImg.get_width()
self.mHeight = self.mImg.get_height()
...
En la clase se definen dos constantes, TYPE_PLATINUM y TYPE_GOLD, que se usan para cargar la imagen que corresponda al tipo de nave. Al final, se cargan en las variables self.mWidth y self.mHeight el ancho y el alto de la imagen.
De esta forma, la clase CNave se encarga de todos los detalles de la nave (su tipo, su imagen y sus propiedades como el alto y el ancho) y desde afuera de la clase se establecen parámetros de acuerdo a la lógica del juego.
En la clase CNave, agregamos dos funciones, getWidth() y getHeight() para retornar el ancho y el alto de la nave respectivamente. Estas funciones se denominan getters, y se utilizan para consultar propiedades del objeto (en inglés “get” significa “obtener”):
# Retorna el ancho de la nave.
def getWidth(self):
return self.mWidth
# Retorna el alto de la nave.
def getHeight(self):
return self.mHeight
Tipos de Objetos
En los juegos es muy bueno tener variaciones en los objetos. En nuestro juego, tendremos naves espaciales de varios colores. Para eso hemos definido un tipo de nave (un número) que almacenamos en la variable self.mType, y según el tipo, de nave se cargará el gráfico que le corresponda.
De esta forma tendremos naves que se comportan en forma muy parecida pero que cambiarán los gráficos, o algún atributo como por ejemplo su velocidad. La alternativa a tener un tipo de objeto es tener diferentes clases de objetos. Si ambos objetos del juego comparten mucho código deberían ser la misma clase. Si los objetos del juego son muy diferentes entre sí, deberían estar en clases diferentes. Esta es la diferencia, por ejemplo, entre tener una sola clase CNave para las naves espaciales del juego con un tipo según el cual se cargan diferentes gráficos, versus tener una clase para cada tipo de nave: por ejemplo, clases CNaveGris, CNaveRoja, etc.
Bueno, hemos terminado de armar la estructura básica de un videojuego. Ahora pasaremos a la parte divertida, la física de movimiento.
Comments