import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.RenderingHints; import java.awt.Toolkit; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import javax.swing.JComponent; import java.text.DecimalFormat; import java.util.List; import java.util.ArrayList; import java.util.LinkedList; /** * @author grom */ public class Animation extends JComponent implements Runnable { private static final long serialVersionUID = 9214955127889918259L; /** * Number of frames with a delay of 0 ms before the animation thread yields * to other running threads. */ private static final int NO_DELAYS_PER_YIELD = 16; /** * Number of frames that can be skipped in any one animation loop. i.e the games * state is updated but not rendered */ private static final int MAX_FRAME_SKIPS = 5; private Ball[] balls; private long period; // The amount of time in nanoseconds allocated to each frame public Animation(int width, int height, long period) { setOpaque(true); setBackground(Color.WHITE); setSize(width, height); setPreferredSize(new Dimension(width, height)); this.period = period; List ballList = new ArrayList(); ballList.add(new Ball(this, 10, 10, 0.5, 0.2, Color.BLUE)); ballList.add(new Ball(this, 30, 30, 0.3, 0.1, Color.GREEN)); ballList.add(new Ball(this, 150, 150, -0.2, -0.3, Color.RED)); ballList.add(new Ball(this, 300, 440, -0.3, -0.4, Color.CYAN)); ballList.add(new Ball(this, 600, 10, 0.3, 0.3, Color.GRAY)); ballList.add(new Ball(this, 10, 460, 0.1, 0.2, Color.MAGENTA)); ballList.add(new Ball(this, 50, 50, 0.3, 0.2, Color.ORANGE)); ballList.add(new Ball(this, 620, 30, -0.4, -0.2, Color.YELLOW)); ballList.add(new Ball(this, 10, 330, 0.2, 0.3, Color.PINK)); balls = (Ball[]) ballList.toArray(new Ball[0]); initStats(); setFocusable(true); requestFocus(); readyForTermination(); } private void readyForTermination() { addKeyListener(new KeyAdapter() { // listen for esc, q, end, ctrl-c on the canvas to // allow a convenient exit from the full screen configuration public void keyPressed(KeyEvent e) { int keyCode = e.getKeyCode(); if ((keyCode == KeyEvent.VK_ESCAPE) || (keyCode == KeyEvent.VK_Q) || (keyCode == KeyEvent.VK_END) || ((keyCode == KeyEvent.VK_C) && e.isControlDown())) { isRunning = false; } } }); } private Thread animator; // Thread for performing the animation public void start() { // initialise and start the thread if (animator == null || !isRunning) { animator = new Thread(this); animator.start(); } } private volatile boolean isPaused = false; private volatile boolean isRunning = false; public void pause() { isPaused = true; } public void resume() { isPaused = false; } public void stop() { isRunning = false; } public void run() { long beforeTime, afterTime, timeDiff, sleepTime, lastRunTime, delta; long overSleepTime = 0L; int noDelays = 0; long excess = 0L; Thread.currentThread().setPriority(Thread.MAX_PRIORITY); isRunning = true; gameStartTime = System.nanoTime(); prevStatsTime = gameStartTime; beforeTime = gameStartTime; lastRunTime = gameStartTime; while (isRunning) { delta = System.nanoTime() - lastRunTime; // How long since we last execute the main game loop lastRunTime = System.nanoTime(); nextFrame(delta/1000000L); updateDisplay(); afterTime = System.nanoTime(); timeDiff = afterTime - beforeTime; sleepTime = (period - timeDiff) - overSleepTime; if (sleepTime > 0) { if (sleepTime > 1000000L) { // Don't try to sleep for any less then 1ms try { Thread.sleep(sleepTime / 1000000L /* nanosecs -> ms */); } catch (InterruptedException ex) { // do nothing } overSleepTime = (System.nanoTime() - afterTime) - sleepTime; } else { overSleepTime = 0L; } } 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; nextFrame(period/1000000L); // update state but don't render skips++; } framesSkipped += skips; updateStats(); } cleanup(); } public void cleanup() { isRunning = false; System.exit(0); } /** * Creates the next frame */ public void nextFrame(double t) { if (isPaused) return; long startTime = System.nanoTime(); double rt = t; // remaining frame time boolean isCollision = false; do { isCollision = false; // t is the time of the earliest collision. // if t == rt then there are either no collisions // or collision occured at time rt // Since only the earliest collision needs to be found, // the collision test are performed for the current // time of the earliest collision (t). // Test for wall collisions for (int i = 0; i < balls.length; i++) { t = Math.min(t, balls[i].testWallCollision(t)); } // Test for ball collisions // collision are added to the head of this list List collisions = new LinkedList(); Ball a, b; for (int i = 0; i < balls.length; i++) { a = balls[i]; for (int j = i+1; j < balls.length; j++) { b = balls[j]; t = Math.min(t, a.testBallCollision(b, t, collisions)); } } // Move the balls for (int i = 0; i < balls.length; i++) { balls[i].move(t); } // Handle the collisions that occured // NOTE: It is possible that two collision occurred at the // same time. Therefore the collision list is iterated // until we get to a collision that occured after the // time of the first collision for (Collision c : collisions) { if (c.t == t) { c.handle(); isCollision = true; } else { break; } } // calculate frame time renaming t = rt - t; rt = t; // continue updating frame if there was a collision and the frame period has not expired } while(isCollision && rt > 0 && System.nanoTime() - startTime < period); } private void renderFrame(Graphics2D g2d) { // clear screen in background g2d.setColor(getBackground()); g2d.fillRect(0, 0, this.getWidth(), this.getHeight()); g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); for (int i = 0; i < balls.length; i++) { balls[i].paint(g2d); } drawStats(g2d); } // off screen rendering private Graphics dbg; private Image dbImage = null; private void updateDisplay() { if (dbImage == null) { // Create buffer image dbImage = createImage(getWidth(), getHeight()); if (dbImage == null) { System.out.println("dbImage is null"); return; } else { dbg = dbImage.getGraphics(); } } // Render frame to off-screen buffer renderFrame((Graphics2D) dbg); // Use active rendering Graphics g; try { g = this.getGraphics(); if ((g != null) && (dbImage != null)) { g.drawImage(dbImage, 0, 0, null); Toolkit.getDefaultToolkit().sync(); // sync the display on some systems g.dispose(); } } catch (Exception e) { System.out.println("Graphics context error: " + e); } } // used for gathering statistics private static final long MAX_STATS_INTERVAL = 1000000000L; // record (roughly) every 1 second private static final int NUM_FPS = 10; // number of FPS values stored to get an average private long statsInterval = 0L; // in ns private long prevStatsTime; private long totalElapsedTime = 0L; private long gameStartTime; 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 void initStats() { 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; } } /** * 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 updateStats() { 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; 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; } framesSkipped = 0; prevStatsTime = timeNow; statsInterval = 0L; // reset } } private DecimalFormat df = new DecimalFormat("0.##"); private void drawStats(Graphics g) { // report frame count & average FPS and UPS at top left g.setColor(Color.GRAY); g.drawString("Average FPS/UPS: " + df.format(averageFPS) + ", " + df.format(averageUPS), 20, 25); g.drawString("Frames skipped: " + totalFramesSkipped, 20, 50); } // Overrides for performance public boolean isOpaque() { return true; } public void paint(Graphics g) { } public void paintComponent(Graphics g) { } public void paintImmediately(int x, int y, int w, int h) { } public void paintImmediately(java.awt.Rectangle r) { } public void validate() { } public void revalidate() { } public void repaint(long tm, int x, int y, int width, int height) { } public void repaint(java.awt.Rectangle r) { } protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) { } public void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) { } public void updateUI() { } }