¡Saludos, estudiantes!. ¡Pasad!. ¡Pasad!. ¿Cómo estáis?. ¡Cuánto tiempo ha pasado!. La verdad es que tengo la web abandonadísima... ¡Así que lo mejor será retomarlo donde lo habíamos dejado!. Hoy vamos a avanzar un poquito más sobre lo ya explicado acerca de la Programación Orientada a Objetos, POO en Processing, a través de un ejercicio práctico. ¡Vamos a realizar un pequeño videojuego al estilo de Space Invaders o Phoenix! Y ya de paso, empezamos a obligar a las clases a interactuar entre sí, pasándose objetos entre ellas como parámetros. ¡Nos vamos a divertir!.
¿Por dónde empezamos?
¿Por dónde vamos a empezar?. ¡Por el principio!. Si queremos crear un videojuego de tipo matamarcianos, lo primero que necesitamos es marcianos. ¡Un montón de marcianos!. ¡Miriadas de marcianos!. Y para ello, implementaremos una única clase a la que llamaremos (oh, originalidad, estás sobrevalorada) marciano.
Vale. La primera clase servirá para crear marcianos. ¿Qué debemos meter ahí?.
La pregunta correcta, desde el punto de vista de un programador profesional, sería:
- ¿Qué atributos queremos que tenga la clase?.
- ¿Qué metodos?.
- ¿Cómo implementar un constructor?.
- Para generar cada marciano, ¿tendré que pasarle algún dato al programa?.
Ok. Ok. Ok. Empecemos por el principio.
Para empezar, queremos unos enemigos que se muevan horizontalmente (al menos al principio) a lo largo de la pantalla. Eso implicará que necesitarán, como mínimo, unas coordenadas horizontal y vertical (atributos de tipo entero). También necesitarán un parámetro que llamaremos vx y que indicará la velocidad de desplazamiento. Por ahora (aunque eso lo dejo a vuestra elección), éste lo estableceremos por defecto a 5. Nos preocuparemos después de dotar a cada marciano de una imagen de tipo sprite mediante el comando PImage. De momento, un ellipse() nos valdrá.
Cada marciano tendrá dos estados: vivo o muerto(dependerá de si uno de nuestros misiles le ha alcanzado o no). Eso se lo dejaremos a un atributo de tipo boolean que llamaremos vivo, y que en el constructor de la clase se establecerá por defecto a true.
Estos atributos son los básicos. Después podremos ir añadiendo más elementos a nuestra clase para enriquecerla. Pasamos a pensar en los métodos que habría que implementar.
Como repito por activa y por pasiva a mis alumnos de Bachillerato (pobrecicos... La paciencia que tienen conmigo todas las mañanas), es una buena práctica identificar a qué atributos vamos a necesitar acceder para leerlos o modificarlos desde otras partes del programa, y generar de modo semiautomático para cada uno de ellos dos métodos: uno para leerlos (tipo dameAtributo() ), y otro para modificarlos (estilo grabaAtributo(int valor)). En nuestro caso, como mínimo, necesitaremos saber en cada momento las coordenadas de posición de cada marciano (para compararlas con las coordenadas de nuestros disparos), y, en caso de resultar alcanzados por nuestro misil de neutrones, habrá que modificar su estado vivo de true a false.
En otro orden de cosas, cada marciano necesitará que el programa realice dos procesos: el de cálculo de sus coordenadas en cada momento (que llamaremos mueveMarciano(), y el que lo dibuje en el escenario (de momento, sólo un circulito con el comando ellipse(); luego ya veremos).
¿Nos ponemos ya a redactar el código que implementará la clase marciano?. ¡Estoy impaciente!
Sí. Ya podemos empezar. Cada uno de vosotros tendrá una idea distinta, pero yo, en Processing, empezaría por el planteamiento más simplificado dentro de lo posible:
class marciano{
//Declaramos los atributos que necesitaremos
int x, y, vx;
boolean vivo;
//Para el constructor sólo precisaremos los parámetros para x e y
//El resto los definimos nosotros por sistema (podéis cambiar esto)
marciano(int a, int b){
x=a;
y=b;
vx=5;
vivo=true;
}
//En el futuro necesitaremos leer las coordenadas x e y
int dameX(){
return x;
}
int dameY(){
return y;
}
//Movemos el marciano. Las condiciones de rebote se explicaron anteriormente
//Sólo se ejecuta la orden si vivo==true
void mueveMarciano(){
if (vivo){
x+=vx;
if (x>width||x<0){
vx=-vx;
}
}
}
void dibujaMarciano(){
//Sólo se ejecuta la orden si vivo==true
if (vivo){
ellipse(x,y,20,20);
}
}
}
Implementada la clase, ya sólo habría que crear los enemigos. Para ello, ya sabéis como se suele trabajar en programación (al menos, en plan sencillito, que es lo que yo enseño):
- Lo primero, en la zona de declaración de variables, indicaremos las clases que queremos utilizar, indicando el nombre de cada objeto.
- En la función setup() inicializaremos los objetos correspondientes, pasándoles los parámetros que estimemos oportunos.
- En el bucle principal, draw(), en cada objeto, apelaremos a su método mueveMarciano() (que calculará las nuevas coordenadas) y a continuación a dibujaMarciano() (cuya función es obvia).
Justo debajo del código de la clase (o encima, me da igual) añadiremos, pues, las siguientes líneas:
//Metemos tres marcianos para empezar
marciano m1,m2,m3;
void setup(){
size(800,600);
//Inicializamos m1, m2, m3
m1=new marciano(50,100);
m2=new marciano(400,250);
m3=new marciano(450, 150);
}
void draw(){
background(255,255,255);
m1.mueveMarciano();
m2.mueveMarciano();
m3.mueveMarciano();
m1.dibujaMarciano();
m2.dibujaMarciano();
m3.dibujaMarciano();
}
¿Ya lo tienes?. ¿Te sale algo así?:
Si es así, ¡felicidades!. Si no, algo habremos hecho mal. Revisa de nuevo lo escrito y vuelve a probar.
Bueno, pues como ya sabemos trabajar con lo básico, lo que hay que hacer ahora es llenar la pantalla de enemigos. Para ello utilizaremos matrices o arrays, lo que nos permitirá ahorrarnos el proceso de ir moviendo y dibujando a cada enemigo en el bucle principal (que de momento, es relativamente cómodo, pero imagina lo que sería llamar a los procesos mueveMarciano() y dibujaMarciano() si quisiéramos mover cincuenta enemigos por la pantalla).
Arrays (matrices) de objetos
Lo que vamos a hacer ahora es crear 4 filas de enemigos, a 4 alturas distintas (en mi código, uso height/6, 2*height/6, 3*height/6 y 4*height/6) y con distintas velocidades.
Para crear un array de objetos de la clase marciano, daremos siempre los mismos pasos (yo, en mi código, voy a utilizar los nombres f1, f2, f3 y f4):
marciano f1[], f2[], f3[], f4[];
Antes de seguir, permitidme hacer un inciso: hemos dicho que queremos distintas filas de enemigos por la pantalla, que además circulen a velocidades distintas. Para ello, en la clase marciano, modificaremos el constructor para que se pueda introducir, además de las coordenadas horizontal y vertical, un tercer valor entero correspondiente a la velocidad:
marciano(int a, int b,int c){
x=a;
y=b;
vx=c;
vivo=true;
}
Ahora sí. Ya estamos preparados para seguir. Bueno, como decía, una vez declaradas las cuatro matrices que recogerán todas las naves enemigas, habrá que recorrerlas una vez, en setup(), para inicializar cada uno de los objetos que contiene, mediante un bucle de iteración for():
for (int a=0;a<f1.length;a++)
{
f1[a]=new marciano(a*width/f1.length,height/6,3);
}
Luego, en draw(), volveremos a recorrer cada matriz para, en cada enemigo de la fila, llamar a los métodos mueveMarciano() y dibujaMarciano(); ¡dentro código!
for (int a=0;a<f1.length;a++)
{ f1[a].mueveMarciano();
f1[a].dibujaMarciano();
}
El código final sería algo así:
class marciano{
//Declaramos los atributos que necesitaremos
int x, y, vx;
boolean vivo;
//Para el constructor sólo precisaremos los parámetros para x e y
//El resto los definimos nosotros por sistema (podéis cambiar esto)
marciano(int a, int b,int c){
x=a;
y=b;
vx=c;
vivo=true;
}
//En el futuro necesitaremos leer las coordenadas x e y
int dameX(){
return x;
}
int dameY(){
return y;
}
//Movemos el marciano. Las condiciones de rebote se explicaron anteriormente
//Sólo se ejecuta la orden si vivo==true
void mueveMarciano(){
if (vivo){
x+=vx;
if (x>width||x<0){
vx=-vx;
}
}
}
void dibujaMarciano(){
//Sólo se ejecuta la orden si vivo==true
if (vivo){
ellipse(x,y,20,20);
}
}
}
//Cuatro filas de marcianos
marciano f1[],f2[],f3[],f4[];
void setup(){
size(800,600);
//Inicializamos los 4 array (cada uno tendrá un
//número diferente de enemigos)
f1=new marciano[5];
f2=new marciano[6];
f3=new marciano[4];
f4=new marciano[7];
//Recorremos cada array para inicializar las naves enemigas
//(la posición horizontal se calculará repartiendo el ancho
//de pantalla por el número de enemigos de la fila)
for (int a=0;a<f1.length;a++){
f1[a]=new marciano(a*width/f1.length,height/6,3);
}
for (int a=0;a<f2.length;a++){
f2[a]=new marciano(a*width/f2.length,2*height/6,5);
}
for (int a=0;a<f3.length;a++){
f3[a]=new marciano(a*width/f3.length,3*height/6,4);
}
for (int a=0;a<f4.length;a++){
f4[a]=new marciano(a*width/f4.length,4*height/6,6);
}
}
void draw(){
background(255,255,255);
//Recorremos cada array para llamar a los métodos de movimiento y de dibujo
for (int a=0;a<f1.length;a++){
f1[a].mueveMarciano();
f1[a].dibujaMarciano();
}
for (int a=0;a<f2.length;a++){
f2[a].mueveMarciano();
f2[a].dibujaMarciano();
}
for (int a=0;a<f3.length;a++){
f3[a].mueveMarciano();
f3[a].dibujaMarciano();
}
for (int a=0;a<f4.length;a++){
f4[a].mueveMarciano();
f4[a].dibujaMarciano();
}
}
Dicho lo cual, te quedaría una cosita parecida a ésta:
No ha exigido muchas líneas de código, ¿verdad?. Para acabar por hoy (que esto ya se hace pesado), vamos a dotar a nuestros enemigos de un sprite con el comando PImage, que es muy socorrido en Processing. Para ello, he buscado tres sprites de video juego en fuentes Open Source (es decir, entiendo que se pueden distribuir libremente), que puedes descargar aquí, aquí y aquí.
He llamado a cada sprite nave1.png, nave2.png y nave3.png. Al inicializarse cada enemigo, elegirá uno de los sprites disponibles aleatoriamente. Ya sabes que, en Processing, (y si no lo sabes, yo te lo explico), cuando se quieren utilizar archivos externos, como es el caso de las imágenes, hay que adjuntarlos a la carpeta data del sketch (menú Sketch->Añadir archivo).
La clase Marciano variaría en dos aspectos: en primer lugar, en el constructor: declararemos una nueva variable PImage que llamaremos imagen, y que elegirá aleatoriamente entre tres valores, de acuerdo a los cuales elegirá que archivo *.png utiliza como sprite (con una sentencia switch(nº aleatorio)). En segundo lugar, en la clase dibujaMarciano(), sustituiremos la orden ellipse (que simbolizaba la nave enemiga) por el comando image(imagen,x,y).
Total, que el código de la clase se quedaría así (no hay que tocar nada de momento en las funciones setup() y draw():
class marciano{
//Declaramos los atributos que necesitaremos
int x, y, vx;
PImage imagen;
boolean vivo;
//Para el constructor sólo precisaremos los parámetros para x e y
//El resto los definimos nosotros por sistema (podéis cambiar esto)
marciano(int a, int b,int c){
x=a;
y=b;
vx=c;
vivo=true;
int calculo=(int)random(1,4);
switch(calculo){
case 1:
imagen=loadImage("nave1.png");
break;
case 2:
imagen=loadImage("nave2.png");
break;
case 3:
imagen=loadImage("nave3.png");
break;
}
//como no sé qué resolución vais a utilizar, dejo un redimensionado estándar
imagen.resize(width/10,height/10);
}
//En el futuro necesitaremos leer las coordenadas x e y
int dameX(){
return x;
}
int dameY(){
return y;
}
//Movemos el marciano. Las condiciones de rebote se explicaron anteriormente
//Sólo se ejecuta la orden si vivo==true
void mueveMarciano(){
if (vivo){
x+=vx;
if (x>width||x<0){
vx=-vx;
}
}
}
void dibujaMarciano(){
//Sólo se ejecuta la orden si vivo==true
if (vivo){
image(imagen,x,y);
}
}
}
Si así lo hiciéreis, el dios de la Programación os lo premiare con algo parecido a esto:
¡Ups!. ¡Ha sonado el timbre!
¡Y eso ha sido todo por hoy!. ¡No me negaréis que estamos avanzando bastante deprisa, máxime cuando estamos hablando ya de conceptos tan farragosos como la POO!. El próximo día, habrá que implementar una clase para la nave protagonista, así como disponer un sistema de disparos que nos permita eliminar las naves alienígenas, así como algún algoritmo que les permita avanzar hacia nosotros, de modo que también nos puedan matar. Un sistema de vidas, puntos... ¡no sé!. ¡Hasta donde queramos llegar!. ¡Lo iremos viendo en sucesivos artículos!. Mientras tanto, ¡sed felices!. ¡Nos vemos pronto!. ¡Siempre creciendo!. ¡Siempre aprendiendo!. ¡Cultura maker!.