Thursday, September 30, 2010

Space Invaders Class Diagram, Iteration 1

I am beginning the Space Invaders project the same way I begin every project -- with a rough class diagram to start getting some ideas down. It needs a lot of refinement, but it's a start.  At this point, I foresee uses of the Factory, Facade, Observer, and possibly the Template Method pattern, with probably more to come.

A first attempt at a class diagram for the Space Invaders project.

Monday, September 27, 2010

PA2...I think.

A first (and maybe final) draft of PA2.
I'm uncertain of the precise instructions for PA2.  From what I remember from last week's lecture, we were to have three sprites, each of different shapes.  One was to move horizontally, one vertically, and one diagonally.  All three were to bounce off the screen borders.

So, my implementation so far has a blue rectangle that bounces horizontally, a red rounded rectangle that bounces vertically, and a green oval that bounces diagonally.

Also, I wasn't sure if we were to create the entire program from scratch or if it was OK to take advantage of some reusable code from the past.  So to save some time, I used my own framework to handle the UI and video mode stuff.  I did, however, create the sprites from scratch since that was the focus of the assignment.

Friday, September 24, 2010

My Game Loop

In my last post, I said I would follow up with a listing of my game loop showing the use of the new metrics calculation class.  Here it is!


Note:  The metrics calculator object is called "timer" in this code.

// Loop until the program is over
while (!programOver)
{
    // Record the starting time of the loop
    timer.recordStartTime();

    // Update the screen data
    screenManager.advance();

    // Draw the updated screen
    screenManager.render(MHDisplayModeChooser.getGraphics2D());
            
    // Record the ending time of the loop
    timer.recordEndTime();

    // Calculate how long it took to run this loop and
    // use that value to see how long we should wait
    // (or "sleep") before starting the next iteration
    timer.sleep();

    // Separate UPS from FPS to maintain a  better frame rate.
    while (timer.shouldUpdate())
        screenManager.advance();
} // while (!programOver) . . .

A Slightly Improved FPS/UPS/Sleep Time Calculator

Irritated by the monolithic structure provided by the book's examples, I decided to separate out the metrics calculations into a separate class. I'm not sure if this is the best possible solution, but it does provide certain advantages over the author's examples.  To name a few:

  • The metrics calculation logic is separated out from the game logic, greatly improving the game class' cohesion.
  • This new metrics class is directly reusable and stands on its own integrity.
  • The metrics calculations are all encapsulated neatly into private methods, eliminating the need for a programmer to understand them and reducing the odds of making an error in reproducing this functionality.
  • Several variables in the original code were assigned but then never referenced.  This code eliminates them.
  • This code is documented with comments.  Not completely or perfectly, but much better than the textbook examples.

I will follow up in a separate post to show what my game loop using this class looks like.

public class MHRuntimeMetrics
{
    // Constants to make the math easier and less error-prone.
    private static final long ONE_SECOND_IN_MILLI = 1000L; 
    private static final long ONE_MILLI_IN_NANO = 1000000L; 
    private static final long ONE_SECOND_IN_NANO  = ONE_SECOND_IN_MILLI * ONE_MILLI_IN_NANO;
    
    /** The frame rate we're trying to achieve. */
    public static final short TARGET_FPS = 50; // 28 = average FPS for Japanese anime
    
    /** Average period in nanoseconds required to achieve the target frame rate. */
    public static final long PERIOD = ONE_SECOND_IN_NANO/TARGET_FPS;

    /** Maximum number of renders to skip when compensating for long loop iterations. */
    static final short MAX_FRAME_SKIPS = 2;
    
    /** Nanoseconds between metrics calculations. */
    private static final long MAX_STATS_INTERVAL = ONE_SECOND_IN_NANO;
    
    /** Number of measurements to track for calculating an average. */
    private static final int SAMPLE_SIZE = 10;

