Skip to content

Animate players on the SDK base player anim state - #2244

Open
sunzenshen wants to merge 3 commits into
NeotokyoRebuild:masterfrom
sunzenshen:anim-sdk-base
Open

sunzenshen wants to merge 3 commits into
NeotokyoRebuild:masterfrom
sunzenshen:anim-sdk-base

Conversation

@sunzenshen

@sunzenshen sunzenshen commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Description

Moves the player animation back to the original system used to drive the original NT player animation (CBasePlayerAnimState).


Implements Cases 1, 2, 3, 4 and 7 (L1, L3, L4) of the animation review: jump pose, torso swap on small movements, standing twist, sprint legs, leg speed on the zero-movement models, ladder legs, feet on a flick.
Review page: exported review page (Dropbox) · live review page (opening it lets you request access with your email or Claude account).

This moves the player animation back onto the SDK base, CBasePlayerAnimState (shared/base_playeranimstate.cpp), which classic NEOTOKYO is built on and which neo has compiled all along without using it. A new CNEOPlayerAnimState (shared/neo/neo_playeranimstate.cpp) derives from it and replaces CHL2MPPlayerAnimState and its base CMultiPlayerAnimState (TF2's anim state, arrived with #653).

What the base gives, and what is kept from master

From the base, unchanged: the main leg sequence by activity, the 9-way leg pose scaled by speed over the sequence's ground speed, the upper-body aim layers (idle blended with walk / run / crouch walk by speed), the feet / eyes convergence (720 deg/s, 45 deg fade, 90 deg snap, face-front after 3 s via mp_facefronttime / mp_feetyawrate), and the server setting the body to the feet yaw.

In CNEOPlayerAnimState:

  • Leg activity: idle / walk / crouch idle / crouch walk by horizontal speed as in classic; run legs only while sprinting above 100 u/s ([WONTFIX?] Play the sprint legs only at sprint speed #2242's rule; neo's slowest sprint is 185 u/s, and bots sprint through turns at 100-180 u/s, where classic's 175 would swap the legs back and forth).
  • Ground speed: constant 100 (walk), 250 (run), 85 (crouch walk), as classic. Two original models carry no movement in a forward animation (NSF Support walk, Jinrai Recon 1-2 run), which made master's measured ground speed divide by about zero.
  • Jump: ACT_HOP from the jump event until the player lands; on the client, landing is the drawn body reaching the ground ([WONTFIX?] Hold the jump pose until a drawn player lands #2241's hull trace, 0.5 s flag fallback). Waist-deep water ends it, as in master.
  • Gestures: fire, reload, grenade / smoke throw, detpack throw and remote, knife and supa7 play on one layer above the aim layers, restarted by each event and cleared by a weapon switch, with master's sequence names and class substitutions (VIP, detpack, smac). PlayerAnimEvent_t keeps the event names the weapons send, so no weapon file changes.
  • Lean (Feet face aim yaw when leaning #1955): a leaning player's feet turn to the eyes.
  • Standing twist timer: the base counts the 3 s from the feet's last turn, so a player who stood still squared up on the next turn at once. The server, whose eye yaw changes in one tick, did that on every turn while the client, which interpolates the eyes, kept the twist: server and drawn body disagreed by up to 30 degrees (p90 28). The timer counts from the last frame the feet faced the eyes, or from the end of a lean.
  • Reset the player animation state when the model changes #2108: the base's two aim-layer transitioners are emptied on a model change, as master's are.

Dropped on purpose: master's TranslateActivity hook through the weapon's activity table. The only table is the knife's, compiled server-only and keyed on ACT_MP_* activities master's main activities never matched.

Kept from #653 unchanged: client-side animation (UseClientSideAnimation()), the excluded animation network fields, and the player anim event temp entity. #653 left the earlier neo anim state on this base because of "jittery animations"; that class ran without client-side animation and with the cycle excludes commented out. With #653's networking kept, the per-frame check below finds no cycle jitter at cl_updaterate 20.

What this does not change

Aim pitch (case 5, classic's -5 degrees comes from elsewhere), lean timing (case 6, viewmodel code), first-person weapon animations (case 8), the lean interpolation workaround in c_baseanimating.cpp (#653), ragdolls. Instant 180-degree eye turns: the 90-degree feet step can go to opposite sides on server and client, since which side an exact half turn falls on comes down to float rounding (with the eyes at 0 / 0.4 / 0.7 degrees off a half turn, 6 flips each: this PR 6 of 18 flips, master 5 of 18). Here the twist squares up after 3.6 s on both sides; in master it stays until the next turn. Classic's base has the same rule. L4: after a 60-degree flick while running, this PR's feet catch up faster than classic's (largest twist 16-21 degrees against 27-34, back under 3 degrees in 0.11-0.14 s against 0.17-0.19); the 720 deg/s rule is the same, and whether classic's eyes interpolate more slowly was not checked. L3: classic's legs during the climb itself are not verified (its log has no z, first person only); the table's Idle_lower is its reading at 2D speed 0.

How this was compared

The code came first: neo against the base classic is built on (base_playeranimstate.cpp, the SDK template sdk_playeranimstate.cpp), and classic's client.dll read for names, cvars and constants. Then per-frame animation logs from both games (classic's cl_showanimstate_log, neo's harness log) with the same map (nt_rise_ctg), spot, camera, class and weapon (NSF Assault bot with the MPN on bot_mimic 1, its eyes turned by bot_mimic_yaw_offset keys), the same scripted keys, read by the same scripts. Then side-by-side clips of classic, master and this PR. Numbers come from the logs, not from watching. neo builds: Release, master f426c22 and this PR; the Debug checks are named. Built with GCC on Linux; not built on MSVC here (upstream CI).

Results against classic and master

Review-page pass (eyes turn, lean, pitch, crouch, two jumps, fire, reload, switch, strafe taps, crouch walk, run, running jump):

case classic master this PR per-case PR
2 torso walk share by speed, taps (0.5-10 / 10-50 / 50-90 / 90-120 / 120+ u/s) .04 / .29 / .70 / .98 / 1 .67 / .76 / .92 / .97 / .99 .04 / .27-.29 / .70-.72 / .97-.98 / 1 #2240
2 same, crouch walk .05 / .31 / .87 / 1 / 1 .57 / .79 / 1 / 1 / 1 .03-.06 / .30-.31 / .87 / 1 / 1 #2240
2 standing frames with walk torso >= 0.5 0 24 of 342 0 #2240
1 standing jump pose length 0.55 / 0.54 s 0.37 / 0.43 s 0.55-0.56 / 0.51-0.53 s #2241: 0.55 / 0.52
1 after landing: walk legs / walk torso none 0.2 s / 7-8 frames none #2241: none
3 30 deg turn: feet square up after 3.18 s never (held until the next turn) 2.94-3.14 s #2243: 2.94 s
3 back 120 deg: twist held 23-25 90 30 #2243: 30
4 slowest sprint-leg frame at start / at a stop with the key held 180-194 / 218 (needs > 175) 74 / 1 127 / 110 #2242: 100-127 / 104
4 sprint-leg frames below 100 u/s 0 32 0 #2242: 0
7 L1 Jinrai Recon 1 sprint, move pose at 105-233 u/s v / 250 0.15-1.02, 97 of 103 frames on the blend edge 0.42-0.93, none on the edge -
7 L3 ladder climb (west alley ladder, nt_rise_ctg): legs while climbing Idle_lower (after a 0.9 s Jump on mounting; climb not verified, first person) walk_lower Idle_lower -
7 L4 eyes flick 60 deg while running: largest twist / back under 3 deg 27-34 deg / 0.17-0.19 s 21-35 (one 64) / 0.18-0.23 s 16-21 / 0.11-0.14 s -
gestures: fire / reload 0.76 / 3.08 s 0.76 / 3.05 s 0.76 / 3.04 s -

Gestures on the local player (fire standing and crouched, reload, knife, frag, smoke, supa7 shot and shell reload, a reload cut by a switch, detpack throw and remote): the same sequences and lengths as master within a frame.

Server and network:

  • Server body yaw (the hitboxes) against the drawn feet of the same player, turning and leaning: median 0.1 deg, p90 0.2, over 10 deg in 16 frames (master 38, at its 90-degree jumps). One lag-compensated burst at a still player registers its hits.
  • cl_updaterate 20: on every frame the leg cycle advances by exactly rate x frame time, no backward steps (master the same), in the review pass and in bot spectating.

Bots (10-12, ntre_rise_ctg, spectating in-eye and chase at cl_updaterate 20; bot_class recon, assault, support, VIP; a juggernaut on ntre_sentinel_jgr): no living player drew sequence 0 (spawn frames included), no missing sequence besides master's own Reload_ZR68C for a VIP, ghost carrier upper body present, no run or crouch-walk legs on a still frame (walk legs on a still frame: 13 frames, master 7), no feet spin on a spectator target switch. Standing twists over 20 degrees held more than 3.5 s: 0 (master 43 frames). Walk / run leg swaps while sprinting: 21 (master 37).

Debug client, 10 bots: class and model changes with respawns (#2108), a round restart, a juggernaut, jumps: only the known neo_root.cpp localizedWszStr assert at startup, no fixme: animDesired. A bot class change at a round restart crashes the Debug server in master too (CNEOPredictedViewModelMuzzleFlash, outside the anim state).

Clips (classic / master / this PR, MP4): case 1 jumps, case 2 strafe taps, case 3 face-front, case 4 sprint (master / this PR; classic cannot be filmed in third person), crouch walk / run / running jump, gestures, lean, L4 flick while running; L3 ladder (master / this PR, camera behind in a dark shaft; no classic ladder clip was recorded).

How to test

  1. Strafe-tap or walk slowly next to another player: the upper body stays near the idle pose and blends to the walk pose with speed.
  2. Jump in place: the jump pose holds until the landing; no walk legs after landing.
  3. Stand and turn the view 30 degrees: the feet hold, then square up about 3 s later. Lean: they square up at once.
  4. Sprint and stop with the sprint key held: run legs only while sprinting fast.
  5. Fire, reload, throw grenades and the detpack, switch weapons mid-reload: the same gestures as before.

Comment thread src/game/client/hl2mp/c_hl2mp_player.h
Comment thread src/game/server/hl2mp/hl2mp_player.h
Comment on lines +237 to +272
//-----------------------------------------------------------------------------
// Purpose: Whether a jumping player has landed. The client draws other players a few tenths of a second behind
// their networked ground flag, so there the jump lasts until the drawn body reaches the ground.
//-----------------------------------------------------------------------------
bool CNEOPlayerAnimState::HasLandedFromJump()
{
if ( !( m_pPlayer->GetFlags() & FL_ONGROUND ) )
{
#ifdef CLIENT_DLL
m_flJumpGroundFlagTime = 0.0f;
#endif
return false;
}

#ifdef CLIENT_DLL
if ( m_flJumpGroundFlagTime <= 0.0f )
{
m_flJumpGroundFlagTime = gpGlobals->curtime;
}

if ( gpGlobals->curtime - m_flJumpGroundFlagTime > JUMP_LAND_MAX_FLAG_LEAD )
{
return true;
}

const Vector &vecOrigin = m_pPlayer->GetAbsOrigin();
trace_t tr;
UTIL_TraceHull( vecOrigin, vecOrigin - Vector( 0, 0, JUMP_LAND_GROUND_DIST ),
m_pPlayer->GetPlayerMins(), m_pPlayer->GetPlayerMaxs(),
MASK_PLAYERSOLID, m_pPlayer, COLLISION_GROUP_PLAYER_MOVEMENT, &tr );

return tr.fraction < 1.0f || tr.startsolid;
#else
return true;
#endif // CLIENT_DLL
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain what this code is doing? A player's animation determines where their hitboxes are, so it's very important we predict a players animation state correctly. Seeing different logic on the client vs server makes me nervous.

@sunzenshen sunzenshen Oct 24, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for this code was an attempt to fix an animation quirk with the jump animation, where in the middle of the jump the walk animation is triggered on the way down.

jump-reset

Even with the side by side comparison and foreknowledge of the transition, I don't easily notice it, so I can revert this change and try to stay consistent with upstream. Then we can address this case in a future PR if people still see the issue. I'll take a look when I get back to my dev setup.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Falling from a great height shows the issue more clearly
image
image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually just moving in a direction while landing

@AdamTadeusz AdamTadeusz Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is subjective but testing with host_timescale 0.25, I prefer the look where the character begins moving their legs in the direction of their movement in mid air.

sunzenshen and others added 3 commits October 25, 2026 21:48
Move the player animation back onto CBasePlayerAnimState, the 2006
SDK base classic NEOTOKYO is built on, which neo compiles but did not
use. CNEOPlayerAnimState replaces CHL2MPPlayerAnimState and takes
CMultiPlayerAnimState out of the build (the files stay on disk).

The base gives classic's torso blend by speed, constant leg ground
speeds, the 3 s face-front timer and its feet rules. The new class
adds the run legs only while sprinting, the jump held until a drawn
landing, one gesture layer for fire, reload, grenade and detpack
events, the lean rule, the NeotokyoRebuild#2108 transitioner reset, and a face-front
timer counted from the last squared frame so server and client agree.
Client-side animation and the network excludes from NeotokyoRebuild#653 are kept.
Wrap neo_playeranimstate.h includes in #ifdef NEO blocks in the
HL2MP player headers, following review feedback.

Remove HasLandedFromJump() — jump animation tampering is out of
scope for this PR. The jump pose now ends when the player has been
in the air at least JUMP_MIN_TIME and FL_ONGROUND is set.

Also removes the JUMP_LAND_* constants and m_flJumpGroundFlagTime
member that were only used by that function.

@AdamTadeusz AdamTadeusz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not opposed to the attempt to stop the legs from running mid air, I just think we need to be careful about diverging behavior between client and server. Also I'm curious if there is a way to check how far away from the ground a player is without doing a trace, maybe by comparing a player's interpolated position against their networked position or something like that, although I couldn't figure it out in the limited testing I did, but at least it makes sense to me in theory that the update that updates the Ground flag should also have the player origin on the ground, and we can maybe check somehow how the players draw origin compares against that, but again an idea for another PR. IDK, networking is hard

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Animations Animation stuff. Including the syncronizing of animations between client and server based on state.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants