Realizacrión de un videojuego
sábado, febrero 1, 2003 at 1:00AM Programar un videojuego en Java.
Fecha de creación: 10.01.2003
Revisión 1.0 (30.01.2003)
Raúl Bonachía Castillo
rbonachia AT educaline DOT com
|
Introducción
La intención de estos artículo es proporcionar una técnica para poder organizar la programación de un sencillo videojuego en 2D utilizando J2SE. El propósito es estructurar la mentalidad del programador para atacar el objetivo de emular un videojuego, un juego en movimiento. Para tal fin, desarrollaremos uno sencillo.
La idea es que todo lo escrito sea criticable. Mi experiencia en Java es reciente y seguramente todo sea muy mejorable, pueda optimizarse de alguna manera...etc. Pero como me suele gustar decir, lo importante es ofrecer PASIÓN a lo que dedicas tu tiempo. Con ello quiero animar a quienes puedan aportar conocimientos en estos temas a que escriban algo parecido a lo que se pretende con estos artículos, para favorecer la difusión y aprendizaje del desarrollo de juegos en java, como Java3D...etc Aunque Java no parece ser la plataforma adecuada para desarrollar juegos o videojuegos, ¿es impensable que algún día cambie tal situación?. Opinad e ilustrarnos.
En este artículo vamos a introducir los conceptos básicos que nos permitirán desarrollar un videojuego sencillo en posteriores entregas. Así, al acabar la lectura del presente tutorial habremos creado las siguiente clases (pequeñas y comprensibles): En este artículo vamos a introducir los conceptos básicos que nos permitirán desarrollar un videojuego sencillo en posteriores entregas. Así, al acabar la lectura del presente tutorial habremos creado las siguiente clases (pequeñas y comprensibles):
- Clase Molde
- Clase Dimension2D
- Clase Rectangulo
- Clase Estrella
- Clase Imagen
- Clase Avion
- Clase Disparo
- Interfez Colisionable
- Interfez Movimiento
no acentuaré nunca nombres de métodos ni de clases
No olvidemos nuestro objetivo. Programar un videojuego. Bien, para ello concentrémonos en uno. Necesitaremos:
- Un argumento
- Un componente gráfico en el que se produzca la acción del argumento. (eligiremos el applet).
- Objetos en movimiento que den vida al argumento.
Somos un avión, y nos ataca un numero incierto de aviones enemigos.
¡¡¡Nosotros tenemos que defendernos!!!. Ellos disparan fuego y nosotros tenemos la capacidad de disparar también, siempre en trayectoria horizontal. (Pero claro, eso es modificable. En una mejora del juego, ¿por qué no introducir trayectorias sinusoidales...?. Puede programarse...) Nuestra capacidad de movimiento se limita a ascender y descender en vertical, y a desplazarnos a izquierda y derecha horizontalmente. Nuestros enemigos se desplazan horizontalmente de derecha a izquierda, surgiendo del extremo derecho de la pantalla y desapareciendo por el extremo izquierdo de ella. Si alcanzamos a alguno con un solo disparo, desaparece de la pantalla. ¡Lo hemos eliminado!. Ello debiera suponer una puntuación, preestablecida anteriormente. Ahora bien, si sucede vicerversa, si son ellos lo que nos alcanzan...bueno, seamos generosos con nosotros mismos. Dispondremos de un nivel de vida. No nos eliminarán del juego hasta que hayamos recibido un cierto número de impactos.
Como cualquier batalla que se precie, debemos enmarcarla dentro de una escena amigable. Por ejemplo, una noche tachonada de estrellas. Programaremos estrellas que se desplazan también horizontalmente, independientemente de lo que suceda en el campo de batalla, proporcionando una mayor sensación de movimiento.
Hay ciertos elementos que no deben faltar, tales como avisos de mensajes (suponga que hemos eliminado un elevado número de aviones, como recompensa podríamos quedar inmunes a los disparos enemigos durante un cierto lapso de tiempo), indicándonos ciertas situaciones, un indicador gráfico del nivel de vida ( lo emularemos mediante una corona circular, cuyo color desciende progresivamente a medida que recibimos impactos), sonidos al disparar ...etc. Estos pequeños detalles configuran y dar cuerpo al videojuego, permitiendo que el usuario se sienta más protagonista e identificado con el argumento.
Este breve esquema resulta más que suficiente para clarificar cuál será nuestro objetivo y podría representar la primera pantalla del videojuego. En una segunda pantalla, el protagonista podría ser un tanque, con distintas posiciones para su cañón en un ángulo de 180 grados. Ahora los aviones nos bombardearían y sus proyectiles tendrían desplazamientos verticales.....Durante el desarrollo de este tutorial, en principio nos limitararemos a programar la primera pantalla, pero hecho esto lo demás no debiera tener mayor problema.
He aquí un tema crucial. Vamos a utilizar un applet. Por varias razones. La primera, porque es lo que recomendaba el libro que he seguido. Segundo, porque da la oportunidad de utilizarlo en los navegadores, y tercero porque la herramienta appletviewer permite visualizar casi siempre todos nuestras variaciones sin mayores problemas. Para programar el juego utilizé el IDE Jcreator. Con él u otro editor se hará la tarea más fácil. Si no, si estás empezando y no dispones de ninguno no te preocupes. El block de notas te sirve igualmente.
Alternativas: podría hacerse con otro componente gráfico distinto de un applet. No se alteraría demasiado la programación. Además, daría la oportunidad de utilizar Java Web Star... Una u otra alternativa parecen igualmente razonables. De todas formas, podría ser un buen punto para dar lugar a la discusión.
Como todo esto es muy opinable, yo opto por elegir un applet.
Denoto como objetos en movimiento a todos aquello que van a formar parte del videojuego y que son visibles o no, según la situación. Ejemplo de ellos serían las estrellas, los disparos, los aviones..... Todos ellos tienen un denominador común: son entes pertenecientes al juego, aunque su naturaleza y funcionalidad sean diferentes. Aquí entra el concepto de modularidad. Tenemos que diseñar el programa teniendo en cuenta que debemos poder eliminar o añadir elementos nuevos sin destrozar el código de forma severa. ¿Cómo se consigue esto?.Utilizando adecuadamente clases jerárquicas, extendiendo unas de otras, de tal manera que la relación de herencia nos permita sobreescibir métodos para la aparición de nuevos elementos. Del libro al que se hace referencia en la bibliografía extraje las siguientes ideas para organizar las clases
Una clase madre, que denotaremos Molde. Una instancia de Molde representa cualquier objeto participante en el videojuego, pero en su concepto más abstracto. Contendrá métodos para decidir si es visible o no y si está activo o no. Heredaremos de Molde para construir las clases Dimension2D y Imagen. Estas clases son fundamentales. De Dimension2D obtendremos aquellos objetos que queramos construir y que se visualizarán sin utilizar imágenes, usando procedimientos de pintura de Java. De Imagen obtendremos, en cambio, aquellos objetos que necesitarán una imagen para ser representados. Así, por ejemplo, si necesitamos que aparezca un rectángulo que ascienda y descienda (para simular una maza que aplaste aviones) heredaremos de Dimension2D. Y para simular a los aviones en sí necesitaremos heredar de Imagen, con una clase que se denote MolveAvion, por ejemplo. De Avion haremos que hereden diferentes tipos de aviones, si es necesario. Bombardero, Caza....lo que haga falta, cada uno con la imagen que le caracterice.
En nuestro esfuerzo por adquirir una destreza y una técnica lo más general posible, emplearemos interfaces. Las interfaces se denominarán: ![]()
¿Por qué hacer uso de ellas?. Porque para nosotros, todos los objetos van a ser objetos en movimiento, algunos de ellos con la capacidad de poder intersecar unos con otros. Luego en un procedimiento de abstracción, nos dará igual que sea una estrella que un avión. Lo cierto es que la estrella y el avión se mueven, luego debemos dotarles de métodos para que ACTUALICEN sus posiciones en el componente gráfico. Para eso se utilizan las interfaces: dotan de métodos de nombre común a las clases que las implementan y que deben realizar misiones similares. Veremos posteriormente que son ciertamente utiles en aras de una mayor organización en la programación.
interface Movimiento{
/* Actualizará (modificará) la antigua posición del Molde en cuestión
a la coordenada (x,y) */
public abstract void nuevaPosicion(int x, int y);
/* En caso de necesidad, podemos asignarle mayor velocidad aparente de
movimiento al molde, llamando a este método*/
public abstract void nuevaVelocidad(int x, int y);
}
Aclaraciones: cuando hablamos de colisión de moldes, cada molde está representado por una imagen (si hereda de Imagen), o una figura geométrica pintada por java (si hereda de Dimension2D). Aquí debemos trabajar con una limitación. Para imaginar la colisión de la forma más general posible, debemos considerar siempre el rectángulo más pequeño en el que se inscribe la imagen o figura, respectivamente, y trabajar con las coordenadas de la esquina superior izquierda e inferior derecha para poder hablar de intersección (colisión) de rectángulos, que es mucho más fácil que hablar de otro tipo de colisiones.
interface Colisionable{
/*indica al molde que implemente esta interfaz si ha colisionado con otro molde
ajeno encapsulado por el rectángulo de menor dimensión con bordes paralelos a
los ejes X e Y, de coordenadas (x1,y1) e (x2,y2)*/
public boolean intersecta(int x1, int y1, int x2, int y2);
// Llamaremos a este método si la intersección se ha producido.
public void colision();
}
Para determinar si dos moldes han colisionado, podemos aplicar esta caracterización, que es muy útil.Si { (x1,y1),(x2,y2) } y { (x3,y3),(x4,y4) } conforman los límites de dos moldes ambos intersecan si y solo si: (x2>=x3) && (x4>=x1) && //las x se solapan (y2>=y3)&&(y4>=y1)//las y se solapan

