Jump to content
YOUR-AD-HERE
HOSTING
TOOLS

Locked Programación de juegos para principiantes


Expermicid

Recommended Posts

En esta serie de tutoriales enseñaré conceptos intermedios sobre java (threads, AWT, Swing, etc.) y conceptos básicos para la programación de juegos (game loop, FPS, sprite, etc). Mi idea es que estos tutoriales sirvan tanto para los que desean programar juegos, como para que la gente con un nivel de principiante o intermedio en java pueda aprender y afianzar conceptos de programación java en general, de forma divertida.

 

 

Juegos en Java

 

Hasta hace poco tiempo casi todos los juegos profesionales se desarrollaban en C o C++. Esto ha cambiado, hoy día hay grandes juegos realizados completamente en java. La industria de los juegos para móviles esta creciendo y java es el lenguaje para programar en Android. Android debe ser ya el sistema operativo para smart phones más usado en este momento. Por otro lado, juegos como Minecraft, tienen millones de usuarios a pesar de iniciarse como proyecto de un sólo programador, sin el apoyo de una gran empresa.

 

Espero, con esta serie de tutoriales, motivarlos a entrar en el mundo de la programación java y en particular de la programación de juegos.

 

El juego: Mini Tenis

 

En esta serie de tutoriales desarrollaremos desde cero una versión de uno de los primeros juegos que alcanzaron popularidad en la era de la informática.

 

This is the hidden content, please

 

Este juego no pretende ser el próximo éxito en ventas sino tan sólo una plataforma desde donde enseñar y quien sabe si inspirar a algún programador principiante a convertirse en el próximo exitoso en el mundo de la informática ;)

 

Índice

 

  • Nuestro primer gráfico: JFrame, JPanel, método paint
  • Animación de un objeto en movimiento
  • Sprites
  • Eventos. Capturando las entrada por teclado
  • Agregando el sprite raqueta
  • Detección de colisiones
  • Agregando sonido a nuestro juego
  • Creando una clase Sound para nuestro juego
  • Agregando puntuación y aumentando la velocidad
  • Creando archivo jar ejecutable y qué es la máquina virtual de java
  • Descargar el código fuente de minicraft y configurarlo en eclipse
  • Agregar vista de mapa a Minicraft

 

Programación de juegos: JFrame, JPanel, método paint

 

Para dibujar algo necesitamos una superficie donde pintar. Esta superficie o lienzo (Canvas en inglés) donde pintaremos nuestro primer ejemplo es un objeto JPanel. Así como un lienzo necesita un marco para sostenerse, nuestro JPanel estará enmarcado en una ventana modelada por la clase JFrame.

 

 

JFrame: La Ventana

 

El siguiente código crea una ventana con titulo "Mini Tennis" de 300 pixels por 300 pixels. La ventana no será visible hasta que llamemos setVisible(true). Si no incluimos la última línea "frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOS E)", cuando cerremos la ventana el programa no terminará y seguirá ejecutándose.

 

This is the hidden content, please

 

Si ejecutamos obtendremos:

 

This is the hidden content, please

 

Con estas pocas instrucciones obtenemos una ventana que se puede maximizar, minimizar, cambiar de tamaño con el ratón, etc. En realidad cuando creamos un objeto JFrame iniciamos un motor que maneja la interfaz de usuario. Este motor se comunica con el sistema operativo tanto para pintar en la pantalla como para recibir información del teclado o el ratón. Llamaremos a este motor "Motor AWT" o "Motor Swing" ya que está compuesto por estas dos librerías. En las primeras versiones de java solo existía AWT y luego se agregó Swing. Este Motor utiliza varios hilos de ejecución.

 

¿Qué es un hilo o thread en java?

 

Normalmente un programa se ejecuta línea tras línea por un solo procesador en una sola línea o hilo de ejecución. El concepto de hilo (en ingles Thread) permite a un programa iniciar varias ejecuciones concurrentes. Esto es como si existieran varios procesadores ejecutándo al mismo tiempo sus propias secuencias de instrucciones.

 

Aunque los hilos y la concurrencia son herramientas muy potentes puede traer muchos problemas como que dos hilos accedan a las mismas variables de forma conflictiva. Es interesante considerar que dos hilos pueden estar ejecutando el mismo código de un método a la vez.

 

Podemos pensar que un hilo es un cocinero preparando un plato leyendo una receta de cocina. Dos hilos concurrentes serían como dos cocineros trabajando en la misma cocina, preparando cada uno un plato leyendo cada uno una receta o también podrían estar leyendo la misma receta. Los conflictos surgen por ejemplo cuando los dos intentan usar una sartén al mismo tiempo.

 

Motor AWT e Hilo de cola de eventos - Thread AWT-EventQueue-0

 

El Motor AWT inicia varios Hilos (Threads) que podemos ver en la vista Debug si iniciamos la aplicación con debug y vamos a la perspectiva Debug. Cada hilo es como si fuera un programa independiente ejecutándose al mismo tiempo que los otros hilos. Más adelante veremos más sobre hilos, por lo pronto solo me interesa que recuerden el tercer hilo que vemos en la vista Debug llamado "Thread [AWT-EventQueue-0]" este hilo es el encargado de pintar la pantalla y recibir los eventos del teclado y el ratón.

 

This is the hidden content, please

 

