viernes, 17 de abril de 2009

Pong

Muy bien! Suficiente teoria, es hora de hacer algo, como primer juego, me gusta hacer un Pong, me parece el mas simple de todos, y engloba bien hit test, input y animacion, que es lo que vimos hasta ahora.
Creamos un nuevo proyecto, yo lo llame Pong, y el package "pong".
Veamos la clas Main, la "principal" del proyecto, esta solo tendra la ventana y creara un nuevo Board.

package pong;

import javax.swing.JFrame;

/**
*
* @author fede
*/
public class Main extends JFrame {
public Main(){
add(new Board());
setTitle("Basic JPong");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(300,280);
setLocationRelativeTo(null);
setVisible(true);
setResizable(false);
}

public static void main(String[] args){
new Main();
}
}


Como veran es bastante parecido a lo que veniamos haciendo, solo cambie el tamaño y el titulo.
Veamos ahora la clase Player, esa clase es la encargada de las "barritas" que seran los jugadores, en este caso usaremos dos, para hacerlo de dos jugadores, aunque podrias hacer otra clase para el jugador dos, que sea inteligencia artificial y juege solo, asi seria de un jugador. En este ejemplo lo haremos de dos jugadores.
Primero, las librerias que importamos, ellas son

import javax.swing.ImageIcon;
import java.awt.Image;
import java.awt.Rectangle;

// Para tener las constantes de las teclas y el tipo KeyEvent, despues se manda por argumento bien
import java.awt.event.KeyEvent;


Ahora hacemos las variables que usaremos (esto yo lo hize antes, por eso se todas las variables que usare, por lo general esto lo modifico mucho, al igual que los 'imports')

private Image image;
private int x,y,dy,upKey,downKey,width,height,score;

private final int SPEED = 4;


En el constructor iniciamos los valores necesarios, y luego es mayormente get's y set's. Aca les dejo la clase Player

package pong;

import javax.swing.ImageIcon;
import java.awt.Image;
import java.awt.Rectangle;

import java.awt.event.KeyEvent;

/**
*
* @author fede
*/
public class Player {
private Image image;
private int x,y,dy,upKey,downKey,width,height,score;

private final int SPEED = 4;

public Player(int x, int y, String img, int upKey, int downKey){
ImageIcon ii = new ImageIcon(this.getClass().getResource(img));
image = ii.getImage();
this.x = x; // Es irrelevante para getear, solo se mueve arriba y abajo la barra
this.y = y;
dy = 0;
this.upKey = upKey;
this.downKey = downKey;
width = image.getWidth(null);
height = image.getHeight(null);
score = 0;
}

public Rectangle getBounds(){
return new Rectangle(x,y,width,height);
}

public Image getImage(){
return image;
}

public int getY(){
return y;
}

public int getX(){
return x;
}

public int getScore(){
return score;
}

public void setScore(int score){
this.score = score;
}

public void move(){
if((y > 0 && dy <> 0))
y += dy;
}

public void keyPressed(KeyEvent e){
int key = e.getKeyCode();

if(key == upKey)
dy = SPEED * -1;
else if(key == downKey)
dy = SPEED;
}

public void keyReleased(KeyEvent e){
dy = 0;
}
}


La funcion keyPressed se llamara cuando en board se presione alguna tecla, y mandamos la tecla por argumento, asi podemos evaluar bien que hacer con ella en nuestra clase, lo imsmo para keyReleased. La funcion move se llama en el update (en este caso, cada 5ms) y en ella actualizamos la posicion del sprite.
Ahora Ball.java. Esta clase es bastante parecida a la anterior por lo que no explicare mucho.

package pong;

import java.awt.Image;
import java.awt.Rectangle;

import javax.swing.ImageIcon;


