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; private Ball[] balls; private long period; // The amount of time in nanoseconds allocated to each frame public Animation(int width, int height, int fps) { setOpaque(true); setBackground(Color.WHITE); setSize(width, height); setPreferredSize(new Dimension(width, height)); this.period = (long) 1000.0 / fps * 1000000L; List<Ball> ballList = new ArrayList<Ball>(); 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() { final long dt = (long) (1000.0 / 50) * 1000000L; // Physics - 50 FPS final long dtLimit = 1000000000L; // Limit number of updates to 1 second Thread.currentThread().setPriority(Thread.MAX_PRIORITY); isRunning = true; prevStatsTime = System.nanoTime(); long accumulator = 0; long newTime, deltaTime, sleepTime; long overSleepTime = 0; long noDelays = 0; long sleepRemaining, beforeSleep, afterSleep; long currentTime = System.nanoTime(); while (isRunning) { newTime = System.nanoTime(); deltaTime = newTime - currentTime; currentTime = newTime; sleepTime = (period - deltaTime) - overSleepTime; if (deltaTime > dtLimit) deltaTime = dtLimit; // Limit the amount of physic updates if (sleepTime > 0) { /*sleepRemaining = sleepTime; while(sleepRemaining > 0) { beforeSleep = System.nanoTime(); if (sleepRemaining > 1000000L) { // Don't try to sleep any less then 1ms try { Thread.sleep(sleepRemaining / 1000000L); } catch (InterruptedException ex) { // do nothing } } afterSleep = System.nanoTime(); sleepRemaining -= (afterSleep - beforeSleep); }*/ if (sleepTime > 1000000L) { // Don't try to sleep any less then 1ms try { Thread.sleep(sleepTime / 1000000L); } catch (InterruptedException ex) { // do nothing } } while(System.nanoTime() - currentTime < sleepTime) { Thread.yield(); } overSleepTime = (System.nanoTime() - currentTime) - sleepTime; } else { overSleepTime = 0L; if (++noDelays >= NO_DELAYS_PER_YIELD) { Thread.yield(); // give another thread a chance to run noDelays = 0; } } accumulator += deltaTime; while (accumulator >= dt) { nextFrame(dt/1000000L); accumulator -= dt; physicUpdates++; } updateDisplay(); updateStats(); } cleanup(); } public void cleanup() { isRunning = false; System.exit(0); } /** * Creates the next frame */ public void nextFrame(double t) { if (isPaused) return; double rt = t; // remaining frame time do { // 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<Collision> collisions = new LinkedList<Collision>(); 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(); } 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(rt > 0); } 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); } frameCount++; } // 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 prevStatsTime; private long frameCount = 0; private double fpsStore[]; private long statsCount = 0; private double averageFPS = 0.0; private long physicUpdates = 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; } } private void updateStats() { long elapsedTime = System.nanoTime() - prevStatsTime; // time since last if (elapsedTime >= MAX_STATS_INTERVAL) { // record stats every MAX_STATS_INTERVAL // stats collection double actualFPS = (((double) frameCount / elapsedTime) * 1000000000L); double actualUPS = (((double) physicUpdates / elapsedTime) * 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; } physicUpdates = 0; frameCount = 0; prevStatsTime = System.nanoTime(); } } 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); } // 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() { }}