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:
Game | Esta contendrá el método de inicio donde se creara la ventana del juego. |
GamePanel | Esta 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
package net.elhacker.game; import javax.swing.JFrame; /* Importamos JFrame necesario para crear la ventana */ public class Game { 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
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 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 */ /* Los primeros dos valores son las cordenadas */ /* Los siguientes dos son el ancho y largo */ public GamePanel() { GamePanel.GAME_WIDTH, GamePanel.GAME_HEIGHT)); /* Se establece el tamaño del juego */ } /* * Thread que controla los FPS del juego */ @Override public void run() { long start; long elapsed; long wait; while(true){ this.repaint(); wait = 1000/expectedFps - (elapsed-start)/1000000; /* Utilizamos formula de FPS */ wait = (wait < 0)? 0 : wait; /* Prevenimos posibles errores */ try { } } /* * 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 g.fillRect(0, 0, GamePanel.GAME_WIDTH, GamePanel.GAME_HEIGHT); /* Pintamos el fondo gris oscuro */ g.fillRect(rectangle.x, rectangle.y, rectangle.width, rectangle.height); /* Pintamos el rectangulo */ } @Override /* * Metodo de escucha que nos permite realizar acciones dependiendo de las * teclas presionadas */ @Override switch(e.getExtendedKeyCode()){ rectangle.x-=32; break; rectangle.x+=32; break; rectangle.y-=32; break; rectangle.y+=32; break; } } @Override }
Partamos por esta parte:
Código
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
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 */ /* 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
public GamePanel() { GamePanel.GAME_WIDTH, GamePanel.GAME_HEIGHT)); /* Se establece el tamaño del juego */ }
Ahora definiremos el método run (el cual implementamos de runnable)
Código
@Override public void run() { long start; long elapsed; long wait; while(true){ this.repaint(); wait = 1000/expectedFps - (elapsed-start)/1000000; /* Utilizamos formula de FPS */ wait = (wait < 0)? 0 : wait; /* Prevenimos posibles errores */ try { } }
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
while (true) { this.repaint(); }
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
this.repaint(); wait = 1000/expectedFps - (elapsed-start)/1000000; /* Utilizamos formula de FPS */ wait = (wait < 0)? 0 : wait; /* Prevenimos posibles errores */ try {
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
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
@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
@Override g.fillRect(0, 0, GamePanel.GAME_WIDTH, GamePanel.GAME_HEIGHT); /* Pintamos el fondo gris oscuro */ 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
/* * Metodo de escucha que nos permite realizar acciones dependiendo de las * teclas presionadas */ @Override switch(e.getExtendedKeyCode()){ rectangle.x-=32; break; rectangle.x+=32; break; rectangle.y-=32; break; rectangle.y+=32; break; } } @Override
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.