En esta parte del tutorial de juegos Java 2D, trabajaremos con sprites, esos pequeños y entrañables objetos gráficos que se mueven graciosamente por las pantallas de nuestros juegos.
También uno de los significados es el código que encapsula a un personaje en un juego. En este tutorial al usar el término sprite nos refeririremos a un objeto móvil o su clase Java.
Sprite en movimiento
En el primer ejemplo tenemos una nave espacial. Podemos mover la nave espacial en el lienzo usando las teclas del cursor.
SpaceShip.java
package com.codbas;
import java.awt.Image;
import java.awt.event.KeyEvent;
import javax.swing.ImageIcon;
public class SpaceShip {
private int dx;
private int dy;
private int x = 40;
private int y = 60;
private int w;
private int h;
private Image image;
public SpaceShip() {
loadImage();
}
private void loadImage() {
ImageIcon ii = new ImageIcon("src/resources/spaceship.png");
image = ii.getImage();
w = image.getWidth(null);
h = image.getHeight(null);
}
public void move() {
x += dx;
y += dy;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getWidth() {
return w;
}
public int getHeight() {
return h;
}
public Image getImage() {
return image;
}
public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
if (key == KeyEvent.VK_LEFT) {
dx = -2;
}
if (key == KeyEvent.VK_RIGHT) {
dx = 2;
}
if (key == KeyEvent.VK_UP) {
dy = -2;
}
if (key == KeyEvent.VK_DOWN) {
dy = 2;
}
}
public void keyReleased(KeyEvent e) {
int key = e.getKeyCode();
if (key == KeyEvent.VK_LEFT) {
dx = 0;
}
if (key == KeyEvent.VK_RIGHT) {
dx = 0;
}
if (key == KeyEvent.VK_UP) {
dy = 0;
}
if (key == KeyEvent.VK_DOWN) {
dy = 0;
}
}
}
Esta clase representa una nave espacial. En esta clase mantenemos la imagen del sprite y las coordenadas del sprite. Los métodos keyPressed () y keyReleased () controlan si el sprite se está moviendo.
public void move() {
x += dx;
y += dy;
}
El método move () cambia las coordenadas del sprite. Estos valores x e y se usan en el método paintComponent () para dibujar la imagen del sprite.
if (key == KeyEvent.VK_LEFT) {
dx = 0;
}
Cuando soltamos la tecla de cursor izquierda, establecemos la variable dx en cero. La nave espacial dejará de moverse.
Esta es la clase Lienzo:
Lienzo.java
package com.codbas;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import javax.swing.JPanel;
import javax.swing.Timer;
public class Lienzo extends JPanel implements ActionListener {
private Timer timer;
private SpaceShip spaceShip;
private final int DELAY = 10;
public Lienzo() {
initLienzo();
}
private void initLienzo() {
addKeyListener(new TAdapter());
setBackground(Color.black);
setFocusable(true);
spaceShip = new SpaceShip();
timer = new Timer(DELAY, this);
timer.start();
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
doDrawing(g);
Toolkit.getDefaultToolkit().sync();
}
private void doDrawing(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
g2d.drawImage(spaceShip.getImage(), spaceShip.getX(),
spaceShip.getY(), this);
}
@Override
public void actionPerformed(ActionEvent e) {
step();
}
private void step() {
spaceShip.move();
repaint(spaceShip.getX()-1, spaceShip.getY()-1,
spaceShip.getWidth()+2, spaceShip.getHeight()+2);
}
private class TAdapter extends KeyAdapter {
@Override
public void keyReleased(KeyEvent e) {
spaceShip.keyReleased(e);
}
@Override
public void keyPressed(KeyEvent e) {
spaceShip.keyPressed(e);
}
}
}
En el método doDrawing (), dibujamos la nave espacial con el método drawImage(). Obtenemos la imagen y las coordenadas de la clase sprite.
@Override
public void actionPerformed(ActionEvent e) {
step();
}
El método actionPerformed() se llama cada DELAY ms. Llamamos al método step().
private void step() {
ship.move();
repaint(ship.getX()-1, ship.getY()-1,
ship.getWidth()+2, ship.getHeight()+2);
}
Movemos el sprite y repintamos la parte del tablero que ha cambiado. Utilizamos una pequeña técnica de optimización que vuelve a pintar solo el área pequeña de la ventana que realmente cambió.
private class TAdapter extends KeyAdapter {
@Override
public void keyReleased(KeyEvent e) {
craft.keyReleased(e);
}
@Override
public void keyPressed(KeyEvent e) {
craft.keyPressed(e);
}
}
En la clase de la Lienzo escuchamos los eventos clave. Los métodos anulados de la clase KeyAdapter delegan el procesamiento a los métodos de la clase Craft. Esta es la clasae principal:
MovingSpriteEx.java
package com.codbas;
import java.awt.EventQueue;
import javax.swing.JFrame;
public class MovingSpriteEx extends JFrame {
public MovingSpriteEx() {
initUI();
}
private void initUI() {
add(new Board());
setTitle("Moving sprite");
setSize(400, 300);
setLocationRelativeTo(null);
setResizable(false);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
MovingSpriteEx ex = new MovingSpriteEx();
ex.setVisible(true);
});
}
}
Y asi va quedando de momento el programa:
¡¡¡ DISPARANDO MISILES !!!
En el siguiente ejemplo, agregamos otro tipo de sprite a nuestro ejemplo: un misil. Los misiles se lanzan con la tecla Space.
Sprite.java
package com.codbas;
import java.awt.Image;
import javax.swing.ImageIcon;
public class Sprite {
protected int x;
protected int y;
protected int width;
protected int height;
protected boolean visible;
protected Image image;
public Sprite(int x, int y) {
this.x = x;
this.y = y;
visible = true;
}
protected void loadImage(String imageName) {
ImageIcon ii = new ImageIcon(imageName);
image = ii.getImage();
}
protected void getImageDimensions() {
width = image.getWidth(null);
height = image.getHeight(null);
}
public Image getImage() {
return image;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public boolean isVisible() {
return visible;
}
public void setVisible(Boolean visible) {
this.visible = visible;
}
}
La clase Sprite comparte código común de las clases Missile y SpaceShip.
public Sprite(int x, int y) {
this.x = x;
this.y = y;
visible = true;
}
El constructor inicia las coordenadas x e y y la variable visible.
Missile.java
package com.codbas;
public class Missile extends Sprite {
private final int BOARD_WIDTH = 390;
private final int MISSILE_SPEED = 2;
public Missile(int x, int y) {
super(x, y);
initMissile();
}
private void initMissile() {
loadImage("src/resources/missile.png");
getImageDimensions();
}
public void move() {
x += MISSILE_SPEED;
if (x > BOARD_WIDTH) {
visible = false;
}
}
}
Aquí tenemos un nuevo sprite llamado Misil.
public void move() {
x += MISSILE_SPEED;
if (x > BOARD_WIDTH) {
vis = false;
}
}
El misil se mueve a velocidad constante. Cuando golpea el borde derecho del tablero, se vuelve invisible. Luego se elimina de la lista de misiles. Esta es la clase SpaceShip.
SpaceShip.java
package com.codbas;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
public class SpaceShip extends Sprite {
private int dx;
private int dy;
private List<Missile> missiles;
public SpaceShip(int x, int y) {
super(x, y);
initSpaceShip();
}
private void initSpaceShip() {
missiles = new ArrayList<>();
loadImage("src/resources/spaceship.png");
getImageDimensions();
}
public void move() {
x += dx;
y += dy;
}
public List<Missile> getMissiles() {
return missiles;
}
public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
if (key == KeyEvent.VK_SPACE) {
fire();
}
if (key == KeyEvent.VK_LEFT) {
dx = -1;
}
if (key == KeyEvent.VK_RIGHT) {
dx = 1;
}
if (key == KeyEvent.VK_UP) {
dy = -1;
}
if (key == KeyEvent.VK_DOWN) {
dy = 1;
}
}
public void fire() {
missiles.add(new Missile(x + width, y + height / 2));
}
public void keyReleased(KeyEvent e) {
int key = e.getKeyCode();
if (key == KeyEvent.VK_LEFT) {
dx = 0;
}
if (key == KeyEvent.VK_RIGHT) {
dx = 0;
}
if (key == KeyEvent.VK_UP) {
dy = 0;
}
if (key == KeyEvent.VK_DOWN) {
dy = 0;
}
}
}
Si presionamos la tecla Espacio, disparamos.
if (key == KeyEvent.VK_SPACE) {
fire();
}
El método fire() crea un nuevo objeto Missile y lo agrega a la lista de misiles.
public void fire() {
missiles.add(new Missile(x + width, y + height / 2));
}
El método getMissiles() devuelve la lista de misiles. Se llama desde la clase Lienzo.
public List<Missile> getMissiles() {
return missiles;
}
Esta es la clase Lienzo:
Lienzo.java
package com.zetcode;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.List;
import javax.swing.JPanel;
import javax.swing.Timer;
public class Board extends JPanel implements ActionListener {
private final int ICRAFT_X = 40;
private final int ICRAFT_Y = 60;
private final int DELAY = 10;
private Timer timer;
private SpaceShip spaceShip;
public Board() {
initBoard();
}
private void initBoard() {
addKeyListener(new TAdapter());
setBackground(Color.BLACK);
setFocusable(true);
spaceShip = new SpaceShip(ICRAFT_X, ICRAFT_Y);
timer = new Timer(DELAY, this);
timer.start();
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
doDrawing(g);
Toolkit.getDefaultToolkit().sync();
}
private void doDrawing(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
g2d.drawImage(spaceShip.getImage(), spaceShip.getX(),
spaceShip.getY(), this);
List<Missile> missiles = spaceShip.getMissiles();
for (Missile missile : missiles) {
g2d.drawImage(missile.getImage(), missile.getX(),
missile.getY(), this);
}
}
@Override
public void actionPerformed(ActionEvent e) {
updateMissiles();
updateSpaceShip();
repaint();
}
private void updateMissiles() {
List<Missile> missiles = spaceShip.getMissiles();
for (int i = 0; i < missiles.size(); i++) {
Missile missile = missiles.get(i);
if (missile.isVisible()) {
missile.move();
} else {
missiles.remove(i);
}
}
}
private void updateSpaceShip() {
spaceShip.move();
}
private class TAdapter extends KeyAdapter {
@Override
public void keyReleased(KeyEvent e) {
spaceShip.keyReleased(e);
}
@Override
public void keyPressed(KeyEvent e) {
spaceShip.keyPressed(e);
}
}
}
En el método doDrawing(), dibujamos la nave y todos los misiles disponibles.
private void doDrawing(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
g2d.drawImage(spaceShip.getImage(), spaceShip.getX(),
spaceShip.getY(), this);
List<Missile> missiles = spaceShip.getMissiles();
for (Missile missile : missiles) {
g2d.drawImage(missile.getImage(), missile.getX(),
missile.getY(), this);
}
}
En el método updateMissiles() analizamos todos los misiles de la lista de misiles. Dependiendo de lo que devuelva el método isVisible(), movemos el misil o lo retiramos del contenedor.
private void updateMissiles() {
List<Missile> missiles = spaceShip.getMissiles();
for (int i = 0; i < missiles.size(); i++) {
Missile missile = missiles.get(i);
if (missile.isVisible()) {
missile.move();
} else {
missiles.remove(i);
}
}
}
Finalmente, asi queda la clase principal:
ShootingMissilesEx.java
package com.zetcode;
import java.awt.EventQueue;
import javax.swing.JFrame;
public class ShootingMissilesEx extends JFrame {
public ShootingMissilesEx() {
initUI();
}
private void initUI() {
add(new Board());
setSize(400, 300);
setResizable(false);
setTitle("Shooting missiles");
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
ShootingMissilesEx ex = new ShootingMissilesEx();
ex.setVisible(true);
});
}
}
Y asi se deberia ver el programita:

