Y aquí vengo con lo prometido, esa maravilla de documento que os había comentado hace tiempo y que explicaba paso por paso cómo hacer un juego y con los códigos incluidos. Os dejo el enlace a dicho manual/tutorial de Nacho Cabanes y vamos con la parte práctica.
En dicho tutorial, usa C# y Mono y va paso por paso, sin presuponer apenas conocimientos de programación ni del lenguaje ni nada. En mi caso, tengo experiencia y me saltaré la parte de explicar, asi que directamente comento lo que hago y os dejo el código.
Y como soy masoca y me gusta ser diferente, en vez de C#, que no me ha hecho gracia, usaré C++, y en vez de Mono, SDL. SDL es una de las librerías gráficas que había visto y tenía en mente usarlo en mis proyectos junto a ogrex, como ogrex es más para 3D y se me ha hecho complicado empezar, pues toma SDL.
Y si a alguien le resulta interesante, lo hago con mi fiel Netbeans, al que incluí la documentación de SDL (no sin trabajo y ni idea de cómo lo hice). Y para poder aprender un poco el manejo de SDL, me he encontrado con un tutorial de SDL ya mencionado que es al mismo tiempo de videojuegos también.
En otra entrada comentaré las diferencias y opiniones que tengo acerca de ambos libros, que son al mismo tiempo iguales y distintos. Por ahora, ¡vamos a la "chicha"!
Ya tengo la base SDL que comenté en dos entradas (1 y 2). El manual comienza con juegos de números aleatorios, de programación muy básica, asi que me lo salto. Lo siguiente ya va formando paso por paso la estructura del videojuego en modo texto.
1º Crear un bucle de juego
Es decir, un bucle en el main donde pondremos el grueso del juego. Empezamos creando dicho bucle y una condición de parada que podría ser pulsar una tecla concreta.
Una vez lo tenemos, en dicho bucle dibujamos una letra, representando al personaje. Establecemos cuatro teclas para el movimiento y esperamos al usuario a que meta una tecla.
En función de la tecla, movemos la posición del personaje y repetimos el bucle para que se dibuje y espere otra tecla. Si la tecla pulsada es de salida (condición de parada), el bucle se acaba.
Pseudocódigo
salida=false; Mientras salida==false Limpiar pantalla Dibujar letra en su posición Esperar pulsación tecla Si tecla=Q entonces salida=true sino Según tecla hacer A: mover izquierda W: mover arriba S: mover abajo D: mover derecha default: no hacer nada fin Si Fin mientras
A la hora de hacer el código, lo he separado en dos clases, una para el manejo y dibujado en pantalla, y otra para el personaje.
Graphics: Me permitirá inicializar todo el sistema SDL, limpiar la pantalla, escribir y dibujar. A medida que avance, añadiré los métodos necesarios.
Graphics.h: Pulsa para ver/ocultar el código
/* * File: Graphics.h * Author: Dorian Hawkmoon * * Created on 25 de agosto de 2013, 19:13 */ #ifndef GRAPHICS_H #define GRAPHICS_H //entrada/salida #include <iostream> //SDL básico #include <SDL/SDL.h> //fuentes SDL #include <SDL/SDL_ttf.h> //personaje #include "Character.h" using namespace std; class Graphics { private: /*pantalla principal*/ SDL_Surface *screen; /*pantalla para escribir*/ SDL_Surface *letters; /*fuente*/ TTF_Font *font; /*color de letra*/ SDL_Color foreground; /*color de fondo*/ SDL_Color background; /*Inicialización común*/ void comunInit(); /*Inicialización de la pantalla*/ void initScreen(); /*Inicialización de las fuentes*/ void initTTF(); public: /*Constructor*/ Graphics(); /*Destructor*/ virtual ~Graphics(); /*Limpia la pantalla y actualiza*/ void clearScreen(); /*Escribe en la posición dada y actualiza*/ void write(char *phrase, int x, int y); /*Escribe en la posición dada y actualiza*/ void write(char letter, int x, int y); /*Escribe en la posición dada y actualiza*/ void write(char* phrase, SDL_Rect point); /*Escribe en la posición dada y actualiza*/ void write(char letter, SDL_Rect point); /*Pinta al personaje en la posición dada*/ void paintCharacter(Character *character); /*Establece el color de letra*/ void setForeground(int red, int green, int blue); /*Establece el color de fondo*/ void setBackground(int red, int green, int blue); }; #endif /* GRAPHICS_H */
Graphics.cpp: Pulsa para ver/ocultar el código
/* * File: Graphics.cpp * Author: Dorian Hawkmoon * * Created on 25 de agosto de 2013, 19:13 */ #include "Graphics.h" /*Constructor*/ Graphics::Graphics() { //llamo a submétodos para inicializar todo initScreen(); initTTF(); comunInit(); } /*destructor*/ Graphics::~Graphics() { } /*Inicialización común*/ void Graphics::comunInit() { //establezco un título SDL_WM_SetCaption("Mundos del Videojuego", NULL); //establezco color de letra y fondo setBackground(0, 0, 0); setForeground(255, 255, 255); } /*Inicialización de la pantalla*/ void Graphics::initScreen() { //inicializamos el "video" (la pantalla). -1 indica error if (SDL_Init(SDL_INIT_VIDEO) < 0) { cout << "No se ha podido iniciar SDL: %s\n" << SDL_GetError(); exit(1); } else { //cuando el programa se cierre, ejecuta la función SDL_Quit //que libera la inicialización atexit(SDL_Quit); } //establezco modo de vídeo screen = SDL_SetVideoMode(640, 480, 32, SDL_SWSURFACE | SDL_DOUBLEBUF); if (screen == NULL) { cout << "No se ha podido establecer el modo de vídeo: %s\n" << SDL_GetError(); exit(1); } } /*Inicialización de las fuentes*/ void Graphics::initTTF() { //mismo procedimiento para el módulo de la escritura if (TTF_Init() < 0) { cout << "No se puedo inicializar SDL_ttf\n"; exit(1); } else { atexit(TTF_Quit); } //abro una fuente del sistema font = TTF_OpenFont("/usr/share/fonts/TTF/VeraMono.ttf", 22); } /*Escribe en la posición dada y actualiza*/ void Graphics::write(char* phrase, int x, int y) { //genero las letras en el lienzo (letter es un surface) Graphics::letters = TTF_RenderText_Shaded (Graphics::font, phrase, Graphics::foreground, Graphics::background); //establezco unas coordenadas donde empezar a escribir SDL_Rect coordenadas; coordenadas.x = x; coordenadas.y = y; //copio la imagen del texto a la principal SDL_BlitSurface(Graphics::letters, NULL, Graphics::screen, &coordenadas); //actualiza la imagen SDL_Flip(Graphics::screen); } /*Escribe en la posición dada y actualiza*/ void Graphics::write(char letter, int x, int y) { //paso el carácter a un array para convertirlo a puntero //y delegar la tarea al método principal (overloading) char letters[2]; letters[0]=letter; letters[1]='\0'; this->write(letters, x, y); } /*Escribe en la posición dada y actualiza*/ void Graphics::write(char* phrase, SDL_Rect point) { //delego la tarea en otro método this->write(phrase, point.x, point.y); } /*Escribe en la posición dada y actualiza*/ void Graphics::write(char letter, SDL_Rect point) { //delego la tarea en otro método this->write(letter, point.x, point.y); } /*Limpia la pantalla y actualiza*/ void Graphics::clearScreen() { //relleno todo con negro SDL_FillRect(screen, 0, SDL_MapRGB(screen->format, 0, 0, 0)); //actualizo SDL_Flip(screen); } /*Establece el color de fondo*/ void Graphics::setBackground(int red, int green, int blue) { //compruebo que los valores tengan rango adecuado if ((red>=0 & red<=255) & (green>=0 & green<=255) & (blue>=0 & blue<=255)) { background.r = red; background.g = green; background.b = blue; } } /*Establece el color de letra*/ void Graphics::setForeground(int red, int green, int blue) { //compruebo que los valores tengan rango adecuado if ((red>=0 & red<=255) & (green>=0 & green<=255) & (blue>=0 & blue<=255)) { foreground.r = red; foreground.g = green; foreground.b = blue; } } /*Pinta al personaje en la posición dada*/ void Graphics::paintCharacter(Character *character){ //guardo el color anterior para reestablecerlo SDL_Color foregroundBackup=foreground; //establezco el color del personaje foreground=character->getColor(); //dibujo el personaje (escribir una letra) this->write(character->getSymbol(), character->getActualPosition()); //dejo el color de letra como estaba foreground=foregroundBackup; }
Character: Tendrá todos los detalles de un personaje. Me he adelantado un poco a los acontecimientos y he añadido detalles que hacen falta después. Dicha clase tendrá la letra y color que le represente, y la posición original y actual, pudiendo moverlo.
Character.h: Pulsa para ver/ocultar el código
/* * File: Character.h * Author: Dorian Hawkmoon * * Created on 25 de agosto de 2013, 19:41 */ #ifndef CHARACTER_H #define CHARACTER_H //SDL básico #include <SDL/SDL.h> class Character { private: /*Posición inicial que al mismo tiempo guarda el tamaño del personaje*/ SDL_Rect originalPositionAndSize; /*Posición actual*/ SDL_Rect actualPosition; /*letra del personaje*/ char symbol; /*color del personaje*/ SDL_Color color; public: /*Enumeración de los movimientos que puede hacer*/ enum Movement { UP, DOWN, LEFT, RIGHT }; /*Constructor * Se le pasa la letra, color y posición inicial */ Character(char character, SDL_Color color, SDL_Rect position); /*destructor*/ virtual ~Character(); /*Devuelve la letra del personaje*/ char getSymbol(); /*Devuelve el color del personaje*/ SDL_Color getColor(); /*Devuelve la posición actual del personaje*/ SDL_Rect getActualPosition(); /*Setea la posición del personaje con un booleano que indica si se ha cambiado correctamente*/ bool setActualPosition(SDL_Rect position); /*Devuelve la posición actual a la posición original*/ void returnOriginalPosition(); /*Mueve la posición del personaje según su movimiento*/ bool move(Movement move); }; #endif /* CHARACTER_H */
Character.cpp: Pulsa para ver/ocultar el código
/* * File: Character.cpp * Author: Dorian Hawkmoon * * Created on 25 de agosto de 2013, 19:41 */ #include "Character.h" /*Constructor * Se le pasa la letra, color y posición inicial */ Character::Character(char symbolCharacter, SDL_Color colorCharacter, SDL_Rect position) { symbol=symbolCharacter; color=colorCharacter; originalPositionAndSize=actualPosition=position; } /*Destructor*/ Character::~Character() { } /*Devuelve la letra del personaje*/ char Character::getSymbol(){ return symbol; } /*Devuelve el color del personaje*/ SDL_Color Character::getColor(){ return color; } /*Devuelve la posición actual del personaje*/ SDL_Rect Character::getActualPosition(){ return actualPosition; } /*Setea la posición del personaje con un booleano que indica si se ha * cambiado correctamente*/ bool Character::setActualPosition(SDL_Rect position){ //comprobaciones!! actualPosition=position; return true; } /*Devuelve la posición actual a la posición original*/ void Character::returnOriginalPosition(){ setActualPosition(originalPositionAndSize); } /*Mueve la posición del personaje según su movimiento*/ bool Character::move(Character::Movement move){ switch(move){ case UP: actualPosition.y--; break; case DOWN: actualPosition.y++; break; case LEFT: actualPosition.x--; break; case RIGHT: actualPosition.x++; break; } }
En el main, instancio un objeto de cada uno y creo el bucle según el pseudocódigo indicado antes.
Main.cpp: Pulsa para ver/ocultar el código
/* * File: Main.cpp * Author: Dorian Hawkmoon * * Created on 25 de agosto de 2013, 19:12 */ #include "Graphics.h" #include "Character.h" using namespace std; /*Espera por una tecla y devuelve la tecla pulsada*/ SDLKey waitKey(); /* * */ int main(int argc, char** argv) { //creo los gráficos Graphics *screen=new Graphics; //creo un color rojo para el personaje SDL_Color rojo={255,0,0}; //seteo su posicion inicial SDL_Rect position; position.x=50; position.y=50; //creo el personaje con su letra y color y la posición inicial Character *character=new Character('A', rojo, position); //tecla que pulsa el usuario SDLKey key; //bucle del juego bool exit=false; while (exit==false){ //limpio la pantalla screen->clearScreen(); //dibujo el personaje screen->paintCharacter(character); //espero por una tecla key=waitKey(); //si es la de escape, salgo del bucle if( key == SDLK_ESCAPE){ exit=true; }else{ //según la tecla, muevo el personaje switch (key) { case SDLK_UP : character->move(Character::UP); break; case SDLK_DOWN: character->move(Character::DOWN); break; case SDLK_RIGHT: character->move(Character::RIGHT); break; case SDLK_LEFT: character->move(Character::LEFT); break; default: break; } } } return 0; } /*Espera por una tecla y devuelve la tecla pulsada*/ SDLKey waitKey(){ //creo un evento SDL_Event event; //creo la variable de la tecla SDL_keysym key; int done=0; //mientras no haya pulsado tecla... while (done == 0) { //espero por un evento while (SDL_PollEvent(&event)) { //si es una pulsación de tecla... if (event.type == SDL_KEYDOWN){ done = 1; //guardo la tecla a devolver key=event.key.keysym; } } } //devuelvo la tecla return key.sym; }
Lo siguiente es meternos con la colisión. Nacho nos propone como ejercicio controlar cuando el personaje choca contra las paredes y como siguiente apartado, incluir obstáculos.
Los obstáculos son fáciles, los establecemos como personajes. Las paredes me han costado más por el hecho de separar la gráfica del personaje. Al principio lo hice añadiendo una variable más, pero veía que tenía problemas y al final encontré una solución mejor, y es que odio meter todo en el main y me encanta dividir el problema y crear clases. Quiero creer que hago bien con esto.
Lo que hice fue crear otra clase que fuera el escenario. En él, guardo los límites de la pantalla, las listas de los objetos y personajes y controlo las colisiones. También modifiqué Graphics para que el tamaño de la ventana fuera personalizable.
Al final lo mandé todo a la soberana mierda y me voy a contentar con seguir al pie de la letra el manual, que si no, no avanzo. Más adelante me atreveré con C++ y SDL cuando lo tenga un poco más dominado y pueda ir más rápido, que con la chorrada de código que tengo que escribir, vergüenza me dá no haberlo terminado ya.
Aquí el código escrito en C# y Mono. Es el mismo que antes, con tres clases metidas en el mismo fichero.cs y compilado con gmcs. Otro día continúo haciendo mejor código.
game.cs: Pulsa para ver/ocultar el código
using System; public class Game{ public static void Main(){ Game game=new Game(); Point point=new Point(10,10); Character protagonist=new Character('@', point); ConsoleKeyInfo key; bool end=false; while(!end){ //limpiar pantalla Console.Clear(); //dibujar game.paintCharacter(protagonist); //leer tecla key = Console.ReadKey(false); switch(key.Key){ case ConsoleKey.RightArrow: protagonist.move(Moves.RIGHT); break; case ConsoleKey.LeftArrow: protagonist.move(Moves.LEFT); break; case ConsoleKey.UpArrow: protagonist.move(Moves.UP); break; case ConsoleKey.DownArrow: protagonist.move(Moves.DOWN); break; case ConsoleKey.Escape: end=true; break; } } Console.Clear(); } public void paintCharacter(Character element){ int x, y; Point site=element.getActualPoint(); x=(int) site.getPointX(); y=(int) site.getPointY(); Console.SetCursorPosition(x,y); Console.Write(element.getSymbol()); } } public enum Moves { UP, DOWN, LEFT, RIGHT}; public class Point{ private float x; private float y; public Point(float x, float y){ this.x=x; this.y=y; } // propiedades: métodos para manejar los atributos public float getPointX() { return x; } public float getPointY() { return y; } public void setPointY(float value) { y=value; } public void setPointX(float value) { x=value; } public void sumX(float value){ x=x+value; } public void sumY(float value){ y=y+value; } } public class Character{ private char symbol; private Point initialPoint; private Point actualPoint; public Character(char symbol, Point initialPoint){ this.symbol=symbol; this.initialPoint=actualPoint=initialPoint; } public void move(Moves move){ switch(move){ case Moves.RIGHT: actualPoint.sumX(1); break; case Moves.LEFT: actualPoint.sumX(-1); break; case Moves.UP: actualPoint.sumY(-1); break; case Moves.DOWN: actualPoint.sumY(1); break; } } public Point getActualPoint(){ return actualPoint; } public void setActualPoint(Point point){ actualPoint=point; } public Point getInitialPoint(){ return initialPoint; } public char getSymbol(){ return symbol; } }