Skip to content
Damian Small

Investigating Biped Animations in Halo Combat Evolved

While developing replay ghosts for Halo Combat Evolved, I needed spawned bipeds to animate.

The replay system could already record and restore a player's position, rotation and movement. This worked for vehicles, but a spawned player biped remained frozen in its current pose while moving around the map.

There were two possible approaches:

  • record and restore every body-part transform; or

  • reproduce the compact animation state and allow Halo to animate the biped.

Recording the complete pose would work, but it would add a large amount of data to every replay frame. The second option was preferable, provided a spawned biped could enter Halo's native animation update path.

Biped body-part transforms

The standard cyborg biped has 18 body-part transforms stored in its object memory. Each transform is 0x34 bytes and contains 13 floats.

The following is pseudocode for reading one transform: [1]

function getBodyPart(address, offset)
    address = address + (offset or 0x0)

    return {
        scale = read_float(address + 0x00),

        rotationx0 = read_float(address + 0x04),
        rotationy0 = read_float(address + 0x08),
        rotationz0 = read_float(address + 0x0C),

        rotationx1 = read_float(address + 0x10),
        rotationy1 = read_float(address + 0x14),
        rotationz1 = read_float(address + 0x18),

        rotationx2 = read_float(address + 0x1C),
        rotationy2 = read_float(address + 0x20),
        rotationz2 = read_float(address + 0x24),

        x = read_float(address + 0x28),
        y = read_float(address + 0x2C),
        z = read_float(address + 0x30),
    }
end

The values appear to contain a scale, a 3×3 rotation matrix and a translation. The known transform offsets are:

0x550, 0x584, 0x5B8, 0x5EC, 0x620, 0x654,
0x688, 0x6BC, 0x6F0, 0x724, 0x758, 0x78C,
0x7C0, 0x7F4, 0x828, 0x85C, 0x890, 0x8C4

This produces 234 floats for a complete pose, before recording the object's position, orientation, velocity or other replay data.

It would also require the replay system to apply the pose continuously. If Halo recalculated the node transforms afterwards, the values could be overwritten during the same update.

For exact pose capture or procedural animation, these transforms are still useful. They were not a good primary format for a racing replay.

Finding the compact animation state

Runtime testing identified a much smaller group of fields used by the active animation:

Object offset

Size

Purpose

0xD0

16-bit

Base animation index

0xD2

16-bit

Current animation frame

0xD4

16-bit

Transition frame

0xD6

16-bit

Transition length

0x2A0

8-bit

Animation stance

0x2A3

8-bit

High-level animation state

These values are small enough to include in the replay format. The remaining question was whether Halo would advance them for a biped created using spawn_object("bipd", ...).

Following the native animation update

Writing an animation index or frame does not prove that an object is being animated. The values can exist in memory without Halo advancing them.

Following the biped update code in Ghidra showed several checks before the object reaches the native frame stepper. These include:

  • the game has started;

  • the object has a valid sub-object pointer;

  • an external object lookup succeeds;

  • the per-class data at [ESI+0x44] is not -1; and

  • an animation is assigned at [EDI+0xD0].

The per-frame biped update is dispatched indirectly through a function-pointer table at DAT_0069B75C.

The primary biped frame stepper was identified as FUN_004C1530. A second function, FUN_004D48D0, advances the frame at biped+0xD2 and selects the next animation state when the current animation finishes.

This initially suggested that a spawned biped might be filtered out before reaching the frame stepper. Reproducing the required object registration data in Lua would not have been practical, and calling the function directly would require the correct object and register state.

Testing a spawned cyborg

The next test spawned a standard cyborg biped and wrote animation index 173 to biped+0xD0.

The result was the native forward-running animation. The frame at biped+0xD2 advanced without further writes and the animation looped after reaching its final frame.

This happened without repeatedly setting the frame, calling an animation selector or injecting a DLL. Halo's native animation code was updating the spawned object.

Breakpoints also confirmed that the spawned cyborg reached the native biped frame stepper.

The earlier frozen results therefore did not mean that spawned bipeds were excluded from animation updates. They showed that writing an animation field alone was not always enough to produce a valid result. The biped and its animation state still had to be initialised correctly.

Applying this to replay ghosts

The replay system can keep movement and animation as separate parts of playback.

The existing transform playback remains responsible for:

  • position;

  • velocity;

  • forward and up vectors; and

  • facing direction.

The animation data can store the animation index, frame, transition values, state and stance. Halo can then calculate the final body-part transforms.

This is considerably smaller than storing 18 transforms for every sample. It also allows movement to be interpolated without separately interpolating every body part.

There is still a timing decision to make. Writing the recorded frame on every update would fight against Halo's native frame advancement. The replay player could instead apply animation changes when they occur, allow Halo to advance normally, and periodically correct the frame if playback drifts.

Direct body-part animation

The body-part transforms may still be useful for animation that does not already exist in the map. Possible uses include:

  • procedural posing;

  • additive offsets over Halo's base pose;

  • client-side inverse kinematics;

  • custom gestures without a compiled animation; and

  • paired animations such as assassinations.

The main unknown is where these transforms can be written without Halo replacing them later in the animation or render update.

Body-part transforms are also separate from the object's root transform. Moving the skeleton does not necessarily move the biped's collision volume or network position. A custom sequence would need to manage the object position, pose, camera and player input separately.

One assassination script I examined avoided this problem by using a custom vehicle tag as the animation carrier. The script spawned the vehicle, placed the victim and attacker in separate seats, and allowed the vehicle's authored animation to move both players.

This works because the clients already know how to render players seated in a vehicle. It also prevents normal player movement during the sequence.

The limitation is that the vehicle and its animation must be included in the map. It cannot add a new paired animation to stock maps.

Server and client replication

Replication remains the main limitation.

Halo is unlikely to transmit every body-part matrix for each player. It is more likely that clients receive compact object and animation state, then evaluate the animation graph locally.

If the compact animation fields are replicated, animations already available in the map should be reproducible on each client. Direct body-part writes made by a server-side Lua script may never reach remote clients.

A client mod such as Chimera would be the more realistic option for procedural animation. It could write the transforms locally, control the camera and restrict input while an animation plays. Players without the client script would still require a fallback.

The compact animation fields also need to be tested with remote clients. Proving that the dedicated server advances a spawned biped confirms that the object can use Halo's native animator. It does not prove that arbitrary server-side animation writes are replicated.

Conclusion

The investigation identified two useful levels of biped animation data:

  1. Compact native animation state — small enough for replay recordings and capable of driving Halo's own frame stepper.

  2. Final per-node transforms — much larger, but potentially useful for procedural or client-side animation.

For replay ghosts, the compact animation state is the better option. A spawned cyborg was able to play, advance and loop a native animation after setting its animation index. This allows the replay format to remain small while Halo performs the skeletal animation itself.

The body-part transforms remain useful for procedural animation and further investigation, but their update timing and client replication are not yet proven.

The next step is to record the compact fields during a real lap, apply them to the spawned replay biped and verify the result from a separate client.

References

  1. [1]
    Halo CE memory offsets — Wizard & aLTis94

    Community-maintained collection of Halo Custom Edition memory offsets.