Cuando jugamos a un juego parece que todo pasa a la vez, en el mismo instante, sin embargo, sabemos que un procesador sólo puede realizar una acción a la vez. La clave es realizar cada una de las acciones tan rápidamente como sea posible y pasar a la siguiente, de forma que todas se completen antes de visualizar el siguiente frame del juego.
El "game loop" o bucle de juego es el encargado de "dirigir" en cada momento que tarea se está realizando. En la figura 6.1. podemos ver un ejemplo de game loop, y aunque más o menos todos son similares, no tienen por que tener exactamente la misma estructura. Analicemos el ejemplo.
Lo primero que hacemos es leer los dispositivos de entrada para ver si el jugador ha realizado alguna acción. Si hubo alguna acción por parte del jugador, el siguiente paso es procesarla, esto es, actualizar su posición, disparar, etc..., dependiendo de qué acción sea. En el siguiente paso realizamos la lógica de juego, es decir, todo aquello que forma parte de la acción y que no queda bajo control del jugador, por ejemplo, el movimiento de los enemigos, cálculo de trayectoria de sus disparos, comprobación de colisiones entre la nave enemiga y la del jugador, etc... Fuera de la lógica del juego quedan otras tareas que realizamos en la siguiente fase, como son actualizar el scroll de fondo (si lo hubiera), activar sonidos (si fuera necesario), realizar trabajos de sincronización, etc.. Ya por último, nos resta volcar todo a la pantalla y mostrar el siguiente frame. Esta fase es llamada "fase de render".
Normalmente, el game loop tendrá un aspecto similar a lo siguiente:
int done = 0;
while (!done) {
// Leer entrada
// Procesar entrada
// Lógica de juego
// Otras tareas
// Mostrar frame
}
Antes de que entremos en el game loop, tendremos que realizar múltiples tareas, como inicializar todas las estructuras de datos, etc...
El siguiente ejemplo es mucho más realista. Está implementado en un thread.
public void run() {
iniciar();
while (true) {
// Actualizar fondo de pantalla
doScroll();
// Actualizar posición del jugador
computePlayer();
// Actualizar pantalla
repaint();
serviceRepaints();
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
System.out.println(e.toString());
}
}
}
Lo primero que hacemos es inicializar el estado del juego. Seguidamente entramos en el bucle principal del juego o game loop propiamente dicho. En este caso, es un bucle infinito, pero en un juego real, tendríamos que poder salir usando una variable booleana que se activara al producirse la destrucción de nuestro avión o cualquier otro evento que suponga la salida del juego.
Ya dentro del bucle, lo que hacemos es actualizar el fondo de pantalla -en la siguiente sección entraremos en los detalles de este proceso-, a continuación, calculamos la posición de nuestro avión para posteriormente forzar un repintado de la pantalla con una llamada a repaint() y serviceRepaints(). Por último, utilizamos el método sleep() perteneciente a la clase Thread para introducir un pequeño retardo. Este retardo habrá de ajustarse a la velocidad del dispositivo en que ejecutemos el juego.
Movimiento del avión.- Para mover nuestro avión utilizaremos, como comentamos en la sección dedicada a la lectura del teclado, los métodos keyPressed() y keyReleased(). Concretamente, lo que vamos a hacer es utilizar dos variables para almacenar el factor de incremento a aplicar en el movimiento de nuestro avión en cada vuelta del bucle del juego. Estas variables son deltaX y deltaY para el movimiento horizontal y vertical, respectivamente.
public void keyReleased(int keyCode) {
int action=getGameAction(keyCode);
switch (action) {
case LEFT:
deltaX=0;
break;
case RIGHT:
deltaX=0;
break;
case UP:
deltaY=0;
break;
case DOWN:
deltaY=0;
break;
}
}
public void keyPressed(int keyCode) {
int action=getGameAction(keyCode);
switch (action) {
case LEFT:
deltaX=-5;
break;
case RIGHT:
deltaX=5;
break;
case UP:
deltaY=-5;
break;
case DOWN:
deltaY=5;
break;
}
}
Cuando pulsamos una tecla, asignamos el valor correspondiente al desplazamiento deseado, por ejemplo, si queremos mover el avión a la derecha, el valor asignado a deltaX será 5. Esto significa que en cada vuelta del game loop, sumaremos 5 a la posición de avión, es decir, se desplazará 5 píxeles a la derecha. Cuando se suelta la tecla, inicializamos a 0 la variable, es decir, detenemos el movimiento.
La función encargada de calcular la nueva posición del avión es, pues, bastante sencilla.
void computePlayer() {
// actualizar posición del avión
if (hero.getX()+deltaX>0 && hero.getX()+deltaX<getWidth() &&
hero.getY()+deltaY>0 && hero.getY()+deltaY<getHeight()) {
hero.setX(hero.getX()+deltaX);
hero.setY(hero.getY()+deltaY);
}
}
Simplemente sumamos deltaX a la posición X del avión y deltaY a la posición Y. Antes comprobamos que el avión no sale de los límites de la pantalla.
Construyendo el mapa del juego.- En nuestro juego vamos a utilizar una técnica basada en tiles para construir el mapa. La traducción de la palabra tile es baldosa o azulejo. Esto nos da una idea de en qué consiste la técnica: construir la imagen a mostrar en la pantalla mediante tiles de forma cuadrada, como si enlosáramos una pared. Mediante tiles distintos podemos formar cualquier imagen. La siguiente figura pertenece al juego Uridium, un juego de naves o shooter de los años 80 parecido al que vamos a desarrollar para ordenadores de 8 bits.
Las líneas rojas dividen los tiles empleados para construir la imagen.
En nuestro juego manejaremos mapas sencillos que van a estar compuestos por mosaicos de tiles simples. Algunos juegos tienen varios niveles de tiles (llamados capas). Por ahora, vamos a almacenar la información sobre nuestro mapa en un array de enteros tal como éste:
int map[]= {1,1,1,1,1,1,1,
1,1,1,1,1,1,1,
1,2,1,1,1,1,1,
1,1,1,4,1,1,1,
1,1,1,1,1,1,1,
1,1,3,1,2,1,1,
1,4,1,1,1,1,1};
Este array representa un mapa de 7x7 tiles. Vamos a utilizar los siguientes tiles, cada uno con un tamaño de 32x32 píxeles.
Para cargar y manejar los tiles nos apoyamos en la librería de manejo de sprites que desarrollamos en el capítulo anterior.
private Sprite[] tile=new Sprite[5];
// Inicializamos los tiles
for (i=1 ; i<=4 ; i++) {
tile[i]=new Sprite(1);
tile[i].on();
}
tile[1].addFrame(1,"/tile1.png");
tile[2].addFrame(1,"/tile2.png");
tile[3].addFrame(1,"/tile3.png");
tile[4].addFrame(1,"/tile4.png");
Hemos creado un array de sprites, uno por cada tile que vamos a cargar.
El proceso de representación del escenario consiste en ir leyendo el mapa y dibujar el sprite leído en la posición correspondiente. El siguiente código realiza este proceso:
// Dibujar fondo
for (i=0 ; i<7 ; i++) {
for (j=0 ; j<7 ; j++) {
t=map[i*xTiles+j];
// calculo de la posición del tile
x=j*32;
y=(i-1)*32;
// dibujamos el tile
tile[t].setX(x);
tile[t].setY(y);
tile[t].draw(g);
}
}
El mapa es de 7x7, así que los dos primero bucles se encargan de recorrer los tiles. La variable t, almacena el valor del tile en cada momento. El cálculo de la coordenada de la pantalla en la que debemos dibujar el tile tampoco es complicada. Al tener cada tile 32x32 píxeles, sólo hay que multiplicar por 32 el valor de los contadores i o j, correspondiente a los bucles, para obtener la coordenada de pantalla.
Scrolling.- Con lo que hemos visto hasta ahora, somos capaces de controlar nuestro avión por la pantalla, y mostrar el fondo del juego mediante la técnica de los tiles que acabamos de ver. Pero un fondo estático no es demasiado atractivo, además, un mapa de 7X7 no da demasiado juego, necesitamos un mapa más grande. Para el caso de nuestro juego será de 7X20.
// Mapa del juego
int map[]= {1,1,1,1,1,1,1,
1,1,1,1,1,1,1,
1,2,1,1,1,1,1,
1,1,1,4,1,1,1,
1,1,1,1,1,1,1,
1,1,3,1,2,1,1,
1,1,1,1,1,1,1,
1,4,1,1,1,1,1,
1,1,1,1,3,1,1,
1,1,1,1,1,1,1,
1,4,1,1,1,1,1,
1,1,1,3,1,1,1,
1,1,1,1,1,1,1,
1,1,1,1,1,1,1,
1,2,1,1,1,1,1,
1,1,1,4,1,1,1,
1,1,1,1,1,1,1,
1,1,3,1,2,1,1,
1,1,1,1,1,1,1,
1,4,1,1,1,1,1};
Se hace evidente que un mapa de 7x20 tiles no cabe en la pantalla. Lo lógico es que según se mueva nuestro avión por el escenario, el mapa avance en la misma dirección. Este desplazamiento del escenario se llama scrolling. Si nuestro avión avanza un tile en la pantalla hemos de dibujar el escenario pero desplazado (offset) un tile. Desafortunamente, haciendo esto exclusivamente veríamos como el escenario va dando saltitos, y lo que buscamos es un scroll suave. Necesitamos dos variables que nos indiquen a partir de que tile debemos dibujar el mapa en pantalla (la llamaremos indice) y otra variable que nos indique, dentro del tile actual, cuánto se ha desplazado (la llamaremos indice_in). Las variables xTile y yTile contendrán el número de tiles horizontales y verticales respectivamente que caben en pantalla. El siguiente fragmento de código cumple este cometido:
void doScroll() {
// movimiento del escenario (scroll)
indice_in+=2;
if (indice_in>=32) {
indice_in=0;
indice-=xTiles;
}
if (indice <= 0) {
// si llegamos al final, empezamos de nuevo.
indice=map.length-(xTiles*yTiles);
indice_in=0;
}
}
El código para dibujar el fondo quedaría de la siguiente manera:
// Dibujar fondo
for (i=0 ; i<yTiles ; i++) {
for (j=0 ; j<xTiles ; j++) {
t=map[indice+(i*xTiles+j)];
// calculo de la posición del tile
x=j*32;
y=(i-1)*32+indice_in;
// dibujamos el tile
tile[t].setX(x);
tile[t].setY(y);
tile[t].draw(g);
}
}
Como diferencia encontramos dos nuevas variables. La variable indice contiene el desplazamiento (en bytes) a partir del cual se comienza a dibujar el mapa. La variable indice_in, es la encargada de realizar el scroll fino. Al dibujar el tile, se le suma a la coordenada Y el valor de la variable indice_in, que va aumentando en cada iteración (2 píxeles). Cuando esta variable alcanza el valor 32, es decir, la altura del tile, ponemos la variable a 0 y restamos el número de tiles horizonlates del mapa a la variable indice, o lo que es lo mismo, el offset a partir del que dibujamos el mapa. Se realiza una resta porque la lectura del mapa la hacemos de abajo a arriba (del último byte al primero). Recuerda que el mapa tiene 7 tiles de anchura, es por eso que restamos xTiles (que ha de valer 7) a la variable indice. Una vez que llegamos al principio del mapa, comenzamos de nuevo por el final, de forma que se va repitiendo el mismo mapa de forma indefinida. Vamos a suponer que el dispositivo es capaz de mostrar 8 líneas de tiles verticalmente (yTiles vale 8). Si puede mostrar menos, no hay problema alguno. El problema será que la pantalla pueda mostrar más, es decir, sea mayor de lo que hemos supuesto. En ese caso aumentaremos la variable yTiles. Un valor de 8 es lo suficientemente grande para la mayoría de los dispositivos. Ten cuenta que las primeras 8 filas del mapa tienen que ser iguales que las 8 últimas si no quieres notar un molesto salto cuando recomienza el recorrido del mapa.
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
public class Scrolling extends MIDlet implements CommandListener {
private Command exitCommand;
private Display display;
private SSCanvas screen;
public Scrolling() {
display=Display.getDisplay(this);
exitCommand = new Command("Salir",Command.SCREEN,2);
screen=new SSCanvas();
screen.addCommand(exitCommand);
screen.setCommandListener(this);
new Thread(screen).start();
}
public void startApp() throws MIDletStateChangeException {
display.setCurrent(screen);
}
public void pauseApp() {}
public void destroyApp(boolean unconditional) {}
public void commandAction(Command c, Displayable s) {
if (c == exitCommand) {
destroyApp(false);
notifyDestroyed();
}
}
}
class SSCanvas extends Canvas implements Runnable {
private int indice_in, indice, xTiles, yTiles, sleepTime;
private int deltaX,deltaY;
private Sprite hero=new Sprite(1);
private Sprite[] tile=new Sprite[5];
// Mapa del juego
int map[] ={ 1,1,1,1,1,1,1,
1,1,1,1,1,1,1,
1,2,1,1,1,1,1,
1,1,1,4,1,1,1,
1,1,1,1,1,1,1,
1,1,3,1,2,1,1,
1,1,1,1,1,1,1,
1,4,1,1,1,1,1,
1,1,1,1,3,1,1,
1,1,1,1,1,1,1,
1,4,1,1,1,1,1,
1,1,1,3,1,1,1,
1,1,1,1,1,1,1,
1,1,1,1,1,1,1,
1,2,1,1,1,1,1,
1,1,1,4,1,1,1,
1,1,1,1,1,1,1,
1,1,3,1,2,1,1,
1,1,1,1,1,1,1,
1,4,1,1,1,1,1};
public SSCanvas() {
// Cargamos los sprites
hero.addFrame(1,"/hero.png");
// Iniciamos los Sprites
hero.on();
}
void iniciar() {
int i;
sleepTime = 50;
hero.setX(getWidth()/2);
hero.setY(getHeight()-20);
deltaX=0;
deltaY=0;
xTiles=7;
yTiles=8;
indice=map.length-(xTiles*yTiles);
indice_in=0;
// Inicializamos los tiles
for (i=1 ; i<=4 ; i++) {
tile[i]=new Sprite(1);
tile[i].on();
}
tile[1].addFrame(1,"/tile1.png");
tile[2].addFrame(1,"/tile2.png");
tile[3].addFrame(1,"/tile3.png");
tile[4].addFrame(1,"/tile4.png");
}
void doScroll() {
// movimiento del scenario (scroll)
indice_in+=2;
if (indice_in>=32) {
indice_in=0;
indice-=xTiles;
}
if (indice <= 0) {
// si llegamos al final, empezamos de nuevo.
indice=map.length-(xTiles*yTiles);
indice_in=0;
}
}
void computePlayer() {
// actualizar posición del avión
if (hero.getX()+deltaX>0 && hero.getX()+deltaX<getWidth() &&
hero.getY()+deltaY>0 && hero.getY()+deltaY<getHeight()) {
hero.setX(hero.getX()+deltaX);
hero.setY(hero.getY()+deltaY);
}
}
// thread que contiene el game loop
public void run() {
iniciar();
while (true) {
// Actualizar fondo de pantalla
doScroll();
// Actualizar posición del jugador
computePlayer();
// Actualizar pantalla
repaint();
serviceRepaints();
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
System.out.println(e.toString());
}
}
}
public void keyReleased(int keyCode) {
int action=getGameAction(keyCode);
switch (action) {
case LEFT:
deltaX=0;
break;
case RIGHT:
deltaX=0;
break;
case UP:
deltaY=0;
break;
case DOWN:
deltaY=0;
break;
}
}
public void keyPressed(int keyCode) {
int action=getGameAction(keyCode);
switch (action) {
case LEFT:
deltaX=-5;
break;
case RIGHT:
deltaX=5;
break;
case UP:
deltaY=-5;
break;
case DOWN:
deltaY=5;
break;
}
}
public void paint(Graphics g) {
int x=0,y=0,t=0;
int i,j;
g.setColor(255,255,255);
g.fillRect(0,0,getWidth(),getHeight());
g.setColor(200,200,0);
// Dibujar fondo
for (i=0 ; i<yTiles ; i++) {
for (j=0 ; j<xTiles ; j++) {
t=map[indice+(i*xTiles+j)];
// calculo de la posición del tile
x=j*32;
y=(i-1)*32+indice_in;
// dibujamos el tile
tile[t].setX(x);
tile[t].setY(y);
tile[t].draw(g);
}
}
// Dibujar el jugador
hero.setX(hero.getX());
hero.setY(hero.getY());
hero.draw(g);
}
}
|
1. Flash. Programación de juegos Tutorial de Flash , es lo que podrás aprender hoy en nuestro nuevo curso de... [24/06/11] |
257 |
|
|||
|
2. Programación en C++ C++ es un potente lenguaje de programación que apareció en 1980, continuando con... [04/06/04] |
69.596 | ||||
|
3. C# Curso de Programación Curso Básico de programación en C#, este curso básico abarcará desde las bases del... [14/07/05] |
15.672 | ||||