在 java 中加载精灵图像
Load a sprites image in java
我想问问为什么在将任何精灵图像加载到对象中时出错
这是我获取图像的方法。
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);
}
}
}
}
下面是我的实现方式
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;
}
我也想问一下我的精灵数组的使用方法。我想通过更改当前精灵图像来更改动作事件绘制的图像,从而在计时器中使用
tmr = new Timer(20, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
for(Rock r:rocks){
r.move();
r.changeSprite();
}
repaint();
}
});
用方法
public void changeSprite(){
if(ds==12)
ds=0;
ds++;
currentSprite = sprite[ds];
}
每个岩石对象都有一个精灵数组,其中包含从加载的精灵图像接收到的缓冲图像和绘制的当前图像。计时器将更改当前图像并将其重新绘制在对象上,以便绘制整个精灵,但它似乎不起作用。所以是我的 loadingSpriteImage 有问题还是我的绘制方式导致了问题?
好的,所以我们需要知道很多事情。
- 有多少张图片构成精灵sheet,它们是如何布局的(rows/cols),如果图片数量不均匀(
count != rows * cols
)甚至可能每个精灵的大小
- 我们在给定周期中走了多远(比方说一秒钟)
所以根据你之前问题的图片...
我们知道有 5 列,4 行但只有 19 张图像。现在您可以花很多时间,为每个可能的精灵编写大量代码 sheet,或者您可以尝试共生其中的一些问题...
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);
}
}
所以,这是非常基本的,它只是一个图像列表。特殊部分是 getSprite
方法,它在当前动画循环中取得进展,并根据您可用的图像数量 returns 图像。这基本上从精灵中解耦了时间的概念,并允许您在外部定义“循环”的含义。
现在,因为构建 SpriteSheet
的实际过程涉及很多可能的变量,构建器是个好主意...
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);
}
}
因此,再次基于您的精灵 sheet,这意味着我可以使用类似...
的方式构建 SpriteSheet
spriteSheet = new SpriteSheetBuilder().
withSheet(sheet).
withColumns(5).
withRows(4).
withSpriteCount(19).
build();
但它让我有能力构建任意数量的 SpriteSheets
,所有这些都可能由不同的矩阵组成
但是现在我们有了 SpriteSheet
,我们需要一些方法来为它们设置动画,但我们真正需要的是一些方法来计算给定周期的进展(假设一秒是一个周期) ,我们可以使用一个简单的“引擎”,比如...
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);
}
}
}
}
现在,这基本上只是 Swing Timer
的包装器 class,但它所做的是为我们计算循环进程。它还具有很好的 ActionListener
支持,因此我们可以在报价发生时得到通知
好的,但是这一切是如何协同工作的?
基本上,给定一个或多个 SpriteSheet
和一个 SpriteEngine
,我们可以绘制 sheet 做类似...
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();
}
}
现在,好吧,这很基础,但它提供了一个想法。
对于您要移动(或旋转)的实体,我会创建另一个 class,其中包含该信息和 spriteSheet
。这可能包含“绘制”方法,或者您可以使用对象的属性然后绘制各个帧...
类似...
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);
}
}
可以使用类似...
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();
}
请注意,我只创建了精灵 sheet 一次。这可能需要您向 AstroidEntity
提供随机“偏移量”,这将改变它 returns 给定进度值的帧...
一个简单的方法可能是添加...
public SpriteSheet offsetBy(int amount) {
List<BufferedImage> images = new ArrayList<>(sprites);
Collections.rotate(images, amount);
return new SpriteSheet(images);
}
到 SpriteSheet
,然后在 AstroidEntity
中,您可以使用类似...
的方式创建偏移量 SpriteSheet
public AstroidEntity(SpriteSheet spriteSheet) {
this.spriteSheet = spriteSheet.offsetBy((int) (Math.random() * spriteSheet.count()));
绘画可能会使用类似...
@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();
}
基本上,这里的关键因素是尝试将您的代码与“时间”概念分离。
例如,我将引擎使用的每秒帧数更改为 60,但没有看到精灵动画有任何变化,因为它正在处理一个时间周期为 1 秒的概念。但是,这将允许您相对简单地更改对象移动的速度。
您还可以将引擎设置为具有“周期长度”的概念,并将其设置为半秒或 5 秒,这将影响动画速度 - 但您的其余代码将保持不变!
我想问问为什么在将任何精灵图像加载到对象中时出错
这是我获取图像的方法。
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);
}
}
}
}
下面是我的实现方式
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;
}
我也想问一下我的精灵数组的使用方法。我想通过更改当前精灵图像来更改动作事件绘制的图像,从而在计时器中使用
tmr = new Timer(20, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
for(Rock r:rocks){
r.move();
r.changeSprite();
}
repaint();
}
});
用方法
public void changeSprite(){
if(ds==12)
ds=0;
ds++;
currentSprite = sprite[ds];
}
每个岩石对象都有一个精灵数组,其中包含从加载的精灵图像接收到的缓冲图像和绘制的当前图像。计时器将更改当前图像并将其重新绘制在对象上,以便绘制整个精灵,但它似乎不起作用。所以是我的 loadingSpriteImage 有问题还是我的绘制方式导致了问题?
好的,所以我们需要知道很多事情。
- 有多少张图片构成精灵sheet,它们是如何布局的(rows/cols),如果图片数量不均匀(
count != rows * cols
)甚至可能每个精灵的大小 - 我们在给定周期中走了多远(比方说一秒钟)
所以根据你之前问题的图片...
我们知道有 5 列,4 行但只有 19 张图像。现在您可以花很多时间,为每个可能的精灵编写大量代码 sheet,或者您可以尝试共生其中的一些问题...
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);
}
}
所以,这是非常基本的,它只是一个图像列表。特殊部分是 getSprite
方法,它在当前动画循环中取得进展,并根据您可用的图像数量 returns 图像。这基本上从精灵中解耦了时间的概念,并允许您在外部定义“循环”的含义。
现在,因为构建 SpriteSheet
的实际过程涉及很多可能的变量,构建器是个好主意...
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);
}
}
因此,再次基于您的精灵 sheet,这意味着我可以使用类似...
的方式构建SpriteSheet
spriteSheet = new SpriteSheetBuilder().
withSheet(sheet).
withColumns(5).
withRows(4).
withSpriteCount(19).
build();
但它让我有能力构建任意数量的 SpriteSheets
,所有这些都可能由不同的矩阵组成
但是现在我们有了 SpriteSheet
,我们需要一些方法来为它们设置动画,但我们真正需要的是一些方法来计算给定周期的进展(假设一秒是一个周期) ,我们可以使用一个简单的“引擎”,比如...
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);
}
}
}
}
现在,这基本上只是 Swing Timer
的包装器 class,但它所做的是为我们计算循环进程。它还具有很好的 ActionListener
支持,因此我们可以在报价发生时得到通知
好的,但是这一切是如何协同工作的?
基本上,给定一个或多个 SpriteSheet
和一个 SpriteEngine
,我们可以绘制 sheet 做类似...
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();
}
}
现在,好吧,这很基础,但它提供了一个想法。
对于您要移动(或旋转)的实体,我会创建另一个 class,其中包含该信息和 spriteSheet
。这可能包含“绘制”方法,或者您可以使用对象的属性然后绘制各个帧...
类似...
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);
}
}
可以使用类似...
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();
}
请注意,我只创建了精灵 sheet 一次。这可能需要您向 AstroidEntity
提供随机“偏移量”,这将改变它 returns 给定进度值的帧...
一个简单的方法可能是添加...
public SpriteSheet offsetBy(int amount) {
List<BufferedImage> images = new ArrayList<>(sprites);
Collections.rotate(images, amount);
return new SpriteSheet(images);
}
到 SpriteSheet
,然后在 AstroidEntity
中,您可以使用类似...
SpriteSheet
public AstroidEntity(SpriteSheet spriteSheet) {
this.spriteSheet = spriteSheet.offsetBy((int) (Math.random() * spriteSheet.count()));
绘画可能会使用类似...
@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();
}
基本上,这里的关键因素是尝试将您的代码与“时间”概念分离。
例如,我将引擎使用的每秒帧数更改为 60,但没有看到精灵动画有任何变化,因为它正在处理一个时间周期为 1 秒的概念。但是,这将允许您相对简单地更改对象移动的速度。
您还可以将引擎设置为具有“周期长度”的概念,并将其设置为半秒或 5 秒,这将影响动画速度 - 但您的其余代码将保持不变!