Creating a Game Framework

The aim of this section is to create a screen management system for your games, so you can create distinct screens within your game that have their own logic and rendering code, as well as methods that will perform the necessary loading and unloading of the screens.

The game framework is a cocktail of three different examples from the book, with some extra bits here and there to make them all work nicely together. We use the rendering techniques from the FullScreenDemo example in Chapter 9, the input system from the EventAndFocusHandling example in Chapter 10, and finally the Sound Manager from Chapter 11.

Let's start with looking at how we are going to define a screen within our game framework. Here is the complete source listing for our TemplateScreen class.

Code Listing 12-6: TemplateScreen.java

start example
import java.awt.*; import java.awt.event.*;     public abstract class TemplateScreen {     public abstract void render(Graphics g);       public void process()     {       }       public void handleEvent(AWTEvent e)     {       }       public void load()     {       }       public void unload()     {       }         public Rectangle bounds = new Rectangle(0, 0 ,         Globals.DISPLAY_WIDTH, Globals.DISPLAY_HEIGHT); }
end example

As you can see, our TemplateScreen class is abstract, so to use it we need to extend it and implement the render method, which all the screens in the framework must implement.

Along with the render method, we also have the option of overriding four other methods in the TemplateScreen: the handleEvent method that will be passed events from the event processor when the screen is active, the load method that will be called once each time the screen is loaded (i.e., made the current screen), the unload method that will be called once just before another screen is loaded to replace this one as the current screen, and finally the process method that will handle any game logic relevant to this screen.

Note also that we store the bounds of the screen in a Rectangle object. Notice also that we access two static members called DISPLAY_WIDTH and DISPLAY_HEIGHT from a class called Globals. Let's look at this class now.

Code Listing 12-7: Globals.java

start example
public class Globals {     public static GameFramework framework;       public static Keyboard keyboard;     public static Mouse mouse;       public static TemplateScreen currentScreen;     public static TemplateScreen previousScreen;       public static SampleScreen sampleScreen;       public static int DISPLAY_WIDTH = 800;     public static int DISPLAY_HEIGHT = 600;     public static String WINDOW_TITLE = "Game Framework";       public static SoundManager soundManager; }
end example

