Search code examples
javaswingtimerjpanelpaintcomponent

Java Swing - Call to paintComponent doesn't clear JPanel?


I'm working on a little 2D game engine just to experiment with some design ideas. I like the ideas of entities being given the burden of rendering themselves and registering their own listeners - an entity or "actor" should define all of it's own behaviour.

Problem is, my JPanel I'm painting to is "smearing". Easiest way to describe it is to give you guys a SSCCE is you'd be so inclined.

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

public class SSCCE_BufferImageRender {

    public static void main(String[] args) {
        SSCCE_BufferImageRender sbim = new SSCCE_BufferImageRender();
        Environment e = sbim.new Environment();
        Stage s = sbim.new Stage(e);
        e.addStage(s);
        e.setVisible(true);
        s.addActor(sbim.new Actor(s));
    }

    class Environment extends JFrame implements Runnable{

    private static final long serialVersionUID = 1L;
    private ScheduledExecutorService mainService = Executors.newSingleThreadScheduledExecutor();
    private List<Stage> environmentStages;
    private Stage activeStage;

        public Environment() {
            super();
            setSize(new Dimension(400,400));
            setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            Timer timer = new Timer(17, new ActionListener() {
                public void actionPerformed(ActionEvent arg0) {
                    run();
                }
            });
            environmentStages = new ArrayList<Stage>();
            timer.start();
        }

        @Override
        public void run() {
            if(activeStage != null)
                activeStage.act();
        }

        public void addStage(Stage stage) {
            if(environmentStages.size() == 0) {
                activeStage = stage;
                setContentPane(stage);
            }

            environmentStages.add(stage);
        }

        public KeyListener[] getKeyListeners() {
            return getListeners(KeyListener.class);
        } 
    }

    /**
     * Stage - Basically where everything will be painted
     * */
    class Stage extends JPanel {
        private Environment stageEnvironment;   
        private List<Actor> stageActors;
        private int stageHeight;
        private int stageWidth;

            public Stage(Environment environment) {
                super();
                stageEnvironment = environment;
                stageHeight= stageEnvironment.getHeight();
                stageWidth = stageEnvironment.getWidth();
                this.setSize(stageWidth, stageHeight);
                stageActors = new ArrayList<Actor>();
            }

            @Override
            public void paintComponent(Graphics g) {
                super.paintComponent(g);
                Graphics2D g2d = (Graphics2D) g.create();
                for(Actor actor : stageActors) {
                    BufferedImage bi = actor.render();
                    g2d.drawImage(bi, 0, 0, null);
                }
                g2d.drawString("Hello World!", 50, 50);
                g2d.dispose();
            }

            public void act() {
                for(Actor actor : stageActors) {
                    actor.act();
                }
                repaint();
            }

            public void addActor(Actor actor){
                stageActors.add(actor);
            }

            public Environment getStageEnvironment() {
                return stageEnvironment;
            }

            public int getWidth() {
                return stageWidth;
            }

            public int getHeight() {
                return stageHeight;
            }

            public void setStageEnvironment(Environment stageEnvironment) {
                this.stageEnvironment = stageEnvironment;
            }
    }

    class Actor {
        private String actorID;
        private Stage actorStage;
        private BufferedImage actorImage;
        private int x,y,radius;
            public Actor(Stage stage) {
                actorStage = stage;
                x = y = radius = 20;
                setActorImage(new BufferedImage(actorStage.getWidth(), actorStage.getHeight(), BufferedImage.TYPE_INT_ARGB));
                if(getKeyListener() != null) {
                    System.out.println("Adding new listener from actor");
                    stage.getStageEnvironment().addKeyListener(getKeyListener());
                }
            }

            public Stage getActorStage() {
                return actorStage;
            }

            public void setActorStage(Stage actorStage) {
                this.actorStage = actorStage;
            }

            public BufferedImage getActorImage() {
                return actorImage;
            }

            public void setActorImage(BufferedImage actorImage) {
                this.actorImage = actorImage;
            }

            public void act(){
                // Do nothing
            }

            public BufferedImage render() {
                Graphics2D g2d = getActorImage().createGraphics();
                g2d.setColor(Color.red);
                g2d.fillOval(x, y, radius, radius);
                g2d.dispose();
                return getActorImage();
            }

            private KeyListener getKeyListener(){
                return new KeyListener() {

                    @Override
                    public void keyPressed(KeyEvent arg0) {}

                    @Override
                    public void keyReleased(KeyEvent arg0) {
                        switch(arg0.getKeyChar()) {
                        case '4': x-=5;break;
                        case '6': x+=5;break;
                        case '8': y-=5;break;
                        case '2': y+=5;break;
                        default:break;
                        }
                    }

                    @Override
                    public void keyTyped(KeyEvent arg0) {}
                };
            }
    }
}

You'll notice I'm using a Timer to simulate a thread... I also have some code where I was using a ScheduledExecutor but I've seen other places that messing with how the EDT handles Swing components can cause problems.

So, any idea why my JPanel isn't painting correctly?


Solution

  • The problem is in you Actor...

    public BufferedImage render() {
        Graphics2D g2d = getActorImage().createGraphics();
        g2d.setColor(Color.red);
        g2d.fillOval(x, y, radius, radius);
        g2d.dispose();
        return getActorImage();
    }
    

    Here, you are getting the Graphics context of the BufferedImage and painting to it...but you've not removed what ever was previously painted to the BufferedImage, so it is simply "adding" to it.

    While there is away to clear a BufferedImage, a better solution would be to change the way that the rendering is done.

    Instead of the Actor rendering to it's own BufferedImage, which as you scale this up, is going to put a load on your memory, you should pass a reference of the Graphics context to be painted, for example...

    public void render(Graphics2D g2d) {
        g2d.setColor(Color.red);
        g2d.fillOval(x, y, radius, radius);
    }
    

    The in your paintComponent method, simply pass the components Graphics to it...

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g.create();
        for (Actor actor : stageActors) {
            actor.render(g2d);
        }
        g2d.drawString("Hello World!", 50, 50);
        g2d.dispose();
    }
    

    If you're worried about the Actor doing something strange to the Graphics context, you could create a separate copy for each....

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        for (Actor actor : stageActors) {
            Graphics2D g2d = (Graphics2D) g.create();
            actor.render(g2d);
            g2d.dispose();
        }
        g.drawString("Hello World!", 50, 50);
    }
    

    Also, there's no reason for your paintComponent method to be public as you never want anyone to call it directly.