This round came straight out of playtesting. Every item below started as a note like "the animations just feel too fast" or "if I press S while locked on, he just turns around and walks back". Here is what changed and what I learned fixing it.

Everything was playing at double speed

The biggest fix of the session was also the smallest in code. The movement felt twitchy, the landing roll looked like a fast forward, and punches snapped out too quickly. I kept tuning play rates down until I measured the clips themselves: they are authored at 30 frames per second, but the conversion step had stamped them as 60. Every animation in the game was running twice as fast as intended.

The fix was to run the whole pipeline at 30 fps (export, retarget and import), then undo all the play rate hacks I had added to compensate. The landing roll is back to a play rate of 1.0, and attack hit windows now line up with the real swing:

C++
// Hit/recover times scaled x1.8 when the clips moved to their real 30 fps speed
Steps.Add(MakeStep(8, 4, 0, 0.32f, 0.54f, 130, 60));
Steps.Add(MakeStep(9, 5, 0, 0.32f, 0.54f, 130, 60));
Steps.Add(MakeStep(14, 12, 0, 0.45f, 0.81f, 140, 70, 300)); // finisher with a small step forward

Lesson written into the project notes: before touching play rates, check the frame rate of the data.

Lock-on as a toggle, with a halo

Middle mouse (or R3 on a gamepad) now toggles lock-on. The target is picked inside a 60 degree cone in front of the camera, within 25 meters, and only if there is a clear line of sight. The locked target gets a soft white halo: an unlit translucent overlay material driven by a Fresnel term, so it glows around the silhouette without hiding the model. It uses Unreal's per-mesh overlay material, so no custom render pass was needed.

While locked on, attacks snap to face the target when they start, so punches stop flying off in whatever direction the character happened to be facing.

Strafing that keeps the right arm carriage

Locked-on movement needs strafe and backpedal clips, which the placeholder set does not have. I brought in generic strafe clips made for a different skeleton and wrote a Blender retarget script for them. It compares each bone's world rotation against both rigs' T-poses, scales the hip height to the new body, and removes root travel so the clips stay in place.

Two problems showed up. The sideways walks turned the whole body almost 70 degrees, so the script now measures the average hip facing across the clip and cancels it. Also, the generic arms looked nothing like the game's relaxed swing. So the blend is layered: legs and hips come from the strafe clip, while the spine and everything above it comes from the normal forward clip at the same stride phase. The anim instance builds an upper-body bone mask once per bone setup and blends only those bones:

C++
// Strafing: legs from the strafe clips, upper body from the forward clips
if (UpperBodyWeight > ZERO_ANIMWEIGHT_THRESH)
{
    FPoseContext Upper(Output);
    if (BlendLayers(true, Upper))
    {
        RefreshUpperBodyMask(Output);
        for (int32 Index : UpperBodyBones)
        {
            const FCompactPoseBoneIndex BoneIndex(Index);
            Locomotion.Pose[BoneIndex].BlendWith(Upper.Pose[BoneIndex], UpperBodyWeight);
        }
    }
}

One more gotcha: the source clips were 60 fps, and Blender's FBX importer silently switches the scene to the file's rate. Two of the six clips ended up with lengths halfway between frames, and Unreal refused them. The script now reads the source rate and resets the scene before exporting.

Movement that matches the original

  • WASD jogs. Walking is for a slight stick tilt, or the comma key as a walk toggle on keyboard.
  • Low stamina swaps the jog for a tired run, and a hunched walk is ready as a posture option for character creation later.
  • A normal jump uses a standing takeoff. A sprint jump has its own takeoff, keeps its momentum when you let go of the keys, and ends in a landing roll. Plain jumps no longer roll.
  • Standing still for 12 seconds plays an idle fidget.
  • Unarmed means unarmed: punches and kicks only, heavy attack on press with no charge, and no skills.

HUD, enemy bars and a pause menu

The HUD now follows the original layout: round minimap bottom left, health number and bars right beside it, the info log top left, and control hints bottom right. Enemies show a named health bar above their heads in #8e0470 once they engage you within range, and bosses get a wide bar at the top center of the screen instead. Escape, P or Start opens a pause menu laid out like the original, with a cross of main options, a description box and key hints. Most entries are placeholders for now.

An in-game clip previewer

Picking the right clip from a long list of numbered files is slow when you only have filenames. So the game has a debug previewer: press T to cycle categories and 1 to 9 to play a clip full body on the character, 0 to go back to normal animation. It already helped choose the idle, walk, jog and jump clips. It now holds candidates for falling, ledge teetering, knockbacks, knockdowns and deaths, which are the next features.

Next up

  • A falling state for long drops, with a hard landing.
  • Ledge teeter: stop at an edge with a balance animation, and fall if you keep pushing.
  • Death animations blending into ragdoll.
  • Knockback and knockdown reactions when hit.