Win32 Assembly Part 3
by Chris Hobbs

Where Did We Leave Off?

Now, if I remember correctly ... which I do ... the last time we were together, we had just completed our loading game screen for SPACE-TRIS. Which means we have a Direct Draw library and a bitmap library, but not much else. We had to use the WM_KEYDOWN message to process our input, and we just got the privilege of looking at a loading game screen. That was it.

Well, all of that is about to change. First, we are going to get a Direct Input library developed. After that, we will code some advanced timing routines. Next, we will develop the menu code for our menus. Finally, we will take a look at the new game loop and how it had to be changed.

We will be covering A LOT of code in this article so you may want to review the earlier articles for basic concepts if you are new to Win32 ASM programming. Otherwise, head to the next section where we will get started with our Direct Input routines.

Direct Input's A Breeze

Are you ready? Good. For all intents and purposes, Direct Input code has the same basic format as our Direct Draw code. You will notice the more you use DirectX that it is all the same ... just different parameters. Anyway, we will want to put together an initialization and a shutdown routine. We are also going to look at the coding of a routine to handle reading the keyboard. The accompanying code has routines for the mouse in it also. But, since we don't care about the mouse in our game, I won't be covering it in the article. It is there however ... so feel free to use it if you would like.

To start with ... I guess you are going to want to see the Direct Input initialization routine.


;########################################################################
; DI_Init Procedure
;########################################################################
DI_Init PROC

    ;=======================================================
    ; This function will setup Direct Input
    ;=======================================================

    ;=============================
    ; Create our direct Input obj
    ;=============================
    INVOKE DirectInputCreate, hInst, DIRECTINPUT_VERSION, ADDR lpdi,0

    ;=============================
    ; Test for an error creating
    ;=============================
    .IF EAX != DI_OK
        JMP    err
    .ENDIF

    ;=============================
    ; Intiialize the keyboard
    ;=============================
    INVOKE DI_Init_Keyboard

    ;=============================
    ; Test for an error in init
    ;=============================
    .IF EAX == FALSE
        JMP    err
    .ENDIF

    ;=============================
    ; Intiialize the mouse
    ;=============================
    INVOKE DI_Init_Mouse

    ;=============================
    ; Test for an error in init
    ;=============================
    .IF EAX == FALSE
        JMP    err
    .ENDIF

done:
    ;===================
    ; We completed
    ;===================
    return TRUE

err:
    ;===================
    ; Give the error msg
    ;===================
    INVOKE MessageBox, hMainWnd, ADDR szNoDI, NULL, MB_OK

    ;===================
    ; We didn't make it
    ;===================
    return FALSE

DI_Init    ENDP
;########################################################################
; END DI_Init
;########################################################################

This code isn't very complex at all. It starts by creating the main Direct Input object. This is the object that you use to derive all of the device objects and such. Just a single call -- passing it the address of the variable, the version of Direct Input to use, and the instance of our application. Then, we call routines to setup our keyboard and mouse. Thus, we need to take a peek at the routine that initializes the keyboard.


;########################################################################
; DI_Init_Keyboard     Procedure
;########################################################################
DI_Init_Keyboard     PROC 

    ;=======================================================
    ; This function will initialize the keyboard
    ;=======================================================

    ;===========================
    ; Now try and create it
    ;===========================
    DIINVOKE CreateDevice, lpdi, ADDR GUID_SysKeyboard, ADDR lpdikey, 0

    ;============================
    ; Test for an error creating
    ;============================
    .IF EAX != DI_OK
        JMP    err
    .ENDIF

    ;==========================
    ; Set the coop level
    ;==========================
    DIDEVINVOKE SetCooperativeLevel, lpdikey, hMainWnd, \
              DISCL_NONEXCLUSIVE OR DISCL_BACKGROUND

    ;============================
    ; Test for an error querying
    ;============================
    .IF EAX != DI_OK
        JMP    err
    .ENDIF

    ;==========================
    ; Set the data format
    ;==========================
    DIDEVINVOKE SetDataFormat, lpdikey, ADDR c_dfDIKeyboard

    ;============================
    ; Test for an error querying
    ;============================
    .IF EAX != DI_OK
        JMP    err
    .ENDIF

    ;===================================
    ; Now try and acquire the keyboard
    ;===================================
    DIDEVINVOKE Acquire, lpdikey

    ;============================
    ; Test for an error acquiring
    ;============================
    .IF EAX != DI_OK
        JMP    err
    .ENDIF

