Skip to content

Camera Dead Zones on Amiga

A camera need not move every time the player takes a step.

A camera need not move every time the player takes a step. Keep a quiet area on screen, and scroll only when the player pushes beyond it. The camera should also stop at the edge of the world rather than revealing an empty border.

Amiga shows the working diagnostic row 14 64 28 50 9B 01 01 00.
A native completed caller shows its state as hexadecimal bytes. The field order below explains the row; the full packet retains every call.

Include, initialise and call

Reserve CAMERA, PLAYER, LEFT_EDGE, RIGHT_EDGE, SCROLL_LIMIT, SHIFTED, VALID as 7 contiguous private bytes in that order. Include the helper once. Call camera_deadzone_init at the intended reset boundary: it clears every named byte, including input fields. Set the starting values afterwards, then call camera_deadzone_step for each intended service.

Initialise, establish valid world/view limits and update PLAYER in world coordinates. Service after movement, then pass CAMERA to the renderer’s coordinate conversion or scrolling backend. The normal 40–80 dead zone moves the camera from zero to ten for player 90, then to twenty for player 100.

CAMERA and PLAYER are unsigned world coordinates. LEFT_EDGE/RIGHT_EDGE are permitted screen-relative player thresholds, with LEFT_EDGE ≤ RIGHT_EDGE. SCROLL_LIMIT is the largest camera coordinate; the current CAMERA must already be within 0–SCROLL_LIMIT. Invalid bounds leave CAMERA unchanged and clear SHIFTED/VALID. Otherwise step sets VALID, moves just enough to put the player on the crossed threshold, clamps to 0–SCROLL_LIMIT and sets SHIFTED exactly when CAMERA changes. Player and bounds survive.

Reusable initialisation and service

component.inc
; SPDX-License-Identifier: MIT
; Camera Dead Zones. Contiguous private bytes: CAMERA, PLAYER, LEFT_EDGE, RIGHT_EDGE, SCROLL_LIMIT, SHIFTED, VALID.
; init clears all fields; set inputs afterwards. No I/O, waits or IRQ ownership.
; D0-D3/A0-A1/CCR change; D4-D7/A2-A6 survive.
camera_deadzone_init:
    clr.b CAMERA
    clr.b PLAYER
    clr.b LEFT_EDGE
    clr.b RIGHT_EDGE
    clr.b SCROLL_LIMIT
    clr.b SHIFTED
    clr.b VALID
    rts
camera_deadzone_step:
    clr.b SHIFTED
    clr.b VALID
    move.b LEFT_EDGE,d0
    cmp.b RIGHT_EDGE,d0
    bhi.s camera_done
    move.b CAMERA,d0
    cmp.b SCROLL_LIMIT,d0
    bhi.s camera_done
    move.b #1,VALID
    moveq #0,d0
    move.b PLAYER,d0
    moveq #0,d1
    move.b CAMERA,d1
    sub.w d1,d0
    bmi.s camera_left
    moveq #0,d1
    move.b LEFT_EDGE,d1
    cmp.w d1,d0
    blo.s camera_left
    move.b RIGHT_EDGE,d1
    cmp.w d1,d0
    bls.s camera_done
    moveq #0,d0
    move.b PLAYER,d0
    sub.w d1,d0
    bra.s camera_cap
camera_left:
    moveq #0,d0
    move.b PLAYER,d0
    moveq #0,d1
    move.b LEFT_EDGE,d1
    sub.w d1,d0
    bpl.s camera_cap
    moveq #0,d0
camera_cap:
    moveq #0,d1
    move.b SCROLL_LIMIT,d1
    cmp.w d1,d0
    bls.s camera_store
    move.w d1,d0
camera_store:
    cmp.b CAMERA,d0
    beq.s camera_done
    move.b d0,CAMERA
    move.b #1,SHIFTED
camera_done:
    rts
component_end:

State, registers and ownership

Seven bytes. CAMERA persists; PLAYER and the three bounds are inputs; SHIFTED and VALID are replaced flags. PLAYER − CAMERA is computed without byte-wrap mistakes, including when the player lies to the left of the camera.

D0–D3/A0–A1/CCR may change; D4–D7/A2–A6 survive. A direct BSR/JSR needs four return-address stack bytes.

The implementation binds fixed symbols and has one mutation owner. Independent users need separate state blocks and scoped/renamed labels, including routine labels; sharing the same bindings means sharing the state. Do not let an interrupt and the main loop mutate the same block without an explicit ownership policy.

The bootable caller takes over Copper/bitplane/interrupt/DMA resources and explicitly disarms all eight sprites. It does not restore Workbench. These are caller presentation choices, not dependencies of the reusable helper.

A caller you can adapt

The buildable program supplies bindings and include order. Its separate client retains state between calls. checked_step calls the public camera_deadzone_step entry and checks register sentinels; emit_state records diagnostics. Your game can call the public entry directly, remove the diagnostics and consume the documented outputs.

Bindings, buildable caller and include order

demo.asm
    section code,code_c
CUSTOM = 0
BROKEN = 0
COYOTE_STEPS = 4
BUFFER_STEPS = 4
TEXT_CAP = 8
PROPERTY_COUNT = 16
STREAM_BYTES = 13
CRC_POLY = 7
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "component.inc"
    include "data.inc"
    include "cases.inc"
    even
    include "display.inc"
    include "../../framework/turn-deadline/font.inc"
code_end:
STATE:
CAMERA: dc.b 0
PLAYER: dc.b 0
LEFT_EDGE: dc.b 0
RIGHT_EDGE: dc.b 0
SCROLL_LIMIT: dc.b 0
SHIFTED: dc.b 0
VALID: dc.b 0
    even
