Combat: melee swing animation + live range slash-arc VFX (MC-4 polish)
Rukhanka swing animation: PlayerRigTools builds a procedural Root-bone PlayerMeleeSwing.anim and adds an IsAttacking param + MeleeSwing state to AC_PlayerTopDown (mirroring the enemy attack recipe -- no authored Synty Generic melee clip exists). PlayerAnimationDriveSystem pulses IsAttacking from the replicated MeleeCombo swing window (local + remote, NetworkTick wrap-safe, re-triggers per chained hit). CombatFeedbackSystem flashes a procedural cone slash-arc mesh matching the LIVE cleave range + half-angle on each swing (finisher wider/warmer) -- the arc IS the range telegraph. Addresses 'range isn't clear + no animation'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.NetCode; // GhostOwnerIsLocal
|
||||
using Unity.NetCode; // GhostOwnerIsLocal, NetworkTime, NetworkTick
|
||||
using Unity.Transforms; // LocalTransform
|
||||
using Unity.CharacterController; // KinematicCharacterBody
|
||||
|
||||
@@ -20,13 +20,17 @@ namespace ProjectM.Client
|
||||
/// 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).
|
||||
///
|
||||
/// Drives MoveX/MoveZ/Speed/IsDead from locomotion + IsDead, plus IsAttacking (MC-4) pulsed from the
|
||||
/// replicated <see cref="MeleeCombo"/> swing window so the AC_PlayerTopDown MeleeSwing state plays per swing.
|
||||
///
|
||||
/// 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.
|
||||
/// tick by StatRecomputeSystem (present on remotes). MeleeCombo replicates so teammates' swings
|
||||
/// animate too. Cache prevPos per Entity, prune each frame.
|
||||
/// </summary>
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.LocalSimulation | WorldSystemFilterFlags.ClientSimulation)]
|
||||
[UpdateBefore(typeof(RukhankaAnimationSystemGroup))]
|
||||
@@ -35,10 +39,16 @@ namespace ProjectM.Client
|
||||
{
|
||||
// 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");
|
||||
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");
|
||||
static readonly FastAnimatorParameter k_IsAttacking = new FastAnimatorParameter("IsAttacking");
|
||||
|
||||
// Ticks after a swing-start that IsAttacking stays true (drives the MeleeSwing state). Kept < the swing lock
|
||||
// (MeleeRecoverTicks ~16) so a CHAINED swing re-pulses the bool false->true and re-triggers the Any State
|
||||
// transition per hit. ~0.22s @ 60Hz. Presentation-only.
|
||||
const uint k_AttackAnimTicks = 13;
|
||||
|
||||
// Remote prevPos cache (per ghost Entity). Pruned every frame (a vanished remote = a despawn).
|
||||
NativeParallelHashMap<Entity, float3> _prevPos;
|
||||
@@ -58,10 +68,14 @@ namespace ProjectM.Client
|
||||
float dt = SystemAPI.Time.DeltaTime; // wall-frame delta is correct for presentation
|
||||
if (dt < 1e-5f) dt = 1e-5f;
|
||||
|
||||
// Current authoritative tick for the swing-window check (default = invalid -> IsAttacking stays false).
|
||||
NetworkTick serverTick = SystemAPI.TryGetSingleton<NetworkTime>(out var nt) ? nt.ServerTick : default;
|
||||
|
||||
// --- LOCAL owner (CC velocity) ---
|
||||
var localJob = new LocalDriveJob
|
||||
{
|
||||
moveX = k_MoveX, moveZ = k_MoveZ, speed = k_Speed, isDead = k_IsDead,
|
||||
isAttacking = k_IsAttacking, serverTick = serverTick, attackTicks = k_AttackAnimTicks,
|
||||
};
|
||||
Dependency = localJob.ScheduleParallel(Dependency);
|
||||
|
||||
@@ -70,6 +84,7 @@ namespace ProjectM.Client
|
||||
var remoteJob = new RemoteDriveJob
|
||||
{
|
||||
moveX = k_MoveX, moveZ = k_MoveZ, speed = k_Speed, isDead = k_IsDead,
|
||||
isAttacking = k_IsAttacking, serverTick = serverTick, attackTicks = k_AttackAnimTicks,
|
||||
dt = dt,
|
||||
prevPos = _prevPos,
|
||||
seen = seen,
|
||||
@@ -88,6 +103,16 @@ namespace ProjectM.Client
|
||||
if (!seen.Contains(keys[i])) _prevPos.Remove(keys[i]);
|
||||
}
|
||||
|
||||
// True while now is within [SwingStartTick, SwingStartTick + animTicks) -- a per-swing pulse that re-triggers
|
||||
// on each chained swing. NetworkTick arithmetic (wrap-safe). Presentation-only, Burst-safe.
|
||||
static bool SwingActive(in MeleeCombo mc, NetworkTick serverTick, uint animTicks)
|
||||
{
|
||||
if (mc.SwingStartTick == 0u || !serverTick.IsValid) return false;
|
||||
var start = new NetworkTick(mc.SwingStartTick);
|
||||
var end = new NetworkTick(TickUtil.NonZero(mc.SwingStartTick + animTicks));
|
||||
return start.IsValid && end.IsValid && !start.IsNewerThan(serverTick) && end.IsNewerThan(serverTick);
|
||||
}
|
||||
|
||||
// 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).
|
||||
@@ -96,7 +121,9 @@ namespace ProjectM.Client
|
||||
[WithPresent(typeof(Dead))]
|
||||
partial struct LocalDriveJob : IJobEntity
|
||||
{
|
||||
public FastAnimatorParameter moveX, moveZ, speed, isDead;
|
||||
public FastAnimatorParameter moveX, moveZ, speed, isDead, isAttacking;
|
||||
public NetworkTick serverTick;
|
||||
public uint attackTicks;
|
||||
|
||||
void Execute(
|
||||
AnimatorControllerParameterIndexTableComponent indexTable,
|
||||
@@ -104,11 +131,13 @@ namespace ProjectM.Client
|
||||
in PlayerFacing facing,
|
||||
in EffectiveCharacterStats stats,
|
||||
in KinematicCharacterBody body,
|
||||
in MeleeCombo melee,
|
||||
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);
|
||||
if (a.HasParameter(isAttacking)) a.SetParameterValue(isAttacking, SwingActive(melee, serverTick, attackTicks));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +148,9 @@ namespace ProjectM.Client
|
||||
[WithPresent(typeof(Dead))]
|
||||
partial struct RemoteDriveJob : IJobEntity
|
||||
{
|
||||
public FastAnimatorParameter moveX, moveZ, speed, isDead;
|
||||
public FastAnimatorParameter moveX, moveZ, speed, isDead, isAttacking;
|
||||
public NetworkTick serverTick;
|
||||
public uint attackTicks;
|
||||
public float dt;
|
||||
public NativeParallelHashMap<Entity, float3> prevPos;
|
||||
public NativeParallelHashSet<Entity> seen;
|
||||
@@ -131,6 +162,7 @@ namespace ProjectM.Client
|
||||
in LocalTransform xform,
|
||||
in PlayerFacing facing,
|
||||
in EffectiveCharacterStats stats,
|
||||
in MeleeCombo melee,
|
||||
EnabledRefRO<Dead> dead)
|
||||
{
|
||||
seen.Add(e);
|
||||
@@ -143,6 +175,7 @@ namespace ProjectM.Client
|
||||
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);
|
||||
if (a.HasParameter(isAttacking)) a.SetParameterValue(isAttacking, SwingActive(melee, serverTick, attackTicks));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user