done:
    ;===================
    ; We completed
    ;===================
    return TRUE

err:
    ;===================
    ; We didn't make it
    ;===================
    return FALSE

DI_Init_Keyboard    ENDP
;########################################################################
; END DI_Init_Keyboard    
;########################################################################

The first thing that this code does, is attempt to create the keyboard device. Remember, like the Direct Draw object, the Direct Input object is generic and other things must be created off of it ... such as keyboard devices. Once you have created the object you will need to set the cooperative level on it. In our case we would like to have exclusive use of it.

Now that we have the keyboard's cooperative level set, we can inform Direct Input of the type of data that it will receive. For the keyboard, this is c_dfDIKeyboard. The final step left in initializing our keyboard is acquiring it. This is the step that most people tend to forget. Currently, we have an object that we have told to accept keyboard data and take exclusive access of the keyboard. But, we have not told it that it has permission to take control of the object. So, we make a call and that's that.

We are now ready to use it. That means we need to have a look at the code to read the keyboard. Unless, of course, you would prefer to simply make a call, and never see the code in your life. Oh my!! What kind of programmer are you? I can't believe you just thought that! Here's the code anyway ...


;########################################################################
; DI_Read_Keyboard     Procedure
;########################################################################
DI_Read_Keyboard     PROC 

    ;================================================================
    ; This function will read the keyboard and set the input state
    ;================================================================

    ;============================
    ; Read if it exists
    ;============================
    .IF lpdikey != NULL 
        ;========================
        ; Now read the state
        ;========================
        DIDEVINVOKE GetDeviceState, lpdikey, 256, ADDR keyboard_state
        .IF EAX != DI_OK
            JMP    err
        .ENDIF
    .ELSE
        ;==============================================
        ; keyboard isn't plugged in, zero out state
        ;==============================================
        DIINITSTRUCT ADDR keyboard_state, 256
        JMP    err

    .ENDIF

done:
    ;===================
    ; We completed
    ;===================
    return TRUE

err:
    ;===================
    ; We didn't make it
    ;===================
    return FALSE

DI_Read_Keyboard    ENDP
;########################################################################
; END DI_Read_Keyboard
;########################################################################

This code first tests to see if we have a valid object. If not, it could be for a number of reasons ... such as the keyboard not being plugged in, or maybe a bad port. This is unlikely to happen, but better safe than sorry.

Then, if we exist, the code reads the device. In the case of the keyboard, all entries are cleared and are only set if the key has been actively pressed down at the time of the read. If so, then the keyboard constant associated with that key is set to TRUE, otherwise it is left as FALSE. They key constants are defined in the "DInput.inc" file --- examples are: DIK_J, DIK_N, etc.

Finally the only thing left to do is shutdown the Direct Input stuff. That is done with the following routine.


;########################################################################
; DI_ShutDown Procedure
;########################################################################
DI_ShutDown PROC

    ;=======================================================
    ; This function will close down Direct Input
    ;=======================================================

    ;=============================
    ; Shutdown the Mouse
    ;=============================
    DIDEVINVOKE Unacquire, lpdimouse
    DIDEVINVOKE Release, lpdimouse

    ;=============================
    ; Shutdown the Keyboard
    ;=============================
    DIDEVINVOKE Unacquire, lpdikey
    DIDEVINVOKE Release, lpdikey

    ;==================================
    ; Shutdown the Direct Input object
    ;==================================
    DIINVOKE Release, lpdi

done:
    ;===================
    ; We completed
    ;===================
    return TRUE

err:
    ;===================
    ; We didn't make it
    ;===================
    return FALSE

DI_ShutDown    ENDP
;########################################################################
; END DI_ShutDown
;########################################################################

This code un-acquires the devices that we acquired during initialization and then releases them. Then, it releases the main Direct Input object that we created. That is all that needs to be done during the shutdown process.

