"Tank" in Action!

Bill Kendrick, May 2015

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.

What is it?

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).

How does it work?

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.

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).

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.

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)).

Source File Summary

The code is broken into a few pieces:

Source File Contents

TANK.ACT

; 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

TANKMEM.ACT

; 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


TANKSPR.ACT

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 

TXT2SPR.ACT

; 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

TANK.SPR

ATASCII art that gets converted to TANKSPR.ACT by TXT2SPR.ACT.

Executable Download

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.)

Nsvigation