/**
*
* @author fede
*/
public class Ball {
private Image image;
private int x,y,dx,dy,width,height;
private final int SPEED = 2;
public Ball(){
ImageIcon ii = new ImageIcon(this.getClass().getResource("ball.png"));
image = ii.getImage();
x = y = 30;
dy = dx = SPEED;
width = image.getWidth(null);
height = image.getHeight(null);
}

public Rectangle getBounds(){
return new Rectangle(x,y,width,height);
}

public Image getImage(){
return image;
}

public int getX(){
return x;
}

public int getY(){
return y;
}

public int getDy(){
return dy;
}

public int getDx(){
return dx;
}

public void setDx(int dx){
this.dx = dx;
}

public void setDy(int dy){
this.dy = dy;
}

public void setPosition(int x, int y, boolean invert){
this.x = x;
this.y = y;
if(invert){
dy*=-1;
dx*=-1;
}
}

public void move(){
y+=dy;
x+=dx;
}
}


Como veran solo importamos lo necesario para usar la imagen y rectangles. La mayoria de las funciones son get's y set's, setPosition se usa para resetear facimente la posicion de la pelota, move actualiza la posicion, igual que en Player, el resto es solo iniciar variables en el constructor.
Finalmente veremos la clase Board. Es muy importante que hallan seguido el blog hasta ahora ya que utiliza todo lo que vimos hasta ahora.

package pong;

import javax.swing.JPanel;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Color;
import java.awt.Rectangle; // Para collision detection

// Timer
import java.awt.Toolkit;
import javax.swing.Timer;

// Para leer las teclas
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; // interface con un metodo, actionPerformed
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

// Para escribir
import java.awt.Font;
import java.awt.FontMetrics;

/**
*
* @author fede
*/
public class Board extends JPanel implements ActionListener {
private Player player1, player2;
private Timer timer;
private Ball ball;
private Font small;
private String msg;

private static final int MAX_SCORE = 10;

public Board(){
setDoubleBuffered(true);
setBackground(Color.white);
setFocusable(true);
addKeyListener(new TAdapter());

player1 = new Player(3,100,"bar.png", KeyEvent.VK_UP, KeyEvent.VK_DOWN);
player2 = new Player(285,100, "bar.png", KeyEvent.VK_W, KeyEvent.VK_S);
ball = new Ball();

// font
small = new Font("Verdana", Font.PLAIN, 12);
FontMetrics mtr = this.getFontMetrics(small);

// Timer
timer = new Timer(5, this);
timer.start();
}

public void paint(Graphics g){
super.paint(g);

Graphics2D g2d = (Graphics2D)g;

// Paint
g2d.drawImage(player1.getImage(), player1.getX(), player1.getY(), this);
g2d.drawImage(player2.getImage(), player2.getX(), player2.getY(), this);
g2d.drawImage(ball.getImage(), ball.getX(), ball.getY(), this);

// Write
g2d.setColor(Color.black);
g2d.setFont(small);
if(isGameOver()==0)
msg = "P1: "+player1.getScore()+" - P2: "+player2.getScore();
g2d.drawString(msg, 10, 13);

// Timer
Toolkit.getDefaultToolkit().sync();

g.dispose();
}

public void actionPerformed(ActionEvent e){ // Esto updatea el timer
// Logic
player1.move();
player2.move();
ball.move();

// Collision Detection
Rectangle br = ball.getBounds();
Rectangle p1r = player1.getBounds();
Rectangle p2r = player2.getBounds();

if(ball.getX() <>290 || br.intersects(p1r) || br.intersects(p2r))
ball.setDx(ball.getDx()*-1);
if(ball.getY() <>250)
ball.setDy(ball.getDy()*-1);

// Aver score
if(ball.getX() <= 0){
player2.setScore(player2.getScore()+1);
ball.setPosition(150,100, true);
}
if(ball.getX() >= 285){
player1.setScore(player1.getScore()+1);
ball.setPosition(150,100, true);
}
if(isGameOver()>0){
msg = "Player " + isGameOver() + " wins - Press any key to play again";
ball.setDx(0);
ball.setDy(0);
}

repaint();
}

private int isGameOver(){
if(player1.getScore() >= MAX_SCORE || player2.getScore() >= MAX_SCORE){
if(player1.getScore()>player2.getScore())
return 1;
else
return 2;
}
else
return 0;
}

private class TAdapter extends KeyAdapter {
public void keyPressed(KeyEvent e){
player1.keyPressed(e);
player2.keyPressed(e);
if(isGameOver()>0){
player1.setScore(0);
player2.setScore(0);
ball = new Ball();
}
}
public void keyReleased(KeyEvent e){
player1.keyReleased(e);
player2.keyReleased(e);
}
}
}