ready: dc.w 0
preview: dcb.b 8,0
OUTPUT: dcb.b 1300,0
    even
signature: dc.b "198XPLAYTOOLS00",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 #0,CAMERA
    move.b #90,PLAYER
    move.b #60,LEFT_EDGE
    move.b #60,RIGHT_EDGE
    move.b #155,SCROLL_LIMIT
    ifne BROKEN
    move.b CAMERA,d0
    sub.b d0,PLAYER
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    move.b #100,PLAYER
    ifne BROKEN
    move.b CAMERA,d0
    sub.b d0,PLAYER
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    else
    move.b #0,CAMERA
    move.b #90,PLAYER
    move.b #40,LEFT_EDGE
    move.b #80,RIGHT_EDGE
    move.b #155,SCROLL_LIMIT
    ifne BROKEN
    move.b CAMERA,d0
    sub.b d0,PLAYER
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    move.b #100,PLAYER
    ifne BROKEN
    move.b CAMERA,d0
    sub.b d0,PLAYER
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    endif
    rts

The display fields are CAMERA, PLAYER, LEFT_EDGE, RIGHT_EDGE, SCROLL_LIMIT, SHIFTED, VALID; remaining display fields are zero. Each is one hexadecimal byte. The normal row is 14 64 28 50 9B 01 01 00. This is a completed state observation, not a recording of an actual character jumping, a camera scrolling or an asset being displayed.

Make it your own

The custom caller sets both thresholds to 60. The dead zone has zero width, so the camera tracks the player at that screen position until a world edge prevents it. The two calls finish at camera 40 instead of 20.

Maintained custom configuration or caller

custom.asm
    section code,code_c
CUSTOM = 1
BROKEN = 0
COYOTE_STEPS = 2
BUFFER_STEPS = 2
TEXT_CAP = 4
PROPERTY_COUNT = 16
STREAM_BYTES = 13
CRC_POLY = 49
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "component.inc"
    include "data.inc"
    include "cases.inc"
    even
    include "display.inc"
    include "../../framework/turn-deadline/font.inc"
code_end:
STATE:
CAMERA: dc.b 0
PLAYER: dc.b 0
LEFT_EDGE: dc.b 0
RIGHT_EDGE: dc.b 0
SCROLL_LIMIT: dc.b 0
SHIFTED: dc.b 0
VALID: dc.b 0
    even
ready: dc.w 0
preview: dcb.b 8,0
OUTPUT: dcb.b 1300,0
    even
signature: dc.b "198XPLAYTOOLS00",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 custom row is 28 64 3C 3C 9B 01 01 00. These records qualify the named builds and inputs; another layout, format or input domain needs explicit bounds and fresh execution evidence.

Common mistake and repair

The wrong client subtracts CAMERA from PLAYER before calling the world-coordinate API. The second request supplies screen position 90 in place of world position 100, leaving the camera at ten. Keep world and screen coordinates separate and subtract the camera only in the rendering path.

Select the deliberately wrong client

wrong.asm
    section code,code_c
CUSTOM = 0
BROKEN = 1
COYOTE_STEPS = 4
BUFFER_STEPS = 4
TEXT_CAP = 8
PROPERTY_COUNT = 16
STREAM_BYTES = 13
CRC_POLY = 7
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "component.inc"
    include "data.inc"
    include "cases.inc"
    even
    include "display.inc"
    include "../../framework/turn-deadline/font.inc"
code_end:
STATE:
CAMERA: dc.b 0
PLAYER: dc.b 0
LEFT_EDGE: dc.b 0
RIGHT_EDGE: dc.b 0
SCROLL_LIMIT: dc.b 0
SHIFTED: dc.b 0
VALID: dc.b 0
    even
ready: dc.w 0
preview: dcb.b 8,0
OUTPUT: dcb.b 1300,0
    even
signature: dc.b "198XPLAYTOOLS00",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 row 0A 5A 28 50 9B 00 01 00.
The wrong client completes with 0A 5A 28 50 9B 00 01 00. Its code and the explanation above identify the cause; the capture observes the result.

The BROKEN branch is visible in the client. repaired.asm restores the normal selection; its executable matches demo exactly. Spectrum tape filenames may differ while loaded CODE remains identical. The fault is a labelled experiment, not a suggested alternative.

Cost, limits and verification

The measured layout has 180 code bytes, 0 immutable data bytes and 7 private state bytes. Alignment, stack usage, diagnostic tables and display machinery are additional. The extents come from the loaded program’s symbols, not from counting source lines.

A relative-position comparison and one clamped correction, with no map copy or display transfer. C64/Amiga hardware capabilities are not reduced to this logical byte coordinate; their renderers may need wider and differently split scroll values.

The byte world is a bounded example, not a universal camera range. Wider maps need wider coordinates; smoothing adds fractional state and a time-step policy. Hardware coarse/fine scrolling, exposed map columns and parallax remain separate display-specific work. If bounds shrink, first move CAMERA into the new valid interval.

Four native runs use A500 OCS PAL with Kickstart 1.3 and an OFS cold-boot disk; Asm198x 0.0.72 and Emu198x 0.44.2, with Build198x 0.2.9 for disk mastering. Each checks 64 boundary/varied/error cases, dirty initialisation and every state of a separate sequential client against an independent integer model. This is not exhaustive coverage of every possible byte combination. Source/tool/firmware hashes, immutable loaded code/data, native startup/completion, preserved-register sentinels, helper/data sizes, screenshot bytes/pixels and exact repair are checked. Deliberately altered observations must fail. Firmware is hashed, never distributed.

Related technique · Maths for Games · The Craft.

Source and native evidence.