Poco a poco, vayamos diseñando las clases necesarias para poder desarrollar después aquellas que representen los aviones, disparos, estrellas....etc. Mostremos sus relaciones jerárquicas para ponernos en situación:
import java.awt.Graphics;
abstract class Molde{
protected boolean visible; //¿es visible el molde?
protected boolean activo; //¿ Es actualizable el molde?
//metodos abstractos
abstract void pintar(Graphics g);
//metodos basicos;
public boolean esVisible(){
return visible;
}
public void cambiarVisible(boolean b){
visible=b;
}
public boolean esActivo(){
return activo;
}
public void cambiarActivo(boolean b){
activo=b;
}
//Suspende el molde
public void suspender(){
cambiarVisible(false);
cambiarActivo(false);
}
//Reactiva el molde
public void restablecer(){
cambiarVisible(true);
cambiarActivo(true);
}
}
Los métodos son bastante claros. Con ellos determinamos si la clase que herede de Molde, y que representará un objeto visual, está visible y/o activo y nos damos la posiblidad de modificarlo cuando sea necesario. Además, contamos con dos métodos BÁSICOS, como son pintar y actualizar. Todos los objetos heredados de molde serán supceptibles de ser pintados, (de eso se trata...), y deberán actualizarse. Este concepto es FUNDAMENTAL. Cada objeto que pertenezca al videojuego estará en movimiento, aunque esté parado en algún instante. Eso significa que deberemos ACTUALIZAR su posición, es decir, modificarla. Esto habrá que hacerlo para todos los objetos. Aunque pueda parecer un poco complicado a priori (si hay mucho objetos...), se solucionará utilizando unas clases Controladoras. Habrá una ControladoraAviones, otra ControladoraDisparos...etc, que se encargarán de actualizar A LA VEZ, todos los objetos que controlen. Así, el repintado se realizará sin mayores problemas.
Poco a poco....
import java.awt.Color;
abstract class Dimension2D extends Molde{
protected int locx;//coordenada X del molde
protected int locy;//coordenada Y del molde
Color color;
boolean relleno;
//si tiene color de relleno o no
public boolean obtenerRelleno(){
return relleno;
}
public void cambiarRelleno(boolean b){
relleno=b;
}
public void cambiarColor(Color c){
color=c;
}
public Color obtenerColor(){
return color;
}
}
locx, locy representarán las coordenadas en el componente gráfico seleccionado del molde en cuestión. Tener en cuenta que todo videojuego, este en particular, depende continuamente de las posiciones y, por tanto, de las coordenadas. Habrá que hacer uso de estas variables continuamente, tanto aquí como en Imagen.
Para observar cómo utilizar la clase Dimension2D construyamos la clase Rectangulo.
import java.awt.Graphics;
import java.awt.Color;
class Rectangulo extends Dimension2D implements Movimiento,Colisionable{
public int anchura,altura;
//CONSTRUCTOR de Rectangulo
public Rectangulo(int x,int y,int anchura, int altura,Color c){
locx=x;
locy=y;
this.anchura=anchura;
this.altura=altura;
color=c; //Inicializamos variables de Dimension2D
relleno=false;
restablecer();
cambiarRelleno(true);
}
/* cambiarPosición es fundamental para ACTUALIZAR la posición.
( actualizar=modificar)*/
public void nuevaPosicion(int x, int y){
locx=x;
locy=y;
}
/* si en algún momento deseamos INCREMENTAR la ACTUALIZACION y, por tanto,
incrementar la velocidad aparente de movimiento.*/
public void nuevaVelocidad(int vx, int vy){
locx+=vx;
locy+=vy;
}
// Para saber si intersecta con un avion, un disparo...etc
public boolean intersecta(int x1, int y1, int x2, int y2){
return visible&&(x2>=locx)
&&(locx+anchura>=x1)&&(y2>=locy)
&&(locy+altura>=y1);
}
public void colision (){
suspender();
/*Si le damos la función de aplastar aviones, lo lógico es que
desaparezca al intersecar con uno.*/
}
public void cambiarOrigen(int x,int y){
locx=x;
locy=y; //Método Propio de Rectangulo
}
public void actualizar(int an,int al){//Método Propio de Rectangulo
anchura=an;
altura=al;
}
public void pintar(Graphics g){
if (visible){
g.setColor(color);
if (relleno){
g.fillRect(locx,locy,anchura,altura);
}
else{
g.drawRect(locx,locy,anchura,altura);
}
}
}
}
Con Rectangulo obtenemos un molde Dimension2D que define a un rectángulo: sus coordenadas x,y, respecto a la esquina superior izquierda por él definida, y su anchura y altura. Además, le asignamos un color y le proporcionamos la posibilidad de ACTUALIZAR su posición gracias a la interfaz Movimiento y de PINTARLO en un componente gráfico. Por otra parte, es supceptible de colisionar con otros objetos al implementarle la interfaz Colisionable. Paciencia, todavía no hemos realizado el código necesario para ver en acción su utilidad. Para ello necesitaremos un hilo dentro del applet, pero esto lo veremos en el/los siguientes artículos. Sigamos viendo más código.
Ahora, veamos cómo construir la clase Estrella, que representa a una estrella.
import java.awt.Color;
import java.awt.Graphics;
import java.util.Random;
public class Estrella extends Dimension2D implements Movimiento{
Random r;
int aleatorio;
boolean apagar;
Estrella(int x, int y){
r=new Random();
this.apagar=apagar;
/*para ofrecer vistosidad, otorgaremos color aleatorio a cada Estrella*/
color=new Color(r.nextInt(255),r.nextInt(255),r.nextInt(255));
cambiarVisible(true);// Estrella nacerá visible
locx=x;
locy=y;
}
// cambiarPosición es fundamental para ACTUALIZAR la posición.
public void nuevaPosicion(int x, int y){
locx=x;
locy=y;
}
/* si en algún momento deseamos INCREMENTAR la ACTUALIZACION y, por tanto,
incrementar la velocidad aparente de movimiento.*/
public void nuevaVelocidad(int vx, int vy){
locx+=vx;
locy+=vy;
}
void pintar(Graphics g){
g.setColor(color);
/* Un punto se puede representar como una recta de mismo origen y final.*/
g.drawLine(locx,locy,locx,locy);
}
}
Aquí introducimos un aspecto fundamental que forma parte de cualquier videojuego: aleatoriedad. La clase Random (java.util.Random) proporcina números aleatorios. En concreto, el método nextInt(n) obtiene un número cualesquiera entre 0 y n. A la estrella vamos a darle color aleatorio. Volveremos a utilizarlo posteriormente.
import java.awt.Image;
import java.awt.Graphics;
class Imagen extends Molde{
protected int locx; //coordenada x respecto esquina superior izqda.
protected int locy; // coordenada y respecto esquina superior dcha
protected int anchura,altura; //dimensiones de la imagen
protected Image imagen; // la imagen
//CONSTRUCTORES
/*Construimos la imagen con coordenadas (0,0) en su esquina superior izquierda*/
public Imagen(Image i, int anchura, int altura){
locx=0;
locy=0;
imagen=i;
if (imagen!=null){
this.anchura=anchura;//obtener anchura de la imagen
this.altura=altura;// obtener altura de la imagen
}
restablecer();
}
/*Construimos la imagen con coordenadas (x,y) en su esquina superior izquierda */
public Imagen(int x, int y,Image i, int anchura, int altura){
locx=x;
locy=y;
imagen=i;
if (imagen!=null){
this.anchura=anchura;//obtener anchura de la imagen
this.altura=altura;// obtener altura de la imagen
}
restablecer();
}
public void actualizar(){}
public void pintar(Graphics g){
if (visible){
g.drawImage(imagen,locx,locy,anchura,altura,null);
}
}
}
Detengámonos con esta clase, porque es muy importante y de ella heredarán todos los objetos (aviones y disparos) de los que hará gala el videojuego. Lograremos pintar la imagen que deseemos, sea cual sea, en el applet que va a a ser soporte de nuestro videojuego. Así, en el applet original, cargaremos las imágenes que consideremos oportunas y a la hora de crear los objetos que hereden de Imagen se los pasaremos al constructor.
Veamos ahora ya cómo construir la clase Avion. Notemos ante de ello que será necesario implementarle la interfaz Colisionable, lo que posibilitará que un objeto de Avión detecte colisiones con objetos en movimiento de diferente naturaraleza como otros aviones, disparos, rectángulos....Percatarse de la importancia y el poder que ello proporcionará al videojuego. Por supuesto, también hay que dotarle de la interfaz Movimiento. He considerado oportuno poder dimensionar la imagen a la hora de dibujarla con g.drawImage(...,anchura,altura,null). Si se observase cierta lentitud en el juego es posible que esa fuese una de las causa. En ese caso intentad sustituir esa línea por g.drawImage(imagen,locx,locy,null) dotándole, pues, de su dimensión real.
import java.applet.Applet;
import java.applet.AudioClip;
import java.awt.Image;
import java.awt.Graphics;
public class Avion extends Imagen implements Colisionable,Movimiento{
protected AudioClip audio;
//CONSTRUCTOR de Avion
public Avion(Image avion,AudioClip audio, int anchura, int altura){
super(avion,anchura,altura); //llama al constructor de Imagen
this.audio=audio; /* proporciona un sonido que utilizaremos cuando deseemos.*/
suspender(); /*cuando creamos el objeto Avion, no está activo.*/
}
// nuevaPosición es fundamental para ACTUALIZAR la posición.
public void nuevaPosicion(int x, int y){
locx=x;
locy=y;
}
/* si en algún momento deseamos INCREMENTAR la ACTUALIZACION y, por tanto,
incrementar la velocidad aparente de movimiento.*/
public void nuevaVelocidad(int vx, int vy){
locx+=vx;
locy+=vy;
}
/* devuelve cierto si un objeto delimitado por el rectángulo de coordenadas
(x1,y1),(x2,y2) interseca con nuestro Avion*/
public boolean intersecta(int x1, int y1, int x2, int y2){
return visible&&(x2>=locx)
&&(locx+anchura>=x1)&&(y2>=locy)
&&(locy+altura>=y1);
}
// Si nuestro Avion ha muerto, sonará un sonido y lo desactivaremos.
public void colision(){
audio.play();
suspender();
}
}
Notemos la pecualiaridad de esta clase: le hemos dotado de sonido. El sonido proporciona credibilidad y aumenta la interacción con el usuario. No deberían faltar este tipo de cuestiones, así que nosotros no vamos a ser menos. Ahora bien, procuremos que no ocupe mucho memoria el que elijamos. Podremos utilizar extensiones .wav, .au..etc.
import java.awt.Image;
public class Disparo extends Imagen implements Movimiento,Colisionable{
/* CONSTRUCTOR. */
public Disparo(Image disparo, int anchura, int altura){
super(disparo,anchura,altura);
suspender();/*Los disparos nacerán inactivos y no visibles.
Estarán en la recámara de los aviones*/
}
public void nuevaPosicion(int x, int y){
locx=x;
locy=y;
}
public void nuevaVelocidad(int vx, int vy){
locx+=vx;
locy+=vy;
}
public boolean intersecta(int x1, int y1, int x2, int y2){
return visible&&(x2>=locx)
&&(locx+anchura>=x1)&&(y2>=locy)
&&(locy+altura>=y1);
}
public void colision (){
suspender();/*Si un disparo alcanza a un avión aquel debe desaparecer.*/
}
}
Bien, llegados a este punto ya disponemos de las clases necesarias para simular a los elementos del videojuego, como son las estrellas, los aviones y los disparos que efectúen los aviones enemigos y el protagonista. Todos disponen de métodos para ACTUALIZAR su posición y algunos métodos para detectar colisiones.
Ahora, el siguiente paso será construir las clases Controladoras que hagan nacer a todos aquellos objetos en movimiento y determinen tanto las interacciones entre ellos como sus actualizaciones.
Necesitaremos pues a:
- Clase ControladorAviones
- Clase ControladorDisparos
- Clase ControladorEstrellas
Podremos añadir, posteriormente, para dar vistosidad al juego, una clase que emule una corona circular y que represente el nivel de vida del protagonista.
Podríamos, también, incluir una presentación, y dotar al juego de un menu incluyendo el applet en una frame (ventana), para Iniciar Juego, Finalizar Juego...e incluso, como meta final, dotarle de la opción Guardar Partida, haciendo uso de una Base de Datos. Eso lo decidiremos en el/los posteriores artículos.
Eso sí, más tarde, necesitaremos construir el applet principal mediante un hilo y dotarle de métodos de eventos de teclado para que el usuario pueda interaccionar con el videojuego. Queda mucho por hacer, pero si este artículo os han interesado seguro que vuestra PASIÓN hará el resto. ;).
Recursos
[1]
Programación de juegos en Java. (Black Art of Java in Game Programming),
Joel Fan,Eric Ries, Callin Tenitchi,
Acerca del autor
Raúl Bonachía Castillo
Raúl Bonachía Castillo (Ruly). Licenciado en Matemáticas por la U.R,
y un gran apasionado por el desarrollo de juegos en java, y deseoso
por emular juegos multijugador en red de calidad en esa plataforma.
Actualmente trabaja en el desarrollo de un proyecto para la creación
de un sistema de edición de acceso universal para la empresa Educaline S.L
j2se 
Reader Comments