Win32 Assembly Part 4
by Chris Hobbs

Where Did We Leave Off?

Up until now we have made some good progress. We have a nice framework for our game to sit in, and, more importantly, we are all set to create the actual game. Which, coincidentally, is what we are going to do in this article. So, instead of just "good" progress ... be prepared for some extraordinary progress.

Here is the current line-up ...

Leading off today is Animation. He is a very important player on our team so we want him up first. Without him we won't be able to play at all.

Next up, we have Mr. Structures. Mr. S, as he is often called, has the job of keeping everything organized. He is the guy in the dugout often stacking things, or keeping track of statistics. He plays a more important role later in the season when we start playing "real games" and keeping score.

Third, we have the New Shape maker. He is responsible for setting things up for the later players. He is often overlooked since he works behind the scenes.

Batting cleanup for us is Update. Update is a big boy. He has a large job but does a great job of delegating things he needs to other people. Often known as our power hitter, he is the most recognized of all members.

Batting fifth and sixth for us are twins Move Shape and Rotate Shape. They are the ones we rely on to keep things going. If they strike out we know something has gone amiss somewhere along the lines.

The seventh in our line up is Line Test. He typically will clear the bases ... but only if certain conditions are met.

The eighth and ninth positions are filled by another set of twins, Draw Grid and Draw Shape. They are the publicity freaks on the team and make sure the fans can see everything that is happening.

Our manager is of course "the loop." He is the leader and holds everything together. It is his job to dictate what needs to be done and make sure nobody fails. He will not hesitate to act upon any error and is very demanding ... likes to make sure everything is done on his clock.

We have a good solid team and are ready to take a look at their statistics, background, and of course ... how they think. Are you ready ... play ball!

Stepping to the plate...

Animation is a very complicated player. He can play in many different ways, and has numerous styles. On our team, he has adopted the style commonly referred to as "Pre-Set." What this means is that everything that he does was determined before the game has even been started. This occurred in the initialization section, of course.

In our game we have single blocks that are selected at random. That random block is used with a random shape. The shapes that exist are the same ones as in the original Tetris. Now the animation sequences that are needed by the shapes are relatively simple ... you merely rotate the shapes. Therefore, I had three choices when deciding how to animate them.

  1. I could make bitmaps of every shape pre-built and rotate them at runtime as needed.
  2. I could do the same as above except pre-rotate them, then save many bitmaps and cycle through them at run-time as they are needed.
  3. I could build every shape from a block and use some sort of table to tell me how the shape was to be built for that frame.

Because of speed, and size, I decided to go with the third method. It is a little bit more complex ... but I think the speed gain, and size drop, is worth it. (NOTE: As a programmer, if you have the ability to make a piece of code more robust, more user-friendly, smaller, or faster then do so).

So, now that we know what we want, how do we accomplish it? Well, the first thing I did was sit down with a piece of paper, and determine the patterns that a shape could have. Then, I took those patterns, encapsulated them into mini-grids, and made them represent either ON, or OFF, states depending on if a block was in that position. Here is an example for a square.


0 0 0 0
0 1 1 0
0 1 1 0
0 0 0 0

Notice the grid is 4x4, the reason is because the largest a shape can be is four squares wide, or tall. The ones are the places were the blocks are and the zeros are empty locations.

With that gigantic list built, I needed a way to organize them into look-up tables. The decision was to pad the left of each line with four zeros and thus get an 8x4 grid. I could then use an array of four bytes for each frame where a single bit represented a block. This caused a 2-byte waste for every block ... yet it made the code about 100000 times easier to understand.

The table access is really simple. We just offset into the table according to the shape we want. Then, we offset into that address by the frame that we want. Every shape has four frames, no matter what, and are all aligned to four bytes. So, we can easily adjust our "frame pointer" with a few simple arithmetic operations.

That is how animation works in our game. You simply tell him when to adjust to a new frame and he does. Need a new shape? No problem, just point him where it is and he will know what to do.

Here is our table.


	;===================================================
	; Here is our shape table it contains every possible
	; combination of values for the different shapes
	; in order to give us the correct shape for every
	; possible rotation.
	;===================================================