JPanel: El lienzo (Canvas en inglés)

 

Para poder pintar necesitamos donde y el donde es un objeto JPanel que incluiremos en la ventana. Extenderemos la clase JPanel para poder sobrescribir el método paint que es el método que llamará el Motor AWT para pintar lo que aparece en la pantalla.

 

This is the hidden content, please

 

El método paint recibe por parámetro un objeto Graphics2D que extiende de Graphics. Graphics es la vieja clase usada por AWT que ha sido reemplazada por Graphics2D que tiene más y mejor funcionalidad. El parámetro sigue siendo de tipo Graphics por compatibilidad pero nosotros siempre utilizaremos Graphics2D por lo que es necesario crear una variable g2d: "Graphics2D g2d = (Graphics2D) g;". Una vez que tenemos g2d podemos utilizar todos los métodos de Graphics2D para dibujar.

 

Lo primero que hacemos es elegir el color que utilizamos para dibujar: "g2d.setColor(Color.RED);". Luego dibujamos unos círculos y cuadrados.

 

Posicionamiento en el lienzo. Coordenadas x e y

 

Para dibujar algo dentro del lienzo debemos indicar en que posición comenzaremos a pintar. Para esto cada punto del lienzo tiene una posición (x,y) asociada siendo (0,0) el punto de la esquina superior izquierda.

 

This is the hidden content, please

 

El primer circulo rojo se pinta con "g2d.fillOval(0, 0, 30, 30)": los primeros dos parámetros son la posición (x,y) y luego se indica el ancho y alto. como resultado tenemos un circulo de 30 pixeles de diámetro en la posición (0,0).

 

El circulo vacío se pinta con "g2d.drawOval(0, 50, 30, 30)": el la posición x=0 (pegado al margen izquierdo) y la posición y=50 (50 pixeles más abajo del margen superior) pinta un circulo de 30 pixeles de alto y 30 de ancho.

 

Los rectángulos se pintan con "g2d.fillRect(50, 0, 30, 30)" y "g2d.drawRect(50, 50, 30, 30)" de forma similar a los círculos.

 

Por último "g2d.draw(new Ellipse2D.Double(0, 100, 30, 30))" pinta el ultimo circulo usando un objeto Ellipse2D.Double.

 

Existen muchísimos métodos en Graphics2D. Algunos los veremos en siguientes tutoriales.

 

¿Cuándo el motor AWT llama al método paint?

 

El motor AWT llama al método paint cada vez que el sistema operativo le informa que es necesario pintar el lienzo. Cuando se carga por primera vez la ventana se llama a paint, si minimizamos y luego recuperamos la ventana se llama a paint, si modificamos el tamaño de la ventana con el ratón se llama a paint.

 

Podemos comprobar este comportamiento si ponemos un breakpoint en la primer línea del método paint y ejecutamos en modo debug.

 

This is the hidden content, please

 

Es interesante ver que el método paint es ejecutado por el Hilo de cola de eventos (Thread AWT-EventQueue) que como indicamos antes es el encargado de pintar la pantalla.

 

Game loop y Animación de un objeto

 

En este tutorial veremos como hacer que un círculo se mueva sobre nuestro lienzo. Esta animación se consigue pintando el círculo en una posición y luego borrando y pintando el círculo en una posición cercana. El efecto logrado es un círculo en movimiento.

 

 

Posición del círculo

 

Como mencionamos antes cada vez que pintamos debemos definir la posición (x,y) donde dibujaremos en este caso el círculo. Para que el círculo se mueva debemos modificar la posición (x,y) cada cierto tiempo y volver a pintar el círculo en la nueva posición.

 

En nuestro ejemplo mantendremos en dos propiedades llamadas "x" e "y", la posición actual de nuestro círculo. También creamos un método moveBall() que incrementará en 1 tanto a "x" como a "y" cada vez que es llamado. En el método paint dibujamos un circulo de 30 pixeles de diámetro en la posición (x,y) dada por las propiedades antes mencionadas "g2d.fillOval(x, y, 30, 30);".

 

Game loop

 

Al final del método main iniciamos un ciclo infinito "while (true)" donde repetidamente llamamos a moveBall() para cambiar la posición del circulo y luego llamamos a repaint() que fuerza al motor AWT a llamar al método paint para repintar el lienzo.

 

Este ciclo o repetición se conoce como "Game loop" y se caracteriza por realizar dos operaciones:

 

  • Actualización (Update): actualización de la física de nuestro mundo. En nuestro caso nuestra actualización esta dada tan solo por el método moveBall() que incrementa las propiedades "x" e "y" en 1.
  • Renderizado (Render): aquí se dibuja según el estado actual de nuestro mundo reflejando los cambios realizados en el paso anterior. En nuestro ejemplo este renderizado esta dado por la llamada a repaint() y la subsecuente llamada a paint realizada por el motor AWT o más específicamente por el Hilo de cola de eventos.

 

This is the hidden content, please

 

Al ejecutar el código anterior obtendremos:

 

This is the hidden content, please

 

Analizando nuestro método paint

 

Como mencionamos en el tutorial anterior este método se ejecuta cada vez que el sistema operativo le indica a Motor AWT que es necesario pintar el lienzo. Si ejecutamos el método repaint() de un objeto JPanel lo que estamos haciendo es decirle al Motor AWT que ejecute el método paint tan pronto como pueda. La llamada a paint la realizará el Hilo de cola de eventos. Llamando a repaint() logramos que se repinte el lienzo y así poder reflejar el cambio en la posición del circulo.

 