Direct Input is one of the easiest portions of DirectX to write code in. There are very few calls that you need to make, and most of them are really straight forward, with the exception of that "acquire" thing. The advantage is that we now have a high performance input system to use and do not have to rely on Windows messages to bring us our needed information.

With Direct Input completely out of the way we have what we need to code our menu system. But before we get to that, let's make a brief sojourn to the land of timing, and see what kind of havoc we can INVOKE.

Timing and Windoze

Timing. Without it your game will do some very weird things. From roller-coaster frame rates to objects that move too quickly, it all can be traced back to timing. Unfortunately finding good documentation on timing in Windows can be a chore. Typically you will uncover a function called GetTickCount(). Since this function deals with milliseconds it appears to be perfect for our needs. The only problem: it is very inaccurate and unreliable. So, we may dig a little further and come up with timeGetTime() in the multimedia library of Windows. This is a much better function, is more reliable, and still works with milliseconds ... which is what we need.

It kind of makes you wonder though ... if we are writing high performance games, shouldn't we have a "high performance" timer? And, indeed we do. Getting it to work nicely can be a bit of a trick. But, we will cover that in a moment.

First, what is our functionality needs? Obviously, we will want a function to initialize our timing system. We will also want a function to return the current time, and one to delay, for the amount of time that you pass in to it. Then we will want to add timer routines to control our frame rate. One to start a timer and one to wait until the specified amount of time has elapsed. That should be it.

Here is the code for the Init_Time() function.


;########################################################################
; Init_Time Procedure
;########################################################################
Init_Time    PROC    

    ;=======================================================
    ; This function will find out if we can use the HP timer
    ; and will set the needed vars to use it
    ;=======================================================

    ;=============================================
    ; Get the timer Frequency, at least try to.
    ;=============================================
    INVOKE QueryPerformanceFrequency, ADDR HPTimerVar

    .IF EAX == FALSE
        ;===================
        ; Set to use no HP
        ;====================
        MOV    UseHP, FALSE
        JMP    done

    .ENDIF

    ;========================================
    ; We can use it so set the Var and Freq
    ;========================================
    MOV    UseHP, TRUE
    MOV    EAX, HPTimerVar
    MOV    HPTimerFreq, EAX
    MOV    ECX, 1000
    XOR    EDX, EDX
    DIV    ECX
    MOV    HPTicksPerMS, EAX

done:
    ;===================
    ; We completed
    ;===================
    return TRUE

Init_Time    ENDP
;########################################################################
; END Init_Time
;########################################################################

This code performs the task of finding out if we have a high performance timer on the system to use. If not it falls back on timeGetTime() since that is guaranteed to be there. We start out by calling the function QueryPerformaceFrequency(). If this succeeds we have a high performance timer on the system and this call returns the frequency of it. If not we set the things up to not use the HP timer.

Presuming we do have one, then the code saves the timer frequency and calculates the number of ticks per millisecond. The reason for this value is the HP timer may have a resolution far smaller than 1 millisecond. So, we need to determine how many ticks of the HP timer are equal to 1 millisecond. That is all the setup procedure needs to do to get things ready.

The next function we will cover is Start_Time(). This function is used to start a timer with the variable that you pass in to it. This will be used to control our frame rate as you will see later. Here is the code ...


;########################################################################
; Start_Time Procedure
;########################################################################
Start_Time    PROC    ptr_time_var:DWORD

    ;=======================================================
    ; This function will start our timer going and store
    ; the value in  a variable that you pass it the ADDR of
    ;=======================================================

    ;========================================
    ; Are we using the Highperformance timer
    ;========================================
    .IF UseHP == TRUE
        ;==================================
        ; Yes. We are using the HP timer
        ;==================================
        INVOKE QueryPerformanceCounter, ADDR HPTimerVar
        MOV    EAX, HPTimerVar
        MOV    EBX, ptr_time_var
        MOV    DWORD PTR [EBX], EAX

    .ELSE
        ;==================================
        ; No. Use timeGetTime instead.
        ;==================================

        ;==================================
        ; Get our starting time
        ;==================================
        INVOKE timeGetTime

        ;=================================
        ; Set our variable
        ;=================================
        MOV    EBX, ptr_time_var
        MOV    DWORD PTR [EBX], EAX
    
    .ENDIF