ShapeTable	\			; Here is our square
		\	
	DB	00000000b		; Position 1
	DB	00000110b	
	DB	00000110b	
	DB	00000000b	
			
	DB	00000000b		; Position 2
	DB	00000110b	
	DB	00000110b	
	DB	00000000b	
			
	DB	00000000b		; Position 3
	DB	00000110b	
	DB	00000110b	
	DB	00000000b	
			
	DB	00000000b		; Position 4
	DB	00000110b	
	DB	00000110b	
	DB	00000000b	
				; Here is our Line
			
	DB	00001000b		; Position 1
	DB	00001000b	
	DB	00001000b	
	DB	00001000b		
			
	DB	00000000b		; Position 2
	DB	00000000b	
	DB	00000000b	
	DB	00001111b		
			
	DB	00000001b		; Position 3
	DB	00000001b	
	DB	00000001b	
	DB	00000001b		
			
	DB	00001111b		; Position 4
	DB	00000000b	
	DB	00000000b	
	DB	00000000b		
				; Here is our Pyramid
			
	DB	00001110b		; Position 1
	DB	00000100b	
	DB	00000000b	
	DB	00000000b		
			
	DB	00001000b		; Position 2
	DB	00001100b	
	DB	00001000b	
	DB	00000000b		
			
	DB	00000000b		; Position 3
	DB	00000100b	
	DB	00001110b	
	DB	00000000b		
			
	DB	00000001b		; Position 4
	DB	00000011b	
	DB	00000001b	
	DB	00000000b		
				; Here is our L
			
	DB	00001000b		; Position 1
	DB	00001000b	
	DB	00001100b	
	DB	00000000b		
			
	DB	00000000b		; Position 2
	DB	00000010b	
	DB	00001110b	
	DB	00000000b		
			
	DB	00000110b		; Position 3
	DB	00000010b	
	DB	00000010b	
	DB	00000000b		
			
	DB	00001110b		; Position 4
	DB	00001000b	
	DB	00000000b	
	DB	00000000b		
				; Here is our Backwards L
			
	DB	00001100b		; Position 1
	DB	00001000b	
	DB	00001000b	
	DB	00000000b		
			
	DB	00000000b		; Position 2
	DB	00001000b	
	DB	00001110b	
	DB	00000000b		
			
	DB	00000001b		; Position 3
	DB	00000001b	
	DB	00000011b	
	DB	00000000b		
			
	DB	00001110b		; Position 4
	DB	00000010b	
	DB	00000000b	
	DB	00000000b		
				; Here is our Backwards Z
			
	DB	00000100b		; Position 1
	DB	00000110b	
	DB	00000010b	
	DB	00000000b		
			
	DB	00000110b		; Position 2
	DB	00001100b	
	DB	00000000b	
	DB	00000000b		
			
	DB	00000100b		; Position 3
	DB	00000110b	
	DB	00000010b	
	DB	00000000b		
			
	DB	00000110b		; Position 4
	DB	00001100b	
	DB	00000000b	
	DB	00000000b		
				; Here is our Z
			
	DB	00000010b		; Position 1
	DB	00000110b	
	DB	00000100b	
	DB	00000000b		
			
	DB	00001100b		; Position 2
	DB	00000110b	
	DB	00000000b	
	DB	00000000b		
			
	DB	00000010b		; Position 3
	DB	00000110b	
	DB	00000100b	
	DB	00000000b		
			
	DB	00001100b		; Position 4
	DB	00000110b	
	DB	00000000b	
	DB	00000000b	

Mr. Structure

Oh, yes ... Mr. Structure. He often looks like a container, however in our game he is spread out. It is his responsibility to hold the X and Y coordinates, current shape, current shape block to use, and the current frame. He has just a few variables to keep things semi-organized.

As I mentioned earlier, he will have a larger job when it comes to keep score, and manage any other statistics.

He is a really open guy, global to be precise. He doesn't mind helping people and, of course, will let anybody know what he knows.

The declarations for him are in Shapes.asm and for the time being are relatively simple.

Because Mr. Structure is so open bad things can POSSIBLY happen. It is your job, as a programmer, to make sure that those bad things can NEVER happen. If you let him be corrupted in some manner your whole game might go down the toilet.

The New Shape Maker

With the dreary setup stuff behind us we have things ready for the New Shape maker. His responsibility is fairly straight forward so let's take a look at the code before I start explaining things.


;########################################################################
; New_Shape Procedure
;########################################################################
New_Shape	PROC	

	;================================================
	; This function will select a new shape at random
	; for the current shape
	;================================================

	;======================================
	; 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	CurShape, 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	CurShapeColor, EAX

	;================================
	; Initialize the Starting Coords
	;================================
	MOV	CurShapeX, 5
	MOV	CurShapeY, 24

	;================================
	; Set the Current Frame Variable
	;================================
	MOV	CurShapeFrame, 0

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

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

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

Do you see what I am doing with the code? You should start having at least a general idea when looking at the code segments. If not, start studying more ... that means writing code, not staring at mine!

To start with we check the area directly under where we want the block to start to see if there are blocks already in there. If so, then they died. It is a really simple concept. No more room on grid = DEATH!

Next we grab some random numbers to use for the block texture and the shape. I chose just to use the Get_Time() function that we have. We may write a true random number generator later in this article series. For now, this function call will serve our purposes.

In order to get a number between zero and six we divide by seven and take the remainder ( this is placed in EDX after a DIV ) . This way, the highest number we could have is six, and the lowest is zero, which is perfect since we have seven shapes to choose from.

We do something a bit different for the blocks. Instead of performing a MOD operation, we AND the number with ( N-1 ). Where N is the number you would normally MOD with. This only works for numbers that are powers of 2 however. We are taking advantage of another bit manipulation operation to speed things up.

The next step is to merely initialize the starting X and Y coordinates along with the starting frame to use.

That is all we need to do in order to create a new shape during the game. Once this function is finished everything is setup to start moving and manipulating the current shape, whatever it may be.

Update takes a few practice swings

Update is our power hitter. He has the job of handling all updates. Let's take a look at exactly what it is he does.


;########################################################################
; Update_Shape Procedure
;########################################################################
Update_Shape	PROC

	;================================================
	; This function will update our shape ... or 
	; drop it down by a grid notch and test for
	; a collision with the grid
	;================================================
	
	;========================
	; Can we move down???
	;========================
	INVOKE Test_Collision
	.IF EAX == TRUE
		;=======================
		; NO... we hit something
		;=======================

		;=============================
		; Place the piece in the grid
		;=============================
		INVOKE Place_In_Grid

		;=========================
		; Jmp & Return with False
		;=========================
		JMP	err

	.ELSE
		;===========================
		; yes we can drop down
		;===========================

		;=================================
		; Drop our piece down by a notch
		;=================================
		DEC	CurShapeY

	.ENDIF

