[Aporte] Desarrollo Videojuego Java [Muy Basico]

Iniciado por 3n31ch, 4 Febrero 2015, 02:24 AM

0 Miembros y 1 Visitante están viendo este tema.

3n31ch

Saludos gente, en este aporte busco dar un primer paso para el desarrollo de videojuegos 2D de escritorio en Java.

Quiero dar entender que esta no es la mejor manera para desarrollar un videojuego en java y que tanpoco representa una arquitectura completa. Con esto ultimo me refiero a que el proyecto que mostrare de ejemplo no esta terminado y faltan piezas importantes para ser viable. (La razón por la cual no hice un proyecto completo es porque no busco imponer la forma en la cual programo, y a que no puedo encontrar un patrón ideal para el desarrollo de este tipo de proyectos ya que son muy variados).

Si gustan pondré mas ejemplos y ahondare mas en el tema a futuro, pero como no se cual es el real interés de este foro para este tipo de proyectos no explicare con mucho detalle el presente documento.

Se recomienda saber como funcionan los elementos Thread, JFrame y JPanel de java para poder comprender este documento.

En el presente documento se trataran los siguientes puntos:

  • Como controlar los gráficos (Muy básico, no se hablaran de sprites ni nada por el estilo).
  • Como controlar los FPS. (Intentare detallar en el tema)
  • Como recibir datos del teclado.

Este documento utilizara herramientas de las librerías java.awt y javax.swing (no se utilizara JavaFX).

Lo único que se hará en este proyecto es dibujar un simple cuadrado el cual se moverá por la pantalla utilizando teclado.



Empecemos creando un proyecto (no importa el nombre que le des). crea un paquete (nuevamente no importa el nombre) y posterior crea dos clases:

GameEsta contendrá el método de inicio donde se creara la ventana del juego.
GamePanelEsta clase sera el motor principal del videojuego en donde se dibujaran los gráficos y captara los datos del teclado.

Primero trabajaremos con la clase Game la cual contendrá el siguiente código:

Código (Java) [Seleccionar]

package net.elhacker.game;

import javax.swing.JFrame; /* Importamos JFrame necesario para crear la ventana */

public class Game {

   public static void main(String[] args) {
       JFrame window = new JFrame("Title"); /* Creamos la ventana del juego */
       window.setContentPane(new GamePanel()); /* Establecemos el panel del juego */
       window.setResizable(false); /* Bloqueamos el tamaño de la ventana */
       window.pack(); /* Ajustamos el tamaño de la ventana al tamaño del juego */
       window.setLocationRelativeTo(null); /* Colocamos la ventana en el centro */
       window.setVisible(true); /* Hacemos la ventana visible */
       window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); /* Indicamos que al cerrar la ventana finaliza el proceso */
   }
   
}


No te preocupes es normal que de error en este momento el método setContentPane

Bien, explicare los aspectos mas importantes de esta clase:

El método setContentPane nos permite cambiar el panel por defecto de JFrame por nuestro panel en el cual dibujaremos los gráficos del videojuego.

Establecemos en el método setResizable que sea imposible alterar el tamaño del JFrame (A menos que lo hagamos nosotros por código) Este método inhabilita la opción de agrandar la ventana.

El método pack nos permite ajustar el tamaño de la ventana al tamaño del panel. (De esta manera no tendremos que preocuparnos de las medidas del panel)

el método setLocationRelativeTo(null) nos ahorrara mucho trabajo a la hora de posicionar la ventana de nuestro juego en medio. (Con este método nos ahorramos el tener que hacer cálculos para posicionar la ventana en medio de la pantalla)

En este momento window.setContentPane(new GamePanel()) da error debido a que GamePanel no es aun un panel, ahora trabajaremos con GamePanel y arreglaremos esto.

El código de GamePanel es el siguiente (No te preocupes se que es extenso pero intentare explicar cada  una de las partes por separado.)

Código (java) [Seleccionar]
package net.elhacker.game;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JPanel;

public class GamePanel extends JPanel implements Runnable, KeyListener {
   
