• Create Account

Like
0Likes
Dislike

# Win32 Assembly Part 6

By Chris Hobbs | Published May 31 2000 06:07 AM in General Programming

eax mov ecx font add text_rect
 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?

Okay, time to get back to work. During our last session we added in the totally awesome sound effects (Thanks EvilX -- http://www.evilx.com), and we made some simple screen transitions. I also showed the solution to the rotation problem we had. It may not sound like much, but trust me those things had a significant impact on the game.

Before I get into this article’s topics, allow me to sidetrack a little bit. I have received numerous eMails lately asking me to add feature X, Y, or Z to SPACE-TRIS. As much as I’d like to ... I won’t, and here is why. I simply don’t have the time, or the space to add in everybody’s wishes. Many of the suggestions are very good ones, and, I too have numerous ideas that could be integrated into the game. The trouble is that this article series isn’t about making a commercial-level game. My sole goal is to introduce you, the reader, to game concepts implemented in assembly language.

Often times, a feature asked for is something I have already covered the basis for in this series. At those times, I suggest that you add those features yourself. Other times, I may not have covered that type of concept at all. Those are the things you need to let me know about. Send me an eMail saying "Hey Chris, try and cover blah-blah-blah...". Then, I will attempt to fit it in, or at least add it to my list of needed articles for later. This way, a little bit of everything is covered instead of one main concept and 300 variations of it.

Enough of that, what are we going to cover today? You don’t know! In that case I had better let you know exactly what I am up to. Or should I just leave it as a surprise? Ah, okay ... I’ll let you, and just you, in on the secrets.

We’ll start off by adding in the ability to see the preview piece, which means we’ll have to add a preview piece to our list of needed data (duh!). Once that is taken care of we’ll add the ability to draw text of different font sizes -- this will take a few new routines and alteration of an old one. Then, we’ll write the code to draw text for our level, score, and the current lines we have earned. Finally, we can add the scoring system along with a primitive level system. I had originally hoped to write the code that would save and load our games, but I just didn’t have the time to get it in. So, it looks like that little feature gets pushed off to the last article.

Okay, that is the plan. So, I suppose I should stop chattering and get to the good stuff.

Integrating a new piece into the ‘pipeline’ was very easy. Basically, what I wanted was a piece that would stand in line. Then, when the current piece finished dropping, the next piece in the line would become current and the new piece, that was just created, would take it’s place waiting.

So, I started out by copying all the variables the current piece had. Then I just gave them new names to show they were for the next piece and not the current one. Then, I needed to alter the New_Shape() procedure. Take a look at the code I added.

;########################################################################
; New_Shape Procedure
;########################################################################
New_Shape    proc

;================================================
; This function will select a new shape at random
; for the Next shape and assign the old next
; shape values to the current shape
;================================================

;=================================
; Do the swaps if this isn't our
; very first piece of the game
;=================================
again:
.if NextShape != -1
m2m    CurShape, NextShape
m2m    CurShapeColor, NextShapeColor
m2m    CurShapeX, NextShapeX
m2m    CurShapeY, NextShapeY
m2m    CurShapeFrame, NextShapeFrame
.endif

;======================================
; First make sure they haven't reached
; the top of the grid yet
;
; Begin by calculating the start of
; the very last row where the piece
; is initialized at ... aka (5,19)
;======================================
mov    eax, 13
mov    ecx, 19
mul    ecx
mov    ebx, BlockGrid
mov    ecx, eax

;==========================
; Loop through and test the
; next 4 positions
;==========================
.while eax <= ecx
;=====================
; Is this one filled?
;=====================
mov    bl, BYTE PTR [eax]
.if bl != 0
;===================
; They are dead
;===================
jmp    err

.endif

;=================
; Inc the counter
;=================
inc    eax
.endw

;=============================
; Use a random number to get
; the current shape to use
;
; For this we will just use
; the time returned by the
; Get_Time() function
;=============================
invoke Get_Time

;=============================
; Mod this number with 7
; since there are 7 shapes
;=============================
mov    ecx, 7
xor    edx, edx
div    ecx
mov    eax, edx

;=============================
; Multiply by 16 since there
; are 16 bytes per shape
;=============================
shl    eax, 4

;=============================
; Use that number to select
; the shape from the table
;=============================
mov    ebx, offset ShapeTable
mov    NextShape, eax

;=============================
; Use a random number to get
; the block surface to use
;
; For this we will just use
; the time returned by the
; Get_Time() function
;=============================
invoke Get_Time

;=============================
; And this result with 7
; since there are 8 blocks
;=============================
and    eax, 7

;================================
; Use it as the block surface
;================================
mov    NextShapeColor, eax

;================================
; Initialize the Starting Coords
;================================
mov    NextShapeX, 5
mov    NextShapeY, 24

;================================
; Set the Current Frame Variable
;================================
mov    NextShapeFrame, 0

;====================================
; Go back to the top and load again
; if this was our very first piece
;====================================
.if CurShape == -1
jmp    again
.endif

done:
;=======================
; They have a new piece
;=======================
return TRUE

err:
;===================
; They died!
;===================
return FALSE

New_Shape    ENDP
;########################################################################
; END New_Shape
;########################################################################

Notice that the first thing I do is test to see if NextShape is currently -1. I assign NextShape this value during initialization to show that I need to create two new shapes, one for the current and one for the next. After that special very first iteration though everything runs as normal. I place the values in the next shape into the current shape’s variables. Then, I create everything just as before except I store the values in my next shape instead. At the bottom I test the current shape to see if it is -1. If so, then I know I need to create another shape, so I jump back to the top and do it all over again.

The only other modification I had to make was, as I mentioned, during initialization. At that point, both the current shape and the next shape were set equal to -1 to indicate they needed to be created.

I Can’t See It!

After getting it to create and store the piece, I just needed a way to draw it on the screen. I decided to simply modify the existing Draw_Shape() procedure. The idea was, to have it draw either the current shape, or the next shape, based upon a variable that was passed in. Have a look at the new version.

;########################################################################
; Draw_Shape Procedure
;########################################################################
Draw_Shape    proc    UseNext:BYTE

;=======================================================
; This function will draw our current shape at its
; proper location on the screen or it will draw the next
; shape on the screen in the next window
;=======================================================

;===========================
; Local Variables
;===========================
LOCAL    DrawY:   	 DWORD
LOCAL    DrawX:   	 DWORD
LOCAL    CurRow:   	 DWORD
LOCAL    CurCol:   	 DWORD
LOCAL    CurLine:    DWORD
LOCAL    XPos:   	 DWORD
LOCAL    YPos:   	 DWORD

;===================================
; Get the Current Shape Pos
;===================================
.if UseNext == FALSE
mov    ebx, CurShape
mov    eax, CurShapeFrame
.else
mov    ebx, NextShape
mov    eax, NextShapeFrame
.endif
shl    eax, 2
mov    CurLine, ebx

;===================================
; Set the Starting Row and Column
; for the drawing
;===================================
.if UseNext == FALSE
mov    eax, CurShapeX
mov    ebx, CurShapeY
.else
mov    eax, 2   	 ; X Coord
mov    ebx, 4   	 ; Y Coord
.endif
mov    DrawX, eax
mov    DrawY, ebx

;===================================
; Loop through all four rows
;===================================
mov    CurRow, 0
.while CurRow < 4
;=====================================
; Loop through all four Columns if
; the Y Coord is in the screen
;=====================================
mov    CurCol, 4
.while CurCol > 0 && DrawY < 20
;===============================
; Shift the CurLine Byte over
; by our CurCol
;===============================
mov    ecx, 4
sub    ecx, CurCol
mov    ebx, CurLine
xor    eax, eax
mov    al, BYTE PTR [ebx]
shr    eax, cl

;===============================
; Is it a valid block?
;===============================
.if ( eax & 1 )
;============================
; Yes it was a valid block
;============================

;=============================
; Calculate the Y coord
;=============================
mov    eax, (GRID_HEIGHT - 5)
sub    eax, DrawY
mov    ecx, BLOCK_HEIGHT
mul    ecx
mov    YPos, eax

;===========================
; Adjust the Y coord for
; certain shapes in the next
; window since they are off
; of the center
;===========================
.if UseNext == TRUE
mov    ecx, NextShape
.if ecx == Offset Square || ecx == Offset Line
sub    YPos, 7
.elseif ecx == offset Pyramid
.else
.endif
.endif

;=============================
; Calculate the X coord
;=============================
mov    eax, DrawX
dec    eax
mov    ecx, BLOCK_WIDTH
mul    ecx
.if UseNext == FALSE
.else
;=============================
; Now adjust the X coord on a
; shape by shape basis
;=============================
mov    ecx, NextShape
.if ecx == offset Square
sub    eax, 12
.elseif ecx == offset Line
.elseif ecx == offset L
.elseif ecx == offset Back_L
.elseif ecx == offset Z
sub    eax, 15
.elseif ecx == offset Back_Z
sub    eax, 15
.endif
.endif
mov    XPos, eax

;=============================
; Calculate the surface to use
;=============================
.if UseNext == FALSE
mov    eax, CurShapeColor
.else
mov    eax, NextShapeColor
.endif
shl    eax, 2
mov    ebx, DWORD PTR BlockSurface[eax]

;=============================
; Blit the block
;=============================
DDS4INVOKE BltFast, lpddsback, XPos, YPos, \
ebx, ADDR SrcRect, \
DDBLTFAST_NOCOLORKEY or DDBLTFAST_WAIT

.endif

;=====================
; Dec our col counter
;=====================
dec    CurCol

.endw

;=======================
; Inc the CurLine
;=======================
inc    CurLine

;====================
; decrement Y coord
;====================
dec    DrawY

;====================
; Inc the row counter
;====================
inc    CurRow

.endw

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

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

Draw_Shape    ENDP
;########################################################################
; END Draw_Shape
;########################################################################

The start of that code is pretty self-explanatory. It simply decides which variables to use based upon the piece we are drawing. Take note, that the coordinates 2, and 4, are not pixel coordinates. They are the number of 32x32 blocks on the X-axis, and the number of blocks from the bottom on the Y-axis.

There is one major change in the code and that is where I adjust the position of the blocks that are drawn. Because our window we are trying to draw them in is square, but our shapes typically aren’t, we needed a way to center them. So, I decided to hard-code in the coordinate adjustments.

I used a special technique in order to do this though. You’ll notice that I labeled the start of each shape’s declaration in the shape table. Remember when we were declaring the shapes by using bits? Well, all I did was place a label before the start of every new shape. This is very, very powerful. I am now able to address the middle of a huge table by name. Needless to say, this adds to the clarity of what would have been a very difficult thing to understand.

The only exception to this rule was the square. Because the square was the first shape, I couldn’t have two names both at the same place, the first name being, of course, our variable name ShapeTable. So, at the end of ShapeTable I put an equate that said treat ‘Square’ the same as ShapeTable. In code, I could have easily just used ShapeTable directly ... but then it wouldn’t have been as clear as to what I was doing.

Finally, in the main code we call this routine both with TRUE, and with FALSE, so we can have both pieces drawn. The next step is to modify the drawing routine to let us change fonts to draw our text.

The New Text

The text support didn’t require too much alteration. Basically, I wanted to be able to support drawing the text with GDI in different fonts instead of the system default. This is something that I should have planned in from the beginning, but I didn’t. I would like to be able to say I was just saving it for later ... but, the truth is, I plum forgot about it. Oh well, I guess you’ll get to see it now.

The very first thing we have to do is add in support for selecting and deselecting certain fonts. In Windows you specify what font you want to use by selecting it into your object after you create it. This sounds pretty crazy but the code is fairly straightforward. Here are the routines to select and deselect the font.

;########################################################################
; DD_Select_Font Procedure
;########################################################################
DD_Select_Font PROC    handle:DWORD, lfheight:DWORD, lfweight:DWORD,\
ptr_szName:DWORD, ptr_old_obj:DWORD

;=======================================================
; This function will create & select the font after
; altering the font structure based on the params
;=======================================================

;=================================
; Create the FONT object
;=================================
INVOKE CreateFont, lfheight, 0, 0, 0, lfweight, 0, 0, \
0, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_STROKE_PRECIS,\
DEFAULT_QUALITY, DEFAULT_PITCH OR FF_DONTCARE, ptr_szName
MOV    temp, EAX

;===================================
; Select the font and preserve old
;===================================
INVOKE SelectObject, handle, EAX
MOV    EBX, ptr_old_obj
MOV    [EBX], EAX

done:
;===================
; We completed
;===================
return temp

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

DD_Select_Font    ENDP
;########################################################################
; END DD_Select_Font
;########################################################################

;########################################################################
; DD_UnSelect_Font Procedure
;########################################################################
DD_UnSelect_Font PROC    handle:DWORD, font_object:DWORD, old_object:DWORD

;=======================================================
; This function will delete the font object and restore
; the old object
;=======================================================

;==================================
; Restore old obj and delete font
;==================================
INVOKE SelectObject, handle, old_object
INVOKE DeleteObject, font_object

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

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

DD_UnSelect_Font    ENDP
;########################################################################
; END DD_UnSelect_Font
;########################################################################

This probably doesn’t mean too much to you right now. But here is how the routines work. In order to select two steps are required. First, we must create a font object. My functions lets you control three different things: size, weight, and font name. The size is how large you want it, the weight controls BOLD and normal, while the name controls the actual font you use. There are many other parameters that can be played with ... I suggest reviewing the Win32 API calls for those parameters. The second step is to ‘select’ that font object into the current device context. The only trick here is we preserve the old object with the pointer that was passed in for that old object. This is all that needs to be done to select a new font.

Our routine top deselect the font is pretty much the same process but in reverse. First we select our old object back into the device context. This step is important because we may have had something else in there that we want to restore. When programming it is best to abide by the adage most of our mothers taught us ... "put it back the way you found it." Anyway, after we select the old object we can delete our current font object and we are finished.

That is all that there is to selecting a new font to use for drawing. But, it doesn’t do much good without some code to put it on the screen.

Wanna See This Too?

I suppose now would be the time to show you the code for drawing our captions on the screen. I simply added a new function to our shapes module. Here it is ...

;########################################################################
; Draw_Captions Procedure
;########################################################################
Draw_Captions    proc

;=======================================================
; This function will draw our captions, such as the
; score and the current level they are on
;=======================================================

;====================
; Local Variables
;====================
LOCAL    hFont   	 :DWORD

;=====================================
; Get the DC for the back buffer
;=====================================
invoke DD_GetDC, lpddsback
mov    hDC, eax

;====================================
; Set the font to "IMPACT" at the
; size that we need it
;====================================
invoke DD_Select_Font, hDC, -32, FW_BOLD, ADDR szImpact, ADDR Old_Obj
mov    hFont, eax

;=============================
; Setup rect for score text
;=============================
mov    text_rect.top, 161
mov    text_rect.left, 54
mov    text_rect.right, 197
mov    text_rect.bottom, 193

;=============================
; Draw the Score Text
;=============================
RGB 255, 255, 255
push    eax
mov    eax, Score
mov    dwArgs, eax
invoke wvsprintfA, ADDR szBuffer, ADDR szScore, Offset dwArgs
pop    ebx
invoke DD_Draw_Text, hDC, ADDR szBuffer, eax, ADDR text_rect,\
DT_CENTER or DT_VCENTER or DT_SINGLELINE, ebx

;=============================
; Setup rect for Level text
;=============================
mov    text_rect.top, 67
mov    text_rect.left, 102
mov    text_rect.right, 151
mov    text_rect.bottom, 99

;=============================
; Draw the Level Text
;=============================
RGB 255, 255, 0
push    eax
mov    eax, CurLevel
mov    dwArgs, eax
invoke wvsprintfA, ADDR szBuffer, ADDR szLevel, Offset dwArgs
pop    ebx
invoke DD_Draw_Text, hDC, ADDR szBuffer, eax, ADDR text_rect,\
DT_CENTER or DT_VCENTER or DT_SINGLELINE, ebx

;=============================
; Setup rect for Lines text
;=============================
mov    text_rect.top, 256
mov    text_rect.left, 90
mov    text_rect.right, 162
mov    text_rect.bottom, 288

;=============================
; Draw the Lines Text
;=============================
RGB 255, 255, 0
push    eax
mov    eax, NumLines
mov    dwArgs, eax
invoke wvsprintfA, ADDR szBuffer, ADDR szLines, Offset dwArgs
pop    ebx
invoke DD_Draw_Text, hDC, ADDR szBuffer, eax, ADDR text_rect,\
DT_CENTER or DT_VCENTER or DT_SINGLELINE, ebx

;=============================
; Unselect the font
;=============================
invoke DD_UnSelect_Font, hDC, hFont, Old_Obj

;============================
; Release the DC
;============================
invoke DD_ReleaseDC, lpddsback, hDC

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

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

Draw_Captions    ENDP
;########################################################################
; END Draw_Captions
;########################################################################

I have tried to keep it in the same form as the rest of what I’ve shown you. The code reads from a few module variables to get the current numbers to draw. It then makes a call to set the font how we want. This isn’t anything new I hope. We then set our rectangle for the drawing and make the call. If you don’t remember wvsprintfA() is a function that is used for formatting a string buffer ... almost exactly like sprintf().

The other thing I am doing is setting the color we will use. I don’t know about you but I prefer to make things a little bit varied and stand-out-ish ( <-- Is that even a word???).

In short, this routine just calls upon a few library routines and pieces things together as needed. I can’t remember if I have told you guys, or not ... but programming is like one big jigsaw puzzle. It is just a matter of finding the right pieces and putting them together correctly. There is no one right way to do it and that is why everybody creates different pictures. Make sense?

Scoring and Levels

It is truly amazing how primitive I made this scoring and level system. The thing does about as much as the old Atari games, but hey, it is a start.

Inside the Line_Test() function the code increments a variable which tests itself for a MAX condition. This is where the number of lines is counted. Once that MAX condition is exceeded the number of lines gets reset and the level increased. Then, in our main code, another function we call is the Is_Game_Won() function. It is called to find out if they have gone over the maximum number of levels in the game. In our case, the MAX levels is ten, but you can make it whatever you would like it to be.

The other function we added was one to keep track of the score. As expected it is called Adjust_Score() and performs the same type of adjustment we did for the levels. The only difference is that if the user exceeds the maximum score we simply reset their score to the maximum amount. Nothing fancy, but it works as it is supposed to, which is always a nice side effect. This function is called from the main module based upon how many lines they achieved in one swoop. So, the more lines they eliminate at once the more points they would achieve.

When they have reached the end of the game our main code sets the state to GS_WON and simply restarts them. It is in that section that we would perform credits and special winning sequences. But, I was lacking in both art and creativity when I coded it, so they just restart the game.

Here are the Line_Test(), Adjust_Score(), and Is_Game_Won() functions. I’ll let you sort through the main code yourself and see what alterations I made.

;########################################################################
; Line_Test Procedure
;########################################################################
Line_Test    proc

;================================================
; This function will test to see if they earned a
; line ... if so it will eliminate that line
; and update our grid of blocks
;================================================

;==========================
; Local Variables
;==========================
LOCAL    CurLine:    DWORD
LOCAL    CurBlock:    DWORD

;===============================
; Start at the Base of the Grid
;===============================
mov    CurLine, 0

;=================================
; Loop through all possible Lines
;=================================
.while CurLine < (GRID_HEIGHT - 4)
;===================================
; Goto the base of the current line
;===================================
mov    eax, CurLine
mov    ecx, 13
mul    ecx

;==================================
; Loop through every block
; testing to see if it is valid
;==================================
mov    CurBlock, 0
.while CurBlock < (GRID_WIDTH)
;==========================
; Is this Block IN-Valid?
;==========================
mov    bl, BYTE PTR [eax]
.if bl == 0
;===================
; Yes, so break
;===================
.break

.endif

;======================
; Next Block
;======================
inc    eax

;======================
; Inc the counter
;======================
inc    CurBlock

.endw

;==============================
; Did our inner loop go all
; of the way through??
;==============================
.if CurBlock == (GRID_WIDTH)
;============================
; Yes. That means that it was
; a valid line we just earned
;============================

;===================================
; Calculate How much memory to move
; TOTAL - Amount_IN = TO_MOVE
;===================================
mov    ebx, (GRID_WIDTH * (GRID_HEIGHT -5))
mov    eax, CurLine
mov    ecx, 13
mul    ecx
push    eax
sub    ebx, eax

;============================
; Move the memory one line
; up to our current line
;============================
pop    eax
mov    edx, eax

;==============================
; Move the memory down a notch
;==============================
invoke RtlMoveMemory, eax, edx, ebx

;============================
; Jump down and return TRUE
;============================
jmp    done

.endif

;==============================
; Incrment our Line counter
;==============================
inc    CurLine

.endw

err:
;===================
; We didn't get one
;===================
return FALSE

done:
;===================
; Play the sound
;===================
invoke Play_Sound, Thud_ID, 0

;==========================
; Adjust their line count
;==========================
inc    NumLines
.if NumLines >= MAX_LINES
mov    NumLines, 0
inc    CurLevel
.endif

;===================
; We earned a line
;===================
return TRUE

Line_Test    ENDP
;########################################################################
; END Line_Test
;########################################################################

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

;================================================
; This function will adjust the score by the
; passed in value if possible, adjusting the
; level if necessary
;================================================
mov    eax, amount
.if Score > MAX_SCORE
mov    Score, MAX_SCORE
.endif

done:
;===================
; We earned a line
;===================
return TRUE

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

;########################################################################
; Is_Game_Won Procedure
;########################################################################
Is_Game_Won    proc

;================================================
; This function will return TRUE if we have won
; the game and false otherwise
;================================================

.if CurLevel > MAX_LEVEL
return TRUE
.else
return FALSE
.endif

Is_Game_Won    ENDP
;########################################################################
; END Is_Game_Won
;########################################################################

Until Next Time...

Whoopie! We are finished with yet another installment. So, have you guys been working on your different versions like I keep hounding you about? I really hope so ... especially since I have a nice little challenge to offer you in our final installment.

Gosh, I can hardly think of anything else to say right now. I am excited to be bringing this series under wraps here soon. I have some things I would like to talk/write about but aren’t really applicable to this series. Ergo, it will be totally cool to see this series end and get into some more advanced stuff. I can tell from some of the letters that I get, that many of you are waiting for that to happen also.

The one thing I do want to mention is it could be a couple of months before my final installment is complete. I have been really pressed for time here lately. Those of you who visit my web-site may have noticed the lack of updates. I wish I had more time right now, but my job is keeping me really, really busy. Anyway, I just wanted to let you know that it was coming, just not as soon as we all would have liked. So ...

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