done:
	;===================
	; We hit nothing
	;===================
	return TRUE

err:
	;===================
	; We hit something
	;===================
	return FALSE

Update_Shape	ENDP
;########################################################################
; END Update_Shape
;########################################################################

Wow! For somebody so important he sure doesn't do very much. Almost like real life, what do you think?

To begin with we make a call to test the collision status of the current shape. If the call returns TRUE then we can not move the shape anymore and need to place it in the grid. So he makes a call to Place_In_Grid(). However, if the call returns FALSE, then we can still move the shape. So, we drop it down a notch by decrementing the Y coordinate of the shape.

The last thing we need to do is return to our manager and tell him whether we succeeded or failed. Before we continue though, let's take a closer look at Test_Collision() and Place_In_Grid() since they are the ones who really do the work.


;########################################################################
; Place_In_Grid Procedure
;########################################################################
Place_In_Grid	PROC	

	;================================================
	; This function will place the current shape 
	; into the grid
	;================================================

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

	;===================================
	; Get the Current Shape Pos
	;===================================
	MOV	EBX, CurShape
	MOV	EAX, CurShapeFrame
	SHL	EAX, 2
	ADD	EBX, EAX
	MOV	CurLine, EBX

	;===================================
	; Set the Starting Row and Column
	; for the placement of the block
	;===================================
	MOV	EAX, CurShapeX
	MOV	EBX, CurShapeY
	MOV	DrawX, EAX
	MOV	DrawY, EBX

	;===================================
	; Loop through all four rows
	;===================================
	MOV	CurRow, 0
	.WHILE CurRow < 4
		;=====================================
		; Loop through all four Columns 
		;=====================================
		MOV	CurCol, 4
		.WHILE CurCol > 0 
			;===============================
			; 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 Block in our
				; BlockGrid to place it in
				;=============================
				MOV	EAX, DrawY
				MOV	ECX, 13
				MUL	ECX
				MOV	EBX, DrawX
				ADD	EBX, CurCol
				DEC	EBX
				ADD	EAX, EBX
				MOV	ECX, BlockGrid
				ADD	EAX, ECX

				;=============================
				; Store the Color in the Block
				; add one since we let 0 mean
				; the block is empty
				;=============================
				MOV	EBX, CurShapeColor
				INC	EBX
				MOV	BYTE PTR [EAX], BL

			.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

Place_In_Grid	ENDP
;########################################################################
; END Place_In_Grid
;########################################################################

;########################################################################
; Test_Collision Procedure
;########################################################################
Test_Collision	PROC	

	;================================================
	; This function will test for a collision between
	; the grid and the current shape
	;================================================

	;==============================
	; Local Variables
	;==============================
	LOCAL	Index:	DWORD
	LOCAL	Adjust:	DWORD

	;========================================
	; Loop through and find the first block
	; in each of the four columns
	;
	; NOTE: 0 = RIGHT 3 = LEFT
	;========================================
	MOV	Index, 0
	.WHILE Index < 4
		;==========================================
		; Start at the bottom of the Current Frame
		;==========================================
		MOV	EBX, CurShape
		MOV	EAX, CurShapeFrame
		SHL	EAX, 2
		ADD	EBX, EAX
		ADD	EBX, 3

		;=========================================
		; Now loop until we have a one in the 
		; current colum we are working on or we 
		; reach the top
		;==========================================
		MOV	Adjust, 4
		.WHILE	Adjust > 0
			;=======================
			; Get the Current Line
			;=======================
			XOR	EAX, EAX
			MOV	AL, BYTE PTR [EBX]

			;=======================
			; Adjust by the Column
			;=======================
			MOV	ECX, Index
			SHR	EAX, CL

			;=========================
			; Was there a block there
			;=========================
			.IF ( EAX & 1 )
				;======================
				; Yes there was a block
				;======================

				;=============================
				; Have we hit Bottom
				;=============================
				MOV	EAX, CurShapeY
				SUB	EAX, Adjust
				INC	EAX		; Off by 1 syndrome
				.IF EAX == 0
					;================
					; Bottom of grid
					;================
					JMP	done

				.ENDIF

				;===========================
				; Calculate the Block right 
				; under it on the grid
				;===========================
				DEC	EAX		; Move Under it
				MOV	ECX, 13
				MUL	ECX
				ADD	EAX, CurShapeX
				ADD	EAX, 3
				SUB	EAX, Index
				MOV	ECX, BlockGrid
				ADD	ECX, EAX

				;===========================
				; Does the Block have one
				; underneath it on the grid?
				;===========================
				MOV	AL, BYTE PTR [ECX]
				.IF AL != 0
					;===========================
					; We had a valid collision
					;===========================
					JMP	done
			
				.ENDIF

			.ENDIF
		
			;=================================
			; No Block -- Previous Line Please
			;=================================
			DEC	EBX

			;===============================
			; Decrement the Adjust counter
			;===============================
			DEC	Adjust

		.ENDW

		;==================================
		; Next Column Please!
		;==================================
		INC	Index

	.ENDW

err:
	;===================
	; We didn't collide
	;===================
	return FALSE

done:
	;===================
	; We collided
	;===================
	return TRUE

Test_Collision	ENDP
;########################################################################
; END Test_Collision
;########################################################################

The Place_In_Grid() function is the simpler of the two so let's cover that one first. It moves to the location in Grid Memory based upon where our current shape is located. Once there, it simply loops through every row in the frame and, if there is a block in that bit position, it sets the block to TRUE by indicating the current block texture + 1. The reason we had to do ( texture + 1 ) is we use zero to indicate that no blocks are there.

