【问题标题】:Load a sprites image in java在java中加载精灵图像
【发布时间】:2016-02-18 03:25:22
【问题描述】:

我想问一下为什么将任何精灵图像加载到对象中时会出错

这是我获取图像的方式。

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

我还想问一下我使用 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 有问题还是我的绘制方式导致了问题?

【问题讨论】:

    标签: java sprite bufferedimage


    【解决方案1】:

    好的,所以我们需要知道很多事情。

    • 由多少张图片组成精灵表,它们的布局方式(行/列),如果图片数量不均匀 (count != rows * cols),甚至可能是每个精灵的大小
    • 我们在一个给定的周期中走了多远(比如说一秒钟)

    所以根据你上一个问题的图片...

    我们知道有 5 列 4 行但只有 19 张图片。现在您可以花费大量时间,为每个可能的 sprite 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 方法,它在当前动画循环中取得进展,并根据您可用的图像数量返回一个图像。这基本上将时间概念与精灵分离,并允许您在外部定义“循环”的含义。

    现在,因为构建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);
        }
    }
    

    所以,再一次,根据您的精灵表,这意味着我可以使用类似...的东西构建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 的包装类,但它的作用是为我们计算循环进程。它也有很好的ActionListener 支持,所以我们可以在发生滴答声时得到通知

    好的,但是这一切如何协同工作?

    基本上,给定一个或多个SpriteSheets 和一个SpriteEngine,我们可以像...

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

    现在,好吧,这很基本,但它提供了一个想法。

    对于您想要移动(或旋转)的实体,我将创建另一个包含该信息和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();
    }
    

    注意,我只创建了一次精灵表。这可能需要您向AstroidEntity 提供一个随机“偏移量”,这将改变它为给定进度值返回的帧...

    一个简单的方法可能是添加...

    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 秒,这会影响动画速度 - 但其余代码将保持不变!

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-06-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-08-10
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多