www.CokeAndCode.com | Space Invaders 101 | 102 | 103 | 104 |
The result of this tutorial can be seen here. The complete source for the tutorial can be found here. Its intended that you read through this tutorial with the source code at your side. The tutorial isn't going to cover every line of code but should give you enough to fully understand how it works.
Context highlighted source is also available here:
Game.java
SpriteStore.java
Sprite.java
Entity.java
ShipEntity.java
ShotEntity.java
AlienEntity.java
SystemTimer.java
Disclaimer: This tutorial is provided as is. I don't guarantee that the provided source is perfect or that that it provides best practices.
Currently, Java has some issues with timing. In Java 1.4.2 the simplest way to get hold of the time is to use System.currentTimeMillis(). Infact it was used to time the movement in the first tutorial. However, on some Windows based machines the resolution of the timer (the smallest value you can time) isn't very useful. On most systems (Linux, SunOS, etc) the resolution is at least 1 millisecond. However, on some Windows sytems the resolution is bad enough to mean that timing can be relied on to give a consistant result, which in turn can lead to stuttering in updates. Lets look at some possible solutions.
So the algorithm looks something like this:
While this doesn't give perfect results, it does give "good" results. Its not required on anything other than Windows. So if you do choose to implement this method its nice to add a check on the system property "os.name".
Using the Java 1.5 timer couldn't be simpler:
long currentTime = System.nanoTime()
There you have it! As a side note, there is a high resolution timer "hidden" in Java 1.4.2, however this tutorial doesn't cover it. Why? There is no guarantee the hidden timer will be available in any given JVM and hence there is no point using it except in very specific circumstances.
Well, that sounds terrible doesn't it? We have to wait for Sun to update the JVM before we can access the latest and greatest platform dependant feature. Actually, no, this is exactly what the Java Native Interface (JNI) is designed to allow. The interface lets us use native libraries from Java to access features of the platform not yet exposed to the JVM. However, the downside is that unless you implement a native library for every platform your software now only works on a particular operating system. Its a trade off that should be taken pretty seriously when you're looking at writing Java software.
Whats even better, is that there are at least a few free implementations of this timing library already available, so we don't even need to touch the C++. A good implementation that a large number of people use is the GAGE Timer. Its freely available and has a good track record.
If you download the GAGE Timer package, you'll have a dynamic link library (DLL) for Windows and a Jar file containing the interface to the timer. To continue with this tutorial the DLL must be in the directory you are running from (only if you're on Windows of course), and the Jar file must be referenced in your classpath.
This timer class is based on the use of the GAGE timer. We need to support two main operations, getting the time and sleeping for a set period of time. The gage timer supports both of these operations, however it requires you specify the time in "timer ticks" not in milliseconds. The main job of this class is to map between the timer ticks provided from the native timer to milliseconds and back.package org.newdawn.spaceinvaders; import com.dnsalias.java.timer.AdvancedTimer; /** * A wrapper class that provides timing methods. This class * provides us with a central location where we can add * our current timing implementation. Initially, we're going to * rely on the GAGE timer. (@see http://java.dnsalias.com) * * @author Kevin Glass */ public class SystemTimer { /** Our link into the GAGE timer library */ private static AdvancedTimer timer = new AdvancedTimer(); /** The number of "timer ticks" per second */ private static long timerTicksPerSecond; /** A little initialisation at startup, we're just going to get the GAGE timer going */ static { timer.start(); timerTicksPerSecond = AdvancedTimer.getTicksPerSecond(); } /** * Get the high resolution time in milliseconds * * @return The high resolution time in milliseconds */ public static long getTime() { // we get the "timer ticks" from the high resolution timer // multiply by 1000 so our end result is in milliseconds // then divide by the number of ticks in a second giving // us a nice clear time in milliseconds return (timer.getClockTicks() * 1000) / timerTicksPerSecond; } /** * Sleep for a fixed number of milliseconds. * * @param duration The amount of time in milliseconds to sleep for */ public static void sleep(long duration) { timer.sleep((duration * timerTicksPerSecond) / 1000); } }
Simply put, we create an "AdvancedTimer", the timer given to us by GAGE. We find out its resolution and start it off running. The final step is to provide our methods based on using the timer.
Now, in the last tutorial since the timer wasn't designed to be perfect we didn't worry to much about a few lost milliseconds. This time we can rely on our timer to be millisecond accurate so we're going to try and strictly limit our frame time so we get exactly 100 frames per second (FPS).// work out how long its been since the last update, this // will be used to calculate how far the entities should // move this loop long delta = SystemTimer.getTime() - lastLoopTime; lastLoopTime = SystemTimer.getTime();
To do this we're going to want each cycle round the game loop to take exactly 10 milliseconds. We know at what time the cycle started (lastLoopTime) and we know what time it is now, so with a small amount of maths we can sleep for the right amount of time like this:
Note: GAGE Timer actually supports a "sleepUntil()" method that could be used here. However, since the SystemTimer is trying to allow us to change between timing mechanisms we should try to rely on simply sleeping for the right amount of time.// we want each frame to take 10 milliseconds, to do this // we've recorded when we started the frame. We add 10 milliseconds // to this and then factor in the current time to give // us our final value to wait for SystemTimer.sleep(lastLoopTime+10-SystemTimer.getTime());
Since we designed our source nicely last time our changes are limited to one class, AlienEntity. Instead of the entity maintaining just a single sprite we'll add a few sprites and flip between them over time, i.e. Animation. Our first step is to add some addition variables to our AlienEntity:
The frames array is going to hold our frames of animation. lastFrameChange is going to be a record of the last time we changed animation frame. frameDuration will be the length of time that each frame will be displayed on the screen. Making this small will make the aliens dance more quickly. Finally, frameNumber will be the index of the frame we are currently showing in a frames array. This will be incremented to cycle us through the animation./** The animation frames */ private Sprite[] frames = new Sprite[4]; /** The time since the last frame change took place */ private long lastFrameChange; /** The frame duration in milliseconds, i.e. how long any given frame of animation lasts */ private long frameDuration = 250; /** The current frame of animation being displayed */ private int frameNumber;
Next we're going to need to load up our sprites. We're going to modify the constructor to grab the frames. However, our standard Entity class will already load one sprite for us (the one it used to display). So we need to load two additional ones, like so:
We've asked the Entity class to load "sprites/alien.gif" for us. Then we need to go off and load up a couple of additional sprites. We put the frame loaded by Entity and our two additional frames in the array in the right place to play the animation. Note that we've modified the constructor slightly to remove the name of the sprite, so the Game class will need some minor modifications.public AlienEntity(Game game,int x,int y) { super("sprites/alien.gif",x,y); // setup the animatin frames frames[0] = sprite; frames[1] = SpriteStore.get().getSprite("sprites/alien2.gif"); frames[2] = sprite; frames[3] = SpriteStore.get().getSprite("sprites/alien3.gif"); this.game = game; dx = -moveSpeed; }
The final step in getting our animation to play is to update the current sprite as time progresses. We already have a handy place in which we can perform this action. Our "move()" method already gets told when time passes, so we can update the animation there.
So, as time passes our lastFrameChange counter will get updated. Once its passed our frameDuration limit we reset it. In addition we move to the next frame number. Then we reset the current sprite by setting the "sprite" member in the Entity super class to the current frame of animation. Next time the entity is rendered a different sprite is drawn and the animation takes place!public void move(long delta) { // since the move tells us how much time has passed // by we can use it to drive the animation, however // its the not the prettiest solution lastFrameChange += delta; // if we need to change the frame, update the frame number // and flip over the sprite in use if (lastFrameChange > frameDuration) { // reset our frame change time counter lastFrameChange = 0; // update the frame frameNumber++; if (frameNumber >= frames.length) { frameNumber = 0; } sprite = frames[frameNumber]; } ... }
If you have any comments or corrections feel free to mail me here