   public static final int GAME_WIDTH = 640; /* Ancho sin escala del panel */
   public static final int GAME_HEIGHT = 480; /* Largo sin escala del panel */
 
   
   private int expectedFps = 60; /* FPS esperados */
   private Thread gameThread; /* Thread del juego  */
   
   private final Rectangle rectangle = new Rectangle(0,0,32,32); /* Rectangulo blanco */
                                                                 /* Los primeros dos valores son las cordenadas */
                                                                 /* Los siguientes dos son el ancho y largo */
   
   
   public GamePanel() {
       this.setPreferredSize(new Dimension(
               GamePanel.GAME_WIDTH, GamePanel.GAME_HEIGHT)); /* Se establece el tamaño del juego */
       this.gameThread = new Thread(this); /* inicializamos el thread del juego */
   }

   /*
    * Thread que controla los FPS del juego
    */
   
   @Override
   public void run() {
       long start;        
       long elapsed;      
       long wait;
       while(true){
           start = System.nanoTime();  /* Tomamos tiempo de inicio */
           this.repaint();
           elapsed = System.nanoTime(); /* Tomamos tiempo de fin */
           wait = 1000/expectedFps - (elapsed-start)/1000000; /* Utilizamos formula de FPS */
           wait = (wait < 0)? 0 : wait;  /* Prevenimos posibles errores */
           try {
               Thread.sleep(wait); /* Pausamos el thread */
           } catch (InterruptedException ex) { /* Reportar Error */ }
           
       }
   }
   
   
   /*
    * Este metodo inicializa el motor del juego
    */
   
   private void init(){
       this.gameThread.start();    /* iniciamos el motor del juego */
       this.addKeyListener(this);  /* Hacemos que el juego capte las teclas del teclado */
       this.setFocusable(true); /* Hacemos que sea posible hacer un focus a la ventana */
       this.requestFocus();    /* Establecemos el foco al juego */
   }
   
   /*
    * Este metodo se activa al hacer visible el juego
    */
   
   @Override
   public void addNotify() {  
       super.addNotify();
       init(); /* se inicia el motor del juego */
   }
   
   /*
    * Metodo para pintar
    */
   
   @Override
   public void paintComponent(Graphics g) {
       g.setColor(Color.DARK_GRAY); /* Seleccionamos el color gris oscuro */
       g.fillRect(0, 0, GamePanel.GAME_WIDTH,
               GamePanel.GAME_HEIGHT); /* Pintamos el fondo gris oscuro */
       
       g.setColor(Color.WHITE); /* Seleccionamos el color blanco */
       g.fillRect(rectangle.x, rectangle.y, rectangle.width, rectangle.height); /* Pintamos el rectangulo */
   }
   
   
   

   @Override
   public void keyTyped(KeyEvent e) { }

   
   /*
    * Metodo de escucha que nos permite realizar acciones dependiendo de las
    * teclas presionadas
    */
   @Override
   public void keyPressed(KeyEvent e) {
       switch(e.getExtendedKeyCode()){
           case KeyEvent.VK_LEFT:
               rectangle.x-=32;
               break;
           case KeyEvent.VK_RIGHT:
               rectangle.x+=32;
               break;
           case KeyEvent.VK_UP:
               rectangle.y-=32;
               break;
           case KeyEvent.VK_DOWN:
               rectangle.y+=32;
               break;
       }
   }

