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: