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?
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.