done:
    ;===================
    ; We completed
    ;===================
    return TRUE

Start_Time    ENDP
;########################################################################
; END Start_Time
;########################################################################

The code is relatively simple. If we have access to the high performance timer we make a call to QueryPerformanceCounter(), if not we make the call to timeGetTime() instead. The only thing that may be confusing is the notation used to store the value that these functions return:


MOV    EBX, ptr_time_var
MOV    DWORD PTR [ EBX], EAX    

What this code does is move the value of ptr_time_var into the register EBX. In this case ptr_time_var is the address of a variable that was passed in to hold the starting time. Next, the code moves the value of register EAX into the address that EBX contains. In assembly that is what [XXX] means " the address of". The "DWORD PTR" will let the assembler know that we will be moving a DWORD and that [EBX] is a pointer. This will effectively store the value EAX in the location pointed to by ptr_time_var.

The final timing function that we will go over is Wait_Time(). This is the sister function for Start_Time(). And, together, they are used to control our game's frame rate. Take a peek at the code.


;########################################################################
; Wait_Time Procedure
;########################################################################
Wait_Time    PROC    time_var:DWORD, time:DWORD

    ;=========================================================
    ; This function will wait for the passed time in MS based
    ; on the distance from the passed start time. It returns 
    ; time it took the loop to complete in MS
    ;=========================================================
    
    ;========================================
    ; Are we using the Highperformance timer
    ;========================================
    .IF UseHP == TRUE
        ;==================================
        ; Yes. We are using the HP timer
        ;==================================
    
        ;==================================
        ; Adjust time for frequency
        ;==================================
        MOV    EAX, 1000
        MOV    ECX, time
        XOR    EDX, EDX
        DIV    ECX
        MOV    ECX, EAX
        MOV    EAX, HPTimerFreq
        XOR    EDX, EDX
        DIV    ECX
        MOV    time, EAX

        ;================================
        ; A push so we can pop evenly
        ;================================
        PUSH    EAX

    again1:
        ;================================
        ; Pop last time or misc push off
        ;================================
        POP    EAX

        ;======================================
        ; Get the current time
        ;======================================
        INVOKE QueryPerformanceCounter, ADDR HPTimerVar
        MOV    EAX, HPTimerVar

        ;======================================
        ; Subtract from start time
        ;======================================
        MOV    ECX, time_var
        MOV    EBX, time
        SUB    EAX, ECX

        ;======================================
        ; Save how long it took
        ;======================================
        PUSH    EAX

        ;======================================
        ; Go up and do it again if we were not
        ; yet to zero or less than the time
        ;======================================
        SUB    EAX, EBX
        JLE    again1

        ;========================================
        ; Pop the final time off of the stack
        ;========================================
        POP    EAX

        ;========================================
        ; Adjust it to MS
        ;========================================
        MOV    ECX, HPTicksPerMS
        XOR    EDX, EDX
        DIV    ECX

    .ELSE
        ;==================================
        ; No. Use timeGetTime instead.
        ;==================================

        ;================================
        ; A push so we can pop evenly
        ;================================
        PUSH    EAX

    again:
        ;================================
        ; Pop last time or misc push off
        ;================================
        POP    EAX

        ;======================================
        ; Get the current time
        ;======================================
        INVOKE timeGetTime

        ;======================================
        ; Subtract from start time
        ;======================================
        MOV    ECX, time_var
        MOV    EBX, time
        SUB    EAX, ECX

        ;======================================
        ; Save how long it took
        ;======================================
        PUSH    EAX

        ;======================================
        ; Go up and do it again if we were not
        ; yet to zero or less than the time
        ;======================================
        SUB    EAX, EBX
        JLE    again
    
        ;========================================
        ; Pop the final time off of the stack
        ;========================================
        POP    EAX

    .ENDIF

    ;=======================
    ; return from here
    ;=======================
    RET    

