Animate the player: Rukhanka skeletal locomotion (client-derived)
Replace the capsule with a rigged Synty SciFiSpace soldier driven by Rukhanka 2.9 (netcode replication off; animation derived client-side from replicated state). Adds a slim top-down AnimatorController (idle / 2D-strafe locomotion / death) from Synty clips; client-only PlayerAnimationDriveSystem (local CC-velocity + remote position-delta paths); AnimParamMath (+10 EditMode tests); ServerStripAnimationSystem (disables Rukhanka on the server, zero server-side animation). Client.asmdef gains Rukhanka.Runtime/CharacterController/Physics. EditMode 204/204; Play-validated. See DR-022. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
using ProjectM.Simulation;
|
||||
using Rukhanka; // FastAnimatorParameter, AnimatorParametersAspect, ParameterValue,
|
||||
// AnimatorControllerParameterComponent, AnimatorControllerParameterIndexTableComponent,
|
||||
// RukhankaAnimationSystemGroup
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.NetCode; // GhostOwnerIsLocal
|
||||
using Unity.Transforms; // LocalTransform
|
||||
using Unity.CharacterController; // KinematicCharacterBody
|
||||
|
||||
namespace ProjectM.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Client-only animation driver. OBSERVES authoritative/replicated state and writes Rukhanka animator
|
||||
/// blend params; never mutates the sim (presentation-only). Runs once/frame in the client/local world,
|
||||
/// BEFORE Rukhanka evaluates the controller this frame (same-frame, no 1-tick lag). NOTE: this lands in
|
||||
/// SimulationSystemGroup (via UpdateBefore the Rukhanka group, which itself has no UpdateInGroup), a
|
||||
/// deliberate, documented exception to the project's "all juice = PresentationSystemGroup" rule -- the
|
||||
/// params MUST be set before Rukhanka's same-frame controller eval (see DR-022).
|
||||
///
|
||||
/// Two paths:
|
||||
/// LOCAL (owner-predicted, GhostOwnerIsLocal ENABLED): realized CC RelativeVelocity (wall-aware).
|
||||
/// REMOTE (interpolated, GhostOwnerIsLocal DISABLED): KinematicCharacterBody is NOT a [GhostField] and
|
||||
/// the CC processor is owner-only, so RelativeVelocity stays baked-zero on remotes -> derive
|
||||
/// planar velocity from replicated LocalTransform.Position deltas. PlayerFacing.Direction is a
|
||||
/// [GhostField] (valid on remotes); EffectiveCharacterStats.MoveSpeed is derived locally each
|
||||
/// tick by StatRecomputeSystem (present on remotes). Cache prevPos per Entity, prune each frame.
|
||||
/// </summary>
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.LocalSimulation | WorldSystemFilterFlags.ClientSimulation)]
|
||||
[UpdateBefore(typeof(RukhankaAnimationSystemGroup))]
|
||||
[RequireMatchingQueriesForUpdate]
|
||||
public partial class PlayerAnimationDriveSystem : SystemBase
|
||||
{
|
||||
// Perfect-hash keys, built once (managed string ctor). Names MUST match AC_PlayerTopDown.controller
|
||||
// parameter names exactly. Immutable readonly hashes -> domain-reload safe.
|
||||
static readonly FastAnimatorParameter k_MoveX = new FastAnimatorParameter("MoveX");
|
||||
static readonly FastAnimatorParameter k_MoveZ = new FastAnimatorParameter("MoveZ");
|
||||
static readonly FastAnimatorParameter k_Speed = new FastAnimatorParameter("Speed");
|
||||
static readonly FastAnimatorParameter k_IsDead = new FastAnimatorParameter("IsDead");
|
||||
|
||||
// Remote prevPos cache (per ghost Entity). Pruned every frame (a vanished remote = a despawn).
|
||||
NativeParallelHashMap<Entity, float3> _prevPos;
|
||||
|
||||
protected override void OnCreate()
|
||||
{
|
||||
_prevPos = new NativeParallelHashMap<Entity, float3>(16, Allocator.Persistent);
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
if (_prevPos.IsCreated) _prevPos.Dispose();
|
||||
}
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
float dt = SystemAPI.Time.DeltaTime; // wall-frame delta is correct for presentation
|
||||
if (dt < 1e-5f) dt = 1e-5f;
|
||||
|
||||
// --- LOCAL owner (CC velocity) ---
|
||||
var localJob = new LocalDriveJob
|
||||
{
|
||||
moveX = k_MoveX, moveZ = k_MoveZ, speed = k_Speed, isDead = k_IsDead,
|
||||
};
|
||||
Dependency = localJob.ScheduleParallel(Dependency);
|
||||
|
||||
// --- REMOTE players (position-delta). Single-threaded write to the shared prevPos cache. ---
|
||||
var seen = new NativeParallelHashSet<Entity>(16, Allocator.TempJob);
|
||||
var remoteJob = new RemoteDriveJob
|
||||
{
|
||||
moveX = k_MoveX, moveZ = k_MoveZ, speed = k_Speed, isDead = k_IsDead,
|
||||
dt = dt,
|
||||
prevPos = _prevPos,
|
||||
seen = seen,
|
||||
};
|
||||
Dependency = remoteJob.Schedule(Dependency); // .Schedule (not parallel): mutates _prevPos
|
||||
// Prune stale entries (despawned remotes) AFTER the job, on the main thread.
|
||||
Dependency.Complete();
|
||||
PruneCache(seen);
|
||||
seen.Dispose();
|
||||
}
|
||||
|
||||
void PruneCache(NativeParallelHashSet<Entity> seen)
|
||||
{
|
||||
using var keys = _prevPos.GetKeyArray(Allocator.Temp);
|
||||
for (int i = 0; i < keys.Length; i++)
|
||||
if (!seen.Contains(keys[i])) _prevPos.Remove(keys[i]);
|
||||
}
|
||||
|
||||
// LOCAL: GhostOwnerIsLocal ENABLED -> exactly the owned player. WithPresent<Dead> so alive
|
||||
// (Dead-disabled) players are visited. NOTE: GhostOwnerIsLocal as a WithAll filter respects the
|
||||
// enable bit; do NOT take it as an `in` parameter (that matches on presence -> drives remotes too).
|
||||
[BurstCompile]
|
||||
[WithAll(typeof(GhostOwnerIsLocal))]
|
||||
[WithPresent(typeof(Dead))]
|
||||
partial struct LocalDriveJob : IJobEntity
|
||||
{
|
||||
public FastAnimatorParameter moveX, moveZ, speed, isDead;
|
||||
|
||||
void Execute(
|
||||
AnimatorControllerParameterIndexTableComponent indexTable,
|
||||
DynamicBuffer<AnimatorControllerParameterComponent> parametersArr,
|
||||
in PlayerFacing facing,
|
||||
in EffectiveCharacterStats stats,
|
||||
in KinematicCharacterBody body,
|
||||
EnabledRefRO<Dead> dead)
|
||||
{
|
||||
var a = new AnimatorParametersAspect(parametersArr, indexTable);
|
||||
float3 p = AnimParamMath.LocomotionParams(body.RelativeVelocity, facing.Direction, stats.MoveSpeed);
|
||||
Write(ref a, p, dead.ValueRO, moveX, moveZ, speed, isDead);
|
||||
}
|
||||
}
|
||||
|
||||
// REMOTE: GhostOwnerIsLocal DISABLED -> interpolated teammates. Velocity from LocalTransform.Position
|
||||
// delta (KinematicCharacterBody.RelativeVelocity is baked-zero on remotes, non-GhostField, owner-only).
|
||||
[BurstCompile]
|
||||
[WithDisabled(typeof(GhostOwnerIsLocal))]
|
||||
[WithPresent(typeof(Dead))]
|
||||
partial struct RemoteDriveJob : IJobEntity
|
||||
{
|
||||
public FastAnimatorParameter moveX, moveZ, speed, isDead;
|
||||
public float dt;
|
||||
public NativeParallelHashMap<Entity, float3> prevPos;
|
||||
public NativeParallelHashSet<Entity> seen;
|
||||
|
||||
void Execute(
|
||||
Entity e,
|
||||
AnimatorControllerParameterIndexTableComponent indexTable,
|
||||
DynamicBuffer<AnimatorControllerParameterComponent> parametersArr,
|
||||
in LocalTransform xform,
|
||||
in PlayerFacing facing,
|
||||
in EffectiveCharacterStats stats,
|
||||
EnabledRefRO<Dead> dead)
|
||||
{
|
||||
seen.Add(e);
|
||||
float3 cur = xform.Position;
|
||||
float3 vel = float3.zero;
|
||||
if (prevPos.TryGetValue(e, out var prev))
|
||||
vel = (cur - prev) / dt;
|
||||
prevPos[e] = cur;
|
||||
|
||||
var a = new AnimatorParametersAspect(parametersArr, indexTable);
|
||||
float3 p = AnimParamMath.LocomotionParams(vel, facing.Direction, stats.MoveSpeed);
|
||||
Write(ref a, p, dead.ValueRO, moveX, moveZ, speed, isDead);
|
||||
}
|
||||
}
|
||||
|
||||
// ParameterValue has implicit float/bool operators -> SetParameterValue(key, float) / (key, bool) compile.
|
||||
static void Write(ref AnimatorParametersAspect a, float3 p, bool isDeadVal,
|
||||
FastAnimatorParameter moveX, FastAnimatorParameter moveZ,
|
||||
FastAnimatorParameter speed, FastAnimatorParameter isDead)
|
||||
{
|
||||
if (a.HasParameter(moveX)) a.SetParameterValue(moveX, p.x);
|
||||
if (a.HasParameter(moveZ)) a.SetParameterValue(moveZ, p.y);
|
||||
if (a.HasParameter(speed)) a.SetParameterValue(speed, p.z);
|
||||
if (a.HasParameter(isDead)) a.SetParameterValue(isDead, isDeadVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ec2539089c1135e4cbe1f320a604119a
|
||||
Reference in New Issue
Block a user