Tagalong Todd Tutorial: Part 3

From Intellivision Wiki
Revision as of 18:54, 8 December 2012 by Rickreynolds (talk | contribs) (That's it!)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

In this part of the tutorial, we'll add code that will read the hand controller and move our player figure around the screen in response.

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

Changes to our movement routine

In order to move the character figure around in a believable manner, we'll change the algorithm we've been using for the movement. Our last update just moved the figure via an offset. We'll change that here to include a velocity measure. As the user presses the disc, we'll speed up the character movement until it hits a maximum speed.

Introducing scanhand

A set of routines that read the hand controllers is provided with the SDK in a library file called scanhand.asm. It is built to be used with the TASKQ library and both pieces make assumptions about the other. At this point, it's enough to know that we need to allocate specifically named chunks of memory for the library routines.

            ; Hand-controller 8-bit variables
SH_TMP      RMB     1               ; Temp storage.
SH_LR0      RMB     3               ;\
SH_FL0      EQU     SH_LR0 + 1      ; |-- Three bytes for left controller
SH_LV0      EQU     SH_LR0 + 2      ;/
SH_LR1      RMB     3               ;\
SH_FL1      EQU     SH_LR1 + 1      ; |-- Three bytes for right controller
SH_LV1      EQU     SH_LR1 + 2      ;/

            ; Hand-controller 16-bit variables
SHDISP      RMB     1               ; ScanHand dispatch

We're also going to need more variables to store the new velocity measures we'll be manipulating.

PLYR        PROC
@@XP        RMB     1               ; X position
@@YP        RMB     1               ; Y position
@@XV        RMB     1               ; X velocity
@@YV        RMB     1               ; Y velocity
@@TXV       RMB     1               ; Target X velocity
@@TYV       RMB     1               ; Target Y velocity

The XP and YP variable should look familiar from part 2 of the tutorial. We're introducing two new pairs of values: a current velocity and a target velocity in each of the X and Y directions.

After those pieces of setup are done, we can get into the algorithm changes.

            ;; ------------------------------------------------------------ ;;
            ;;  Set up our hand-controller dispatch.                        ;;
            ;; ------------------------------------------------------------ ;;
            MVII    #HAND,  R0      ;\__ Set up scanhand dispatch table
            MVO     R0,     SHDISP  ;/

Here we're setting up the dispatch table that the scanhand routine expects. This probably warrants a bit of explanation. scanhand wants the addresses of three routines:

  1. a routine to call when someone presses a button on the keypad
  2. a routine to call when someone presses a side action button
  3. a routine to call when someone presses a disc direction

The addresses of those routines need to be provided in that order. So we see this chunk of code later in the file:

;; ======================================================================== ;;
;;  HAND    Dispatch table.                                                 ;;
;; ======================================================================== ;;
HAND        PROC
            DECLE   HIT_KEYPAD
            DECLE   HIT_ACTION
            DECLE   HIT_DISC

These lines setup the three routines that scanhand should call. Those routines need to be defined in our code - after all, scanhand doesn't know what we want to do when the player presses buttons or disc.

Precomputing velocities for various angles

;; ======================================================================== ;;
;;  SINTBL  -- Sine table.  sin(disc_dir) * 511                             ;;
;; ======================================================================== ;;
            DECLE   $0000
            DECLE   $00C3
            DECLE   $0169
            DECLE   $01D8
            DECLE   $01FF
            DECLE   $01D8
            DECLE   $0169
            DECLE   $00C3
            DECLE   $0000
            DECLE   $FF3D
            DECLE   $FE97
            DECLE   $FE28
            DECLE   $FE01
            DECLE   $FE28
            DECLE   $FE97
            DECLE   $FF3D

This is a lookup table that defines the values for sin(x) * 511 for the 16 directions that the directional disc can input. These will be the values we'll apply to the Y direction velocity as we move the player around the screen. Note that we don't need a cos(x) table because cos(x) = sin(90 - x) (for values of x in degrees). For our purposes here, since there are 16 directions: cos(direction) = sin(direction - 4). That is to say that the cosine value for a disc direction can be found by looking 4 entries earlier in this table (16 directions covers 360 degrees, so 4 directions covers 90 degrees).

Defining the three scanhand callbacks

;; ======================================================================== ;;
;;  HIT_KEYPAD -- Someone hit a key on a keypad.                            ;;
;; ======================================================================== ;;
            JR      R5

;; ======================================================================== ;;
;;  HIT_ACTION -- Someone hit an action button                              ;;
;; ======================================================================== ;;
            JR      R5

These two routines just return immediately when called. Basically, our game will do nothing on keypad presses or action button presses.

That leaves the directional disc algorithm.

When discussing the decoding of the information coming from scanhand, it is really kind of hard to make much sense without rewriting the documentation that comes in scanhand. When reading along with this part of the write-up, I suggest having the source code for scanhand.asm nearby. I'll be duplicating a small amount of what is written there just to make the tutorial simpler to follow, but there is a lot of good info in the comments in scanhand.asm.

Here's the short version (put together from choice info in scanhand.asm). Our routine will be given a control word, from which we can determine what occured. It will look like this:

       15                     9   8   7                         0
      |       RESERVED      |CTRL # |RLS|      Input Number      |
  • The Input Number bit pattern will tell us what was pressed.
  • The RLS bit tells us if this is a "press down" or a "release up" event.
  • The CTRL # bits tell us which controller was pressed.

On to the code.

;; ======================================================================== ;;
;;  HIT_DISC   -- Someone hit a directional disc                            ;;
;; ======================================================================== ;;
            PSHR    R5

The standard saving of R5 onto the stack so we know where to return.

            ANDI    #$FF,   R2      ; Ignore controller number

scanhand puts the control word into R2. For our demo, we don't care about left vs. right controller, so we'll just wipe out the top half of the word - that includes the RESERVED area as well as the two controller number bits.

            CMPI    #$80,   R2
            BLT     @@pressed

Here we'll check the RLS bit to see if this is a "press down" event. If it is, we should process movement one way; if this is a "release up" event, we'll do something different. The logic for release is coming next, we'll branch to @@pressed for a disc press.

            CLRR    R0
            MVO     R0,     PLYR.TXV
            MVO     R0,     PLYR.TYV
            PULR    PC

In the event of a disc release, we'll set our target velocity to zero in both the X and Y directions. If the player releases the disc, he is indicating a desire to stop movement. That's all we need to do in the case of a release, so we use PULR PC to bail out of this routine immediately.

The next chunk of code takes the direction the player is pressing on the directional disc and sets the X and Y target velocity values accordingly. Line by line, it goes like this:

@@pressed:  MOVR    R2,     R1

Make a copy of the direction data (in R1). We'll use this copy for the cosine value.

            ADDI    #4,     R1

Adjust the R1 (cosine pointer) by 4 entries in the sine table. Again, that works because cos(x) = sin(90 - x).

            ANDI    #$F,    R1

If we moved off the end of the 16 values in the sine table, this operation handles the wrap-around because of the way that binary numbers work in this case. (clever!)

            ADDI    #SINTBL,R2          ; sine pointer

R2 contains the POSITION in the table of sine values for the value that we care about. By adding the base address of the table itself, we get a memory pointer to the sine value we want.

            ADDI    #SINTBL,R1          ; cosine pointer

Do the same as above for R1 (the cosine value).

            MVI@    R2,     R2          ; sine for our direction

Here we overwrite the R2 value with the sine value we are fetching. Before this operation, R2 was pointing at the value we wanted, now it contains the value.

            MVI@    R1,     R1          ; cosine for our direction

Again, the same for R1.

            NEGR    R2

We negate the velocity value for sine just to make it consistent with the addressing scheme of the screen. Y values start at 0 at the top of the screen and get larger as you go down.

            MVO     R2,     PLYR.TYV    ; Set our target Y velocity to sine

And finally, take the computed target Y velocity (really just the adjusted lookup value from the sine table) and stick it into our data structure.

            MVO     R1,     PLYR.TXV    ; Set our target X velocity to cosine

Again, the same for R1 - this time for the X velocity.

            PULR    PC

End procedure as normal.

That routine really only computed a current "target velocity." So we know what the player is attempting to do by looking at PLYR.TXV and PLYR.TYV. We'll make changes to our MOB_UPDATE function that will incorporate those values and move the player on the screen.

Positioning the player

;; ======================================================================== ;;
;;  MOB_UPDATE -- This updates the player's position                        ;;
;; ======================================================================== ;;
            PSHR    R5

This procedure opening should look very familiar.

            ;; ------------------------------------------------------------ ;;
            ;;  Bring our actual velocity closer to our target velocity,    ;;
            ;;  and apply the velocity to our position.                     ;;
            ;; ------------------------------------------------------------ ;;

As can be seen from this comment, we're going to do the math necessary to bring our current velocity closer to the target that we've computed in the scanhand call-back routine. The approach we'll take here will be to compute the difference between the target velocity and the current velocity, then adjust the velocity towards the target by 1/4 of that difference. That way the speed of the player adjusts in a way that is noticeable in gameplay.

The first chunk deals with the X direction.

            MVI     PLYR.TXV,   R0
            SUB     PLYR.XV,    R0

Put the target velocity into R0, then subtract the actual velocity. R0 will now contain the difference between them.

            SARC    R0

SARC is shift right, which effects a divide by 2. It also shifts the right-most bit into the carry flag. In terms of the math, that works just fine except for the case when the difference is very small, i.e. only 1. When we SARC the value of 1 (computing 1/2), we'll get 0 left in our register as our value to adjust by, and that will mean that the actual velocity will never really converge on the target velocity. So in that case, we'll just add that 1 back to our register as a way of rounding that 1/2 value back up.

However if the value is negative we don't want to add one back for the rounding effect. We'll just let that value fall away (rounding down).

            BMI     @@nr0

BMI branches on "minus" which skips over the add carry. This is the rounding down effect for a negative number.

            ADCR    R0

Here we're adding the 1 back to our number -- rounding up.

@@nr0       SARC    R0
            BMI     @@nr1
            ADCR    R0

And these three instructions do it all again, making our adjustment 1/4 of the overall difference we computed.

@@nr1       ADD     PLYR.XV,    R0
            MVO     R0,         PLYR.XV
            ADD     PLYR.XP,    R0
            MVO     R0,         PLYR.XP

Now we add this computed difference to the player X velocity, and then add that velocity value to the player's X position. So we now have a new position for the player that is reflective of how the player has pressed the directional disc!

            MVI     PLYR.TYV,   R0
            SUB     PLYR.YV,    R0
            SARC    R0
            BMI     @@nr2
            ADCR    R0
@@nr2       SARC    R0
            BMI     @@nr3
            ADCR    R0
@@nr3       ADD     PLYR.YV,    R0
            MVO     R0,         PLYR.YV
            ADD     PLYR.YP,    R0
            MVO     R0,         PLYR.YP

And there is a second set of identical operations for the Y velocity.

And the rest...

The rest of the routine hasn't changed from the last tutorial part. We just changed out how we calculated the positions and our rendering of the player graphic is exactly the same.

Wrapping it up

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

The library includes.

Build it and try it!

You should now be able to move the character around the screen via the hand controllers.

That's it!

We're really close to having this demo fully functional. With the player moving around, we really only need to add Todd.