Combat feel pass: enemy hit-flash, melee connect cue, kill pop, gamepad rumble, footsteps, damage-number tiering
Implements the "what's missing" feel backlog from the combat overhaul investigation (wf_c6c87dc5-9c3). All client-only, observe-only (PresentationSystemGroup), no sim/netcode change, rollback-safe; new FeelConfig knobs default-stamped on play-enter. - ENEMY HIT-FLASH (#1 missing): on the enemy Health-decrease edge, a bright body-scaled particle puff in the previously-unused FeelConfig.HitFlashColor (via a new EmitColored helper using per-emit startColor) -- the staple "I connected" read. (A true material body-flash needs an AnimatedLitShader emission property + Entities Graphics MaterialProperty; the puff is the asset-free version.) - MELEE CONNECT-vs-WHIFF: on the local swing, a client-side MeleeConeMath.InCone overlap over the cached enemy snapshot -> immediate connect read (brightens the slash arc, hit-spark at the nearest enemy, a meaty low "thunk" SFX, extra FOV punch) before the authoritative server spark arrives. Whiffs stay dim. - KILL POP: a colored flash burst + rumble on enemy death (on top of the existing burst/shake/FOV). - GAMEPAD RUMBLE: new RumbleUtil (auto-stopping pulse, gamepad-only, stops on focus-loss, reset on play-enter) pulsed on local hit-taken / hit-dealt / kill, gated on AimPresentation.Scheme==Gamepad. - FOOTSTEPS: edge-detect local locomotion from the position delta -> soft step SFX at a cadence while moving. - DAMAGE-NUMBER TIERING: SpawnNumber scales font + life by hit magnitude (vs HitStopRefDamage) so heavy hits read. 390/390 EditMode, clean compile + Play (no exceptions; per-frame footstep/rumble-tick paths verified). On-screen feel is the operator's eyes. Deferred (focused follow-ups): true material body hit-flash (ShaderGraph), co-op remote-swing rendering, near-impact telegraph beep (clip reserved). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -86,6 +86,8 @@ namespace ProjectM.Client
|
|||||||
AudioClip _telegraphClip;
|
AudioClip _telegraphClip;
|
||||||
AudioClip _dashClip;
|
AudioClip _dashClip;
|
||||||
AudioClip _swingClip;
|
AudioClip _swingClip;
|
||||||
|
AudioClip _meleeConnectClip, _footstepClip, _strikeBeepClip; // combat feel pass: connect thunk / footstep / strike beep
|
||||||
|
Vector3 _lastFootPos; float _footTimer; bool _footInit; // footstep edge-detect (local player locomotion)
|
||||||
|
|
||||||
Entity _localPlayer = Entity.Null;
|
Entity _localPlayer = Entity.Null;
|
||||||
uint _lastLocalFireTick;
|
uint _lastLocalFireTick;
|
||||||
@@ -108,6 +110,9 @@ namespace ProjectM.Client
|
|||||||
_telegraphClip = MakeClip("telegraph", 680f, 1020f, 0.12f, 0.35f, noise: false);
|
_telegraphClip = MakeClip("telegraph", 680f, 1020f, 0.12f, 0.35f, noise: false);
|
||||||
_dashClip = MakeClip("dash", 950f, 240f, 0.12f, 0.50f, noise: false);
|
_dashClip = MakeClip("dash", 950f, 240f, 0.12f, 0.50f, noise: false);
|
||||||
_swingClip = MakeClip("swing", 720f, 200f, 0.09f, 0.42f, noise: false);
|
_swingClip = MakeClip("swing", 720f, 200f, 0.09f, 0.42f, noise: false);
|
||||||
|
_meleeConnectClip = MakeClip("melee_thunk", 180f, 60f, 0.13f, 0.55f, noise: true); // meaty low connect
|
||||||
|
_footstepClip = MakeClip("step", 200f, 110f, 0.06f, 0.18f, noise: true); // soft footfall
|
||||||
|
_strikeBeepClip = MakeClip("strike", 1150f, 1500f, 0.05f, 0.30f, noise: false); // (reserved) near-impact beep
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnStartRunning()
|
protected override void OnStartRunning()
|
||||||
@@ -219,6 +224,8 @@ namespace ProjectM.Client
|
|||||||
PlayClip(_hitClip, (Vector3)p, FeelConfig.HitSfxVolume);
|
PlayClip(_hitClip, (Vector3)p, FeelConfig.HitSfxVolume);
|
||||||
PrototypeCameraRig.AddShake(isLocalPlayer ? FeelConfig.HitShakeLocal : FeelConfig.HitShakeRemote);
|
PrototypeCameraRig.AddShake(isLocalPlayer ? FeelConfig.HitShakeLocal : FeelConfig.HitShakeRemote);
|
||||||
if (isLocalPlayer) PrototypeCameraRig.PunchFov(FeelConfig.HitStopFovKick, FeelConfig.HitStopDurationMs);
|
if (isLocalPlayer) PrototypeCameraRig.PunchFov(FeelConfig.HitStopFovKick, FeelConfig.HitStopDurationMs);
|
||||||
|
if (isLocalPlayer && FeelConfig.RumbleEnabled && AimPresentation.Scheme == 1)
|
||||||
|
RumbleUtil.Pulse(FeelConfig.RumbleHit * 0.8f, FeelConfig.RumbleHit, FeelConfig.RumbleDurationSec);
|
||||||
if (isEnemy)
|
if (isEnemy)
|
||||||
{
|
{
|
||||||
// MC-3: net-new player-dealt-hit camera punch — scales with the bite size
|
// MC-3: net-new player-dealt-hit camera punch — scales with the bite size
|
||||||
@@ -227,6 +234,10 @@ namespace ProjectM.Client
|
|||||||
float hitMag = math.saturate((prev.Hp - cur) / math.max(1f, FeelConfig.HitStopRefDamage));
|
float hitMag = math.saturate((prev.Hp - cur) / math.max(1f, FeelConfig.HitStopRefDamage));
|
||||||
PrototypeCameraRig.PunchFov(math.lerp(FeelConfig.HitStopFovKickMin, FeelConfig.HitStopFovKickMax, hitMag), FeelConfig.HitStopDurationMs);
|
PrototypeCameraRig.PunchFov(math.lerp(FeelConfig.HitStopFovKickMin, FeelConfig.HitStopFovKickMax, hitMag), FeelConfig.HitStopDurationMs);
|
||||||
ShowHealthBar(entity); // Feature B: arm/refresh this enemy's bar on a damage edge
|
ShowHealthBar(entity); // Feature B: arm/refresh this enemy's bar on a damage edge
|
||||||
|
// Hit-flash: a bright body-scaled puff in FeelConfig.HitFlashColor — the staple "I lit it up" read.
|
||||||
|
EmitColored(_hitFx, (Vector3)p + Vector3.up * 0.7f, FeelConfig.HitFlashBurstCount, FeelConfig.HitFlashColor);
|
||||||
|
if (FeelConfig.RumbleEnabled && AimPresentation.Scheme == 1)
|
||||||
|
RumbleUtil.Pulse(FeelConfig.RumbleHit * 0.6f, FeelConfig.RumbleHit, FeelConfig.RumbleDurationSec);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,6 +279,9 @@ namespace ProjectM.Client
|
|||||||
PlayClip(_deathClip, (Vector3)c.Pos, FeelConfig.KillSfxVolume);
|
PlayClip(_deathClip, (Vector3)c.Pos, FeelConfig.KillSfxVolume);
|
||||||
PrototypeCameraRig.AddShake(FeelConfig.KillShake);
|
PrototypeCameraRig.AddShake(FeelConfig.KillShake);
|
||||||
PrototypeCameraRig.PunchFov(FeelConfig.KillFovKick, FeelConfig.HitStopDurationMs);
|
PrototypeCameraRig.PunchFov(FeelConfig.KillFovKick, FeelConfig.HitStopDurationMs);
|
||||||
|
EmitColored(_hitFx, (Vector3)c.Pos + Vector3.up * 0.6f, FeelConfig.KillFlashBurstCount, FeelConfig.HitFlashColor); // kill pop
|
||||||
|
if (FeelConfig.RumbleEnabled && AimPresentation.Scheme == 1)
|
||||||
|
RumbleUtil.Pulse(FeelConfig.RumbleKill * 0.7f, FeelConfig.RumbleKill, FeelConfig.RumbleDurationSec);
|
||||||
}
|
}
|
||||||
_cache.Remove(_stale[i]);
|
_cache.Remove(_stale[i]);
|
||||||
}
|
}
|
||||||
@@ -331,13 +345,52 @@ namespace ProjectM.Client
|
|||||||
float slashRange = tcfg.MeleeRange > 0f ? tcfg.MeleeRange : 2.6f;
|
float slashRange = tcfg.MeleeRange > 0f ? tcfg.MeleeRange : 2.6f;
|
||||||
float slashHalf = tcfg.MeleeConeHalfAngleRad > 0f ? tcfg.MeleeConeHalfAngleRad : 0.9f;
|
float slashHalf = tcfg.MeleeConeHalfAngleRad > 0f ? tcfg.MeleeConeHalfAngleRad : 0.9f;
|
||||||
if (finisher) slashRange *= tcfg.MeleeFinisherMult > 0f ? tcfg.MeleeFinisherMult : 1.8f;
|
if (finisher) slashRange *= tcfg.MeleeFinisherMult > 0f ? tcfg.MeleeFinisherMult : 1.8f;
|
||||||
TriggerSlash((Vector3)localPos, new float2(face.x, face.z), slashRange, slashHalf, step, comboLen); // arc sweeps + ramps per step (MC-4 clarity)
|
// MC-4 connect-vs-whiff: client-side cone overlap over the cached enemy snapshot gives an IMMEDIATE
|
||||||
|
// "you bit" read (the authoritative server damage spark/number arrives a few ticks later).
|
||||||
|
bool connected = false; Vector3 nearestHit = (Vector3)localPos; float ndist = float.MaxValue;
|
||||||
|
float cosHalf = Mathf.Cos(slashHalf);
|
||||||
|
float2 fdir = new float2(face.x, face.z);
|
||||||
|
foreach (var kv in _cache)
|
||||||
|
{
|
||||||
|
if (!kv.Value.IsEnemy) continue;
|
||||||
|
if (MeleeConeMath.InCone(localPos, fdir, slashRange, cosHalf, kv.Value.Pos))
|
||||||
|
{
|
||||||
|
float d2 = math.distancesq(localPos, kv.Value.Pos);
|
||||||
|
if (d2 < ndist) { ndist = d2; nearestHit = (Vector3)kv.Value.Pos; connected = true; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TriggerSlash((Vector3)localPos, new float2(face.x, face.z), slashRange, slashHalf, step, comboLen, connected); // sweeps + ramps + brightens on connect
|
||||||
|
if (connected)
|
||||||
|
{
|
||||||
|
Burst(_hitFx, cfg != null ? cfg.Hit : null, nearestHit + Vector3.up * 0.7f, FeelConfig.HitBurstCount);
|
||||||
|
PlayClip(_meleeConnectClip, nearestHit, FeelConfig.MeleeConnectVolume);
|
||||||
|
PrototypeCameraRig.PunchFov(FeelConfig.MeleeConnectFovKick, FeelConfig.HitStopDurationMs);
|
||||||
|
if (FeelConfig.RumbleEnabled && AimPresentation.Scheme == 1)
|
||||||
|
RumbleUtil.Pulse(FeelConfig.RumbleHit * 0.6f, FeelConfig.RumbleHit, FeelConfig.RumbleDurationSec);
|
||||||
|
}
|
||||||
if (finisher) PrototypeCameraRig.PunchFov(FeelConfig.DashFovKick * 0.6f, FeelConfig.HitStopDurationMs);
|
if (finisher) PrototypeCameraRig.PunchFov(FeelConfig.DashFovKick * 0.6f, FeelConfig.HitStopDurationMs);
|
||||||
}
|
}
|
||||||
_lastLocalSwingTick = mc.SwingStartTick;
|
_lastLocalSwingTick = mc.SwingStartTick;
|
||||||
_swingTickInit = true;
|
_swingTickInit = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Footsteps (combat feel): edge-detect local locomotion from the position delta; a soft step at a cadence.
|
||||||
|
if (_localPlayer != Entity.Null)
|
||||||
|
{
|
||||||
|
Vector3 lp = (Vector3)localPos;
|
||||||
|
if (_footInit)
|
||||||
|
{
|
||||||
|
float sp = dt > 1e-4f ? new Vector2(lp.x - _lastFootPos.x, lp.z - _lastFootPos.z).magnitude / dt : 0f;
|
||||||
|
_footTimer -= dt;
|
||||||
|
if (sp >= FeelConfig.FootstepMinSpeed && _footTimer <= 0f)
|
||||||
|
{
|
||||||
|
PlayClip(_footstepClip, lp, FeelConfig.FootstepVolume);
|
||||||
|
_footTimer = FeelConfig.FootstepIntervalSec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_lastFootPos = lp; _footInit = true;
|
||||||
|
}
|
||||||
|
RumbleUtil.Tick(); // auto-stop any elapsed gamepad rumble pulse
|
||||||
UpdateProjectileTrails(cfg);
|
UpdateProjectileTrails(cfg);
|
||||||
PruneVfx();
|
PruneVfx();
|
||||||
AnimateNumbers(dt, cam);
|
AnimateNumbers(dt, cam);
|
||||||
@@ -354,6 +407,15 @@ namespace ProjectM.Client
|
|||||||
return cfg.PlayerDeath != null ? cfg.PlayerDeath : cfg.EnemyDeath;
|
return cfg.PlayerDeath != null ? cfg.PlayerDeath : cfg.EnemyDeath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Emit a colored particle burst at a position (per-emit startColor) — used for the enemy hit-flash + kill pop
|
||||||
|
// without a dedicated particle system (the unused FeelConfig.HitFlashColor finally lights enemies on a hit).
|
||||||
|
void EmitColored(ParticleSystem ps, Vector3 pos, int count, Color color)
|
||||||
|
{
|
||||||
|
if (ps == null || count <= 0) return;
|
||||||
|
var ep = new ParticleSystem.EmitParams { position = pos, startColor = color };
|
||||||
|
ps.Emit(ep, count);
|
||||||
|
}
|
||||||
|
|
||||||
void Burst(ParticleSystem fallback, GameObject prefab, Vector3 pos, int count)
|
void Burst(ParticleSystem fallback, GameObject prefab, Vector3 pos, int count)
|
||||||
{
|
{
|
||||||
if (prefab != null) SpawnVfx(prefab, pos, Quaternion.identity);
|
if (prefab != null) SpawnVfx(prefab, pos, Quaternion.identity);
|
||||||
@@ -497,12 +559,14 @@ namespace ProjectM.Client
|
|||||||
|
|
||||||
fn.Active = true;
|
fn.Active = true;
|
||||||
fn.Age = 0f;
|
fn.Age = 0f;
|
||||||
fn.Life = 0.7f;
|
float mag = Mathf.Clamp01(amount / Mathf.Max(1f, FeelConfig.HitStopRefDamage)); // big hits read bigger
|
||||||
|
fn.Life = Mathf.Lerp(0.6f, 0.95f, mag);
|
||||||
fn.Tm.text = Mathf.Max(1, Mathf.RoundToInt(amount)).ToString();
|
fn.Tm.text = Mathf.Max(1, Mathf.RoundToInt(amount)).ToString();
|
||||||
fn.BaseColor = isLocalPlayer ? new Color(1f, 0.5f, 0.22f) : new Color(0.45f, 0.92f, 1f); // Blight orange (hurt) / Aether cyan (you hit)
|
fn.BaseColor = isLocalPlayer ? new Color(1f, 0.5f, 0.22f) : new Color(0.45f, 0.92f, 1f); // Blight orange (hurt) / Aether cyan (you hit)
|
||||||
fn.Tm.color = fn.BaseColor;
|
fn.Tm.color = fn.BaseColor;
|
||||||
fn.Tr.position = worldPos + Vector3.up * 1.4f + new Vector3(UnityEngine.Random.Range(-0.25f, 0.25f), 0f, 0f);
|
fn.Tr.position = worldPos + Vector3.up * 1.4f + new Vector3(UnityEngine.Random.Range(-0.25f, 0.25f), 0f, 0f);
|
||||||
fn.Vel = new Vector3(0f, 2.2f, 0f);
|
fn.Vel = new Vector3(0f, 2.2f, 0f);
|
||||||
|
fn.Tr.localScale = Vector3.one * Mathf.Lerp(0.85f, 1.5f, mag);
|
||||||
fn.Tr.gameObject.SetActive(true);
|
fn.Tr.gameObject.SetActive(true);
|
||||||
if (cam != null) fn.Tr.rotation = cam.transform.rotation;
|
if (cam != null) fn.Tr.rotation = cam.transform.rotation;
|
||||||
}
|
}
|
||||||
@@ -668,7 +732,7 @@ namespace ProjectM.Client
|
|||||||
// Trigger a cone-shaped slash matching the LIVE melee range + half-angle, oriented along facing. The arc IS
|
// Trigger a cone-shaped slash matching the LIVE melee range + half-angle, oriented along facing. The arc IS
|
||||||
// the range telegraph (MC-4 clarity) AND now SWEEPS across + ramps per combo step so the swing reads as a
|
// the range telegraph (MC-4 clarity) AND now SWEEPS across + ramps per combo step so the swing reads as a
|
||||||
// directional, escalating cleave rather than a static flash.
|
// directional, escalating cleave rather than a static flash.
|
||||||
void TriggerSlash(Vector3 pos, float2 facing, float range, float halfAngle, int step, int comboLen)
|
void TriggerSlash(Vector3 pos, float2 facing, float range, float halfAngle, int step, int comboLen, bool connected)
|
||||||
{
|
{
|
||||||
if (_slashMr == null || _slashMat == null) return;
|
if (_slashMr == null || _slashMat == null) return;
|
||||||
bool finisher = step >= comboLen;
|
bool finisher = step >= comboLen;
|
||||||
@@ -685,6 +749,7 @@ namespace ProjectM.Client
|
|||||||
_slashTint = finisher
|
_slashTint = finisher
|
||||||
? new Color(3.4f, 2.4f, 0.7f) // finisher: warm HDR flash
|
? new Color(3.4f, 2.4f, 0.7f) // finisher: warm HDR flash
|
||||||
: Color.Lerp(new Color(1.2f, 1.9f, 2.6f), new Color(1.9f, 2.8f, 3.4f), t); // cool, brighter per step
|
: Color.Lerp(new Color(1.2f, 1.9f, 2.6f), new Color(1.9f, 2.8f, 3.4f), t); // cool, brighter per step
|
||||||
|
if (connected) _slashTint *= 1.6f; // brighter arc on a confirmed bite (the immediate "you hit" read)
|
||||||
_slashLife = finisher ? 0.28f : Mathf.Lerp(0.15f, 0.20f, t);
|
_slashLife = finisher ? 0.28f : Mathf.Lerp(0.15f, 0.20f, t);
|
||||||
_slashAge = 0f;
|
_slashAge = 0f;
|
||||||
_slashActive = true;
|
_slashActive = true;
|
||||||
|
|||||||
@@ -109,6 +109,32 @@ namespace ProjectM.Client
|
|||||||
/// Health flicker on a clean dodge — the documented acceptable-not-a-bug interaction).</summary>
|
/// Health flicker on a clean dodge — the documented acceptable-not-a-bug interaction).</summary>
|
||||||
public static bool DashHitSuppress;
|
public static bool DashHitSuppress;
|
||||||
|
|
||||||
|
// ---- Combat feel pass (2026-06): connect cue, hit-flash, kill pop, footsteps, rumble, telegraph beep ----
|
||||||
|
/// <summary>Hit-flash puff density (a colored particle burst in HitFlashColor on an enemy damage edge).</summary>
|
||||||
|
public static int HitFlashBurstCount;
|
||||||
|
/// <summary>Extra FOV punch (deg) when the LOCAL melee cleave is confirmed to connect (vs a whiff).</summary>
|
||||||
|
public static float MeleeConnectFovKick;
|
||||||
|
/// <summary>Volume of the meaty melee connect "thunk".</summary>
|
||||||
|
public static float MeleeConnectVolume;
|
||||||
|
/// <summary>Kill-pop colored flash density on an enemy death.</summary>
|
||||||
|
public static int KillFlashBurstCount;
|
||||||
|
/// <summary>Soft footstep SFX volume.</summary>
|
||||||
|
public static float FootstepVolume;
|
||||||
|
/// <summary>Seconds between footsteps while the local player is moving.</summary>
|
||||||
|
public static float FootstepIntervalSec;
|
||||||
|
/// <summary>Local player speed (u/s) above which footsteps play.</summary>
|
||||||
|
public static float FootstepMinSpeed;
|
||||||
|
/// <summary>Master gate for gamepad rumble (no-op on KBM).</summary>
|
||||||
|
public static bool RumbleEnabled;
|
||||||
|
/// <summary>Rumble strength on a local hit taken / dealt.</summary>
|
||||||
|
public static float RumbleHit;
|
||||||
|
/// <summary>Rumble strength on a kill.</summary>
|
||||||
|
public static float RumbleKill;
|
||||||
|
/// <summary>Rumble strength on dash / local death (the heaviest).</summary>
|
||||||
|
public static float RumbleHeavy;
|
||||||
|
/// <summary>Seconds a rumble pulse lasts before it auto-stops.</summary>
|
||||||
|
public static float RumbleDurationSec;
|
||||||
|
|
||||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||||
public static void ResetDefaults()
|
public static void ResetDefaults()
|
||||||
{
|
{
|
||||||
@@ -161,6 +187,20 @@ namespace ProjectM.Client
|
|||||||
DashSfxVolume = 0.55f;
|
DashSfxVolume = 0.55f;
|
||||||
DashShimmerPerFrame = 2;
|
DashShimmerPerFrame = 2;
|
||||||
DashHitSuppress = true;
|
DashHitSuppress = true;
|
||||||
|
|
||||||
|
// Combat feel pass (2026-06)
|
||||||
|
HitFlashBurstCount = 16;
|
||||||
|
MeleeConnectFovKick = 0.8f;
|
||||||
|
MeleeConnectVolume = 0.55f;
|
||||||
|
KillFlashBurstCount = 20;
|
||||||
|
FootstepVolume = 0.16f;
|
||||||
|
FootstepIntervalSec = 0.32f;
|
||||||
|
FootstepMinSpeed = 1.5f;
|
||||||
|
RumbleEnabled = true;
|
||||||
|
RumbleHit = 0.25f;
|
||||||
|
RumbleKill = 0.45f;
|
||||||
|
RumbleHeavy = 0.6f;
|
||||||
|
RumbleDurationSec = 0.12f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.InputSystem;
|
||||||
|
|
||||||
|
namespace ProjectM.Client
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gamepad rumble for combat feel — a static bridge (mirrors <see cref="FeelConfig"/> / <see cref="AimPresentation"/>).
|
||||||
|
/// <see cref="Pulse"/> sets the motors and stamps a stop time; <see cref="Tick"/> (called once per frame from
|
||||||
|
/// <c>CombatFeedbackSystem</c>) stops them when the pulse elapses OR the app loses focus, so a rumble never sticks.
|
||||||
|
/// A no-op when no pad is connected; the CALLER gates to the Gamepad scheme. Statics survive fast-enter-playmode
|
||||||
|
/// reloads, so <see cref="ResetState"/> re-arms clean on play-enter and stops any leaked motor (the AimPresentation
|
||||||
|
/// reset idiom). Presentation-only, main-thread, never touches the simulation.
|
||||||
|
/// </summary>
|
||||||
|
public static class RumbleUtil
|
||||||
|
{
|
||||||
|
static float s_StopTime;
|
||||||
|
static bool s_Active;
|
||||||
|
|
||||||
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||||
|
static void ResetState()
|
||||||
|
{
|
||||||
|
s_Active = false;
|
||||||
|
s_StopTime = 0f;
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Pulse both motors at the given low/high strengths for durSec, then auto-stop. No-op without a pad.</summary>
|
||||||
|
public static void Pulse(float low, float high, float durSec)
|
||||||
|
{
|
||||||
|
var pad = Gamepad.current;
|
||||||
|
if (pad == null) return;
|
||||||
|
pad.SetMotorSpeeds(Mathf.Clamp01(low), Mathf.Clamp01(high));
|
||||||
|
s_StopTime = Time.unscaledTime + Mathf.Max(0.02f, durSec);
|
||||||
|
s_Active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Call once per frame: stops the motors when the pulse elapses or focus is lost.</summary>
|
||||||
|
public static void Tick()
|
||||||
|
{
|
||||||
|
if (!s_Active) return;
|
||||||
|
if (!Application.isFocused || Time.unscaledTime >= s_StopTime)
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
s_Active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Stop()
|
||||||
|
{
|
||||||
|
var pad = Gamepad.current;
|
||||||
|
if (pad != null) pad.ResetHaptics();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2f7005e66cfc2ce4d825ebad3cdc9eac
|
||||||
Reference in New Issue
Block a user