• Create Account

\$30

Like
0Likes
Dislike

Win32 Assembly Part 5

By Chris Hobbs | Published Jan 23 2000 09:59 AM in General Programming

mov eax sound ecx invoke null
 If you find this article contains errors or problems rendering it unreadable (missing images or files, mangled code, improper text formatting, etc) please contact the editor so corrections can be made. Thank you for helping us improve this resource

Where Did We Leave Off?

Until last issue SPACE-TRIS wasn't even a game, it was merely an exercise in coding modules. Well, now we have a game that is working, it is just ugly. Sadly enough though, we are back to coding modules today. The good news is these modules will pretty things up by a large margin.

The modules that we will be covering today are Direct Sound (YUCK!) and screen transitions (YA!). The Direct Sound module isn't going to be anything to complex, just a simple sound effects manager that allows you to play, stop, delete, add, or set variables on the sounds themselves. The screen transitions module is going to consist of one main function that gets called and then a transition is selected at random to perform.

When we last saw each other, the game also needed a way to check for an "out-of-bounds" occurrence while rotating pieces. You guys were working on it, like I told you too, right? Yeah, I'm sure. Anyway, I will present my quick hack of a solution in a few moments. But first, I want to say that I am going to be glossing over many of the things we have already covered in past issues. For instance, in the Direct Sound module you guys have all seen how to check for an error, so I won't be explaining that again. This means, if you are new to the series, do yourself a favor and start at the beginning. This isn't your favorite TV series where you can just pop by, on any old day, and know what is going on. We are going to be moving at warp speed through a lot of things. All I am really going to provide now is an overview of the techniques I use ... it is up to you to understand them.

With that said, let us get right down to business with that solution to the rotation problem.

Rotation Solution

The solution to our rotation problem was fairly straightforward. Basically, we already had all of the routines that we needed. What we had to know was if, at any given frame, the current piece would be out of bounds -- which MoveShape() does for us. So, the fix we have is a simple one. We can just call that routine with the frame it "would" be on next, right? Wrong. That is what I tried at first because it makes sense. But, there is a hidden problem with that method.

The problem lies in that fact that any piece could already be out of bounds when you adjust the frame. Move_Shape() only tells you if you can move the shape to the left or right, and does so if it can. If we fake our next frame for that call it may succeed because it is already out of bounds by one column if it was on the edges previously. This means we need a way to prevent it from ever being out of bounds to begin with.

The solution is to move it in towards the center by one column beforehand. Then, when we make the call, the shape is guaranteed to be on the edge, or in the middle, never outside the grid. The way we decide if we could go to the next frame is by seeing if the X coordinate sent before we made the call matches the one we have after the call. If it does, then that means the shape can be rotated ... if they don't match then the shape can not be rotated.

This method has the advantage of eliminating the need for any other code. The Move_Shape() function will not succeed if something else is blocking its move. Therefore, we do not need to do any other tests on the shape to see if other blocks are in the way. Just that simple call based on the next frame. So, we not only solved the problem ... but also made the routine shorter in the process.

The new Rotate_Shape()

;########################################################################
; Rotate_Shape Procedure
;########################################################################
Rotate_Shape	PROC

;=======================================================
; This function will rotate the current shape it tests
; to make sure there are no blocks already in the place
; where it would rotate.
;=======================================================

;================================
; Local Variables
;================================
LOCAL	Index:		DWORD
LOCAL	CurBlock:	DWORD
LOCAL	Spot:		BYTE

;=================================
; Make sure they are low enough
;=================================
.IF CurShapeY > 21
JMP	err
.ENDIF

;================================
; Are they at the last frame?
;================================
.IF CurShapeFrame == 3
;=====================================
; Yep ... make sure they can rotate
;=====================================

;========================================
; We will start by seeing which half of
; the grid they are currently on that way
; we know we much too move the shape
;=========================================
.IF CurShapeX < 6
;===========================
; They are on the left half
; of the grid
;===========================

;=============================
; So start by moving them one
; coord right and saving the
; old coordinat
;=============================
PUSH	CurShapeX
INC	CurShapeX

;=============================
; Now adjust the frame to what
; it would be
;=============================
MOV	CurShapeFrame, 0

;=============================
; Try to move them to the left
;=============================
INVOKE Move_Shape, MOVE_LEFT

;=============================
; If we succeeded then the old
; X will be equal to the new
; X coordinate
;=============================
MOV	EAX, CurShapeX
POP	CurShapeX
.IF	EAX == CurShapeX
JMP	done
.ELSE
;================
; Can't rotate
;================
MOV	CurShapeFrame, 3
JMP	err

.ENDIF

.ELSE
;===========================
; They are on the right half
; of the grid
;===========================

;=============================
; So start by moving them one
; coord left and saving the
; old coordinat
;=============================
PUSH	CurShapeX
DEC	CurShapeX

;=============================
; Now adjust the frame to what
; it would be
;=============================
MOV	CurShapeFrame, 0

;=============================
; Try & move them to the right
;=============================
INVOKE Move_Shape, MOVE_RIGHT