   @Override
   public void keyReleased(KeyEvent e) {
   
   }


Partamos por esta parte:

Código (java) [Seleccionar]
public class GamePanel extends JPanel implements Runnable, KeyListener

Heredamos de JPanel el cual nos permitirá transformar nuestra clase a una clase tipo Container (GamePanel hereda de container) la cual repara el error de window.setContentPane(new GamePanel()). GamePanel tiene un método de doble buffer el cual nos permitirá controlar mas fácilmente las imágenes.

También implementamos Runnable el cual utilizaremos para crear el Thread que permitirá controlar los FPS de nuestro juego e implementamos KeyListener el cual nos permitirá escuchar datos del teclado.

Bien Ahora veamos los atributos de nuestra clase:

Código (java) [Seleccionar]
public static final int GAME_WIDTH = 640; /* Ancho sin escala del panel */
public static final int GAME_HEIGHT = 480; /* Largo sin escala del panel */

private int expectedFps = 60; /* FPS esperados */
private Thread gameThread; /* Thread del juego  */

private final Rectangle rectangle = new Rectangle(0,0,32,32); /* Rectangulo blanco */
                                                             /* Los primeros dos valores son las cordenadas */
                                                             /* Los siguientes dos son el ancho y largo */


GAME_WIDTH y GAME_HEIGHT serán las que indicaran respectivamente el ancho y largo de la ventana.

expectedFps representaran a los FPS esperados (FPS = Frames Per Second)

gameThread sera el thread que utilizaremos para repintar nuestro juego cada X milisegundos utilizando.

rectangle sera en este caso nuestro cuadrado blanco que se moverá por la pantalla (representando a un elemento del juego). Los dos primeros valores representan las cordenadas y los dos ultimos el ancho y largo.

Posterior a esto declaramos nuestro constructor en donde define el ancho y largo de nuestra ventana. Ten en cuenta que esto lo hacemos por el método PreferredSize y no por el método setSize (esto es necesario ya que pack() funciona teniendo en cuenta el tamaño preferido, no el real). Y en el mismo contructor crearemos nuestro Thread señalando que se utilizara la misma clase como Thread.

Código (java) [Seleccionar]
public GamePanel() {
   this.setPreferredSize(new Dimension(
           GamePanel.GAME_WIDTH, GamePanel.GAME_HEIGHT)); /* Se establece el tamaño del juego */
   this.gameThread = new Thread(this); /* inicializamos el thread del juego */
}


Ahora definiremos el método run (el cual implementamos de runnable)

Código (java) [Seleccionar]
@Override
   public void run() {
       long start;        
       long elapsed;      
       long wait;
       while(true){
           start = System.nanoTime();  /* Tomamos tiempo de inicio */
           this.repaint();
           elapsed = System.nanoTime(); /* Tomamos tiempo de fin */
           wait = 1000/expectedFps - (elapsed-start)/1000000; /* Utilizamos formula de FPS */
           wait = (wait < 0)? 0 : wait;  /* Prevenimos posibles errores */
           try {
               Thread.sleep(wait); /* Pausamos el thread */
           } catch (InterruptedException ex) { /* Reportar Error */ }
           
       }
   }
   
   
Nos detendremos un poco acá para explicar un par de cosas:

Las películas, videojuegos o cualquier animación no son mas que un montón de fotografías pasadas a una gran velocidad. La velocidad por la cual pasan estas fotografías es medida en FPS(Framies Per Second) (Cuadros Por Segundo)  y  la velocidad optima son unos 60 fotografías por segundo en el caso de los videojuegos.

Entonces nuestro thread tiene como objetivo pintar 60 fotografías por segundo, para esto se utiliza un algoritmo que explicare a continuación.

bien te explico, utilizando el metodo repaint() obligamos a nuestro panel volver a pintarse, luego utilizando Thread.sleep() hacemos que nuestro Thread se detenga por una cantidad minúscula de tiempo ya que si no hiciéramos esto el Thread no pararía de pintar a la mayor velocidad posible. Lo cual ocasionaría problemas de rendimiento (Esto tenemos que evitarlo. Claro, porque despues dicen que java es lento) pues bien aqui es cuando entra el concepto de FPS, ¿Cuanto tiempo es necesario que duerma nuestro Thread para lograr que se pinte 60 veces por segundo?

Sabiendo que Thread.sleep() recibe como parámetro milisegundos usaremos la siguiente formula 1000/60. De esta manera cada unos 16.6 milisegundos nuestro programa pintara un nuevo cuadro (esto medido en 1 segundo serán 60 cuadros).

Código (java) [Seleccionar]
while (true) {
this.repaint();
Thread.sleep(1000/60);
}


Pero si fuera tan fácil porqué a algunos videojuegos le cuesta tanto llegar a los 60 cuadros por segundo?

Pues esto es porque la formula no termina acá. nos ha faltado algo importante. Y es que no tuvimos el cuenta la cantidad de tiempo que se demora en repintar nuestro panel.

Te daré un ejemplo simple. Imagina un pintor. Este pintor tiene por obligación pintar un cuadro todos los días.
Su obligación es empezar a pintar a las 8AM y se puede ir a dormir cuando termine de pintar el cuadro.

Si el primer día se demora 12 horas en pintar el cuadro el hombre podrá dormir 12 horas.
Si el segundo día se demora 10 horas en pintar el cuadro entonces el pintor dormirá 14 horas.
Pero si el tercer día se demora 23 horas en pintar el cuadro, el pobre hombre solo podrá dormir una hora.

Pues bien, si nuestro programa se demora 3 milisegundos en pintar el cuadro entonces tendremos que restar esos 3 milisegundos a los 16.6 milisegundos lo que daria un total de 13.6 milisegundos. El problema es que nosotros no sabremos cuanto se demora en pintar el cuadro ya que esto depende de cuantos recursos disponibles hay en el sistema (si la computadora esta ejecutando el antivirus mientras juegan nuestro juego, lo mas natural es que nuestro juego no funcione al 100% de velocidad)

Para saber cuanto se demora en pintar el cuadro necesitaremos tomar el tiempo antes de pintar, y después de pintar el cuadro luego hacer una resta y obtendremos el tiempo. Este tiempo lo restamos a los 1000/60 y obtendremos el tiempo real en que nuestro thread puede dormir.

Si te fijas bien para prevenir errores verifique que el tiempo nunca sea menor a 0.

Código (java) [Seleccionar]

start = System.nanoTime();  /* Tomamos tiempo de inicio */
this.repaint();
elapsed = System.nanoTime(); /* Tomamos tiempo de fin */
wait = 1000/expectedFps - (elapsed-start)/1000000; /* Utilizamos formula de FPS */
wait = (wait < 0)? 0 : wait;  /* Prevenimos posibles errores */
try {
   Thread.sleep(wait); /* Pausamos el thread */
} catch (InterruptedException ex) { /* Reportar Error */ }


sleep puede causar errores, por eso se utiliza un try-catch

Posterior mente declararemos el método init, en el cual pondremos todo lo necesario para que nuestro juego sea funcional y visible, para esto iniciamos el Thread previamente declarado, y hacemos que la ventana pueda escuchar las teclas presionadas en el teclado.

Código (java) [Seleccionar]
private void init(){
   this.gameThread.start();    /* iniciamos el motor del juego */
   this.addKeyListener(this);  /* Hacemos que el juego capte las teclas del teclado */
   this.setFocusable(true); /* Hacemos que sea posible hacer un focus a la ventana */
   this.requestFocus();    /* Establecemos el foco al juego */
}


(para que el juego capte las teclas del teclado tenemos que hacer que sea posible hacer focus en el panel, si tienes una duda respecto a esto comentalo e intentare responderte a la brevedad.)

La siguiente parte no es nada complicada, solo redefinimos el metodo addNotify (Este método se activa cuando hacemos el juego visible) y agregamos nuestro metodo init() el cual a su vez hará que nuestro Thread se ejecute.

Código (java) [Seleccionar]
@Override
public void addNotify() {  
   super.addNotify();
   init(); /* se inicia el motor del juego */
}


Ahora redefinimos el método paintComponent que es el encargado de dibujar los gráficos de nuestro juego.

Código (java) [Seleccionar]
@Override
public void paintComponent(Graphics g) {
   g.setColor(Color.DARK_GRAY); /* Seleccionamos el color gris oscuro */
   g.fillRect(0, 0, GamePanel.GAME_WIDTH,
           GamePanel.GAME_HEIGHT); /* Pintamos el fondo gris oscuro */
   
   g.setColor(Color.WHITE); /* Seleccionamos el color blanco */
   g.fillRect(rectangle.x, rectangle.y, rectangle.width, rectangle.height); /* Pintamos el rectangulo */
}


Graphics funciona como si utilizaras paint, primero le indicas el color y luego lo que quieres hacer, en este caso primero indicamos el color gris oscuro y pintamos un cuadrado del mismo tamaño que nuestro panel (de esta manera hacemos un fondo oscuro), luego seleccionamos el color blanco y pintamos nuestro rectángulo.

Ahora solo es necesario redefinir los métodos del teclado (cada vez que se apreté una tecla se hará algo que nosotros queramos). En este caso solo redefiniremos keyPressed (este se activa al presionar una tecla)

Código (java) [Seleccionar]
/*
* Metodo de escucha que nos permite realizar acciones dependiendo de las
* teclas presionadas
*/
@Override
public void keyPressed(KeyEvent e) {
   switch(e.getExtendedKeyCode()){
       case KeyEvent.VK_LEFT:
           rectangle.x-=32;
           break;
       case KeyEvent.VK_RIGHT:
           rectangle.x+=32;
           break;
       case KeyEvent.VK_UP:
           rectangle.y-=32;
           break;
       case KeyEvent.VK_DOWN:
           rectangle.y+=32;
           break;
   }
}

@Override
public void keyReleased(KeyEvent e) { }



Con ayuda de un switch y case haremos cada caso posible.

