package ch.sahits.game.rendering;
import java.awt.Color;
import java.awt.DisplayMode;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import org.apache.log4j.Logger;
import ch.sahits.game.event.KeyPressEvent;
public abstract class MainFullScreenFrame extends JFrame implements Runnable, WindowListener {
private static final long serialVersionUID = 6115721848767388765L;
private static final Logger logger = Logger.getLogger(MainFullScreenFrame.class);
private Rectangle rect;
private List<Renderable> views = new ArrayList<Renderable>();
private static final int NUM_BUFFERS = 2; // used for page flipping
private static int DEFAULT_FPS = 25;
public static long ONE_SECOND_IN_NS = 1000000000L;
// private static long MAX_STATS_INTERVAL = ONE_SECOND_IN_NS;
// record stats every 1 second (roughly)
private static final int NO_DELAYS_PER_YIELD = 16;
/*
* Number of frames with a delay of 0 ms before the animation thread yields
* to other running threads.
*/
private static int MAX_FRAME_SKIPS = 5; // was 2;
// no. of frames that can be skipped in any one animation loop
// i.e the games state is updated but not rendered
private static int NUM_FPS = 10;
// number of FPS values stored to get an average
// used for gathering statistics
// private long statsInterval = 0L; // in ns
// private long prevStatsTime;
// private long totalElapsedTime = 0L;
private long gameStartTime; // TODO remove
// private long frameCount = 0;
private double fpsStore[];
// private long statsCount = 0;
// private double averageFPS = 0.0;
//
@SuppressWarnings("unused")
private long framesSkipped = 0L;
// private long totalFramesSkipped = 0L;
private double upsStore[];
// private double averageUPS = 0.0;
// private DecimalFormat df = new DecimalFormat("0.##"); // 2 dp
// private DecimalFormat timedf = new DecimalFormat("0.####"); // 4 dp
private Thread animator; // the thread that performs the animation
private volatile boolean running = false; // used to stop the animation
// thread
/** Period between two consecutive draw actions in ns */
private long period;
private boolean finishedOff = false;
@SuppressWarnings("unused")
private KeyListener listener = null;
// // used by quit 'button'
// private volatile boolean isOverQuitButton = false;
// private Rectangle quitArea;
// used by the pause 'button'
// private volatile boolean isOverPauseButton = false;
// private Rectangle pauseArea;
// private volatile boolean isPaused = false;
private Font font;
@SuppressWarnings("unused")
private FontMetrics metrics;
// used for full-screen exclusive mode
private GraphicsDevice gd;
@SuppressWarnings("unused")
private Graphics gScr;
@SuppressWarnings("unused")
private BufferStrategy bufferStrategy;
private Graphics dbg;
private Image dbImage = null;
public MainFullScreenFrame() {
period = ONE_SECOND_IN_NS / DEFAULT_FPS;
initFullScreen();
readyForTermination();
font = new Font("SansSerif", Font.BOLD, 24);
metrics = this.getFontMetrics(font);
addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
testPress(e.getX(), e.getY());
}
});
addMouseMotionListener(new MouseMotionAdapter(){
public void mouseMoved(MouseEvent e) {
testMove(e.getX(), e.getY());
}
});
addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent e) {
testKey(e);
}
});
// initialise timing elements
fpsStore = new double[NUM_FPS];
upsStore = new double[NUM_FPS];
for (int i = 0; i < NUM_FPS; i++) {
fpsStore[i] = 0.0;
upsStore[i] = 0.0;
}
pack();
calcSizes();
}
private void initFullScreen() {
GraphicsEnvironment ge = GraphicsEnvironment
.getLocalGraphicsEnvironment();
gd = ge.getDefaultScreenDevice();
setUndecorated(true); // no menu bar, borders, etc. or Swing components
setIgnoreRepaint(true); // turn off all paint events since doing active
// rendering
// setResizable(false); // This causes Linux to draw taskbars over the
// java window (Gnome)
if (!gd.isFullScreenSupported()) {
logger.fatal("Full-screen exclusive mode not supported");
System.exit(0);
}
gd.setFullScreenWindow(this); // switch on full-screen exclusive mode
// we can now adjust the display modes, if we wish
showCurrentMode();
// setDisplayMode(800, 600, 8); // or try 8 bits
// setDisplayMode(1280, 1024, 32);
// reportCapabilities();
// pWidth = getBounds().width;
// pHeight = getBounds().height;
setBufferStrategy();
} // end of initFullScreen()
/**
* Switch on page flipping: NUM_BUFFERS == 2 so there will be a 'primary
* surface' and one 'back buffer'.
*
* The use of invokeAndWait() is to avoid a possible deadlock with the event
* dispatcher thread. Should be fixed in J2SE 1.5
*
* createBufferStrategy) is an asynchronous operation, so sleep a bit so
* that the getBufferStrategy() call will get the correct details.
*/
private void setBufferStrategy()
{
try {
EventQueue.invokeAndWait(new Runnable() {
public void run() {
createBufferStrategy(NUM_BUFFERS);
}
});
} catch (Exception e) {
logger.fatal("Error while creating buffer strategy", e);
System.exit(0);
}
try { // sleep to give time for the buffer strategy to be carried out
Thread.sleep(500); // 0.5 sec
} catch (InterruptedException ex) {
}
bufferStrategy = getBufferStrategy(); // store for later
} // end of setBufferStrategy()
private void readyForTermination() {
// for shutdown tasks
// a shutdown may not only come from the program
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
updateRunning(false);
finishOff();
}
});
} // end of readyForTermination()
/**
* This method sets up the thread driving the game animation. For special needs
* this method can be overridden, however the super method should always be called.
*/
protected void gameStart()
// initialise and start the thread
{
if (animator == null || !running) {
animator = new Thread(this);
animator.start();
}
} // end of gameStart()
/**
* Use active rendering to replace the off screen image on the screen
*/
private void paintScreen()
// use active rendering to put the buffered image on-screen
{
Graphics g;
try {
g = this.getGraphics();
BufferedImage img = new BufferedImage(dbImage.getWidth(this), dbImage.getHeight(this), BufferedImage.TYPE_INT_RGB);
Graphics gr = img.getGraphics();
gr.drawImage(dbImage, 0, 0, this);
ImageIO.write(img, "png", new File("renderedImage.png"));
if ((g != null) && (dbImage != null)){
// counter++;
g.drawImage(dbImage, 0, 0, null);
}
// Sync the display on some systems.
// (on Linux, this fixes event queue problems)
Toolkit.getDefaultToolkit().sync();
g.dispose();
}
catch (Exception e) // quite commonly seen at applet destruction
{ logger.warn("Graphics error", e); }
}
/**
* Check the mouse clicks.
*
* @param x
* @param y
*/
public abstract void testPress(int x, int y);
/**
* Handle the mouse movement (e.g. for hovering effects or tooltips )
*
* @param x
* @param y
*/
protected abstract void testMove(int x, int y);
/**
* Handle key presses.
* A typical implementation would generate a {@link KeyPressEvent}
* @param key
*/
protected abstract void testKey(KeyEvent key);
/**
* Main method for running the game animation
*/
public void run()
/* The frames of the animation are drawn inside the while loop. */
{
long beforeTime, afterTime, timeDiff, sleepTime;
long overSleepTime = 0L;
int noDelays = 0;
long excess = 0L;
gameStartTime = System.nanoTime();
beforeTime = gameStartTime;
running = true;
while (running) {
gameUpdate();
gameRender();
paintScreen();
afterTime = System.nanoTime();
timeDiff = afterTime - beforeTime;
sleepTime = (period - timeDiff) - overSleepTime;
if (sleepTime > 0) { // some time left in this cycle
try {
Thread.sleep(sleepTime / 1000000L); // nano -> ms
} catch (InterruptedException ex) {
}
overSleepTime = (System.nanoTime() - afterTime) - sleepTime;
} else { // sleepTime <= 0; the frame took longer than the period
excess -= sleepTime; // store excess time value
overSleepTime = 0L;
if (++noDelays >= NO_DELAYS_PER_YIELD) {
Thread.yield(); // give another thread a chance to run
noDelays = 0;
}
}
beforeTime = System.nanoTime();
/*
* If frame animation is taking too long, update the game state
* without rendering it, to get the updates/sec nearer to the
* required FPS.
*/
int skips = 0;
while ((excess > period) && (skips < MAX_FRAME_SKIPS)) {
excess -= period;
gameUpdate(); // update state but don't render
skips++;
}
framesSkipped += skips;
}
finishOff();
} // end of run()
/**
* Timed events belong in this method
*/
protected abstract void gameUpdate();
/**
* Rendering the off screen image that is then replaced
*/
private void gameRender() {
if (dbImage == null){
dbImage = createImage(rect.width, rect.height);
if (dbImage == null) {
logger.error("dbImage is null");
return;
}
else
dbg = dbImage.getGraphics();
}
// clear the background
dbg.setColor(Color.BLACK);
dbg.fillRect(rect.x, rect.y, rect.width, rect.height);
dbg.setFont(font);
// report frame count & average FPS and UPS at top left
// dbg.drawString("Frame Count " + frameCount, 10, 25);
for (Renderable panel : views) {
if (panel.isEnabled()){
panel.gameRender(dbg);
}
}
BufferedImage img = new BufferedImage(dbImage.getWidth(this), dbImage.getHeight(this), BufferedImage.TYPE_INT_RGB);
Graphics gr = img.getGraphics();
gr.drawImage(dbImage, 0, 0, this);
try {
ImageIO.write(img, "png", new File("renderedImage_1.png"));
} catch (IOException e) {
e.printStackTrace();
}
} // end of gameRender()
// /**
// * The statistics: - the summed periods for all the iterations in this
// * interval (period is the amount of time a single frame iteration should
// * take), the actual elapsed time in this interval, the error between these
// * two numbers;
// *
// * - the total frame count, which is the total number of calls to run();
// *
// * - the frames skipped in this interval, the total number of frames
// * skipped. A frame skip is a game update without a corresponding render;
// *
// * - the FPS (frames/sec) and UPS (updates/sec) for this interval, the
// * average FPS & UPS over the last NUM_FPSs intervals.
// *
// * The data is collected every MAX_STATS_INTERVAL (1 sec).
// */
// private void storeStats()
// {
// frameCount++;
// statsInterval += period;
//
// if (statsInterval >= MAX_STATS_INTERVAL) { // record stats every
// // MAX_STATS_INTERVAL
// long timeNow = System.nanoTime();
//
// long realElapsedTime = timeNow - prevStatsTime; // time since last
// // stats collection
// totalElapsedTime += realElapsedTime;
//
// double timingError = ((double) (realElapsedTime - statsInterval) / statsInterval) * 100.0;
//
// totalFramesSkipped += framesSkipped;
//
// double actualFPS = 0; // calculate the latest FPS and UPS
// double actualUPS = 0;
// if (totalElapsedTime > 0) {
// actualFPS = (((double) frameCount / totalElapsedTime) * 1000000000L);
// actualUPS = (((double) (frameCount + totalFramesSkipped) / totalElapsedTime) * 1000000000L);
// }
//
// // store the latest FPS and UPS
// fpsStore[(int) statsCount % NUM_FPS] = actualFPS;
// upsStore[(int) statsCount % NUM_FPS] = actualUPS;
// statsCount = statsCount + 1;
//
// double totalFPS = 0.0; // total the stored FPSs and UPSs
// double totalUPS = 0.0;
// for (int i = 0; i < NUM_FPS; i++) {
// totalFPS += fpsStore[i];
// totalUPS += upsStore[i];
// }
//
// if (statsCount < NUM_FPS) { // obtain the average FPS and UPS
// averageFPS = totalFPS / statsCount;
// averageUPS = totalUPS / statsCount;
// } else {
// averageFPS = totalFPS / NUM_FPS;
// averageUPS = totalUPS / NUM_FPS;
// }
// /*
// * System.out.println(timedf.format( (double)
// * statsInterval/1000000000L) + " " + timedf.format((double)
// * realElapsedTime/1000000000L) + "s " + df.format(timingError) +
// * "% " + frameCount + "c " + framesSkipped + "/" +
// * totalFramesSkipped + " skip; " + df.format(actualFPS) + " " +
// * df.format(averageFPS) + " afps; " + df.format(actualUPS) + " " +
// * df.format(averageUPS) + " aups" );
// */
// framesSkipped = 0;
// prevStatsTime = timeNow;
// statsInterval = 0L; // reset
// }
// } // end of storeStats()
private void finishOff()
/*
* Tasks to do before terminating. Called at end of run() and via the
* shutdown hook in readyForTermination().
*
* The call at the end of run() is not really necessary, but included for
* safety. The flag stops the code being called twice.
*/
{ // System.out.println("finishOff");
if (!finishedOff) {
finishedOff = true;
// printStats();
restoreScreen();
System.exit(0);
}
} // end of finishedOff()
/**
* Switch off full screen mode. This also resets the display mode if it's
* been changed.
*/
private void restoreScreen()
{
Window w = gd.getFullScreenWindow();
if (w != null)
w.dispose();
gd.setFullScreenWindow(null);
} // end of restoreScreen()
// ------------------ display mode methods -------------------
private void showCurrentMode()
// print the display mode details for the graphics device
{
DisplayMode dm = gd.getDisplayMode();
logger.info("Current Display Mode: (" + dm.getWidth() + ","
+ dm.getHeight() + "," + dm.getBitDepth() + ","
+ dm.getRefreshRate() + ") ");
}
/**
* Calculate the size of the drawing panel to fill the screen, but
* leaving room for the JFrame's title bar and insets, the OS's insets
* (e.g. taskbar) and the textfields under the JPanel.
*/
private void calcSizes()
{
GraphicsConfiguration gc = getGraphicsConfiguration();
Rectangle screenRect = gc.getBounds();
//System.out.println("Screen bounds: "+screenRect);
// System.out.println("Screen size: " + screenRect);
Toolkit tk = Toolkit.getDefaultToolkit();
Insets desktopInsets = tk.getScreenInsets(gc);
//System.out.println("Desktop insets: "+desktopInsets);
//System.out.println("Frame insets "+frameInsets);
rect = new Rectangle(new Point(desktopInsets.left,desktopInsets.top));
rect.width = screenRect.width - (desktopInsets.left + desktopInsets.right);
// - (frameInsets.left + frameInsets.right);
rect.height = screenRect.height - (desktopInsets.top + desktopInsets.bottom);
// - (frameInsets.top + frameInsets.bottom);
//System.out.println("Frame bounds: "+rect);
// System.out.println("pWidth: " + pWidth + "; pHeight: " + pHeight);
}
/**
* Add a new view to the renderable pipeline.
* @param view to be added
* @return true if the adding was successful
*/
public boolean addView(Renderable view) {
return views.add(view);
}
/**
* Remove a view from the render pipeline
* @param view to be removed
* @return true if removal was succesful
*/
public boolean removeView(Renderable view) {
return views.remove(view);
}
public final Rectangle getRactangle(){
return rect;
}
/**
* This method my be overriden by subclasses to handle specific tasks when closing the window
*/
public void windowClosing(WindowEvent e) {
}
/**
* This method my be overriden by subclasses to handle specific tasks when closing the window
*/
public void windowClosed(WindowEvent e) {
}
public void windowIconified(WindowEvent e) {
updateRunning(false);
// Send pause to the server
}
public void windowDeiconified(WindowEvent e) {
updateRunning(true);
// Send pause to the server
}
/**
* Update the running flag
* @param running
*/
protected void updateRunning(boolean running) {
this.running=running;
}
public void windowActivated(WindowEvent e) {
updateRunning(true);
// Send pause to the server
}
public void windowDeactivated(WindowEvent e) {
updateRunning(false);
// Send pause to the server
}
@Override
public void windowOpened(WindowEvent e) {
}
}