This is the hidden content, please

 

La llamada a "super.paint(g)" limpia la pantalla, si comentamos esta línea podemos ver el siguiente efecto:

 

This is the hidden content, please

 

La instrucción "g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASI NG, RenderingHints.VALUE_ANTIALIAS_ON)" suaviza los bordes de las figuras como se puede ver en el siguiente gráfico. El círculo de la izquierda es sin aplicar ANTIALIAS y el de la derecha aplicando ANTIALIAS.

 

This is the hidden content, please

 

Analizando la concurrencia y el comportamiento de los hilos

 

Cuando se inicia la ejecución del método main sólo existe un hilo en ejecución. Esto se puede ver colocando un breakpoint en la primera línea del método main.

 

This is the hidden content, please

 

Si agregamos un breakpoint en la línea game.repaint() y en la primera línea del método paint y a continuación oprimimos F8 (Resume: ordena que continúe la ejecución hasta el final o hasta que encuentre el próximo breakpoint) obtendremos:

 

This is the hidden content, please

 

En la vista de la izquierda podemos ver que se han creado cuatro hilos de los cuales dos están detenidos en breakpoints. El Thread main está detenido en la línea 40 en la instrucción game.repaint(). El thread AWT-EventQueue está detenido en el método paint en la línea 22.

 

Si seleccionamos el thread AWT-EventQueue en la vista Debug y oprimimos F8 repetidamente (2 veces) veremos que no se detiene más en el metodo paint. Esto es porque el sistema operativo no ve motivo para solicitar un repintado del lienzo una vez inicializado.

 

This is the hidden content, please

 

Si oprimimos F6 (avanza la ejecución del hilo sólo una línea), esta vez sobre el thread main, veremos que el método paint es vuelto a llamar por el thread AWT-EventQueue. Ahora sacamos el breakpoint del método paint, oprimimos F8 y volvemos a tener sólo detenido el thread main.

 

La siguiente animación nos muestra que pasa en el lienzo cada vez que oprimimos resume (F8) repetidamente. Cada llamada a moveBall() incrementa la posición (x,y) del círculo y la llamada a repaint() le dice al thread AWT-EventQueue que repinte el lienzo.

 

This is the hidden content, please

 

Por último analicemos la línea "Thread.sleep(10)" (la última instrucción dentro del "Game loop"). Para esto comentamos la línea con // y ejecutamos sin debug. El resultado es que no se pinta el círculo en el lienzo. ¿Por qué pasa esto? Esto es debido a que el thread main se apodera del procesador y no lo comparte con el thread AWT-EventQueue que entonces no puede llamar al método paint.

 

"Thread.sleep(10)" le dice al procesador que el thread que se está ejecutando descanse por 10 milisegundos lo que permite que el procesador ejecute otros threads y en particular el thread AWT-EventQueue que llama al método paint.

 

Me gustaría aclarar que en este ejemplo la solución planteada es muy pobre y sólo pretende ilustrar los conceptos de "game loop", threads y concurrencia. Existen mejores formas de manejar el game loop y la concurrencia en un juego y las veremos en los próximos tutoriales.

 

Sprites - Velocidad y dirección

 

Cada objeto que se mueve en la pantalla tiene características propias como la posición (x,y), la velocidad y la dirección en que se mueve, etc. Todas estas características se pueden aislar en un objeto que llamaremos Sprite.

 

 

Velocidad y dirección

 

En el tutorial anterior logramos que la pelota (el círculo) se moviera hacia abajo y a la derecha a un píxel por vuelta en el Game Loop. Cuando llegaba al limite de la pantalla la pelota seguía su curso desapareciendo del lienzo. Lo que haremos a continuación es que la pelota rebote en los limites del lienzo cambiando su dirección.

 

This is the hidden content, please

 

En el código anterior se agregaron dos propiedades "xa" y "ya" que representan la velocidad en que se mueve la pelota. Si xa=1, la pelota se mueve hacia la derecha a un píxel por vuelta del Game Loop y si xa=-1, la pelota se mueve hacia la izquierda. Similarmente ya=1 mueve hacia abajo y ya=-1 mueve hacia arriba. Esto lo logramos con las líneas "x = x + xa" e "y = y + ya" del método moveBall().

 

Antes de ejecutar las instrucciones anteriores verificamos que la pelota no salga de los márgenes del lienzo. Por ejemplo cuando la pelota alcance el margen derecho o lo que es lo mismo cuando (x + xa > getWidth() - 30) lo que haremos es cambiar la dirección del movimiento sobre el eje x o lo que es lo mismo asignar menos uno a xa "xa = -1".

 

This is the hidden content, please

 

Cada sentencia if limita un borde del lienzo.

 

Crear el Sprite Ball (pelota en inglés)

 

La idea es crear una clase llamada Ball que aisle todo lo referente a la pelota. En el siguiente código podemos ver como extraemos todo el código referente a la pelota de la clase Game2 y lo incorporamos a nuestra nueva clase Ball.

 

This is the hidden content, please

 

