渲染多个(简单)动画崩溃:ConcurrentModificationException

时间:2014-11-18 18:05:43

标签: java event-handling hashmap awt concurrentmodification

我尝试使用java.awt渲染简单的动画(在光标位置单击鼠标就会发生简单爆炸)。

首先是我的动画类:

package animation;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

public class Animation {

    private BufferedImage spriteSheet;
    private int currentStateNum;
    private int spriteSize;
    private int spritsInWidth; // number of sprites in one row of the spritesheet
    private long startTime;
    private long delay;        // delay between each sprite
    private long sicleTime;
    private long lifeTime;
    private boolean alive;

    public Animation(String spriteSheetPath, int spriteSize, 
                     int spritsCount, int spritsInWidth, long delay,
                     long lifeTime) {
        try {
            this.spriteSheet = ImageIO.read(new File(spriteSheetPath));
            this.currentStateNum = 0;
            this.spriteSize = spriteSize;
            this.delay = delay;
            this.spritsInWidth = spritsInWidth;
            this.sicleTime = delay * spritsCount;
            this.lifeTime = lifeTime;
            this.alive = true;
            this.startTime = System.currentTimeMillis();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public BufferedImage getCurrentState() {
        int x = (currentStateNum % spritsInWidth) * spriteSize;
        int y = (currentStateNum / spritsInWidth) * spriteSize;
        return spriteSheet.getSubimage(x, y, spriteSize, spriteSize);
    }

    public void update() {
        long currTime = System.currentTimeMillis();
        long passedTime = currTime - startTime;
        alive = passedTime < lifeTime;
        long currSicleTime = passedTime % sicleTime;
        currentStateNum = (int) (currSicleTime / delay);
    }

    public boolean isAlive() {
        return alive;
    }
}

现在,我实际渲染动画的第一次尝试类似于:

import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferStrategy;

import javax.swing.JFrame;

import animation.Animation;

public class Main {

    public static void main(String[] args) {

        JFrame frame = new JFrame("Animation test");
        Canvas canvas = new Canvas();

        canvas.setPreferredSize(new Dimension(600, 400));
        canvas.setBackground(Color.white);
        canvas.addMouseListener(new MouseAdapter() {

            @Override
            public void mouseReleased(MouseEvent e) {

                Animation a = new Animation("res/explosion.png", 64, 16, 
                                            4, 40, 40 * 16);

                BufferStrategy bs;
                Graphics g;
                while (a.isAlive()) {
                    bs = canvas.getBufferStrategy();
                    g = bs.getDrawGraphics();
                    g.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
                    g.drawImage(a.getCurrentState(), e.getX(), e.getY(), null);
                    g.dispose();
                    bs.show();
                    a.update();
                }

                bs = canvas.getBufferStrategy();
                g = bs.getDrawGraphics();
                g.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
                g.dispose();
                bs.show();
                super.mouseReleased(e);
            }

        });

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(canvas);
        frame.pack();
        frame.setVisible(true);

        canvas.createBufferStrategy(3);
    }
}

虽然这种方法适用于屏幕上的单个动画,但不用说这个 多次点击失败了。 我很快意识到MouseListener将事件放入队列中导致动画一个接一个地延迟渲染而不是一起渲染,几分钟后我意识到它实际上很好,因为它可能是一个坏主意同时编辑同一个栅格形成多个线程。

所以我的下一个方法是在mouseReleased(MouseEvent e)内渲染动画而不是 方法是创建一个动画地图,然后将它们添加到地图中,并使用其他地图进行渲染。

代码:

import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferStrategy;
import java.util.HashMap;
import java.util.Map;

import javax.swing.JFrame;

import animation.Animation;

public class Main {

    static boolean running = true;

    public static void main(String[] args) {

        JFrame frame = new JFrame("Animation test");
        Canvas canvas = new Canvas();
        Map<Animation, Point> explosions = new HashMap<Animation, Point>();

        canvas.setPreferredSize(new Dimension(600, 400));
        canvas.setBackground(Color.white);
        canvas.addMouseListener(new MouseAdapter() {

            @Override
            public void mouseReleased(MouseEvent e) {
                explosions.put(new Animation("res/explosion.png", 64, 16, 
                               4, 25, 25 * 16),
                               new Point(e.getX() - 32, e.getY() - 32));

                super.mouseReleased(e);
            }

        });

        frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        frame.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent we) {
                running = false;
                explosions.clear();
                System.exit(0);
            }
        });
        frame.getContentPane().add(canvas);
        frame.pack();
        frame.setVisible(true);

        while (running) {

            BufferStrategy bs = canvas.getBufferStrategy();
            if (bs == null) {
                canvas.createBufferStrategy(3);
                continue;
            }
            Graphics g = bs.getDrawGraphics();

            g.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());

            for (Animation a : explosions.keySet()) {
                Point p = explosions.get(a);
                if (a.isAlive()) {
                    g.drawImage(a.getCurrentState(), p.x, p.y, null);
                    a.update();
                } else {
                    explosions.remove(a);
                }
            }

            g.dispose();
            bs.show();

        }

    }
}

这种(有点)有效,当它正确地渲染所有动画并且没有延迟时,它经常与java.util.ConcurrentModificationException

崩溃

堆栈跟踪是:

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.HashMap$HashIterator.nextNode(Unknown Source)
    at java.util.HashMap$KeyIterator.next(Unknown Source)
    at Main.main(Main.java:65)

我的第一个假设是问题是我在迭代它时从地图中删除了键,所以我删除了explosions.remove(a)但仍然遇到了同样的问题,实际上我仍然认为问题正在改变迭代时映射,因为event可以触发,而我将从地图渲染,这将为其添加新密钥。

我需要的是洞察我的假设是否正确,如果不是,那么问题是什么? 虽然(如果我的假设是正确的)我可以用一些讨厌的方式来解决这个问题,但我更倾向于为这个问题提供一个很好的优雅解决方案。

2 个答案:

答案 0 :(得分:0)

好的,似乎我的假设确实是正确的,问题只是简单地解决了:

synchronized (explosions) {
    Iterator<Map.Entry<Animation, Point>> it = explosions.entrySet().iterator();
    for (; it.hasNext();) {
        Map.Entry<Animation, Point> entry = it.next();
        if (entry.getKey().isAlive()) {
            g.drawImage(entry.getKey().getCurrentState(), entry.getValue().x, entry.getValue().y, null);
            entry.getKey().update();
        } else {
            it.remove();
        }
    }
}

答案 1 :(得分:0)

并发哈希映射应该提供更好的性能,特别是因为只有删除或插入操作才会实际阻止映射