;=============================
; If we succeeded then the old
; X will be equal to the new
; X coordinate
;=============================
MOV	EAX, CurShapeX
POP	CurShapeX
.IF	EAX == CurShapeX
;================
; Can rotate
;================
JMP	done

.ELSE
;================
; Can't rotate
;================
MOV	CurShapeFrame, 3
JMP	err

.ENDIF

.ENDIF

.ELSE
;=====================================
; NO ... make sure they can rotate
;=====================================

;========================================
; We will start by seeing which half of
; the grid they are currently on that way
; we know we much too move the shape
;=========================================
.IF CurShapeX < 6
;===========================
; They are on the left half
; of the grid
;===========================

;=============================
; So start by moving them one
; coord right and saving the
; old coordinat
;=============================
PUSH	CurShapeX
INC	CurShapeX

;=============================
; Now adjust the frame to what
; it would be
;=============================
INC	CurShapeFrame

;=============================
; Try to move them to the left
;=============================
INVOKE Move_Shape, MOVE_LEFT

;=============================
; If we succeeded then the old
; X will be equal to the new
; X coordinate
;=============================
MOV	EAX, CurShapeX
POP	CurShapeX
.IF	EAX == CurShapeX
;================
; Can rotate
;================
JMP	done

.ELSE
;================
; Can't rotate
;================
DEC	CurShapeFrame
JMP	err

.ENDIF

.ELSE
;===========================
; They are on the right half
; of the grid
;===========================

;=============================
; So start by moving them one
; coord left and saving the
; old coordinat
;=============================
PUSH	CurShapeX
DEC	CurShapeX

;=============================
; Now adjust the frame to what
; it would be
;=============================
INC	CurShapeFrame

;=============================
; Try & move them to the right
;=============================
INVOKE Move_Shape, MOVE_RIGHT

;=============================
; If we succeeded then the old
; X will be equal to the new
; X coordinate
;=============================
MOV	EAX, CurShapeX
POP	CurShapeX
.IF	EAX == CurShapeX
;================
; Can rotate
;================
JMP	done

.ELSE
;================
; Can't rotate
;================
DEC	CurShapeFrame
JMP	err

.ENDIF

.ENDIF

.ENDIF

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

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

Rotate_Shape	ENDP
;########################################################################
; END Rotate_Shape
;########################################################################

The Sound Module

The sound module for this game is pretty simple. It merely presents an interface to Load WAV files, play the sounds, delete them, and edit properties about them. However, there are a few tricky things to watch out for in the module.

The first thing I want to illustrate is how to create an array of structures. Take a look at the following modified code snippet.

;#################################################################################
;#################################################################################
; STRUCTURES
;#################################################################################
;#################################################################################

;=============================
; this holds a single sound
;=============================
pcm_sound	STRUCT
dsbuffer	DD 0	; the ds buffer for the sound
state		DD 0	; state of the sound
rate		DD 0	; playback rate
lsize		DD 0	; size of sound
pcm_sound	ENDS

;============================
; max number of sounds in
; the game at once
;============================
MAX_SOUNDS	EQU	16

;=========================================
; Our array of sound effects
;=========================================
sound_fx	pcm_sound	MAX_SOUNDS dup(<0,0,0,0>)
You will notice that anytime we declare a structure we need to use angle brackets, or curly braces (not shown), for them. The numbers inside consist of the members of your structure and nothing more. Whatever you place there is what things get initialized to. Also, pay attention to how the structure is defined. It consists of normal variable declarations in between a couple of keywords and a tag to give it a name.

Of special note, is that you must use another set of braces, or brackets, if you wish to have nested structures. The way we get an array with a structure is the same as any other variable. We use the number we want followed by the DUP ... then, in parenthesis, what you want the values initialized as.

We are going to skip over the DS_Init() and DS_Shutdown() procedures, since they do the same exact things as the other DX counterparts. Instead let's take a peek at Play_Sound().

;########################################################################
; Play_Sound Procedure
;########################################################################
Play_Sound PROC	id:DWORD, flags:DWORD

;=======================================================
; This function will play the sound contained in the
; id passed in along with the flags which can be either
; NULL or DSBPLAY_LOOPING
;=======================================================

;==============================
; Make sure this buffer exists
;==============================
MOV	EAX, sizeof(pcm_sound)
MOV	ECX, id
MUL	ECX
MOV	ECX, EAX
.IF sound_fx[ECX].dsbuffer != NULL
;=================================
; We exists so reset the position
; to the start of the sound
;=================================
PUSH	ECX
DSBINVOKE SetCurrentPosition, sound_fx[ECX].dsbuffer, 0
POP	ECX

;======================
; Did the call fail?
;======================
.IF EAX != DS_OK
;=======================
; Nope, didn't make it
;=======================
JMP	err

.ENDIF

;==============================
; Now, we can play the sound
;==============================
DSBINVOKE Play, sound_fx[ECX].dsbuffer, 0, 0, flags

;======================
; Did the call fail?
;======================
.IF EAX != DS_OK
;=======================
; Nope, didn't make it
;=======================
JMP	err

.ENDIF

.ELSE
;======================
; No buffer for sound
;======================
JMP	err