El Sprite Ball necesita que le envíen una referencia al objeto Game para obtener los limites del lienzo y así saber cuando debe cambiar de dirección. En el método move() de la clase Ball se llama a game.getWidth() y game.getHeight().

 

This is the hidden content, please

 

Si ejecutamos Game2 obtendremos el mismo resultado que si ejecutamos la versión anterior Game. La conveniencia de esta separación del código referente a la pelota en una clase de tipo Sprite se vuelve más obvia cuando incluimos la raqueta mediante un nuevo Sprite en un próximo tutorial.

Link to comment
Share on other sites

Re: Programación de juegos para principiantes

 

Eventos. Capturando las entrada por teclado

 

En este tutorial veremos como funcionan los eventos y en particular como obtener la información acerca de los eventos producidos en el teclado desde un programa java. Además explicaremos el concepto y uso de clases anónimas que es el método más comúnmente usado para manejar eventos en java. Abandonaremos nuestro juego momentáneamente y haremos un simple ejemplo de captura de eventos.

 

 

Ejemplo de lectura del teclado

 

Para leer del teclado es necesario registrar un objeto que se encargue de "escuchar si una tecla es presionada". Este objeto conocido como "Listener" u "oyente" y tendrá métodos que serán llamados cuando alguien presione una tecla. En nuestro ejemplo el Listener se registra en el JPanel (o KeyboardExample) usando el método addKeyListener(KeyListener listener).

 

This is the hidden content, please

 

En el constructor de la clase KeyboardExample creamos el listener y lo registramos. Para que un objeto JPanel reciba las notificaciones del teclado es necesario incluir la instrucción setFocusable(true) que permite que KeyboardExample reciba el foco.

 

This is the hidden content, please

 

La clase MyKeyListener es la que uso para crear el objeto Listener. Este Listener imprimirá en la consola el nombre del método y la tecla afectada por el evento.

 

This is the hidden content, please

 

Una vez registrado, cuando KeyboardExample (nuestro JPanel) tenga el foco y alguien oprima una tecla KeyboardExample informará al objeto listener registrado. El objeto Listener de nuestro ejemplo implementa la interfaz KeyListener que tiene los métodos keyTyped(), keyPressed() y keyReleased(). El método keyPressed será llamado cada vez que una tecla sea oprimida (y varias veces si se mantiene oprimida). El método keyReleased será llamado cuando solemos una tecla.

 

Los métodos antes mencionados reciben como parámetro un objeto KeyEvent que contiene información sobre que tecla se ha oprimido o soltado. Usando e.getKeyCode() podemos obtener el código de la tecla y si le pasamos un código de tecla a la función estatica KeyEvent.getKeyText(...) podemos obtener el texto asociado a la tecla.

 

¿Cómo funcionan los eventos en AWT/Swing?

 

Lo eventos del ratón y el teclado son controlados por el sistema operativo. El motor AWT, en particular el thread AWT-Windows se comunica con el sistema operativo y se entera de si hubo un evento. Cuando encuentra un nuevo evento lo coloca en la "Cola de Eventos" para que sea atendido cuando le llegue su turno por el Thread AWT-EventQueue.

 

This is the hidden content, please

 

Cuando el Thread AWT-EventQueue atiende a un evento se fija a que componente afecta y le informa. En nuestro caso el componente es el JPanel que informa a todos los listeners que se hayan registrado para recibir notificaciones de ese evento.

 

En el caso del teclado la llamada addKeyListener(KeyListener listener) es la que realiza este registro. Si queremos registrar un objeto para escuchar los eventos del ratón podemos usar addMouseListener(MouseListener listener).

 

Si quieren profundizar en como funcionan los eventos en AWT/Swing les recomiendo el siguiente artículo.

 

Clase anónima

 

En el ejemplo anterior la clase MyKeyListener será solo usada una vez por lo que podríamos reemplazarla por una clase anónima. KeyboardExample2 muestra como sería:

 

This is the hidden content, please

 

En el constructor de la clase KeyboardExample2 podemos ver como se reemplaza

 

This is the hidden content, please

 

por

 

This is the hidden content, please

 

Esta instrucción tiene el mismo efecto que la anterior. Reemplaza la definición de la clase MyKeyListener por una clase anónima que hace exactamente lo mismo.

 

La forma de crear un objeto desde una clase anónima es reemplazar el nombre de la clase a crear por una definición que empieza por la interfaz a implementar seguida por () y luego dentro de {} la definición de la clase como hacemos normalmente.

 

Aunque parezca un poco extraño esta es la forma más cómoda de implementar Listeners de eventos y es la forma que más encontrarán en código java avanzado.

 

Ahora sigamos con el desarrollo de nuestro juego en el próximo tutorial.

 

Agregando el sprite raqueta

 

En este tutorial agregaremos la raqueta mediante un Sprite llamado Racquet. La raqueta se moverá hacia la izquierda o derecha cuando oprimamos las teclas del cursor por lo que nuestro programa necesita leer del teclado.

 

 

Nuevo Sprite Racquet

 

Lo primero que hacemos es agregar en la clase Game una nueva propiedad llamada racquet donde mantendremos el Sprite que maneja la raqueta. En el método move() añadimos una llamada a racquet.move() y en paint() una llamada a racquet.paint(). Hasta ahora todo es similar al sprite Ball pero como la posición de la raqueta responde al teclado tenemos que hacer algo más.

 

