Universidad Nacional del Litoral Facultad de Ingeniería y Ciencias Hídricas Departamento de Informática

Ingeniería Informática PROGRAMACIÓN ORIENTADA A OBJETOS

Introducción al desarrollo de Videojuegos
Pablo Abratte
pabratte@gmail.com
Última Revisión: 16/11/11

3).5). se diseñarán las clases encargadas de representar la lógica del juego. (2. Figura 2. independientemente de cualquier tipo de interface. (3.2). Figura 1. Luego. manejo de eventos y otros aspectos específicos de un videojuego. En la Figura 1 puede observarse una captura del juego terminado. nos adentraremos en los detalles de implementación de gráficos. Pág 1 de 12 . (2.2). la víbora será representa como una lista de las celdas que la misma ocupa dentro de la matriz.4). Matriz utilizada para representar un nivel del juego. éstas son las celdas: (2. Captura de clon del juego Snake. utilizaremos una matriz de enteros en la que cada celda tomará distintos valores según esté vacía. en el ejemplo de la Figura 2. represente una pared o contenga comida. Por otro lado. En primer lugar. etc. (2. Diseño del videojuego Snake El videojuego que desarrollaremos es un clon del clásico y famoso Snake que consiste en una víbora que se desplaza a través de un nivel o laberinto evitando chocar con las paredes y consumiendo comida que hace aumentar su longitud. Para representar los niveles del juego. como puede observarse en la Figura 2.Introducción al desarrollo de videojuegos En este texto abordaremos el desarrollo completo de un videojuego simple para explicar cuestiones básicas relacionadas con la implementación de los mismos.

la víbora es representada mediante una lista de las celdas que ocupa en la matriz que representa al nivel. Dejarémos el análisis de la clase juego para la sección final y veremos primero las clases encargadas de representar el juego a nivel lógico. y poseerá un método para averiguar el contenido de una celda determinada. Figura 3.En la Figura 3 se observa el diagrama de clases propuesto para el diseño del juego. Como mencionamos anteriormente. la responsabilidad de generar comida en posiciones aleatorias de la matriz que se encuentren vacías. a continuación. Cada celda es identificada mediante dos enteros que indican su posición (fila y columna) dentro de la matriz. algunos de los detalles de implementación de la clase Vibora. Para simplificar el almacenamiento y la operatoria de dichas celda. La clase tendrá. además. moverla. Pág 2 de 12 . y conocer su longitud o las celdas que ocupa dentro del nivel. La declaración e implementación de dicha clase es mostrada a continuación a continuación. Diagrama de clases del diseño del juego. El constructor de la clase Nivel leerá desde un archivo de texto la matriz que especificará cuales celdas están vacías y cuales corresponden a secciones de las paredes. Mostraremos. La clase Juego estará compuesta de objetos de tipo Vibora y Nivel y será la encargada de manejar tanto la lógica del juego como la entrada y salida del mismo. crearemos una estructura llamada Coord2i para representarlas. saber si aún sigue viva. Los métodos de la clase Vibora permitirán cambiar su dirección de movimiento.

que deberá ser pasado al constructor y será utilizado para preguntar el contenido de las celdas antes de moverse. los operadores de suma y comparación. class Vibora { private: bool estaViva.struct Coord2i{ int x. int j):x(i). vector<Coord2i> celdas. no ha colisionado con el nivel o consigo misma. Coord2i operator+(const Coord2i &c). bool ChocaraConSigoMisma().. void CambiarDireccion(Coord2i nuevaDir). IZQUIERDA. Pág 3 de 12 . ABAJO. guarda un puntero al nivel actual. Coord2i direccionActual.. bool operator==(const Coord2i &c). DERECHA.y. La dirección actual y la próxima dirección en la que se realizará el movimiento serán de tipo Coord2i y el usuario podrá especificar la dirección del movimiento mediante llamadas a CambiarDirección(.x. la función Mover() se encargará de hacer efectivo el movimiento. La operación de suma será útil para encontrar celdas vecinas a partir de una celda determinada y un desplazamiento.y(j){ } // permite sumar coordenadas componente a componente // devuelve un objeto de tipo Coord2i con // la suma de las coordenadas Coord2i Coord2i::operator+(const Coord2i &c){ return Coord2i(x+c. La clase no manejará la velocidad del movimiento ya que la misma será responsabilidad de la clase Juego.) que podrá ser llamada pasándole como parámetro alguno de los cuatro valores constantes dispuestos por la clase. // constructor que recibe los valores de las coordendas Coord2i::Coord2i(int i. Esta clase posee un vector dinámico con las celdas que la víbora ocupa dentro del nivel.y). mientras que la comparación servirá para revisar que. }. Coord2i(int i. } La estructura posee dos valores enteros para representar la fila y columna de una celda y se han sobrecargado. y. proxDireccion. bool EstaViva(). Nivel *nivel. } // devuelve si las coordendas de las celdas son iguales // comparando componente a componente bool Coord2i::operator==(const Coord2i &c){ return x==c. int j). que se encuentra en la posición 0.x && y==c. y+c. la víbora no choque con ninguna de las celdas que la conforman. public: static const static const static const static const Coord2i Coord2i Coord2i Coord2i ARRIBA. ordenadas desde la cola. void Mover(). a la celda de la cabeza que ocupa la ultima posición del vector. al moverse. Otras funciones de la clase permiten conocer el largo de la vibora y si aún esta viva. Además. A su vez. Vibora(Nivel *n). es decir. además. En el recuadro de código siguiente puede observarse la declaración de la clase Vibora.

Como se mencionó anteriormente. así hasta llegar a la cabeza. y las celdas restantes simplemente la seguirán. pasando por las mismas posiciones que esta. muere if(ChocaraConSigoMisma()){ estaViva=false. if(contenidoProxCelda==Nivel::PARED){ // en la prox posicion hay una pared estaViva=false. El siguiente código corresponde a la función encargada del movimiento de la vivora. Pág 4 de 12 . return. nivel->ConsumirComida(proxCelda). Este procedimiento es ilustrado en la Figura 4. el cual será explicado a continuación. Dicha dirección indicará el movimiento de la cabeza la cabeza de la vibora. dicha función será llamada por el juego cuando sea el momento de que la víbora se mueva. direccionActual=proxDireccion. } // averiguamos el contenido de la proxima celda int contenidoProxCelda=nivel->QueHayEnCelda(proxCelda).unsigned Largo().-1). Vibora::ABAJO(0. Vibora::DERECHA(1. la cual será desplazada a la nueva posición que se encuentra vacía. En un caso normal. Coord2i GetCelda(unsigned i).1).0). void Vibora::Mover(){ if(!estaViva) return.push_back(proxCelda). // calcula la celda a la que se movera la cabeza Coord2i proxCelda(celdas[celdas. const const const const Coord2i Coord2i Coord2i Coord2i Vibora::ARRIBA(0. return. Vibora::IZQUIERDA(-1. el usuario puede especificar la dirección del movimiento llamando a la función CambiarDirección(. Figura 4.. Secuencia de movimiento de la víbora.) con alguna de las cuatro constantes de dirección especificadas por la clase. // si va a chocar con si misma. }.size()-1]+direccionActual).. }else if(contenidoProxCelda==Nivel::COMIDA){ // hay comida en la proxima celda // la vibora aumenta su tamano agregando una celda celdas. cada una de las celdas empezando por la de la cola toma la posición de la celda que le sigue en la secuencia. Uno de los procedimientos más importantes es el algoritmo de movimiento de la vibora. en el que la cabeza de la vibora se desplaza hacia una posición libre.0).

agregando una nueva celda en el próximo lugar al que se desplazará. se realiza el procedimiento de movimiento descrito anteriormente recorriendo las celdas de la víbora con un bucle.c_str()). la víbora aumenta su tamaño en celdas. Si por el contrario hay comida en la próxima. Para observar otros detalles y el resto de las funciones de la clase. Si en la nueva posición ya existe otra celda de la víbora.size()-1]=proxCelda.. // genera la comida GenerarNuevaComida(). El tamaño de los niveles es fijo. Las funciones de la clase Nivel son bastante sencillas. j++){ entrada>>nivel[i][j]. la víbora debe morir ya que colisionará con el nivel. } } entrada. Luego se calculará cual es la próxima casilla a la que la cabeza de la vibora debe moverse sumando la celda actual de la cabeza con el desplazamiento especificado por la dirección de movimiento. j<50. } celdas[celdas. Al consumir comida. resta consultar al nivel por el contenido de la próxima celda. En caso de que la víbora pueda mover sin colisionar con ella misma. Finalmente.i++){ celdas[i]=celdas[i+1]. exit(-1). esta debe morir ya que colisiona consigo misma. } } La función actualiza la dirección actual del movimiento según la variable proxDireccion.. se le debe indicar al nivel que la misma será consumida por la víbora para que la borre y genere nueva comida en otra posición. puede consultarse el código completo del juego.i<celdas. la función miembro ChocaConsigoMisma() realiza esta comprobación comparando las coordenadas de la nueva celda con las de cada una de las celdas existentes utilizando el operador == de la clase Coord2i. Si en dicha celda se encuentra una pared. if(!entrada){ cout<<"ERROR: No se encontro el archivo "<<archivoNivel<<endl. sin necesidad de desplazar las anteriores. } Introducción al desarrollo de videojuegos con SFML Pág 5 de 12 .close(). i<37.}else{ // la posicion siguiente esta vacia // mueve cada celda un lugar hacia adelante for(unsigned i=0. Nivel::Nivel(const string &archivoNivel) { // abre el archivo y comprueba la correcta apertura ifstream entrada(archivoNivel.size()-1. i++){ for(int j=0. que será alterada por el usuario llamando a la función CambiarDirección(.). } // lee el nivel desde el archivo a la matriz for(int i=0. El siguiente cuadro de código muestra el constructor de la clase. si la posición que se debe ocupar está vacía. encargado de leer desde un archivo de texto el contenido de cada celda y generar la primer celda de comida en una posición aleatoria.

Pág 6 de 12 . La cantidad de veces por segundo que la imagen del juego se actualiza (y por ende la cantidad de veces que el bucle se ejecuta) se conoce como tasa de frames por segundo o FPS. etc. Luego de la inicialización. También se inicializan. por ejemplo la colisión con una celda con comida. la actualización y el redibujado.org) es una biblioteca orientada al desarrollo de videojuegos que provee acceso a dispositivos gráficos.). la adquisición de determinado item o la colisión con un enemigo. objetos que intervengan en la lógica del juego. el juego entra en un bucle que repite tres conjuntos principales de acciones: el manejo de eventos. En los párrafos siguientes se explicará con mayor detalle la implementación cada una de las etapas mencionadas. En la gran mayoría de los casos el estado del juego es actualizado teniendo en cuenta el tiempo transcurrido. lo cual hace más sencilla su utilización y nos permite aplicar muchos conceptos abordados durante el cursado de la materia. Una de sus principales ventajas es ser orientada a objetos. se encuentra disponible para múltiples plataformas y diversos lenguajes de programación y provee muchas funcionalidades de alto nivel que resultan útiles a la hora de crear videojuegos. de entrada y muchas otras utilidades. En la etapa de inicialización se crea la ventana de juego y se cargan todos los recursos necesarios (imágenes. Además.sfml-dev. la mayoría de los cuales corresponden a la entrada por parte del usuario del usuario. de sonido. sonidos. más fluido se visualizará el juego.En esta sección se explicarán algunos conceptos y técnicas utilizadas en el desarrollo de videojuegos en general y se mostrará como completar la implementación del videojuego diseñado anteriormente utilizando la biblioteca SFML (Simple and Fast Multimedia Library) SFML (www. La biblioteca es software libre y se encuentra en desarrollo continuo. El ciclo de vida de un videojuego genérico puede observarse en la Figura 5. este valor es utilizado generalmente como una medida de la performance del juego ya que cuanto mayor sean los FPS. consolidándose como una de las más utilizadas. El primer paso de una iteración es el procesamiento de los eventos. Algunos ejemplo de estos son presionar teclas. soltarlas o mover el ratón. esto es muy importante ya que permite que un juego se actualice siempre a la misma velocidad sin importar la potencia de la máquina en la que esté ejecutándose. En la etapa de actualización se modifica el estado del juego en base a la entrada del usuario adquirida en la etapa anterior y otras variables referentes al juego. La última etapa consiste en redibujar en la pantalla los objetos objetos que conforman el juego. de ser necesario.

al nivel y a una ventana. SFML crea automáticamente la ventana de dibujo. explicaremos su utilización más adelante. El constructor también se encarga de cargar las imágenes que serán utilizadas para dibujar el juego. class Juego { private: Vibora *miVibora. En el recuadro de código siguiente se observa la declaración de la clase que representa al juego. Pág 7 de 12 . }. El constructor se encargará de la inicialización de estos componentes. Ciclo de vida de un videojuego. sf::Image imgPared. void ProcesarEventos(). a la cual se le pedirá la entrada del usuario y sobre la cual se dibujará el juego. imgPoo. public: Juego(). También posee como atributos las imágenes que serán utilizadas en la representación del juego. Al inicializar el objeto apuntado por el puntero ventana. Nivel *miNivel. void Dibujar(). void ActualizarEstado(). imgVibora. void Correr(). ~Juego(). La clase posee como atributos punteros al objeto víbora.Figura 5. El código correspondiente al constructor de la clase Juego se muestra debajo. mientras que la función Correr() realizará el bucle principal del juego llamando a las tres funciones privadas que se corresponden con las etapas mencionadas anteriormente. sf::RenderWindow *ventana. el mismo se encarga de la inicialización de la ventana creando un nuevo objeto de tipo sf::RenderWindow al cual se le especifica el tamaño deseado mediante un objeto de tipo sf::VideoMode y el título de la ventana.

En la Figura 6 puede observarse la ventana de juego y varios puntos representados sobre la misma.. el bucle while se encarga de quitar uno a uno los eventos de la cola.LoadFromFile("poo.png").400).png").) quita un evento de la cola y lo coloca en la variable e que es de tipo sf::Event.LoadFromFile("pared. ya que puede haber Pág 8 de 12 . esta consiste en consultar a la ventana en cada iteración del juego por los eventos ocurridos. Para cada evento primero se debe averiguar si es del tipo que nos interesa.Juego::Juego(){ // crea la ventana del juego ventana=new sf::RenderWindow(sf::VideoMode(600. es conveniente destacar que todas las clases de la biblioteca SFML se encuentran dentro del espacio de nombres sf.LoadFromFile("vibora. los eventos ocurridos son almacenados en una cola de la cual es necesario ir extrayéndolos uno a uno y realizar las acciones pertinentes si coinciden con el tipo de evento buscado. imgPoo.niv"). un tipo definido especialmente para representar eventos. El origen de coordenadas se encontrará en la esquina superior izquierda de la ventana. al principio de cada archivo en el que se utilice algún componente de SFML. miVibora=new Vibora(miNivel). En el recuadro de código que sigue se observa la implementación de la función ProcesarEventos(). Además."POO Snake"). // carga las imagenes imgPared. La función GetEvent(. Para procesar los eventos SFML utiliza una técnica conocida como polling. y la posición de cualquier punto estará medida en función de la distancia positiva a este origen. En este caso hemos preferido la primer alternativa para que a simple vista resulte más fácil identificar cuáles clases son las que pertenecen a la biblioteca. imgVibora.. Figura 6. A la hora de dibujar sobre la ventana. Sistema de coordenadas de la ventana de juego. Los máximos valores de las coordenadas estarán limitados por las dimensiones de la ventana especificadas en el momento de su creación. la función devuelve verdadero cuando efectivamente pudo desencolarse un evento o falso si la cola de eventos está vacía. por lo cual es necesario anteponer el prefijo sf:: al nombre de cada clase correspondiente a la biblioteca o colocar la sentencia using namespace sf. } En este punto.png"). // inicializa la vibora y el nivel miNivel=new Nivel("nivel. De esta manera. Para esto. debe tenerse en cuenta el sistema de coordenadas utilizado por la misma.

Type == e. case sf::Key::D: miVibora->CambiarDireccion(Vibora::DERECHA). En este caso. una tecla presionada. La variable tiempoAcumulado permite acumular el tiempo transcurrido en cada actualización para poder conocer el momento en el que debe moverse la víbora.Code){ case sf::Key::A: miVibora->CambiarDireccion(Vibora::IZQUIERDA). un movimiento del ratón.php.Key. para actualizar el estado del juego es necesario tener en cuenta el tiempo transcurrido entre las sucesivas actualizaciones para que el juego funcione a la misma velocidad en cualquier computadora. se cambia la dirección de la víbora. los eventos que nos interesan manejar son las teclas presionadas.6/window-events.eventos de diversos tipos (por ejemplo. etc). Para conocer este valor. break. // sacamos el proximo evento while(ventana->GetEvent(e)) { // pregunta si el evento es una presion de tecla if(e. Puede utilizarse el valor devuelto por esta función para mover a la víbora solo cuando haya pasado un determinado lapso de tiempo. // mientras queden eventos en la cola. break. Para esto se realiza nuevamente una comparación entre un campo del evento que indica el código de la tecla correspondiente y constantes de SFML que representan las teclas deseadas. En el recuadro de código siguiente se muestra el código de la función encargada de actualizar el estado del juego. se hubiera podido lograr el mismo comportamiento declarándola como atributo de la clase. void Juego::ProcesarEventos(){ sf::Event e. La dirección de movimiento de la víbora ya habrá sido ajustada en la función ProcesarEventos(). En cada actualización el tiempo transcurrido es adicionado a dicha variable y cuando se cumple un lapso de 0. Una vez se conoce que el evento es efectivamente la pulsación de una tecla. puede utilizarse la función GetFrameTime() de la clase sf::RenderWindow. una tecla soltada. break. Pág 9 de 12 . Esta función devuelve un real con la cantidad de segundos transcurridos desde la última actualización de la ventana. La variable es declarada como estática para que no se destruya al terminar la función y conserve su valor entre llamada y llamada.org/tutorials/1. break.2 segundos. case sf::Key::W: miVibora->CambiarDireccion(Vibora::ARRIBA). por lo que se compara el campo Type del evento con una constante que representa al tipo buscado (tecla presionada).sfmldev.KeyPressed){ // pregunta cual es la tecla y mueve la vibora switch(e. es tiempo de mover a la víbora y vol ver hacia atrás el acumulador de tiempo. case sf::Key::S: miVibora->CambiarDireccion(Vibora::ABAJO). En función de la tecla que haya sido presionada. Para mayores detalles sobre el manejo de eventos en SFML puede consultarse el tutorial: http://www. } } } } Como se dijo anteriormente. resta averiguar cuál es dicha tecla.

pero posee además otras propiedades que definen completamente su representación.2 segundos // y movemos la vibora if(tiempoAcumulado>=0. etc. miVibora->Mover(). Primero debe borrarse todo el contenido de la ventana. si posee funciones para dibujar un sprite (que debe estar previamente asociado a alguna imagen). un píxel es el elemento mínimo en que puede separase una imagen.. El dibujado de la víbora es muy similar al realizado con el nivel..) de la clase ventana especificando el color mediante un objeto de tipo sf::Color. SFML nos provee de las clases sf::Image y sf::Sprite. // actualizamos el acumulador con el tiempo // transcurrido desde la ultima actualización tiempoAcumulado+=ventana->GetFrameTime(). } } Para la representación gráfica de los elementos del juego. es un punto que tiene un único color. // reseteamos el acumulador cada 0. La distinción entre imagen y sprite es muy importante. El proceso de representar al nivel mediante una matriz de imágenes más pequeñas se conoce generalmente como tiled rendering o tile map. A diferencia de las imágenes. El proceso de redibujado consiste básicamente en tres pasos.. Para el dibujado del sprite se utiliza la función miembro Draw(. Por otro lado. por ejemplo su posición en la pantalla. su rotación. La primera de estas clase sirve para representar imágenes. los sprites son livianos ya que no almacenan grandes volúmenes de datos.) de la clase sf::RenderWindow. En siguiente fragmento de código se observa la función Dibujar() de la clase Juego.void Juego::ActualizarEstado(){ // un acumulador de tiempo para saber cuando mover static float tiempoAcumulado=0. En el caso del nivel se recorre cada celda y se pregunta por su contenido. mientras que las propiedades de los sprites varían continuamente. un sprite es un objeto que tiene una representación gráfica dada por una imagen. void Juego::Dibujar(){ // limpia la ventana Pág 10 de 12 . En el recuadro de código siguiente se muestra la implementación de la función Dibujar() de la clase Juego. se la dibuja utilizando un sprite al que se le asigna una imagen correspondiente a dicho contenido (pared o comida) y se lo posiciona en la pantalla teniendo en cuenta su ubicación en la matriz y el tamaño de la imagen (recordar el sistema de coordenadas utilizado por la ventana). El tercer y último paso del redibujado es hacer efectivos los cambios en la ventana llamando a la función Display() para que la misma sea actualizada. las imágenes son cargadas una única vez desde el disco y rara vez son modificadas. en caso de no estar vacía. Esto se logra llamando a la función miembro Clear(. pintándola de color negro.2){ tiempoAcumulado-=0. En segundo lugar debe realizarse el dibujado de los objetos mediante sprites. en este caso. sino únicamente propiedades sobre la representación de la imagen a la que están asociados. SFML no permite dibujar una imagen en pantalla ya que muchas de sus propiedades no están definidas (por ejemplo su posición o tamaño). Las imágenes son recursos que ocupan generalmente bastante memoria y son costosos de manipular. su tamaño. pero utilizando una única imagen. Una imágen es un arreglo bidimensional de píxeles..2. Por otro lado.

i<37.0. posiciona y dibuja cada celda for(int i=0.GetWidth(). i++){ for(int j=0.net/ Pág 11 de 12 .255)). posiciona el sprite.SetImage(imgPoo). } } } // asigna al sprite la imagen para las celdas del cuerpo de la vibora s.SetImage(imgPared). // recorre las celdas de la vibora. ventana->Draw(s).sfml-dev. if(contenidoCelda==1) s.GetWidth(). // recorre las celdas del nivel for(int i=0.ventana->Clear(sf::Color(0. Diseño del videojuego Snake • • • Documentación de SFML: www. i++){ celda=miVibora->GetCelda(i). así como el template de proyecto para videojuegos que la utilicen. j++){ // pregunta por el contenido de la celda int contenidoCelda=miNivel->QueHayEnCelda(Coord2i(j.x*imgVibora. j<50. Para apreciar los detalles restantes puede analizarse el código completo del ejemplo.SetPosition(celda. Coord2i celda(0.GetHeight()).org/tutorials/1.0. i)).6/ Tutoriales de SFML: www.SetPosition(j*imgPared. se recomienda recurrir a la documentación de la biblioteca SFML para una referencia completa de las clases y funcionalidades provistas por la biblioteca.y*imgVibora. i<miVibora->Largo(). y a los tutoriales para una introducción a la utilización básica de los principales componentes de la misma la misma.sfml-dev. celda.SetImage(imgVibora). s.org/documentation/1.6/ Paquete SFML para Zinjai: http://zinjai. } // actualiza la ventana ventana->Display(). // en caso de no estar vacia. if(contenidoCelda==2) s. // crea el sprite para dibujar el nivel y la vibora sf::Sprite s. pueden encontrarse en la sección descargas de la página de ZinjaI.0). i*imgPared. tanto del diseño de su lógica como los aspectos específicos correspondientes a un videojuego. } Hemos analizado el desarrollo completo de un pequeño videojuego. // le asigna la imagen correspondiente y la dibuja if(contenidoCelda!=0){ s.sourceforge. Asímismo. El paquete con las cabeceras y binarios de la biblioteca para ZinjaI.GetHeight()). ventana->Draw(s).

Sign up to vote on this title
UsefulNot useful