Legend of Zelda/Technical Information

From Red Candle
Jump to: navigation, search

(Note that details and naming are subject to change as more is learned; analysis of the game is ongoing)


Object system

The game allocates memory for up to 19 objects of varying types. These types and their indices are:

  • #$00 - Link
  • #$01-0B - dynamic objects
  • #$0D - the sword
  • #$0E - the sword or rod beam
  • #$0F - the boomerang or the bait
  • #$10-11 - flames and bombs
  • #$12 - the rod or an arrow
  • #$13 - and the room treasure

Dynamic objects consist of enemies, their projectiles, NPCs, and Link's ladder, and are allocated dynamically rather than having a static index. Link's weapons have their own slots so they can be used even when the enemy cap has been reached, though some share slots to limit their memory footprint, which means they can't be used at the same time. For bombs and flames, they are dynamically assigned the first free slot of the two available, so a mixture of two of these can be on the screen at once. Note that nothing is ever assigned to slot #$0C, though its timer and memory are sometimes used for other purposes.

Object data is stored in a number of arrays that are indexed with the object index. Many of these arrays are available to all objects, while some are limited to only dynamic objects and sometimes Link. The following are many of the most relevant ones:

$28 - object_timer: A timer that automatically decrements each frame, usually dedicated to the object.

$3C - object_stun_timer: A long timer that decrements when the long_timer_tick ($26) goes below 0, usually used to indicate an enemy is stunned by the boomerang. Because this timer can be set at any time, the first tick will usually be too short.

$70 - object_pos_x: The X position of the object on the screen.

$84 - object_pos_y: The Y position of the object on the screen. Note that this is considered block-aligned when ending in #$D rather than #$0. This is to give some illusion of depth by drawing objects 2 pixels higher than the block they're on (only 2 because the PPU draws sprites 1 scanline lower than their specified Y coordinate). Unfortunately, this gives rise to various bugs, such as gels popping out of zols a full block too low.

$98 - object_face_direction: The direction the object is facing. Objects with grid-based movement will restrict this to the grid to avoid off-grid movement. For room treasures, this is the item ID. Like with most direction-based tracking in this game, the direction order is up, down, left, right (UDLR, most to least significant) in the low nybble.

$AC - object_status: Used for many different state-related tracking for different objects. Many objects use this to track what state the AI is in. Link uses it for his attack sequences as well as entirely disabling his handling (movement and collision). Link's weapon object slots are empty when this is clear.

$C0 - object_knockback_direction: Indicates which direction the object should travel when the knockback timer is set. UDLR in the low nybble. When the object has been hit, bit 7 is set to indicate that the direction needs to be sanity-checked to prevent knockback from knocking an object off-grid, and if the object doesn't reverse direction when it collides with Link, bit 6 is also set, but this appears to go unused.

$D3 - object_knockback_timer: The number of movement ticks for which the object should undergo knockback. Like with standard movement, 4 ticks are consumed each frame, but the knockback speed is always 1 full pixel per tick. When this hits 0, the corresponding object_knockback_direction variable is cleared.

$034F - dynamic_id: The object ID for dynamic objects. If this is 0, the slot is unoccupied, except in rare circumstances when slots are being controlled by another object (eg gleeok controlling its attached heads). (No index 0)

$0394 - object_subgrid_offset: The object's position along its current gridline. When sitting on a grid intersection, this will be 0, and positive (right and down) and negative (left and up) movement will increase or decrease this in lock-step with the object's pixel position. This is clamped in either direction by subgrid_offset_limit_positive ($010E) and subgrid_offset_limit_negative ($010F), which are +/-8 for Link and +/-16 for enemies, and further movement is not allowed that frame if clamped. If this is 0 or either limit after movement is complete, it is set to 0 and collision is handled.

$03A8 - object_subpos: The object's subpixel position. Because grid-based objects can't move more than one direction at a time, this acts as the subpixel position for the current direction, whether horizontal or vertical. This doesn't get cleared when clamped to a grid intersection, and isn't cleared when changing directions. This results in quirks like faster movement when going diagonally by alternating positive/negative directions, or the boomerang having unpredictable speed when moving diagonally (because both directions interpret the subpos as being in their own direction). For dropped items, this variable acts as the timer (2 frames per tick).

