Jump to content
NHL'94 Forums

Recommended Posts

Posted

NHL Hockey (NHL92), NHLPA Hockey 93, and NHL94 all share a similar proprietary format when it comes to sprites, frames, and animations. While 92 is a little different in the way the frame data is laid out, 93 and 94 are the same. The format used might actually be similar to other EA Sports games at the time (early Madden games). Note, the labels used in the NHL92 source code will be used in this post.

What's a sprite?

A sprite is a special object that can be moved around and placed on top of, or below the background tiles and other sprites. The amount of sprites that can be displayed on the screen is limited, depending on the console. Sprites are usually small compared to the background they are on. 

A tile is a group of 8x8 pixels. The sprites in these games vary in tile size. The sprites for the on-ice objects vary from 1x1 to 4x4 tiles. Sprites are used for on-ice objects, crowd aminations, faceoff window, and the zamboni.

What's a frame?

A frame is a group of sprites. Each frame also has a "hot spot", which is used as a second "anchor point" (i.e. a player's stick blade can be a hot spot in a frame, where the puck will "glue" to when the player has possession).

For on-ice objects, there are 549 frames in 92, 649 frames in 93, and 845 frames in 94.

 What's an animation?

An animation is a group of frames that are assigned based on a specific action (i.e. skating with the puck, making a pad stack save, shooting). 

 

There are different on-ice objects that the game keeps track of:

Fixed Frame Objects (FFO): These objects are tied to the rink scrolling. These vary from game to game.

  • 92 - Player Stars (2), possession star, gloves/sticks for fighting, center ice logo
  • 93 - Player Stars (2), possession star, replay cursor, gloves/sticks for fighting
  • 94 - Player Stars (4), possession star, replay cursor, gloves/sticks for fighting (yes, this is still there, though it points to a blank frame)

These are fixed frame objects (there are no animations).

SSO (no idea what this stands for): These objects are not tied to rink scrolling. These vary from game to game.

  • 92 - Off-screen player arrows (2), logos (2) on the score bug
  • 93 - Off-screen player arrows (2)
  • 94 - Off-screen player arrows (4)

These are also fixed frame objects (there are no animations).

Sort Graphics Objects: These are objects that are involved in the action, and are "sorted" on a list depending on their location in the Y, where lower objects on the screen have a higher priority. There are 16 of these objects - 12 skaters (6 per team), 2 nets, the puck, and the puck shadow (which converts to the goal light when a goal is scored). These objects have many different frames and animations that are assigned to them based on game conditions (except for the nets, they have 1 frame each).

The Sort Objects are the ones we will concentrate on. These objects each have a data structure that has variables for X/Y/Z positions and velocities, current frame number, current animation list and animation index, player attributes, conditional flags, etc. Not every object uses all the data structure variables (there is no Chk rating for a goal net for example). The data structure size is $80 hex or 128 decimal. They start at RAM location $FFB04A (6 Home Team objects, 6 Away Team objects, 2 goal nets, puck, puck shadow).

Sprite Animation List (SPAList)

SPAList locations in ROM:

  • 92 - $35B6
  • 93 - $4D8E
  • 94 - $5B1C

First, lets look at directions. The object can face 8 different directions (0-7). Direction 0 is facing upward, and we go clockwise from there.

Directions-export.gif

 

Now, lets say the player is skating with the puck. The code assigns the player the SPA (SPrite Animation) of skating with the puck. But, depending on the direction the player is facing, the frames for the animation should be different. So, for each SPA, there is a table containing offsets based on direction. This offset will be used to get the correct animation frame list based on that direction. Also included for each SPA is a flag for repeating the animation. 

Let's use skating with the puck as an example (SPAskatewp). The player is skating in direction 1. Let's find the direction table, and get the offset to the animation frame list. We will use NHL94 in this example.

SPAskatewp = $53E
direction = 1
SPAList starts at $5B1C in 94


To get to the table for SPAskatewp, we add SPAskatewp to SPAList - $5B1C+$53E = $605A.

Now, each SPA table in the SPAList is 18 bytes long (2 bytes per direction), and the last 2 bytes are for the SPA attribute (which only the first bit is used in the NHL games, for repeating the animation). The frame animation lists for this animation follow immediately after this table.

Here is the table at $605A from the ROM:
Dir 0 - 00 12 
Dir 1 - 00 22 
Dir 2 - 00 32 
Dir 3 - 00 42 
Dir 4 - 00 52 
Dir 5 - 00 62 
Dir 6 - 00 72 
Dir 7 - 00 82 
Attrib - 00 00

To get the offset, we take the start of the table ($605A). We add direction*2 to get the offset (in the case of direction 1, 00 22). We take this offset, and add it to the start of the table to get to the animation frame list ($605A + $0022 = $607C).

So for SPAskatewp, player skating in direction 1, the animation frame list is at ROM location $607C.

 

The animation frame list will have values in pairs. The pair is Frame # and the time to show the frame (value in video frames). The last frame in the animation will show a negative time (this is how the code knows it's the last frame in the animation). Let's look at our example:

Animation frame list at $607C:

00 07 00 0A (frame 7, time 10)
00 08 00 0A (frame 8, time 10)
00 09 00 0A (frame 9, time 10)
00 0A FF F6 (frame 10, time 10) - last frame in animation

Each animation frame is displayed for 10 video frames. There are 4 frames total in this animation. Since our repeat flag was 0, the animation does not repeat. The code will take care of changing or restarting this animation if needed.

Let's look at our animation:

 

skatewp-1.gif

The SPAList contains the table for direction offsets, attribute flags (only repeat is used), and animation frame lists for each animation in the game.

OK, so we know how to get the frame for each animation. But how is that frame put together? Remember earlier, I described a frame as a group of sprites with data associated with it. Our SPAskatewp animation is not a single sprite in each frame. 

Now this is where 92 is vastly different from 93 and 94. In 92, the Frame data and the sprite data bytes are together in one structure. In 93 and 94, they are separated, and the sprite data bytes are a little different. I will go over how they are laid out in 92 then show how they are laid out in 93/94.

 

Frame Data (NHL92)

If you look at the 92 source code, the Sprites.anim file starts in the base ROM at address $3D5EE. The first frame data starts at offset 6 ($3D5F4). 

Here is the layout of each Frame data:

Frame data structure (First frame at $3DF54):
SprStratt - offset $A: attribute flags (2 bytes) - not used in NHL
SprStrhot - offset $C: hotspot data (24 bytes) - though this is 24 bytes, only 4 bytes are used. 2 for Hot Spot X (byte 12-13), 2 for Hot Spot Y (byte 14-15)
SprStrnum - offset $24: # of sprites in this frame - 1
SprStrdat - offset $26: start of sprite data bytes (8 bytes per sprite)

The Sprite data bytes contain data for each specific sprite in the frame:

Sprite Data Bytes:
Byte 0-1: Y Global position
Byte 2-3: sizetab and top 4 bits of tile data offset
Byte 4-5: bottom 11 (0-10) bits of tile data offset, H/V flip priority (bits 11+12), palette (13-15)
Byte 6-7: X Global position

Let's go over the easy ones:

Y Global and X Global: Each Sort Object has an X and Y position stored in its structure. The X and Y Globals of the Sprite Data are added to that X and Y position to determine the upper left corner of the sprite.

Sizetab: This is an index for the sizetab table. The sizetab table contains values related to the number of tiles in the sprite. 

Sizetab table:

    Lists # of tiles in the sprite, and is linked to their layout:

    Index   |   Value   |   Tile Layout (XY)
    0           1           1x1
    1           2           1x2
    2           3           1x3
    3           4           1x4
    4           2           2x1
    5           4           2x2
    6           6           2x3
    7           8           2x4
    8           3           3x1
    9           6           3x2
    A           9           3x3
    B           C           3x4
    C           4           4x1
    D           8           4x2
    E           C           4x3
    F           10          4x4

  To get the index, you need to look at bytes 2-3. 

Bytes 2-3: Sizetab bytes

Byte 2:
	Upper nibble (top 4 bits) used for the top 4 bits of tile data pointer (more later).
	Bottom nibble (bottom 4 bits) used as an index to the sizetab table.
Byte 3:
	Not used, always 00 in 92.

Tile Data Offset, H/V Priority, Palette:

This part gets a little complicated. The tile data offset is 15 bits. The lower 11 bits are in byte 4-5, and the upper 4 bits are in byte 2. 

Bytes 4-5: Data Offset, Flip and Palette

    Data Offset:
        This is used to find the 1st starting tile for the sprite.
        First 11 bits (0-10) are used for the data pointer.
        Then the top 4 bits are taken from byte 2 to get the full 15 bits of the offset (these are the upper 4 bits of the offset).
        The result is then sign-extended long word, multiplied by $20 (32 decimal), and used as an offset to the Spritetiles.
    
    Flip:
        Bits 11 and 12 are used for H and V flip of the sprite. If the lower nibble of byte 4 is 8 or higher, the sprite will be
        H flipped by default. If the upper nibble of byte 4 is odd, it will be V flipped by default. Later on, depending on the 
        handedness of the player, the whole frame can be flipped before stored in memory.

    Palette:
        The last 3 bits (13-15) are used for palette and priority.
        Bits 13 and 14 will decide which palette to use (Home, Visitor, 1st or 2nd Rink Palette). These bits can be manipulated before 
        stored in memory, depending on if it's a player and what team they are playing for.
        Bit 15 is priority. If it is set, the sprite will appear in front of the background.

If you want a better description of flip and palettes, please check out @AdamCatalyst's awesome post here - 

 

Let's look at our SPAskatewp example. We will choose the first frame from the animation, frame 7. But in order to find the data for frame 7, we do not know how long the frame data for 1-6 are (length will vary depending on the # of sprites in each frame). If we open the sprites.anim file in the 92 source, each start frame data structure starts with 53 53. If we were to automate this, we would look at the # of sprites in each frame data (offset $24), multiply that by 8, and add it to $26 (the length of the frame data struct before the sprite data starts) to get our frame data size, and continue through the frame data that way.

41 41 02 24 00 02
Frame 1:
53 53 00 CD 00 C2 00 20 00 40 00 00 00 00 00 00 00 FF 00 00 00 00 00 FF FF FC FF EC 00 00 00 00 00 00 00 FF 00 00 FF E7 07 00 46 A0 FF F9 

Frame 2:
53 53 00 CD 00 C2 00 30 00 40 00 00 00 00 00 00 00 FF 00 00 00 00 00 FF FF FF FF E8 00 00 00 00 00 00 00 FF 00 01 FF E5 07 00 46 98 FF F9 FF FD 20 00 46 0D FF F1 

Frame 3:
53 53 00 CD 00 C3 00 20 00 40 00 00 00 00 00 00 00 FF 00 00 00 00 00 FF FF FE FF E4 00 00 00 00 00 00 00 FF 00 00 FF E1 07 00 46 90 FF F8 

Frame 4:
53 53 00 CD 00 C2 00 20 00 40 00 00 00 00 00 00 00 FF 00 00 00 00 00 FF FF FF FF E7 00 00 00 00 00 00 00 FF 00 00 FF E4 07 00 46 88 FF FB 

Frame 5:
53 53 00 CD 00 C2 00 20 00 40 00 00 00 00 00 00 00 FF 00 00 00 00 00 FF 00 01 FF E7 00 00 00 00 00 00 00 FF 00 00 FF E3 07 00 46 80 FF FB 

Frame 6:
53 53 00 DB 00 C9 00 40 00 40 00 00 00 00 00 00 00 FF 00 00 00 00 00 FF 00 10 FF F8 00 01 00 00 00 00 00 FF 00 01 FF E8 07 00 46 78 FF F9 FF F6 24 00 43 4E 00 09 

Frame 7:
53 53 00 D8 00 CD 00 40 00 40 00 00 00 00 00 00 00 FF 00 00 00 00 00 FF 00 17 FF F6 00 C8 00 00 00 00 00 FF 00 03 FF F2 19 00 46 DE FF FA FF F2 20 00 46 0C 00 12 FF EA 24 00 43 4C FF FC FF E2 20 00 46 0B 00 04

 

Frame 7, byte $24 and $25 = 00 03, which means 4 sprites in the frame (3+1 =4)

Sprite 1:
FF F2 19 00 46 DE FF FA 
Sprite 2:
FF F2 20 00 46 0C 00 12 
Sprite 3:
FF EA 24 00 43 4C FF FC
Sprite 4:
FF E2 20 00 46 0B 00 04

Lets get the size in tiles and the tile data offsets for each sprite:

Sprite 1: FF F2 19 00 46 DE FF FA 

Sizetab index - lower nibble of Byte 2 = 9, which is a 3x2 tile sprite.

Tile data offset:
Byte 4+5 = $46DE. To get the first 11 bits, we AND with $7FF = $6DE
Byte 2+3 = $1900. To get the top nibble, we need to AND with $F000 = $1000
Since we are 1 bit off (only took 11 of the 12 bits from 4+5), we need to shift right 1 bit (divide by 2) $1000 >> 1 = $800
Now, we OR (or add) the 2 results together - $6DE OR $800 = $EDE.

We take our $EDE and mult. by $20 (32 decimal) = $1DBC0 -> this is our offset to the first tile in this sprite.

Sprite 2: FF F2 20 00 46 0C 00 12 

Sizetab = 0 = 1x1 sprite

Tile data offset:
$460C AND $7FF = $60C
$60C + $1000 ($2000 >> 1) = $160C
$160C * $20 = $2C180

Sprite 3: FF EA 24 00 43 4C FF FC

Sizetab = 4 = 2x1 sprite
Tile data offset: $434C AND $7FF = $34C
$34C + $1000 ($2000 >> 1) = $134C
$134C * $20 = $26980

Sprite 4: FF E2 20 00 46 0B 00 04

Sizetab = 0 = 1x1 sprite
Tile data offset: $460B AND $7FF = $60B
$60B + $1000 ($2000 >> 1) = $160B
$160B * $20 = $2C160

The sprite tiles in the NHL92 ROM start at $45F16. Each tile is 32 bytes of data (4bpp linear = 4 bits per pixel. 64 pixels are in a single tile, so this = 64 * 4 = 256, divide by 8 bits (8 bits = 1 byte), we get 32 bytes).

So our tile data for each sprite starts at:

$45F16 + $1DBC0 = $63AD6 - 3x2 sprite. 6 tiles = 192 bytes

$45F16 + $2C180 = $72096 - 1x1. 1 tile = 32 bytes

$45F16 + $26980 = $6C896 - 2x1. 2 tiles = 64 bytes

$45F16 + $2C160 = $72076 - 1x1. 1 tile = 32 bytes

Using tile molester (used HFD palette, $19C8):

1L.png

2L.png

3L.png

Sprite4L.png

 

Now, we need to assemble them using the global X and Y positions. Since the player is not located on the ice, we don't have an actual start position to work off of. So I will make a canvas big enough for the sprite, and designate a point near the bottom as the position. Usually the position of the player on the ice is where their feet are in the sprite.

Let's get the global X,Y for the sprites:

Sprite 1: FF F2 19 00 46 DE FF FA 
  Global Y (Byte 0+1) = FF F2 = -14 decimal
  Global X (Bytes 6+7) = FF FA = -6

Sprite 2:
FF F2 20 00 46 0C 00 12 
  Y = FF F2 = -14
  X = 00 12 = 18

Sprite 3:
FF EA 24 00 43 4C FF FC
  Y = FF EA = -22
  X = FF FC = -4
Sprite 4:
FF E2 20 00 46 0B 00 04
  Y = FF E2 = -30
  X = 00 04 = 4

Now we can assemble the frame. The black dot in the bottom shadow is the point used for 0,0.

Assembled.png

 

What about the Hot Spot of the frame? Let's look at the frame data again:

 

Frame 7:
53 53 00 D8 00 CD 00 40 00 40 00 00 00 00 00 00 00 FF 00 00 00 00 00 FF 00 17 FF F6 00 C8 00 00 00 00 00 FF 00 03 FF F2 19 00 46 DE FF FA FF F2 20 00 46 0C 00 12 FF EA 24 00 43 4C FF FC FF E2 20 00 46 0B 00 04

Hotspot data starts at offset $C, and is 24 bytes long:

00 00 00 00 00 FF 00 00 00 00 00 FF 00 17 FF F6 00 C8 00 00 00 00 00 FF

X Hotspot is bytes 12-13 - 00 17 - 23 decimal
Y Hotspot is bytes 13-14 - FF F6 - -10 decimal

These are offset from the Sort Cord's XY position, just like the sprites. Since we were able to derive the XY position in the frame (the black dot in bottom shadow), we can find the Hot Spot in the frame as well, 23 decimal to the right, and 10 decimal up:

 

AssembledHS.png

The black dot near the end of the stick blade is the Hot Spot. This is where the puck will "stick" to the blade in this frame.

Frame Data (NHL93-94)

The frame data in 93 and 94 is different than 92. In 92, everything was combined into one Frame Data structure for each frame (Hot spots, # of sprites, sprite data bytes). Here, it is all split up into different lists (probably to save space).

Hotlist table - Contains the X and Y Hot Spots for each frame. 1 byte for each Hot Spot, so 2 bytes for each frame. This is indexed using the current frame # of the Sort Cord :

X Hot Spot - Hotlist + frame*2
Y Hot Spot - Hotlist + frame*2 + 1

These are used in word size, so after the byte is retreived, it will sign extend it to word size (i.e $17 = 0017, $F6 = FFF6)

Hotlist table locations:

  • NHL93 (v1.1) - $743FE ($512 long)
  • NHL94 - $A44CA ($68A long)

 

# of Sprites - There is no data stored for this. Instead, there's a table of offsets to the sprite data bytes. The code will take the current frame's offset, and the next frame's offset, find the difference, divide by 8 (8 bytes per sprite), and find the # of sprites that way. 

 

Sprite Data Bytes - The layout for the sprite data bytes is a little different.

Sprite data bytes:
Byte 0-1: X Global
Byte 2-3: Y Global
Byte 4-5: Tile offset
Byte   6: Palette/HV Flip byte
Byte   7: Sizetab byte    

Note the differences from 92. We have a full byte for Sizetab, 2 bytes for Tile offset, and a full byte for palette and sprite orientation. This makes the data much easier to deal with compared to 92. So there is no need for extra tricky math here.

This layout is just like the layout given in Adam's post below. You can look at that post for a better description.

 

The sprite data bytes are accessed by adding the sprite data offset to the start of the list. 

 

Let's look at our example again, the SPAskatewp frame 7. Luckily, there are many frames shared across all 3 games, and this is one of them. So the goal in this example is to get the same data that we got in 92, but from the 94 ROM.

(Trivia: the animation for SPAskatewp has an extra frame in 92 than in 93 and 94. It's the still frame of the faced direction from the Directions GIF in this post).

Ok, let's get the Hot Spot data for frame 7 first, since that is the easiest:

Hotlist table starts at $A44C8 in 94.
Hot Spots for frame 7:

X Hot Spot - $A44C8 + (7 * 2) = $A44D6 address, which is $17. Sign extend, we get 00 17.
Y Hot Spot - $A44C8 + (7 * 2 + 1) = $A44D7 address, which is $F6. Sign extend, we get FF F6.

OK, Hot Spot is the same as 92.

Now, lets find the # of sprites in frame 7:

Frame offset list in 94 starts at $9E724.

Frame 7 offset:

$9E724 + (7*2) = $9E732 address -> 06 DE

We also need frame 8 offset:

$9E724 + (8*2) = $9E734 address -> 06 FE

$06FE - $06DE = $20 (32 decimal). $20 / 8 (8 bytes per sprite) = 4 sprites in frame

Same # of frames as 92.

Let's get the sprite data bytes for each sprite in the frame:

Sprite data offset list in 94 starts at $9E724.

Frame 7 offset = $06DE

$9E724 + $6DE = $9EE02

Since we found that there are 4 sprites, we need 32 bytes of data, split into 8 bytes per sprite:

Data:

FF FA FF F2 00 33 40 09 
00 12 FF F2 00 39 40 00 
FF FC FF EA 00 3A 40 04 
00 04 FF E2 00 3C 40 00

Let's break it up:

Sprite 1: FF FA FF F2 00 33 40 09

X Global - FF FA - -6 decimal
Y Global - FF F2 - -14 decimal
Tile data offset - 00 33 - $33 x $20 = $660
Palette/Flip - 40 - no flip, home team palette (can be changed later)
Sizetab - 09 - 3x2 sprite

Sprite 2: 00 12 FF F2 00 39 40 00

X - 00 12 - 18 decimal
Y - FF F2 - -14 decimal
Tile data offset - 00 39 - $39 x $20 = $720
Palette/Flip - 40 - no flip, home team palette (can be changed later)
Sizetab - 00 - 1x1 sprite

Sprite 3: FF FC FF EA 00 3A 40 04
X - FF FC - -4 decimal
Y - FF EA - -22 decimal
Tile data offset - 00 3A - $3A x $20 = $740
Palette/Flip - 40 - no flip, home team palette (can be changed later)
Sizetab - 04 - 2x1 sprite

Sprite 4: 00 04 FF E2 00 3C 40 00
X - 00 04 - 4 decimal
Y - FF E2 - -30 decimal
Tile data offset - 00 3C - $3C x $20 = $780
Palette/Flip - 40 - no flip, home team palette (can be changed later)
Sizetab - 00 - 1x1 sprite

 

Let's use the tile data offsets to find the starting tile data address in the ROM for each sprite:

Sprite tiles start at $5DE84 in 94.


Offsets:
1 - $660 -> $5DE84 + $660 = $5E4E4 (need 6 tiles, so 192 decimal bytes)
2 - $720 -> $5DE84 + $720 = $5E5A4 (need 1 tile, so 32 bytes) 
3 - $740 -> $5DE84 + $740 = $5E5C4 (need 2 tiles, so 64 bytes)
4 - $780 -> $5DE84 + $780 = $5E604 (need 1 tile, so 32 bytes)

Let's grab the tiles (using VAN home palette, $4828): 

94Sprite1L.png

94Sprite2L.png

94Sprite3L.png

94Sprite4L.png

 

And assemble them using the X and Y globals, with a reference point and the Hot Spot:

  image.png

Success! Frame 7 from 92 and 94 both look the same here.

 

AssembledHS.png          image.png

Adding sprites?

Is it possible? Yes, it is! I added the fighting sprites from 93 into 94 here:

This is for another topic though. Because all the frame and sprite related info are right next to each other in the ROM, the tables need to be moved, the offsets updated, and some hard coded pointers need changed. A topic for another day.

Table and Data Locations:

NHL92:

  • SPAList starts at $35B6 (start of frames.asm in ROM)
  • Frame data starts at $3D5F4 (first frame)
  • Sprite tiles start at $45F16

NHL93 (v1.1 ROM):

  • SPAList - $4DBE-$6446 ($16B8 long)
  • Sprite tiles - $3A3B0-$6FAF0 ($35740 long)
  • Frame sprite data offsets - $6FAF0-$70006 ($516 long, first 2 and last 2 bytes are 00 00, so $512 bytes are used for 649 frames)
  • Sprite data bytes - $70006-$743FE ($43F8 long, 2175 total sprites)
  • Hotlist table - $743FE-$74910 ($512 long)

NHL94:

  • SPAList - $5B1C-$76B2 ($1B96 long)
  • Sprite tiles - $5DE84-$9E724 ($408A0 long)
  • Frame sprite data offsets - $9E724-$9EDC2 ($69E long, first 2 and last 2 bytes are just used for start and end, so $69A bytes are used for 845 frames)
  • Sprite data bytes - $9EDC2-$A44CA ($5708 long, 2785 total sprites)
  • Hotlist table - $A44CA-$A4B54 ($68A long, which is 16 bytes shorter, 8 frames worth of XY Hot Spots, than the number of frames. The last 8 frames are the extra arrows and stars for the 3rd and 4th player, and they do not have hotspots.)

 

Big time credit and thanks go to @McMarkis and @bcrt2000. The 3 of us worked hard figuring this all out! 

 

Sprite1L.png

Sprite2L.png

Sprite3L.png

  • Love 3
  • Wow 1

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...