Skip to content

Approach a Target Without Overshoot on Amiga

An enemy can move towards a waypoint without orbiting it forever.

An enemy can move towards a waypoint without orbiting it forever. Move by the smaller of the remaining distance and your allowed step, then report arrival. That last short step matters when the target is not an exact multiple of the speed.

Amiga shows the working result FF FF 0A 01 00 00 00 00.
The completed native caller shows hexadecimal state bytes. The field order below explains the result; the full verification packet retains every call.

Include, initialise and call

Bind POSITION, TARGET, STEP, ARRIVED as 4 contiguous private bytes in that order. Include the helper once. Call approach_target_init at the deliberate reset boundary: it clears all named bytes, including inputs. Set your starting values afterwards. Call approach_target_step when this service is needed.

Initialise, then set the position, target and allowed step. Call once per simulation update. Change TARGET to follow a moving goal, or choose the next waypoint only after ARRIVED. Use another state block for the second axis if independent axis movement is the intended policy.

POSITION, TARGET and STEP are unsigned bytes. Step moves POSITION towards TARGET by at most STEP, without wrapping or overshooting, and replaces ARRIVED with 1 exactly when the resulting position equals TARGET. STEP zero stops movement but still reports an already reached target. TARGET and STEP survive.

Reusable initialisation and service

component.inc
; SPDX-License-Identifier: MIT
; Approach a Target Without Overshoot. Bind these contiguous private bytes: POSITION, TARGET, STEP, ARRIVED.
; init clears the whole block; step uses current inputs. No I/O, waits or IRQ owner.
; D0-D3/A0-A1/CCR may change; D4-D7/A2-A6 survive.
approach_target_init:
    clr.b POSITION
    clr.b TARGET
    clr.b STEP
    clr.b ARRIVED
    rts
approach_target_step:
    clr.b ARRIVED
    moveq #0,d0
    move.b POSITION,d0
    moveq #0,d1
    move.b TARGET,d1
    cmp.w d1,d0
    beq.s approach_arrived
    blo.s approach_up
    sub.w d1,d0
    moveq #0,d2
    move.b STEP,d2
    cmp.w d2,d0
    bls.s approach_snap
    sub.b d2,POSITION
    rts
approach_up:
    sub.w d0,d1
    moveq #0,d2
    move.b STEP,d2
    cmp.w d2,d1
    bls.s approach_snap
    add.b d2,POSITION
    rts
approach_snap:
    move.b TARGET,POSITION
approach_arrived:
    move.b #1,ARRIVED
    rts
component_end:

State and ownership

POSITION persists. TARGET and STEP are inputs. ARRIVED describes the resulting position and stays true on repeated calls at the target; it is not a one-time arrival event. Detect its edge if an action should occur only once.

D0–D3/A0–A1/CCR may change; D4–D7/A2–A6 survive. A direct BSR/JSR uses four return-address stack bytes. The program is a 68000 implementation with no division instruction requirement.

This implementation has fixed symbol bindings and one mutation owner. Independent users need distinct state blocks and scoped/renamed bindings; including another copy with the same global labels is not sufficient. Sequential queries may reuse a scratch block only after the previous result has been consumed. Do not let an interrupt and the main loop mutate the same block without a separately designed ownership policy.

The standalone boot program takes over Copper/bitplane/interrupt/DMA resources, explicitly disarms all eight sprites, and does not restore Workbench. Those are diagnostic presentation choices; the helper itself owns no chipset registers.

A small caller you can adapt

The standalone program includes the helper and a separate client. Its read-only table exercises 96 valid inputs, then this client initialises once and retains state between calls. Replace the supplied input changes with your own game values; the display and packet machinery are diagnostics rather than dependencies of the component.

Buildable caller, bindings and include order

demo.asm
    section code,code_c
CUSTOM = 0
BROKEN = 0
SCORE_CAP = 65535
QUEUE_CAP = 4
REPEAT_DELAY = 4
REPEAT_RATE = 2
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "component.inc"
    include "cases.inc"
    include "display.inc"
    include "../turn-deadline/font.inc"
code_end:
STATE:
POSITION: dc.b 0
TARGET: dc.b 0
STEP: dc.b 0
ARRIVED: dc.b 0
    even
ready: dc.w 0
preview: dcb.b 8,0
OUTPUT: dcb.b 1300,0
    even
signature: dc.b "198XGAMEPLAY000",0
    even
copperlist:
    dc.w $08e,$2c81,$090,$2cc1,$092,$0038,$094,$00d0
    dc.w $100,$1200,$102,0,$104,0,$108,0,$10a,0
    dc.w $180,$012,$182,$fff
copbpl:
    dc.w $0e0,0,$0e2,0
    dc.w $ffff,$fffe
plane: dcb.b 10240,0

A separate client retaining state between calls

exercise.inc, excerpt
preview_rules:
    ifne CUSTOM
    move.b #250,POSITION
    move.b #255,TARGET
    move.b #2,STEP
    ifne BROKEN
    move.b STEP,d0
    add.b d0,POSITION
    clr.b ARRIVED
    move.b POSITION,d0
    cmp.b TARGET,d0
    bne.s trace_bad_end_0_custom
    move.b #1,ARRIVED
trace_bad_end_0_custom:
    else
    bsr checked_step
    endif
    bsr emit_state
    ifne BROKEN
    move.b STEP,d0
    add.b d0,POSITION
    clr.b ARRIVED
    move.b POSITION,d0
    cmp.b TARGET,d0
    bne.s trace_bad_end_1_custom
    move.b #1,ARRIVED
