Skip to content

Aim a Projectile at a Target in Spectrum BASIC

Compute a bounded two-component velocity towards a target with an explicit dominant-axis speed rule.

An enemy can fire towards the player’s current position without an angle table. Subtract the two positions, scale both components by the same dominant distance, and give the resulting velocity to a projectile. The speed rule matters as much as the arithmetic.

Spectrum BASIC shows both retained behaviour checks passing.
The working caller completes with both saved checks equal to one.

Include and initialise

Reserve public U/V/O/X/Y/A/B/S, private D/E/F and lines 3000–3220. There are no arrays. Combine routines.bas with a caller through build.py, keeping END, STOP or a jump before the helper. C64/BBC use GOSUB; Spectrum uses GO SUB. Call 3000 to clear public fields, then supply the intended shot.

X/Y are the origin and A/B the target, all integral byte coordinates in the same nonwrapping space. S is an integral speed from zero through fifteen. Call 3100 to calculate a velocity. O=1 means valid inputs; invalid input returns U/V/O=0 and preserves the inputs.

Scale both axes together

The helper computes signed differences before taking their magnitudes. Their largest magnitude is the scale. Each output magnitude is the integer part of absolute-difference*S/scale, then its original sign is restored. Coincident points produce a valid zero vector without division.

Reusable numbered routines

routines.bas
3000 LET U=0: LET V=0: LET O=0: LET X=0: LET Y=0: LET A=0: LET B=0: LET S=0: RETURN
3100 LET U=0: LET V=0: LET O=0
3110 IF X<0 OR X>255 OR X<>INT (X) THEN RETURN
3120 IF Y<0 OR Y>255 OR Y<>INT (Y) THEN RETURN
3130 IF A<0 OR A>255 OR A<>INT (A) THEN RETURN
3140 IF B<0 OR B>255 OR B<>INT (B) THEN RETURN
3150 IF S<0 OR S>15 OR S<>INT (S) THEN RETURN
3160 LET O=1: LET D=A-X: LET E=B-Y: LET F=ABS (D)
3170 IF ABS (E)>F THEN LET F=ABS (E)
3180 IF F=0 THEN RETURN
3190 LET U=INT (ABS (D)*S/F): LET V=INT (ABS (E)*S/F)
3200 IF D<0 AND U>0 THEN LET U=256-U
3210 IF E<0 AND V>0 THEN LET V=256-V
3220 RETURN

U/V are signed velocities encoded as bytes. Decode a value of 128 or more by subtracting 256 before ordinary BASIC position arithmetic. Four pixels left is encoded as 252. Zero is always encoded as zero, including a negative component rounded down to zero.

For a noncoincident target, the dominant component has magnitude exactly S. This is a dominant-axis speed rule, not constant Euclidean speed. A diagonal shot can travel farther per update than an axis-aligned shot. That may suit the game’s movement model; otherwise choose a different normalisation deliberately.

Launch once, retain the velocity

The normal caller aims from 100,100 at 110,105 with speed four. It returns U=4,V=2. The custom caller aims at 90,95 and returns 252,254, meaning minus four and minus two. Saved horizontal and vertical checks verify both components.

Example calls from the maintained caller

demo.bas, excerpt
  70 GO SUB 3000: LET AA=0: LET BB=0
  80 LET X=100: LET Y=100: LET A=110: LET B=105: LET S=4
  90 GO SUB 3100: GO SUB 2000
 100 IF U=4 THEN LET AA=1
 110 IF V=2 THEN LET BB=1
 120 LET A=100: LET B=100
 130 GO SUB 3100: GO SUB 2000
 140 LET X=255: LET Y=0: LET A=0: LET B=255: LET S=15
 150 GO SUB 3100: GO SUB 2000
 160 LET S=16
 170 GO SUB 3100: GO SUB 2000
 180 PRINT "AIMD RESULTS";AA;BB
 190 POKE MB+995,AA: POKE MB+996,BB: POKE MB+997,GD: POKE MB+998,WC-256*INT (WC/256): POKE MB+999,INT (WC/256): POKE MB+1000,239: STOP

