[size="5"]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.
[size="5"]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
;########################################################################
[size="5"]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.
[size="5"]One Big Headache
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.
;########################################################################
; Load_WAV Procedure
;########################################################################
Load_WAV PROC fname_ptr:DWORD, flags:DWORD
;=======================================================
; 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
;=================================
INVOKE mmioDescend, hwav, ADDR child, ADDR parent, NULL
.IF EAX != NULL
;===================
; Close the file
;===================
INVOKE mmioClose, hwav, NULL
;=====================
; Jump and return out
;=====================
JMP err
.ENDIF
;=================================
; Now read the wave format info in
;=================================
INVOKE mmioRead, hwav, ADDR wfmtx, sizeof(WAVEFORMATEX)
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
;=================================
INVOKE mmioDescend, hwav, ADDR child, ADDR parent, MMIO_FINDCHUNK
.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 mmioRead, hwav, snd_buffer, child.ckSize
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
MOV sound_fx[ECX].state, SOUND_LOADED
;==========================
; 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
;=================================
DSINITSTRUCT ADDR dsbd, sizeof(DSBUFFERDESC)
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,\
ADDR audio_length_1, ADDR audio_ptr_2, ADDR audio_length_2,\
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
ADD ESI, audio_length_1
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
Load_WAV ENDP
;########################################################################
; END Load_WAV
;########################################################################
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.
[size="5"] 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 [lessthan][lessthan] 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
;================================
INVOKE Start_Time, ADDR StartTime
;================================
; Blit the strip onto the screen
;================================
DDS4INVOKE BltFast, lpddsprimary, SrcRect.left, SrcRect.top,\
lpddsback, ADDR DestRect, DDBLTFAST_WAIT
;===============================
; 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
ADD EBX, EAX
MOV DestRect.right, EBX
MOV SrcRect.right, EBX
.ELSEIF direction == WIPE_VERT
MOV EBX, SrcRect.bottom
MOV SrcRect.top, EBX
MOV DestRect.top, EBX
ADD EBX, EAX
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
;================================
INVOKE Start_Time, ADDR StartTime
;================================
; Blit the strip onto the screen
;================================
DDS4INVOKE BltFast, lpddsprimary, SrcRect.left, SrcRect.top,\
lpddsback, ADDR DestRect, DDBLTFAST_WAIT
;===============================
; 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
;======================
; Adjust by the strip
;======================
MOV EAX, strip_width
.IF direction == WIPE_HORZ
ADD SrcRect.left, EAX
ADD SrcRect.right, EAX
ADD DestRect.left, EAX
ADD DestRect.right, EAX
.ELSEIF direction == WIPE_VERT
ADD SrcRect.top, EAX
ADD SrcRect.bottom, EAX
ADD DestRect.top, EAX
ADD DestRect.bottom, EAX
.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.
[size="5"]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.
;###########################################################################
;###########################################################################
; ABOUT Menu:
;
; 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
; menu and one for the load/save menu stuff.
;
; 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
;===============================
szMainMenu DB "Art\Menu.sfp",0
szFileMenu DB "Art\FileMenu.sfp",0
;================================
; Our very cool menu sound
;================================
szMenuSnd DB "Sound\Background.wav",0
;===============================
; PTR to the BMP's
;===============================
ptr_MAIN_MENU DD 0
ptr_FILE_MENU DD 0
;===============================
; ID for the Menu sound
;===============================
Menu_ID DD 0
;======================================
; 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
;=================
; The Menu Codes
;=================
; Generic
MENU_ERROR EQU 0h
MENU_NOTHING EQU 1h
; Main Menu
MENU_NEW EQU 2h
MENU_FILES EQU 3h
MENU_GAME EQU 4h
MENU_EXIT EQU 5h
; File Menu
MENU_LOAD EQU 6h
MENU_SAVE EQU 7h
MENU_MAIN EQU 8h
;#################################################################################
;#################################################################################
; BEGIN THE CODE SECTION
;#################################################################################
;#################################################################################
.CODE
;########################################################################
; Init_Menu Procedure
;########################################################################
Init_Menu PROC
;===========================================================
; This function will initialize our menu systems
;===========================================================
;=================================
; Local Variables
;=================================
;======================================
; Read in the bitmap and create buffer
;======================================
INVOKE Create_From_SFP, ADDR ptr_MAIN_MENU, ADDR szMainMenu, 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_FILE_MENU, ADDR szFileMenu, screen_bpp
;====================================
; Test for an error
;====================================
.IF EAX == FALSE
;=