This is the hidden content, please

 

En el constructor de la clase game se puede ver como se registra un listener para capturar los eventos del teclado. En el método keyPressed() del listener informamos a la raqueta que una tecla ha sido oprimida llamando a racquet.keyPressed(e). Lo mismo hacemos para keyReleased(). Con esto el Sprite racquet se enterará cuando una tecla sea oprimida. Veamos ahora las clases Ball y Racquet que implementan los sprites.

 

This is the hidden content, please

 

La clase Ball no tiene cambios. Comparémosla con la clase Racquet:

 

This is the hidden content, please

 

A diferencia de Ball, Racquet no tiene propiedades para la posición "y" ni la velocidad "ya". Esto es debido a que la raqueta no variará su posición vertical, solo se moverá hacia la izquierda o derecha, nunca hacia arriba o abajo. En el método paint la instrucción g.fillRect(x, 330, 60, 10) define un rectángulo de 60 por 10 pixeles en la posición (x,y)=(x,330). Como vemos "x" puede variar pero "y" está fijada a 330 pixeles del limite superior del lienzo.

 

El método move() es similar al de Ball en el sentido de incrementar en "xa" la posición "x" y controlar que el sprite no se salga de los limites.

 

This is the hidden content, please

 

Inicialmente el valor de "x" es cero lo que indica que la raqueta estará en el limite izquierdo del lienzo. "xa" también está inicializado a cero, lo que hace que en principio la raqueta aparezca estática ya que x = x + xa no modificará "x" mientras "xa" sea cero.

 

Cuando alguien presione una tecla el método keyPressed de Racquet será llamado y este pondrá "xa" en 1 si la tecla presionada es la de dirección derecha (KeyEvent.VK_RIGHT) lo que a su vez hará que la raqueta se mueva a la derecha la próxima vez que se llame al método move (recordar x = x + xa). De la misma forma si se presiona la tecla KeyEvent.VK_LEFT se moverá a la izquierda.

 

This is the hidden content, please

 

Cuando una tecla deja de ser presionada el método keyReleased es llamado y "xa" pasa a valer cero lo que hace que el movimiento de la raqueta se detenga.

 

This is the hidden content, please

 

Si ejecutamos el ejemplo podemos ver como la pelota se mueve rebotando contra los límites y la raqueta se mueve cuando presionamos las teclas de dirección correspondientes. Pero cuando la pelota choca con la raqueta la atraviesa pareciendo como si esta no existiese. En el próximo tutorial veremos como hacer que la pelota rebote sobre la raqueta.

Link to comment
Share on other sites

Re: Programación de juegos para principiantes

 

Detección de colisiones

 

En este tutorial aprenderemos como detectar cuando un sprite choca con otro. En nuestro juego haremos que la pelota rebote contra la raqueta. Además haremos que el juego termine si la pelota alcanza el limite inferior del lienzo mostrando una ventana popup con el clásico mensaje "Game Over".

 

 

Game Over

 

A continuación vemos nuestra clase Game que es idéntica a la anterior con la sola diferencia de que se ha agregado el método gameOver();

 

This is the hidden content, please

 

El método gameOver() lanza un popup usando JOptionPane.showMessageDialog con el mensaje "Game Over" y un solo botón "Aceptar". Después del popup, System.exit(ABORT) hace que se termine el programa. El método gameOver() es público ya que será llamado desde el sprite Ball cuando detecte que ha llegado al límite inferior del lienzo.

 

Colisión de Sprites

 

Para detectar la colisión entre la pelota y la raqueta usaremos rectángulos. El caso de la pelota crearemos un cuadrado alrededor de la pelota como se ve el la figura 2.

 

This is the hidden content, please

 

La clase java.awt.Rectangle tiene un método intersects(Rectangle r) que retorna true cuando dos rectángulos ocupan el mismo espacio como en el caso de la figura 3 o 4. Cabe destacar que este método no es exacto ya que en la figura 4 la pelota no toca a la raqueta pero para nuestro ejemplo será más que suficiente.

 

A continuación vemos la clase Racquet donde el único cambio funcional es que se ha agregado el método getBounds() que retorna un objeto de tipo rectángulo indicando la posición de la raqueta. Este método será usado por el sprite Ball para saber la posición de la raqueta y así detectar la colisión.

 

This is the hidden content, please

 

Otro cambio que funcionalmente no afecta pero que es una buena práctica de programación es la inclusión de constantes:

 

This is the hidden content, please

 

Como antes mencionamos el valor de posición "y" estaba fijo en 330. Este valor es usado tanto en el método paint como en getBounds. Si queremos cambiarlo ahora sólo tenemos que cambiarlo en un sólo lugar evitando el posible error que se produciría si lo cambiáramos en un método y en otro no.

 

La forma de definir una constante en java es declarando una propiedad "static final" y en mayúsculas. El compilador permite usar minúsculas pero el estándar dice que se deben usar mayúsculas para los nombres de las constantes.

 

Por último la clase Ball:

 

This is the hidden content, please

 

De forma similar a la clase Racquet se ha incluido el método getBounds() y la constante DIAMETER.

 

Más interesante es la aparición de un nuevo método llamado collision() que retorna true (verdadero) si el rectángulo ocupado por la raqueta "game.racquet.getBounds()" intersecta al rectángulo que encierra a la pelota "getBounds()".

 