trace_bad_end_1_custom:
    else
    bsr checked_step
    endif
    bsr emit_state
    else
    move.b #250,POSITION
    move.b #255,TARGET
    move.b #10,STEP
    ifne BROKEN
    move.b STEP,d0
    add.b d0,POSITION
    clr.b ARRIVED
    move.b POSITION,d0
    cmp.b TARGET,d0
    bne.s trace_bad_end_0
    move.b #1,ARRIVED
trace_bad_end_0:
    else
    bsr checked_step
    endif
    bsr emit_state
    ifne BROKEN
    move.b STEP,d0
    add.b d0,POSITION
    clr.b ARRIVED
    move.b POSITION,d0
    cmp.b TARGET,d0
    bne.s trace_bad_end_1
    move.b #1,ARRIVED
trace_bad_end_1:
    else
    bsr checked_step
    endif
    bsr emit_state
    endif
    rts

The eight display fields follow POSITION, TARGET, STEP, ARRIVED; unused display fields are zero. Each field is a hexadecimal byte. The working row is FF FF 0A 01 00 00 00 00. The output is a completed diagnostic state, not a recording of movement, input or sound hardware.

Make it your own

The custom caller reduces STEP from ten to two. Starting at 250 with target 255, the two calls end at 254 without arrival; a later call would take the last one-unit step.

Maintained custom configuration or caller

custom.asm
    section code,code_c
CUSTOM = 1
BROKEN = 0
SCORE_CAP = 999
QUEUE_CAP = 2
REPEAT_DELAY = 6
REPEAT_RATE = 3
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "component.inc"
    include "cases.inc"
    include "display.inc"
    include "../turn-deadline/font.inc"
code_end:
STATE:
POSITION: dc.b 0
TARGET: dc.b 0
STEP: dc.b 0
ARRIVED: dc.b 0
    even
ready: dc.w 0
preview: dcb.b 8,0
OUTPUT: dcb.b 1300,0
    even
signature: dc.b "198XGAMEPLAY000",0
    even
copperlist:
    dc.w $08e,$2c81,$090,$2cc1,$092,$0038,$094,$00d0
    dc.w $100,$1200,$102,0,$104,0,$108,0,$10a,0
    dc.w $180,$012,$182,$fff
copbpl:
    dc.w $0e0,0,$0e2,0
    dc.w $ffff,$fffe
plane: dcb.b 10240,0

The observed custom row is FE FF 02 00 00 00 00 00. The retained records qualify these named configurations; a new input domain or larger layout needs its own bounds and fresh execution checks.

Common mistake and repair

The wrong client replaces the helper with an unconditional byte addition. Starting at 250, adding ten wraps to 4, then 14, moving away from target 255. Compare the remaining distance before adding or subtracting; increasing the target tolerance does not fix byte overflow.

Deliberately wrong client selection

wrong.asm
    section code,code_c
CUSTOM = 0
BROKEN = 1
SCORE_CAP = 65535
QUEUE_CAP = 4
REPEAT_DELAY = 4
REPEAT_RATE = 2
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "component.inc"
    include "cases.inc"
    include "display.inc"
    include "../turn-deadline/font.inc"
code_end:
STATE:
POSITION: dc.b 0
TARGET: dc.b 0
STEP: dc.b 0
ARRIVED: dc.b 0
    even
ready: dc.w 0
preview: dcb.b 8,0
OUTPUT: dcb.b 1300,0
    even
signature: dc.b "198XGAMEPLAY000",0
    even
copperlist:
    dc.w $08e,$2c81,$090,$2cc1,$092,$0038,$094,$00d0
    dc.w $100,$1200,$102,0,$104,0,$108,0,$10a,0
    dc.w $180,$012,$182,$fff
copbpl:
    dc.w $0e0,0,$0e2,0
    dc.w $ffff,$fffe
plane: dcb.b 10240,0
Amiga shows the deliberate fault result 0E FF 0A 00 00 00 00 00.
The wrong client completes and records 0E FF 0A 00 00 00 00 00. Compare its state with the working row and the explained failure.

The wrong branch is in the displayed client above, selected by BROKEN. repaired.asm selects the original client again. Its executable matches demo exactly; Spectrum tape filenames can differ while loaded CODE is identical. The bad caller is a labelled experiment, not an alternative recommended implementation.

Cost and verification

The measured helper occupies 108 code bytes plus 4 named state bytes in this layout. Return addresses, alignment, tables and display diagnostics are additional. The sizes cover init and step together, not the whole standalone program.

One comparison chooses direction, then a distance comparison chooses a full or short step. No multiplication, division or path search occurs. This is straight-axis pursuit, not obstacle-aware navigation.

Signed coordinates, wider worlds and fixed-point subpixels need wider arithmetic and explicit bounds. Moving both axes by the same maximum gives faster diagonal travel in Euclidean distance; normalisation or an axis budget is a separate movement policy. Collision may still block a proposed position.

Four native runs use A500 OCS PAL with Kickstart 1.3 and an OFS boot disk; Asm198x 0.0.72 and Emu198x 0.44.2, with Build198x 0.2.9 for disk mastering. Every run checks 96 table results, dirty initialisation and every state in a separate sequential trace against an independent integer model. This is boundary/varied-input qualification, not exhaustive coverage of every byte combination. Source/tool/firmware hashes, native startup and completion, preserved-register sentinels, immutable loaded code, actual helper extent, capture bytes and pixels, and exact repair are checked. Deliberately changed observations must fail. Firmware is hashed, never distributed.

Related reusable technique · Maths for Games · The Craft.

Source and native evidence.