Power Ups y Obstáculos
Una vez visto el jugador, vamos a añadir los elementos que le darán vida al juego: los Power Ups y los Obstáculos. Estos elementos son los que harán que el jugador tenga que esquivar o recoger para poder avanzar en el juego.
El PowerUp estará representado por un caramelo que nos dará puntos si es recolectado por el jugador. Por otro lado, el Obstáculo estará representado por un esqueleto de un pescado que nos restará vidas si es tocado por el jugador. Ambos elementos se moverán de arriba a abajo, simulando que el jugador está avanzando en el juego.
Todos los recursos tanto de Power Ups como de Obstáculos se encuentran en fichero .zip del proyecto. En el caso del Power Up, el recurso es candy-16x16.png, mientras que para el Obstáculo es fish.png.
De igual forma que hicimos con el jugador, vamos a transformar estos ficheros en Sprites, usando el conversor online https://www.bloggingadeadhorse.com/TeamARGImgConverter/. El resultado, como en el caso del jugador, lo vamos a guardar en el fichero resources.h que ya hemos creado anteriormente. De esta forma, tendremos todos los recursos del juego en un único fichero.
#ifndef RESOURCES_H
#define RESOURCES_H
const uint8_t PROGMEM playerSprt[] = {
16, 16,
0x00, 0xf0, 0x88, 0x88, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x88, 0x88, 0xf0, 0x00,
0x00, 0x1f, 0x3f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x3f, 0x1f, 0x00,
};
const uint8_t PROGMEM candy[] = {
16, 16,
0x08, 0x0c, 0x0e, 0x0f, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xe0, 0xe0, 0x80, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0x07, 0x07, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0xf0, 0x70, 0x30, 0x10,
};
const uint8_t PROGMEM fish[] = {
16, 16,
0x80, 0xc0, 0xe0, 0x80, 0xe0, 0x80, 0xf0, 0x80, 0xe0, 0x80, 0x80, 0xf0, 0xb0, 0xe0, 0xc0, 0x80,
0x00, 0x01, 0x03, 0x00, 0x03, 0x00, 0x07, 0x00, 0x03, 0x00, 0x00, 0x07, 0x07, 0x03, 0x03, 0x01,
};
Con las imágenes ya convertidas, vamos a crear una nueva clase llamada ObJ, que representará tanto a los Power Ups como a los Obstáculos. Esta clase se encargará de manejar la posición, el movimiento y la interacción de estos elementos con el jugador.
Para saber que tipo de elemento es, vamos a crear un enumerado llamado ObjType, que tendrá dos valores: CANDY, FISH, NONE. De esta forma, podremos diferenciar entre los dos tipos de elementos y aplicar la lógica correspondiente en el juego.
typedef enum{
CANDY,
FISH,
NONE
}ObjType;
Una vez creado el enumerado, vamos a definir la clase ObJ en el fichero objects.h. Esta clase tendrá propiedades como la posición del elemento en pantalla, el tipo de elemento y métodos como el movimiento del elemento y la detección de colisiones con el jugador.
class ObJ{
private:
byte x,y;
ObjType type;
bool active;
public:
ObJ();
ObJ(byte x, byte y, ObjType type);
void setPosition(byte x, byte y);
void setActive(bool active);
bool isActive();
ObjType getType();
void Update(Arduboy2Base* arduboy);
void Draw(Arduboy2Base* arduboy, Sprites* sprites);
};
Como podemos ver en el código anterior, la clase ObJ tiene propiedades como la posición del elemento en pantalla, el tipo de elemento y un booleano que indica si el elemento está activo o no. Además, tiene métodos para actualizar la posición del elemento, dibujarlo en pantalla y detectar colisiones con el jugador.
Vamos a pasar a la implementación de la clase ObJ en el fichero objects.cpp. En este fichero, vamos a implementar los métodos de la clase ObJ, como el constructor, el método Update y el método Draw.
#include "objects.h"
#include "resources.h"
#define FALL_SPEED 1
ObJ::ObJ(){
this->x = 0;
this->y = 0;
this->type = NONE;
this->active = false;
}
ObJ::ObJ(byte x, byte y, ObjType type){
this->x = x;
this->y = y;
this->type = type;
this->active = false;
}
void ObJ::setPosition(byte x, byte y) {
this->x = x;
this->y = y;
}
void ObJ::setActive(bool active) {
this->active = active;
}
bool ObJ::isActive() {
return this->active;
}
ObjType ObJ::getType(){
return this->type;
}
void ObJ::Update(Arduboy2Base* arduboy) {
if (this->y < 64 && this->active) {
this->y += FALL_SPEED;
} else {
this->active = false;
}
}
void ObJ::Draw(Arduboy2Base* arduboy, Sprites* sprites) {
if (!this->active) {
return;
}
switch (this->type) {
case CANDY:
sprites->drawOverwrite(this->x, this->y, candy, 0);
break;
case FISH:
sprites->drawOverwrite(this->x, this->y, fish, 0);
break;
}
}
Esta vez hemos definido la velocidad de caída de los elementos con la constante FALL_SPEED, que indica la cantidad de píxeles que se moverá el elemento hacia abajo en cada actualización. En el método Update, comprobamos si el elemento está activo y si su posición en el eje Y es menor que 64 (el límite inferior de la pantalla). Si es así, aumentamos su posición en Y en FALL_SPEED. Si no, desactivamos el elemento.
Ahora vamos a implementar el uso de esta clase para crear y manejar los Power Ups y Obstáculos en el juego. Para ello, vamos a crear un array de objetos ObJ que representará los elementos que aparecerán en pantalla. Además, vamos a implementar la lógica para generar estos elementos de forma aleatoria y detectar colisiones con el jugador. En este caso, vamos a crear un array de 10 elementos, que representará los Power Ups y Obstáculos que aparecerán en pantalla.
#define MAX_OBJECTS 10
ObJ objs[MAX_OBJECTS];
int currentObj = 0;
Ahora en la función UpdateGamePlay() del fichero ardugame.ino, vamos cargar cada objeto cuando pase 1 segundo. Estableceremos su posición inicial y su tipo. Para ello, vamos a crear un bucle que recorra el array y establezca la posición y el tipo de cada objeto de forma aleatoria.
void UpdateGamePlay(void){
if(arduboy.everyXFrames(FRAMES)){
objs[currentObj] = ObJ(rand()%112,0, rand()%2);
objs[currentObj].setActive(true);
currentObj++;
if(currentObj>=MAX_OBJS){
currentObj = 0;
}
}
for(int i=0; i<MAX_OBJS; i++){
objs[i].Update(&arduboy);
}
player.Update(&arduboy);
}
Ya solo nos queda dibujar los elementos en pantalla. Para ello, vamos a llamar al método DrawGamePlay() de cada objeto en el bucle que recorre el array de objetos. De esta forma, cada objeto se dibujará en su posición correspondiente en pantalla.
void DrawGamePlay(void){
for(int i=0;i<MAX_OBJS;i++){
objs[i].Draw(&arduboy, &sprites);
}
player.Draw(&arduboy,&sprites);
}
Una vez terminado, ya tenemos implementados los Power Ups y Obstáculos en nuestro juego. Ahora el jugador tendrá que esquivar los obstáculos y recoger los Power Ups para poder avanzar en el juego. Aunque no hemos implementado la lógica para detectar colisiones entre el jugador y los elementos, esto lo veremos en el siguiente capítulo, donde implementaremos la lógica de colisiones y la gestión de vidas y puntuación del jugador.