This page will only be of interest to Atari 8-bit computer users who like to program in the Action! programming language. (In fact, one target audience member is "future me", so that when I come back to do more, I'll understand what it was I did this time around.)
This is a work in progress. My objective is to dust off some cobwebs in my brain regarding coding in Action!, as well as to learn some of the tried & true tricks that commercial games (and "machine language" type-in programs in magazines) used. In particular, use of the Vertical Blank Interrupt to handle the motion (smoothly) and drawing (without flicker) of Player/Missile Graphics (aka "sprite" on other systems) objects on the screen.

It's a simple two-player, joystick-controlled tank simulation, which uses Player graphics for the tanks, Missiles for the bullets, and redefined character set graphics in a large (20x12) colored text mode (a Playfield in Atari terms) for the terrain (some brick walls and a river). It's based on Atari 2600 "Combat" (in turn based on the arcade game "Tank").
Currently (2015-05-13) there's no real objective, there are undoubtedly bugs, and some aspects of player 2's tank don't work (I simply haven't duplicated the code from player 1).
A VBI handles the drawing and motion of the tanks and bullets (which involves erasing and redrawing when a tank rotates, or when an object moves vertically), the joystick input, collision detection, and the sound effects.
VBIDrawSprite Procedure is the VBI
VBIInit Procedure enables that VBI
(by first setting CRITIC to 1, pointing
VVBLKD at VBIDrawSprite's address,
and then clearing CRITIC)
SAVETEMPS and GETTEMPS macros
are raw 6502 machine language to dump things to the stack
on the beginning of the VBI, and retrieve them at the end of
the VBI, so that 'normal' code in the program isn't adversely
affected when it's interrupted. See
"Interrupt
With Action" by David Plotkin,
ANTIC
Volume 3, Number 3 (July 1984)
and
"Action!
Bug Sheet #3" (November 1984).
Joystick input, and corresponding motion (and hence drawing), is handled only every few frames, to keep the tanks from moving too quickly. Motion of bullets happens every frame (60 times/second on an NTSC Atari).
Spd counter counts up to MaxSpd
before handling joystick input (so increase MaxSpd
to slow the tanks down; set it to 0 to make them move as
fast as the bullets).
The coordinate system used by the objects is scaled up from their on-screen positions (times 4; handled via left-shift and right-shift math operators). This means the objects can move in sub-pixel steps. e.g., for a tank pointing North-North-West, it moves upwards one pixel almost every frame, while moving one pixel towards the right only every couple of frames.
Delta array contains the directions & speeds
for each angle (index 0=N, 1=NNW, 2=NW, 3=WNW, etc.).
Delta are mapped to positive values
(so 0=max negative (-4), 4=no change (0), etc.)
Hardware collision detection is utilized. Collisions bits are cleared
each frame by writing to HITCLR. Collisions are then detected
by reading P0PL and P1PL for player-to-player
(bumping into opponent), P0PF and P1PF for
player-to-playfield (bumping into obstacles), and M0PL
and M1PL for missile-to-player (shooting opponent)).
The code is broken into a few pieces:
TANK.ACTTANKMEM.ACTSTICK0=$278"). Thanks to
Mapping
the Atari by Ian Chadwick (1985).
TANKSPR.ACTINCLUDE-able plaintext
file, that get stored in an array that describes the various
shapes (rotated angles) of the tank sprites.
Generated using TXT2SPR.ACT, and based on the
hand-typed ATASCII graphics contained in TANK.SPR.
; Tank
; Bill Kendrick 2015
Byte Array PMG,PMGM,PMG0,PMG1,CHSET,SC
Byte Array Tank=[
INCLUDE "TANKSPR.ACT"
]
INCLUDE "TANKMEM.ACT"
; No motion on input except every
; MaxSpd-th frame
Byte Spd,MaxSpd
; Players' directions & "been hit" ctr
Byte D0,D1,HIT0,HIT1
; Players' positions
Int X0,Y0,X1,Y1
; Whether to draw player this frame
Byte Draw0=[1],Draw1=[1]
; Bullet "alive" counters & directions
Byte B0=[0],BD0,B1=[0],BD1
; Bullet positions
Int BX0,BY0,BX1,BY1
; Explosion sfx counter
Byte Expl=[0]
; X/Y deltas (shifted to positive)
; for player & bullet internal X/Y
; locations (which are right-shifted
; to become screen X/Y locations)
Int Array Delta=[
4 0
5 1
6 2
7 3
8 4
7 5
6 6
5 7
4 8
3 7
2 6
1 5
0 4
1 3
2 2
3 1
]
; Meat of the game, all within a VBI!
Proc VBIDrawSprite()
Byte I
Int OX0,OY0,OX1,OY1
Byte Pointer Src0Ptr,Src1Ptr
Byte Pointer PMGY0,PMGY1,PMGYM
SAVETEMPS
; Only handle user input & movement
; occasionally
Spd=Spd+1
If Spd>=MaxSpd Then
Spd=0
; --- MOVE PLAYER 1 ---
OX0=X0
OY0=Y0
; Spin right or left from joy input
; (or auto spin if hit)
If (STICK0 & 8)=0 Or HIT0>0 Then
D0=(D0+1)&15
Draw0=1
ElseIf (STICK0 & 4)=0 Then
D0=(D0-1)&15
Draw0=1
Fi
; Move forward on joy input
; (if not spinning due to hit)
If HIT0=0 And (STICK0 & 1)=0 Then
AUDC2=15 ; loud engine sfx
X0=X0+Delta(D0 Lsh 1)-4
Y0=Y0+Delta((D0 Lsh 1)+1)-4
; Don't go off-screen
If X0<0 Or X0>604 Or Y0<0 Or Y0>244 Then
X0=OX0
Y0=OY0
Else
Draw0=1
Fi
Else
If HIT0>0 Then
If HIT0=32 Then
; On first contact w/ bullet,
; fly across the screen in
; direction of enemy's bullet
PMGY0=PMG0+16+(Y0 Rsh 2)-2
For I=0 To 7 Do
PMGY0(I+2)=0
Od
OX0=X0
OY0=Y0
X0=X0+Delta(BD1 Lsh 1)-4
Y0=Y0+Delta((BD1 Lsh 1)+1)-4
If X0<0 Or X0>604 Or Y0<0 Or Y0>244 Then
X0=OX0
Y0=OY0
Else
Draw0=1
Fi
Fi
; Being-hit sfx
AUDC2=HIT0 Rsh 2
AUDF2=HIT0
HIT0=HIT0-1
Else
; Low engine idle sfx
AUDC2=2
AUDF2=200
Fi
Fi
; --- MOVE PLAYER 2 ---
; (see above)
OX1=X1
OY1=Y1
If (STICK1 & 8)=0 Or HIT1>0 Then
D1=(D1+1)&15
Draw1=1
ElseIf (STICK1 & 4)=0 Then
D1=(D1-1)&15
Draw1=1
Fi
If HIT1=0 And (STICK1 & 1)=0 Then
X1=X1+Delta(D1 Lsh 1)-4
Y1=Y1+Delta((D1 Lsh 1)+1)-4
AUDC4=15
If X1<0 Or X1>604 Or Y1<0 Or Y1>244 Then
X1=OX1
Y1=OY1
Else
Draw1=1
Fi
Else
If HIT1>0 Then
If HIT1=32 Then
PMGY1=PMG1+16+(Y1 Rsh 2)-2
For I=0 To 7 Do
PMGY1(I+2)=0
Od
OX1=X1
OY1=Y1
X1=X1+(Delta(BD0 Lsh 1)-4) Lsh 3
Y1=Y1+(Delta((BD0 Lsh 1)+1)-4) Lsh 3
If X1<0 Or X1>604 Or Y1<0 Or Y1>244 Then
X1=OX1
Y1=OY1
Else
Draw1=1
Fi
Fi
AUDC4=HIT1 Rsh 2
AUDF4=HIT1
HIT1=HIT1-1
Else
AUDC4=2
AUDF4=200
Fi
Fi
Fi
; --- DRAW PLAYER 1 ---
If Draw0=1 Then
; Draw shape based on dir tank faces
Src0Ptr=Tank+(D0 Lsh 3)
; Draw at tank's Y position
PMGY0=PMG0+16+(Y0 Rsh 2)-2
; Erase above/below, draw in new pos
PMGY0(0)=0
PMGY0(1)=0
For I=0 To 7 Do
PMGY0(I+2)=Src0Ptr^
Src0Ptr==+1
Od
PMGY0(10)=0
PMGY0(11)=0
; Move to X position
HPOSP0=(X0 Rsh 2)+48
Draw0=0
; Bump backwards if we touch other
; tank, water, or brick
If P0PL>0 Or (P0PF&2)=2 Or (P0PF&4)=4 Then
OX0=X0
OY0=Y0
X0=X0-((Delta(D0 Lsh 1) Lsh 1)-8)
Y0=Y0-((Delta((D0 Lsh 1)+1) Lsh 1)-8)
If X0<0 Or X0>604 Or Y0<0 Or Y0>244 Then
X0=OX0
Y0=OY0
Else
Draw0=1
Fi
Fi
Fi
; --- DRAW PLAYER 2 ---
; (see above)
If Draw1=1 Then
Src1Ptr=Tank+(D1 Lsh 3)
PMGY1=PMG1+16+(Y1 Rsh 2)-2
PMGY1(0)=0
PMGY1(1)=0
For I=0 To 7 Do
PMGY1(I+2)=Src1Ptr^
Src1Ptr==+1
Od
PMGY1(10)=0
PMGY1(11)=0
HPOSP1=(X1 Rsh 2)+48
Draw1=0
If P1PL>0 Or (P1PF&2)=2 Or (P1PF&2)=4 Then
OX1=X1
OY1=Y1
X1=X1-((Delta(D1 Lsh 1) Lsh 1)-8)
Y1=Y1-((Delta((D1 Lsh 1)+1) Lsh 1)-8)
If X1<0 Or X1>604 Or Y1<0 Or Y1>244 Then
X1=OX1
Y1=OY1
Else
Draw1=1
Fi
Fi
Fi
; --- PLAYER 1 FIRE ---
; If not hit, bullet not already in
; flight, and fire btn press, shoot!
If HIT0+HIT1=0 And
STRIG0=0 And B0=0 Then
; Set flight time, direction,
; and starting pos
B0=64
BD0=D0
BX0=X0+8
BY0=Y0+16
Fi
; --- PLAYER 2 FIRE ---
; (see above)
If HIT0+HIT1=0 And
STRIG1=0 And B1=0 Then
B1=64
BD1=D1
BX1=X1+8
BY1=Y1+16
Fi
; --- PLAYER 1 BULLET ---
If B0>0 Then
; Erase in old position
; (missiles shared, so &ing vs
; just setting to 0)
PMGYM=PMGM+16+(BY0 Rsh 2)
PMGYM(0)=PMGYM(0)&252
; Move bullet
BX0=BX0+Delta(BD0 Lsh 1)-4
BY0=BY0+Delta((BD0 Lsh 1)+1)-4
; Countdown flight time
B0=B0-1
; Stop bullet immediately if out of
; bounds
If BX0<0 Or BX0>604 Or BY0<0 Or BY0>260 Then
B0=0
Fi
; Bullet sfx
AUDC1=(B0 Rsh 2)
; If still alive, set new X pos
; and draw in new Y pos
; (missiles shared, so %ing vs
; just setting to 1)
If B0>0 Then
HPOSM0=(BX0 Rsh 2)+48
PMGYM=PMGM+16+(BY0 Rsh 2)
PMGYM(0)=PMGYM(0)%1
Fi
; Now that it's drawn in new pos,
; check for detection...
; Touching opponent? Make them
; explode!
If M0PL=2 Then
B0=1 ;Will die next loop
HIT1=32
; FIXME: Scoring
Fi
; Touching brick? Destroy it!
If M0PF=2 Then
B0=1 ;Will die next loop
SC((BY0 Rsh 5)*20+(BX0 Rsh 5))=2
Expl=15
Fi
Else
HPOSM0=0
Fi
; Prepare for fresh collision
; detection during the next frame
HITCLR=0
; Brick just exploded? Play sfx
If Expl>0 Then
Expl=Expl-1
CONSOL=RANDOM
Fi
GETTEMPS
XITVBV
; Enable our VBI
Proc VBIInit()
CRITIC=1
OldVBI=VVBLKD
VVBLKD=VBIDrawSprite
CRITIC=0
Return
; Disable our VBI
Proc ClearVBI()
CRITIC=1
VVBLKD=OldVBI
CRITIC=0
Return
; Draw a random map
Proc DrawMap()
Byte I,X,Y,X1,X2,Y1,Y2
Zero(SC,240)
; River going down center(ish) of
; screen
X=Rand(10)+5
For Y=0 To 9 Do
SC(Y*20+X)=130
If X>0 And Rand(255)<64 Then
X=X-1
ElseIf X<19 And Rand(255)<64 Then
X=X+1
Fi
Od
; Some random brick walls
; FIXME: This sucks
For I=0 To 5 Do
X1=Rand(20)
X2=Rand(20)
Y=Rand(10)
For X=X1 To X2 Do
SC(Y*20+X)=65
Od
X=Rand(20)
Y1=Rand(10)
Y2=Rand(10)
For Y=Y1 To Y2 Do
SC(Y*20+X)=65
Od
Od
Return
Proc Main()
Byte I,B
Graphics(2)
; Some space for Player/Missile gfx
; and redefine character set
PMG=RAMTOP-16
PMBASE=PMG
CHBAS=PMG+8
; Pointers to tops of PMG memory
; for missiles, P0, and P1
PMG==*256
PMGM=PMG+384
PMG0=PMG+512
PMG1=PMG+640
; Zero out PMG data
Zero(PMG+384,384)
; Zero out charset data
CHSET=PMG+2048
Zero(CHSET+1024)
; Chset: Dirt
Poke(CHSET+2,2)
Poke(CHSET+4,32)
Poke(CHSET+7,8)
; Chset: Brick
Poke(CHSET+8,251)
Poke(CHSET+9,251)
Poke(CHSET+10,251)
Poke(CHSET+12,223)
Poke(CHSET+13,223)
Poke(CHSET+14,223)
; Chset: Water/Explosion (the same)
For I=16 To 31 Do
B=Rand(255)
Poke(CHSET+I,B)
Od
; Find beginning of screen memory
SC=SAVMSC
; PMG config
GPRIOR=1 ; All PMGs above playfield
SIZEP0=0 ; Narrow P0
SIZEP1=0 ; Narrow P1
SDMCTL=46 ; Std playfield (+2),
; missile DMA (+4),
; player DMA (+4),
; screen DMA (+32)
GRACTL=3 ; Enable players & missiles
; Players' starting state
D0=0
X0=1
Y0=1
HIT0=0
D1=6
X1=50
Y1=50
HIT1=0
; Movement speed
MaxSpd=3
; Sound config
AUDCTL=0 ; ???
SKCTL=3 ; ???
; Default sounds
AUDF1=40 ; Player 1 engine
AUDC1=0
AUDF2=200 ; Player 1 bullet
AUDC2=0
AUDF3=30 ; Player 2 engine
AUDC3=0
AUDF4=220 ; Player 2 bullet
AUDC4=0
; Screen colors
COLOR0=34 ; Yellow dirt
COLOR1=70 ; Red bricks
COLOR2=172 ; Blue water
COLOR3=0 ; Black (unused)
COLOR4=192 ; Dark green ground (bkgd)
PCOLR0=220 ; Light yellow green P1
PCOLR1=186 ; Light bluegreen P2
; Set up map & start game VBI
DrawMap()
VBIInit()
; Main program loop (outside VBI)
CH=255
DO
; FIXME: Do anything else? :)
UNTIL CH=28 OD ; Exit loop on [Esc]
CH=255
; End game VBI
ClearVBI()
GRACTL=0
; Silence sounds
AUDCTL=0
SKCTL=3
AUDF1=0
AUDC1=0
AUDF2=0
AUDC2=0
AUDF3=0
AUDC3=0
AUDF4=0
AUDC4=0
Return
tank_act.txt (ATASCII EOL converted to ASCII CR)
TANK.ACT (original ATASCII)
; Tank
; Memory stuff
; Bill Kendrick 2015
Byte
CH=$2FC,
CHBAS=$2F4,
PMBASE=$D407,
SDMCTL=$22F,
GRACTL=$D01D,
HPOSP0=$D000,
HPOSP1=$D001,
HPOSM0=$D004,
HPOSM1=$D005,
PCOLR0=704,
PCOLR1=705,
COLOR0=708,
COLOR1=709,
COLOR2=710,
COLOR3=711,
COLOR4=712,
RAMTOP=106,
SIZEP0=$D008,
SIZEP1=$D009,
CONSOL=$D01F,
RANDOM=$D20A,
GPRIOR=$26F,
STICK0=$278,
STICK1=$279,
STRIG0=$284,
STRIG1=$285,
HITCLR=$D01E, ;(W)
P0PL=$D00C, ;(R)
P0PF=$D004, ;(R)
P1PL=$D00D, ;(R)
P1PF=$D005, ;(R)
M0PF=$D000, ;(R)
M0PL=$D008, ;(R)
M1PF=$D001, ;(R)
M1PL=$D009, ;(R)
AUDF1=$D200,
AUDC1=$D201,
AUDF2=$D202,
AUDC2=$D203,
AUDF3=$D204,
AUDC3=$D205,
AUDF4=$D206,
AUDC4=$D207,
AUDCTL=$D208,
SKCTL=$D20F
Define
SAVETEMPS="[$A2 $07 $B5 $A8 $48
$CA $10 $FA]",
GETTEMPS="[$A2 $00 $68 $95 $A8 $E8
$E0 $08 $D0 $F8]",
XITVBV="[$4C $E462]"
Byte NMIEN=$D40E
Card OldVBI,VVBLKD=$224
Card SAVMSC=$58
Byte CRITIC=$42
tankmem_act.txt (ATASCII EOL converted to ASCII CR)
TANKMEM.ACT (original ATASCII)
0 16 16 214 254 254 254 198 4 68 232 250 255 223 6 6 17 58 116 250 95 14 28 8 48 248 243 60 56 28 62 60 0 248 248 112 126 112 248 248 60 62 28 56 60 243 248 48 8 28 14 95 250 116 58 17 6 6 223 255 250 232 68 4 198 254 254 254 214 16 16 0 96 96 251 255 95 23 34 32 16 56 112 250 95 46 92 136 60 124 56 28 60 207 31 12 0 31 31 14 126 14 31 31 12 31 207 60 28 56 124 60 136 92 46 95 250 112 56 16 32 34 23 95 255 251 96 96
tankspr_act.txt (ATASCII EOL converted to ASCII CR)
TANKSPR.ACT (original ATASCII)
; TXT2SPR.ACT
; Bill Kendrick 2015
Proc Main()
Byte N,I,Z,L
Byte Array Buf(255)
Close(1)
Close(2)
Open(1,"D:TANK.SPR",4,0)
Open(2,"D:TANKSPR.ACT",8,0)
L=0
Do
InputSD(1,Buf)
If Buf(0)=8 Then
Print(Buf)
N=0
Z=128
For I=1 To 8 Do
If Buf(I)=' Then
N=N+Z
Fi
Z=Z Rsh 1
Od
PrintBE(N)
PrintBD(2,N)
PutD(2,' )
L=L+1
If L=8 Then
PutDE(2)
L=0
Fi
Fi
Until EOF(1) Od
PutDE(2)
Close(1)
Close(2)
Return
txt2spr_act.txt (ATASCII EOL converted to ASCII CR)
TXT2SPR.ACT (original ATASCII)
ATASCII art that gets converted to TANKSPR.ACT by TXT2SPR.ACT.
TANKSPR.ACT (original ATASCII)
Sorry, there's no executable download at this time!
I'll eventually make a stand-alone executable (.XEX)
and/or bootable disk image (.ATR, maybe also
.DCM), especially if anyone's actually interested.
(I'll have to dig out an Action! "runtime" library to compile with, to produce something usable without requiring an Action! language cartridge.)