// Constructs the game level and enters the main game loop
public void doGame()
{
mainFrame.setBackground( new Color(240,240,255) );
BufferStrategy bufferStrategy = mainFrame.getBufferStrategy();
bounds = mainFrame.getBounds();
System.out.println("bounds = "+bounds);
treasureBag = new LinkedList();
level.initialize();
System.gc();
// Wait for a game to start
preGameScreen.renderLoop(bufferStrategy);
// Initialize new game params
lives = 5;
livesS = "5";
worldX = 0;
worldY = 50;
while( lives > 0 )
{
System.gc(); // ask for clean up where a GC pause is least likely to affect the game
gameOn = true;
// Main game loop
mainGameLoop.renderLoop( bufferStrategy );
// either no lives left or all levels completed
// This logic seems out of place here - refactor
// set a state variable so it is clear why the loop above terminated
// either game is won, or all lives lost.
if( level.gotAllTreasures() )// player completed level
{
// TODO: go to next level unless this is the last one
break; // last level break out of game loop
}
// Died...
System.out.println("Died at tick="+tick);
// player died
long deathTime = System.currentTimeMillis();
// death loop
while( true )
{
// TODO Death animation (oxymoron?)
if( System.currentTimeMillis() - deathTime > 3000 )
break;
Thread.yield();
}
// don't let FPS scaling spaz
fpsTick = tick;
tickTime = System.currentTimeMillis();
} // lives remaining loop
gameOverLoop.setGameOverMessage( lives > 0 ? "You Won!!! (Press Space)"
: "GAME OVER (Press SPACE)" );
// end of game idle screen
gameOverLoop.renderLoop(bufferStrategy);
}
private RenderLoop mainGameLoop = new RenderLoop()
{
public boolean render( Graphics2D g )
{
gameTick(); // do game logic
g.clearRect(0,0,bounds.width,bounds.height);
// remember original screen coordinate system
AffineTransform overlaytrans = g.getTransform();
// vector graphics look much better with antialiasing
g.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// move origin to center of screen
g.translate(bounds.width/2, bounds.height/2);
// and remember this coordinate system
AffineTransform oldtrans = g.getTransform();
drawLevel(g);
// TODO draw enemies, exits, decorations, etc.
// restore origin to center of screen
g.setTransform( oldtrans );
drawShip(g);
// restore original coordinate system
g.setTransform( overlaytrans );
drawOverlays(g); // Draw score, lives remaining, level etc...
return gameOn;
}
};
// Game Over loop
class GameOverLoop extends RenderLoop
{
public boolean render( Graphics2D g )
{
g.clearRect(0,0,bounds.width,bounds.height);
g.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// remember original screen coordinate system
AffineTransform overlaytrans = g.getTransform();
// move origin to center of screen
g.translate(bounds.width/2, bounds.height/2);
drawLevel(g);
// restore original coordinate system
g.setTransform( overlaytrans );
// Game Over Message
g.drawString(geMsg,300,200);
return !space;
}
public void setGameOverMessage( String msg )
{
geMsg = msg;
}
private String geMsg;
};
private GameOverLoop gameOverLoop = new GameOverLoop();
private RenderLoop preGameScreen = new RenderLoop()
{
// Waiting to start loop
public boolean render( Graphics2D g )
{
g.clearRect(0,0,bounds.width,bounds.height);
g.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// remember original screen coordinate system
AffineTransform overlaytrans = g.getTransform();
// move origin to center of screen
g.translate(bounds.width/2, bounds.height/2);
drawLevel( g );
// restore original coordinate system
g.setTransform( overlaytrans );
g.drawString("Press SPACE to Start",300,200);
return !space;
}
};
private void gameTick()
{
int oldScore = score;
controlShip();
moveShip();
// TODO Check if a line segment from old to new intersects with anything
// currently just checks new position
// Check for collisions
// check key (treasure) collisions. all objects that are picked up
// at this position will be placed in the treasureBag
treasureBag.clear();
if( level.collectTreasure( worldX, worldY, treasureBag) )
{
// Level completed
// TODO: open door to next level
// For now just stop ship
shipX = 0;
shipY = 0;
shipVelX = 0;
shipVelY = 0;
gameOn = false; // and end the game loop
}
// Update score display and handle bonuses
if( !treasureBag.isEmpty() )
{
for( Iterator it = treasureBag.iterator(); it.hasNext(); )
{
score += ((Integer)it.next()).intValue();
}
// TODO play sound for treasure pickup
// (could use magnitude of change in score to choose the sound)
}
if( oldScore != score )
{
// TODO check if score crossed special boundaries like earning a
// free life.
scoreS = String.valueOf(score);
}
// Check map collisions (ship crashes)
if( level.checkCollision( -worldX, -worldY ) )
{
msg = "ouch";
// TODO handle this properly
// for now reset position here (move to doGame after death anim)
worldX = 0;
worldY = 50;
shipX = 0;
shipY = 0;
shipVelX = 0;
shipVelY = 0;
shipRot = 0;
--lives;
gameOn = false;
livesS = String.valueOf( lives );
// TODO trigger death sound and death animation
// (should drop out of 'gameOn' loop for this)
}
else
{
msg = null;
}
// this should always be called for each render loop iteration
calcFpsAndGameClock();
keyRot = ctick & 15;
}
private void calcFpsAndGameClock()
{
// FPS indicator
long t = System.currentTimeMillis();
if( t-tickTime >= 1000 )
{
fps = ((tick-fpsTick)*1000.0/(t-tickTime));
fpsS = NumberFormat.getNumberInstance().format(fps)+" fps";
fpsTick = tick;
tickTime = t;
// adjust for frame rate - scale numbers relative to 30 fps
FAC = 1.0;
if( fps > 5 )
FAC = 30.0/fps;
}
// frame tick and frame rate corrected 'ctick' for anims
tick++;
ctick = (int)(tick * FAC);
}
/** apply controls to ship parameters */
private void controlShip()
{
// calculate ship heading
if( left )
{
shipRot--;
shipAngle = FAC * shipRot * 0.5 / Math.PI;
}
else if( right )
{
shipRot++;
shipAngle = FAC * shipRot * 0.5 / Math.PI;
}
// acceleration due to ships engine
if( thrust )
{
shipVelX += FAC * ACCEL * Math.sin(shipAngle);
shipVelY -= FAC * ACCEL * Math.cos(shipAngle);
}
}
/** move ship - scaled by FAC */
private void moveShip()
{
// acceleration due to gravity
shipVelY += FAC * GRAVITY;
// put a cap on the speed (vertical only, proper cap needs vector math)
double terminalV = FAC * TERMINAL_VELOCITY;
if( shipVelY > terminalV )
shipVelY = terminalV;
else if( shipVelY < -terminalV )
shipVelY = -terminalV;
// translation of ship for this iteration (floating point)
shipX += FAC * shipVelX;
shipY += FAC * shipVelY;
// do movement (if we have moved at least an entire pixel)
int dx = (int) shipX;
int dy = (int) shipY;
if( dx != 0)
{
worldX -= dx; // adjust world position
shipX -= dx; // (allows fractional part to accumulate until it makes a difference)
}
if( dy != 0)
{
worldY -= dy;
shipY -= dy;
}
}
private void drawLevel(Graphics2D g)
{
level.renderBackground( g, worldX, worldY, ctick);
}
private void drawShip(Graphics2D g)
{
//(center of screen or maybe near edge if level can't scroll any further)
// rotate ship
g.rotate( shipAngle );
g.fill( shipShape );
if( thrust )
{
g.setColor( Color.ORANGE );
g.fill( thrustShape );
g.setColor( Color.BLACK );
}
}
private void drawOverlays( Graphics2D g )
{
// general message
if( msg != null )
{
g.drawString(msg,10,44);
}
// fps indicator
g.drawString(fpsS,10,34);
// Score and other stats
g.drawString("Score:",460,34);
g.drawString(scoreS, 500, 34);
g.drawString("Lives:",360,34);
g.drawString(livesS, 400, 34);
// position (mainly a debug thing)
g.drawString(String.valueOf(worldX),500,44);
g.drawString(String.valueOf(worldY),590,44);
}
private void computeRotatedKeyShapes()
{
// TODO pre-render the transformed key shapes
for(int i = 0; i < 16; i++)
rotKeys[i] = AffineTransform.getRotateInstance( i * RF )
.createTransformedShape( keyShape );
}
private static DisplayMode getBestDisplayMode(GraphicsDevice device)
{
for (int x = 0; x < BEST_DISPLAY_MODES.length; x++)
{
DisplayMode[] modes = device.getDisplayModes();
for (int i = 0; i < modes.length; i++)
{
if (modes[i].getWidth() == BEST_DISPLAY_MODES[x].getWidth()
&& modes[i].getHeight() == BEST_DISPLAY_MODES[x].getHeight()
&& modes[i].getBitDepth()
== BEST_DISPLAY_MODES[x].getBitDepth())
{
return BEST_DISPLAY_MODES[x];
}
}
}
return null;
}
public static void chooseBestDisplayMode(GraphicsDevice device)
{
DisplayMode best = getBestDisplayMode(device);
if (best != null)
{
device.setDisplayMode(best);
}
}
public static void main(String[] args) throws Exception
{
GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice device = env.getDefaultScreenDevice();
try
{
boolean fs = true;
int numBuffers = 2;
for(int i = 0; i < args.length; i++ )
{
if( args[i].equalsIgnoreCase("window") )
fs = false;
}
VectorCollector game = new VectorCollector(numBuffers, device, fs);
game.doGame();
}
finally
{
device.setFullScreenWindow(null);
}
System.exit(0);
}
public void keyTyped(KeyEvent e)
{
}
public void keyPressed(KeyEvent e)
{
switch( e.getKeyCode() )
{
case KeyEvent.VK_UP:
thrust = true;
break;
case KeyEvent.VK_LEFT:
left = true;
break;
case KeyEvent.VK_RIGHT:
right = true;
break;
case KeyEvent.VK_SPACE:
worldX = 0;
worldY = 50;
shipX = 0;
shipY = 0;
shipVelX = 0;
shipVelY = 0;
space = true;
break;
case KeyEvent.VK_ESCAPE:
lives = 0;
gameOn = false;
}
}
public void keyReleased(KeyEvent e)
{
switch( e.getKeyCode() )
{
case KeyEvent.VK_UP:
thrust = false;
break;
case KeyEvent.VK_LEFT:
left = false;
break;
case KeyEvent.VK_RIGHT:
right = false;
break;
case KeyEvent.VK_SPACE:
space = false;
break;
}
}
}
interface VCLevel
{
// called after loading a level
public void initialize();
// draw the level to 'g' with world position x,y at the origin
public void renderBackground( Graphics2D g, int x, int y, int ctick );
public boolean checkCollision( int x, int y );
public boolean collectTreasure( int x, int y, Collection bag);
public boolean gotAllTreasures();
}
class DefaultLevel implements VCLevel
{
public void initialize()
{
assert px.length == py.length;
final int zoom = 6;
// useless variety - jiggle level
for (int i = 0; i < px.length; i++)
{
int x = px[i];
int y = py[i];
x += Math.random() * 10;
y += Math.random() * 10;
x *= zoom; // scale level
y *= zoom;
px[i] = x;
py[i] = y;
}
mapShape = new Polygon( px, py, px.length );
levelKeys = new HashSet();
for (int i = 0; i < keyX.length; i++)
{
Point p = new Point(keyX[i]*zoom, keyY[i]*zoom);
levelKeys.add( p );
}
}
public void renderBackground(Graphics2D g, int x, int y, int ctick)
{
// adjust for position within level
g.translate( x, y );
g.setColor(COLORS[ctick % COLORS.length]);
g.fill( mapShape );
g.setColor( Color.BLACK );
g.draw( mapShape );
// TODO pre-render the transformed key shapes
Shape rotKey = VectorCollector.rotKeys[VectorCollector.keyRot];
for (Iterator i = levelKeys.iterator(); i.hasNext(); )
{
Point p = (Point) i.next();
if( onScreen( g, p.x, p.y) )
{
g.translate( p.x, p.y );
g.draw( rotKey );
g.translate( -p.x, -p.y );
}
}
g.translate( -x, -y);
}
public boolean gotAllTreasures()
{
return levelKeys.isEmpty();
}
public boolean checkCollision(int x, int y)
{
return mapShape.contains(x,y);
}
/** Handles treasure pickup
* returns true if all treasures have been collected */
public boolean collectTreasure( int x, int y, Collection bag)
{
for (Iterator i = levelKeys.iterator(); i.hasNext();)
{
Point p = (Point) i.next();
// TODO: key array positions should be sorted so
// we can jump out of this loop early
int dx = (p.x + x);
if( dx > 8 || dx < -8)
continue; // out of range on x - check next key
// x in range, check Y
int dy = (p.y + y);
if( dy > 8 || dy < -8)
continue; // out of range on y - check next key
// y also in range - good enough for me - count the hit
i.remove(); // take the key out of the map for the next render pass
bag.add( tenPoints );// and give out some points
// TODO: place a keyCapture animation at point p to play for a few milliseconds
// before decaying away.
// TODO: change this entirely to keep track of where the ship would be inserted
// into the array.. then it is constant time.. to check collision against next
// or previous key . (and also update ships virtual position in the array)
}
return levelKeys.isEmpty();
}
private static Integer tenPoints = new Integer(10);
private boolean onScreen( Graphics2D g, int x, int y )
{
Rectangle clip = g.getClipBounds();
return clip != null ? clip.contains(x,y) : true;
}
// need colors for an 'eerie glow'
private static Color[] COLORS = new Color[16];
static
{
for(int i = 0; i < COLORS.length/2; i++)
{
COLORS[i] = new Color(0,180+i*6,i*7);
COLORS[COLORS.length-1-i] = new Color(0,180+i*6,i*7);
}
}
// level polygon X
private int [] px = new int [] { -80,-80,-40,-40, 80, 80, 70,70,60, 60, 50, 50, 40, 40, 30,30,-40,-40};
// level polygon Y
private int [] py = new int [] { -20, 20, 20, 10, 10,-20,-20,00,00,-20,-20,-10,-10,-20,-20,00, 00,-20};
// treasure placement X
private int [] keyX = new int [] { 45,-80, 0, 65 };
// treasure placement Y
private int [] keyY = new int [] { -15,-28, 15, -5 };
private transient Shape mapShape;
private transient Set levelKeys;
}
There you go. As you can see it is extremely simple with many areas to improve. That is sort of the point. I wanted a simple functional game that I could show evolving through a series of tutorials that make it better and better and maybe add a few key features.
Note that the simple level that is hardcoded has no boundary and gravity will pull you to negative infinity and it can be hard to find your way back…