6502 Assembly / hardware interaction / Systems Programming / Memory Optimization

The Creature of Wellstown

A multiplayer NES game built from scratch in 6502 assembly, pushing the console's memory and rendering limitations to support procedural terrain, dynamic entities and hardware-specific visual effects.

My contribution: designing the entity architecture, optimising its memory representation and implementation of the NES Zapper interaction.

Trailer — The Creature of Wellstown

Learning the hardware.
Then pushing it further.

Although originally a 6502 assembly exercise, the project grew into an exploration of the NES's hardware limitations. We deliberately pushed its memory, rendering and timing constraints to achieve effects and gameplay systems beyond what a "normal" implementation would allow. The project involved frame-sensitive data manipulation, procedural terrain, dynamic entities and hardware-specific rendering techniques.

6502 Assembly for the entire game
generated terrain
dynamic entities
frame-perfect data modification

Designing around
hard limits.

The entity system had to deal with several competing constraints. Memory usage needed to remain predictable, while entities themselves could be completely different from one another.

01

2KB system RAM

Meaning entity data needs to be compact and predictable, leaving memory available for the rest of the game.

02

Limited CPU budget

with a 1.66 MHz CPU, it's important that per-entity logic stays inexpensive.

03

Unpredictable entity count

The number of entites changes constantly, but diferent types require diferent behaviour and data.

04

8 sprites per scanline.

A NES limitation, any additional sprites on the same scanline are not rendered.

05

Max 64 sprites per frame.

Another NES limitation, requiring the game to decide what sprites it prioritizes which entities receive sprite slots.

One compact entity format.

To guarantee predictable memory usage, the entities get stored in a static array rather than dynamically allocating them.

But this introduces another constraint: every entity had to fit inside the same fixed-size memory block, regardless of what type of entity it represented.

Patrolling enemies | Animated sprites | Falling objects | State-dependent obstacles | Pickups | etc

Just 8 bits are used to track entity state. There is also 8 bits to identify the entity type. And position was represented using 2-byte fixed-point values.

Together, this allowed the essential entity data to fit into just 6 bytes. Twenty entities could therefore occupy approximately 120 bytes, easily leaving enough headroom for the rest of the game. (max entities : 84)

8 bits of entity state
6 bytes total per entity
0.12 KB for 20 entities

8 bit ID    | %0000_0000
            |  ++++_++++- entity ID
            
8 bit state | %0000_0000
            |  |||| ++++- sprite/kind/velocity/direction/...     
            |  ||||
            |  |||+------ on screen
            |  ||+------- above/below camera
            |  |+-------- horizontal flip / entity alocated
            |  +--------- vertical flip / entity alocated
            

16 bit X    | %0000_0000__0000_0000
            |  ++++_++++__++++_++++- Xpos 
            

16 bit Y    | %0000_0000__0000_0000
            |  ++++_++++__++++_++++- Ypos 
            

STATE BITS

The 8 state bits are largely entiy alocated, but have a few comon demoninators. (smallest significat bit first)
The first 4 bits are dependant onthe entity.
- Crates use it to track falling speed, using it as a multiplier to achieve acceleration.
- Skeletons use 1 bit to track modified speed and 3 as a modifier timer. When dead, 4 to track their sprite.
- Pickups track their kind.
- traps use a bit to check if they are trigered.

Bits 5 and 6 are global, required to keep track of their position relative to the screen since we have a camera that can scroll back and forth. An active entity can be eihter above, on, or below the screen.

Then bits 7 and 8 are usualy used for checking if the entity is flipped in some way.
but, for example skeletons will never be upside down, so the bit is instead used to track if they're alive.

Making sure nothing
gets lost between frames.

Entity iteration uses a constant update loop. However, the NES has strict limits on how many sprites can be rendered during a frame.

There is a maximum of eight sprites per scanline. And there is only space for 64 sprites in the shadow OAM, as this is all that gets copied to the PPU. Simply iterating from the first entity every frame will most definitly cause the later entities to not get rendered.

Full is full. The 9th sprite on the scanline in memory isn't rendered. Neither is a 65th sprite at the shadow OAM, which, even worse, would cause undefined behaviour.

sprite missing | each crate is 2 sprites wide + 1 sprite for player = 9

To prevent this, the starting element of the update function shifts continuously. This makes it so all entites rotate in priority, ensuring they all get the chance to be drawn. Equaly, all entities that try to write to the sprite OAM past the 64th sprite get blocked.

              
 frame 1 - start at entity 0
0→ 1 2 3 ...

frame 1 - 9th sprite mising

                
 frame 2  - start at entity 1

0 1→ 2 3 ...

frame 2 - 9th sprite mising

The zapper & binary search

For the zapper, the chalenge isn't reading the input. The Zapper provides a binary light-sensor signal, while a CRTV displays the image progressively across the screen. The input reading's relevancy therefore depends on when it's sampled.

Input reading

Input can be read from the 2 designated adresses: $4016 & $4017. In order to read, you first need to strobe $4016 after which the 8 bit output can be read one bit at a time.

For the zapper:
- Bit 5: trigger | pulled/released : 0, half-pulled : 1
- Bit 4: light sensor | no light : 1, light : 1
The light sensor needs to be read continuously while the frame is being drawn, as once the scanner passed by, the light quickly disapears.

Once the trigger has been detecetd, the game enters the zapper protocol.

Hit detection

the game halts, for the next couple of frames, all that will happen is the continuous reading of the zapper output to test if it detects light during the frame.

The first frame is empty.
If light is detected during this frame, the protocol ends as it means the player is cheating by pointing the gun at an external light source.

Next, in order to find if the player hit an entity, each eligible entity is added to an array.
Each subsequent frame, the game will use binary search to determine what entity, if any, was shot.

Each frame, half the remaining entities are desplayed as solid white sprites against a black screen. If the zaper detected light, the hit target is within that half. If no light is detected, the other half gets checked.
This gets repeated untill either a single entity remains, or neither halves detected a hit.

Candidates: [ 0 1 2 3  ]

Frame 1:     black screen
                    ↓
        Zapper detects light?
             /       \
            no       yes -> cheater
            ↓
frame 2:  Display 0 1 as white sprites
                    ↓
                detected?
                /    \
             yes      --> no
              ↓            ↓
Frame 3:    test 0        test 2 3 
              ↓
           detected?
            /    \
         yes      --> no
          ↓            ↓
   0 is target       test 1
            

entities to hit in game - white area on a black background to test the hit.

Noteworthy - A cool concequence of the way the light detection works, is that depending on when the light is detected during the frame, you can infer at what scanline it hapened.