Tuning Knobs
This commit is contained in:
@@ -60,5 +60,8 @@ namespace ProjectM.Simulation
|
||||
|
||||
/// <summary>Set ThreatState.Heat to ArgA (inert until the Heat source ships).</summary>
|
||||
public const byte SetHeat = 11;
|
||||
|
||||
/// <summary>Set the <see cref="TuningKnob"/> ArgA to ArgB/1000f (live dash/Charger feel-tuning; MC-0).</summary>
|
||||
public const byte SetTuning = 12;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// MC-0 — live-tunable dash/Charger feel knobs (a per-world singleton). UNCONDITIONAL type (no #if) so the
|
||||
/// reflection-built RpcCollection hash matches across release/dev peers — only the dev SYSTEMS that create,
|
||||
/// mutate, broadcast and receive it are <c>#if UNITY_EDITOR</c>. Consumers (DashSystem, EnemyAISystem) read it
|
||||
/// via <c>TryGetSingleton</c> and FALL BACK to <see cref="Defaults"/> when it is absent (release builds +
|
||||
/// EditMode worlds), so behaviour is identical to the old baked consts when no dev singleton exists. NOT a
|
||||
/// <c>[GhostField]</c> (no ghost-hash change / re-bake); the server broadcasts it to clients via
|
||||
/// <see cref="DebugTuningReport"/> so a PREDICTING client's DashSystem stays in sync (an MPPM thin client with no
|
||||
/// overlay learns tuned values ONLY through this broadcast).
|
||||
/// <para>
|
||||
/// Values match the historical baked consts (DashSystem / EnemyAISystem / <see cref="Tuning.AttackWindupTicks"/>);
|
||||
/// <c>TuningConfigTests</c> pins <see cref="Defaults"/> to them so the promote-to-singleton refactor is
|
||||
/// behaviour-preserving. Tick knobs clamp to >= 1 and value knobs to >= 0 (<see cref="ClampKnob"/>) on EVERY
|
||||
/// write path (<see cref="Apply"/> + the client-side optimistic SetLocal), and DashSystem additionally clamps the
|
||||
/// i-frame window AT the divide site — a 0 there NaNs the kinematic body permanently (MC-0 design-review F1).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public struct TuningConfig : IComponentData
|
||||
{
|
||||
public float DashDistance;
|
||||
public float IFrameWindowTicks;
|
||||
public float RecoverTailTicks;
|
||||
public float DashCooldownTicks;
|
||||
public float DashSharpness;
|
||||
public float ChargerWindupTicks;
|
||||
public float ChargerLungeSpeed;
|
||||
public float ChargerLungeDurationTicks;
|
||||
public float ChargerWhiffStaggerTicks;
|
||||
public float GruntWindupTicks;
|
||||
|
||||
/// <summary>The baked feel defaults == the pre-MC-0 consts. Single source of truth for the fallback path.</summary>
|
||||
public static TuningConfig Defaults() => new TuningConfig
|
||||
{
|
||||
DashDistance = 4.0f,
|
||||
IFrameWindowTicks = 12f,
|
||||
RecoverTailTicks = 9f,
|
||||
DashCooldownTicks = 45f,
|
||||
DashSharpness = 200f,
|
||||
ChargerWindupTicks = 30f,
|
||||
ChargerLungeSpeed = 16f,
|
||||
ChargerLungeDurationTicks = 18f,
|
||||
ChargerWhiffStaggerTicks = 36f,
|
||||
GruntWindupTicks = Tuning.AttackWindupTicks, // canonical Grunt-windup source (TelegraphTests couples to it)
|
||||
};
|
||||
|
||||
/// <summary>Clamp a knob to its safe floor: tick knobs >= 1, value knobs >= 0. Used by every write path
|
||||
/// (<see cref="Apply"/> + the client-side optimistic SetLocal) so no 0/negative ever reaches a consumer.</summary>
|
||||
public static float ClampKnob(byte knob, float value)
|
||||
{
|
||||
switch (knob)
|
||||
{
|
||||
// value knobs: non-negative
|
||||
case TuningKnob.DashDistance:
|
||||
case TuningKnob.DashSharpness:
|
||||
case TuningKnob.ChargerLungeSpeed:
|
||||
return math.max(0f, value);
|
||||
// tick knobs: >= 1 (a 0 tick count is degenerate; a 0 i-frame window divides-by-zero in DashSystem)
|
||||
default:
|
||||
return math.max(1f, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Authoritatively set one knob (clamped) by its <see cref="TuningKnob"/> index. Unknown index = no-op.</summary>
|
||||
public static void Apply(ref TuningConfig c, byte knob, float value)
|
||||
{
|
||||
value = ClampKnob(knob, value);
|
||||
switch (knob)
|
||||
{
|
||||
case TuningKnob.DashDistance: c.DashDistance = value; break;
|
||||
case TuningKnob.IFrameWindowTicks: c.IFrameWindowTicks = value; break;
|
||||
case TuningKnob.RecoverTailTicks: c.RecoverTailTicks = value; break;
|
||||
case TuningKnob.DashCooldownTicks: c.DashCooldownTicks = value; break;
|
||||
case TuningKnob.DashSharpness: c.DashSharpness = value; break;
|
||||
case TuningKnob.ChargerWindupTicks: c.ChargerWindupTicks = value; break;
|
||||
case TuningKnob.ChargerLungeSpeed: c.ChargerLungeSpeed = value; break;
|
||||
case TuningKnob.ChargerLungeDurationTicks: c.ChargerLungeDurationTicks = value; break;
|
||||
case TuningKnob.ChargerWhiffStaggerTicks: c.ChargerWhiffStaggerTicks = value; break;
|
||||
case TuningKnob.GruntWindupTicks: c.GruntWindupTicks = value; break;
|
||||
// unknown index -> no-op (matches the no-default switch convention in DebugCommandReceiveSystem)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Read one knob by its <see cref="TuningKnob"/> index (overlay display). Unknown index = 0.</summary>
|
||||
public static float Get(in TuningConfig c, byte knob)
|
||||
{
|
||||
switch (knob)
|
||||
{
|
||||
case TuningKnob.DashDistance: return c.DashDistance;
|
||||
case TuningKnob.IFrameWindowTicks: return c.IFrameWindowTicks;
|
||||
case TuningKnob.RecoverTailTicks: return c.RecoverTailTicks;
|
||||
case TuningKnob.DashCooldownTicks: return c.DashCooldownTicks;
|
||||
case TuningKnob.DashSharpness: return c.DashSharpness;
|
||||
case TuningKnob.ChargerWindupTicks: return c.ChargerWindupTicks;
|
||||
case TuningKnob.ChargerLungeSpeed: return c.ChargerLungeSpeed;
|
||||
case TuningKnob.ChargerLungeDurationTicks: return c.ChargerLungeDurationTicks;
|
||||
case TuningKnob.ChargerWhiffStaggerTicks: return c.ChargerWhiffStaggerTicks;
|
||||
case TuningKnob.GruntWindupTicks: return c.GruntWindupTicks;
|
||||
default: return 0f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Project the full config onto the wire snapshot.</summary>
|
||||
public static DebugTuningReport ToReport(in TuningConfig c) => new DebugTuningReport
|
||||
{
|
||||
DashDistance = c.DashDistance,
|
||||
IFrameWindowTicks = c.IFrameWindowTicks,
|
||||
RecoverTailTicks = c.RecoverTailTicks,
|
||||
DashCooldownTicks = c.DashCooldownTicks,
|
||||
DashSharpness = c.DashSharpness,
|
||||
ChargerWindupTicks = c.ChargerWindupTicks,
|
||||
ChargerLungeSpeed = c.ChargerLungeSpeed,
|
||||
ChargerLungeDurationTicks = c.ChargerLungeDurationTicks,
|
||||
ChargerWhiffStaggerTicks = c.ChargerWhiffStaggerTicks,
|
||||
GruntWindupTicks = c.GruntWindupTicks,
|
||||
};
|
||||
|
||||
/// <summary>Reconstruct the full config from a wire snapshot (FULL state, not a delta).</summary>
|
||||
public static TuningConfig FromReport(in DebugTuningReport r) => new TuningConfig
|
||||
{
|
||||
DashDistance = r.DashDistance,
|
||||
IFrameWindowTicks = r.IFrameWindowTicks,
|
||||
RecoverTailTicks = r.RecoverTailTicks,
|
||||
DashCooldownTicks = r.DashCooldownTicks,
|
||||
DashSharpness = r.DashSharpness,
|
||||
ChargerWindupTicks = r.ChargerWindupTicks,
|
||||
ChargerLungeSpeed = r.ChargerLungeSpeed,
|
||||
ChargerLungeDurationTicks = r.ChargerLungeDurationTicks,
|
||||
ChargerWhiffStaggerTicks = r.ChargerWhiffStaggerTicks,
|
||||
GruntWindupTicks = r.GruntWindupTicks,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Byte indices for <see cref="TuningConfig"/> knobs (bytes — never an enum on a Bursted/RPC path).</summary>
|
||||
public static class TuningKnob
|
||||
{
|
||||
public const byte DashDistance = 0;
|
||||
public const byte IFrameWindowTicks = 1;
|
||||
public const byte RecoverTailTicks = 2;
|
||||
public const byte DashCooldownTicks = 3;
|
||||
public const byte DashSharpness = 4;
|
||||
public const byte ChargerWindupTicks = 5;
|
||||
public const byte ChargerLungeSpeed = 6;
|
||||
public const byte ChargerLungeDurationTicks = 7;
|
||||
public const byte ChargerWhiffStaggerTicks = 8;
|
||||
public const byte GruntWindupTicks = 9;
|
||||
|
||||
/// <summary>Knob count (overlay iteration bound).</summary>
|
||||
public const byte Count = 10;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MC-0 — server → dev-client snapshot of the full <see cref="TuningConfig"/> (sent periodically by the
|
||||
/// editor-only broadcaster). UNCONDITIONAL wire type (like <see cref="DebugTelemetryReport"/>) for RpcCollection
|
||||
/// hash parity; only the send/receive SYSTEMS are <c>#if UNITY_EDITOR</c>. FULL state (not a delta) so a
|
||||
/// late-joining client converges in one report.
|
||||
/// </summary>
|
||||
public struct DebugTuningReport : IRpcCommand
|
||||
{
|
||||
public float DashDistance;
|
||||
public float IFrameWindowTicks;
|
||||
public float RecoverTailTicks;
|
||||
public float DashCooldownTicks;
|
||||
public float DashSharpness;
|
||||
public float ChargerWindupTicks;
|
||||
public float ChargerLungeSpeed;
|
||||
public float ChargerLungeDurationTicks;
|
||||
public float ChargerWhiffStaggerTicks;
|
||||
public float GruntWindupTicks;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 24ab752e5f2d5a8428b4580f2b2588e4
|
||||
@@ -29,12 +29,9 @@ namespace ProjectM.Simulation
|
||||
[BurstCompile]
|
||||
public partial struct DashSystem : ISystem
|
||||
{
|
||||
// Baked-first feel knobs (MC-1; promote to a live TuningConfig later). Sim runs at 60 ticks/sec.
|
||||
const float DashDistance = 4.0f; // world units covered during the i-frame window
|
||||
const uint IFrameWindowTicks = 12; // ~0.20 s of i-frames
|
||||
const uint RecoverTailTicks = 9; // ~0.15 s movement-locked tail (punishes spam)
|
||||
const uint DashCooldownTicks = 45; // ~0.75 s
|
||||
const float DashSharpness = 200f; // GroundedMovementSharpness during the dash -> blink
|
||||
// Feel knobs are LIVE-tunable via the TuningConfig singleton (MC-0): OnUpdate reads it each tick and falls
|
||||
// back to TuningConfig.Defaults() when absent (release builds / EditMode), so behaviour is identical to the
|
||||
// old baked consts. DefaultSharpness (the restore target) + SimTickRate stay compile-time (not tuned).
|
||||
const float DefaultSharpness = 15f; // CharacterComponent.GetDefault() base
|
||||
const float SimTickRate = 60f;
|
||||
|
||||
@@ -45,7 +42,12 @@ namespace ProjectM.Simulation
|
||||
return;
|
||||
var serverTick = netTime.ServerTick;
|
||||
uint now = serverTick.TickIndexForValidTick;
|
||||
float dashSpeed = DashDistance / (IFrameWindowTicks / SimTickRate);
|
||||
var t = SystemAPI.TryGetSingleton<TuningConfig>(out var tc) ? tc : TuningConfig.Defaults();
|
||||
uint iFrameTicks = (uint)math.max(1f, t.IFrameWindowTicks);
|
||||
uint recoverTicks = (uint)math.max(1f, t.RecoverTailTicks);
|
||||
uint cooldownTicks = (uint)math.max(1f, t.DashCooldownTicks);
|
||||
float dashSpeed = t.DashDistance / (iFrameTicks / SimTickRate); // iFrameTicks>=1 -> never div-by-0 (review F1)
|
||||
float dashSharpness = t.DashSharpness;
|
||||
|
||||
foreach (var (ds, cd, control, character, input, facing) in
|
||||
SystemAPI.Query<RefRW<DashState>, RefRW<DashCooldown>, RefRW<CharacterControl>,
|
||||
@@ -64,9 +66,9 @@ namespace ProjectM.Simulation
|
||||
dir = math.normalize(dir);
|
||||
ds.ValueRW.Dir = dir;
|
||||
ds.ValueRW.StartTick = TickUtil.NonZero(now);
|
||||
ds.ValueRW.IFrameUntilTick = TickUtil.NonZero(now + IFrameWindowTicks);
|
||||
ds.ValueRW.RecoverUntilTick = TickUtil.NonZero(now + IFrameWindowTicks + RecoverTailTicks);
|
||||
cd.ValueRW.NextTick = TickUtil.NonZero(now + DashCooldownTicks);
|
||||
ds.ValueRW.IFrameUntilTick = TickUtil.NonZero(now + iFrameTicks);
|
||||
ds.ValueRW.RecoverUntilTick = TickUtil.NonZero(now + iFrameTicks + recoverTicks);
|
||||
cd.ValueRW.NextTick = TickUtil.NonZero(now + cooldownTicks);
|
||||
}
|
||||
|
||||
// --- OVERRIDE (runs every predicted pass so rollback re-simulation re-applies it) ---
|
||||
@@ -84,7 +86,7 @@ namespace ProjectM.Simulation
|
||||
{
|
||||
float2 d = ds.ValueRO.Dir;
|
||||
control.ValueRW.MoveVelocity = new float3(d.x, 0f, d.y) * dashSpeed;
|
||||
character.ValueRW.GroundedMovementSharpness = DashSharpness;
|
||||
character.ValueRW.GroundedMovementSharpness = dashSharpness;
|
||||
}
|
||||
else if (recoverActive)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user