.ENDIF

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

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

Play_Sound	ENDP
;########################################################################
; END Play_Sound
;########################################################################
This is the routine that we use to start a sound playing. You can pass it flags to alter how it sounds. So far as I know there are only 2 options for the flags. If you pass in NULL then it plays the sound once. If you pass in DSBPLAY_LOOPING it will play the sound repeatedly.

The routine begins by checking that the sound has a valid buffer associated with it. If so, it sets the position of that sound to the beginning and then makes a call to begin playing it with whatever flags were passed in.

The only thing worth illustrating in this routine is how the structure element is referenced. To begin with we obtain the size of the structure and multiply that by the id of the sound to give us our position in the array. Then, in order to reference a member you treat it just like you would in C/C++ ... StructName[position].member ... the important thing is not to forget to multiply the element by the size of the structure.

The next 3 routines allow you to set the volume, frequency, and pan of a sound. There is nothing to these routines ... they are just wrappers for the Direct Sound function calls. However, if you want to use anything but Set_Sound_Volume() you need to tell Direct Sound that you want those enabled when you load the sound. This is done by passing in DSBCAPS_CTRL_PAN or DSBCAPS_CTRLFREQ respectively. If you do not specify these flags when you load your sound you will not be able to manipulate those items.

The next two functions are for stopping sounds from playing. One will stop the specific sound you pass in and the other will stop all of the sounds from playing. Here is the code if you want to take a peek. Once again these are merely wrapper functions to shield you from the DX headache.

;########################################################################
; Stop_Sound Procedure
;########################################################################
Stop_Sound PROC	id:DWORD

;=======================================================
; This function will stop the passed in sound from
; playing and will reset it's position
;=======================================================

;==============================
; Make sure the sound exists
;==============================
MOV	EAX, sizeof(pcm_sound)
MOV	ECX, id
MUL	ECX
MOV	ECX, EAX
.IF sound_fx[ECX].dsbuffer != NULL
;==================================
; We exist so stop the sound
;==================================
PUSH	ECX
DSBINVOKE Stop, sound_fx[ECX].dsbuffer
POP	ECX

;=================================
; Now reset the sound position
;=================================
DSBINVOKE SetCurrentPosition, sound_fx[ECX].dsbuffer, 0

.ENDIF

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

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

Stop_Sound	ENDP
;########################################################################
; END Stop_Sound
;########################################################################

;########################################################################
; Stop_All_Sounds Procedure
;########################################################################
Stop_All_Sounds PROC

;=======================================================
; This function will stop all sounds from playing
;=======================================================

;==============================
; Local Variables
;==============================
LOCAL	index	:DWORD

;==============================
; Loop through all sounds
;==============================
MOV	index, 0
.WHILE index < MAX_SOUNDS
;==================================
; Stop this sound from playing
;==================================
INVOKE Stop_Sound, index

;================
; Inc the counter
;================
INC	index

.ENDW

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

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

Stop_All_Sounds	ENDP
;########################################################################
; END Stop_All_Sounds
;########################################################################
Delete_Sound() and Delete_All_Sounds() are remarkably similar to the sound stopping functions. The only difference is you make a different function call to DX. Delete_Sound() will call Stop_Sound() first, to make sure the sound isn't trying to be played while you are trying to delete it. The interesting thing about these two functions is that you do not personally have to release any of your sounds if you don't want to. During shutdown of Direct Sound all the sounds that you loaded will be deleted. However, if you have reached your maximum in sounds, and want to free one up, you will need to manually delete it.

The next function Status_Sound() is yet another wrapper routine. It is used when you need to find out if a sound is still playing, or if it has stopped already. You will see this function put to use later on.

There, 90% of that stupid module is out of the way. Now, we need to move on to the final 10% of that code ... the Load_WAV() procedure.

Loading file formats is always a pain to do. Loading a WAV file proves to be no different. It is a long function, that probably could have been broken up a little bit better, but for now it will have to do. It works and that is all I am concerned about. So, have a peek at it.

;########################################################################
;########################################################################

;=======================================================
; This function will load the passed in WAV file
; it returns the id of the sound, or -1 if failed
;=======================================================

;==============================
; Local Variables
;==============================
LOCAL	sound_id	:DWORD
LOCAL	index		:DWORD

;=================================
; Init the sound_id to -1
;=================================
MOV	sound_id, -1

;=================================
; First we need to make sure there
; is an open id for our new sound
;=================================
MOV	index, 0
.WHILE index < MAX_SOUNDS
;========================
; Is this sound empty??
;========================
MOV	EAX, sizeof(pcm_sound)
MOV	ECX, index
MUL	ECX
MOV	ECX, EAX
.IF sound_fx[ECX].state == SOUND_NULL
;===========================
; We have found one, so set
; the id and leave our loop
;===========================
MOV	EAX, index
MOV	sound_id, EAX
.BREAK

.ENDIF

;================
; Inc the counter
;================
INC	index

.ENDW

;======================================
; Make sure we have a valid id now
;======================================
.IF sound_id == -1
;======================
; Give err msg
;======================
INVOKE MessageBox, hMainWnd, ADDR szNoID, NULL, MB_OK

