Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
205 views
in Technique[技术] by (71.8m points)

Load a sprites image in java

I want to ask if why am getting error loading any sprite images into the object

here is how I get the image in.

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);
            }
        }
    }

}

here is how I'm implementing it

    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;
    }

I also want to ask about my way of using the sprites array. I want to use in the timer by changing the image drawn by action event by changing the current sprite image

        tmr = new Timer(20, new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                for(Rock r:rocks){
                    r.move();
                    r.changeSprite();
                }
                repaint();
            }
        });

with the method

    public void changeSprite(){
        if(ds==12)
            ds=0;
        ds++;
        currentSprite = sprite[ds];
    }

Each rock object has a sprite array full of Buffered image received from the sprite image loaded in and a current image which get drawn. the timer will change the current image and redrawn it on the object so that the whole sprite get drawn but it doesn't seem to work. So it is my loadingSpriteImage that have the problem or my way of drawing it causing the problem?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Okay, so there a lots of things we need to know.

  • How many images make up the sprite sheet, how they are laid out (rows/cols), if there is an uneven number of images (count != rows * cols) and possibly even the size of each sprite
  • How far we are through a given cycle (let's say a second)

So based on your image from a previous question...

Sprite Sheet

we know there are 5 columns, 4 rows but only 19 images. Now you could spend a lot of time, write lots of code for each possible sprite sheet, or you could try and commensalism some of those problems...

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);
    }

}

So, this is pretty basic, it's simply a list of images. The special part is the getSprite method, which takes a progression through the current animation cycle and returns an image based on the number of images you have available. This basically decouples the concept of time from the sprite and allows you to define the meaning of a "cycle" externally.

Now, because the actual process of building a SpriteSheet involves a lot of possible variables, a builder would be a good 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);
    }
}

So, again, based on your sprite sheet, this means I could build a SpriteSheet using something like...

spriteSheet = new SpriteSheetBuilder().
        withSheet(sheet).
        withColumns(5).
        withRows(4).
        withSpriteCount(19).
        build();

but it gives me the power to build any number of SpriteSheets, all of which might be made up of different matrices

But now that we have a SpriteSheet, we need some way to animate them, but what we really need is some way to calculate the progression through a given cycle (let's say a second is a cycle), we could use a simple "engine", something like...

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);
            }
        }

    }

}

Now, this is basically just a wrapper class for a Swing Timer, but what it does is it calculates the cycle progression for us. It also has nice ActionListener support, so we can be notified when a tick occurs

Okay, but how does that all work together?

Basically, given one or more SpriteSheets and a SpriteEngine, we can paint the sheets doing something like...

Spinny

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();
    }

}

Now, okay, that's pretty basic, but it gives an idea.

For entities you want to move (or rotate), I would create another class which contained that information AND the spriteSheet. This might contain a "paint" method or you could use the properties of the object to then paint the individual frames...

Something like...

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);
    }

}

Which could be created using something like...

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();
}

Note, I only created the sprite sheet once. This might require you to supply a random "offset" to the AstroidEntity which will change which frame it returns for a given progress value...

A simple way might be to add...

public SpriteSheet offsetBy(int amount) {
    List<BufferedImage> images = new ArrayList<>(sprites);
    Collections.rotate(images, amount);

    return new SpriteSheet(images);
}

to SpriteSheet, then in AstroidEntity you could create an offset SpriteSheet using something like...

public AstroidEntity(SpriteSheet spriteSheet) {
    this.spriteSheet = spriteSheet.offsetBy((int) (Math.random() * spriteSheet.count()));

Painting might be done using something like...

@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();
}

Basically, the key factor here is, try and decouple your code from a concept of "time".

For example, I changed the frames-per-second the engine was using to 60 and saw no change in the animation of the sprites, because it was working on the concept of a single cycle of time been 1 second. This would, however allow you to change the speed at which the objects moved relatively simply.

You could also set the engine up to have a concept of "cycle-length" and m


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...