Tagalong Todd Tutorial: Part 1

From Intellivision Wiki
Revision as of 13:18, 18 November 2011 by Rickreynolds (talk | contribs)
Jump to: navigation, search

This part of the tutorial will show you how to draw a basic title screen, then put the player graphic onto the screen.

The complete source for this tutorial (it's a subset of the full tagalong.asm found in the SDK) is here.

The Title screen

The code to produce the title screen doesn't involve anything more complicated than what you've already done for the "Hello World" examples. So I'll just dump that code out here with minimal comment.

            ROMW    16              ; Use 16-bit ROM

;------------------------------------------------------------------------------
; Include system information
;------------------------------------------------------------------------------
            INCLUDE "gimini.asm"

Depending on the version of "Hello World" you worked through, you may not have seen this include statement before. Basically, gimini.asm just contains a lot of nice mnemonic definitions about the INTV system that will allow us to use identifiers like C_BLU (to indicate the code for the color blue) instead of having to always use hard to remember numeric codes.

;------------------------------------------------------------------------------
; EXEC-friendly ROM header.
;------------------------------------------------------------------------------
            ORG     $5000           ; Use default memory map
ROMHDR:     BIDECLE ZERO            ; MOB picture base   (points to NULL list)
            BIDECLE ZERO            ; Process table      (points to NULL list)
            BIDECLE MAIN            ; Program start address
            BIDECLE ZERO            ; Bkgnd picture base (points to NULL list)
            BIDECLE ONES            ; GRAM pictures      (points to NULL list)
            BIDECLE TITLE           ; Cartridge title/date
            DECLE   $03C0           ; No ECS title, run code after title,
                                    ; ... no clicks
ZERO:       DECLE   $0000           ; Screen border control
            DECLE   $0000           ; 0 = color stack, 1 = f/b mode
ONES:       DECLE   C_BLU, C_BLU    ; Initial color stack 0 and 1: Blue
            DECLE   C_BLU, C_BLU    ; Initial color stack 2 and 3: Blue
            DECLE   C_BLU           ; Initial border color: Blue
;------------------------------------------------------------------------------

This should be pretty familiar from the "Hello World" example.

   ;; ======================================================================== ;;
   ;;  TITLE  -- Display our modified title screen & copyright date.           ;;
   ;; ======================================================================== ;;
   TITLE:      PROC
               STRING  102, "Tagalong Todd", 0
               BEGIN
             
               ; Patch the title string to say '=JRMZ=' instead of Mattel.
               CALL    PRINT.FLS       ; Write string (ptr in R5)
               DECLE   C_WHT, $23D     ; White, Point to 'Mattel' in top-left
               STRING  '=JRMZ='        ; Guess who?  :-)
               STRING  ' Productions'
               BYTE    0
             
               CALL    PRINT.FLS       ; Write string (ptr in R1)
               DECLE   C_WHT, $2D0     ; White, Point to 'Mattel' in lower-right
               STRING  '2002 =JRMZ='   ; Guess who?  :-)
               BYTE    0
             
               ; Done.
               RETURN                  ; Return to EXEC for title screen display
               ENDP

This is the title screen, again hopefully this is familiar territory. If you have any questions about this code, I recommend going back through the "Hello World" tutorials again.

   ;; ======================================================================== ;;
   ;;  MAIN:  Here's our main program code.                                    ;;
   ;; ======================================================================== ;;
   MAIN:       PROC
               BEGIN
   
               CALL    CLRSCR          ; Clear the screen
   
               RETURN                  ; Return to the EXEC and sit doing nothing.
               ENDP

At this point, we'll just clear the screen and do nothing.

   ;; ======================================================================== ;;
   ;;  LIBRARY INCLUDES                                                        ;;
   ;; ======================================================================== ;;
               INCLUDE "print.asm"       ; PRINT.xxx routines
               INCLUDE "fillmem.asm"     ; CLRSCR/FILLZERO/FILLMEM