$03BC - object_speed: An object's subpixel speed per movement tick. 4 ticks are done per frame, limiting an object's speed to just under 4 pixels. Because object movement is clamped when reaching new grid intersections, an object might only move 1-3 ticks on a given frame.

$03D0 - object_animation_timer: The number of frames before advancing to the next frame of the object's animation.

$03E4 - object_frame: The current frame of the object's animation.

$03F8 - dynamic_move_direction: The direction the object is moving in, for Link or dynamic objects. UDLR in the low nybble.

$0405 - dynamic_spawn_state: The object's state when spawning or dying. (No index 0)

$0485 - dynamic_health: The object's current health. For some reason, object health is handled in multiples of #$10, so it can't exceed an effective value of 15. If health is 0 and an object gets hit by any weapon it's vulnerable to, it will die, even if that weapon does 0 damage. (No index 0)

$049E - object_collision_tile: The ID of the last tile the object collided with.

$04B2 - dynamic_immunities: Flags indicating the weapons the object is immune to. These are:

  • Bit 0 - sword
  • Bit 1 - boomerang
  • Bit 2 - arrow
  • Bit 3 - bomb
  • Bit 4 - rod
  • Bit 5 - flame

Bits 6 and 7 appear to be unused, but are set normally set for enemies that have any immunities. (No index 0)

$04BF - dynamic_properties: Flags indicating properties of the object. These are:

  • Bit 0 - The object's AI is responsible for drawing the object and calling weapon and Link collision functions, rather than these being done automatically after the AI has run.
  • Bit 1 - The object is drawn using 1 tile, not 2.
  • Bit 2 - The object's AI is responsible for drawing the object.
  • Bit 3 - The object's AI is responsible for setting its own frame attributes (in $04 and $05) before calling the object drawing functions.
  • Bit 4 - The object always reverses direction when colliding with an obstacle (block, wall, or screen boundary), overriding the more complicated turning behavior normally exhibited. This appears to be unused.
  • Bit 5 - Disables weapon collision for the object.
  • Bit 6 - Moves the object's collision center 4 pixels to the left, under the (often incorrect) assumption that thin objects are centered by being positioned 4 pixels to the right. This is why many vertical projectiles can be dodged more easily on the right.
  • Bit 7 - Makes the object not reverse direction when colliding with Link.

(No index 0)

$04F0 - object_iframes: The number of frames before the object will handle object collision again. The low two bits determine which palette the object's sprite will use.

Object movement

Most non-flying objects are restricted to moving along a movement grid, consisting of edges and vertices. The length of an edge in this grid depends on the object index, with the standard length being 16 pixels and Link's being 8. When an object isn't on a vertex, its direction is restricted such that it can only be parallel to the current edge. An object is on a vertex when its position along the current edge (object_subgrid_offset) is 0, regardless of its current subpixel position (object_subpos).

Every frame that an object chooses to move, it undergoes 4 movement ticks that each move the object by less than one pixel. Within each tick, its speed (object_speed) is added to or subtracted from its subpixel position, depending on whether the movement is in a positive (down or right) or negative (up or left) direction. If the subgrid offset hasn't reached either limit for this kind of object ($010E subgrid_offset_limit_positive and $010F subgrid_offset_limit_negative), the carry from the subpixel arithmetic is added to or subtracted from both the subgrid offset and whole pixel position.

If either limit has been reached, then subsequent ticks within the same frame continue to modify the subpixel position as normal, but the offset and whole pixel values lock into place. This causes objects to stick to vertices, which is where important operations such as background collision detection and screen edge detection take place. When movement is complete, if the subgrid offset is equal to one of the limits, it is set back to 0.

Note that there is only one subpixel position, despite there being two axes. This is because standard objects can only move along one axis at a time, with the other axis coordinate aligned to the grid, so it's only ever relevant to the current one. However, this value isn't cleared when turning perpendicular, so the object may be closer to or further from the next pixel than expected when turning.

