Difference between revisions of "Don't hardcode OAM addresses"
(equ, for RGBDS syntax) |
(Define "cel" because sylvie in nesdev thought the lack of a definition might confuse novices. Also define "sprite" because Pan Docs is using "object" lately) |
||
| (One intermediate revision by the same user not shown) | |||
| Line 1: | Line 1: | ||
| − | To display sprites, a program builds a display list in the first 160 bytes of a 256-byte page of WRAM, sometimes called "shadow OAM". | + | To display sprites (or moving objects), a program builds a display list in the first 160 bytes of a 256-byte page of WRAM, sometimes called "shadow OAM". |
Then the program copies it to OAM ($FE00-$FE9F) during vertical blanking (mode 1) by writing the high byte of the display list's address to [[Video Display#FF46 - DMA - DMA Transfer and Start Address (R/W)|$FF46]]. | Then the program copies it to OAM ($FE00-$FE9F) during vertical blanking (mode 1) by writing the high byte of the display list's address to [[Video Display#FF46 - DMA - DMA Transfer and Start Address (R/W)|$FF46]]. | ||
For example, if it places the display list at $C200-$C29F, it writes $C2 to $FF46. | For example, if it places the display list at $C200-$C29F, it writes $C2 to $FF46. | ||
| Line 5: | Line 5: | ||
Occasionally, programmers get the idea to define variables so as to hardcode the position of a particular actor in the display list, using code similar to the following: | Occasionally, programmers get the idea to define variables so as to hardcode the position of a particular actor in the display list, using code similar to the following: | ||
<pre> | <pre> | ||
| − | player_oam_base equ SOAM + $00 | + | def player_oam_base equ SOAM + $00 |
| − | player_y equ player_oam_base + $00 | + | def player_y equ player_oam_base + $00 |
| − | player_x equ player_oam_base + $01 | + | def player_x equ player_oam_base + $01 |
| − | player_tile equ player_oam_base + $02 | + | def player_tile equ player_oam_base + $02 |
| − | player_palette equ player_oam_base + $03 | + | def player_palette equ player_oam_base + $03 |
</pre> | </pre> | ||
| Line 19: | Line 19: | ||
*Reuse movement logic among multiple actor types | *Reuse movement logic among multiple actor types | ||
*Turn objectionable dropout when more than 10 sprites are briefly on a line into less objectionable flicker by varying from frame to frame which sprite appears earlier in OAM | *Turn objectionable dropout when more than 10 sprites are briefly on a line into less objectionable flicker by varying from frame to frame which sprite appears earlier in OAM | ||
| + | |||
| + | (A "cel", also called a "metasprite", is a group of sprites that represent one frame of a character's animation.) | ||
A better way: | A better way: | ||
| Line 28: | Line 30: | ||
That way, you can perform flicker by varying the order in which actors are drawn during step 3. And if you skip drawing a sprite because it is offscreen, you can skip increasing the OAM used variable. | That way, you can perform flicker by varying the order in which actors are drawn during step 3. And if you skip drawing a sprite because it is offscreen, you can skip increasing the OAM used variable. | ||
| + | |||
| + | Putting the OAM used variable in the same page as shadow OAM allows a shortcut for loading the pointer into shadow OAM. | ||
| + | <pre> | ||
| + | section "shadow OAM", WRAM0, ALIGN[8] | ||
| + | wShadowOAM: ds 160 | ||
| + | wOAMCanary: ds 1 ; Set an emulator watchpoint on writing here | ||
| + | wOAMUsed: ds 1 ; Offset into wShadowOAM | ||
| + | |||
| + | section "OAM drawing", WRAM0, ALIGN[8] | ||
| + | DrawPlayer: | ||
| + | ld hl, wOAMUsed | ||
| + | ld l, [hl] ; HL points within wShadowOAM | ||
| + | ; TODO: write groups of 4 bytes to [hl+] | ||
| + | ld a, l | ||
| + | ld [wOAMUsed], a | ||
| + | ret | ||
| + | </pre> | ||
Latest revision as of 15:03, 8 February 2026
To display sprites (or moving objects), a program builds a display list in the first 160 bytes of a 256-byte page of WRAM, sometimes called "shadow OAM". Then the program copies it to OAM ($FE00-$FE9F) during vertical blanking (mode 1) by writing the high byte of the display list's address to $FF46. For example, if it places the display list at $C200-$C29F, it writes $C2 to $FF46.
Occasionally, programmers get the idea to define variables so as to hardcode the position of a particular actor in the display list, using code similar to the following:
def player_oam_base equ SOAM + $00 def player_y equ player_oam_base + $00 def player_x equ player_oam_base + $01 def player_tile equ player_oam_base + $02 def player_palette equ player_oam_base + $03
In all but the simplest cases, hardcoding OAM addresses of actors is an anti-pattern because it limits the ability to do several things:
- Separate the motion of the camera from motion of game objects
- Change the size of a cel later on, in case some cels need more sprites than others
- Draw a cel half-offscreen
- Reuse movement logic among multiple actor types
- Turn objectionable dropout when more than 10 sprites are briefly on a line into less objectionable flicker by varying from frame to frame which sprite appears earlier in OAM
(A "cel", also called a "metasprite", is a group of sprites that represent one frame of a character's animation.)
A better way:
- Keep separate variables for each actor's position in world space that are stored outside of the display list. Also keep variables for the camera's position.
- Each frame, clear a variable denoting how many bytes of shadow OAM have been filled so far.
- Each frame, fill shadow OAM from start to finish based on the position of the actor, plus the position of the individual sprites within the actor's cel, minus the position of the camera.
- Clear the Y coordinates of unused sprites.
That way, you can perform flicker by varying the order in which actors are drawn during step 3. And if you skip drawing a sprite because it is offscreen, you can skip increasing the OAM used variable.
Putting the OAM used variable in the same page as shadow OAM allows a shortcut for loading the pointer into shadow OAM.
section "shadow OAM", WRAM0, ALIGN[8] wShadowOAM: ds 160 wOAMCanary: ds 1 ; Set an emulator watchpoint on writing here wOAMUsed: ds 1 ; Offset into wShadowOAM section "OAM drawing", WRAM0, ALIGN[8] DrawPlayer: ld hl, wOAMUsed ld l, [hl] ; HL points within wShadowOAM ; TODO: write groups of 4 bytes to [hl+] ld a, l ld [wOAMUsed], a ret