Feedback Search Top Backward Forward
EDM/2

The Making of WarpTris - Part 2

Written by Paulo Gago da Camara

Linkbar

 
Part1 Part2

Introduction

Welcome to the second installment of "The making of WarpTris". As promised, this month I'll cover the PM portion of the game.

The User Interface

The WarpTris User Interface (UI) was completely written using the Presentation Manager (PM) API; after all, the main purpose of this game was for me to learn PM programming.

I chose to use a multi-window interface, because it gives the user more freedom on how to display the different information of the game. Before describing the details of the PM implementation, I'm going to explain how the digital panel that shows the score is implemented.

Creating a Digital Panel

The scores digital panel is implemented as a class called 'DigitalPanel'. This class is declared and its methods are defined inside the 'Digital.h' file.

The graphical digits are 16 color bitmaps with a size of 11x21 pixels each. I had to draw each digit separately for the numbers '0' to '9' and an additional 'Off digit' which is used where no digit is lit. When an object of this class is created, the constructor loads all these bitmaps using the GpiLoadBitmap API into an array of 'HBITMAP', which is used later to draw the necessary digit. Now I'll give a description of all the methods and data members of the class 'DigitalPanel'.

  • Private Members
    • HWND hwndOwner; - This variable holds the handle of the window where the digital panel is to be drawn. This member is initialized using the constructor 'hwnd' parameter.
    • HBITMAP hbmp[11]; - These are the eleven 'HBITMAP's that are used to hold the handles of the bitmaps loaded. The bitmaps are loaded in the following order: '0'...'9' and the 'Off digit' bitmap.
    • INT digitCount; - This member holds the maximum number of digits that this panel can hold. This is also initialized in the constructor.
    • INT posX, posY; - These two values hold the digital panel lower left corner coordinates (in window coordinates).
    • ULONG value; - This is the numeric value currently displayed by the digital panel.
    • VOID LightDigit(HPS hps, INT digit, INT v); - This private method lights a given digit with a given value between '0' and '9'. If the value of 'v' is 10 (ten), then the 'Off digit' is drawn.
  • Constructors
    • DigitalPanel(HWND hwnd, INT x, INT y, INT digitC, ULONG v=0); - This is the constructor for this class. It initializes the private data members and loads the digit bitmaps.
  • Destructors
    • ~DigitalPanel(); - The destructor of this class is used to delete the bitmaps loaded by the constructor, using the GpiDeleteBitmap API.
  • Public Members
    • VOID Draw(BOOL showValue=TRUE); - This method draws the digital panel and, by default, shows the value contained in the private member 'value'.
    • VOID ShowValue(VOID); - This method extracts digits from the private data member 'value', and shows them graphically on the digital panel.
    • VOID SetValue(ULONG v) {value=v;}; - This method just sets the value of the private data member 'value'.
    • VOID AddToValue(ULONG v) {value += v; Draw();} - This method adds a given number to 'value' and draws the digital panel to update it.
    • ULONG GetValue(VOID) {return(value);} - This function returns the score stored in 'value'.
Below is the full DigitalPanel class declaration:

class DigitalPanel
{
 private:
  HWND hwndOwner;    // Handle of the window that owns the digital panel
  HBITMAP hbmp[11];  // The handles of the bitmaps that represents the numbers
  INT digitCount;    // The number of digits that the display has
  INT posX, posY;    // Coordinates of the lower left corner
  ULONG value;       // The value that it currently displays
  VOID LightDigit(HPS hps, INT digit, INT v); // Light a given digit with
                                              // a given value(0..9)

 public:
  DigitalPanel(HWND hwnd, INT x, INT y, INT digitC, ULONG v=0); // Constructor
  ~DigitalPanel();                        // Destructor
  VOID Draw(BOOL showValue=TRUE);         // Draw the panel
  VOID ShowValue(VOID);                   // Show the value on it
  VOID SetValue(ULONG v) {value=v;};      // Set the value
  VOID AddToValue(ULONG v) {value += v; Draw();} // Add a number to the value
  ULONG GetValue(VOID) {return(value);}   // Get the value
};
It is very easy to use this class in any other PM program, so feel free to use it any time.

WarpTris Windows and Window Procedures

There are eight windows and window procedures in WarpTris. Some of them are visible to the user and others aren't. In this section I'll describe each window and associated window procedure separately. We will also take a look at some of the most important messages processed by them.

