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.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import ch.sahits.game.event.KeyPressEvent;
public abstract class MainFullScreenFrame extends JFrame implements Runnable, WindowListener {
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;
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;
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;
private FontMetrics metrics;
// used for full-screen exclusive mode
private GraphicsDevice gd;
private Graphics gScr;
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()) {
System.out.println("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) {
System.out.println("Error while creating buffer strategy");
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
{ System.out.println("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) {
System.out.println("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();
System.out.println("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);
Insets frameInsets = getInsets(); // only works after a pack() call
//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) {
}
}