;======================
; Jump and return out
;======================
JMP	err

.ENDIF

;=========================
; Setup the parent "chunk"
; info structure
;=========================
MOV	parent.ckid, 0
MOV	parent.ckSize, 0
MOV	parent.fccType, 0
MOV	parent.dwDataOffset, 0
MOV	parent.dwFlags, 0

;============================
; Do the same with the child
;============================
MOV	child.ckid, 0
MOV	child.ckSize, 0
MOV	child.fccType, 0
MOV	child.dwDataOffset, 0
MOV	child.dwFlags, 0

;======================================
; Now open the WAV file using the MMIO
; API function
;======================================
INVOKE mmioOpen, fname_ptr, NULL, (MMIO_READ OR MMIO_ALLOCBUF)
MOV	hwav, EAX

;====================================
; Make sure the call was successful
;====================================
.IF EAX == NULL
;======================
; Give err msg
;======================
INVOKE MessageBox, hMainWnd, ADDR szNoOp, NULL, MB_OK

;======================
; Jump and return out
;======================
JMP	err

.ENDIF

;===============================
; Set the type in the parent
;===============================
mmioFOURCC 'W', 'A', 'V', 'E'
MOV	parent.fccType, EAX

;=================================
; Descend into the RIFF
;=================================
INVOKE mmioDescend, hwav, ADDR parent, NULL, MMIO_FINDRIFF
.IF EAX != NULL
;===================
; Close the file
;===================
INVOKE mmioClose, hwav, NULL

;=====================
; Jump and return out
;=====================
JMP	err

.ENDIF

;============================
; Set the child id to format
;============================
mmioFOURCC 'f', 'm', 't', ' '
MOV	child.ckid, EAX

;=================================
; Descend into the WAVE format
;=================================
.IF EAX != NULL
;===================
; Close the file
;===================
INVOKE mmioClose, hwav, NULL

;=====================
; Jump and return out
;=====================
JMP	err

.ENDIF

;=================================
; Now read the wave format info in
;=================================
MOV	EBX, sizeof(WAVEFORMATEX)
.IF EAX != EBX
;===================
; Close the file
;===================
INVOKE mmioClose, hwav, NULL

;=====================
; Jump and return out
;=====================
JMP	err

.ENDIF

;=================================
; Make sure the data format is PCM
;=================================
.IF wfmtx.wFormatTag != WAVE_FORMAT_PCM
;===================
; Close the file
;===================
INVOKE mmioClose, hwav, NULL

;=====================
; Jump and return out
;=====================
JMP	err

.ENDIF

;=================================
; Ascend up one level
;=================================
INVOKE mmioAscend, hwav, ADDR child, NULL
.IF EAX != NULL
;===================
; Close the file
;===================
INVOKE mmioClose, hwav, NULL

;=====================
; Jump and return out
;=====================
JMP	err

.ENDIF

;============================
; Set the child id to data
;============================
mmioFOURCC 'd', 'a', 't', 'a'
MOV	child.ckid, EAX

;=================================
; Descend into the data chunk
;=================================
.IF EAX != NULL
;===================
; Close the file
;===================
INVOKE mmioClose, hwav, NULL

;=====================
; Jump and return out
;=====================
JMP	err

.ENDIF

;===================================
; Now allocate memory for the sound
;===================================
INVOKE GlobalAlloc, GMEM_FIXED, child.ckSize
MOV	snd_buffer, EAX
.IF EAX == NULL
;===================
; Close the file
;===================
INVOKE mmioClose, hwav, NULL

;=====================
; Jump and return out
;=====================
JMP	err

.ENDIF

;=======================================
; Read the WAV data and close the file
;=======================================
INVOKE mmioClose, hwav, 0

;================================
; Set the rate, size, & state
;================================
MOV	EAX, sizeof(pcm_sound)
MOV	ECX, sound_id
MUL	ECX
MOV	ECX, EAX
MOV	EAX, wfmtx.nSamplesPerSec
MOV	sound_fx[ECX].rate, EAX
MOV	EAX, child.ckSize
MOV	sound_fx[ECX].lsize, EAX

;==========================
; Clear the format struc
;==========================
INVOKE RtlFillMemory, ADDR pcmwf, sizeof(WAVEFORMATEX), 0

;=============================
; Now fill our desired fields
;=============================
MOV	pcmwf.wFormatTag, WAVE_FORMAT_PCM
MOV	AX, wfmtx.nChannels
MOV	pcmwf.nChannels, AX
MOV	EAX, wfmtx.nSamplesPerSec
MOV	pcmwf.nSamplesPerSec, EAX
XOR	EAX, EAX
MOV	AX, wfmtx.nBlockAlign
MOV	pcmwf.nBlockAlign, AX
MOV	EAX, pcmwf.nSamplesPerSec
XOR	ECX, ECX
MOV	CX, pcmwf.nBlockAlign
MUL	ECX
MOV	pcmwf.nAvgBytesPerSec, EAX
MOV	AX, wfmtx.wBitsPerSample
MOV	pcmwf.wBitsPerSample, AX
MOV	pcmwf.cbSize, 0