    private int frameCount = 0;        // Number of frames elapsed.
    private int statsInterval;         // Time since last metrics calculation.
    private long gameStartTime;        // Approximate time that game started.
    private long prevStatsTime = 0;    // Time that metrics were last calculated.
    private long totalElapsedTime = 0; // Total time elapsed in game loop.
    private int totalFramesSkipped;    // Total renders skipped due to long loop iterations.
    private int framesSkipped = 0;     // No. renders skipped since last calculation.
    private int statsCount = 0;        // Index into fpsStore and upsStore arrays.
    private double averageFPS;         // Calculated average frames per second.
    private double averageUPS;         // Calculated average updates per second.
    private int[] fpsStore, upsStore;  // Arrays for storing calculation results.
    private long startTime, endTime;   // Start and end times for current loop iteration.
    private long excess = 0;           // Extra time acquired from short loop iterations.
    

    /****************************************************************
     * Default constructor.
     */
    public MHRuntimeMetrics()
    {
        gameStartTime = System.nanoTime();
        prevStatsTime = gameStartTime;
        
        fpsStore = new int[SAMPLE_SIZE];
        upsStore = new int[SAMPLE_SIZE];
    }
    
       
    /****************************************************************
     */
    public void recordStartTime()
    {
        startTime = System.nanoTime();
    }

    
    /****************************************************************
     */
    public void recordEndTime()
    {
        endTime = System.nanoTime();
        storeStats();  // Update the stored metrics.
        Toolkit.getDefaultToolkit().sync(); // Sync the display (for Linux users).
    }
    
        
    /****************************************************************
     */
    private int nanoToSec(long nano)
    {
        return (int)(nano / ONE_SECOND_IN_NANO);
    }
    
    
    /****************************************************************
     */
    private long nanoToMilli(long nano)
    {
        return nano / ONE_MILLI_IN_NANO;
    }
    
    /****************************************************************
     */
    private void storeStats()
    { 
      frameCount++;
      statsInterval += PERIOD;

      if (statsInterval >= MAX_STATS_INTERVAL) 
      {
        long timeNow = System.nanoTime();

        long realElapsedTime = timeNow - prevStatsTime;   // time since last stats collection
        totalElapsedTime += realElapsedTime;

        totalFramesSkipped += framesSkipped;
        
        int actualFPS = 0;     // calculate the latest FPS and UPS
        int actualUPS = 0;
        if (totalElapsedTime > 0) 
        {
          actualFPS = (frameCount / nanoToSec(totalElapsedTime));
          actualUPS = ((frameCount + totalFramesSkipped) / nanoToSec(totalElapsedTime));
        }

        // store the latest FPS and UPS
        fpsStore[ statsCount%SAMPLE_SIZE ] = actualFPS;
        upsStore[ statsCount%SAMPLE_SIZE ] = actualUPS;
        statsCount += 1;

        double totalFPS = 0.0;     // total the stored FPSs and UPSs
        double totalUPS = 0.0;
        for (int i=0; i < SAMPLE_SIZE; i++) 
        {
          totalFPS += fpsStore[i];
          totalUPS += upsStore[i];
        }

        if (statsCount < SAMPLE_SIZE) // obtain the average FPS and UPS
        { 
          averageFPS = totalFPS/statsCount;
          averageUPS = totalUPS/statsCount;
        }
        else 
        {
          averageFPS = totalFPS/SAMPLE_SIZE;
          averageUPS = totalUPS/SAMPLE_SIZE;
        }

        framesSkipped = 0;
        prevStatsTime = timeNow;
        statsInterval = 0;   // reset
      }
    }  // end of storeStats()

    
    /****************************************************************
     * Calculate how long the application thread should sleep based on the time
     * it took to run the game loop.
     */
    public void sleep()
    {
        long sleepTime = PERIOD - (endTime - startTime);

        if (sleepTime > 0)
        {
            try
            {
                Thread.sleep(nanoToMilli(sleepTime));
            } 
            catch (final InterruptedException e)
            {
            }
        } 
        else
        {
            excess -= sleepTime; // store excess time value
            Thread.yield(); // give another thread a chance to run
        }
    }
       
    
    /****************************************************************
     */
    public int getFramesPerSecond()
    {
        return (int) averageFPS;
    }


