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.
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
See Also: © 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
|