Test_Collision() is not quite so simple. It loops through all four columns, and, inside that, loops through all four rows of the current frame. It then tests the bit at its' own ( row, col ) location. If there is a bit turned ON there, it checks whether or not the grid has a block in the position directly under it. If it fails this test, on ANY bit, then the block can not be moved so we return TRUE. Otherwise, at the end, if there have been no collisions we return FALSE. At this point we also check to see if it is at the bottom of the grid. This constitutes the same thing as having a block underneath it.

As you an see, although Update() is very important to our team, he has so much to do that you ALWAYS want to have him delegate his responsibilities out to others. Then, just let him pretend to do what he is supposed to do.

NOTE: In case you missed my crude attempt at symbolism here is a quick explanation. YOU NEVER want to make functions that do many things. The ideal is to only have them accomplish one, maybe two things. Let a manager type function make calls, test for errors, and things like that.

Let's Get Moving!

Now that we have a piece to play with, we need to do just that, play with it. Get your minds out of the gutter!

Anyway, here is the code:


;########################################################################
; 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. 
	;
	; NOTE; It is missing the check for out of the grid on
	; rotation. That is left for the time being as an 
	; exercise. 
	;
	; My solution will be show in Article #5.
	;=======================================================

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

	;================================
	; Are they at the last frame?
	;================================
	.IF CurShapeFrame == 3
		;=====================================
		; Yep ... make sure they can rotate
		;=====================================
		
		;=========================================
		; Adjust to the current Block they are at
		;=========================================
		MOV	EAX, CurShapeY
		MOV	ECX, 13
		MUL	ECX
		ADD	EAX, CurShapeX
		ADD	EAX, BlockGrid
		MOV	CurBlock, EAX
	
		;========================================
		; Loop through all four rows of our Shape
		;========================================
		MOV	Index, 0
		.WHILE Index < 4
			;=======================
			; Get the current line
			;=======================
			MOV	EBX, CurShape	; Same as Frame 0
			ADD	EBX, Index
			XOR	ECX, ECX
			MOV	CL, BYTE PTR [EBX]
			MOV	Spot, CL

			;==============================
			; Test all 4 of the valid bits
			;==============================

			;=====================
			; Position 4
			;=====================
			.IF ( Spot & 8 )	; 2^3
				;=======================
				; Test this on the Grid
				;=======================
				MOV	EAX, CurBlock
				.IF ( BYTE PTR [EAX] ) != 0
					;======================
					; Failed! Can't rotate
					;======================
					JMP	err
				
				.ENDIF

			.ENDIF

			;=================
			; Inc our CurBlock
			;=================
			INC	CurBlock

			;=====================
			; Position 3
			;=====================
			.IF ( Spot & 4 )	; 2^2
				;=======================
				; Test this on the Grid
				;=======================
				MOV	EAX, CurBlock
				.IF ( BYTE PTR [EAX] ) != 0
					;======================
					; Failed! Can't rotate
					;======================
					JMP	err

				.ENDIF

			.ENDIF

			;=================
			; Inc our CurBlock
			;=================
			INC	CurBlock

			;=====================
			; Position 2
			;=====================
			.IF ( Spot & 2 )	; 2^1
				;=======================
				; Test this on the Grid
				;=======================
				MOV	EAX, CurBlock
				.IF ( BYTE PTR [EAX] ) != 0
					;======================
					; Failed! Can't rotate
					;======================
					JMP	err

				.ENDIF

			.ENDIF

			;=================
			; Inc our CurBlock
			;=================
			INC	CurBlock

			;=====================
			; Position 1
			;=====================
			.IF ( Spot & 1 )	; 2^0
				;=======================
				; Test this on the Grid
				;=======================
				MOV	EAX, CurBlock
				.IF ( BYTE PTR [EAX] ) != 0
					;======================
					; Failed! Can't rotate
					;======================
					JMP	err

				.ENDIF

			.ENDIF

			;========================
			; Drop Down by a line 
			; plus the amount we 
			; incrmented over by
			;========================
			SUB	CurBlock, 16

			;========================
			; Incrment our Index
			;========================
			INC	Index

		.ENDW

		;=======================
		; Ok ... start over
		;=======================
		MOV	CurShapeFrame, 0
		
	.ELSE
		;=====================================
		; NO ... make sure they can rotate
		;=====================================

		;=========================================
		; Adjust to the current Block they are at
		;=========================================
		MOV	EAX, CurShapeY
		MOV	ECX, 13
		MUL	ECX
		ADD	EAX, CurShapeX
		ADD	EAX, BlockGrid
		MOV	CurBlock, EAX
	
		;========================================
		; Loop through all four rows of our Shape
		;========================================
		MOV	Index, 0
		.WHILE Index < 4
			;=======================
			; Get the current line
			;=======================
			MOV	EBX, CurShape
			MOV	EAX, CurShapeFrame
			INC	EAX	; Get to new frame
			SHL	EAX, 2
			ADD	EBX, Index
			ADD	EBX, EAX
			MOV	CL, BYTE PTR [EBX]
			MOV	Spot, CL

			;==============================
			; Test all 4 of the valid bits
			;==============================

			;=====================
			; Position 4
			;=====================
			.IF ( Spot & 8 )	; 2^3
				;=======================
				; Test this on the Grid
				;=======================
				MOV	EAX, CurBlock
				.IF ( BYTE PTR [EAX] ) != 0 
					;======================
					; Failed! Can't rotate
					;======================
					JMP	err

				.ENDIF

			.ENDIF

			;=================
			; Inc our CurBlock
			;=================
			INC	CurBlock

			;=====================
			; Position 3
			;=====================
			.IF ( Spot & 4 )	; 2^2
				;=======================
				; Test this on the Grid
				;=======================
				MOV	EAX, CurBlock
				.IF ( BYTE PTR [EAX] ) != 0 
					;======================
					; Failed! Can't rotate
					;======================
					JMP	err

				.ENDIF

			.ENDIF

			;=================
			; Inc our CurBlock
			;=================
			INC	CurBlock

			;=====================
			; Position 2
			;=====================
			.IF ( Spot & 2 )	; 2^1
				;=======================
				; Test this on the Grid
				;=======================
				MOV	EAX, CurBlock
				.IF ( BYTE PTR [EAX] ) != 0 
					;======================
					; Failed! Can't rotate
					;======================
					JMP	err

				.ENDIF

			.ENDIF

			;=================
			; Inc our CurBlock
			;=================
			INC	CurBlock

			;=====================
			; Position 1
			;=====================
			.IF ( Spot & 1 )	; 2^0
				;=======================
				; Test this on the Grid
				;=======================
				MOV	EAX, CurBlock
				.IF ( BYTE PTR [EAX] ) != 0 
					;======================
					; Failed! Can't rotate
					;======================
					JMP	err

				.ENDIF

			.ENDIF

			;========================
			; Drop Down by a line 
			; plus the amount we 
			; incrmented over by
			;========================
			SUB	CurBlock, 16

			;========================
			; Incrment our Index
			;========================
			INC	Index

		.ENDW

		;========================
		; OK ... just increment
		;========================
		INC	CurShapeFrame
	
	.ENDIF

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

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

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

