In my program I try to paint on a JPanel
when the mouse is pressed. The mousePressed method is just to test the painting from another class. Later on the spawn meth
Basically, you want to decouple your code and centralise the responsible to the classes. So the "data" should be maintained by a model of some kind, the rendering should be handle by some kind of view and the updates to the model and view should be handled by some kind of controller.
This makes it easier to swap out any one part with out requiring a whole bunch of new code or other changes. It also means that each class has a defined domain of responsibility and discourages you from trying to, for example, make changes to state from within the view which should be handled by the model (which could put the state into disarray)
Let's start with something that what's to be painted
public interface Sprite {
public void paint(Graphics2D g2d);
}
public interface MoveableSprite extends Sprite {
public void update(Container container);
}
These represent either a static sprite (like a tree for example) or a sprite which is moving (and wants to be updated on a regular bases)
These are contained within a model
public interface GameModel {
public List<Sprite> getSprites();
public void setObserver(Observer<MoveableSprite> observer);
public Observer<MoveableSprite> getObserver();
public void spawnSprite();
}
Which provides some means by which it can notify (in this case, a single) interested party about some kind of state change. For this example, that means a new MoveableSprite
has become available
The Observer
is pretty basic and just has a single call back...
public interface Observer<T> {
public void stateChanged(T parent);
}
And an "engine" to help drive it...
public class GameEngine {
private GameModel model;
private SurfacePane surface;
private Timer timer;
public GameEngine(GameModel model, SurfacePane surface) {
this.model = model;
this.surface = surface;
model.setObserver(new Observer<MoveableSprite>() {
@Override
public void stateChanged(MoveableSprite sprite) {
sprite.update(getSurface());
}
});
timer = new Timer(40, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
for (Sprite sprite : getModel().getSprites()) {
if (sprite instanceof MoveableSprite) {
((MoveableSprite) sprite).update(getSurface());
}
}
getSurface().repaint();
}
});
}
public GameModel getModel() {
return model;
}
public SurfacePane getSurface() {
return surface;
}
public void start() {
timer.start();
}
public void stop() {
timer.stop();
}
}
This is a pretty basic example, but basically, it updates the position of MoveableSprite
and asks the surface to repaint itself. It's also observing the GameModel
for any new sprites and it will update their position immediately, so they don't appear in some "weird" place
Okay, now we actually need to implement some of this to make it work
public class DefaultGameModel implements GameModel {
private Observer<MoveableSprite> observer;
private List<Sprite> sprites;
public DefaultGameModel() {
sprites = new ArrayList<>(25);
for (int index = 0; index < 10; index++) {
spawnSprite();
}
}
@Override
public List<Sprite> getSprites() {
return Collections.unmodifiableList(sprites);
}
public void spawnSprite() {
try {
ZombieSprite sprite = new ZombieSprite();
sprites.add(sprite);
Observer<MoveableSprite> observer = getObserver();
if (observer != null) {
observer.stateChanged(sprite);
}
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void setObserver(Observer<MoveableSprite> observer) {
this.observer = observer;
}
@Override
public Observer<MoveableSprite> getObserver() {
return observer;
}
}
public class ZombieSprite implements MoveableSprite {
private int x;
private int y;
private int xDelta;
private int yDelta;
private BufferedImage img;
private Observer<Sprite> observer;
private boolean initialised = false;
public ZombieSprite() throws IOException {
img = ImageIO.read(getClass().getResource("/LogoZombi.png"));
}
@Override
public void update(Container container) {
if (!initialised) {
x = (int) (Math.random() * container.getWidth());
y = (int) (Math.random() * container.getHeight());
Random rnd = new Random();
xDelta = rnd.nextBoolean() ? 1 : -1;
yDelta = rnd.nextBoolean() ? 1 : -1;
initialised = true;
}
x += xDelta;
y += yDelta;
if (x < 0) {
x = 0;
xDelta *= -1;
} else if (x + img.getWidth() > container.getWidth()) {
x = container.getWidth() - img.getWidth();
xDelta *= -1;
}
if (y < 0) {
y = 0;
yDelta *= -1;
} else if (y + img.getHeight() > container.getHeight()) {
y = container.getHeight() - img.getHeight();
yDelta *= -1;
}
}
@Override
public void paint(Graphics2D g2d) {
g2d.drawImage(img, x, y, null);
}
}
These two classes implement the GameModel
and MoveableSprite
interfaces. We use interfaces to decouple the code, which makes it easier to change the way in which things work and provides a jumping off point for agreed to contracts and exceptions of the implemenations
And finally, something that actually paints the current state...
public class SurfacePane extends JPanel {
private GameModel model;
public SurfacePane(GameModel model) {
this.model = model;
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
getModel().spawnSprite();
}
});
}
public GameModel getModel() {
return model;
}
@Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
GameModel model = getModel();
for (Sprite sprite : model.getSprites()) {
sprite.paint(g2d);
}
g2d.dispose();
}
}
You'll not that this class has the MouseListener
, this is kind of deliberate, as other components which might be added to this container could prevent the MouseListener
from been notified, so don't do that. But the MouseListener
just calls the model to spawn another zombie...
And finally, we need to plumb it altogether...
GameModel model = new DefaultGameModel();
SurfacePane surfacePane = new SurfacePane(model);
GameEngine engine = new GameEngine(model, surfacePane);
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(surfacePane);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
engine.start();
And just because I know that's a lot of disjointed concepts to put together, a complete example...
import java.awt.Container;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.ChangeListener;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
GameModel model = new DefaultGameModel();
SurfacePane surfacePane = new SurfacePane(model);
GameEngine engine = new GameEngine(model, surfacePane);
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(surfacePane);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
engine.start();
}
});
}
public class SurfacePane extends JPanel {
private GameModel model;
public SurfacePane(GameModel model) {
this.model = model;
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
getModel().spawnSprite();
}
});
}
public GameModel getModel() {
return model;
}
@Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
GameModel model = getModel();
for (Sprite sprite : model.getSprites()) {
sprite.paint(g2d);
}
g2d.dispose();
}
}
public class GameEngine {
private GameModel model;
private SurfacePane surface;
private Timer timer;
public GameEngine(GameModel model, SurfacePane surface) {
this.model = model;
this.surface = surface;
model.setObserver(new Observer<MoveableSprite>() {
@Override
public void stateChanged(MoveableSprite sprite) {
sprite.update(getSurface());
}
});
timer = new Timer(40, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
for (Sprite sprite : getModel().getSprites()) {
if (sprite instanceof MoveableSprite) {
((MoveableSprite) sprite).update(getSurface());
}
}
getSurface().repaint();
}
});
}
public GameModel getModel() {
return model;
}
public SurfacePane getSurface() {
return surface;
}
public void start() {
timer.start();
}
public void stop() {
timer.stop();
}
}
public interface Observer<T> {
public void stateChanged(T parent);
}
public interface Sprite {
public void paint(Graphics2D g2d);
}
public interface MoveableSprite extends Sprite {
public void update(Container container);
}
public interface GameModel {
public List<Sprite> getSprites();
public void setObserver(Observer<MoveableSprite> observer);
public Observer<MoveableSprite> getObserver();
public void spawnSprite();
}
public class DefaultGameModel implements GameModel {
private Observer<MoveableSprite> observer;
private List<Sprite> sprites;
public DefaultGameModel() {
sprites = new ArrayList<>(25);
for (int index = 0; index < 10; index++) {
spawnSprite();
}
}
@Override
public List<Sprite> getSprites() {
return Collections.unmodifiableList(sprites);
}
public void spawnSprite() {
try {
ZombieSprite sprite = new ZombieSprite();
sprites.add(sprite);
Observer<MoveableSprite> observer = getObserver();
if (observer != null) {
observer.stateChanged(sprite);
}
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void setObserver(Observer<MoveableSprite> observer) {
this.observer = observer;
}
@Override
public Observer<MoveableSprite> getObserver() {
return observer;
}
}
public class ZombieSprite implements MoveableSprite {
private int x;
private int y;
private int xDelta;
private int yDelta;
private BufferedImage img;
private Observer<Sprite> observer;
private boolean initialised = false;
public ZombieSprite() throws IOException {
img = ImageIO.read(getClass().getResource("/LogoZombi.png"));
}
@Override
public void update(Container container) {
if (!initialised) {
x = (int) (Math.random() * container.getWidth());
y = (int) (Math.random() * container.getHeight());
Random rnd = new Random();
xDelta = rnd.nextBoolean() ? 1 : -1;
yDelta = rnd.nextBoolean() ? 1 : -1;
initialised = true;
}
x += xDelta;
y += yDelta;
if (x < 0) {
x = 0;
xDelta *= -1;
} else if (x + img.getWidth() > container.getWidth()) {
x = container.getWidth() - img.getWidth();
xDelta *= -1;
}
if (y < 0) {
y = 0;
yDelta *= -1;
} else if (y + img.getHeight() > container.getHeight()) {
y = container.getHeight() - img.getHeight();
yDelta *= -1;
}
}
@Override
public void paint(Graphics2D g2d) {
g2d.drawImage(img, x, y, null);
}
}
}