This is the hidden content, please

 

Si la colisión se produce, además de cambiar la dirección ajustaremos la posición de la pelota. Si la colisión es por el lado (figura 1), la pelota podría estar varios pixeles por debajo de la cara superior de la raqueta. En el siguiente game loop aunque la pelota se movería hacia arriba (figura 2) podría todavía estar en colisión con la raqueta.

 

This is the hidden content, please

 

Para evitar esto colocamos a la pelota sobre la raqueta (figura 3) mediante:

 

This is the hidden content, please

 

El método getTopY() de Racquet nos da la posición en el eje y de la parte superior de la raqueta y restando DIAMETER conseguimos la posición y exacta donde colocar la pelota para que esté sobre la raqueta.

 

Por último es el método move() de la clase Ball el que usa los nuevos métodos collision() y gameOver() de la clase Game. El rebote al alcanzar el lÍmite inferior ha sido reemplazado por una llamada a game.gameOver().

 

This is the hidden content, please

 

Y poniendo un nuevo condicional usando el método collision() logramos que la pelota rebote hacia arriba si esta colisiona con la raqueta:

 

This is the hidden content, please

 

Si ejecutamos el ejemplo podemos ver:

 

This is the hidden content, please

 

Agregando sonido a nuestro juego

 

Un juego sin sonido no está completo. En este tutorial agregaremos música de fondo, el ruido del rebote de la pelota y un "Game Over" con voz graciosa al terminar el juego. Para evitar problemas de copyright vamos a crear nosotros mismos los sonidos.

 

 

Creando sonidos

 

Para crear los sonidos me tomé la libertad de buscar en Google "free audio editor" y como respuesta encontré

This is the hidden content, please
Tengo que decir que la versión gratis de este producto es potente y fácil de manejar.

 

This is the hidden content, please

 

Con este editor he creado los archivos: back.wav, gameover.wav y ball.wav. En el video de youtube pueden ver como lo hice y crearlos ustedes mismos. También pueden descargar y usar estos tres que en esta misma línea los declaro libres de copyright. Lo que tienen que hacer es copiar estos archivos al paquete com.edu4java.minitennis7.

 

This is the hidden content, please

 

Reproducir sonidos usando AudioClip

 

Para reproducir los archivos de sonido usaremos la clase AudioClip. Crearemos objetos AudioClip usando el método estático de la clase Applet: Applet.newAudioClip(URL url). Este método necesita un objeto URL que le indique donde está el archivo de audio que queremos cargar para luego reproducir. La siguiente instrucción crea un objeto URL utilizando una ubicación en Internet:

 

This is the hidden content, please

 

La siguiente utiliza un directorio dentro del sistema de archivos local:

 

This is the hidden content, please

 

Nosotros buscaremos nuestro archivo utilizando el classpath. Este es el sistema que usa java para cargar las clases o mejor dicho los archivos *.class que definen las clases del programa. Para obtener un URL desde el classpath se utiliza el método getResource(String name) de la clase Class donde name es el nombre del archivo que queremos obtener.

 

A continuación vemos dos formas de como conseguir el URL del archivo "back.wav" que está en el mismo paquete que la clase SoundTest o lo que es lo mismo en el mismo directorio donde esta el archivo SoundTest.class.

 

This is the hidden content, please

 

Tanto "SoundTest.class" como "new SoundTest().getClass()" nos dan un objeto class que tiene el método getResource que queremos usar.

 

He creado la clase SoundTest con el sólo propósito de mostrarles como trabaja AudioClip y no es necesaria para nuestro juego. A continuación se muestra el código fuente de SoundTest completo:

 

This is the hidden content, please

 

De esta forma el archivo back.wav se obtienen desde el classpath. El classpath es el conjunto de directorios y archivos *.jar desde donde nuestro programa puede leer las clases (archivos *.class).

 

Una ventaja de esta metodología es que sólo tenemos que indicar la posición del archivo con respecto a la clase que lo usa. En nuestro caso como está en el mismo paquete basta con el nombre "back.wav". Otra ventaja es que los archivos de sonido se pueden incluir en un archivo *.jar. Veremos más sobre archivos *.jar más adelante. Una ves que tenemos el objeto URL podemos crear objetos AudioClip usando Applet.newAudioClip(url).

 

This is the hidden content, please

 

El objeto AudioClip tiene un método play() que inicia un thread independiente que reproduce sólo una vez el audio contenido en el archivo. Para reproducir el audio en forma repetitiva podemos usar el método loop() de AudioClip que reproducirá el sonido una y otra vez hasta que se llame al método stop sobre el mismo objeto AudioClip.

 

Dos audioClips pueden reproducirse al mismo tiempo. En el ejemplo creo dos audioClips con el mismo audio: clip y clip2. Reproduzco clip con play, espero un segundo Thread.sleep(1000) y reproduzco clip2 con loop. El resultado es una mezcla de los dos audios. Por ultimo después de 20 segundos Thread.sleep(20000) llamo a clip2.stop() y detengo la repetición de clip2.

Link to comment
Share on other sites

Re: Programación de juegos para principiantes

 

Creando una clase Sound para nuestro juego

 