;########################################################################
; Move_Shape Procedure
;########################################################################
Move_Shape	PROC	Direction:DWORD

	;================================================
	; This function will move the shape in the 
	; desired direction
	;================================================

	;===========================
	; Local Variables
	;===========================
	LOCAL	CurCol:		DWORD
	LOCAL	CurRow:		DWORD
	LOCAL	CanMove:	DWORD

	;====================================
	; Set CanMove to true it will
	; be fasle later if we can't move
	;====================================
	MOV	CanMove, TRUE

	;================================================
	; Perform the Tests based on direction they want
	;================================================
	.IF Direction == MOVE_LEFT
		;====================================
		; They want to move to the left
		;====================================
		
		;====================================
		; Find the Left most column with a
		; valid block inside of it
		;====================================
		MOV	CurCol, 0
		.WHILE CurCol < 4
			;==========================
			; Calculate Our Mask
			;==========================
			MOV	EAX, 1
			MOV	ECX, 3	; Start from the Left
			SUB	ECX, CurCol
			SHL	EAX, CL
			MOV	EDX, EAX
			PUSH	EDX

			;===========================
			; Go through all 4 rows
			;===========================
			MOV	CurRow, 0
			.WHILE CurRow < 4
				;===============================
				; Get the Current Line of Blocks
				;===============================
				MOV	EBX, CurShape
				MOV	EAX, CurShapeFrame
				SHL	EAX, 2
				ADD	EBX, EAX
				ADD	EBX, CurRow
				XOR	ECX, ECX
				MOV	CL, BYTE PTR [EBX]

				;========================
				; Test the Mask and the
				; current line of blocks
				;========================
				POP	EDX
				PUSH	EDX
				.IF ( EDX & ECX )
					;====================
					; There was a Block
					;====================

					;====================
					; Calculate the 
					; block's X value
					;====================
					MOV	EAX, CurShapeX
					ADD	EAX, CurCol

					;====================
					; Can we move?
					;====================
					.IF EAX == 0
						;=============
						; Nope
						;=============
						MOV	CanMove, FALSE

					.ELSE
						;========================
						; Calculate the block to 
						; the left of us
						;========================
						MOV	EAX, CurShapeY
						SUB	EAX, CurRow
						MOV	ECX, 13
						MUL	ECX
						ADD	EAX, CurShapeX
						ADD	EAX, CurCol
						DEC	EAX	; 1 to the Left
						MOV	ECX, BlockGrid
						ADD	ECX, EAX
						MOV	AL, BYTE PTR [ECX]
								
						;======================
						; Are we blocked?
						;======================
						.IF AL != 0
							;================
							; We are blocked
							;================
							MOV	CanMove, FALSE
						
						.ENDIF

					.ENDIF
				
				.ENDIF

				;===========================
				; Inc our current row
				;===========================
				INC	CurRow

			.ENDW

			;===========================
			; Clean Off the stack
			;===========================
			POP	EDX

			;===========================
			; Inc our current column
			;===========================
			INC	CurCol

		.ENDW

		;==================================
		; Can we Still Move
		;==================================
		.IF CanMove == TRUE
			;=======================
			; yes we can
			;=======================
			DEC	CurShapeX
	
		.ENDIF

	.ELSEIF Direction == MOVE_RIGHT
		;====================================
		; They want to move to the right
		;====================================

		;====================================
		; Find the Right most column with a
		; valid block inside of it
		;====================================
		MOV	CurCol, 4
		.WHILE CurCol > 0
			;==========================
			; Calculate Our Mask
			;==========================
			MOV	EAX, 1
			MOV	ECX, 4	; Start from the Right
			SUB	ECX, CurCol
			SHL	EAX, CL
			MOV	EDX, EAX
			PUSH	EDX
			
			;===========================
			; Go through all 4 rows
			;===========================
			MOV	CurRow,0
			.WHILE CurRow < 4
				;===============================
				; Get the Current Line of Blocks
				;===============================
				MOV	EBX, CurShape
				MOV	EAX, CurShapeFrame
				SHL	EAX, 2
				ADD	EBX, EAX
				ADD	EBX, CurRow
				XOR	ECX, ECX
				MOV	CL, BYTE PTR [EBX]

				;========================
				; Test the Mask and the
				; current line of blocks
				;========================
				POP	EDX
				PUSH	EDX
				.IF ( EDX & ECX )
					;====================
					; There was a Block
					;====================

					;====================
					; Calculate the 
					; block's X value
					;====================
					MOV	EAX, CurShapeX
					ADD	EAX, CurCol
					DEC	EAX

					;====================
					; Can we move?
					;====================
					.IF EAX == 12
						;=============
						; Nope
						;=============
						MOV	CanMove, FALSE

					.ELSE
						;========================
						; Calculate the block to 
						; the right of us
						;========================
						MOV	EAX, CurShapeY
						SUB	EAX, CurRow
						MOV	ECX, 13
						MUL	ECX
						ADD	EAX, CurShapeX
						ADD	EAX, CurCol	; Already 1 to the
									; Right
						MOV	ECX, BlockGrid
						ADD	ECX, EAX
						MOV	AL, BYTE PTR [ECX]

						;======================
						; Are we blocked?
						;======================
						.IF AL != 0 
							;================
							; We are blocked
							;================
							MOV	CanMove, FALSE
						
						.ENDIF

					.ENDIF

				.ENDIF

				;===========================
				; Inc our current row
				;===========================
				INC	CurRow

			.ENDW

			;===========================
			; Clean Off the stack
			;===========================
			POP	EDX

			;===========================
			; dec our current column
			;===========================
			DEC	CurCol

		.ENDW

		;==================================
		; Can we Still Move
		;==================================
		.IF CanMove == TRUE
			;=======================
			; yes we can
			;=======================
			INC	CurShapeX
	
		.ENDIF

	.ELSEIF Direction == MOVE_DOWN
		;====================================
		; They want to move the piece down
		;====================================

		;====================================
		; Test for a collision
		;====================================
		INVOKE Test_Collision
		.IF EAX == FALSE
			;============================
			; It is safe to drop a notch
			;============================
			DEC	CurShapeY

		.ENDIF

	.ELSE
		;====================================
		; They passed an invalid direction
		;====================================
		JMP	err

	.ENDIF

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

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