One of the window concepts that I used extensively in WarpTris was 'Window Words'. 'Window Words' are nothing more that a given number of bytes that are reserved when we create a window. This storage is private to each instance of a window class and it's normally used to store a pointer (or more) to a data structure, which in turn holds all the data we need to associate with that window. The amount of storage we want to reserve for 'Window Words' is specified in the fifth parameter of the WinRegisterClass API. For a pointer, this value should be four bytes (32 bits). Later we can initialize and read the pointer stored in the "Window Words" using the WinSetWindowPtr and WinQueryWindowPtr APIs, respectively.

Several structures are declared on the 'WarpTris.cpp' file, these structures store relevant information about the windows to which they are associated using the 'Window Words' mechanism described above.

When a window is being created, the first message it receives is the WM_CREATE message. This message is normally used to initialize the data needed for that window. I used this message on all windows to allocate and initialize the associated data structures and to store a pointer to them in the corresponding 'Window Words'.

Later, when the window is destroyed it receives the WM_DESTROY message, and the storage allocated for these structures is freed inside that message processing.

Now I'll start describing each WarpTris window procedure.


MRESULT EXPENTRY GameWindowProc(HWND hwnd,
                                ULONG msg,
                                MPARAM mp1,MPARAM mp2)
This is the prototype of the window procedure that handles the events for the game window. This function processes all the messages that are generated by the user interaction when playing the game. This is also where the objects of class Board and Figure, which were described in an earlier article, are used. Now I'll give a small description of the most important messages processed by this window procedure.
  • WM_CREATETHREAD - This message is used when we are ready to create the thread of execution that plays the sound effects (more on that later).
  • WM_CREATEOBJECTS - This message is sent to the window when the objects that represent the tile and the grid ('Figure' and 'Board') need to be created. This normally happens when a new game is started.
  • WM_NEWGAME - When the new game option is selected from the main menu or the game window context menu is selected, this message is sent to the window procedure in order initialize a new game. The action taken by this message processing includes resetting the score to zero and sending a WM_CREATEOBJECTS to the window procedure.
  • WM_CHAR - Every time the user presses a key when the game window has the input focus, a WM_CHAR message is generated. Actually two WM_CHAR messages are generated, one to signal the KEYDOWN event and other to signal the KEYUP event. It's amazing the degree of detail that the PM gives when a key is pressed. For the WarpTris key processing I was only interested on the KEYDOWN events, so I filtered out any other events.

    In the first release of WarpTris the NUMLOCK key had to be active in order to play the game. On the current release (1.01) this limitation was eliminated. You can ask why didn't I solve the problem at first? Well, the problem was the keypad '5' key. For some reason I couldn't find out which code this key generated. Only later with a bit of patience and on a trial-and-error basis, I discovered that to intercept this key when the NUMLOCK was off I had to look at the hardware scan code that is located on the fourth CHAR of the first MPARAM (mp1), and if it was 76 then the keypad '5' was pressed, the code looks like this:

    
       case WM_CHAR:
          if(SHORT1FROMMP(mp1) & KC_KEYUP) break;// Only process KC_KEYDOWN events
          if(CHAR4FROMMP(mp1)==76) // Hardware scan code for the keypad '5'
             // Keypad 5 with NUMLOCK inactive was pressed
          ...
         break;
      
    Although this works fine, I suspect that there might be a more elegant way to accomplish this. If you know a better way of doing this I would appreciate it if you E-mailed me your suggestion.
  • WM_PLAYSOUND - This message is sent whenever the POP sound needs to be played. What this message does is to instruct the object window procedure on the sound thread to play (more on this later).
  • WM_TIMER - When this window is created, a timer is also created using the WinStartTimer API. When we create a timer for a window the corresponding window procedure starts receiving the WM_TIMER message at time intervals specified at timer creation. This timer is used to make the tiles move at the appropriate speed.
  • WM_CHANGELEVEL - When the user changes the impulse level, this message is sent and the timer that makes the tile move is changed accordingly. The new level is passed on the mp1 parameter.
  • WM_PAUSEGAME - This message simply pauses the game by changing the value of the gamePaused flag stored in the structure pointed to by the pointer stored in the 'Window Words'.
  • WM_RESUMEGAME - This message does exactly the opposite of the WM_PAUSEGAME message.
  • WM_DROPED - Whenever the user drops a tile by pressing the space bar, this message is sent. The processing of this message includes testing if there are lines to be removed and if the game has ended.
  • WM_GAMEOVER - When the game ends, this message is posted to see, among other things, if there is an high score and to display a dialog asking the user whether to play another game.

MRESULT EXPENTRY LogoWindowProc(HWND hwnd,
                                ULONG msg,
                                MPARAM mp1, MPARAM mp2)
This is the window procedure for the logotype that appears when the game is loading. The processing of this window is rather simple, it just shows the WarpTris logotype bitmap for a few seconds or until the user presses a key or clicks on it.