And some library functions. That's about it. This gets us a title screen; the full code for this much is in tag0.asm. We now have even less functionality working than we had with the "Hello World" example! But we're going to be adding to it immediately.

Adding a player graphic

In order to add a player graphic to the screen, we need to tackle a couple of general concepts that will be used in the real game. It is certainly possible to draw a graphic to the screen without doing everything I'll be going over, but you wouldn't be able to build much on code that was that simple. In order for our code to be useful in a real game at some point, we really need to build in some support so that the Intellivision can draw to the screen within game play.

To make that happen, we'll learn about two different game concepts here: "shadowing" the data that will be drawn on the screen, and the taskq library provided with the SDK.

Shadowing display data

I don't want to get too technical here because I want this tutorial to stay at an introductory level. all the intricacies of this subject (yet). But basically, you can think of games on the Intellivision (and on most gaming platforms) as a large loop of instructions running over and over again. One of the things that happens in this master loop is the drawing of the screen. While the screen update is happening, we don't want to be doing anything computationally expensive because there just isn't much time. (For more detailed info, look for VBLANK in the documentation).

A strategy for handling this is to keep a copy of the data that needs to be displayed somewhere in the program so that the copy can be updated at any point. This copy is usually called a "shadow". Then when the Intellivision is updating its display, we can simply have it copy the shadow data to the real display registers - something that is simple and fast. This is the strategy we'll follow for this example game.

So what data will we need to treat in this manner? Any data that will be changing the game's display. In this example we'll only be using 1 sprite graphic called a Moveable OBject or MOB. So we'll need to shadow the data that controls the first MOB. (For more info on the display and MOBs, look for STIC and MOB in the documentation).

Data to shadow for MOB 0

If you look in the gimini.asm library file, you'll see some ASCII art diagrams that detail what pieces of data are used to control the different MOBs. For our purposes, we're interested in updating the MOB's location (X and Y coordinates), visibility (we want our MOB to be visible), and color. Those chunks of data are controlled via registers in the STIC. As it turns out the registers that control all of these data items can be addressed in one block of address space. It looks like this:

   STIC address
   |
   v
   +----------------------------+----------------------------+----------------------------+
   | 'X' registers for MOBs 0-7 | 'Y' registers for MOBs 0-7 | 'A' registers for MOBs 0-7 |
   +----------------------------+----------------------------+----------------------------+