Move_Shape	ENDP
;########################################################################
; END Move_Shape
;########################################################################

These two functions Rotate_Shape() and Move_Shape() are pretty big, so it is good they are twins. Yet, what they do, is fairly cut and dried. Let's cover, just in general, what it is they do.

The rotate function first decided if it is at the last frame. If so, it has code to wrap it around for all of the test, otherwise it just uses the next frame. Then, it loops through all of the bits, finding the valid ones, just like Test_Collision(). If there is already a bit set in the place the shape would be at then it is not allowed to move and the call fails. And ... that is that.

NOTE: Code is not in there to check for out of bounds on the grid. So, if you rotate at a corner, you may slide out of the grid and into the background area. This has been left as AN EXERCISE FOR YOU. I wanted to see something interactive come out of this article series, and I decided this would be as good of a place as any to start asking for it. I will present my solution in the next article. Compare yours to mine at that time.

Back to the code at hand: Move_Shape(). This function will move the shape to the left, or to the right, depending on the value passed into it. It merely tests the bits once again. Only this time we have to find the leftmost, or rightmost, valid bit in each row. Then, we check the grid block to the left or right and see if it is empty. Accordingly, either we move it, or we don't, and then return to the caller.

There isn't much else to talk about in this section. You have seen how to access everything many times now. The only thing that changes is the things we need to access, or the order in which we test stuff. These are the kinds of things that need to be resolved at design time.

Time to Clear the Bases?

When it comes time to clear the grid we call upon Line_Test. This will function will return TRUE if it clears a line. It will return false if it doesn't have a valid line on the grid.

Here is the code for it:


;########################################################################
; 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:
	;===================
	; We earned a line
	;===================
	return TRUE


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

The code loops through every line in our grid memory and test for blocks. If it finds that a grid location is empty then it continues with the next line. If every location has a valid block inside of it, then the function moves all of the memory above it to the row that had the line. It does this by calling the Win32 API function RTLMoveMemory().

We have it return after every valid line it finds, and eliminates, because when we want to keep score it will be easier to track how many lines they earn. It is always a good thing to keep future expansion in mind while programming.

The Final Batters

Our two final hitters are the publicity hounds Draw_Shape() and Draw_Grid(). Below is their code.


;########################################################################
; Draw_Shape Procedure
;########################################################################
Draw_Shape	PROC	

	;=======================================================
	; This function will draw our current shape at its
	; proper location on the screen
	;=======================================================

	;===========================
	; 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
	;===================================
	MOV	EBX, CurShape
	MOV	EAX, CurShapeFrame
	SHL	EAX, 2
	ADD	EBX, EAX
	MOV	CurLine, EBX

	;===================================
	; Set the Starting Row and Column
	; for the drawing
	;===================================
	MOV	EAX, CurShapeX
	MOV	EBX, CurShapeY
	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

				;=============================
				; Calculate the X coord
				;=============================
				MOV	EAX, DrawX
				ADD	EAX, CurCol
				DEC	EAX
				MOV	ECX, BLOCK_WIDTH
				MUL	ECX
				ADD	EAX, 251
				MOV	XPos, EAX

				;=============================
				; Calculate the surface to use
				;=============================
				MOV	EAX, CurShapeColor
				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