Wait_Time    ENDP
;########################################################################
; END Wait_Time
;########################################################################

This routine is probably the most complex of the timing routines. What it does, is wait for the passed in amount of time to elapse PAST the starting time that was passed in. In other words, if your start time was 100 and you told it to wait for 50 ... the function would not return until the current time was >= 150. It returns the elapsed time between finish and start.

Let's view this code a section at a time ...

First, if we can use the HP timer it does so. This code first converts the passed in time to the same frequency as the HP timer. It does this by dividing 1000 by the passed time. This gives it a number relative to times per second. Then it divides the frequency by that number to retrieve the equivalent amount of time needed at our HP frequency level. This is done by way of setup since it determines the "finish" time for the function. It saves this value on the stack so we can pop evenly in our loop.

Next, it enters our main compare loop. It POP's the value of elapsed time off of the stack in order to clear it. NOTE: That on the first iteration this value is not the elapsed time, but the finish time. Then we retrieve the current value of the counter. We subtract the start value from the current value we get, in order to calculate the time that has elapsed so far. We then save this "time" by pushing it onto the stack.

Now, we can subtract the wait time from the elapsed time. If the result is less than zero we haven't waited long enough so we do the above step again. Otherwise we continue on down to the next step.

Here we POP the elapsed time off of the stack and divide that by however many ticks were in a millisecond. That allows us to return a millisecond value even if we use the high performance timer. The code for if we do not have a high performance timer does the same exact thing, but it doesn't perform the conversions since the function is in milliseconds by default and can't be changed.

That is it for timing. The other two functions merely perform variations of the two routines we just covered. So, I am not going to cover those, but will leave it to you to take a look-see at them, and comprehend what they do.

Now, we can knock out our menu code.

The Menu System

A menu can be a complex thing in a game. Lucky for us it's not. The general idea is to provide some sort of selection process for features of your game. Most of the time, this will consist of drawing a bitmap and having another "selector" bitmap drawn on top, to choose an item or feature for the game. I have chosen to do this a little bit differently. Instead of having a selector, I am simply going to have certain keys correspond to the choices in the menu ... kind of like good old days of DOS, but different.

The other important point, is how you setup your system. Do you want the code to go into a "menu loop" and never return until the user makes a selection? Or, do you want to call the menu function over and over again? In the case of Windows, the second choice is 1 million times better since we need to process messages. If we had coded in the first manner, the user could hit alt-tab while we are in the menu code and we would crash. ( NOTE: The game does not have alt-tab support yet ... this will come in a later issue! ). So, we are going to setup the second type of system.

The initialization and shutdown routines are nothing that you haven't seen before. All they do is load in our two menu bitmaps. One of them is for the main menu, and one of them is for the file menu. The shutdown code simply frees their associated memory that we allocated.

The interesting code is in the Process_XXXX_Menu() functions. We will look in detail at the Process_Main_Menu() function. So, as usual, here is the code for that procedure.


;########################################################################
; Process_Main_Menu Procedure
;########################################################################
Process_Main_Menu    PROC    

    ;===========================================================
    ; This function will process the main menu for the game
    ;===========================================================

    ;=================================
    ; Local Variables
    ;=================================

    ;===================================
    ; Lock the DirectDraw back buffer
    ;===================================
    INVOKE DD_Lock_Surface, lpddsback, ADDR lPitch

    ;============================
    ; Check for an error
    ;============================
    .IF EAX == FALSE
        ;===================
        ; Jump to err
        ;===================
        JMP    err

    .ENDIF

    ;===================================
    ; Draw the bitmap onto the surface
    ;===================================
    INVOKE Draw_Bitmap, EAX, ptr_MAIN_MENU, lPitch, screen_bpp

    ;===================================
    ; Unlock the back buffer
    ;===================================
    INVOKE DD_Unlock_Surface, lpddsback

    ;============================
    ; Check for an error
    ;============================
    .IF EAX == FALSE
        ;===================
        ; Jump to err
        ;===================
        JMP    err

    .ENDIF

    ;=====================================
    ; Everything okay so flip displayed
    ; surfaces and make loading visible
    ;======================================
    INVOKE DD_Flip

    ;============================
    ; Check for an error
    ;============================
    .IF EAX == FALSE
        ;===================
        ; Jump to err
        ;===================
        JMP    err

    .ENDIF

    ;========================================================
    ; Now read the keyboard to see if they have presses
    ; any keys corresponding to our menu
    ;========================================================
    INVOKE DI_Read_Keyboard

    ;=============================
    ; Did they press a valid key
    ;=============================
    .IF keyboard_state[DIK_N]
        ;======================
        ; The new game key
        ;======================
        return    MENU_NEW

    .ELSEIF keyboard_state[DIK_G]
        ;======================
        ; The game files key
        ;======================
        return MENU_FILES

    .ELSEIF keyboard_state[DIK_R]
        ;======================
        ; Return to game key
        ;======================
        return MENU_GAME

    .ELSEIF keyboard_state[DIK_E]
        ;======================
        ; The exit game key
        ;======================
        return MENU_EXIT

    .ENDIF

