• 05/31/00 12:07 PM
    Sign in to follow this  
    Followers 0

    Win32 Assembly Part 6

    General and Gameplay Programming

    Myopic Rhino
    [size="5"]Where Did We Leave Off?

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

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

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

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

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

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


    [size="5"]Next Piece Please

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

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

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

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

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

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

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

    .endif

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


    [size="5"]I Can't See It!

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

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

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

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

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

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

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

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

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

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

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

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

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

    .endif

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

    .endw

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

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

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

    .endw

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

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

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

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

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

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

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


    [size="5"]The New Text

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


    [size="5"]Wanna See This Too?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


    [size="5"]Scoring and Levels

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

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

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

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

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

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

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

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

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

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

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

    .endif

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

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

    .endw

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

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

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

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

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

    .endif

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

    .endw

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

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

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

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


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

    ;########################################################################
    ; Adjust_Score Procedure
    ;########################################################################
    Adjust_Score proc amount:DWORD

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

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


    Adjust_Score ENDP
    ;########################################################################
    ; END Adjust_Score
    ;########################################################################

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

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

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

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

    [size="5"]Until Next Time...

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

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

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

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


    Sign in to follow this  
    Followers 0


    User Feedback

    Create an account or sign in to leave a review

    You need to be a member in order to leave a review

    Create an account

    Sign up for a new account in our community. It's easy!


    Register a new account

    Sign in

    Already have an account? Sign in here.


    Sign In Now

    There are no reviews to display.