In the Globals class, we have a reference to our main GameFramework object, which we have called framework (don't worry, we will look at the main GameFramework class soon). Then we have references to a Keyboard object and a Mouse object, which will be used to access the keyboard and mouse states from within the screens. Then we have two references to our abstract TemplateScreen class that we just saw. The first one, currentScreen, contains a reference to the current screen that should be handling the logic for the game and the rendering to the screen. In addition, we have the previousScreen reference, which we will use when we look at handling focus lost and gained in the framework.

Next, we have a SampleScreen object, which is going to be a class that extends the TemplateScreen class and will be a sample screen for the game framework, as you need at least one screen to see it working.

Then we define the width and height of the application as the integer variables DISPLAY_WIDTH and DISPLAY_HEIGHT. Remember here that if you wish to use the full-screen mode as well as windowed mode, you need to define the width and height of the screen as a valid screen resolution (such as 640x480, 800x600, 1024x768, etc.). After this we then have a String object called WINDOW_TITLE, which is used to define the title for our application (although this is only relevant in windowed mode).

Finally, we have a reference in here to a SoundManager object, so we can use the sound manager through the scope of the game framework (i.e., in all the screens we define).

Let's now have a quick look at the SampleScreen class (screen) that we have created by extending our TemplateScreen class. Here is the complete code listing:

Code Listing 12-8: SampleScreen.java

start example
import java.awt.*; import java.awt.event.*;   public class SampleScreen extends TemplateScreen {     public SampleScreen()     {         // setup the screen here...     }       public void process()     {         // place screen logic code here...     }       public void render(Graphics g)     {         // rendering code goes here...         g.setColor(Color.white);         g.fillRect(0, 0, bounds.width, bounds.height);           g.setColor(Color.black);         g.drawString("Sample Screen", 10, 15);     }       public void handleEvent(AWTEvent e)     {         // handle events here...     }       public void load()     {         // put screen loading code here...      }       public void unload()     {         // put screen unloading code here...      } }
end example

In the constructor of our screen classes, we can put all the initialization code that is performed once at the initial loading of the game. Then, for things that should happen when the screen is set as the current screen, we can use the load method (which if you remember is inherited from the TemplateScreen class), just as we use the unload method if the screen used to be the current screen and now it is not.

Other than that, we have implemented the render method, which if you remember is declared as abstract in the TemplateScreen class. In it we have simply cleared the screen to white and displayed the text "Sample Screen" at the top-left corner of the screen.

Next, we have created a Keyboard class to handle the states of the keys within the framework. The complete Keyboard class can be seen here:

Code Listing 12-9: Keyboard.java

start example
public class Keyboard {     public Keyboard()     {         keyState = new boolean[256];     }       public void resetAllStates()     {         for(int i=0; i<keyState.length; i++)             keyState[i] = false;     }         public boolean keyState[];  }
end example

As you can see, this class is very simple. All it has is a Boolean array of size 256, which will be used to store the state of the keys on the keyboard, as we have seen before in Chapter 10. Note we have also created a method here that will reset the state of all the keys called resetAllStates, which we will use when we handle the loss of focus within the framework.

As well as the Keyboard class, we have also declared a Mouse class to handle the current position and states of the mouse buttons:

Code Listing 12-10: Mouse.java

start example
public class Mouse {     public Mouse()     {         button = new boolean[3];     }       public void resetAllStates()     {         for(int i=0; i<button.length; i++)             button[i] = false;     }         public int x, y;     public boolean button[];       public static final int LEFT_BUTTON = 0;     public static final int RIGHT_BUTTON = 1;     public static final int MIDDLE_BUTTON = 2; }
end example

Note in this class how we have also defined three static final integer values, which represent the three buttons on the mouse and can be used as indices for the button array, which will contain the states of the three mouse buttons (i.e., whether they are up or down).

Now that we have seen all the new supporting classes for this example, you will also need to grab the EventProcessor.java and EventProcessable.java source files from Chapter 10 and also the SoundManager.java source file from Chapter 11.

Let's now look at our main class GameFramework.java, which is the core of our framework. The complete source listing for it can be seen here:

Code Listing 12-11: GameFramework.java

start example
import java.awt.*; import java.awt.image.*; import java.awt.event.*; import javax.swing.*;     public class GameFramework extends JFrame implements Runnable,                                                  KeyListener,                                                 MouseListener,                                                 MouseMotionListener,                                                 FocusListener,                                                 EventProcessable {     public GameFramework(GraphicsDevice graphicsDevice)     {         super(graphicsDevice.getDefaultConfiguration());         this.graphicsDevice = graphicsDevice;           getContentPane().setLayout(null);          setIgnoreRepaint(true);         setResizable(false);           addWindowListener(new WindowAdapter()                               {                              public void windowClosing(WindowEvent e)                              {                                                          exitProgram();                              }                              });           addKeyListener(this);         getContentPane().addMouseListener(this);         getContentPane().addMouseMotionListener(this);         addFocusListener(this);           eventProcessor = new EventProcessor(this);           Globals.framework = this;           // set up the sound manager...         Globals.soundManager = new SoundManager();           // set up mouse and keyboard         Globals.keyboard = new Keyboard();         Globals.mouse = new Mouse();     }             public void setMode(int mode)     {         if(mode==FULLSCREEN_MODE)             if(!graphicsDevice.isFullScreenSupported())             {                 mode = WINDOWED_MODE;                 System.out.println("Sorry, fullscreen mode not                     supported, continuing in windowed mode");             }             this.mode = mode;             try         {              if(mode==FULLSCREEN_MODE)             {                 setUndecorated(true);                 graphicsDevice.setFullScreenWindow(this);                     if(graphicsDevice.isDisplayChangeSupported())                 {                     DisplayMode dm = new DisplayMode(Globals                         .DISPLAY_WIDTH, Globals.DISPLAY_HEIGHT, 16,                         DisplayMode.REFRESH_RATE_UNKNOWN);                     if(isDisplayModeAvailable(dm))                         graphicsDevice.setDisplayMode(dm);                         else                     {                         System.out.println("Display mode not                             available: "+                                             dm.getWidth()+":"+                                             dm.getHeight()+":"+                                             dm.getBitDepth());                             System.exit(0);                     }                 }                 else                 {                     System.out.println("Display change not                         supported");                     System.exit(0);                 }             }             else // WINDOWED_MODE             {                 setTitle("Windowed Mode");                                  setVisible(true);                   Insets insets = getInsets();                 DISPLAY_X = insets.left;                 DISPLAY_Y = insets.top;                 resizeToInternalSize(Globals.DISPLAY_WIDTH,                     Globals.DISPLAY_HEIGHT);             }               createBufferStrategy(3);             strategy = getBufferStrategy();         }         catch(Exception e)         {             graphicsDevice.setFullScreenWindow(null);             e.printStackTrace();         }           if(!strategy.getCapabilities().isPageFlipping())             System.out.println("Page flipping is not available in                 this mode");           waitForReadyStrategy();     }         public void resizeToInternalSize(int internalWidth, int         internalHeight)     {         Insets insets = getInsets();         final int newWidth = internalWidth + insets.left +             insets.right;         final int newHeight = internalHeight + insets.top +             insets.bottom;             Runnable resize = new Runnable()         {             public void run()             {                 setSize(newWidth, newHeight);             }         };           if(!SwingUtilities.isEventDispatchThread())         {             try             {                 SwingUtilities.invokeAndWait(resize);             }             catch(Exception e) {}         }         else             resize.run();           validate();     }       public boolean isDisplayModeAvailable(DisplayMode dm)     {         DisplayMode[] availableModes = graphicsDevice             .getDisplayModes();           for(int i=0; i<availableModes.length; i++)         {             if(dm.getWidth()==availableModes[i].getWidth() &&                 dm.getHeight()==availableModes[i].getHeight() &&                 dm.getBitDepth()==availableModes[i].getBitDepth())                 return true;         }           return false;     }         public void waitForReadyStrategy()     {         int iterations = 0;           while(true)         {             try             {                 Thread.sleep(20);             }             catch(InterruptedException e) {}                 try             {                 strategy.getDrawGraphics();                 break;             }             catch(IllegalStateException e)             {                 System.out.println("BufferStrategy not ready yet");             }                 iterations++;             if(iterations == 100)             {                 // (Unlikely event) No use after 2 seconds (100*20ms                 //  = 2secs) give up trying                 System.out.println("Exiting Program, unable to use                     BufferStrategy");                 System.exit(0);             }         }     }         public void start()     {         loop = new Thread(this);         loop.start();     }             public void run()     {         long startTime, waitTime, elapsedTime;         // 1000/25 Frames Per Second = 40 millisecond delay         int delayTime = 1000/25;           Thread thisThread = Thread.currentThread();         while(loop==thisThread)         {             startTime = System.currentTimeMillis();               eventProcessor.processEventList();                 Globals.currentScreen.process();               Graphics g = strategy.getDrawGraphics();               if(!strategy.contentsLost())             {                 g.translate(DISPLAY_X, DISPLAY_Y);                   Globals.currentScreen.render(g);                   g.dispose();                 strategy.show();             }               //  handle frame rate             elapsedTime = System.currentTimeMillis() - startTime;             waitTime = Math.max(delayTime - elapsedTime, 5);               try             {                  Thread.sleep(waitTime);              }             catch(InterruptedException e) {}         }           System.out.println("Program Exited");           dispose();         System.exit(0);     }         public void exitProgram()     {         loop = null;     }         public void initGame()     {         // create your screens...         Globals.sampleScreen = new SampleScreen();           // load any sounds into the sound manager...             // set the current (starting) screen...         Globals.currentScreen = Globals.previousScreen =             Globals.sampleScreen;         Globals.currentScreen.load();     }         public boolean handleGlobalEvent(AWTEvent e)     {         // handle global events...         switch(e.getID())         {             case KeyEvent.KEY_PRESSED:                 KeyEvent keyEvent = (KeyEvent) e;                 Globals.keyboard.keyState[keyEvent.getKeyCode()]                      = true;                   switch(keyEvent.getKeyCode())                 {                     case KeyEvent.VK_ESCAPE:                         exitProgram();                         return true;                 }                 break;               case KeyEvent.KEY_RELEASED:                 Globals.keyboard.keyState[((KeyEvent)e).getKeyCode()]                     = false;                 break;               case MouseEvent.MOUSE_MOVED:             case MouseEvent.MOUSE_DRAGGED:             {                 MouseEvent mouseEvent = (MouseEvent) e;                 Globals.mouse.x = mouseEvent.getX();                 Globals.mouse.y = mouseEvent.getY();                 break;             }               case MouseEvent.MOUSE_PRESSED:                 switch(((MouseEvent)e).getButton())                 {                     case MouseEvent.BUTTON1:                         Globals.mouse.button[Mouse.LEFT_BUTTON]                              = true;                         break;                     case MouseEvent.BUTTON2:                         Globals.mouse.button[Mouse.MIDDLE_BUTTON]                              = true;                         break;                     case MouseEvent.BUTTON3:                         Globals.mouse.button[Mouse.RIGHT_BUTTON]                              = true;                         break;                 }                 break;               case MouseEvent.MOUSE_RELEASED:                 switch(((MouseEvent)e).getButton())                 {                     case MouseEvent.BUTTON1:                         Globals.mouse.button[Mouse.LEFT_BUTTON]                              = false;                         break;                     case MouseEvent.BUTTON2:                         Globals.mouse.button[Mouse.MIDDLE_BUTTON]                              = false;                         break;                     case MouseEvent.BUTTON3:                         Globals.mouse.button[Mouse.RIGHT_BUTTON]                              = false;                         break;                 }                 break;               case FocusEvent.FOCUS_LOST:                 // reset key states...                 Globals.keyboard.resetAllStates();                   // reset mouse button states...                 Globals.mouse.resetAllStates();                 break;               case FocusEvent.FOCUS_GAINED:                 break;         }           return false;     }         public void handleEvent(AWTEvent e)     {         if(!handleGlobalEvent(e))             Globals.currentScreen.handleEvent(e);     }         public void setCurrentScreen(TemplateScreen screen)     {         // unload the current screen...         Globals.currentScreen.unload();             // set this screen to the previous screen...         Globals.previousScreen = Globals.currentScreen;           // assign the new screen...         Globals.currentScreen = screen;           // load it...         Globals.currentScreen.load();     }         // key listener methods     public void keyPressed(KeyEvent e)         { eventProcessor.addEvent(e); }     public void keyReleased(KeyEvent e)         { eventProcessor.addEvent(e); }     public void keyTyped(KeyEvent e)         {} // not used       // mouse listener methods     public void mousePressed(MouseEvent e)             { eventProcessor.addEvent(e); }     public void mouseReleased(MouseEvent e)          { eventProcessor.addEvent(e); }     public void mouseClicked(MouseEvent e)  {} // not used     public void mouseEntered(MouseEvent e)  {} // not used     public void mouseExited(MouseEvent e)   {} // not used       // mouse motion listener methods     public void mouseMoved(MouseEvent e)             { eventProcessor.addEvent(e); }     public void mouseDragged(MouseEvent e)             { eventProcessor.addEvent(e); }       // focus listener methods     public void focusGained(FocusEvent e)              { eventProcessor.addEvent(e); }     public void focusLost(FocusEvent e)              { eventProcessor.addEvent(e); }         public static void main(String args[])     {         GraphicsEnvironment ge = GraphicsEnvironment             .getLocalGraphicsEnvironment();           GameFramework mainAppFrame = new GameFramework             (ge.getDefaultScreenDevice());                 Object[] options = {"FullScreen Mode", "Windowed Mode"};             int choice = JOptionPane.showOptionDialog(null,                                         "Select Display Mode:",                                         "Display Mode",                                          JOptionPane.DEFAULT_OPTION,                                          JOptionPane.QUESTION_MESSAGE,                                         null,                                         options,                                         options[0]);             if(choice!=JOptionPane.CLOSED_OPTION)         {             // choice will be either 0 or 1 corresponding to our mode             // flags, FULLSCREEN_MODE = 0, WINDOWED_MODE = 1               // initialize and start the game...             mainAppFrame.initGame();             mainAppFrame.setMode(choice);             mainAppFrame.start();         }         else             System.exit(0);     }         private Thread loop;     private GraphicsDevice graphicsDevice;       // not final - application may need to adjust these coordinates     // to adapt to windowed border     private int DISPLAY_X = 0;     private int DISPLAY_Y = 0;       private BufferStrategy strategy;       private static final int FULLSCREEN_MODE = 0;     private static final int WINDOWED_MODE = 1;     private int mode;       private EventProcessor eventProcessor; }
end example

Since we are using the FullScreenDemo example from Chapter 9 as a base for this class, there is no point re-explaining all the code, so we are just going to look at what we have added into the example to make it work as a framework.

The only new member that we have added to the class is a reference to an EventProcessor object called eventProcessor, which will be used to synchronize all the events that occur with the main loop in our framework.

Starting in our main method, the first new part is creating an instance of our GameFramework class. So let's look at the additions to the constructor now.

In the constructor, we have added four listeners so that we receive mouse, keyboard, and focus events into our main class via the defined methods in each listener.

addKeyListener(this); getContentPane().addMouseListener(this); getContentPane().addMouseMotionListener(this); addFocusListener(this);

Next we create a new instance of the EventProcessor class, passing in a reference to this object so that the event processor will know that our main class will be handling the events. This can be seen here:

eventProcessor = new EventProcessor(this);

Then we store a reference to our GameFramework object, this, in the Globals.framework object so we can reference our main class from within the screens (note that our main class will contain the method for changing the current screen, so we need access to this object to call this method).

Then we create an instance of our soundManager class, again storing the reference in a static SoundManager reference in the Globals class, so it can be used easily from anywhere in the framework. This can be seen here:

Globals.soundManager = new SoundManager();

Finally, in the constructor, we create instances of the Keyboard and Mouse classes that we looked at earlier and once again store them as static references in the Globals class.

Globals.keyboard = new Keyboard(); Globals.mouse = new Mouse();

So now back to the main method. For the next new part, before we set the mode (i.e., windowed or full screen) and start the main loop running, we have added a call to an initGame method, which can be seen in full here:

public void initGame() {     // create your screens...     Globals.sampleScreen = new SampleScreen();       // load any sounds into the sound manager...         // set the current (starting) screen...     Globals.currentScreen = Globals.previousScreen          = Globals.sampleScreen;         Globals.currentScreen.load(); } 

The first thing we do in the initGame method is create instances of all the screens contained within our game. In this first demo of the framework, we have only one screen called SampleScreen, so we create an instance of it and store a reference to it in the Globals class. Then we load any sounds into the sound manager that we will require during the game, and finally we set the starting screen that the framework should initially display once it is initialized. This is done by assigning the reference to our SampleScreen to the Globals.currentScreen and Globals.previousScreen variables. Then lastly we call the load method of the current screen. Note here that loading and unloading is handled automatically after we have set our first screen, as we have defined a method called setCurrentScreen, which we will look at shortly.

Before we move on any further, however, let's take a look at how events are handled within the framework.

As you know, we are using the EventProcessor that we developed in Chapter 10 for the game framework, so when events come in from the listeners, such as the MouseListener and the KeyboardListener, we add the events to the eventProcessor object by calling the addEvent method.

In our main loop, we call the processEventList method, which will in turn call the handleEvent method in our main class for each event waiting to be processed.

So in our main class, we have defined the handleEvent method as follows:

public void handleEvent(AWTEvent e) {     if(!handleGlobalEvent(e))         Globals.currentScreen.handleEvent(e); }

This first passes the event to another method that we have defined called handleGlobalEvent (that we will look at in a minute), which handles events that are not related to any specific screen. Then this method will return true or false, depending on whether the event was dealt with in the method or not. If the event was not dealt with, we pass the event onto the current active screen (referenced by the Globals.currentScreen reference) by calling the handleEvent method of that screen, passing in the AWTEvent. If you remember, the TemplateScreen class defined the handleEvent method so all screens we derive from this class will also have a handleEvent method.

As we mentioned a moment ago, the handleGlobalEvent method will deal with any events that are to be handled globally within the framework, so let's look at the different cases that we have defined for possible global events now.

The first global event that we deal with is if a key is pressed on the keyboard. The case for this event can be seen here:

case KeyEvent.KEY_PRESSED:     KeyEvent keyEvent = (KeyEvent) e;         Globals.keyboard.keyState[keyEvent.getKeyCode()] = true;       switch(keyEvent.getKeyCode())     {          case KeyEvent.VK_ESCAPE:             exitProgram();             return true;      }     break;

First, we cast the AWTEvent object to be a KeyEvent and then update the relative key state in the keyState array in the static object keyboard in the Globals class. So, in this case, if the key has been pressed, we update the state of the key with true.

Note that this does not consume the event, as we just want to record the state of the keys so that all screens have access to the current state. Then we create a switch statement, which looks at the key code. We then create a special case for the Esc key; if it is pressed, it will exit the program. Note also here that we return true from the method, indicating that this event has been dealt with globally and does not need to be passed onto the current screen for processing.

The next main case that we have created is for a KEY_RELEASED event. When this event occurs, all we want to do is update the relative key in the global keyState array, stating that it is now in the up state. This is done by simply setting the correct value in the array to false. This complete KEY_RELEASED case can be seen here:

case KeyEvent.KEY_RELEASED:     Globals.keyboard.keyState[((KeyEvent)e).getKeyCode()] = false;     break;

Next on the list is to handle the MOUSE_MOVED and MOUSE_DRAGGED events; we want to update the x, y position of the mouse in the static mouse object that we refer to in the Globals class. The complete case for this can be seen here:

case MouseEvent.MOUSE_MOVED: case MouseEvent.MOUSE_DRAGGED: {     MouseEvent mouseEvent = (MouseEvent) e;     Globals.mouse.x = mouseEvent.getX();     Globals.mouse.y = mouseEvent.getY();     break; }

After this, we then have a case for the mouse being pressed, MOUSE_PRESSED, which updates the current state of the mouse buttons in the mouse object. This can be seen here:

case MouseEvent.MOUSE_PRESSED:     switch(((MouseEvent)e).getButton())     {         case MouseEvent.BUTTON1:                 Globals.mouse.button[Mouse.LEFT_BUTTON] = true;         break;         case MouseEvent.BUTTON2:                 Globals.mouse.button[Mouse.MIDDLE_BUTTON] = true;         break;          case MouseEvent.BUTTON3:                 Globals.mouse.button[Mouse.RIGHT_BUTTON] = true;         break;      }     break;

Then we have virtually the same case again, except this time we handle the mouse buttons being released (and update the state of the button array with the mouse object to show this). The MOUSE_RELEASED case can be seen here:

case MouseEvent.MOUSE_RELEASED:     switch(((MouseEvent)e).getButton())     {         case MouseEvent.BUTTON1:                 Globals.mouse.button[Mouse.LEFT_BUTTON] = false;         break;         case MouseEvent.BUTTON2:                 Globals.mouse.button[Mouse.MIDDLE_BUTTON] = false;         break;         case MouseEvent.BUTTON3:                 Globals.mouse.button[Mouse.RIGHT_BUTTON] = false;         break;     }     break;

The next events that we handle globally are FOCUS_LOST and FOCUS_ GAINED. When the focus is lost, we want to reset the keyState array in the keyboard object and the button state array in the mouse object. We can do this really easily by using the helper methods that we made earlier called resetAllStates, which are defined in both the Mouse and Keyboard classes. In this example, we have not implemented any handling for the gain of focus; however, in the next section, "A Framework Demo," we will see this being used to display a different screen. Here are the two cases that we have added for focus lost and gained:

case FocusEvent.FOCUS_LOST:     // reset key states...     Globals.keyboard.resetAllStates();       // reset mouse button states...     Globals.mouse.resetAllStates();     break;   case FocusEvent.FOCUS_GAINED:     break; 

Therefore, after the events are handled in the main loop by the call to the processEventList method, we then call the process method of the Globals.currentScreen reference so that any logic specific to the current screen that is visible will be dealt with. Again, remember that our TemplateScreen class defines the process method, so when we create a screen by extending the TemplateScreen, it ensures the existence of this method (which of course we can override to put in our own logic code for the screen).

After the logic code has been dealt with, we then translate the graphics object to the correct position and call the render method of Globals.currentScreen, which again, if you remember, is defined as abstract in the TemplateScreen class. Therefore, any screens that we derive from it must implement their own render method.

The only part that we haven't looked at in the framework yet is the way that screens are changed, so let's take a look at the setCurrentScreen method that we have defined in the GameFramework class now.

public void setCurrentScreen(TemplateScreen screen) {     // unload the current screen...     Globals.currentScreen.unload();         // set this screen to the previous screen...     Globals.previousScreen = Globals.currentScreen;       // assign the new screen...     Globals.currentScreen = screen;       // load it...     Globals.currentScreen.load();  }

This is very simple really; all we do is pass in a screen that we wish to make active (note it is passed in as a TemplateScreen, but since all our screens in the framework must be derived from this class, it is ideal). Then, the unload method of the current screen will be called, and the Globals.previousScreen reference will be set to refer to the Globals.currentScreen reference. Then we simply assign the screen to which we wish to change to the Globals.currentScreen reference. Finally, we call the load method of our new current screen.

If you now compile the framework, run it, and select windowed mode, you should see something similar to the following on the screen:

click to expand
Figure 12-14: The Game framework (showing Sample Screen)

A Framework Demo

Now that we have looked at how the framework works, let's create a simple demo that contains three different screens, each of which does something different. The first screen will show the StickWalker animation that we created in the animation section of this chapter, the second will show a hot spot that we can move around with the cursor keys (just like in Chapter 9), and the third screen will let you draw a rectangle with the mouse. In addition, we will create another screen that will be displayed if the application loses focus.

Let's now look at the changes in the code for each of the three screens (changes from the original examples that you have seen previously, except the third screen, which is new code).

  • Demo Screen 1: Stick Walker—Here is the complete code listing for our first screen, DemoScreen1.java.

    Code Listing 12-12: DemoScreen1.java

    start example
    import java.awt.*; import java.awt.event.*; import javax.imageio.*; import java.io.*;     public class DemoScreen1 extends TemplateScreen {     public DemoScreen1()     {         // setup the screen here...         try         {             characterSheet = ImageIO.read(new File                 ("stickmansheet.gif"));             backgroundImage = ImageIO.read(new File("backdrop.gif"));         }         catch(IOException e)          {             System.out.println(e);          }             // assign start walk animation, direction and position         walkAnim = 0;         walkDir = 0;         xPos = 200;         yPos = 386;           // work frame limits on the fly          MAX_WALK_ANIMATIONS = characterSheet.getWidth(null)              / FRAME_WIDTH;         MAX_WALK_DIRECTIONS = characterSheet.getHeight(null)              / FRAME_HEIGHT;     }       public void process()     {         // place screen logic code here...           // handle animations         walkAnim++;         if(walkAnim >= MAX_WALK_ANIMATIONS)             walkAnim = 0;           // move character position and handle direction changing         switch(walkDir)          {             case 0: // left                 xPos-=4;                 if(xPos<0)                 {                     xPos = 0;                     walkDir = 1;                 }                  break;               case 1: // right                 xPos+=4;                 if(xPos+FRAME_WIDTH>bounds.width)                 {                     xPos = bounds.width-FRAME_WIDTH;                     walkDir = 0;                 }                  break;         }     }       public void render(Graphics g)     {         // rendering code goes here...         g.drawImage(backgroundImage, 0, 0, null);           // render current frame to current screen position             int srcX0 = walkAnim*FRAME_WIDTH;         int srcY0 = walkDir*FRAME_HEIGHT;         int srcX1 = srcX0+FRAME_WIDTH;         int srcY1 = srcY0+FRAME_HEIGHT;           g.drawImage(characterSheet,                      xPos, yPos, xPos+FRAME_WIDTH, yPos+FRAME_HEIGHT,                     srcX0, srcY0, srcX1, srcY1,                     null);              g.setColor(Color.black);         g.drawString("Stick Walker Screen", 10, 15);     }       public void handleEvent(AWTEvent e)     {         // handle events here...     }       private Image characterSheet;     private int xPos;     private int yPos;      private int walkAnim;     private int walkDir;       private int MAX_WALK_ANIMATIONS;     private int MAX_WALK_DIRECTIONS;       private static final int FRAME_WIDTH = 32;     private static final int FRAME_HEIGHT = 64;       private Image backgroundImage; }
    end example

    All we have really done here is use ImageIO instead of the media tracker to load in our two tile sheets in the constructor. Then we have placed the code to handle the animation and movement of the StickWalker in the process method, which if you remember from the framework will be called every cycle of the main loop while the screen is currently being displayed. Finally, we placed the code to render the StickWalker into the render method, which again is called every cycle of the main loop.

  • Demo Screen 2: Circle Moving Screen—Here is the complete code listing for our second screen, DemoScreen2.java.

    Code Listing 12-13: DemoScreen2.java

    start example
    import java.awt.*; import java.awt.event.*;     public class DemoScreen2 extends TemplateScreen {     public DemoScreen2()     {         animator = new Animator(bounds);     }       public void process()     {         // place screen logic code here...         animator.animate();     }       public void render(Graphics g)     {         // clear the background...         g.setColor(Color.black);         g.fillRect(0, 0, bounds.width, bounds.height);           // draw the movable hotspot...         animator.render(g);           // draw the screen title...         g.setColor(Color.green);         g.drawString("Circle Moving Screen", 10, 15);       }       public void handleEvent(AWTEvent e)     {         // handle events here...     }       Animator animator; } 
    end example

    The code required for this screen is much less, as we are using the HotSpot class from Chapter 9 and a slightly modified version of the Animator class, also from Chapter 9.

    In the actual screen, we create an instance of the Animator class in the constructor by passing the bounds of the screen into the constructor. Then in the process method, we call the animate method of our animator object, which handles the key input that we will see shortly. Then in the render method, we simply call the render method of the animator object, passing in the Graphics object g that was passed to the screen's render method.

    So, for handling input in the Animator class, we now use the Globals.keyboard.keyState array to determine the states of the relevant keys. Here is the complete animate method:

    public void animate() {     if(Globals.keyboard.keyState[KeyEvent.VK_LEFT]          && !Globals.keyboard.keyState[KeyEvent.VK_RIGHT])             moveLeft();     else if(Globals.keyboard.keyState[KeyEvent.VK_RIGHT]          && !Globals.keyboard.keyState[KeyEvent.VK_LEFT])             moveRight();       if(Globals.keyboard.keyState[KeyEvent.VK_UP]          && !Globals.keyboard.keyState[KeyEvent.VK_DOWN])             moveUp();     else if(Globals.keyboard.keyState[KeyEvent.VK_DOWN]          && !Globals.keyboard.keyState[KeyEvent.VK_UP])             moveDown();     }

    As you can see, it is very similar to before, but instead of holding a local reference to the array of key states, we are accessing the array defined in the Globals.keyboard object, which is updated by the framework.

    The only other part of the Animator class that we have changed is the constructor, which now takes a Rectangle object as a parameter to define the area in which the hot spot can move around. The complete constructor method can be seen here:

    public Animator(Rectangle bounds) {     this.bounds = bounds;         createHotSpot();       speedX = 4;     speedY = 4; }
  • Demo Screen 3: Mouse Example Screen—Here is the complete code listing for our third screen, DemoScreen3.java.

    Code Listing 12-14: DemoScreen3.java

    start example
    import java.awt.*; import java.awt.event.*;         public class DemoScreen3 extends TemplateScreen {     public void process()     {         // place screen logic code here...           // start dragging         if(Globals.mouse.button[Mouse.LEFT_BUTTON] && dragStartX              == -1 && dragStartY == -1)         {             dragStartX = Globals.mouse.x;             dragStartY = Globals.mouse.y;         }           if(Globals.mouse.button[Mouse.RIGHT_BUTTON])         {             dragStartX = dragStartY = -1;         }     }       public void render(Graphics g)     {         // rendering code goes here...         g.setColor(Color.white);         g.fillRect(0, 0, bounds.width, bounds.height);           g.setColor(Color.black);           // draw the dragged rectangle...         if(dragStartX != -1 && dragStartY != -1)         {             int x, y, w, h;               if(dragStartX < Globals.mouse.x)             {                 x = dragStartX;                 w = Globals.mouse.x - dragStartX;             }             else             {                 x = Globals.mouse.x;                 w = dragStartX - Globals.mouse.x;             }               if(dragStartY < Globals.mouse.y)             {                 y = dragStartY;                 h = Globals.mouse.y - dragStartY;             }             else             {                 y = Globals.mouse.y;                 h = dragStartY - Globals.mouse.y;             }               g.drawRect(x, y, w, h);         }           // draw the mouse positions...         g.drawString("Mouse Example Screen", 10, 15);         g.drawString("Mouse X: "+Globals.mouse.x+"   Mouse Y:             "+Globals.mouse.y, 10, 35);           // and the button states...         g.drawString("Left Button: "+Globals.mouse.button             [Mouse.LEFT_BUTTON], 10, 55);         g.drawString("Middle Button: "+Globals.mouse.button             [Mouse.MIDDLE_BUTTON], 10, 75);         g.drawString("Right Button: "+Globals.mouse.button             [Mouse.RIGHT_BUTTON], 10, 95);     }       public void handleEvent(AWTEvent e)     {         // handle events here...     }       public void load()     {         dragStartX = dragStartY = -1;     }         int dragStartX, dragStartY; }
    end example

    In the third screen, DemoScreen3, we want to be able to start drawing a "dragging" rectangle with the mouse and then cancel with the right mouse button. The first thing to notice in this screen is that we have overridden the load method so that the starting x, y position of the dragged rectangle is reset every time the screen is loaded.

    Then, in the process method, we first check to see if the starting point of the rectangle has not been defined. If so, we check if the left mouse button is currently down. If it is, we then assign the dragStartX and dragStartY values to be equal to the current mouse x, y position (retrievable from the mouse object in the Globals class).

    Then, also in the process method, we check if the right mouse button is pressed down. If it is, we simply reset the start x, y position for the dragging so the rectangle is no longer drawn (as the render method will only draw the rectangle if the dragStartX and dragStartY variables do not equal –1).

    In the render method, we first check if the dragStartX and dragStartY variables do not equal –1, and if they do not, we find out which values should be used for the starting position of the rectangle, as passing a negative width or height into the g.drawRect method will simply not draw it.

  • The Pause Screen—In addition to our three main screens, we are going to define a PauseScreen, which will be displayed when the application loses focus. Here is the complete source listing for the pause screen:

    Code Listing 12-15: PauseScreen.java

    start example
    import java.awt.*; import java.awt.event.*;     public class PauseScreen extends TemplateScreen {     public PauseScreen()     {         // set up the screen here...     }       public void process()     {         // place screen logic code here...         }       public void render(Graphics g)     {         // rendering code goes here...         g.setColor(Color.red);         g.fillRect(0, 0, bounds.width, bounds.height);           g.setColor(Color.black);         g.drawString("[ LOST FOCUS ]", 370, 300);     }       public void handleEvent(AWTEvent e)     {         // handle events here...     } }
    end example

    All we have actually implemented in here is the render method, where we first fill the screen red and then draw [ LOST FOCUS ] in the middle of the screen in black.

Integrating the Screens into the Framework

Now that we have seen the four screens, we are going to look at the modifications to the GameFramework class to allow for the screens to work correctly.

Rather than regurgitating the entire code for the GameFramework class, it is probably best to look at the changes to the code. We will first start off with the minor change to the Globals class, where we have removed the reference to the SampleScreen class and replaced it with the following four references:

public static DemoScreen1 demoScreen1; public static DemoScreen2 demoScreen2; public static DemoScreen3 demoScreen3; public static PauseScreen pauseScreen;

Next, back in the GameFramework class, we have created an instance of each of our screens, which will then be stored in these references that we have just added to the Globals class. These are to be placed in the initGame method in place of where we created our SampleScreen object.

// create your screens... Globals.demoScreen1 = new DemoScreen1(); Globals.demoScreen2 = new DemoScreen2(); Globals.demoScreen3 = new DemoScreen3(); Globals.pauseScreen = new PauseScreen();

Next, once the screen instances are created, we assign the current and previous screen references in the Globals class to be Globals.demoScreen1 and then we call the load method of the Globals.currentScreen method. This can be seen here:

Globals.currentScreen = Globals.previousScreen = Globals.demoScreen1; Globals.currentScreen.load();

The next change is in the handleGlobalEvent method, when we add an extra three keys to handle where we originally just had the handling for the Esc key. The new case for the KEY_PRESSED event should now look as follows:

case KeyEvent.KEY_PRESSED:     KeyEvent keyEvent = (KeyEvent) e;     Globals.keyboard.keyState[keyEvent.getKeyCode()] = true;       switch(keyEvent.getKeyCode())     {          case KeyEvent.VK_ESCAPE:             exitProgram();             return true;             case KeyEvent.VK_1:             setCurrentScreen(Globals.demoScreen1);             return true;            case KeyEvent.VK_2:             setCurrentScreen(Globals.demoScreen2);             return true;            case KeyEvent.VK_3:             setCurrentScreen(Globals.demoScreen3);             return true;      }     break;

Notice that the three extra keys that we now handle here are 1, 2, and 3, which enable us to change the screens. Also note that we have placed these in the handleGlobalEvent method so that the screen can be set from any other screen (just as we can exit the program from any screen with the Esc key). We could of course not handle the events here but handle them in each screen, making it so that you could only get to screen 2 from screen 1 and only get to screen 3 from screen 2, etc.

The final change that we have made to the framework is for the FOCUS_GAINED and FOCUS_LOST events, again in the handleGlobalEvent method. The new cases for both these events can be seen here:

case FocusEvent.FOCUS_LOST:                 setCurrentScreen(Globals.pauseScreen);       // reset key states...     Globals.keyboard.resetAllStates();       // reset mouse button states...     Globals.mouse.resetAllStates();     break;   case FocusEvent.FOCUS_GAINED:         if(Globals.previousScreen != null)             setCurrentScreen(Globals.previousScreen);     break;

So for the FOCUS_LOST event, we have simply added a call to the setCurrentScreen method, passing in a reference to the PauseScreen object stored in the Globals class. Then we reset the key and mouse states, as we did in the previous example. For the FOCUS_GAINED event, we first ensure that the previousScreen reference in the Globals class is not null, as it may not have been initialized before we receive our first FOCUS_GAINED event when the application initially gains focus. Then, if it does contain a valid reference, we call the setCurrentScreen method, passing in the Globals.previousScreen reference so the screen will return to the same screen as it was before it was changed to the pause screen when the FOCUS_LOST event occurred.

That is all there is to it. Try it out by compiling it all and running it, using the 1, 2, and 3 keys to switch between the different screens.

Here is how the four different screens look when you run the framework demo:

click to expand
Figure 12-15: The four screens in the framework demo



Java 1.4 Game Programming
Java 1.4 Game Programming (Wordware Game and Graphics Library)
ISBN: 1556229631
EAN: 2147483647
Year: 2003
Pages: 237

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net