done:
    ;===================
    ; We completed w/o
    ; doing anything
    ;===================
    return MENU_NOTHING

err:
    ;===================
    ; We didn't make it
    ;===================
    return MENU_ERROR

Process_Main_Menu    ENDP
;########################################################################
; END Process_Main_Menu
;########################################################################

An interesting routine, isn't it? Okay ... maybe not. But, if interesting is what you want, then I doubt you will find it in code. Unless you are really weird like I am. Anyway, what does it do?

ANSWER: It starts out by locking the back buffer and drawing our menu bitmap onto it. Then, it unlocks the back buffer and flips surfaces so that we can see it. This is boring, not to mention the fact that it is nothing new. But wait ... there, on the next line. See it? Yes, something we haven't covered!!!!

We get to call one of our Direct Input routines DI_Read_Keyboard(). If you will recall, this function gets the state of every key on the keyboard. So, when we make this call, everything is set for us to check and see which keys were pressed. We do this by just checking the key values we care about. They can be in any order you want, but be aware that the way it is coded right now, the code will only perform the code for one value ... even if they hit two valid keys. The reason is that the keys are in one huge IF-ELSE statement. So, at the first valid entry the code falls in, executes, and leaves ... forgetting all about the other keys. Thus, if you want/need to support multiple key presses make every "character check" a separate IF statement.

Now then ... we check each key that we want information on. If they key has been pressed, we return a value that corresponds to what was pressed. For example, if the user hits the 'N' key for a new game we will return the value MENU_NEW to the caller. These values are known as equates and are defined at the top of the code module in the section entitled "EQUATES." They are the equivalent of #DEFINE in C. They do nothing more than let me, as a programmer, associate a value to a string of characters for readability.

Finally, if nothing was pressed that we care about we just return a value reflecting such. The same applies to if an error occurs in the code.

This same method has been used for the Process_File_Menu() function. There are many other ways to handle menus and a little creativity will expose them to you. However, this setup is fairly straight forward, and I kind of like it. So, that is what we are using. We have now tied the Direct Input code to our menu system. All that we have left to do is tie the menu and timer code that we just wrote to our main game loop somehow.

Putting the Pieces Together

We are almost finished. Time to tie all of the little things we did together into one nice, neat package. So, we will start off with the game initialization routine.