    /****************************************************************
     */
    public int getUpdatesPerSecond()
    {
        return (int) averageUPS;
    }

    
    /****************************************************************
     */
    public int getTimeSpentInGame()
    {
        return nanoToSec(System.nanoTime() - gameStartTime);
    }


    /****************************************************************
     */
    public boolean shouldUpdate()
    {
        boolean updateNeeded = (excess > PERIOD) && (framesSkipped < MAX_FRAME_SKIPS);
        
        if (updateNeeded)
        {
            excess -= MHRuntimeMetrics.PERIOD;
            framesSkipped++;
        }
        
        return updateNeeded;
    }

Tuesday, September 21, 2010

I got Worms, but do I GET Worms?

Much better than my previous score of -100...I think.
The Worms game is a little fun while a little annoying.  Likewise, the code for the game is also a little fun while a little annoying.  The gameplay is pretty original, and the logic behind the workings of the worm itself is pretty clever.  However, the code itself is full of issues.

The first major problem that I noticed is the utter lack of cohesion in the WormChase class.  The UI, the game logic, the metrics, and just about everything (other than the worm and the obstacles) is all crammed together into one poorly planned class.  Admittedly, I violate the single responsibility principle myself from time to time.  We probably all do.  But for a program that is intended to serve as a clear example, this one leaves a bit to be desired.

The only complaint I have about the Worm class is the way it handles the directions of movement, the Point2D instantiations, and the probabilities for changing direction.  As I already explained in a message board post, every part that I just mentioned could be encapsulated into an enumeration, which would help greatly to simplify the code.

The Obstacles class, in my humble opinion, is well done.  Everything is synchronized to make it safe for use between the game thread and the event dispatch thread, it encapsulates its functionality neatly, and completely fulfills its well-defined purpose.

I do feel a little disappointed by the FPS/UPS example.  I was hoping for a nice, clean, easy-to-read implementation of UPS, but instead we have an unstructured, poorly documented tangle of variables thrown loosely about this monolithic WormChase class.  I've seen plenty of clearer examples of FPS calculations than the one presented here, so I suppose I shouldn't be surprised by the complexity and confusion in this particular UPS example.  Nevertheless, I am going to try to extract the essentials and implement them into my own code to get the experience of doing so.  If I succeed, I'll post some code snippets in a future blog entry.

Sunday, September 12, 2010

Set up and ready to go!

The first week's homework is now finished and out of the way.  If I've done everything correctly, this means that my environment is ready to roll...and so am I.

For the most part, everything went pretty smoothly.  I already had up-to-date versions of the JDK and Eclipse, of course.  I already had the KGPJ sample code as well.  (I've loved this book for awhile and have referred to it several times.)  Downloading and installing Java 3D was no problem, and the Perforce installation was fast and smooth as well.

I couldn't figure out where to get HelloUniverse, so I Googled it, found a couple of different versions, and took the most recent one.  Compiling it caused a few problems with Java 3D features being unavailable due to restrictions, but that was fixed by telling Eclipse to ignore those issues.  Then the code compiled and ran just fine.

The only other issue I had was with Perforce "forgetting" that I have a workspace associated with my connection.  The issue hasn't happened since the second time I fixed it, so maybe this time it will stay fixed.  If it doesn't, at least I know what to do now.

Saturday, September 11, 2010

Getting Started in SE456

This blog will be devoted to my work in SE456:  Architecture of Computer Games.  Software architecture, especially when applied to game development, has always been one of my very favorite topics, and I am eagerly looking forward to getting into some serious game-making action.  What's even more exciting is that the class uses Java, which is without a doubt the very best game development technology yet devised.  (I will fight to the death defending my reasons for using it to develop my MHFramework API.)

My only concern is about the amount of time I can devote to the class.  My work load is especially heavy right now, and won't lighten up until this class is nearly over.  Nevertheless, I am excited and eager to work on some great new games, and will do my best to give them all the time and attention I possibly can.

This is gonna be fun!