;########################################################################

;########################################################################
; Draw_Grid Procedure
;########################################################################
Draw_Grid	PROC	

	;=======================================================
	; This function will draw our grid. If the value is zero
	; there is no block otherwise the value is the block#
	;=======================================================

	;====================
	; Local Variables
	;====================
	LOCAL	CurRow:		DWORD
	LOCAL	CurCol:		DWORD
	LOCAL	CurBlock:	DWORD
	LOCAL	YPos:		DWORD
	LOCAL	XPos:		DWORD

	;============================
	; Start the current block at
	; the beggining of our grid
	;============================
	MOV	EAX, BlockGrid
	MOV	CurBlock, EAX

	;============================
	; Initialize the current row
	;============================
	MOV	CurRow, 0

	;=============================
	; Loop through all of our rows
	;=============================
	.WHILE CurRow < ( GRID_HEIGHT - 4 )
		
		;================================
		; Initialize the currrent column 
		;================================
		MOV	CurCol, 0
	
		;=============================
		; Loop through all of our cols
		;=============================
		.WHILE CurCol < GRID_WIDTH
			;========================
			; Is there a Block here
			;========================
			XOR	EAX, EAX
			MOV	EBX, CurBlock
			MOV	AL, BYTE PTR [EBX]
			.IF AL != 0 
				;=============================
				; Yes there was a block here
				;=============================

				;=============================
				; Get the surface to use
				;=============================
				DEC	EAX
				SHL	EAX, 2
				MOV	EBX, DWORD PTR BlockSurface[EAX]

				;=============================
				; Calculate the Y coord
				;=============================
				MOV	EAX, ( GRID_HEIGHT - 5 )
				SUB	EAX, CurRow
				MOV	ECX, BLOCK_HEIGHT
				MUL	ECX
				MOV	YPos, EAX

				;=============================
				; Calculate the X coord
				;=============================
				MOV	EAX, CurCol
				MOV	ECX, BLOCK_WIDTH
				MUL	ECX
				ADD	EAX, 251
				MOV	XPos, EAX

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

				;==============================
				; Did we succeed?
				;==============================
				.IF EAX == DDERR_SURFACELOST
					;======================
					; We lost the surface
					;======================


				.ELSEIF EAX != DD_OK
					;======================
					; We failed in some way
					;======================
					JMP	err

				.ENDIF

			.ENDIF

			;========================
			; Inc the Current Block
			;========================
			INC	CurBlock

			;========================
			; Incrment the Cur column
			;========================
			INC	CurCol

		.ENDW

		;====================
		; Incrment the row
		;====================
		INC	CurRow

	.ENDW

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

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

Draw_Grid	ENDP
;########################################################################
; END Draw_Grid
;########################################################################

If you have been able to keep up with this article series so far then this code should be a breeze to understand. Both of them do the same basic thing. The only difference between the two is that one operates on the current shape, and the other draws everything currently in the grid.

The basic idea is to loop through every bit in the frame for the shape, or every byte for the grid ( You should be very used to this looping concept by now ). Then, either we use the current shape color, or the number stored in the grid, to access the proper block texture to use. The rest involves a call to draw the surface on the back buffer.

The one thing that you need to make sure that you don't forget however is that you need to convert from grid to screen coordinates. If you do not then everything will be drawn at the left-hand side with A LOT of overlap.

Also, keep in mind that since we use the DX blitting function the back buffer must NOT be locked prior to the call. Make sure that if you did lock it that you unlocked it before you make the call, otherwise you will crash.

That is all that our functions do, and more importantly, all that we need to have on our team for the time being. Now, it is the manager's job to get the game running for us. So, let's go investigate "the loop."

The Loop and His Team