;=================================
; Prepare to create the DS buffer
;=================================
MOV	dsbd.dwSize, sizeof(DSBUFFERDESC)
; Put other flags you want to play with in here such
; as CTRL_PAN, CTRL_FREQ, etc or pass them in
MOV	EAX, flags
MOV	dsbd.dwFlags, EAX
OR	dsbd.dwFlags, DSBCAPS_STATIC OR DSBCAPS_CTRLVOLUME \
OR DSBCAPS_LOCSOFTWARE
MOV	EBX, child.ckSize
MOV	EAX, OFFSET pcmwf
MOV	dsbd.dwBufferBytes, EBX
MOV	dsbd.lpwfxFormat, EAX

;=================================
; Create the sound buffer
;=================================
MOV	EAX, sizeof(pcm_sound)
MOV	ECX, sound_id
MUL	ECX
LEA	ECX, sound_fx[EAX].dsbuffer
DSINVOKE CreateSoundBuffer, lpds, ADDR dsbd, ECX, NULL
.IF EAX != DS_OK
;===================
; Free the buffer
;===================
INVOKE GlobalFree, snd_buffer

;=====================
; Jump and return out
;=====================
JMP	err

.ENDIF

;==================================
; Lock the buffer so we can copy
; our sound data into it
;==================================
MOV	EAX, sizeof(pcm_sound)
MOV	ECX, sound_id
MUL	ECX
MOV	ECX, EAX
DSBINVOKE mLock, sound_fx[ECX].dsbuffer, NULL, child.ckSize, ADDR audio_ptr_1,\
DSBLOCK_FROMWRITECURSOR
.IF EAX != DS_OK
;===================
; Free the buffer
;===================
INVOKE GlobalFree, snd_buffer

;=====================
; Jump and return out
;=====================
JMP	err

.ENDIF

;==============================
; Copy first section of buffer
; then the second section
;==============================
; First buffer
MOV	ESI, snd_buffer
MOV	EDI, audio_ptr_1
MOV	ECX, audio_length_1
AND	ECX, 3
REP	movsb
MOV	ECX, audio_length_1
SHR	ECX, 2
REP	movsd

; Second buffer
MOV	ESI, snd_buffer
MOV	EDI, audio_ptr_2
MOV	ECX, audio_length_2
AND	ECX, 3
REP	movsd
MOV	ECX, audio_length_2
SHR	ECX, 2
REP	movsd

;==============================
; Unlock the buffer
;==============================
MOV	EAX, sizeof(pcm_sound)
MOV	ECX, sound_id
MUL	ECX
MOV	ECX, EAX
DSBINVOKE Unlock, sound_fx[ECX].dsbuffer, audio_ptr_1, audio_length_1,\
audio_ptr_2, audio_length_2
.IF EAX != DS_OK
;===================
; Free the buffer
;===================
INVOKE GlobalFree, snd_buffer

;=====================
; Jump and return out
;=====================
JMP	err

.ENDIF

;===================
; Free the buffer
;===================
INVOKE GlobalFree, snd_buffer

done:
;===================
; We completed
;===================
return sound_id

err:
;===================
; We didn't make it
;===================
return -1

;########################################################################
;########################################################################
The code is fairly simple it is just long. I will skim over the first few parts since they are just setting things up. The code starts out by finding the first available sound in our array. If it finds none, it issues an error and then returns to the caller. Once we have a valid sound id to hold our new sound we can start playing with the file, and setting the structures up for use.

We start by initializing the structures to 0 to make sure we don't have any left over remnants from previous loads. Once that is complete, we get to open up our WAV file using the multimedia I/O functions found in the Winmm.lib file.

Once the file is opened successfully we descend into the internals of the file. This merely takes us to relevant sections in the header so we can setup our structures for loading. A few sections need to be traversed and then we are ready to get the wave format information.

With our wave information intact we can ascend up the file and then down into our data chunk. Once "inside" we allocate memory for our data and then we grab it with the mmioRead() function. Finally, we can close the file ... and the ugly part is over.

Then, we do some more setting of values in structures and clearing things out. All stuff you have seen before, so it should look familiar by now. We are getting ready to create the sound buffer with all these assignments.

Normally I would just say "here is where we create the sound buffer" ... but there is something very weird going on here. If you notice we aren't able to pass in the sound buffer parameter. The reason is that we need to pass in the address. So, the line right before the call uses the LEA (Load Effective Address) instruction to obtain the address of our variable. The reason for this is just a quirk on the INVOKE syntax and something we need to workaround. By loading the address before the call, we can place it in the modified invoke statement without troubles. Another small thing you might want to jot down is that we can't use EAX to hold that value. The reason is that the macro I defined, DSBINVOKE, uses EAX when manipulating things ... this is the only reason, normally you could use it without trouble. Never forget, macros are placed directly into your code ... even if they don't make sense.

Once we have our buffer created we lock the buffer, copy the memory over to the new buffer locations, and then unlock the buffer. The only thing that might seem a little confusing is the method I have chosen to copy the sound data over. Remember how we copied using DWORD's in our Draw_Bitmap() routine? If not, go back and refresh your memory because it is very important. For those of you that do recall ... that is almost exactly what we are doing here.

