Tagalong Todd Tutorial: Part 3
From Intellivision Wiki
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.
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 ENDP
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:
- a routine to call when someone presses a button on the keypad
- a routine to call when someone presses a side action button
- 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 ENDP
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 ;; ;; ======================================================================== ;; SINTBL PROC 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 ENDP
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. ;; ;; ======================================================================== ;; HIT_KEYPAD PROC JR R5 ENDP ;; ======================================================================== ;; ;; HIT_ACTION -- Someone hit an action button ;; ;; ======================================================================== ;; HIT_ACTION PROC JR R5 ENDP
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 ;; ;; ======================================================================== ;; HIT_DISC PROC 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.
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 ENDP
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 ;; ;; ======================================================================== ;; MOB_UPDATE PROC 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 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 branches on "minus" which skips over the add carry. This is the rounding down effect for a negative number.
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.
We're really close to having this demo fully functional. With the player moving around, we really only need to add Todd.