Para guardar los audioclips de nuestro juego creamos una clase Sound que tendrá una constante con un audioclip por cada sonido que usemos. Estas constantes son públicas para que cualquier objeto que tenga acceso a ellas pueda reproducirlas. Por ejemplo en la clase Ball podemos reproducir el sonido del rebote de la pelota usando Sound.BALL.play() en el momento que detectamos que la pelota cambia de dirección.

 

[video=youtube;_vmGMZLUR-E]

 

This is the hidden content, please

 

Los objetos audioclips se crearán al cargarse la clase Sound la primera vez que alguien use la clase Sound. A partir de este momento serán reutilizados una y otra vez. Ahora veamos las modificaciones en la clase Game:

 

This is the hidden content, please

 

En la última línea del constructor de la clase Game añadimos Sound.BACK.loop(), lo que iniciará la reproducción de nuestra música de fondo que se repetirá hasta que se alcance el método gameOver(), donde detenemos la música de fondo con Sound.BACK.stop(). A continuación de Sound.BACK.stop() y antes del popup informamos que se termino la partida reproduciendo "Game Over" Sound.GAMEOVER.play().

 

En la clase Ball modificamos el método move() para que se reproduzca Sound.BALL cuando la pelota rebote.

 

This is the hidden content, please

 

Lo que hice en move() es agregar una variable changeDirection que inicializo a true. Añadiendo un else a cada if y colocando un changeDirection = false que sólo se ejecutará si ninguna condición en los if es cumplida, conseguimos enterarnos si la bola ha rebotado. Si la pelota ha rebotado changeDirection será verdadero y Sound.BALL.play() será ejecutado.

 

Agregando puntuación y aumentando la velocidad

 

Todo juego necesita una medida de logro o éxito. En nuestro caso incluiremos en el rincón izquierdo de la pantalla nuestra puntuación que no será más que la cantidad de veces que logramos pegarle a la pelota con la raqueta. Por otro lado el juego debería ser cada vez más difícil para no matar de aburrimiento al jugador. Para esto aumentaremos la velocidad del juego cada vez que rebote la pelota en la raqueta.

 

 

Los objetos móviles del juego son la pelota y la raqueta. Modificando la velocidad de movimiento de estos dos objetos modificaremos la velocidad del juego. Vamos a incluir una propiedad llamada speed en la clase Game para mantener la velocidad del juego. La propiedad speed será inicialmente 1 e irá incrementándose cada vez que le demos a la pelota con la raqueta.

 

Para la puntuación necesitaríamos otra propiedad a incrementar cada vez que golpeemos la pelota. En vez de crear una nueva propiedad se me ocurrió reutilizar speed. El único inconveniente es que las puntuaciones suelen iniciarse en 0 y no en 1 como speed. La solución que se me ocurrió fue agregar un método getScore() que retorne el valor de speed menos uno.

 

This is the hidden content, please

 

Veamos las modificaciones hechas en la clase Game:

 

This is the hidden content, please

 

Para pintar la puntuación en el rincón superior izquierdo al final del método paint he agregado:

 

This is the hidden content, please

 

En la primera línea elegimos el color gris, en la segunda línea el tipo de letra Verdana, negrita de 30 pixeles y finalmente en la posición (x,y) igual a (10,30) donde dibujamos la puntuación.

 

En el método gameOver() modificamos el segundo parámetro para mostrar la puntuación alcanzada:

 

This is the hidden content, please

 

En la clase Ball el método move() ha sido modificado para considerar la nueva propiedad de velocidad "game.speed". Cuando la pelota cambiaba de dirección las propiedades de velocidad xa y ya eran modificadas a 1 o -1. Ahora considerando la velocidad estas propiedades son cambiadas a game.speed o -game.speed. También se ha agregado en el condicional if(collision()) que la velocidad se incremente "game.speed++".

 

This is the hidden content, please

 

A continuación vemos la clase Racquet:

 

This is the hidden content, please

 

Aquí la modificación es similar que en Ball. En el método keyPressed(KeyEvent e) la modificación de la velocidad xa pasa de -1 y 1 a -game.speed y game.speed.

 

Nota: según el estándar de "Java Beans" el acceso a la propiedad "game.speed" debería hacerse usando un método de la forma "game.getSpeed()". El accesos directo a una propiedad es considerado casi un pecado mortal en el ámbito de java empresarial. Curiosamente en el entorno de desarrollo de juegos es muy común y esta justificado por eficiencia. Esto es especialmente importante en programación para móviles donde los recursos suelen ser más escasos.

 

Creando archivo jar ejecutable y qué es la máquina virtual de java.

 

En este tutorial veremos como crear un archivo ejecutable para una aplicación java, en particular para nuestro juego. Un programa java necesita una maquina virtual para ser ejecutado. A continuación también explicaremos que es la máquina virtual de java y brevemente como funciona.

 

 

La máquina virtual de java; Java Virtual Machine (JVM)

 

Antes de java lo más normal era escribir un programa en un lenguaje de programación de alto nivel como C o Pascal y luego traducirlo a lenguaje de máquina con un compilador. El "lenguaje máquina" o "código máquina" es el lenguaje que entiende la máquina (ordenador o computadora). Una máquina con Windows y un Mac de Apple hablan distinto lenguaje de máquina. Luego se necesita un compilador diferente para cada máquina.

 

