Cargar una imagen de sprites en java
bufferedimage (1)
Bien, entonces hay muchas cosas que necesitamos saber.
-
Cuántas imágenes componen la hoja de sprites, cómo se distribuyen (filas / cols), si hay un número desigual de imágenes (
count != rows * cols
) e incluso el tamaño de cada sprite - Qué tan lejos estamos a través de un ciclo dado (digamos un segundo)
Entonces, según tu imagen de una pregunta anterior ...
En base a esto, sabemos que hay 5 columnas, 4 filas pero solo 19 imágenes. Ahora podría pasar mucho tiempo, escribir mucho código para cada hoja de sprites posible, o podría intentar cometer algunos de esos problemas ...
public class SpriteSheet {
private final List<BufferedImage> sprites;
public SpriteSheet(List<BufferedImage> sprites) {
this.sprites = new ArrayList<>(sprites);
}
public int count() {
return sprites.size();
}
public BufferedImage getSprite(double progress) {
int frame = (int) (count() * progress);
return sprites.get(frame);
}
}
Entonces, esto es bastante básico, es simplemente una lista de imágenes.
La parte especial es el método
getSprite
, que avanza a través del ciclo de animación actual y devuelve una imagen en función del número de imágenes que tiene disponibles.
Básicamente, esto desacopla el concepto del tiempo del sprite y le permite definir el significado de un "ciclo" externamente.
Ahora, debido a que el proceso real de construir un
SpriteSheet
involucra muchas variables posibles, un constructor sería una buena idea ...
public class SpriteSheetBuilder {
private BufferedImage spriteSheet;
private int rows, cols;
private int spriteWidth, spriteHeight;
private int spriteCount;
public SpriteSheetBuilder withSheet(BufferedImage img) {
spriteSheet = img;
return this;
}
public SpriteSheetBuilder withRows(int rows) {
this.rows = rows;
return this;
}
public SpriteSheetBuilder withColumns(int cols) {
this.cols = cols;
return this;
}
public SpriteSheetBuilder withSpriteSize(int width, int height) {
this.spriteWidth = width;
this.spriteHeight = height;
return this;
}
public SpriteSheetBuilder withSpriteCount(int count) {
this.spriteCount = count;
return this;
}
protected int getSpriteCount() {
return spriteCount;
}
protected int getCols() {
return cols;
}
protected int getRows() {
return rows;
}
protected int getSpriteHeight() {
return spriteHeight;
}
protected BufferedImage getSpriteSheet() {
return spriteSheet;
}
protected int getSpriteWidth() {
return spriteWidth;
}
public SpriteSheet build() {
int count = getSpriteCount();
int rows = getRows();
int cols = getCols();
if (count == 0) {
count = rows * cols;
}
BufferedImage sheet = getSpriteSheet();
int width = getSpriteWidth();
int height = getSpriteHeight();
if (width == 0) {
width = sheet.getWidth() / cols;
}
if (height == 0) {
height = sheet.getHeight() / rows;
}
int x = 0;
int y = 0;
List<BufferedImage> sprites = new ArrayList<>(count);
for (int index = 0; index < count; index++) {
sprites.add(sheet.getSubimage(x, y, width, height));
x += width;
if (x >= width * cols) {
x = 0;
y += height;
}
}
return new SpriteSheet(sprites);
}
}
Entonces, de nuevo, basado en su hoja de sprites, esto significa que podría construir una
SpriteSheet
usando algo como ...
spriteSheet = new SpriteSheetBuilder().
withSheet(sheet).
withColumns(5).
withRows(4).
withSpriteCount(19).
build();
pero me da el poder de construir cualquier cantidad de
SpriteSheets
, todas las cuales pueden estar compuestas de diferentes matrices
Pero ahora que tenemos una
SpriteSheet
, necesitamos alguna forma de animarlos, pero lo que realmente necesitamos es una forma de calcular la progresión a través de un ciclo dado (digamos que un segundo es un ciclo), podríamos usar un simple "motor" , algo como...
public class SpriteEngine {
private Timer timer;
private int framesPerSecond;
private Long cycleStartTime;
private TimerHandler timerHandler;
private double cycleProgress;
private List<ActionListener> listeners;
public SpriteEngine(int fps) {
framesPerSecond = fps;
timerHandler = new TimerHandler();
listeners = new ArrayList<>(25);
}
public int getFramesPerSecond() {
return framesPerSecond;
}
public double getCycleProgress() {
return cycleProgress;
}
protected void invaldiate() {
cycleProgress = 0;
cycleStartTime = null;
}
public void stop() {
if (timer != null) {
timer.stop();
}
invaldiate();
}
public void start() {
stop();
timer = new Timer(1000 / framesPerSecond, timerHandler);
timer.start();
}
public void addActionListener(ActionListener actionListener) {
listeners.add(actionListener);
}
public void removeActionListener(ActionListener actionListener) {
listeners.remove(actionListener);
}
protected class TimerHandler implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
if (cycleStartTime == null) {
cycleStartTime = System.currentTimeMillis();
}
long diff = (System.currentTimeMillis() - cycleStartTime) % 1000;
cycleProgress = diff / 1000.0;
ActionEvent ae = new ActionEvent(SpriteEngine.this, ActionEvent.ACTION_PERFORMED, e.getActionCommand());
for (ActionListener listener : listeners) {
listener.actionPerformed(ae);
}
}
}
}
Ahora, esto es básicamente una clase de contenedor para un Swing
Timer
, pero lo que hace es calcular la progresión del ciclo para nosotros.
También tiene un buen soporte de
ActionListener
, por lo que podemos ser notificados cuando ocurre un tic
Está bien, pero ¿cómo funciona todo eso junto?
Básicamente, con uno o más
SpriteSheet
sy un
SpriteEngine
, podemos pintar las hojas haciendo algo como ...
public class TestPane extends JPanel {
private SpriteSheet spriteSheet;
private SpriteEngine spriteEngine;
public TestPane() {
try {
BufferedImage sheet = ImageIO.read(...);
spriteSheet = new SpriteSheetBuilder().
withSheet(sheet).
withColumns(5).
withRows(4).
withSpriteCount(19).
build();
spriteEngine = new SpriteEngine(25);
spriteEngine.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
repaint();
}
});
spriteEngine.start();
} catch (IOException ex) {
ex.printStackTrace();
}
}
@Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
BufferedImage sprite = spriteSheet.getSprite(spriteEngine.getCycleProgress());
int x = (getWidth() - sprite.getWidth()) / 2;
int y = (getHeight() - sprite.getHeight()) / 2;
g2d.drawImage(sprite, x, y, this);
g2d.dispose();
}
}
Ahora bien, eso es bastante básico, pero da una idea.
Para las entidades que desea mover (o rotar), crearía otra clase que contuviera esa información Y el
spriteSheet
.
Esto podría contener un método de "pintura" o podría usar las propiedades del objeto para luego pintar los cuadros individuales ...
Algo como...
public interface PaintableEntity {
public void paint(Graphics2D g2d, double progress);
}
public class AstroidEntity implements PaintableEntity {
private SpriteSheet spriteSheet;
private Point location;
private double angel;
public AstroidEntity(SpriteSheet spriteSheet) {
this.spriteSheet = spriteSheet;
location = new Point(0, 0);
angel = 0;
}
public void update() {
// Apply movement and rotation deltas...
}
public void paint(Graphics2D g2d, double progress) {
g2d.drawImage(
spriteSheet.getSprite(progress),
location.x,
location.y,
null);
}
}
Que podría crearse usando algo como ...
private List<PaintableEntity> entities;
//...
entities = new ArrayList<>(10);
try {
BufferedImage sheet = ImageIO.read(new File("..."));
SpriteSheet spriteSheet = new SpriteSheetBuilder().
withSheet(sheet).
withColumns(5).
withRows(4).
withSpriteCount(19).
build();
for (int index = 0; index < 10; index++) {
entities.add(new AstroidEntity(spriteSheet));
}
} catch (IOException ex) {
ex.printStackTrace();
}
Tenga en cuenta que solo creé la hoja de sprites una vez.
Esto podría requerir que proporcione un "desplazamiento" aleatorio a
AstroidEntity
que cambiará el marco que devuelve para un valor de progreso dado ...
Una forma simple podría ser agregar ...
public SpriteSheet offsetBy(int amount) {
List<BufferedImage> images = new ArrayList<>(sprites);
Collections.rotate(images, amount);
return new SpriteSheet(images);
}
a
SpriteSheet
, luego en
AstroidEntity
puedes crear una
SpriteSheet
compensada usando algo como ...
public AstroidEntity(SpriteSheet spriteSheet) {
this.spriteSheet = spriteSheet.offsetBy((int) (Math.random() * spriteSheet.count()));
La pintura se puede hacer usando algo como ...
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
for (PaintableEntity entity : entities) {
entity.paint(g2d, spriteEngine.getCycleProgress());
}
g2d.dispose();
}
Básicamente, el factor clave aquí es tratar de desacoplar su código de un concepto de "tiempo".
Por ejemplo, cambié los cuadros por segundo que el motor estaba usando a 60 y no vi ningún cambio en la animación de los sprites, porque estaba trabajando en el concepto de un solo ciclo de tiempo de 1 segundo. Sin embargo, esto le permitiría cambiar la velocidad a la que los objetos se movían de manera relativamente simple.
También puede configurar el motor para que tenga un concepto de "duración del ciclo" y hacer que sea medio segundo o 5 segundos, lo que luego afectaría la velocidad de la animación, ¡pero el resto de su código permanecería sin cambios!
Quiero preguntar si por qué recibo un error al cargar imágenes de sprites en el objeto
así es como obtengo la imagen.
import java.awt.image.BufferedImage;
import java.io.IOException;
public class SpriteSheet {
public BufferedImage sprite;
public BufferedImage[] sprites;
int width;
int height;
int rows;
int columns;
public SpriteSheet(int width, int height, int rows, int columns, BufferedImage ss) throws IOException {
this.width = width;
this.height = height;
this.rows = rows;
this.columns = columns;
this.sprite = ss;
for(int i = 0; i < rows; i++) {
for(int j = 0; j < columns; j++) {
sprites[(i * columns) + j] = ss.getSubimage(i * width, j * height, width, height);
}
}
}
}
así es como lo estoy implementando
public BufferedImage[] init(){
BufferedImageLoader loader = new BufferedImageLoader();
BufferedImage spriteSheet = null;
SpriteSheet ss = null;
try {
spriteSheet = loader.loadImage("planet.png");
ss = new SpriteSheet(72,72,4,5,spriteSheet);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return ss.sprites;
}
También quiero preguntar sobre mi forma de usar la matriz de sprites. Quiero usar en el temporizador cambiando la imagen dibujada por evento de acción cambiando la imagen de sprite actual
tmr = new Timer(20, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
for(Rock r:rocks){
r.move();
r.changeSprite();
}
repaint();
}
});
con el método
public void changeSprite(){
if(ds==12)
ds=0;
ds++;
currentSprite = sprite[ds];
}
Cada objeto de roca tiene una matriz de sprites llena de imágenes almacenadas en búfer recibidas de la imagen de sprites cargada y una imagen actual que se dibuja. el temporizador cambiará la imagen actual y la volverá a dibujar en el objeto para que se dibuje todo el sprite, pero parece que no funciona. Entonces, ¿es mi loadingSpriteImage el que tiene el problema o mi forma de dibujarlo causando el problema?