;########################################################################
; Game_Init Procedure
;########################################################################
Game_Init    PROC

    ;=========================================================
    ; This function will setup the game
    ;=========================================================
    
    ;============================================
    ; Initialize Direct Draw -- 640, 480, bpp
    ;============================================
    INVOKE DD_Init, 640, 480, screen_bpp

    ;====================================
    ; Test for an error
    ;====================================
    .IF EAX == FALSE
        ;========================
        ; We failed so leave
        ;========================
        JMP    err
    
    .ENDIF

    ;======================================
    ; Read in the bitmap and create buffer
    ;======================================
    INVOKE Create_From_SFP, ADDR ptr_BMP_LOAD, ADDR szLoading, screen_bpp

    ;====================================
    ; Test for an error
    ;====================================
    .IF EAX == FALSE
        ;========================
        ; We failed so leave
        ;========================
        JMP    err
    
    .ENDIF

    ;===================================
    ; Lock the DirectDraw back buffer
    ;===================================
    INVOKE DD_Lock_Surface, lpddsback, ADDR lPitch

    ;============================
    ; Check for an error
    ;============================
    .IF EAX == FALSE
        ;===================
        ; Jump to err
        ;===================
        JMP    err

    .ENDIF

    ;===================================
    ; Draw the bitmap onto the surface
    ;===================================
    INVOKE Draw_Bitmap, EAX, ptr_BMP_LOAD, lPitch, screen_bpp

    ;===================================
    ; Unlock the back buffer
    ;===================================
    INVOKE DD_Unlock_Surface, lpddsback

    ;============================
    ; Check for an error
    ;============================
    .IF EAX == FALSE
        ;===================
        ; Jump to err
        ;===================
        JMP    err

    .ENDIF

    ;=====================================
    ; Everything okay so flip displayed
    ; surfaces and make loading visible
    ;======================================
    INVOKE DD_Flip

    ;============================
    ; Check for an error
    ;============================
    .IF EAX == FALSE
        ;===================
        ; Jump to err
        ;===================
        JMP    err

    .ENDIF

    ;======================================
    ; Initialize Direct Input
    ;======================================
    INVOKE DI_Init

    ;====================================
    ; Test for an error
    ;====================================
    .IF EAX == FALSE
        ;========================
        ; We failed so leave
        ;========================
        JMP    err
    
    .ENDIF

    ;========================================
    ; Initialize the timing system
    ;========================================
    INVOKE Init_Time

    ;======================================
    ; Initialize Our Menus
    ;======================================
    INVOKE Init_Menu

    ;====================================
    ; Test for an error
    ;====================================
    .IF EAX == FALSE
        ;========================
        ; We failed so leave
        ;========================
        JMP    err
    
    .ENDIF

    ;===================================
    ; Set the game state to the menu
    ; state since that is our fist stop
    ;===================================
    MOV    GameState, GS_MENU

    ;==========================
    ; Free the bitmap memory
    ;==========================
    INVOKE GlobalFree, ptr_BMP_LOAD

done:
    ;===================
    ; We completed
    ;===================
    return TRUE

err:
    ;===================
    ; We didn't make it
    ;===================
    return FALSE

Game_Init    ENDP
;########################################################################
; END Game_Init
;########################################################################

This routine has had a little bit of alteration since you last saw it. First we added some calls. One to initialize our timing system, one for our menu system, and one for our Direct Input library. Also, we delete the loading game screen at the end of the routine. This is so we do not have the memory being used by our bitmap that will never be seen again. The main thing to notice though is the addition of a global variable called GameState. This variable holds the current state of the game. At then end of the game initialization routine we set this variable to the value GS_MENU. This lets our main game loop know what state to process. There are equates for all states in the game.

That is pretty much all that has been altered for the initialization. The shutdown code has been altered to call the shutdown routines for the Direct Input module, and for the menu module. The only other changes that we had to make were in the main game loop. Actually, we didn't have to change anything since the routine was empty the last time we saw it.

Here is the new main game loop.


