In this version, we have the main thread submit an Event to swing EventQueue, which is handled by the swing event dispatch thread. We also created a swing timer to trigger animation update, Again the trigger created an Event in swing EventQueue, and the actionPerformed code is executed by swing event dispatch thread.
In MonitorVehicleTrackerDemo.java all swing component methods are invoked from the event dispatch thread. All the state variables are correctly published.
Remember, Mutable objects must be safely published (initialization safety), and must be either thread-safe or guarded by a lock (visibility and atomicity).
Notice, when you run the program in eclipse LUNA with java SE, the first shot sometimes does not display correctly. (to reproduce on my machine, close and reopen eclipse, while eclipse are still initializing, press the space key, the first shot of the first play will have this problem) This problem still exists even we remove the ActionListener and Timer code. Running from command line or other versions of eclipse is fine.
/*press arrow key to move, press space key to shoot*/
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.geom.Rectangle2D;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
/******* game graphics ********/
public class MonitorVehicleTrackerDemo extends JFrame{
public MonitorVehicleTrackerDemo() {
initGUI();
}
private void initGUI() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBounds(10, 10, 400, 400);
setLayout(new BorderLayout());
Map<String, MutablePoint> locations = new HashMap<>();
for(int i=0; i<5; i++) {
MutablePoint p = new MutablePoint();
p.x = i* GameBoard.TANKWIDTH * 2;
p.y = i* GameBoard.TANKHEIGHT * 2;
locations.put("tank"+i, p);
}
MonitorVehicleTracker mt = new MonitorVehicleTracker(locations);
final GameBoard paintPanel = new GameBoard(mt);
add(paintPanel, BorderLayout.CENTER);
paintPanel.startTimer();
setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new MonitorVehicleTrackerDemo();
}});
}
}
class GameBoard extends JPanel implements KeyListener, ActionListener {
public static final int HEIGHT = 300;
public static final int WIDTH = 400;
public static final int TANKWIDTH = 30;
public static final int TANKHEIGHT = 10;
public static final int MOVESPEED = 2;
public static final int TARGETX = 100;
public static final int TARGETY = 200;
public static final int DELAY = 70;
private static int dx = WIDTH / 2;
private static int dy = HEIGHT;
private static final Rectangle2D yourtank = new Rectangle2D.Double(dx, dy, TANKWIDTH,
TANKHEIGHT); //reference to Rectangle2D is final, but the object's states can change
//{System.out.println(javax.swing.SwingUtilities.isEventDispatchThread());}
private static final Rectangle2D laserBeam = new Rectangle2D.Double(-10, -10, 0,
0);
private volatile MonitorVehicleTracker mt;
public GameBoard(MonitorVehicleTracker mt) {
this.addKeyListener(this);
this.setBackground(Color.white);
this.setFocusable(true);
this.mt = mt;
setDoubleBuffered(true);
}
//avoid starting timer in constructor (partially constructed this object).
public void startTimer() {
new Timer(DELAY, this).start();
}
@Override
public void actionPerformed(ActionEvent e) {
for(String id: mt.getLocations().keySet()) {
MutablePoint p = mt.getLocation(id);
if(p.alive)
mt.setLocation(id, p.x + GameBoard.MOVESPEED, p.y + GameBoard.MOVESPEED * 2, true);
}
//System.out.println(javax.swing.SwingUtilities.isEventDispatchThread());
repaint();
}
@Override
protected void paintComponent(Graphics grphcs) {
super.paintComponent(grphcs);
cleanDead();
Graphics2D gr = (Graphics2D) grphcs;
gr.draw(yourtank);
gr.draw(laserBeam);
for(MutablePoint p: mt.getLocations().values()) {
if (p.alive)
drawTank(grphcs, p.x, p.y);
}
repaint();
Toolkit.getDefaultToolkit().sync(); //smooth on Linux systems that buffer graphics events.
}
private void cleanDead() {
for(String id: mt.getLocations().keySet()) {
MutablePoint p = mt.getLocation(id);
if (overlaps(p.x, p.y, TANKWIDTH, TANKHEIGHT, laserBeam)) {
mt.setLocation(id, -10, -10, false);
}
if (overlaps(p.x, p.y, TANKWIDTH, TANKHEIGHT, yourtank)) {
this.removeKeyListener(this);
}
}
}
private boolean overlaps(int x, int y, int width, int height, Rectangle2D r) {
return x < r.getX() + r.getWidth() && x + width > r.getX()
&& y < r.getY() + r.getHeight() && y + height > r.getY();
}
private void drawTank(Graphics g, int x, int y) {
g.setColor(Color.yellow);
g.draw3DRect(x, y, TANKWIDTH, TANKHEIGHT, true);
}
@Override
public void keyTyped(KeyEvent e) {
System.out.println(e.getKeyCode());
shoot();
repaint();
}
@Override
public void keyPressed(KeyEvent e) {
moveRec(e);
repaint();
}
@Override
public void keyReleased(KeyEvent e) {
laserBeam.setRect(dx + TANKWIDTH/2, -10, 0, 0); //hide it
repaint();
}
public void shoot() {
laserBeam.setRect(dx + TANKWIDTH/2, 0, 2, dy);
}
public void moveRec(KeyEvent evt) {
switch (evt.getKeyCode()) {
case KeyEvent.VK_LEFT:
dx -= MOVESPEED;
yourtank.setRect(dx, dy, TANKWIDTH, TANKHEIGHT);
break;
case KeyEvent.VK_RIGHT:
dx += MOVESPEED;
yourtank.setRect(dx, dy, TANKWIDTH, TANKHEIGHT);
break;
case KeyEvent.VK_UP:
dy -= MOVESPEED;
yourtank.setRect(dx, dy, TANKWIDTH, TANKHEIGHT);
break;
case KeyEvent.VK_DOWN:
if (dy < HEIGHT)
dy += MOVESPEED;
yourtank.setRect(dx, dy, TANKWIDTH, TANKHEIGHT);
break;
}
}
}
class MonitorVehicleTracker {
private final Map<String, MutablePoint> locations;
public MonitorVehicleTracker(Map<String, MutablePoint> locations) {
this.locations = deepCopy(locations);
}
public synchronized Map<String, MutablePoint> getLocations() {
return deepCopy(locations);
}
public synchronized MutablePoint getLocation(String id) {
MutablePoint loc = locations.get(id);
return loc == null ? null : new MutablePoint(loc);
}
public synchronized void setLocation(String id, int x, int y, boolean alive) {
MutablePoint loc = locations.get(id);
if (loc == null)
throw new IllegalArgumentException("No such ID: " + id);
loc.x = x;
loc.y = y;
loc.alive = alive;
}
private static Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> m) {
Map<String, MutablePoint> result = new HashMap<String, MutablePoint>();
for (String id : m.keySet())
result.put(id, new MutablePoint(m.get(id)));
return Collections.unmodifiableMap(result);
}
}
class MutablePoint {
public int x, y;
public boolean alive;
public MutablePoint() {
x = 0;
y = 0;
alive = true;
}
public MutablePoint(MutablePoint p) {
this.x = p.x;
this.y = p.y;
this.alive = p.alive;
}
}