DelegatingVehicleTrackerDemo.java
This version just replaced the game engine MonitorVehicleTracker with DelegatingVehicleTracker, since both classes are thread safe, the program continues to work.
DelegatingVehicleTracker store the locations in a thread-safe Map implementation (ConcurrentHashMap). It also stores the location using and immutable Point class.
Since Point is immutable, we can publish and share a reference to it in multi-threading environment without extra synchronization.
DelegatingVehicleTracker don't need explicit synchronization compare to MonitorVehicleTracker, because all access to state is managed by ConcurrentHashMap, and all the keys and values of the Map are immutable. We don't want the calling thread to replace the component in the Map, so we returns an unmodifiableMap instead.
All the state variables of DelegatingVehicleTracker includes:
- Mutable variable locations, which contains immutable state variable Point.
- Immutable variable unmodifiableMap.
They are safely published with final keyword, so we have initialization safety.
Point is immutable, locations and unmodifiableMap are thread safe.
As a result, they met publication requirements for an object:
- Immutable objects can be published through any mechanism;
- Effectively immutable objects must be safely published;
- Mutable objects must be safely published, and must be either thread safe or guarded by a lock.
/*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 java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.Timer;
/******* game graphics ********/
public class DelegatingVehicleTrackerDemo extends JFrame{
public DelegatingVehicleTrackerDemo() {
initGUI();
}
private void initGUI() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBounds(10, 10, 400, 400);
setLayout(new BorderLayout());
Map<String, Point> locations = new HashMap<>();
for(int i=0; i<5; i++) {
Point p = new Point(i* GameBoard.TANKWIDTH * 2, i* GameBoard.TANKHEIGHT * 2, true);
locations.put("tank"+i, p);
}
DelegatingVehicleTracker mt = new DelegatingVehicleTracker(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 DelegatingVehicleTrackerDemo();
}});
}
}
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 DelegatingVehicleTracker mt; //thread safe
private static final JButton startButton = new JButton("Start");
public GameBoard(DelegatingVehicleTracker mt) {
this.addKeyListener(this);
this.setBackground(Color.white);
this.setFocusable(true);
this.mt = mt;
setDoubleBuffered(true);
startButton.setText("Start");
startButton.setActionCommand("Start");
startButton.addActionListener(this);
this.add(startButton);
}
//avoid starting timer in constructor (partially constructed this object).
public void startTimer() {
new Timer(DELAY/2, this).start();
}
private class Animator extends SwingWorker<Void, Void> {
@Override
protected Void doInBackground() throws Exception {
while(!isCancelled()) {
Thread.sleep(DELAY);
//System.out.println(javax.swing.SwingUtilities.isEventDispatchThread());
for(String id: mt.getLocations().keySet()) {
Point p = mt.getLocation(id);
if(p.alive)
mt.setLocation(id, p.x + GameBoard.MOVESPEED, p.y + GameBoard.MOVESPEED * 2, true);
}
}
return null;
}
}
@Override
public void actionPerformed(ActionEvent e) {
if(e.getActionCommand() == "Start") {
new Animator().execute();
startButton.setEnabled(false);
startButton.setVisible(false);
}
//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(Point 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()) {
Point 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;
}
}
}
/*****Game Engine********/
class DelegatingVehicleTracker {
private final ConcurrentMap<String, Point> locations;
private final Map<String, Point> unmodifiableMap;
public DelegatingVehicleTracker(Map<String, Point> points) {
locations = new ConcurrentHashMap<String, Point>(points);
unmodifiableMap = Collections.unmodifiableMap(locations);
}
public Map<String, Point> getLocations() {
return unmodifiableMap;
}
public Point getLocation(String id) {
return locations.get(id);
}
public void setLocation(String id, int x, int y, boolean alive) {
if (locations.replace(id, new Point(x, y, alive)) == null)
throw new IllegalArgumentException("invalid vehicle name: " + id);
}
// Alternate version of getLocations (Listing 4.8)
public Map<String, Point> getLocationsAsStatic() {
return Collections.unmodifiableMap(
new HashMap<String, Point>(locations));
}
}
class Point {
public final int x, y;
public final boolean alive;
public Point(int x, int y, boolean alive) {
this.x = x;
this.y = y;
this.alive = alive;
}
}