En el caso de java cuando usamos el compilador no obtenemos código máquina. Lo que obtenemos es un código llamado bytecode que no se ejecuta directamente sobre una máquina real. Este bytecode solo se puede ejecutar en una máquina virtual. Una máquina virtual es un programa que se hace pasar por una máquina. Para cada sistema operativo diferente existirá un programa de máquina virtual especifico pero el bytecode que ejecutan será el mismo.

 

This is the hidden content, please

 

Como el bytecode es el mismo potencialmente puede ser ejecutado es cualquier sistema operativo siempre y cuando exista una implementación de JVM para este SO. En esta idea se basa la famosa frase: "Write once, run anywhere" (WORA) "escribir una vez, ejecutar en cualquier parte".

 

Compilación y ejecución en java

 

Existen dos versiones de instalación de java para cada sistema operativo: JRE y JDK. JRE Java Runtime Environment, es una versión reducida que contiene la JVM pero que no incluye el compilador java. JDK Java Development Kit contiene la JVM, el compilador java y muchas herramientas adicionales para el desarrollo de aplicaciones java. Si no tiene instalada la versión JDK tendrán que instalarla para poder continuar con este tutorial.

 

Si tenemos instalado la JDK tendremos un directorio donde estarán todos los archivos que componen la plataforma java. Este directorio es conocido como java Home o JAVA_HOME. En mi caso este es "C:\Program Files (x86)\Java\jdk1.6.0_21".

 

Dentro de JAVA_HOME existe una carpeta bin que contiene los ejecutable entre los que podemos encontrar: El compilador: javac.exe y la máquina virtual: java.exe.

 

Para ejemplificar como funcionan estos programas vamos a crear un archivo llamado HelloWorld.java en un directorio C:\testjava con el siguiente contenido:

 

This is the hidden content, please

 

Luego abrimos una ventana de comandos, ejecutamos "cd C:\testjava" para posicionarnos en el directorio donde esta nuestro archivo java y luego para compilar ejecutamos:

 

This is the hidden content, please

 

Como resultado podemos ver que se ha creado un nuevo archivo HellowWorld.class con el bytecode. Podemos ejecutar este bytecode con la siguiente instrucción:

 

This is the hidden content, please

 

Un programa java normalmente esta compuesto por varios archivos java y por consiguiente muchos archivos *.class. Además están los archivos de recursos como los sonidos en nuestra aplicación. Java permite empaquetar una aplicación con todos los archivos antes mencionados en un archivo *.jar.

 

Archivo JAR

 

Un archivo jar no es más que un archivo comprimido con el algoritmo de compresión ZIP que puede contener:

 

Los archivos *.class que se generan a partir de compilar los archivos *.java que componen nuestra aplicación.

Los archivos de recursos que necesita nuestra aplicación (Por ejemplo los archivo de sonido *.wav)

Opcionalmente se puede incluir los archivos de código fuente *.java

Opcionalmente puede existir un archivo de configuración "META-INF/MANIFEST.MF".

Crear un archivo JAR ejecutable

 

Para que el archivo jar sea ejecutable hay que incluir en el archivo MANIFEST.MF una línea indicando la clase que contiene el método estático main() que se usará para iniciar la aplicación. En nuestro ejemplo anterior sería:

 

This is the hidden content, please

 

Es importante destacar que al final de la línea hay que agregar un retorno de carro para que funcione. Los invito a crear un archivo testjava.zip que contenga el archivo HelloWorld.class, el directorio META-INF y dentro el archivo MANIFEST.MF con la linea Main-Class: HelloWorld. Para esto pueden usar los programas Winzip o WinRAR que pueden descargar gratuitamente (buscar en Google).

 

This is the hidden content, please

 

Una vez creado el archivo testjava.zip, lo renombramos a testjava.jar y lo ejecutamos desde la línea de comandos:

 

This is the hidden content, please

 

También podemos ejecutar haciendo doble click sobre el archivo JAR.

 

Como crear un archivo JAR ejecutable desde eclipse

 

Para crear un JAR ejecutable basta con ir a File-Export, seleccionar Runnable JAR file

 

This is the hidden content, please

 

Como se ve a continuación, en "Launch configuration" seleccionamos la que usamos para ejecutar la versión final de nuestra aplicación y en "Export destination" indicamos donde queremos guardar nuestro JAR y con que nombre:

 

This is the hidden content, please

 

This is the hidden content, please

 

Si java está bien instalado sobre Windows, con un doble click sobre minitennis.jar sería suficiente para ejecutar nuestra aplicación.

 

Examinando minitennis.jar

 

Si descomprimimos nuestro archivo minitennis.jar encontraremos los archivos *.class que componen nuestro juego. Estos archivos están dentro del árbol de directorios con los nombres de los paquetes java que contienen a las clases.

 

Además dentro de META-INF/MANIFEST.MF podemos ver en la última línea como se indica que el juego debe iniciarse con el método main() de la clase Game que esta en el paquete com.edu4java.minitennis8.

 

This is the hidden content, please

 

Eclipse realiza un excelente trabajo compilando, ejecutando y creando archivos JAR pero es bueno entender que por debajo eclipse usa la instalación de java de forma similar a nuestro ejemplo HelloWorld.

 

Bueno, esto es todo ... nos vemos en el próximo tutorial ;)

 

Saludos

 

Fuente: edu4java

Link to comment
Share on other sites

Guest
This topic is now closed to further replies.
×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.