The only thing that is different, is we have to make sure that our data is on a 4-byte boundary. We do this by AND'ing the length with 3 ... this is the same as Number mod 4 ... then moving byte by byte until we hit zero. At that point we are on a 4-byte boundary and can move DWORD's once again.

It is the same basic concept that we have seen before only this time we have to do the checking for alignment ourselves since the data is not guaranteed to be on even 4-byte boundaries.

Once all of that is out of the way we can free the buffer, along with our headache, and we are finished. The sound is now loaded, in the buffer, and we can return the sound's ID to the caller so that they can play the sound later on. One thing I do want to mention is that this WAV loader should be able to load WAV files of any format (8-bit, 16-bit, stereo, mono, etc.). Yet, only 8-bit sounds can be utilized in the DirectSound buffers. The reason is we only set the cooperative level to normal, instead of exclusive. So, if you want to load in, and play, 16-bit sounds you will need to alter the DS_Init() procedure, and put DirectSound into exclusive mode.

With that, our sound module is complete. It is definitely not "state-of-the-art" ... but hey, it works and it removes a lot of the DirectX burden that would normally be placed on us.

Luckily though, we get to talk about something a lot more fun: Screen Transitions.

Screen Transitions

Screen Transitions are things that are usually fun to write. Of course, most anything would be fun after playing with DirectSound. The screen transition is often one of the most important things in a game. If you have a lot of places where the view/screen completely changes, then a transition is typically needed in order to smooth things out. You do not want the user to just be "jarred" to the next scene. To the user, a transition is like riding in a Lexus, while having none is like riding an old Pan-head << inside motorcycle joke >>.

I have taken an interesting approach with the screen transitions in this game. I decided there would be one main interface function. This function, intelligently called Transition(), is responsible for selecting a screen transition, at random, and calling it. This provides some break from the monotony of calling the same one over and over again. Of course, I have only provided one simple transition ( with 2 options ), it is your job to write more. All transitions require the surface you want to transition "from" on the primary buffer, and the surface you want to transition "to" on the back buffer.

Here is the code for the interface function:

;########################################################################
; Transition Procedure
;########################################################################
Transition	PROC

;=======================================================
; This function will call one of the our transitions
; based on a random number. All transitions require
; the primary buffer to be the surface that you want
; to transition from and the back buffer to be the
; surface you want to transition to. Both need to
; be unlocked.
;=======================================================

;=============================
; Get a random number
;=============================
INVOKE Get_Time

;=============================
; Mod the result with 2
;=============================
AND	EAX, 1

;=============================
; Select the transition based
; on our number
;=============================
.IF EAX == 0
;==========================
; Perform a Horizontal Wipe
;==========================
INVOKE Wipe_Trans, 6, WIPE_HORZ

;=========================
; Universal error check
;=========================
.IF EAX == FALSE
JMP	err
.ENDIF

.ELSEIF EAX == 1
;==========================
; Perform a Vertical Wipe
;==========================
INVOKE Wipe_Trans, 4, WIPE_VERT

;=========================
; Universal error check
;=========================
.IF EAX == FALSE
JMP	err
.ENDIF

.ENDIF

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

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

Transition	ENDP
;########################################################################
; END Transition
;########################################################################
The Transition() function grabs a random number by using the same method as our random shape generator, obtaining the time. This is not the optimum method, and we will be replacing it with a true random number generator later on. But, for now, it will have to do. The proper transition is then made based on the time, you can play with the parameters if you wish. I just selected a couple that didn't seem to take away too much of the screen each iteration.

That is all that is there for the management function. It just keeps things random. You can still call a transition directly, I just thought it was more interesting to do it like this. Besides, on a large project, after 4-6 months of looking at the same transitions, you would probably be insane.

Now, we can look at the actual screen transition: Wipe_Trans(). This routine allows us to perform either a vertical (top to bottom) or horizontal (left to right) transition taking away a width that is passed in each time. So, have a look at the code before we continue.

;########################################################################
; Wipe_Trans Procedure
;########################################################################
Wipe_Trans    PROC    strip_width:DWORD, direction:DWORD

;=======================================================
; This function will perform either a horizontal or
; a vertical wipe depending on what you pass in for the
; direction paramter. The width of each step is
; determined by the width that you pass in to it.
;=======================================================

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

;========================================
; Setup the source rectangle and the
; destination rectangle
;
; For the first iteration, the strip may
; not be the height passed in. This is to
; make sure we are on an even boundary
; during the loop below
;========================================
.IF direction == WIPE_HORZ
MOV    SrcRect.top, 0
MOV    SrcRect.left, 0
MOV    EAX, app_width
MOV    ECX, strip_width
XOR    EDX, EDX
DIV    ECX
.IF EDX == 0
MOV    EDX, strip_width
.ENDIF
MOV    EBX, app_height
MOV    SrcRect.bottom, EBX
MOV    SrcRect.right, EDX
MOV    DestRect.top, 0
MOV    DestRect.left, 0
MOV    DestRect.bottom, EBX
MOV    DestRect.right, EDX