The loop is a very complex manager. He does his best to organize things though. He has state variables that way e doesn't process what he doesn't need to, and he also uses many different timers to make sure things are getting done. Have a look at his innards.


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

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

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

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

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

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

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

			;=============================
			; Re-Init the grid
			;=============================
			INVOKE Init_Grid

			;=============================
			; Get a new Starting Shape
			;=============================
			INVOKE New_Shape

			;====================================
			; Get starting time for the input
			;====================================
			INVOKE Get_Time
			MOV	Input_Time, EAX

			;====================================
			; Get starting time for the updates
			;====================================
			INVOKE Get_Time
			MOV	Update_Time, EAX

			;===============================
			; Set the Game state to playing
			;===============================
			MOV	GameState, GS_PLAY

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

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

			;===============================
			; Set the Game state to playing
			;===============================
			MOV	GameState, GS_PLAY

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

		.ENDIF


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

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

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

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


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

		.ENDIF


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

		;===============================
		; Load the main bitmap into the
		; back buffer
		;===============================
		INVOKE DD_Load_Bitmap, lpddsback, ptr_BMP_MAIN, \
			640, 480, screen_bpp

		;=====================================
		; Is it time to process input yet?
		;=====================================
		INVOKE Get_Time
		SUB	EAX, INPUT_DELAY
		.IF EAX  > Input_Time
			;==================
			; It is time.
			;==================
		
			;========================================
			; Read the Keyboard
			;========================================
			INVOKE DI_Read_Keyboard

			;================================
			; What do they want to do
			;================================
			.IF keyboard_state[DIK_ESCAPE]
				;========================
				; The return to menu key
				;========================
				MOV	GameState, GS_MENU

			.ELSEIF keyboard_state[DIK_UP]
				;======================
				; Rotate the shape
				;======================
				INVOKE Rotate_Shape

			.ELSEIF keyboard_state[DIK_DOWN]
				;======================
				; Move the shape down
				;======================
				INVOKE Move_Shape, MOVE_DOWN

			.ELSEIF keyboard_state[DIK_LEFT]
				;======================
				; Move the shape left
				;======================
				INVOKE Move_Shape, MOVE_LEFT

			.ELSEIF keyboard_state[DIK_RIGHT]
				;======================
				; Move the shape Right
				;======================
				INVOKE Move_Shape, MOVE_RIGHT

			.ENDIF

			;============================
			; Get a New Input Time
			;============================
			INVOKE Get_Time
			MOV	Input_Time, EAX

		.ENDIF

		;=====================================
		; Is it time to update the shape yet?
		;=====================================
		INVOKE Get_Time
		SUB	EAX, UPDATE_DELAY
		.IF EAX  > Update_Time
			;==================
			; It is time.
			;==================

			;===============================
			; Update the current shape
			;===============================
			INVOKE Update_Shape

			;===============================
			; Did we not succeed at updating
			;===============================
			.IF EAX == FALSE
				;=======================
				; They had a collision
				;=======================

				;=======================
				; Test for a line
				;=======================
				INVOKE Line_Test

				;=======================
				; Did they earn one?
				;=======================
				.WHILE EAX == TRUE
					;================
					; They got one
					;================

					;=================
					; Test for another
					;=================
					INVOKE Line_Test

				.ENDW

				;=======================
				; Start a new piece
				;=======================
				INVOKE New_Shape

				;=======================
				; Did we make it?
				;=======================
				.IF EAX == FALSE
					;===============
					; They died!
					;===============
					MOV	GameState, GS_DIE

				.ENDIF


			.ENDIF

			;============================
			; Get a New Update Time
			;============================
			INVOKE Get_Time
			MOV	Update_Time, EAX

		.ENDIF

		;===============================
		; Draw our current grid
		;===============================
		INVOKE Draw_Grid

		;===============================
		; Draw our current shape
		;===============================
		INVOKE Draw_Shape

		;===============================
		; Flip the buffers
		;===============================
		INVOKE DD_Flip

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

		;=================================
		; Wait for a couple of seconds so
		; they know that they have died
		;=================================
		INVOKE Sleep, 2000

		;=================================
		; ReInit the Grid
		;=================================
		INVOKE Init_Grid

		;=================================
		; Get a New shape
		;=================================
		INVOKE New_Shape

		;=================================
		; Back to the Main Menu
		;=================================
		MOV	GameState, GS_MENU

	.ENDIF

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

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

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

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

As you can see, if the user selects to have a new game, "loop" makes a bunch of calls. First he make a call to initialize our grid, then one to create a new shape. Next he makes a couple to get the starting time for input and the starting time for updates, and then, finally, he sets the game state to playing.

We have now entered the game. So, every frame, he draws the bitmap onto the back buffer and decides if it is time to process some input. If so, he makes a call to process the input and then he reacts based upon the keys that are pressed. With that completed he re-initializes the input time. Otherwise, if enough time hasn't passed, he skips the input phase altogether.

The same thing is done with Updating. He first finds out if it is time. If enough time has NOT elapsed, he skips over the updating and makes calls to draw everything. If it has, then he calls the update function and reacts to what update has to tell him. If Update fails then that means it is time for a new shape. But, first we call the test to see if there are valid lines. We keep doing this until no more valid lines exist, and then we create the new shape and re-init our update time.

If, during this time, the call to create a new shape fails then the user has died and the game state is set to reflect that. Finally he updates the display by flipping our primary and back buffers. Then he does it all over again, synchronizing it to the desired frame rate.

The one thing I want to comment on here is the use of time based updates. This is a very crucial part of developing a game. If we had updated the input every frame, things would be flying everywhere, and the user would be in a state of shock. The same thing with updating the shapes. Also, your machine may achieve 100 FPS and yet another machine would only be able to do 25 FPS. This means that, if you are using frame-based code, the game will look/react in different ways across the two machines.

By using time, you can close to guarantee it will look the same across all capable machines. The reason for this is time, unlike a frame rate, can not change across machines. The rate at which a second occurs is the same no matter what you run it on.

The code we have here is a simple implementation of this premise. You can definitely get more complex. Still, this code works, and works well for what it needs to do.

Until Next Time...

I told you we would cover a lot of material. We now have a fully working, albeit limited, game to show for all this work. It has all the rudimentary elements it needs. The only thing the game lacks is the bells and whistles that make it pretty.

The next three articles are going to cover those bells and whistles. Next time, we will be covering Direct Sound implementation, adding screen transitions, and I will show you my answer for the Rotation clipping code.

In the meantime, experiment with some things we haven't done yet. Or even just try tweaking some things that we have. Everybody has got to start someplace, and sometime. There is no time and place better to do so than the present. At worst you will crash your game ... and we have all done that. The important thing is to try, and to learn.

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

Get the source code and executable here

Discuss this article in the forums


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

See Also:
Win32 Assembly

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