http://www.dreaminco...topic113451.htm
I received a lot of questions:
refreshing every 16 milli seconds, with the VBL, is nice but what happens if my program spends a few milli seconds to effectively to the repaint ? Then if my Timer calls my program every 16 milli seconds and I actually spend 4 milli seconds to actually do the calculations and the repaint() I will miss a VBL cycle.
This is true.
So here is a better deigned version.
The calculations are done in a different thread (included in the class GameCanvas)
The thread updates the balls positions (it performs the calculation)
The method that does the paint() by itself is called by another thread of the Swing Timer class every 15 milli seconds
If the calculations are terminated it does the repaint(), and just the repaint, it does not waste time performing calculations.
The thread that perform calculation does not perform them if a repaint() has not refresh the display
The class used for the calculations of the sin and the cos. Quite the same as the one in the previous post.
// for speed purpose I wont call the sin and cos math fuction every time
// (these functions are a little time consuming)
// as I work with degrees (from 0 to 359 as int) I build a static table of sin and cos
// for these 360 values then we just have to fetch these values
// also no need to convert from degrees to radian each time
public class SinCosGravity {
// for how much I increment for every 30 degrees it is a "gravity"
// this the number by which I will mutiply the speed of the balls (in degree) according
// to 30 degrees slices
// so from 0 to 30 degrees for 30 to 60 degrees fro 60 to 90 degrees.....
static final int[] weight = {2, 2, 1, 1, 2, 2, 3, 3, 4, 4, 3, 3};
// static arrays shered by all instances
static double[] sinValue = new double[360];
static double[] cosValue = new double[360];
static int[] gravityValue = new int[360];
// all instances share this array build in a static initializer
static {
for(int i = 0; i < 360; i++) {
// have to convert in radian (and negate them if I go counter clockwise)
double radiant = -Math.toRadians((double) i);
// because Java sin/cos method use radian
sinValue[i] = Math.sin(radiant);
cosValue[i] = Math.cos(radiant);
gravityValue[i] = weight[i / 30];
}
}
// ok just a constructor
SinCosGravity() {
}
// retunrs sin for an int degree passed as parameter
double sin(int degree) {
return sinValue[degree % 360];
}
// retunrs cos for an int degree passed as parameter
double cos(int degree) {
return cosValue[degree % 360];
}
// retuns gravity for an int degree passed as parameter
int gravity(int degree) {
return gravityValue[degree % 360];
}
}
The main class, the one extending JFrame, has now a WindowListener to it can stop the threads when the program exists
import java.awt.BorderLayout;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import javax.swing.*;
public class GameFrame extends JFrame implements WindowListener {
// number of balls and balls speed in degree
private static final int NB_BALLS = 10, DEF_SPEED = 1;
GameCanvas canvas;
GameFrame() {
// frame description
super("Rotating balls");
// our Canvas
canvas = new GameCanvas(NB_BALLS, DEF_SPEED);
add(canvas, BorderLayout.CENTER);
// set it's size and make it visible
setSize(600+12, 600);
// have to know when I will be stop by user to stop the thread
addWindowListener(this);
// show us
setVisible(true);
// now that is visible we can tell it that we will use 2 buffers to do the repaint
// before being able to do that, the Canvas as to be visible
canvas.createBufferStrategy(2);
// start the thread that upgrades balls position
Thread thread = new Thread(canvas);
thread.start();
}
// just to start the application
public static void main(String[] args) {
// instance of our stuff
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new GameFrame();
}
});
}
//my windows listeners
// we need at least that one to stop the threads performing the calculation and doing the repaint
public void windowClosing(WindowEvent e) {
canvas.threadStop = true;
dispose();
}
public void windowActivated(WindowEvent e) {}
public void windowClosed(WindowEvent e) {}
public void windowDeactivated(WindowEvent e) {}
public void windowDeiconified(WindowEvent e) {}
public void windowIconified(WindowEvent e) {}
public void windowOpened(WindowEvent e) {}
}
The GameCanvas class now extends Runnable so calculations are performed in a different thread. I have also add an inner class Ball to get ride of the arrays for degree position, ... more OO
import java.awt.*;
import java.awt.image.BufferStrategy;
import java.util.Random;
import javax.swing.Timer;
// OK this the class where we will draw and where computations
// will be done in another thread
public class GameCanvas extends Canvas implements Runnable {
// back color LightYellow
Color backColor = new Color(255, 255, 150);
// my Swing timer
Timer timer;
// the deltaDegree we will apply to each balls times the gravity for the angle
int deltaDegree;
// the balls that will round
Ball[] balls;
// the size of the ball in pixels
final int BALLSIZE = 48, HALF_SIZE = BALLSIZE / 2, QUARTER_SIZE = HALF_SIZE / 2;
// for the computation of the postion and the repaint
Dimension size;
int centerX, centerY;
// a boolean to synchronize computation and drawing
boolean computationDone = false;
// a boolean to ask the treat to stop
boolean threadStop = false;
// this is a Canvas but I wont't let the system when to repaint it I will do it myself
GameCanvas(int nbBalls, int deltaDegree) {
super();
this.deltaDegree = deltaDegree;
// so ignore System's paint request I will handle them
setIgnoreRepaint(true);
balls = new Ball[nbBalls];
Random ran = new Random(); // for random color
// determine initial position of the balls
int deltaEach = 360 / nbBalls;
for(int i = 0; i < nbBalls; i++) {
// determine a different color for the ball
Color color = new Color(ran.nextInt(150), ran.nextInt(150), ran.nextInt(150));
// build balls at different positions evenly distributed
balls[i] = new Ball(i * deltaEach, color);
}
// build Chrono that will call me
Chrono chrono = new Chrono(this);
// ask the chrono to calll me every 60 times a second so every 16 ms (set say 15 for the time it takes to paint)
timer = new Timer(15, chrono);
timer.start();
}
// my own paint method that repaint off line and switch the displayed buffer
// according to the VBL
public synchronized void myRepaint() {
// computation a lot longer than expected (more than 15ms)... ignore it
if(!computationDone) {
return;
}
// ok doing the repaint on the not showed page
BufferStrategy strategy = getBufferStrategy();
Graphics graphics = strategy.getDrawGraphics();
// erase all what I had
graphics.setColor(backColor);
graphics.fillRect(0, 0, size.width, size.height);
// now we draw the center
graphics.setColor(Color.BLACK);
// ce center
graphics.fillOval(centerX + QUARTER_SIZE, centerY + QUARTER_SIZE, HALF_SIZE, HALF_SIZE);
// repaint all the balls
for(int i = 0; i < balls.length; i++) {
balls[i].myRepaint(graphics);
}
if(graphics != null)
graphics.dispose();
// show next buffer
strategy.show();
// synchronized the blitter page shown
Toolkit.getDefaultToolkit().sync();
// ok I can be called again
computationDone = false;
}
public void run() {
// in double for sin/cos calculation
double width, height;
// loop forever
for(;;) {
// my father ask me to stop
if(threadStop) {
// stop the timer called every 15ms
timer.stop();
// and exit myself from the run method
return;
}
// test if the computation where already done without a paint
if(computationDone) {
try {
Thread.sleep(1L); // if it the case sleep a milli
continue; // try again
}
catch(Exception e) {
}
}
// else compute the new position
size = this.getSize();
centerX = size.width / 2 - HALF_SIZE;
centerY = size.height / 2 - HALF_SIZE;
width = size.width / 3.0;
height = size.height / 3.0;
for(int i = 0; i < balls.length; i++) {
balls[i].computePos(width, height, centerX, centerY);
}
// tell myRepaint() that new positions have been calculated
computationDone = true;
}
}
// inner class that describes a ball
class Ball {
// color of the ball
Color color;
// degree of the ball
int degree;
// ball x,y positions
int x, y;
// to have the sin/cos/gravity
SinCosGravity scg = new SinCosGravity();
// constructor that receives the initial degree position
Ball(int degree, Color color) {
// save initial position
this.degree = degree;
// save that ball color
this.color = color;
}
// calculate next postion of the ball
void computePos(double width, double height, int centerX, int centerY) {
// gravity factor
int g = scg.gravity(degree);
// by how much I will increase my degrees
g *= deltaDegree;
// increments
degree += g;
// wrap around
degree %= 360;
// get sin and cos times size
double sin = scg.sin(degree) * height;
double cos = scg.cos(degree) * width;
// compute to int for pixels
x = (int) Math.floor(cos);
y = (int) Math.floor(sin);
// from the center of the screen
x += centerX;
y += centerY;
}
// draw the ball at the last computed position
void myRepaint(Graphics g) {
// set graphics to color of ball
g.setColor(color);
// draw it
g.fillOval(x, y, BALLSIZE, BALLSIZE);
}
}
}
And finally the Chrono class unchanged. This is the class that calls myRepaint() to do only the repaint
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
/** Will be called at each blitter page */
public class Chrono implements ActionListener {
GameCanvas gc;
// constructor that receives the GameCanvas that we will repaint every 60 milliseconds
Chrono(GameCanvas gc) {
this.gc = gc;
}
// calls the method to repaint the anim everytime I am called
public void actionPerformed(ActionEvent e) {
gc.myRepaint();
}
}
Enjoy





MultiQuote





|