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
Proc
edure is the VBI
VBIInit
Proc
edure 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.ACT
TANKMEM.ACT
STICK0=$278
"). Thanks to
Mapping
the Atari by Ian Chadwick (1985).
TANKSPR.ACT
INCLUDE
-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.)