The X registers contain (among other things) the X coordinates and visibility flags for the 8 MOBs. The Y registers contain (among other things) the Y coordinates for the 8 MOBs. The A registers contain (among other things) the color information for the 8 MOBs. So in order for us to update those registers, we will do a copy of memory from a buffer to these registers. The simplest way to do that is to define a contiguous block of 24 words so that we can just copy that into these registers in one efficient loop. This is slightly wasteful in terms of space (we won't be storing or changing data for MOBs 1-7, only MOB 0), but it is simple.

The other info we need to define for the MOB is the graphic image itself. That will also be done via a buffer that will be copied into the right place during the display update (VBLANK) time.

Setup the data

So we'll need a 24 word buffer:

               ; STIC shadow
   STICSH      RMB     24              ; Room for X, Y, and A regs only.

We'll define this in an area of scratch memory along with some other buffers we'll be defining shortly.

Another couple of data items we'll want to have in a stored location are the X and Y coordinates of the player MOB. Technically at this point we could just store these in the STICSH buffer we just defined. But by having a separate dedicated location for these, we can store those coordinates in any method we might choose instead of only being able to store the position in numbers of pixels.

So defining the MOB X and Y positions:

   PLYR        PROC
   @@XP        RMB     1               ; X position
   @@YP        RMB     1               ; Y position
               ENDP

We've chosen to define this as a named data structure. This is a bit of overkill for just two items, but we're going to be adding more later. So we'll have PLYR.XP and PLYR.YP available, each storing one word which can represent the player's position.

The last piece of data setup that we'll need is the data defining the graphic image of our player. We'll do that after we discuss ISRs below.

Code to copy the shadow data

Now that we have storage defined for our "shadow STIC" area and we've defined out graphic image, we need to write some code that will copy that data into the right spots. To that end, we'll create a proc that will update the MOB data for us. This will do whatever computations are necessary to populate the shadow STIC area. The code that copies the data from the shadow area into the actual display area will come later.

At the start of the MOB_UPDATE routine, we push R5 onto the stack to save its value because we're going to use it in our routine. R5 will contain the address that the function should return to after the routine is finished. If you check the bottom of the routine, you can see a corresponding PULR PC call that pulls this address off the stack and puts it back into the program counter (R7).

               MVII    #@@mobr,    R4      ; MOB information template
               MVII    #STICSH,    R5

These two lines define two pointers that we will use to update the shadow STIC area. We've chosen R4 and R5 because they auto-increment during indirect access which will make the coding a bit simpler. R5 points to the shadow STIC buffer we defined earlier, and R4 points to a little local buffer at the end of the routine. That buffer contains bit patterns that we will use to mask our values before storing them.

               MVI     PLYR.XP,    R0      ;\
               SWAP    R0                  ; |
               ANDI    #$00FF,     R0      ; |- Player X position
               XOR@    R4,         R0      ; |
               MVO@    R0,         R5      ;/

To understand this chunk of code, we need to understand that the player's X position will eventually be stored as a fixed point data item, with the integer part in the upper 8 bits and the fractional part in the lower 8 bits. The code then takes the player's X position, SWAPS its bytes so that the integer part is now in the lower 8 bits, then uses the ANDI operation to drop off the fractional part. It then XORs that position with the first word at our local buffer at @@mobr (pointed to via R4). That first word is a bit pattern from gimini.asm that will set the visibility bit. Of course, all this math was done in the R0 register, and that value is then copied into the first word of our shadow STIC buffer (pointed to via R5).

At this point, both R4 and R5 are now pointing to the next word in their respective buffers, because we used them in indirect mode. So R4 is now pointing to the next word in our @@mobr buffer, and R5 is pointing to the second word in our shadow STIC. But that word will shadow the X coordinate of MOB1 (which we're not using at this point, but the data space is there nonetheless). We need to adjust the pointer.

               ADDI    #7,         R5      ;  Move pointer to Y coordinate section of the STICSH

This line advances it to the Y coordinate section.

               MVI     PLYR.YP,    R0      ;\
               SWAP    R0                  ; |
               ANDI    #$007F,     R0      ; |- Player Y position
               XOR@    R4,         R0      ; |
               MVO@    R0,         R5      ;/

The next chunk of code is almost identical for the player Y position. The mask value of $007F is due to the fact that the Y position is only stored in the lowest 7 bits rather than the lowest 8 bits like the X position was.

               ADDI    #7,         R5      ; Move pointer to A register section of the STICSH

R4 is at the next word in our local buffer but we need to advance R5 again.

               MVI@    R4,         R0      ; \_ Player's A register
               MVO@    R0,         R5      ; /

This just copies the data from our local buffer into the shadow STIC. This is to set the color of the MOB.

               CLRR    R0
               MVO     R0,         MOB_BUSY

This bit of code puts a zero into the MOB_BUSY location. Let's postpone discussion of this item until we get to the section on the ISR.

               PULR    PC

As mentioned above, this sets the PC to the address the program should return to.

All that's left is to setup the code such that our MOB_UPDATE routine gets called at appropriate times. We'll do that via an Interrupt Service Routine (or ISR). The SDK already has good documentation of ISRs, so we won't get into all the details of that here. Our ISR is going to employ the taskq library, so we'll discuss that next.

Using the taskq library

The comments in the taskq.asm file really describe all that is necessary to use the taskq library. So I won't duplicate that documentation here. I'll only touch on how we are using the taskq library in this example program.

Without getting too detailed, the taskq library allows you to define tasks (procedures) that need to be executed in order. The queue takes care of executing the tasks for you, and you can keep adding tasks to be executed based on whatever criteria the game requires - including having tasks which queue other tasks.

The taskq library relies on various specially named storage locations being defined in the program. For this tutorial, we'll just note the values used and leave it as an exercise for the reader to read up in the taskq comments for more details.

The taskq related definitions and storage for this game:

   TSKQM       EQU     $7              ; Task queue is 8 entries large
   MAXTSK      EQU     1               ; Only one task
   
   TSKQHD      RMB     1               ; Task queue head
   TSKQTL      RMB     1               ; Task queue tail
   TSKDQ       RMB     2*(TSKQM+1)     ; Task data queue
   TSKACT      RMB     1               ; Number of active tasks
   
   TSKQ        RMB     (TSKQM + 1)     ; Task queue
   TSKTBL      RMB     (MAXTSK * 4)    ; Timer task table

Basically, it is enough at this stage to understand that these are defined as part of using the taskq library.

Let's take a look at the ISR procedure itself:

   ISR         PROC
   
               ;; ------------------------------------------------------------ ;;
               ;;  Basics:  Update color stack and video enable.               ;;
               ;; ------------------------------------------------------------ ;;
               MVO     R0,     STIC.viden  ; Enable display
               MVI     STIC.mode, R0       ; ...in color-stack mode

These lines do exactly what the comments say. It's called the "VBLANK handshake", and it tells the STIC to draw the screen for this frame. Just writing something to .viden (which you can see maps to address $20 if you look in gimini.asm) enables display, and reading STIC.mode tells it to be in color-stack mode. Discussion of the different video modes is beyond the scope of this tutorial. Suffice it to say that this code enables drawing on the screen.

               MVII    #C_GRY, R0          ;\
               MVO     R0,     STIC.cs0    ; |__ Set display to grey
               MVO     R0,     STIC.cs2    ; |
               MVO     R0,     STIC.bord   ;/

These lines set the colors for the background and the border to grey. Again, you can find more detail by reading comments in gimini.asm and the STIC documentation.

               ;; ------------------------------------------------------------ ;;
               ;;  Update STIC shadow and queue updates for MOB velocities.    ;;
               ;; ------------------------------------------------------------ ;;
               MVI     MOB_BUSY, R0
               TSTR    R0
               BNEQ    @@no_mobs
               MVO     PC,     MOB_BUSY

Now we're in a good position to talk about what we're doing with the MOB_BUSY location. This is serving as a flag to make sure that we don't bother copying our shadow STIC area unless we need to.

In detail, this code copies the current value from MOB_BUSY into R0 so it can be tested via the TSTR instruction. This will set the zero flag if the value is zero, clearing that flag if it isn't zero. BNEQ (Branch on Not EQual) will branch if the zero flag is NOT set - effectively making that opcode mean "branch if not zero", indeed a synonym is BNZE for Branch if Not ZEro. So if MOB_BUSY is not zero, then the next chunk of code will be skipped as the PC moves down to the @@no_mobs label.

The first thing executed if the MOB_BUSY location is zero, is to set the current value of the PC to it making it non-zero. So we won't enter this section of code again until that location is reset back to zero when MOB_UPDATE next runs.

               CALL    MEMCPY              ;\__ Copy over the STIC shadow.
               DECLE   $0000, STICSH, 24   ;/

And here's where the magic happens - we copy our STIC shadow area into the actual STIC registers. You can look at the comments for the MEMCPY routine to see that it gets its argument list from the bytes immediately following the call itself. In this case the arguments we're sending are '$0000, STICSH, 24' which will tell MEMCPY to copy 24 words of memory from our STICSH buffer to memory location 0 - which is the start of the STIC MOB registers.

               MVII    #MOB_UPDATE, R0
               JSRD    R5,   QTASK

These lines will stick the address of MOB_UPDATE into the task queue so that it gets run again.

   @@no_mobs:  
   
               CALL    DOTIMER             ; Update timer-based tasks.

This is necessary to make sure that the taskq library updates any tasks that rely on timers. Consider this part of the contract of using the taskq library.

               B       $1014               ; return from interrupt.
               ENDP

And the end of the ISR. You can think of $1014 as the magic address that the Intellivision expects returns from ISRs to branch back to. More detail is (as usual) available in the SDK docs.

Getting our player graphic into the system

The last piece of this puzzle is to define the graphic image for our player and write the code that copies that data into the right place so that it can be displayed. You can look up how to format the data so that it is simply copyable into the display area in the docs, but the gist of it is that we can define it like this:

   ;; ======================================================================== ;;
   ;;  GRAMIMG -- Custom graphics to load into GRAM.                           ;;
   ;; ======================================================================== ;;
   GRAMIMG     PROC
   
   @@person:   ; Crappy person graphic.
               DECLE   %00010000
               DECLE   %00111000
               DECLE   %00111000
               DECLE   %00010000
               DECLE   %00010000
               DECLE   %01111100
               DECLE   %10111010
               DECLE   %10111010
               DECLE   %10111010
               DECLE   %10111010
               DECLE   %00111000
               DECLE   %00101000
               DECLE   %00101000
               DECLE   %00101000
               DECLE   %00101000
               DECLE   %01101100
   @@end:
               ENDP

You can see the shape as the 1's in among the 0's. The data labels are niceties that will allow us to reference the beginning and end of the data space via those labels in our code, allowing us to calculate the length of the data block rather than having to count up the number of words used.

The code that copies this data into the Graphics RAM area (GRAM) has to be handled specially, because we can only copy this data into place during the VBLANK time period. That's when the Intellivision has access to the STIC GRAM data area. What this means is that we need to copy this data into place during an ISR. We'll do that by writing an ISR that only runs once. It will copy the data into place and then immediately setup our real ISR that will handle things from that point forward.

   ;; ======================================================================== ;;
   ;;  INITISR -- Copy our GRAM image over, and then do the plain ISR.         ;;
   ;; ======================================================================== ;;
   INITISR     PROC
               PSHR    R5

Standard start to a proc.

               CALL    MEMCPY
               DECLE   $3800, GRAMIMG, GRAMIMG.end - GRAMIMG

This copies the above memory buffer into the right area. As before, you can see how MEMCPY is working here. It will copy the buffer starting at GRAMIMG with length of (GRAMIMG.end - GRAMIMG) into the memory locations at $3800.

               MVII    #ISR,   R0
               MVO     R0,     ISRVEC
               SWAP    R0
               MVO     R0,     ISRVEC + 1

This little chunk of code is what "wires in" a procedure so that it gets called during the VBLANK time period when the Intellivision has access to the STIC registers. You can see in the listing that ISRVEC is defined to be memory location $100. In this case, we're setting things up so that our real ISR gets called from here on for VBLANK updates.

But how will this ISR itself be called? We'll set that up in MAIN, which we'll detail next.

               PULR    PC
               ENDP

And the standard ending of a proc.

The MAIN routine

And now for MAIN.

   ;; ======================================================================== ;;
   ;;  MAIN:  Here's our main program code.                                    ;;
   ;; ======================================================================== ;;
   MAIN:       PROC
               DIS

This opcode will disable interrupts while we get things setup.

               MVII    #STACK, R6      ; Set up our stack

We need to setup a chunk of memory for the system to use as a stack, and then set R6 to the starting address (i.e. the top of the stack). Note that you may want to use the alias 'SP' for R6 because of its use as a stack pointer.

               MVII    #$25E,  R1      ;\
               MVII    #$102,  R4      ; |-- Clear all of RAM memory
               CALL    FILLZERO        ;/

This code just clears all of the RAM in the Intellivision. A good idea. You can see that it is calling FILLZERO. FILLZERO expects to find its inputs via R1 (the length of the memory to be cleared) and R4 (the starting address of the memory to be cleared). This will clear out memory from $102 to $360, which is all the RAM in the machine.

               MVII    #INITISR, R0    ;\    Do GRAM initialization in ISR.
               MVO     R0,     ISRVEC  ; |__ INITISR will the point to the 
               SWAP    R0              ; |   regular ISR when it's done.
               MVO     R0,     ISRVEC+1;/    

This chunk of code should look very similar. We just discussed how to wire-in an ISR routine. You should now be able to see what we're doing here. We're going to have INITISR called first, which will copy the person graphic into the GRAM area and then wire-in the real ISR. Basically, INITISR is doing a little setup for us that can only be done via an ISR.

               ;; ------------------------------------------------------------ ;;
               ;;  Put the character on the screen                             ;;
               ;; ------------------------------------------------------------ ;;
               MVII    #$1000, R0
               MVO     R0,     PLYR.XP
               MVO     R0,     PLYR.YP

Here we're putting reasonable values as starting values into the X and Y positions for the player. Specifically we're setting both X and Y to $1000. $1000 will look like this bitwise (showing both bytes as halves): 00010000 00000000. When we discussed the algorithm for populating our shadow STIC, we discussed that we are going to represent our position on the screen as a fixed decimal number. So the most significant bits (MSBs) will be the integer part and the least significant bits (LSBs) will be the fractional part. By putting the value of $1000 into X and Y, we are positioning the player at (16.0, 16.0) or (16,16) a spot that is comfortably visible on the screen in the upper left quadrant of the full display resolution of 160x96.

               EIS

This instruction enables interrupts again. Basically this is the "GO!" command to make things start running!

               ;; ------------------------------------------------------------ ;;
               ;;  Fall into the RUNQ.  We should never exit the RUNQ in this  ;;
               ;;  demo, since we never call SCHEDEXIT.                        ;;
               ;; ------------------------------------------------------------ ;;
               CALL    RUNQ            ; Run until a SCHEDEXIT happens

This tells the taskq engine to start executing tasks. As the comments indicate, the taskq engine calls tasks that get queued up until someone calls SCHEDEXIT. We haven't put a call to SCHEDEXIT in any of our tasks or our ISR, so this shouldn't happen. Effectively, this "game" will run forever.

               ;; ------------------------------------------------------------ ;;
               ;;  If a SCHEDEXIT *does* happen (say, due to a bug), crash     ;;
               ;;  gracefully.                                                 ;;
               ;; ------------------------------------------------------------ ;;
               CALL    PRINT.FLS
               DECLE   C_RED, $200 + 11*20
                       ;01234567890123456789
               STRING  "SCHEDEXIT WAS CALLED",0

The comments here pretty much tell the story. If we somehow call SCHEDEXIT accidentally, we'd like to know that. This will put a message onto the screen in red letters.

               DECR    PC              ; Can't get here
               ENDP

Again, we should never get here. But if it happens, this line of code will cause the Intellivision to just spin here forever. Specifically, decrementing the program counter (PC, aka R7) will cause the PC to move back one word such that it points again to the 'DECR PC' instruction. This works because 'DECR PC' is one word in length.

And the includes

   ;; ======================================================================== ;;
   ;;  LIBRARY INCLUDES                                                        ;;
   ;; ======================================================================== ;;
               INCLUDE "print.asm"       ; PRINT.xxx routines
               INCLUDE "fillmem.asm"     ; CLRSCR/FILLZERO/FILLMEM
               INCLUDE "memcpy.asm"      ; MEMCPY
               INCLUDE "timer.asm"       ; Timer-based task stuff
               INCLUDE "taskq.asm"       ; RUNQ/QTASK

A couple new includes need to be added as well.

That's it!

Assemble and run this little program and you should see a custom title screen, followed by our player graphic standing still. Wow, not much at all... Such is the nature of assembly programming! But really, we've put a lot into this program that is quite useful. We'll expand on these concepts in the next tutorials as we work ever closer towards complete understanding of the Tagalong Todd example.

Back to Programming Tutorials