;########################################################################
; Game_Main Procedure
;########################################################################
Game_Main    PROC

    ;============================================================
    ; This is the heart of the game it gets called over and over
    ; and even if we process a message!
    ;============================================================

    ;=========================================
    ; Local Variables
    ;=========================================
    LOCAL    StartTime    :DWORD

    ;====================================
    ; Get the starting time for the loop
    ;====================================
    INVOKE Start_Time, ADDR StartTime

    ;==============================================================
    ; Take the proper action(s) based on the GameState variable
    ;==============================================================
    .IF GameState == GS_MENU
        ;=================================
        ; We are in the main menu state
        ;=================================
        INVOKE Process_Main_Menu
        
        ;=================================
        ; What did they want to do
        ;=================================
        .IF EAX == MENU_NOTHING
            ;=================================
            ; They didn't select anything yet
            ; so don't do  anything 
            ;=================================

        .ELSEIF EAX == MENU_ERROR
            ;==================================
            ; This is where error code would go
            ;==================================

        .ELSEIF EAX == MENU_NEW
            ;==================================
            ; They want to start a new game
            ;==================================

        .ELSEIF EAX == MENU_FILES
            ;==================================
            ; They want the file menu
            ;==================================
            MOV    GameState, GS_FILE

        .ELSEIF EAX == MENU_GAME
            ;==================================
            ; They want to return to the game
            ;==================================

        .ELSEIF EAX == MENU_EXIT
            ;==================================
            ; They want to exit the game
            ;==================================
            MOV    GameState, GS_EXIT

        .ENDIF


    .ELSEIF GameState == GS_FILE
        ;=================================
        ; We are in the file menu state
        ;=================================
        INVOKE Process_File_Menu
        
        ;=================================
        ; What did they want to do
        ;=================================
        .IF EAX == MENU_NOTHING
            ;=================================
            ; They didn't select anything yet
            ; so don't do  anything 
            ;=================================

        .ELSEIF EAX == MENU_ERROR
            ;==================================
            ; This is where error code would go
            ;==================================

        .ELSEIF EAX == MENU_LOAD
            ;==================================
            ; They want to load game
            ;==================================

        .ELSEIF EAX == MENU_SAVE
            ;==================================
            ; They want to save their game
            ;==================================


        .ELSEIF EAX == MENU_MAIN
            ;==================================
            ; They want to return to main menu
            ;==================================
            MOV    GameState, GS_MENU

        .ENDIF


    .ELSEIF GameState == GS_PLAY
        ;=================================
        ; We are in the gameplay mode
        ;=================================

    .ELSEIF GameState == GS_DIE
        ;=================================
        ; We died so perform that code
        ;=================================

    .ENDIF

    ;===================================
    ; Wait to synchronize the time
    ;===================================
    INVOKE Wait_Time, StartTime, sync_time

done:
    ;===================
    ; We completed
    ;===================
    return TRUE

err:
    ;===================
    ; We didn't make it
    ;===================
    return FALSE

Game_Main    ENDP
;########################################################################
; END Game_Main
;########################################################################

The first thing you should notice about the code is that it is wrapped in calls to Start_Time() at the top, and Wait_Time() at the bottom. These calls control our frame rate. I have it set to 25 FPS, or 40 milliseconds. Thus, 25 FPS, or thereabouts, is the fastest our game will ever run.

Next, we have one large IF-ELSE statement that selects the proper game state based on our global variable that we dedicated for that purpose. So, whether we want to run a menu, or perform the death code, it is all right there to manage it.

Inside our GS_MENU and GS_FILE states is their corresponding code. They make the calls to their menu processing function and react based upon the value that is returned to them. Nothing fancy, just simple IF-ELSE statements here also.

That is what we have setup to execute. The new game loop is nothing more than a state manager. It simply looks at what the current state is and performs the code based on that state. All games have something very, if not exactly, similar in their core. This is the heart of the game and without it you would have nothing more than inactive modules, just like we had before this section.

Until Next Time

YES! We have a really nice, clean setup that is just begging for the actual game code. You know ... the code that will let us play the game. Unfortunately, you will have to wait until the next article for that.

During the next article will be covering some more advanced material ( This will hold true for all articles as we progress from here on out – except for maybe the Direct Sound stuff ). In fact, we will hit on almost everything involved in the game play code itself next issue. Including: basic animation, "the loop", structure setup, and ... as usual ... anything else that I can think to cover.

The other thing I should mention is that this game is incomplete. I know that this is obvious, but many of you are probably wondering why there aren't any transitions, sounds, or cool FX in the game yet. The answer ... because I haven't gotten to it. Honestly, I plan to cover it all. So, for those of you who are more advanced, and think I am going way too slow, just hang in there. The good stuff is on its way ... I promise.

As always ... young grasshoppers, until next time ... happy coding.

Click here to download game.zip

Discuss this article in the forums


Date this article was posted to GameDev.net: 10/17/1999
(Note that this date does not necessarily correspond to the date the article was written)

See Also:
Win32 Assembly

© 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
Comments? Questions? Feedback? Click here!