Primero que nada vemos que usamos muchas librerias, de las cuales todas estan "explicadas" y usadas anteriormente, excepto las que usamos para escribir, ellas son

import java.awt.Font;
import java.awt.FontMetrics;

Para escribir, en el draw simplemente hacemos

g2d.setColor(Color.black);
g2d.setFont(small);
if(isGameOver()==0)
msg = "P1: "+player1.getScore()+" - P2: "+player2.getScore();
g2d.drawString(msg, 10, 13);


setColor es en el color que vamos a escribir, en este caso como el fondo es blanco, usaremos negro. setFont usa una font que definimos en el constructor, finalmente drawString escribe la string msg en las coordenadas 10, 13.
Para hacer una font, en el constructor pusimos esto

small = new Font("Verdana", Font.PLAIN, 12);
FontMetrics mtr = this.getFontMetrics(small);


Luego tenemos nuestro metodo paint, ahi dibujamos los players, la pelota y escribimos.
En actionPerformed hacemos las actualizaciones move() de los players y de la pelota, ademas nos fijamos por colisiones y por si el juego acabo.
Finalmente en keyAdapter, mandamos las teclas apretadas a que se procesen en los objetos player.
Si venian siguiendo el blog hasta ahora, espero este claro todo lo que hize, cualquier duda pueden contactarme :)



Eso fue Pong.
Codigo Fuente: de 4shared

Deteccion de Colision

Collision Detection, Hit Test, etc, basicamente, saber cuando se tocan dos sprites. Esto es topico de discusion algunas veces, de los metodos mas usados es usando rectangulos y ver cuando se tocan, en Java esto es facil, gracias a la clase Rectangle, de awt. He viso otros metodos que tienen hasta precision de pixel, pero no en Java, y creo que para este tutorial y las necesidades que tendremos, esto sera suficiente.
Lo que haremos para detectar colision es encerrar el sprite en un rectangulo que lo cubra "perfectamente", y vemos cuando colisiona con otro rectangulo, que sera el que encierra el sprite con el que queremos ver si hay colision o no.
Para esto, primero tenemos que tener como minimo dos sprites, asi que crearemos otro sprite fijo, y haremos que algo ocurra cuando nuestro sprite (al cual movemos con nuestro teclado) lo toque.
Vamos a crear una nueva clase, renombre el package a "hittest", llamada "Coin" (el sprite que mostraremos sera una moneda) asi que la nueva clase se ve algo asi

package hittest;

import java.awt.Image;

import javax.swing.ImageIcon;

/**
*
* @author fede
*/
public class Coin {
Image image;
public Coin(){
ImageIcon ii = new ImageIcon(this.getClass().getResource("coin.png"));
image = ii.getImage();
}
}


Veran que ya importe Image e ImageIcon y carge la imagen, como veniamos haciendo, ahora vamos a definir la posicion de la imagen para despues dibujarla, simplemente creamos dos variables privadas, x e y, y en el constructor les asignamos cualquier valor dentro de la ventana. Hacemos los respectivos get's.

public class Coin {
private Image image;
private int x,y;
public Coin(){
ImageIcon ii = new ImageIcon(this.getClass().getResource("coin.png"));
image = ii.getImage();
x = 10;
y = 40;
}

public int getX(){
return x;
}

public int getY(){
return y;
}

public Image getImage(){
return image;
}
}


Ahora vamos a agregar una funcion que me devuelve un rectangulo que "envuelve" completamente nuestro sprite, para eso, importamos "java.awt.Rectangle" y hacemos la siguiente funcion

public Rectangle getBounds(){
return new Rectangle(x, y, image.getWidth(null), image.getHeight(null));
}