  • Si se aprieta la direccional izquierda nuestro cuadro se mueve 32 pixeles a la izquierda
  • Si se aprieta la direccional derecha nuestro cuadro se mueve 32 pixeles a la derecha
  • Si se aprieta la direccional arriba nuestro cuadro se mueve 32 pixeles a la arriba
  • Si se aprieta la direccional abajo nuestro cuadro se mueve 32 pixeles a la abajo

Ten en cuenta que los pixeles se miden desde el extremo superior izquierdo de la pantalla por esta razón tienes que restar para ir hacia arriba y sumar para ir hacia abajo.

Intentalo tu:

  • Actualmente el cuadrado puede salir de los bordes, intenta evitar que esto sea posible.
  • Intenta crear un segundo cuadra que se mueva con las teclas ASDW
  • Intenta crear una cuadricula amarilla que se se haga visible al apretar la tecla c, y si se apreta nuevamente esta se haga invisible otra vez

Espero la tutorial les haya sido de ayuda. Estoy pensando en hacer un vídeo ya que entiendo que puede ser difícil de entender con tan solo esto. Por otro lado, para los mas experimentados, se que esta tutorial es simple y que no abordo algunos temas importante, pero la verdad es que no se como sera recibido por la comunidad así que no quería hacer algo muy complejo.

PabloPbl

Buenísimo, excelente tutorial para lo que quieren entrarse en este mundo de la programación de juegos, aunque ya se algo de esto, pero siempre se aprende algo nuevo, mas tarde paso y le pego un ojo.
Animo y haz mas aportes como estos, un saludo  ;D.

3n31ch

Dependiendo de como lo reciba el foro haré mas, porque son muy extensas y por esa razón quizas no muchos las quieran leer. Saludos.

PabloPbl

#3
Cita de: Nac-ho en  6 Febrero 2015, 04:04 AM
Dependiendo de como lo reciba el foro haré mas, porque son muy extensas y por esa razón quizas no muchos las quieran leer. Saludos.

Agradecería muchísimo que hicieras un tutorial mas avanzado del tema, me ayudaría muchísimo, ya que justo estoy en eso(Creación de juegos), y de lo extenso que sea no me importa, me gusta leer, un saludo  ;)