Screen scroll glitch

Normally, Link and other objects walk from a subgrid offset of 0 toward either the positive or negative limit, which will prevent further whole-pixel movement that frame. However, this same clamping behavior doesn't occur for 0. Normally, this isn't a problem, because if an object starts on 0, moves in a direction, and then turns around before reaching the next vertex and walks back, it will necessarily land on 0 again because it moves the same speed both ways, precisely retracing the steps it took. Non-Link objects also usually do not change their direction except on vertices. However, knockback can cause a problem for Link. When an object is hit and knocked back, it moves one entire pixel per tick for 4 ticks per frame through a separate mechanism from standard movement. This can cause Link to be at an unusual position along an edge, which could cause him to skip past 0 because he moves 1.5 pixels per frame (#$60 subpixels per tick).

Skipping past 0 results in the collision event at 0 being skipped. This bypasses Link's screen boundary check for scrolling, and causes the object to continue using the background tile it sampled at the last vertex as its current one. Realistically, that tile will be nonsolid, so the object may be able to skip past one solid tile.

A worse side effect, though, is how this interacts with perpendicular movement. When Link receives perpendicular input, he attempts to walk to the nearest vertex before then turning in that direction. If that nearest vertex is at 0 and Link isn't lined up to be able to land on 0, he'll miss the vertex and land on the other side. On the next frame, he'll try again to reach the vertex, missing again and ending up back where he started. This causes him to be stuck, alternating from side to side, unable to reach the vertex without first moving elsewhere.

To fix this issue, the game includes a hack to ensure that the vertex Link walks toward when attempting to turn perpendicular is at a limit rather than at 0, so proper clamping can occur. For example, if Link is walking toward the positive limit and turns around with perpendicular input, his subgrid offset will be adjusted so that the vertex he came from will now be at the negative limit instead of 0. This fixes the stuck issue, but allows Link to easily configure the next vertex to be at 0 rather than a limit by just tapping perpendicular. By just walking toward that vertex, he can skip over it, assuming he's properly aligned.

Let's do some examples. Without the perpendicular movement hack, if Link were facing right on a horizontal edge at subgrid offset 2 and subpixel position #$80, holding up would cause him to walk left to offset 1 subpixel #$00, and then offset -1 subpixel #$80 (with the subpixel for negative numbers representing how close the object is to the next larger number, not smaller). The offset never became 0, so Link didn't handle collision there and wasn't able to start moving up. On the next frame, he would walk right again back to offset 1 subpixel #$00, and would continue to alternate back and forth.

With the hack, holding up would first convert the offset to -6 (preserving the distance of 2.5 pixels from the vertex), and Link would then move to offset -7 subpixel #$00, and try to move to offset -9 subpixel #$80, but the clamping on the -8 limit would cause him to end up at offset -8 subpixel #$80, which would set his offset back to 0 and allow him to handle collision and move up.

For standard screen scrolling, if Link is facing right on a horizontal edge at offset 3 subpixel #$00, perpendicular input will convert the offset to -5 and move Link to offset -7 subpixel #$80. Walking right will then move him to offset -5 subpixel #$00, offset -4 subpixel #$80, offset -2 subpixel #$00, offset -1 subpixel #$80, and then offset 1 subpixel #$00, skipping 0.

Forced drops

Some item drops can be forced and take priority over the standard drop system. Forced drops are handled by two consecutive kill counters and a bomb flag. The first counter, the ten count ($50), controls whether a forced 5 rupee is dropped. The bomb flag ($51) controls whether the ten count drop should be a bomb, instead. The second counter, the fairy count ($0627), controls whether fairies are dropped. All of these variables are cleared when Link collides with an enemy (including bubbles and even the recorder whirlwind).

When Link kills an enemy (that is, the enemy makes a dying sound), both counters are incremented. This occurs at kill time, before the dying animation. Item drops occur later, after the dying animation completes. When the drop is calculated, the game checks the counters to decide what to drop. If the fairy count is 16, then a fairy is dropped and the ten count and bomb flag are cleared. Otherwise, if the ten count is 10, either a 5 rupee is dropped if the bomb flag is clear or a bomb is dropped if the flag is set, and the ten count and bomb flag are then cleared. So, in the case of any forced drop, the ten count and bomb flag are cleared, but the fairy count is not.

Some enemies cannot drop items. Because they don't run the drop code, they also can't drop forced items, and thus won't consume the counters. These enemies are those in the no-drop group, as well as stalfos, gibdos, and like likes in slot 1 (because these enemies may be carrying the room treasure, and it would be undesirable if they dropped both the treasure and an item). We'll call these no-drop enemies.

If the ten count transitions from 9 to 10 due to a bomb kill, the bomb flag is set. The ten count cannot exceed 10 and will hold at that value even if additional kills occur before the forced item can be dropped, such as by killing a group of enemies at once or killing no-drop enemies. Once any forced drop occurs, the counter is reset to 0.

The fairy counter, however, has no hard limit. It will continue to increase with each kill. Because of this, the fairy drop can be skipped. Any drop occurring when the fairy counter is 16 will be a fairy, so killing a group of enemies to bring the count above 16 at drop time will skip the fairy. Alternatively, because no-drop enemies can't drop forced items, they can die with the counter at 16 without performing the drop. Skipping the fairy will avoid resetting the ten count and bomb flag.

Because the fairy counter isn't cleared when the drop occurs, multiple fairies can be dropped from the same count. For example, if the counter is 12 and 4 enemies are quickly killed before any of them can calculate a drop, the counter will be 16 when the drops are handled, so all 4 drops will be fairies.

Note that while the fairy counter has no hard limit, it is an 8-bit value and will overflow to 0 at 256 kills, allowing it to force more fairies at the next 16.

There are some notable special cases and edge cases with counters. These include:

Dodongos: Killing a dodongo does not increment either counter, whether killed with bombs or the sword. If the dodongo is killed with a sword, however, the ten count is set to 10 and the bomb flag is set, forcing a bomb at drop time. This method of forcing the bomb can result in unexpected behavior. If Link takes a hit after killing the dodongo, but before the drop, the forced bomb will be lost. Furthermore, if the fairy count is 16 when the drop occurs, the fairy will be dropped instead of the bomb and the ten count and bomb flag will be cleared. Finally, if Link kills a second dodongo with a sword before the first one has dropped its bomb, the second will not drop a bomb because the forced drop system has no way of queuing up multiple forced bombs.

Vulnerable unkillable enemies: Enemies such as the manhandla core and flying gleeok heads are invincible, but actually handle weapon collision and will clear their iframes and unkill themselves each frame. This means they count as a kill every frame that a weapon is touching them. Bombing these enemies is a good way to force a bomb because it will quickly max out the ten count and set the bomb flag, but it will also be adding to the fairy counter, which could override the bomb drop when killing the boss. The old man in the dungeon also behaves this way, but because he doesn't clear his iframes, he'll only count as one kill per attack.

Multi-part enemies: Manhandla's hands, gleeok's attached heads, and moldorm's and lanmola's segments each increment the consecutive counters when killed, but the death of the whole enemy does not grant an additional consecutive kill; that is, killing the last part will only add 1 to the counters, not 2. A perfect bomb on manhandla will increase the counters by 5 instead of 4, though, because the core is also hit and counts as a kill.

Leader enemies: When a ringleader (such as the level 2 goriya leader) is killed, the other enemies in the room die by being directly transitioned to the dying state (by setting dynamic_spawn_state to #$10) and will not impact the consecutive counters. The ringleader itself counts as normal, however. Ghini leaders also share this same behavior (through a similar mechanism).

Zols: While splitting enemies (zols and vires) don't normally count as kills when split, zols are vulnerable to an additional weapon hit on the frame they split, allowing them to be killed and counted as a consecutive kill. This is due to a bug where zols delete themselves before splitting (in case there aren't enough slots for the gels they're creating), but instead of exiting immediately afterward, their AI continues to do meaningful work on the deleted object, including collision checks. The deletion (through ClearObject at Bank7_FEB1) clears the object_iframes variable, which guarantees the zol can take a hit during this one last check. Because deletion clears the object's dynamic_id variable, the zol doesn't run again and thus can't drop an item nor increment the global counter. Its health is not cleared by ClearObject, so the weapons it collides with must do enough damage to kill it. Note that if a gel was spawned into the zol's slot, the gel will be hit, instead.

Vires are more robustly written and don't suffer from this bug.

Overkill: When an enemy is killed by a weapon, it still finishes checking the remaining weapons that frame, as normal. This means it can be hit by those additional weapons, which may also kill it. The consecutive kill counters are incremented at kill time, so these additional kills will each increment the counters like with any normal kill. As a result, killing a single enemy by hitting it with multiple weapons on one frame can add more than one to the counts. This can be done most practically by timing multiple attacks to hit when an enemy's iframes expire.

Note that the health the enemy has after a killing blow can vary. If the weapon damage was equal to the enemy's current health, the resulting health is 0, so any following hits will count as kills. However, if the damage was more than the current health, the health is unchanged. Furthermore, the order in which weapons are checked can impact how many kills the enemy gives because each weapon hit may reduce the health for the following weapons. Weapon check order is boomerang, beam (sword/rod), flame/bomb slot A, flame/bomb slot B, sword, arrow/rod.

Sub-frame behavior: Because everything in the game must be handled sequentially, events handled earlier can directly impact those handled later, so the order of execution can result in surprising behavior. For example, if an enemy is killed on the same frame that multiple other enemies are dropping items, and that kill sets the fairy counter to 16, then any drops handled before the kill was registered would be normal random drops, and drops after the kill would be forced fairies, despite all of the drops happening on the same frame.


Dungeon graphics corruption

Glitch dungeon graphics corruption.png

When entering a dungeon on the same frame that a background block is changed, such as an armos statue or bombable wall, there's a 50% chance that the dungeon graphics won't load properly, resulting in the overworld graphics being used but with some minor corruption. While some of the corruption will persist until reset, the issue is mostly corrected upon leaving the dungeon.

When you change areas, the game has to copy over all of the area's graphics from ROM to VRAM. This can't be done directly; instead, the game has to tell the PPU where in VRAM it wants to put the graphics, and then hands the bytes to the PPU one-by-one to put at the specified location. After each byte is written, the target address is automatically incremented by the PPU, and there's a PPU setting that controls whether the increment is by 1 or 32. This is to make it easy to change adjacent 8x8 tiles on the screen; if incrementing by 1, it'll target the next tile in the row, and if by 32, the next tile in the column (because rows are 32 tiles wide).

The graphics copying function assumes that the PPU is set to increment by 1 per write, but doesn't actually guarantee this by setting it this way. Background block updates in Zelda go vertically (increment by 32), so if that was the last thing to configure the PPU before you enter a new area, the setting will be wrong and the graphics copy will erroneously splatter the graphics data throughout VRAM (modifying only 1 out of every 32 bytes) instead of copying the contiguous chunks as it should. This normally doesn't happen because updates to the HUD leave the PPU in the correct increment-by-1 configuration. HUD updates occur every other frame, so there is only a 1-frame window during which you can enter with the incorrect PPU settings. Because of the difficulty of timing these events to fall between HUD updates, you effectively have a 50% chance of triggering the glitch by entering a dungeon on the same frame as a background update.

Because area graphics are loaded on every area transition, most of the issue is corrected by leaving the dungeon. However, some graphics are only copied to VRAM on power on or reset and will remain partially corrupted until then.

Additional references

A Moonmap for Legend of Zelda - A technical overview of Zelda, primarily covering graphics layout, level format, and sound.

6502 Instruction Reference - The full 6502 instruction set, including cycle counts.

Nesdev Wiki - Comprehensive documentation on everything related to NES hardware behavior.