MRESULT EXPENTRY MainWindowProc(HWND hwnd,
                                ULONG msg,
                                MPARAM mp1, MPARAM mp2)
This window procedure handles the events for the main WarpTris window. The main purpose of this window is to serve as a parent for all the other windows of the game. On creation, this window procedure reads the settings of the game and initializes all the menus and windows accordingly. These settings and properties are stored in the WarpTris.ini file.
  • WM_CREATE - This is where the settings saved on the WarpTris.ini file and the context menus are loaded. After loading them, the menu items are initialized to reflect the changes stored in the WarpTris.ini file.
  • WM_COMMAND - All the menu items chosen by the user generate a WM_COMMAND event. What the processing of this message does, is to send messages to the other windows depending on the chosen menu item. The ID of the menu item selected is passed on the first short of the mp1 parameter, which is examined using the SHORT1FROMMP(mp1) helper macro.
  • WM_CONTEXTMENU - This message is sent to the window procedure when the user requests a context menu for this window. When this message is received we just query the mouse pointer position and show the menu there.
  • WM_DESTROY - This message is used here to free all the resources allocated for this window, including the menus loaded from the resource file on the WM_CREATE message.

MRESULT EXPENTRY NextWindowProc(HWND hwnd,
                                ULONG msg,
                                MPARAM mp1, MPARAM mp2);
This is the window procedure for the window that displays the next tile and the next direction. This window shares a data structure with the game window. That data structure is used to hold the next tile object and its impulse direction. When a new tile is needed on the game window, the game window procedure reads it from the shared data structure using the overloaded operator '=', and posts a WM_NEXT message to this window procedure, which in turn creates a new figure object and a new direction randomly, to be used the next time the game window needs a new tile.

MRESULT EXPENTRY ScoreWindowProc(HWND hwnd,
                                 ULONG msg,
                                 MPARAM mp1, MPARAM mp2);
This is the window procedure for the window that displays the score digital panel. It exists just to hold the view of an object of the DigitalPanel class described earlier on this article. There's nothing to know about this window procedure, all the relevant processing is done by the DigitalPanel class that was described earlier. This window also has access to the shared data structure described above which holds an object of the DigitalPanel class.

MRESULT EXPENTRY HighWindowProc(HWND hwnd,
                                ULONG msg,
                                MPARAM mp1, MPARAM mp2);
This is the window procedure that handles the events for the 'High Scores' window. This window simply creates a list box control that is filled with an object of the HighScore class, which exists on the shared data structure that it also has access to.

MRESULT EXPENTRY NoteBookWindowProc(HWND hwnd,
                                    ULONG msg,
                                    MPARAM mp1, MPARAM mp2);
This is the window procedure for the window that is used to hold the properties notebook control. When this window is first created it creates the notebook control and initializes it according to the properties saved in the 'WarpTris.ini' file. The notebook control has three pages. The dialog procedures for the dialogs that make up the pages are the following:
  • Page1DlgProc
  • Page2DlgProc
  • Page3DlgProc
I've also intercepted the WM_CLOSE message to change its default processing to hiding the window instead of destroying it. This way when the user closes the properties window, it just hides itself and doesn't get destroyed.

MRESULT EXPENTRY ObjectWindowProc(HWND hwnd,
                                  ULONG msg,
                                  MPARAM mp1, MPARAM mp2);
This is the window procedure for the object window that is used by the sound thread. An 'Object Window' is a window that doesn't have a graphical representation and its normally used to assist other windows in their processing or interaction with other objects. The 'Object Windows' only have two system defined messages which are the WM_CREATE and WM_DESTROY messages. What the programmer normally does is to define a set of user messages to handle the processing needed on a particular situation. On WarpTris I've used this concept to manage the messages that ask the sound thread to play the sound effects that are used during the game.

Playing the Sounds on a separate thread

I decided to play the sound effects on a separate thread of execution, because if they were played on the same thread as the game runs on, it would slow down the tile movement. This wouldn't be a Good Thing(tm)

Because of my weak knowledge of the OS/2 multimedia API, I was forced to use the MCI command string interface which is quite simple to use but obviously has its limitations.

What happens is that, when a sound needs to be played, for example the pop sound, the game window posts the WM_POP user defined window message to the thread object window which in turn plays the selected sound effect.

Conclusion

Well, this is it. I hope that reading this article pleased you as much as it did writing it. If you have any question or suggestion about the game or this article, you're welcome to E-mail me with it, and I'll do my best.

My native language is Portuguese, so please forgive me if my English is poor.

Until next time!

 

Linkbar