Radar ping taunt sound + per-SoundEffect spatialize opt-in
MatchManager: new Radar Ping Sound property; MsgRadarPing now goes to all clients (spawnBlips flag keeps blips hunter-only), sound plays 3D at each pinged hider. Distance-based stagger (speed of sound + 35 ms min gap) prevents phase cancellation when multiple hiders ping. SoundEffect: new spatialize toggle (per asset, applied only to 3D instances -> Steam Audio HRTF); removed unused fade field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a36f398b94
commit
0172a1e834
4 files changed
+96
-32
No files matched your search
@@ -15,8 +15,7 @@ namespace Prototype
|
||||
public AudioMixerGroup audioMixerGroup;
|
||||
public float volumeMultiplierFactor = 1;
|
||||
[SerializeField] bool loop;
|
||||
[SerializeField] Vector2 fade = new();
|
||||
|
||||
[SerializeField] bool spatialize;
|
||||
|
||||
[Header("Additional Settings")]
|
||||
[SerializeField, Range(0, 1)] float defaultVolume = .8f;
|
||||
@@ -36,8 +35,8 @@ namespace Prototype
|
||||
volumeMultiplier = Random.Range(volumeMultiplier.x, volumeMultiplier.y) * volumeMultiplierFactor,
|
||||
pitch = Random.Range(pitch.x, pitch.y),
|
||||
loop = loop,
|
||||
distanceRange = distanceRange ? distanceRange.range : null,
|
||||
fade = fade
|
||||
spatialize = spatialize,
|
||||
distanceRange = distanceRange ? distanceRange.range : null
|
||||
};
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace Prototype
|
||||
var range = options.GetDistanceRange();
|
||||
|
||||
audioSource.spatialBlend = range.x == 0 && range.y == 0 ? 0 : 1;
|
||||
audioSource.spatialize = options.GetSpatialize() && audioSource.spatialBlend > 0;
|
||||
audioSource.rolloffMode = AudioRolloffMode.Linear;
|
||||
audioSource.minDistance = range.x;
|
||||
audioSource.maxDistance = range.y;
|
||||
|
||||
@@ -20,13 +20,13 @@ namespace Prototype
|
||||
public Vector2? distanceRange;
|
||||
public Vector2 GetDistanceRange() => distanceRange ?? new(30, 200);
|
||||
|
||||
public Vector2? fade;
|
||||
public Vector2 GetFade() => fade ?? new();
|
||||
|
||||
public bool? destroyOnLoad;
|
||||
public bool ShouldDestroyOnLoad() => destroyOnLoad ?? true;
|
||||
|
||||
public float? delay;
|
||||
public float GetDelay() => delay ?? 0;
|
||||
|
||||
public bool? spatialize;
|
||||
public bool GetSpatialize() => spatialize ?? false;
|
||||
}
|
||||
}
|
||||
@@ -169,8 +169,8 @@ public class MatchManager : MonoBehaviour
|
||||
|
||||
[Header("Radar pings")]
|
||||
[Tooltip("Seconds between radar sweeps during Seek. Every interval each still-un-found hider is registered " +
|
||||
"as a blip on every HUNTER's radar (hiders and spectators never receive them). Fires only in the " +
|
||||
"Seek phase. 0 disables radar pings.")]
|
||||
"as a blip on every HUNTER's radar (hiders and spectators get no blips, only the taunt sound). " +
|
||||
"Fires only in the Seek phase. 0 disables radar pings.")]
|
||||
[Min(0f)]
|
||||
[SerializeField] private float radarPingInterval = 30f;
|
||||
|
||||
@@ -179,6 +179,12 @@ public class MatchManager : MonoBehaviour
|
||||
[Min(0f)]
|
||||
[SerializeField] private float radarPingLifetime = 3f;
|
||||
|
||||
[Tooltip("Taunt sound played at each pinged hider's world position on EVERY client (hunters, hiders and " +
|
||||
"spectators alike) whenever a radar ping fires. Multiple taunts in one sweep are staggered by " +
|
||||
"audio-listener distance so the same sample never starts twice at once (phase cancellation). " +
|
||||
"Empty = no sound.")]
|
||||
[SerializeField] private Prototype.SoundEffect radarPingSound;
|
||||
|
||||
/// <summary>Range of the hunter's shoot raycast (read by <see cref="FirstPersonMode"/>).</summary>
|
||||
public float ShootRange => shootRange;
|
||||
/// <summary>Hunter shoot cooldown in seconds (read by <see cref="FirstPersonMode"/>).</summary>
|
||||
@@ -267,8 +273,9 @@ public class MatchManager : MonoBehaviour
|
||||
// app restarts). Lets the server map the ephemeral NGO clientId to a stable identity - the reconnect
|
||||
// foundation. See PersistentPlayerId.
|
||||
private const string MsgIdentity = "Match_Identity";
|
||||
// Server -> HUNTER clients only, on each Seek radar sweep: the still-un-found hiders' NetworkObjectIds plus
|
||||
// the blip lifetime. Each hunter resolves the ids locally and drops a Radar blip pointing at the hider.
|
||||
// Server -> all clients, on each Seek radar sweep: the still-un-found hiders' NetworkObjectIds plus the blip
|
||||
// lifetime and a spawnBlips flag (true only for hunters). Each client resolves the ids locally; hunters drop
|
||||
// a Radar blip per hider, and everyone plays the ping taunt at each hider's position.
|
||||
private const string MsgRadarPing = "Match_RadarPing";
|
||||
// Pre-match LOADING barrier. Server -> all clients, once the match avatars are respawned: the target avatar
|
||||
// NetworkObjectIds + a generation tag; each client waits until it has spawned & paint-applied all of them.
|
||||
@@ -303,6 +310,8 @@ public class MatchManager : MonoBehaviour
|
||||
private readonly List<ulong> _phaseRecipients = new List<ulong>(); // reused scratch for the phase broadcast
|
||||
private readonly List<ulong> _radarPingIds = new List<ulong>(); // reused scratch: hider NetworkObjectIds for a sweep
|
||||
private readonly List<ulong> _radarRecipients = new List<ulong>(); // reused scratch: hunter client recipients for a sweep
|
||||
private readonly List<ulong> _radarSoundRecipients = new List<ulong>(); // reused scratch: non-hunter recipients (taunt only, no blips)
|
||||
private readonly List<Vector3> _radarSoundPositions = new List<Vector3>(); // reused scratch: pinged hider positions for the taunt stagger
|
||||
|
||||
// Server-only readiness barrier. _barrierTargets = the objects every client must have spawned & paint-applied
|
||||
// this round (the freshly respawned avatars pre-match, or the conversion statues at Resolution); _expectedReady
|
||||
@@ -1117,8 +1126,8 @@ public class MatchManager : MonoBehaviour
|
||||
}
|
||||
|
||||
// Server-only. One radar sweep during Seek: registers every still-un-found hider as a blip on each HUNTER's
|
||||
// radar. Sends the hiders' NetworkObjectIds + blip lifetime to the hunter clients; a host-hunter applies
|
||||
// locally (no loopback message, mirroring SetPhaseServer). Hiders and spectators never receive a ping.
|
||||
// radar and plays the ping taunt at each hider on EVERY client. A host applies locally (no loopback message,
|
||||
// mirroring SetPhaseServer).
|
||||
private void BroadcastRadarPings()
|
||||
{
|
||||
if (!_net || !_net.IsServer || _pendingHiders.Count == 0) return;
|
||||
@@ -1130,33 +1139,45 @@ public class MatchManager : MonoBehaviour
|
||||
EmitRadarPings();
|
||||
}
|
||||
|
||||
// Server-only. Sends whatever hider NetworkObjectIds are staged in _radarPingIds to every HUNTER as radar
|
||||
// blips (a host-hunter applies locally, no loopback - mirroring SetPhaseServer). Shared by the interval
|
||||
// sweep and the manual single-hider ping so both drop the identical blip (same lifetime, same path).
|
||||
// No-op if nothing is staged or there is no hunter to receive it.
|
||||
// Server-only. Sends whatever hider NetworkObjectIds are staged in _radarPingIds to every client (the host
|
||||
// applies locally, no loopback - mirroring SetPhaseServer). Hunters get radar blips + the taunt sound;
|
||||
// everyone else (hiders, spectators) only hears the taunt at the pinged positions - the spawnBlips flag in
|
||||
// the payload keeps the radar hunter-only. Shared by the interval sweep and the manual single-hider ping.
|
||||
private void EmitRadarPings()
|
||||
{
|
||||
if (_radarPingIds.Count == 0) return;
|
||||
|
||||
// Host-as-hunter: spawn on its own radar directly (it is excluded from the recipient list below).
|
||||
// Host: apply locally (excluded from the recipient lists below) - blips only as a hunter, taunt always.
|
||||
if (IsHunter(_net.LocalClientId)) SpawnRadarBlipsLocal();
|
||||
|
||||
_radarRecipients.Clear();
|
||||
foreach (ulong clientId in _net.ConnectedClientsIds)
|
||||
if (clientId != _net.LocalClientId && IsHunter(clientId)) _radarRecipients.Add(clientId);
|
||||
if (_radarRecipients.Count == 0) return;
|
||||
StagePingTauntsFromIds();
|
||||
|
||||
var cmm = _net.CustomMessagingManager;
|
||||
if (cmm == null) return;
|
||||
int size = sizeof(float) + sizeof(int) + _radarPingIds.Count * sizeof(ulong);
|
||||
using (var writer = new FastBufferWriter(size, Allocator.Temp))
|
||||
|
||||
_radarRecipients.Clear();
|
||||
_radarSoundRecipients.Clear();
|
||||
foreach (ulong clientId in _net.ConnectedClientsIds)
|
||||
{
|
||||
if (clientId == _net.LocalClientId) continue;
|
||||
(IsHunter(clientId) ? _radarRecipients : _radarSoundRecipients).Add(clientId);
|
||||
}
|
||||
SendRadarPing(cmm, _radarRecipients, spawnBlips: true);
|
||||
SendRadarPing(cmm, _radarSoundRecipients, spawnBlips: false);
|
||||
}
|
||||
|
||||
// Writes the staged _radarPingIds as one MsgRadarPing to the given recipients. spawnBlips tells the
|
||||
// receiver whether to also drop radar blips (hunters) or just play the taunt (hiders/spectators).
|
||||
private void SendRadarPing(CustomMessagingManager cmm, List<ulong> recipients, bool spawnBlips)
|
||||
{
|
||||
if (recipients.Count == 0) return;
|
||||
int size = sizeof(float) + sizeof(byte) + sizeof(int) + _radarPingIds.Count * sizeof(ulong);
|
||||
using var writer = new FastBufferWriter(size, Allocator.Temp);
|
||||
writer.WriteValueSafe(radarPingLifetime);
|
||||
writer.WriteValueSafe(spawnBlips);
|
||||
writer.WriteValueSafe(_radarPingIds.Count);
|
||||
for (int i = 0; i < _radarPingIds.Count; i++)
|
||||
writer.WriteValueSafe(_radarPingIds[i]);
|
||||
cmm.SendNamedMessage(MsgRadarPing, _radarRecipients, writer);
|
||||
}
|
||||
cmm.SendNamedMessage(MsgRadarPing, recipients, writer);
|
||||
}
|
||||
|
||||
/// <summary>Client-side: the local hider manually requests a radar ping on itself (the E key), making it
|
||||
@@ -1204,20 +1225,63 @@ public class MatchManager : MonoBehaviour
|
||||
radar.Spawn(no.transform, radarPingLifetime);
|
||||
}
|
||||
|
||||
// Hunter-client handler: a radar sweep arrived. Resolve each hider NetworkObjectId to its live transform and
|
||||
// drop a blip on the local radar. Only hunter clients are sent this message (filtered server-side).
|
||||
// Client handler: a radar sweep arrived. Resolve each hider NetworkObjectId to its live transform, drop a
|
||||
// blip on the local radar (hunters only - spawnBlips is filtered server-side) and play the ping taunt at
|
||||
// each hider's position (every client).
|
||||
private void OnRadarPingMessage(ulong senderClientId, FastBufferReader reader)
|
||||
{
|
||||
reader.ReadValueSafe(out float lifetime);
|
||||
reader.ReadValueSafe(out bool spawnBlips);
|
||||
reader.ReadValueSafe(out int count);
|
||||
var radar = Radar.Instance;
|
||||
var spawnMgr = _net ? _net.SpawnManager : null;
|
||||
_radarSoundPositions.Clear();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
reader.ReadValueSafe(out ulong netObjId);
|
||||
if (radar && spawnMgr != null &&
|
||||
spawnMgr.SpawnedObjects.TryGetValue(netObjId, out var no) && no)
|
||||
radar.Spawn(no.transform, lifetime);
|
||||
if (spawnMgr == null || !spawnMgr.SpawnedObjects.TryGetValue(netObjId, out var no) || !no) continue;
|
||||
if (spawnBlips && radar) radar.Spawn(no.transform, lifetime);
|
||||
_radarSoundPositions.Add(no.transform.position);
|
||||
}
|
||||
PlayStaggeredPingTaunts();
|
||||
}
|
||||
|
||||
// Resolves the staged _radarPingIds to world positions and plays the taunts (host/server local path -
|
||||
// mirrors what OnRadarPingMessage does for remote clients).
|
||||
private void StagePingTauntsFromIds()
|
||||
{
|
||||
if (!radarPingSound) return;
|
||||
var spawnMgr = _net ? _net.SpawnManager : null;
|
||||
if (spawnMgr == null) return;
|
||||
_radarSoundPositions.Clear();
|
||||
for (int i = 0; i < _radarPingIds.Count; i++)
|
||||
if (spawnMgr.SpawnedObjects.TryGetValue(_radarPingIds[i], out var no) && no)
|
||||
_radarSoundPositions.Add(no.transform.position);
|
||||
PlayStaggeredPingTaunts();
|
||||
}
|
||||
|
||||
// Plays the ping taunt at every staged position - staggered, never simultaneous. The same sample starting
|
||||
// at several world positions in the same audio frame phase-cancels, so each instance is delayed by its
|
||||
// distance to the local audio listener at the speed of sound (natural propagation: far players are heard
|
||||
// later), with a minimum gap between consecutive starts as the tie-break for equidistant players.
|
||||
private void PlayStaggeredPingTaunts()
|
||||
{
|
||||
const float speedOfSound = 343f; // m/s
|
||||
const float minStartGap = 0.035f; // s between consecutive starts
|
||||
|
||||
if (!radarPingSound || _radarSoundPositions.Count == 0) return;
|
||||
|
||||
var listener = FindFirstObjectByType<AudioListener>();
|
||||
Vector3 ear = listener ? listener.transform.position
|
||||
: Camera.main ? Camera.main.transform.position : Vector3.zero;
|
||||
|
||||
_radarSoundPositions.Sort((a, b) => (a - ear).sqrMagnitude.CompareTo((b - ear).sqrMagnitude));
|
||||
float previousDelay = -minStartGap;
|
||||
foreach (Vector3 pos in _radarSoundPositions)
|
||||
{
|
||||
float delay = Mathf.Max(Vector3.Distance(pos, ear) / speedOfSound, previousDelay + minStartGap);
|
||||
radarPingSound.Play(pos, delay);
|
||||
previousDelay = delay;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user