.ELSEIF direction == WIPE_VERT
MOV    SrcRect.top, 0
MOV    SrcRect.left, 0
MOV    EAX, app_height
MOV    ECX, strip_width
XOR    EDX, EDX
DIV    ECX
MOV    EAX, app_width
.IF EDX == 0
MOV    EDX, strip_width
.ENDIF
MOV    SrcRect.bottom, EDX
MOV    SrcRect.right, EAX
MOV    DestRect.top, 0
MOV    DestRect.left, 0
MOV    DestRect.bottom, EDX
MOV    DestRect.right, EAX

.ELSE
;==================
; Invalid direction
;==================
JMP    err

.ENDIF

;================================
; Get the starting time
;================================

;================================
; Blit the strip onto the screen
;================================
DDS4INVOKE BltFast, lpddsprimary, SrcRect.left, SrcRect.top,\

;===============================
; Make sure we succeeded
;===============================
.IF EAX != DD_OK
JMP    err
.ENDIF

;===================================================
; Now adjust the distance between the left &
; right, or top and bottom, so that the top, or
; left, corner is where the right hand side was
; at ... and the bottom, or right, is strip_width
; away from the opposite corner.
;===================================================
MOV    EAX, strip_width
.IF direction == WIPE_HORZ
MOV    EBX, SrcRect.right
MOV    SrcRect.left, EBX
MOV    DestRect.left, EBX
MOV    DestRect.right, EBX
MOV    SrcRect.right, EBX

.ELSEIF direction == WIPE_VERT
MOV    EBX, SrcRect.bottom
MOV    SrcRect.top, EBX
MOV    DestRect.top, EBX
MOV    DestRect.bottom, EBX
MOV    SrcRect.bottom, EBX

.ENDIF

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

;=====================================
; Drop into a while loop and blit all
; of the strips synching to our
; desired transition rate
;=====================================
.WHILE TRUE
;================================
; Get the starting time
;================================

;================================
; Blit the strip onto the screen
;================================
DDS4INVOKE BltFast, lpddsprimary, SrcRect.left, SrcRect.top,\

;===============================
; Make sure we succeeded
;===============================
.IF EAX != DD_OK
JMP    err
.ENDIF

;==================================
; Have we reached our extents yet
;==================================
MOV    EAX, SrcRect.bottom
MOV    EBX, app_height
MOV    ECX, SrcRect.right
MOV    EDX, app_width
.IF EAX == EBX && ECX == EDX
;======================
; Trans complete
;======================
.BREAK

.ELSE
;======================
;======================
MOV    EAX, strip_width
.IF direction == WIPE_HORZ

.ELSEIF direction == WIPE_VERT

.ENDIF

.ENDIF

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

.ENDW

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

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

Wipe_Trans    ENDP
;########################################################################
; END Wipe_Trans
;########################################################################
Notice, the first thing we do is setup the source and destination rectangles. We are going to be working with "strips" of the bitmap. So, I am going to walk you through exactly what happens when the routine is called.

Pretend the user passed in a 7 for the "strip_width" parameter and they want a horizontal transition. The first section finds out if the strip's width can go evenly into the screen width. If the strip can go in evenly, then it sets the length to be equal to the width. However, if it can't, then the remainder is placed in the width. The reason we place the remainder in, is that the strip is going to have that little strip left over when we finish. Example: with a 7 and a screen width of 640 you will have 91 sections of 7 and a section of 3 left over. So, for the first strip we would store a 3 for the width. From here on note: You would do the exact same thing, except for the height/top/bottom, if you were doing a vertical wipe.

Next, we blit that small 3 pixel strip over from the back buffer onto the primary buffer. With that out of the way we can get setup to do blits with a 7-pixel width. The way we setup is by moving the right-hand side of the rectangle over to the left-hand side. Then, we add the strip_width, in this case 7, to the left-hand side to obtain the new right-hand side. So, for our example, the left coordinate of the rectangles would now have a 3, and the right coordinate would now have a 10. We need this adjustment since our loop is only going to work with 7-pixel strips in the bitmap, instead of an increasing portion of the bitmap.

We are now ready to delve into our loop. The first thing we do, aside from getting the starting time, is blit the current strip (this is why we had to setup the rectangles out of the loop). Then, we check the right hand side and bottom of our source rectangle is still inside the limits of our screen. If it has met the extents, then we break from the loop since we are finished. If we haven't yet reached the edges, then we adjust the rectangles. To adjust the rectangles, we add the strip_width to both the left and right of our source and destination rectangles. By adding to both sides we are able to blit in strips of 7-pixels. But, if we only added to the right-hand side we would blit in pixels of 7, 14, 21, etc. Which, needless to say, is much, much slower than the way we are doing it. Finally, we synchronize the time to our desired rate and keep doing the loop until we are finished.

There isn't very much to the routine, but it should give you a starting point in making screen transitions. Here are some suggestions in case you are lacking in creativity. Make a modified wipe that would have a bunch of strips at intervals grow to meet each other, like something you would see with a pair of blinds in your house. Design a transition that zooms in to a single pixel, then zooms out to the new picture. Create a circular wipe, or even a spiral one. There are many good articles out there on demo effects and I suggest reading some of them if you find this stuff interesting. Finally, if you are really desperate for an idea, just go and play a game and see how their transitions work. Mimicry is one of the first steps in learning.

