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.
[size="5"]Next Piece Please
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
add eax, 5
mov ebx, BlockGrid
add eax, ebx
mov ecx, eax
add ecx, 4
;==========================
; 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
add eax, ebx
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
;########################################################################
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.
[size="5"]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
add ebx, eax
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
add YPos, 15
.else
add YPos, 5
.endif
.endif
;=============================
; Calculate the X coord
;=============================
mov eax, DrawX
add eax, CurCol
dec eax
mov ecx, BLOCK_WIDTH
mul ecx
.if UseNext == FALSE
add eax, 251
.else
add eax, 40
;=============================
; 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
add eax, 25
.elseif ecx == offset L
add eax, 15
.elseif ecx == offset Back_L
add eax, 15
.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
;########################################################################
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.
[size="5"]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
;########################################################################
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.
[size="5"]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
;########################################################################
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?
[size="5"]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
add eax, BlockGrid
;==================================
; 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
add eax, BlockGrid
mov edx, eax
add edx, 13
;==============================
; 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
;########################################################################
;########################################################################
; Adjust_Score Procedure
;########################################################################
Adjust_Score proc amount:DWORD
;================================================
; This function will adjust the score by the
; passed in value if possible, adjusting the
; level if necessary
;================================================
mov eax, amount
add Score, eax
.if Score > MAX_SCORE
mov Score, MAX_SCORE
.endif
done:
;===================
; We earned a line
;===================
return TRUE
Adjust_Score ENDP
;########################################################################
; END Adjust_Score
;########################################################################
;########################################################################
; 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
;########################################################################
[size="5"]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.