Volvamos ahora a la clase Board, a agregar nuestro nuevo sprite. Simplemente hacemos un objeto privado, luego hacemos una instancia de este objeto en el constructor, y en la parte de dibujar, lo agregamos.



Como podran ver, ya esta dibujado, ahora vamos a agregar una funcion para que nuestro sprite anterior (al cual debimos haber hecho en otra clase, pero preferi en la misma por simplicidad) nos devuelva un rectangulo que lo encierre, es practicamente igual a la funcion en Coin, pero para poder hacerla en Board, tambien tenemos que importar java.awt.Rectangle.
Una vez que hallamos hecho la funcion, vamos a la parte de la logica (actionPerformed) y agregamos la condicion

Rectangle rectanglePlayer = getBounds();
Rectangle rectangleCoin = coin.getBounds();
if(rectangleCoin.intersects(rectanglePlayer))
x = y = 200;


Como veran, es bastante simple, basta con ver si se intersectan, y luego especificar que sucede, en este caso, el personaje se mueve a las coordenadas 200, 200 como muestra la siguiente imagen.



Sprites mas complejos pueden constrar de varios rectangulos, definidos en su clase, y la comparacion se volveria mas minuciosa, pero eso se lo dejo a su imaginacion y complejidad de juego que tengan en mente hacer, para los ejemplos de este blog esto sera suficiente.
Descargar codigo fuente: de 4shared

Recibiendo Input

Otra de las cosas fundamentales en la programacion de juegos, es recibir input del teclado, es decir, saber que teclas se apretan, y cuales se sueltan, para poder asi mover los sprites a voluntad.
Una vez que comenzamos a recibir input tenemos que tener algo muy importante en cuenta, es mas comodo y por decirlo asi un estandar, y mejor la performance, dividir el juego en logica y dibujar. El dibujo se ejecuta las veces que decimos que se actualize la pantalla, y la logica lo hace mas rapido aun, asi aunque el juego sea muy lento o ande mal el input llegara bien y podremos animar los dibujos.
En nuestro ejemplo anterior, se movia automaticamente, ahora vamos a hacer, que lo movamos nosotros, usando el teclado, en este caso, las flechas, pero puede ser cualquier boton.
Primero que nada las clases que debemos importar, esas son KeyAdapter y KeyEvent, de awt.
Esas clases combinadas nos permiten leer las teclas que se han apretado, o soltado.
Agregamos los imports


import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;


En el constructor tenemos que hacer que la ventana sea focus-able, es decir que pueda estar en primer plano y asi recibir el input, esto es muy importante ya que sin esto no funciona, y es solo una linea de codigo, lo otro que vamos a agregar en el constructor es un KeyListener, esto "escucha" constantemente nuevas entradas (inputs).

this.setFocusable(true);
this.addKeyListener(new Adapter());


Otra cosa que podemos agregar es DoubleBuffering, esta opcion recomiendo ponerla siempre, no cuesta nada y ayuda mucho cuando el juego va creciendo

this.setDoubleBuffered(true);


Basicamente, dibuja primero en memoria, y luego todo junto en pantalla de una sola vez, es mucho mejor asi.
Otra cosa que tenemos que actualizar son los dx y dy, en el anterior valian la velocidad, es decir empezaban valiendo 2, pero nosotros queremos que se mueva por las teclas, asi que los vamos a poner igual a 0, y lo cambiamos cuando apretamos las teclas, que en este caso seran las flechas.

dx = dy = 0;


Ahora, dentro de la clase Board, hacemos otra clase, Adapter, que hereda de KeyAdapter.

private class Adapter extends KeyAdapter{
@Override
public void keyPressed(KeyEvent e){
int key = e.getKeyCode();

if(key == KeyEvent.VK_UP)
dy = SPEED * -1;
if(key == KeyEvent.VK_DOWN)
dy = SPEED;
if(key == KeyEvent.VK_RIGHT)
dx = SPEED;
if(key == KeyEvent.VK_LEFT)
dx = SPEED * -1;
}

@Override
public void keyReleased(KeyEvent e){
int key = e.getKeyCode();

if(key == KeyEvent.VK_UP)
dy = 0;
if(key == KeyEvent.VK_DOWN)
dy = 0;
if(key == KeyEvent.VK_RIGHT)
dx = 0;
if(key == KeyEvent.VK_LEFT)
dx = 0;
}
}


