The World of Bouncing Balls - An Introduction To Java Game Programming
The World of Bouncing Balls - An Introduction To Java Game Programming
yetanotherinsignificantprogrammingnotes...|HOME
Letusbeginbygettingsomeballsbouncing,asanintroductiontogameprogramming.
Demos
ClicktheimagetoruntheDEMOApplets:
//Ball'sproperties
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
1/31
29/08/13
privatefloatballRadius=200//Ball'sradius
privatefloatballX=ballRadius+50//Ball'scenter(x,y)
privatefloatballY=ballRadius+20
privatefloatballSpeedX=3//Ball'sspeedforxandy
privatefloatballSpeedY=2
privatestaticfinalintUPDATE_RATE=30//Numberofrefreshpersecond
/**ConstructortocreatetheUIcomponentsandinitgameobjects.*/
publicBouncingBallSimple(){
this.setPreferredSize(newDimension(BOX_WIDTH,BOX_HEIGHT))
//Starttheballbouncing(initsownthread)
ThreadgameThread=newThread(){
publicvoidrun(){
while(true){//Executeoneupdatestep
//Calculatetheball'snewposition
ballX+=ballSpeedX
ballY+=ballSpeedY
//Checkiftheballmovesoverthebounds
//Ifso,adjustthepositionandspeed.
if(ballXballRadius<0){
ballSpeedX=ballSpeedX//Reflectalongnormal
ballX=ballRadius//Repositiontheballattheedge
}elseif(ballX+ballRadius>BOX_WIDTH){
ballSpeedX=ballSpeedX
ballX=BOX_WIDTHballRadius
}
//Maycrossbothxandybounds
if(ballYballRadius<0){
ballSpeedY=ballSpeedY
ballY=ballRadius
}elseif(ballY+ballRadius>BOX_HEIGHT){
ballSpeedY=ballSpeedY
ballY=BOX_HEIGHTballRadius
}
//Refreshthedisplay
repaint()//CallbackpaintComponent()
//Delayfortimingcontrolandgiveotherthreadsachance
try{
Thread.sleep(1000/UPDATE_RATE)//milliseconds
}catch(InterruptedExceptionex){}
}
}
}
gameThread.start()//Callbackrun()
}
/**CustomrenderingcodesfordrawingtheJPanel*/
@Override
publicvoidpaintComponent(Graphicsg){
super.paintComponent(g)//Paintbackground
//Drawthebox
g.setColor(Color.BLACK)
g.fillRect(0,0,BOX_WIDTH,BOX_HEIGHT)
//Drawtheball
g.setColor(Color.BLUE)
g.fillOval((int)(ballXballRadius),(int)(ballYballRadius),
(int)(2*ballRadius),(int)(2*ballRadius))
//Displaytheball'sinformation
g.setColor(Color.WHITE)
g.setFont(newFont("CourierNew",Font.PLAIN,12))
StringBuildersb=newStringBuilder()
Formatterformatter=newFormatter(sb)
formatter.format("Ball@(%3.0f,%3.0f)Speed=(%2.0f,%2.0f)",ballX,ballY,
ballSpeedX,ballSpeedY)
g.drawString(sb.toString(),20,30)
}
/**mainprogram(entrypoint)*/
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
2/31
29/08/13
publicstaticvoidmain(String[]args){
//RunGUIintheEventDispatcherThread(EDT)insteadofmainthread.
javax.swing.SwingUtilities.invokeLater(newRunnable(){
publicvoidrun(){
//Setupmainwindow(usingSwing'sJframe)
JFrameframe=newJFrame("ABouncingBall")
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)
frame.setContentPane(newBouncingBallSimple())
frame.pack()
frame.setVisible(true)
}
})
}
}
Dissecting BouncingBallSimple.java:
IassumethatyouunderstandJavaGraphicsprogramming(AWT/Swingandcustompainting),andthemultithreading
issuesinvolved.
OurmainclassextendstheJPanel,soastooverridethepaintComponent()forourcustomrenderingcodes.
Intheconstructor,wesetuptheUIcomponents(setthepreferredsizefortheJPanel).Wethenstartanewthread
torunthegameupdate(movingtheball).
Foreachupdatestep,wemovetheballaccordingtoitsspeedandcheckforcollision.Ifitexceedsthebound,we
reactbyadjustingtheball'snewpositionandspeed.Inthissimplecase,theballreflectshorizontallyifitexceedsthe
xbound,andreflectsverticallyifitexceedstheybound.Wetheninvokerepaint()torefreshthescreen,whichin
turncallsthepaintComponent().
We override paintComponent() to perform our custom rendering. We use fillRect() to draw the rectangular
containerboxfillOval()todrawtheroundball,anddrawString()todisplayastatusmessage.
Inthemain()method,weconstructaJFrameastheapplication'smainwindow.WesetourcustomJPanelasthe
content pane for the JFrame. The UI codes are run in the Event Dispatcher Thread (EDT), via
javax.swing.SwingUtilities.invokeLater(),asrecommendedbySwingdevelopers.
This program, although works, is poor in design (in terms of modularity, reusability and expansibility). Moreover, the
collisiondetectionandresponsealgorithmiscrude.Thereisalsonotimingcontrol.
LetusrewritethebouncingballprogramwithproperObjectOrientedDesign,whichconsistsofthefollowingclasses:
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
3/31
29/08/13
TheBoxclasshasthefollowinginstancevariables:
minX,minY,maxXandmaxY,whichrepresentthebox'sbounds,withpackageaccess.
Take note that Java (and also Windows) Graphics Coordinates is inverted vertically, with origin (0, 0) at the topleft
corner,asshown:
TheBoxclasshasthefollowingpublicmethods:
Aconstructorwhichacceptthetopleftcorner(x,y),width,heightandcolor. It is safer to use x,y,width and
height,insteadofminX,minY,maxXandmaxY,todefinearectangle.
Aset()methodtosetorresetitsbounds.
Adraw(g)method,todrawitselfwiththegivengraphicscontextg(ofjava.awt.Graphics).
importjava.awt.*
/**
*Arectangularcontainerbox,containingthebouncingball.
*/
publicclassContainerBox{
intminX,maxX,minY,maxY//Box'sbounds(packageaccess)
privateColorcolorFilled//Box'sfilledcolor(background)
privateColorcolorBorder//Box'sbordercolor
privatestaticfinalColorDEFAULT_COLOR_FILLED=Color.BLACK
privatestaticfinalColorDEFAULT_COLOR_BORDER=Color.YELLOW
/**Constructors*/
publicContainerBox(intx,inty,intwidth,intheight,ColorcolorFilled,ColorcolorBorder){
minX=x
minY=y
maxX=x+width1
maxY=y+height1
this.colorFilled=colorFilled
this.colorBorder=colorBorder
}
/**Constructorwiththedefaultcolor*/
publicContainerBox(intx,inty,intwidth,intheight){
this(x,y,width,height,DEFAULT_COLOR_FILLED,DEFAULT_COLOR_BORDER)
}
/**Setorresettheboundariesofthebox.*/
publicvoidset(intx,inty,intwidth,intheight){
minX=x
minY=y
maxX=x+width1
maxY=y+height1
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
4/31
29/08/13
}
/**Drawitselfusingthegivengraphiccontext.*/
publicvoiddraw(Graphicsg){
g.setColor(colorFilled)
g.fillRect(minX,minY,maxXminX1,maxYminY1)
g.setColor(colorBorder)
g.drawRect(minX,minY,maxXminX1,maxYminY1)
}
}
TheBallclasshasthefollowinginstancevariables:
x,y,radiusandcolor,whichrepresenttheball'scenter(x,y)coordinates,radiusandcolor,respectively.
speedXandspeedY,whichrepresentthespeedinthexandydirections,measuredinpixelspertimestep.
Internally, all numbers are expressed in float to ensure smoothness in rendering, especially in the trigonometric
operations.Thenumberswillbetruncatedtointegralpixelvaluesfordisplay.(32bitsingleprecisionfloatissufficient
formostofthegames,doubleisprobablyanoverkill!)
TheBallclasshasthefollowingpublicmethods:
A constructor that accepts x, y,radius, velocity in the polar coordinates of speed and moveAngle (because it is
easierandmoreintuitiveforusertospecifyvelocitythisway),andcolor.
Adraw(g)methodtodrawitselfwiththegivengraphicscontext.
AtoString()todescribeitself,whichisusedinprintingtheball'sstatus.
A moveOneStepWithCollisionDetection(Boxbox) which moves the ball by one step, with collision detection
andresponse.
BelowisthelistingoftheBallclass:
importjava.awt.*
importjava.util.Formatter
/**
*Thebouncingball.
*/
publicclassBall{
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
5/31
29/08/13
floatx,y//Ball'scenterxandy(packageaccess)
floatspeedX,speedY//Ball'sspeedperstepinxandy(packageaccess)
floatradius//Ball'sradius(packageaccess)
privateColorcolor//Ball'scolor
privatestaticfinalColorDEFAULT_COLOR=Color.BLUE
/**
*Constructor:Foruserfriendliness,userspecifiesvelocityinspeedand
*moveAngleinusualCartesiancoordinates.NeedtoconverttospeedXand
*speedYinJavagraphicscoordinatesforeaseofoperation.
*/
publicBall(floatx,floaty,floatradius,floatspeed,floatangleInDegree,
Colorcolor){
this.x=x
this.y=y
//Convert(speed,angle)to(x,y),withyaxisinverted
this.speedX=(float)(speed*Math.cos(Math.toRadians(angleInDegree)))
this.speedY=(float)(speed*(float)Math.sin(Math.toRadians(angleInDegree)))
this.radius=radius
this.color=color
}
/**Constructorwiththedefaultcolor*/
publicBall(floatx,floaty,floatradius,floatspeed,floatangleInDegree){
this(x,y,radius,speed,angleInDegree,DEFAULT_COLOR)
}
/**Drawitselfusingthegivengraphicscontext.*/
publicvoiddraw(Graphicsg){
g.setColor(color)
g.fillOval((int)(xradius),(int)(yradius),(int)(2*radius),(int)(2*radius))
}
/**
*Makeonemove,checkforcollisionandreactaccordinglyifcollisionoccurs.
*
*@parambox:thecontainer(obstacle)forthisball.
*/
publicvoidmoveOneStepWithCollisionDetection(ContainerBoxbox){
//Gettheball'sbounds,offsetbytheradiusoftheball
floatballMinX=box.minX+radius
floatballMinY=box.minY+radius
floatballMaxX=box.maxXradius
floatballMaxY=box.maxYradius
//Calculatetheball'snewposition
x+=speedX
y+=speedY
//Checkiftheballmovesoverthebounds.Ifso,adjustthepositionandspeed.
if(x<ballMinX){
speedX=speedX//Reflectalongnormal
x=ballMinX//Repositiontheballattheedge
}elseif(x>ballMaxX){
speedX=speedX
x=ballMaxX
}
//Maycrossbothxandybounds
if(y<ballMinY){
speedY=speedY
y=ballMinY
}elseif(y>ballMaxY){
speedY=speedY
y=ballMaxY
}
}
/**Returnthemagnitudeofspeed.*/
publicfloatgetSpeed(){
return(float)Math.sqrt(speedX*speedX+speedY*speedY)
}
/**Returnthedirectionofmovementindegrees(counterclockwise).*/
publicfloatgetMoveAngle(){
return(float)Math.toDegrees(Math.atan2(speedY,speedX))
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
6/31
29/08/13
/**Returnmass*/
publicfloatgetMass(){
returnradius*radius*radius/1000f//Normalizebyafactor
}
/**Returnthekineticenergy(0.5mv^2)*/
publicfloatgetKineticEnergy(){
return0.5f*getMass()*(speedX*speedX+speedY*speedY)
}
/**Describeitself.*/
publicStringtoString(){
sb.delete(0,sb.length())
formatter.format("@(%3.0f,%3.0f)r=%3.0fV=(%2.0f,%2.0f)"+
"S=%4.1f\u0398=%4.0fKE=%3.0f",
x,y,radius,speedX,speedY,getSpeed(),getMoveAngle(),
getKineticEnergy())//\u0398istheta
returnsb.toString()
}
//ReusetobuildtheformattedstringfortoString()
privateStringBuildersb=newStringBuilder()
privateFormatterformatter=newFormatter(sb)
}
TheBallWorldclassprovidestheControlLogic(C)(gameStart(),gameUpdate()),aswellasthePresentationView
(V)byextendingtheJPanel.
importjava.awt.*
importjava.awt.event.*
importjava.util.Random
importjavax.swing.*
/**
*Thecontrollogicandmaindisplaypanelforgame.
*/
publicclassBallWorldextendsJPanel{
privatestaticfinalintUPDATE_RATE=30//Framespersecond(fps)
privateBallball//AsinglebouncingBall'sinstance
privateContainerBoxbox//Thecontainerrectangularbox
privateDrawCanvascanvas//Customcanvasfordrawingthebox/ball
privateintcanvasWidth
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
7/31
29/08/13
privateintcanvasHeight
/**
*ConstructortocreatetheUIcomponentsandinitthegameobjects.
*Setthedrawingcanvastofillthescreen(givenitswidthandheight).
*
*@paramwidth:screenwidth
*@paramheight:screenheight
*/
publicBallWorld(intwidth,intheight){
canvasWidth=width
canvasHeight=height
//Inittheballatarandomlocation(insidethebox)andmoveAngle
Randomrand=newRandom()
intradius=200
intx=rand.nextInt(canvasWidthradius*220)+radius+10
inty=rand.nextInt(canvasHeightradius*220)+radius+10
intspeed=5
intangleInDegree=rand.nextInt(360)
ball=newBall(x,y,radius,speed,angleInDegree,Color.BLUE)
//InittheContainerBoxtofillthescreen
box=newContainerBox(0,0,canvasWidth,canvasHeight,Color.BLACK,Color.WHITE)
//Initthecustomdrawingpanelfordrawingthegame
canvas=newDrawCanvas()
this.setLayout(newBorderLayout())
this.add(canvas,BorderLayout.CENTER)
//Handlingwindowresize.
this.addComponentListener(newComponentAdapter(){
@Override
publicvoidcomponentResized(ComponentEvente){
Componentc=(Component)e.getSource()
Dimensiondim=c.getSize()
canvasWidth=dim.width
canvasHeight=dim.height
//Adjusttheboundsofthecontainertofillthewindow
box.set(0,0,canvasWidth,canvasHeight)
}
})
//Starttheballbouncing
gameStart()
}
/**Starttheballbouncing.*/
publicvoidgameStart(){
//Runthegamelogicinitsownthread.
ThreadgameThread=newThread(){
publicvoidrun(){
while(true){
//Executeonetimestepforthegame
gameUpdate()
//Refreshthedisplay
repaint()
//Delayandgiveotherthreadachance
try{
Thread.sleep(1000/UPDATE_RATE)
}catch(InterruptedExceptionex){}
}
}
}
gameThread.start()//InvokeGaemThread.run()
}
/**
*Onegametimestep.
*Updatethegameobjects,withpropercollisiondetectionandresponse.
*/
publicvoidgameUpdate(){
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
8/31
29/08/13
ball.moveOneStepWithCollisionDetection(box)
}
/**Thecustomdrawingpanelforthebouncingball(innerclass).*/
classDrawCanvasextendsJPanel{
/**Customdrawingcodes*/
@Override
publicvoidpaintComponent(Graphicsg){
super.paintComponent(g)//Paintbackground
//Drawtheboxandtheball
box.draw(g)
ball.draw(g)
//Displayball'sinformation
g.setColor(Color.WHITE)
g.setFont(newFont("CourierNew",Font.PLAIN,12))
g.drawString("Ball"+ball.toString(),20,30)
}
/**Calledbacktogetthepreferredsizeofthecomponent.*/
@Override
publicDimensiongetPreferredSize(){
return(newDimension(canvasWidth,canvasHeight))
}
}
}
Thegameloopisrunninginitsownthread(GameThread),bysubclassingThreadandoverridingtherun()methodto
programtherunningbehavior.Multithreadisnecessaryforgameprogramming,asthegraphicssubsystemusesaso
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
9/31
29/08/13
called Event Dispatch Thread (EDT) to monitor the input events (such as keypress, mouseclick), run the event
handlers, and refreshes the display. If the Event Dispatch Thread is starved (e.g., the GameThread does not yield
control), the screen freezes and no inputs can be processed, resulted in the infamous unresponsive userinterface
problem.
Method run() is not supposed to be called directly, but calledback via the Thread's methodstart(). The static
methodThread.sleep()suspendsthisgamethreadbythespecifiedmilliseconds.Thesleep()servestwopurposes:
Itprovidesthenecessarydelaytoachievethetargetupdate(orrefresh)rate,andalsoyieldscontroltootherthreadsto
do their assigned tasks, in particular, the GUI Event Dispatch Thread which refreshes the screen and processes the
inputs.
Thegameloop,inthiscase,isstraightforward.Foreachstep,itmovestheballandchecksifcollisionoccurs.Ifso,it
computestheproperresponse.Itrefreshesthedisplaybyinvokingrepaint(),whichcallsbackpaintComponent()of
theDrawCanvastocarryoutthecustomdrawing.
TheMainclassprovidesthemain()methodtostarttheapplication,whichallocatesandsetsupaJFrame.Aninstance
ofBallWorldisconstructedandsetasthecontentpanefortheJFrame.
Themain()usesthestandardproceduretoconstructtheUIintheEventDispatchThreadtoensurethreadsafety(see
JavaSwingonlinetutorial@http://java.sun.com/docs/books/tutorial/uiswing).
Forthisexample,youcouldincludethemain()methodintheBallWorldclass,anddiscardtheMainclass.
Tryrunningtheprogramandresizingthewindow.
Running as an Applet
InsteadoftheMainclass,wecoulduseaMainAppletclasstorunthisprogramasanJavaapplet.
importjavax.swing.JApplet
/**
*MainProgramtorunasanapplet
*Thedisplayareais640x480.
*/
publicclassMainAppletextendsJApplet{
@Override
publicvoidinit(){
//RunUIintheEventDispatcherThread
javax.swing.SwingUtilities.invokeLater(newRunnable(){
publicvoidrun(){
setContentPane(newBallWorld(640,480))//BallWorldisaJPanel
}
})
}
}
AppletextendsfromJApplet(orApplet),andusesinit()insteadofmain()tobeginoperation.
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
10/31
29/08/13
RuntheJDK'sjarutilityfromacmdshelltojarupalltheclassesasfollow(thecommandlineoptionsare:'c'forcreate,
'v'forverbose,'m'formanifest,'f'forjar'sfilename):
...changethecurrentworkingdirectorytotheapplication'sbasedirectory...
>jarcvmfBallWorld.manifestballworld.jar*.class
In Eclipse, rightclick the project Export... Java JAR file Next Check "Export generated class files and
resources"(youmayalsoexportthesourcefiles)NextNextCheck"Generatethemanifestfile"In"Mainclass",
enteryourmainclassFinish.
YoucanusetheJavaruntimewith"jar"optiontorunthestandaloneapplicationfromtheJARfile(asfollow)orsimply
doubleclicktheJARfile.Theembeddedmanifestspecifiesthemainclasstostarttheapplication.
>javajarballworld.jar
If your program (in the JAR file) requires other JAR files, include a "ClassPath" in the manifest as follows. The JAR
filesareseparatedbyspace(?!).Thereisnoneedtoincludethecurrentdirectory(?!).
ManifestVersion:1.0
MainClass:Main
ClassPath:collisionphysics.jaranother.jar
Distribute Applet in a JAR file: To run an applet from a JAR file, provide an HTML script with attribute
"archive"selectingtheJARfileandattribute"code"selectingtheappletclass:
<html>
<head><title>ABouncingBall</title></head>
<body>
<h2>ABouncingBall</h2>
<appletcode="MainApplet.class"
width="640"height="480"
archive="ballworld.jar">
</applet>
</body>
</html>
YoumayincludeadditionalJARfilesinthearchiveattribute.Thejarfilesareseperatedbycommas.
Summary:Forastandaloneapplication,themainclassisspecifiedinthemanifestforanapplet,themainclassis
specifiedinthe<applet>'scodeattribute,nomanifestneeded.Foranapplication,additionalJARfilesarespecifiedin
themanifestforanapplet,theyareaddedinthe<applet>'sarchiveattribute.
/**ConstructortoinitializeUI*/
publicMainFullScreenOnly(){
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
11/31
29/08/13
//Getthedefaultgraphicdeviceandtryfullscreenmode
GraphicsDevicedevice=GraphicsEnvironment.getLocalGraphicsEnvironment()
.getDefaultScreenDevice()
if(device.isFullScreenSupported()){//Goforfullscreenmode
this.setUndecorated(true)//Don'tshowtitleandborder
this.setResizable(false)
//this.setIgnoreRepaint(true)//IgnoreOSrepaintrequest
device.setFullScreenWindow(this)
}else{//Runinwindowedmodeiffullscreenisnotsupported
Dimensiondim=Toolkit.getDefaultToolkit().getScreenSize()
this.setSize(dim.width,dim.height40)//minustaskbar
this.setResizable(true)
}
//Allocatethegamepaneltofillthecurrentscreen
BallWorldballWorld=newBallWorld(this.getWidth(),this.getHeight())
this.setContentPane(ballWorld)//SetascontentpaneforthisJFrame
//Tohandlekeyevents
this.addKeyListener(newKeyAdapter(){
@Override
publicvoidkeyPressed(KeyEvente){
intkeyCode=e.getKeyCode()
switch(keyCode){
caseKeyEvent.VK_ESCAPE://ESCtoquit
System.exit(0)
break
}
}
})
this.setFocusable(true)//Toreceivekeyevent
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)
this.setTitle("AWorldofBalls")
this.pack()//Packtopreferredsize
this.setVisible(true)//Showit
}
/**Entrymainprogram*/
publicstaticvoidmain(String[]args){
//RunUIintheEventDispatcherThread(EDT),insteadofMainthread
javax.swing.SwingUtilities.invokeLater(newRunnable(){
publicvoidrun(){
newMainFullScreenOnly()
}
})
}
}
The above main program runs the bouncing ball in fullscreen mode, if fullscreen mode is supported. It first queries
GraphicsDevice.isFullScreenSupported(),
and
switches
into
fullscreen
mode
via
GraphicsDevice.setFullScreenWindow(this).
If fullscreen mode is not available, the program runs in windowed mode, using the maximum window size. We use
Toolkit.getDefaultToolkit().getScreenSize()tofindthecurrentscreensize.
Ialsosetupthekeyeventhandler,sothatwecoulduseESCkeytoquittheprograminfullscreenmode.
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
12/31
29/08/13
Asillustrated,theradiusoftheballeffectivelyreducestheboundariesoftheboxbythatamount,orshortenthecollision
time.Hence,bymovingtheboundariesofthebox,wecansimplifytheballtoapoint.Thissimplificationisimportantina
complexsituation.
Package collisionphysics: In order to handle collision detection and response for many situations, I created a
packagecalledcollisionphysics,whichincludesstaticmethodsfordetectingcollisionandcomputingresponses.
CollisionResponse.java:Ifcollisionoccurs,thecollisiontimeandresponses(newSpeedX,newSpeedY)arekeptin
an object of CollisionResponse. My collision detection algorithems are based on ray tracing and uses parametric
equationtofindtheearliestpositivecollisiontime.
packagecollisionphysics
/**
*Ifcollisionoccurs,thisobjectstoresthecollisiontimeand
*thecomputedresponses,newspeed(newSpeedX,newSpeedY).
*/
publicclassCollisionResponse{
/**Detectedcollisiontime,resettoFloat.MAX_VALUE*/
publicfloatt
//Timethresholdtobesubtractedfromcollisiontime
//topreventmovingoverthebound.Assumethatt<=1.
privatestaticfinalfloatT_EPSILON=0.005f
/**Computedspeedinxdirectionaftercollision*/
publicfloatnewSpeedX
/**Computedspeedinydirectionaftercollision*/
publicfloatnewSpeedY
/**Constructorwhichresetsthecollisiontimetoinfinity.*/
publicCollisionResponse(){
reset()//Resetdetectedcollisiontimetoinfinity
}
/**Resetthedetectedcollisiontimetoinfinity.*/
publicvoidreset(){
this.t=Float.MAX_VALUE
}
/**Copythisinstancetoanother,usedtofindtheearliestcollision.*/
publicvoidcopy(CollisionResponseanother){
this.t=another.t
this.newSpeedX=another.newSpeedX
this.newSpeedY=another.newSpeedY
}
/**Returnthexpositionafterimpact.*/
publicfloatgetNewX(floatcurrentX,floatspeedX){
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
13/31
29/08/13
//Subtractbyasmallthreadtomakesurethatitdoesnotcrossthebound.
if(t>T_EPSILON){
return(float)(currentX+speedX*(tT_EPSILON))
}else{
returncurrentX
}
}
/**Returntheypositionafterimpact.*/
publicfloatgetNewY(floatcurrentY,floatspeedY){
//Subtractbyasmallthreadtomakesurethatitdoesnotcrossthebound.
if(t>T_EPSILON){
return(float)(currentY+speedY*(tT_EPSILON))
}else{
returncurrentY
}
}
}
Themethodreset()setsthecollisiontimetopositiveinfinity(Float.MAX_VALUE).
In a complex systems (e.g., mutliple balls), we need to look for the earliest collision in the entire system. The copy()
methodcanbeusedtotransferthecurrentresponsetotheearliestresponse,ifithasasmallertime.
CollisionPhsyics.java: The main class (modeled after java.lang.Math), which provides static methods for
collisiondetectionandresponse.
packagecollisionphysics
publicclassCollisionPhysics{
//Workingcopyforcomputingresponseinintersect(ContainerBoxbox),
//toavoidrepeatedlyallocatingobjects.
privatestaticCollisionResponsetempResponse=newCollisionResponse()
/**
*Detectcollisionforamovingpointbouncinginsidearectangularcontainer,
*withinthegiventimeLimit.
*IfcollisionisdetectedwithinthetimeLimit,computecollisiontimeand
*responseinthegivenCollisionResponseobject.Otherwise,setcollisiontime
*toinfinity.
*TheresultispassedbackinthegivenCollisionResponseobject.
*/
publicstaticvoidpointIntersectsRectangleOuter(
floatpointX,floatpointY,floatspeedX,floatspeedY,floatradius,
floatrectX1,floatrectY1,floatrectX2,floatrectY2,
floattimeLimit,CollisionResponseresponse){
response.reset()//Resetdetectedcollisiontimetoinfinity
//Aouterrectangularcontainerboxhas4borders.
//Needtolookfortheearliestcollision,ifany.
//Rightborder
pointIntersectsLineVertical(pointX,pointY,speedX,speedY,radius,
rectX2,timeLimit,tempResponse)
if(tempResponse.t<response.t){
response.copy(tempResponse)//Copyintoresultantresponse
}
//Leftborder
pointIntersectsLineVertical(pointX,pointY,speedX,speedY,radius,
rectX1,timeLimit,tempResponse)
if(tempResponse.t<response.t){
response.copy(tempResponse)
}
//Topborder
pointIntersectsLineHorizontal(pointX,pointY,speedX,speedY,radius,
rectY1,timeLimit,tempResponse)
if(tempResponse.t<response.t){
response.copy(tempResponse)
}
//Bottomborder
pointIntersectsLineHorizontal(pointX,pointY,speedX,speedY,radius,
rectY2,timeLimit,tempResponse)
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
14/31
29/08/13
if(tempResponse.t<response.t){
response.copy(tempResponse)
}
}
/**
*Detectcollisionforamovingpointhittingahorizontalline,
*withinthegiventimeLimit.
*/
publicstaticvoidpointIntersectsLineVertical(
floatpointX,floatpointY,floatspeedX,floatspeedY,floatradius,
floatlineX,floattimeLimit,CollisionResponseresponse){
response.reset()//Resetdetectedcollisiontimetoinfinity
//NocollisionpossibleifspeedXiszero
if(speedX==0){
return
}
//Computethedistancetotheline,offsetbyradius.
floatdistance
if(lineX>pointX){
distance=lineXpointXradius
}else{
distance=lineXpointX+radius
}
floatt=distance/speedX//speedX!=0
//Accept0<t<=timeLimit
if(t>0&&t<=timeLimit){
response.t=t
response.newSpeedX=speedX//Reflecthorizontally
response.newSpeedY=speedY//Nochangevertically
}
}
/**
*@seemovingPointIntersectsLineVertical().
*/
publicstaticvoidpointIntersectsLineHorizontal(
floatpointX,floatpointY,floatspeedX,floatspeedY,floatradius,
floatlineY,floattimeLimit,CollisionResponseresponse){
response.reset()//Resetdetectedcollisiontimetoinfinity
//NocollisionpossibleifspeedYiszero
if(speedY==0){
return
}
//Computethedistancetotheline,offsetbyradius.
floatdistance
if(lineY>pointY){
distance=lineYpointYradius
}else{
distance=lineYpointY+radius
}
floatt=distance/speedY//speedY!=0
//Accept0<t<=timeLimit
if(t>0&&t<=timeLimit){
response.t=t
response.newSpeedY=speedY//Reflectvertically
response.newSpeedX=speedX//Nochangehorizontally
}
}
}
The pointIntersectsLineXxx() methods take a moving ball (currentX, currentY, speedX, speedY, radius), a
vertical/horizontal line (lineX|lineY), a time limit (timeLimit), and a CollisionResponse object. If a collision is
detected within the given time limit, it computes the collision time and responses (newSpeedX,newSpeedY) and stores
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
15/31
29/08/13
thembacktothegivenCollisionResponseobject.
ThepointIntersectsRectangleOuter()usestheabovemethodstodetecttheearliestcollisiontothe4bordersof
the container box. Only the first collision matters, which nullifies all the subsequent detected collisions. It positions the
ballaccuratelyafterthecollisionatthepointofimpact.
Ball.java: Weshallprepareforthemultipleballcase,whereonlytheearliestcollisionmatters.Eachballshalldetect
probablecollisiontoalltheotherobjectsinthesystem.Itshallmaintaininformationaboutitsearliercollisiondetected.
......
publicclassBall{
......
//Forcollisiondetectionandresponse
//Maintaintheresponseoftheearliestcollisiondetected
//bythisballinstance.(packageaccess)
CollisionResponseearliestCollisionResponse=newCollisionResponse()
......
//Workingcopyforcomputingresponseinintersect(ContainerBoxbox),
//toavoidrepeatedlyallocatingobjects.
privateCollisionResponsetempResponse=newCollisionResponse()
/**
*Checkifthisballcollideswiththecontainerboxinthecomingtimestep.
*
*@parambox:container(obstacle)forthisball
*/
publicvoidintersect(ContainerBoxbox){
//CallmovingPointIntersectsRectangleOuter,whichreturnsthe
//earliestcollisiontooneofthe4borders,ifcollisiondetected.
CollisionPhysics.pointIntersectsRectangleOuter(
this.x,this.y,this.speedX,this.speedY,this.radius,
box.minX,box.minY,box.maxX,box.maxY,
1.0f,tempResponse)
if(tempResponse.t<earliestCollisionResponse.t){
earliestCollisionResponse.copy(tempResponse)
}
}
/**
*Updatethestatesofthisballforonetimestep.
*Moveforonetimestepifnocollisionoccursotherwisemoveupto
*theearliestdetectedcollision.
*/
publicvoidupdate(){
//Checktheearliestcollisiondetectedforthisballstoredin
//earliestCollisionResponse.
if(earliestCollisionResponse.t<=1.0f){//Collisiondetected
//Thisballcollided,getthenewpositionandspeed
this.x=earliestCollisionResponse.getNewX(this.x,this.speedX)
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
16/31
29/08/13
this.y=earliestCollisionResponse.getNewY(this.y,this.speedY)
this.speedX=(float)earliestCollisionResponse.newSpeedX
this.speedY=(float)earliestCollisionResponse.newSpeedY
}else{//Nocollisioninthiscomingtimestep
//Makeacompletemove
this.x+=this.speedX
this.y+=this.speedY
}
//Clearforthenextcollisiondetection
earliestCollisionResponse.reset()
}
}
BallWorld.java:
......
publicclassBallWorldextendsJPanel{
......
publicvoidgameUpdate(){
//Detectcollisionforthisballwiththecontainerbox.
ball.intersect(box)
//Updatetheball'sstatewithpropercollisionresponseifcollided.
ball.update()
}
}
Run this example and compare with previous example. Closely observe the collision by reducing the refresh rate and
increasetheball'sspeed.
Ball.java:
......
publicclassBall{
......
//Forcollisiondetectionandresponse
//Maintaintheresponseoftheearliestcollisiondetected
//bythisballinstance.Onlythefirstcollisionmatters!
CollisionResponseearliestCollisionResponse=newCollisionResponse()
......
//Workingcopyforcomputingresponseinintersect(box,timeLimit),
//toavoidrepeatedlyallocatingobjects.
privateCollisionResponsetempResponse=newCollisionResponse()
/**
*Checkifthisballcollideswiththecontainerboxintheinterval
*(0,timeLimit].
*/
publicbooleanintersect(ContainerBoxbox,floattimeLimit){
//CallmovingPointIntersectsRectangleOuter,whichreturnsthe
//earliestcollisiontooneofthe4borders,ifcollisiondetected.
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
17/31
29/08/13
CollisionPhysics.pointIntersectsRectangleOuter(x,y,speedX,speedY,radius,
box.minX,box.minY,box.maxX,box.maxY,timeLimit,tempResponse)
if(tempResponse.t<earliestCollisionResponse.t){
earliestCollisionResponse.copy(tempResponse)
}
}
publicvoidupdate(floattime){
//Checkifthisballisresponsibleforthefirstcollision?
if(earliestCollisionResponse.t<=time){
//Thisballcollided,getthenewpositionandspeed
this.x=earliestCollisionResponse.getNewX(this.x,this.speedX)
this.y=earliestCollisionResponse.getNewY(this.y,this.speedY)
this.speedX=(float)earliestCollisionResponse.newSpeedX
this.speedY=(float)earliestCollisionResponse.newSpeedY
}else{
//Thisballdoesnotinvolveinacollision.Movestraight.
this.x+=this.speedX*time
this.y+=this.speedY*time
}
//Clearforthenextcollisiondetection
earliestCollisionResponse.reset()
}
}
BallWorld.java: Modify the gameUpdate() method in the class BallWorld to make use of the intersect() to
consume one full timestep of movement, even after possibly multiple collisions. Also modify the game loop to control
onetimestepprecisely.
......
publicclassBallWorldextendsJPanel{
privatestaticfinalfloatEPSILON_TIME=1e2f//Thresholdforzerotime
......
publicvoidgameStart(){
//Runthegamelogicinitsownthread.
ThreadgameThread=newThread(){
publicvoidrun(){
while(true){
longbeginTimeMillis,timeTakenMillis,timeLeftMillis
beginTimeMillis=System.currentTimeMillis()
//Executeonegamestep
gameUpdate()
//Refreshthedisplay
repaint()
//Providethenecessarydelaytomeetthetargetrate
timeTakenMillis=System.currentTimeMillis()beginTimeMillis
timeLeftMillis=1000L/UPDATE_RATEtimeTakenMillis
if(timeLeftMillis<5)timeLeftMillis=5//Setaminimum
//Delayandgiveotherthreadachance
try{
Thread.sleep(timeLeftMillis)
}catch(InterruptedExceptionex){}
}
}
}
gameThread.start()//InvokeGaemThread.run()
}
/**
*Onegametimestep.
*Updatethegameobjects,withpropercollisiondetectionandresponse.
*/
publicvoidgameUpdate(){
floattimeLeft=1.0f//Onetimesteptobeginwith
//Repeatuntiltheonetimestepisup
do{
//Needtofindtheearliestcollisiontimeamongallobjects
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
18/31
29/08/13
floatearliestCollisionTime=timeLeft
//Specialcasehereasthereisonlyonemovingball.
ball.intersect(box,timeLeft)
if(ball.earliestCollisionResponse.t<earliestCollisionTime){
earliestCollisionTime=ball.earliestCollisionResponse.t
}
//UpdatealltheobjectsforearliestCollisionTime
ball.update(earliestCollisionTime)
//TestingOnlyShowcollisionposition
if(earliestCollisionTime>0.05){//Donotdisplaysmallchanges
repaint()
try{
Thread.sleep((long)(1000L/UPDATE_RATE*earliestCollisionTime))
}catch(InterruptedExceptionex){}
}
timeLeft=earliestCollisionTime//Subtractthetimeconsumedandrepeat
}while(timeLeft>EPSILON_TIME)//Ignoreremainingtimelessthanthreshold
}
}
Again, compare the output of example 2, 3, and 4. Closely observe the collisions by reducing the refresh rate and
increasetheball'sspeed.
Click the image to run the DEMO. Click HERE to download the source codes for this example (unzip the downloaded
JARfile).
Modify the BallWorld class to include a ControlPanel inner class, which maintains the control UI components
(checkbox,slider,button):
publicclassBallWorldextendsJPanel{
......
......
privateControlPanelcontrol//Thecontrolpanelofbuttonsandsliders.
/**ConstructortocreatetheUIcomponentsandinitthegameobjects.*/
publicBallWorld(){
.......
//Controlpanel
control=newControlPanel()
//Layoutthedrawingpanelandcontrolpanel
this.setLayout(newBorderLayout())
this.add(canvas,BorderLayout.CENTER)
this.add(control,BorderLayout.SOUTH)
//Starttheballbouncing
gameStart()
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
19/31
29/08/13
......
......
/**Thecontrolpanel(innerclass).*/
classControlPanelextendsJPanel{
/**ConstructortoinitializeUIcomponentsofthecontrols*/
publicControlPanel(){
//Acheckboxtotogglepause/resumemovement
JCheckBoxpauseControl=newJCheckBox()
this.add(newJLabel("Pause"))
this.add(pauseControl)
pauseControl.addItemListener(newItemListener(){
@Override
publicvoiditemStateChanged(ItemEvente){
paused=!paused//Togglepause/resumeflag
}
})
//Asliderforadjustingthespeedoftheball
intminSpeed=2
intmaxSpeed=20
JSliderspeedControl=newJSlider(JSlider.HORIZONTAL,minSpeed,maxSpeed,
(int)ball.getSpeed())
this.add(newJLabel("Speed"))
this.add(speedControl)
speedControl.addChangeListener(newChangeListener(){
@Override
publicvoidstateChanged(ChangeEvente){
JSlidersource=(JSlider)e.getSource()
if(!source.getValueIsAdjusting()){
intnewSpeed=(int)source.getValue()
intcurrentSpeed=(int)ball.getSpeed()
ball.speedX*=(float)newSpeed/currentSpeed
ball.speedY*=(float)newSpeed/currentSpeed
}
}
})
//Asliderforadjustingtheradiusoftheball
intminRadius=10
intmaxRadius=((canvasHeight>canvasWidth)?canvasWidth:canvasHeight)/28
radiusControl=newJSlider(JSlider.HORIZONTAL,minRadius,
maxRadius,(int)ball.radius)
this.add(newJLabel("BallRadius"))
this.add(radiusControl)
radiusControl.addChangeListener(newChangeListener(){
@Override
publicvoidstateChanged(ChangeEvente){
JSlidersource=(JSlider)e.getSource()
if(!source.getValueIsAdjusting()){
floatnewRadius=source.getValue()
ball.radius=newRadius
//Repositiontheballsuchasitisinsidethebox
if(ball.xball.radius<box.minX){
ball.x=ball.radius+1
}elseif(ball.x+ball.radius>box.maxX){
ball.x=box.maxXball.radius1
}
if(ball.yball.radius<box.minY){
ball.y=ball.radius+1
}elseif(ball.y+ball.radius>box.maxY){
ball.y=box.maxYball.radius1
}
}
}
})
}
}
}
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
20/31
29/08/13
ModifythegameThreadofBallWorldclasstoprogramtosupportpause/resumeoperation:
publicclassBallWorld{
......
......
publicvoidgameStart(){
ThreadgameThread=newThread(){
publicvoidrun(){
while(true){
longbeginTimeMillis,timeTakenMillis,timeLeftMillis
beginTimeMillis=System.currentTimeMillis()
if(!paused){
//Executeonegamestep
gameUpdate()
//Refreshthedisplay
repaint()
}
......
......
}
Collision Detection
Collisionoccursifthedistancebetweenthetwoballsisequaltothesumoftheirradii.
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
21/31
29/08/13
Collision Response
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
22/31
29/08/13
Wefirstdissolvethevelocities(V1andV2)alongtheaxesofcollision,pandq(asillustrated).Wethenapplythelawsof
conservation of momentum and energy to compute the velocities after collision, along the axis of collision p. The
velocitiesperpendiculartotheaxisofcollisionqremainsunchanged.
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
23/31
29/08/13
Collision
/**
*Checkifthisballcollideswiththegivenanotherballintheinterval
*(0,timeLimit].
*/
publicvoidintersect(Ballanother,floattimeLimit){
//CallmovingPointIntersectsMovingPoint()withtimeLimit.
//UsethisResponseandanotherResponse,astheworkingcopies,tostorethe
//responsesofthisballandanotherball,respectively.
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
24/31
29/08/13
//Checkifthiscollisionistheearliestcollision,andupdatetheball's
//earliestCollisionResponseaccordingly.
CollisionPhysics.pointIntersectsMovingPoint(
this.x,this.y,this.speedX,this.speedY,this.radius,
another.x,another.y,another.speedX,another.speedY,another.radius,
timeLimit,thisResponse,anotherResponse)
if(anotherResponse.t<another.earliestCollisionResponse.t){
another.earliestCollisionResponse.copy(anotherResponse)
}
if(thisResponse.t<this.earliestCollisionResponse.t){
this.earliestCollisionResponse.copy(thisResponse)
}
}
}
BallWorld.java: The gameUpdate() method is modified to detect collision between any pair of balls and collision
betweentheballthethecontainerbox.Thecontrolsarealsomodified.Anewbuttonisaddedtolaunchnewballs.
publicclassBallWorldextendsJPanel{
......
......
//Balls
privatestaticfinalintMAX_BALLS=25//Maxnumberallowed
privateintcurrentNumBalls//Numbercurrentlyactive
privateBall[]balls=newBall[MAX_BALLS]
/**ConstructortocreatetheUIcomponentsandinitthegameobjects.*/
publicBallWorld(){
......
......
currentNumBalls=11
balls[0]=newBall(100,410,25,3,34,Color.YELLOW)
balls[1]=newBall(80,350,25,2,114,Color.YELLOW)
balls[2]=newBall(530,400,30,3,14,Color.GREEN)
balls[3]=newBall(400,400,30,3,14,Color.GREEN)
balls[4]=newBall(400,50,35,1,47,Color.PINK)
balls[5]=newBall(480,320,35,4,47,Color.PINK)
balls[6]=newBall(80,150,40,1,114,Color.ORANGE)
balls[7]=newBall(100,240,40,2,60,Color.ORANGE)
balls[8]=newBall(250,400,50,3,42,Color.BLUE)
balls[9]=newBall(200,80,70,6,84,Color.CYAN)
balls[10]=newBall(500,170,90,6,42,Color.MAGENTA)
//Therestoftheballs,thatcanbelaunchedusingthelaunchbutton
for(inti=currentNumBallsi<MAX_BALLS++i){
balls[i]=newBall(20,CANVAS_HEIGHT20,15,5,45,Color.RED)
}
......
}
/**Updatethegameobjects,detectcollisionandprovideresponse.*/
publicvoidgameUpdate(){
floattimeLeft=1.0f//Onetimesteptobeginwith
//Repeatuntiltheonetimestepisup
do{
//FindtheearliestcollisionuptotimeLeftamongallobjects
floattMin=timeLeft
//Checkcollisionbetweentwoballs
for(inti=0i<currentNumBalls++i){
for(intj=0j<currentNumBalls++j){
if(i<j){
balls[i].intersect(balls[j],tMin)
if(balls[i].earliestCollisionResponse.t<tMin){
tMin=balls[i].earliestCollisionResponse.t
}
}
}
}
//Checkcollisionbetweentheballsandthebox
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
25/31
29/08/13
for(inti=0i<currentNumBalls++i){
balls[i].intersect(box,tMin)
if(balls[i].earliestCollisionResponse.t<tMin){
tMin=balls[i].earliestCollisionResponse.t
}
}
//UpdatealltheballsuptothedetectedearliestcollisiontimetMin,
//ortimeLeftifthereisnocollision.
for(inti=0i<currentNumBalls++i){
balls[i].update(tMin)
}
timeLeft=tMin//Subtractthetimeconsumedandrepeat
}while(timeLeft>EPSILON_TIME)//Ignoreremainingtimelessthanthreshold
}
......
......
/**Thecontrolpanel(innerclass).*/
classControlPanelextendsJPanel{
/**ConstructortoinitializeUIcomponents*/
publicControlPanel(){
//Acheckboxtotogglepause/resumealltheballs'movement
JCheckBoxpauseControl=newJCheckBox()
this.add(newJLabel("Pause"))
this.add(pauseControl)
pauseControl.addItemListener(newItemListener(){
@Override
publicvoiditemStateChanged(ItemEvente){
paused=!paused//Togglepause/resumeflag
}
})
//Asliderforadjustingthespeedofalltheballsbyafactor
finalfloat[]ballSavedSpeedXs=newfloat[MAX_BALLS]
finalfloat[]ballSavedSpeedYs=newfloat[MAX_BALLS]
for(inti=0i<currentNumBalls++i){
ballSavedSpeedXs[i]=balls[i].speedX
ballSavedSpeedYs[i]=balls[i].speedY
}
intminFactor=5//percent
intmaxFactor=200//percent
JSliderspeedControl=newJSlider(JSlider.HORIZONTAL,minFactor,maxFactor,100)
this.add(newJLabel("Speed"))
this.add(speedControl)
speedControl.addChangeListener(newChangeListener(){
@Override
publicvoidstateChanged(ChangeEvente){
JSlidersource=(JSlider)e.getSource()
if(!source.getValueIsAdjusting()){
intpercentage=(int)source.getValue()
for(inti=0i<currentNumBalls++i){
balls[i].speedX=ballSavedSpeedXs[i]*percentage/100.0f
balls[i].speedY=ballSavedSpeedYs[i]*percentage/100.0f
}
}
}
})
//Abuttonforlaunchingtheremainingballs
finalJButtonlaunchControl=newJButton("LaunchNewBall")
this.add(launchControl)
launchControl.addActionListener(newActionListener(){
@Override
publicvoidactionPerformed(ActionEvente){
if(currentNumBalls<MAX_BALLS){
++currentNumBalls
if(currentNumBalls==MAX_BALLS){
//Disablethebutton,asthereisnomoreball
launchControl.setEnabled(false)
}
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
26/31
29/08/13
}
}
})
}
}
}
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
27/31
29/08/13
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
28/31
29/08/13
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
29/31
29/08/13
[PENDING]SomeExplanation?
A Pong Game
[PENDING]
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
30/31
29/08/13
[PENDING]
Latestversiontested:JDK1.6
Lastmodified:October,2010
Feedback,comments,corrections,anderratacanbesenttoChuaHockChuan([email protected])|HOME
www.ntu.edu.sg/home/ehchua/programming/java/J8a_GameIntro-BouncingBalls.html
31/31