Later calls cover coincident points, extreme opposite corners and an invalid speed. AA/BB/JJ (Z on Spectrum), line 2000 and the mailbox are diagnostics. The helper calculates a shot; it does not allocate an object, advance a position, choose a target or draw anything.

A game normally stores the decoded velocity with a spawned projectile. Recomputing it towards the player’s new position every update instead creates homing behaviour. Either can be useful, but they are different rules. The same distinction applies to where the origin and target were sampled within the update.

Choose the speed and precision

At a small S, a shallow component can round to zero. Fractional-position integration can retain subpixel progress and improve shallow trajectories, using wider state and integration. A Euclidean normaliser needs an appropriate length calculation or approximation and a corresponding cost/rounding policy. A small angle/velocity table trades runtime work for data and discrete directions.

Byte coordinates deliberately limit the query’s world. Wider positions require wider validation and displacement arithmetic. A wrapping world needs a chosen wrap-distance rule; plain subtraction cannot infer which way around the world the projectile should travel.

Mistake and repair

The wrong caller resets S to zero before every query. The helper correctly returns a stationary shot, so both saved velocity checks fail. Keep the intended speed until the aim calculation has used it. Repaired source and loaded program match normal exactly.

Spectrum BASIC shows two failed checks from the deliberate caller mistake.
The wrong caller runs to completion, but both saved behaviour checks are zero.

Cost and Maths for Games

A query validates five inputs, finds signed displacement and its dominant magnitude, then performs two bounded multiply/divides. Products can reach 3825, so they stay wide until scaled. It allocates nothing and owns no input device, timer or display state. A successful numerical query does not prove a complete projectile’s collision, lifespan or frame budget.

Maths for Games connects displacement, ratios, quantisation and different distance measures. The Craft connects aiming, shot commitment and deliberate homing behaviour.

Global names and line numbers

BASIC numeric names, arrays and numbered lines are global. To combine helpers, rename public and private fields and renumber the helper together with every GO SUB/GO TO reference. Keep each state block under one owner. The maintained build.py rejects duplicate line numbers; it does not automatically namespace or renumber routines.

Machine and measured size

Spectrum FOR variables must be single letters: this diagnostic uses Z instead of the other callers’ JJ. The helper uses scalar numeric fields and allocates no arrays. AND/OR in these listings join comparisons; neither is being used as a bitwise operator. The diagnostic reserves its mailbox above CLEAR 59999.

The numbered helper occupies 504 tokenised program bytes on this interpreter, including line headers. This excludes the caller, test tables, numeric scalar/array storage, interpreter stacks and ROM. Tokenised size is not machine-code size or a hardware-cycle count. No real-time or whole-game performance budget is inferred from the diagnostic. The caller’s AA/BB, Z and RAM mailbox are observation machinery, not required game state.

Execution evidence

Working, custom, deliberately wrong and exact repaired callers executed through Spectrum’s stock BASIC interpreter under Emu198x 0.44.2. Build198x 0.2.9 tokenises C64 PRG and Spectrum TAP programs. C64 enters stock RUN, Spectrum uses ROM tape loading, and BBC enters every numbered source line through MOS/BASIC. Actual loaded program bytes, complete state packets, positive completion/count/guards and visible result pixels are checked. After the positive completion marker, the observer advances two complete display frames before capturing, so a partially scanned final character cannot masquerade as a finished picture. Firmware is not distributed.

An independent mathematical/order model checks the trace and rejects altered source, configuration, loaded program, state, completion and image evidence. Host exhaustive checks complement the native boundary/sequential examples; those host ranges are not claimed as natively exhaustive. The supplied diagnostic callers deliberately end with STOP; the interpreter report is expected. The wrong caller prints 0,0 and the normal/custom/repaired callers print 1,1.

Public CI checks retained evidence; it does not execute restricted firmware. Refresh with python3 scripts/verify-basic-aim.py aimed-projectile sinclair-zx-spectrum --tools /path/to/extracted/releases and inspect with python3 scripts/check-basic-aim.py aimed-projectile.

Explore further

Compare the assembly technique. The BASIC game-flow routine makes session boundaries explicit. Complete source and native evidence.