At any rate, everything in our modules is complete. We now have everything that we need to pretty up the game. So, in the next section, we will tie everything into that nice little bow, just as we always do.

Putting More Pieces Together

The title to this section is really accurate. Most programming, at least in some way, is like a jigsaw puzzle. It is about combining pieces in a manner that works best. Often times, you will obtain a completely different result just be re-ordering some of the steps. In this sense, programming is intellectually stimulating. There are so many millions of ways to accomplish any given task. Keep that in mind while reviewing the code I provide. It isn't written in blood anyplace that you have to do things a certain way -- at least, I don't think it is.

The module, we are going to look at for the changes is the Menu module. The reason we are using this module is that it makes use of all of our new features, which makes it perfect for use as an example.

You had better take a look at the code for the new module before we go any further.

[code=auto:0];###########################################################################
;###########################################################################
;
; This code module contains all of the functions that relate to
; the menu that we use.
;
; There are routines for each menu we will have. One for the main
;
; NOTE: We could have combined these two functions into one generic
; function that used parameters to determine the bahavior. But, by coding
; it explicitly we get a better idea for what is going on in the code.
;
;###########################################################################
;###########################################################################

;###########################################################################
;###########################################################################
; THE COMPILER OPTIONS
;###########################################################################
;###########################################################################

.386
.MODEL flat, stdcall
OPTION CASEMAP :none ; case sensitive

;###########################################################################
;###########################################################################
; THE INCLUDES SECTION
;###########################################################################
;###########################################################################

;================================================
; These are the Inlcude files for Window stuff
;================================================
INCLUDE \masm32\include\windows.inc
INCLUDE \masm32\include\comctl32.inc
INCLUDE \masm32\include\comdlg32.inc
INCLUDE \masm32\include\shell32.inc
INCLUDE \masm32\include\user32.inc
INCLUDE \masm32\include\kernel32.inc
INCLUDE \masm32\include\gdi32.inc

;===============================================
; The Lib's for those included files
;================================================
INCLUDELIB \masm32\lib\comctl32.lib
INCLUDELIB \masm32\lib\comdlg32.lib
INCLUDELIB \masm32\lib\shell32.lib
INCLUDELIB \masm32\lib\gdi32.lib
INCLUDELIB \masm32\lib\user32.lib
INCLUDELIB \masm32\lib\kernel32.lib

;====================================
; The Direct Draw include file
;====================================
INCLUDE Includes\DDraw.inc

;====================================
; The Direct Input include file
;====================================
INCLUDE Includes\DInput.inc

;====================================
; The Direct Sound include file
;====================================
INCLUDE Includes\DSound.inc

;=================================================
; Include the file that has our protos
;=================================================
INCLUDE Protos.inc

;###########################################################################
;###########################################################################
; LOCAL MACROS
;###########################################################################
;###########################################################################

m2m MACRO M1, M2
PUSH M2
POP M1
ENDM

return MACRO arg
MOV EAX, arg
RET
ENDM

;#################################################################################
;#################################################################################
; Variables we want to use in other modules
;#################################################################################
;#################################################################################

;#################################################################################
;#################################################################################
; External variables
;#################################################################################
;#################################################################################

;=================================
; The DirectDraw stuff
;=================================
EXTERN lpddsprimary :LPDIRECTDRAWSURFACE4
EXTERN lpddsback :LPDIRECTDRAWSURFACE4

;=========================================
; The Input Device state variables
;=========================================
EXTERN keyboard_state :BYTE

;#################################################################################
;#################################################################################
; BEGIN INITIALIZED DATA
;#################################################################################
;#################################################################################

.DATA

;===============================
; Strings for the bitmaps
;===============================

;================================
; Our very cool menu sound
;================================

;===============================
; PTR to the BMP's
;===============================

;===============================
; ID for the Menu sound
;===============================

;======================================
; A value to hold lPitch when locking
;======================================
lPitch DD 0

;========================================
; Let's us know if we need to transition
;========================================
first_time DD 0

;#################################################################################
;#################################################################################
; BEGIN CONSTANTS
;#################################################################################
;#################################################################################

;#################################################################################
;#################################################################################
; BEGIN EQUATES
;#################################################################################
;#################################################################################

;=================
;Utility Equates
;=================
FALSE EQU 0
TRUE EQU 1

;=================
; The Screen BPP
;=================
screen_bpp EQU 16

;=================
;=================
; Generic

;#################################################################################
;#################################################################################
; BEGIN THE CODE SECTION
;#################################################################################
;#################################################################################

.CODE

;########################################################################
;########################################################################

;===========================================================
; This function will initialize our menu systems
;===========================================================

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

;======================================
; Read in the bitmap and create buffer
;======================================

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

.ENDIF

;======================================
; Read in the bitmap and create buffer
;======================================

;====================================
; Test for an error
;====================================
.IF EAX == FALSE
;=