Puden ver los @Override, indican simplemente que la funcion de abajo esta siendo sobre-escrita de la clase que heredamos. Las funciones en si son bastante faciles de entender, keyPressed es cuando se presiona una tecla, y keyReleased cuando se suelta, ambas requieren como argumento un KeyEvent, ahi es donde podemos ver que tecla se apreto, ese argumento contiene la tecla, luego simplemente comparamos cual fue y cambiamos dx y dy.
Ahora, editamos la funcion actionPerformed

public void actionPerformed(ActionEvent e){ // Se ejecuta cada 5ms
if((x>0 && dx<0)>0))
x += dx;
if((y>0 && dy<0)>0))
y += dy;
repaint(); // "re-pintamos" el panel
}


Lo que hace eso es pura logica, la anterior no funcionaba, aca decimos, si la posicion x del sprite es mayor que 0 y lo quiero mover hacia la izuiqerda, o la posicion x es menor que el limite de la pantalla y lo quiero mover a la derecha, lo muevo x)
Lo importante para resaltar sin embargo aca, es la clase Adapter y como la agregamos en el constructor.
Una vez que hallamos hecho todos los cambios, ya podemos mover el sprite con las flechas de nuestro teclado.
Aca les dejo Board.java

package animation;

import java.awt.Image;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JPanel;
import javax.swing.ImageIcon;
import java.awt.Color;

// Timer Imports
import java.awt.Toolkit;
import javax.swing.Timer;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent; // Para poder usar actionPerformed, necesitamos este tipo

// Para leer teclas
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

/**
*
* @author fede
*/
public class Board extends JPanel implements ActionListener {
private Image image;
private Timer timer;
private int x, y, dx, dy;
private static int SPEED = 2;

public Board(){
ImageIcon ii = new ImageIcon(this.getClass().getResource("image.png"));
image = ii.getImage();
this.setBackground(Color.white);
this.setFocusable(true);
this.addKeyListener(new Adapter());
this.setDoubleBuffered(true); // Dibujo en memoria antes que en pantalla
x = 150;
y = 10;
dx = dy = 0;

// Timer
timer = new Timer(5, this); // cada 5ms llama actionPerformed
timer.start();
}

public void paint(Graphics g){
super.paint(g);

Graphics2D g2d = (Graphics2D)g; // Convertimos a g de Graphics a Graphics2D
g2d.drawImage(image, x, y, this);

// Timer
Toolkit.getDefaultToolkit().sync(); // fuerza sincronizacion, basicamente

g.dispose();
}

public void actionPerformed(ActionEvent e){ // Se ejecuta cada 5ms
if((x>0 && dx<0)>0))
x += dx;
if((y>0 && dy<0)>0))
y += dy;
repaint(); // "re-pintamos" el panel
}

private class Adapter extends KeyAdapter{
@Override
public void keyPressed(KeyEvent e){
int key = e.getKeyCode();

if(key == KeyEvent.VK_UP)
dy = SPEED * -1;
if(key == KeyEvent.VK_DOWN)
dy = SPEED;
if(key == KeyEvent.VK_RIGHT)
dx = SPEED;
if(key == KeyEvent.VK_LEFT)
dx = SPEED * -1;
}

@Override
public void keyReleased(KeyEvent e){
int key = e.getKeyCode();

if(key == KeyEvent.VK_UP)
dy = 0;
if(key == KeyEvent.VK_DOWN)
dy = 0;
if(key == KeyEvent.VK_RIGHT)
dx = 0;
if(key == KeyEvent.VK_LEFT)
dx = 0;
}
}
}


La imagen en este caso, sera exactamente la misma que la anterior, deberian probarlo para ver la diferencia x)



Bueno, moverlo es lo de menos, lo mas importante de esta parte es lo de leer las teclas, ya lo demas es simple logica, espero se halla entendido bien esa parte.