luchi

Yo pienso que a los que nos interesan estos temas no nos importa leer. De todas formas, si te sientes más cómodo puedes hacer vídeos cuando tengas tiempo.

3n31ch

Tengo un proyecto con @Gus Garsaky para hacer video-tutoriales, pero el aun no tiene micro y yo aun estoy trabajando en algunas cosas.

Maurice_Lupin

Me gustó el ejemplo del pintor. Prefiero los tutoriales escritos. Sólo porque pensan muchos menos que un video y traen más info, pero cada quien con sus gustos.

Saludos.
Un error se comete al equivocarse.

3n31ch

Se me ocurrió jugando mario bros... ni idea porque. Pero el tutorial se creo en base a ese ejemplo :xD

rob1104

Hola, me pareció muy interesante el tutorial, me gustaría aprender un poco mas, nunca había hecho algo así en java (aunque si tengo algo de conocimiento del lenguaje). Aqui esta mi codigo modificado con las sugerencias que propones, suena que se está formando un interesante y minimalista juego de estrategia  :P

Código (java) [Seleccionar]
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JPanel;

public class GamePanel extends JPanel implements Runnable, KeyListener {

public static final int GAME_WIDTH = 640;
public static final int GAME_HEIGHT = 480;

private int expectedFps = 60;
private Thread gameThread;

private final Rectangle rectangle = new Rectangle(0, 0, 32, 32);
private final Rectangle rectangle2 = new Rectangle(0, GAME_HEIGHT - 32, 32, 32);

private boolean existenCuadros = false;

public GamePanel() {
this.setPreferredSize(new Dimension(GAME_WIDTH, GAME_HEIGHT));
this.gameThread = new Thread(this);
}

@Override
public void run() {
long start;
long elapsed;
long wait;
while(true) {
start = System.nanoTime();
this.repaint();
elapsed = System.nanoTime();
wait = 1000/expectedFps - (elapsed-start)/1000000;
wait = (wait < 0) ? 0 : wait;
try {
Thread.sleep(wait);
} catch(InterruptedException ex) {

}
}
}

private void init() {
this.gameThread.start();
this.addKeyListener(this);
this.setFocusable(true);
this.requestFocus();
}

@Override
public void addNotify() {
super.addNotify();
init();
}

private void dibujaCuadros(Graphics g) {

}

@Override
public void paintComponent(Graphics g) {
g.setColor(Color.DARK_GRAY);
g.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
g.setColor(Color.WHITE);
g.fillRect(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
g.setColor(Color.CYAN);
g.fillRect(rectangle2.x, rectangle2.y, rectangle2.width, rectangle2.height);

for(int i=1; i<=20; i++) {
if(existenCuadros)
g.setColor(Color.YELLOW);
else
g.setColor(Color.DARK_GRAY);
g.drawLine(0, i*rectangle.height, GAME_WIDTH, i*rectangle.height);
g.drawLine(i*rectangle.width, 0, i*rectangle.width, GAME_HEIGHT);
}

}

@Override
public void keyTyped(KeyEvent e) {

}

@Override
public void keyPressed(KeyEvent e) {
switch(e.getExtendedKeyCode()) {
case KeyEvent.VK_LEFT:
rectangle.x -= 32;
if(rectangle.x < 0) rectangle.x = 0;
break;
case KeyEvent.VK_RIGHT:
rectangle.x += 32;
if(rectangle.x >= GAME_WIDTH) rectangle.x = GAME_WIDTH - rectangle.width;
break;
case KeyEvent.VK_UP:
rectangle.y -= 32;
if(rectangle.y < 0) rectangle.y = 0;
break;
case KeyEvent.VK_DOWN:
rectangle.y += 32;
if(rectangle.y >= GAME_HEIGHT) rectangle.y = GAME_HEIGHT - rectangle.height;
break;

case KeyEvent.VK_A:
rectangle2.x -= 32;
if(rectangle2.x < 0) rectangle2.x = 0;
break;
case KeyEvent.VK_D:
rectangle2.x += 32;
if(rectangle2.x >= GAME_WIDTH) rectangle2.x = GAME_WIDTH - rectangle2.width;
break;
case KeyEvent.VK_W:
rectangle2.y -= 32;
if(rectangle2.y < 0) rectangle2.y = 0;
break;
case KeyEvent.VK_S:
rectangle2.y += 32;
if(rectangle2.y >= GAME_HEIGHT) rectangle2.y = GAME_HEIGHT - rectangle2.height;
break;

case KeyEvent.VK_C:
existenCuadros = !existenCuadros;
break;

}
}

@Override
public void keyReleased(KeyEvent e) {

}

}
Sin análisis de requisitos o sin diseño, programar es el arte de crear errores en un documento de texto vacío.

3n31ch

@rob1104 Intentare hacer una segunda parte. de la tutorial con mas funcionalidades, y que se note que es un juego. (Cuando tenga tiempo, últimamente he estado muuuy liado).

@DeviiAC Exactamente como dices no viene al caso, podrías haber preguntado en tu subforo correspondiente (Estamos en java), de esa manera acudiría gente interesada al ver el titulo del tema ^^.

El problema es que no debes publicar tu información personal  :xD.

Lee las reglas del foro. ^^. Suerte!