Add host-configurable match settings persisted in settings
- GameSettings: 7 match fields (mode, hunter count, hide/seek/resolve durations, radar ping interval/lifetime) with authored defaults - MatchSettingSlider (new): enum-driven panel binder, seeds from settings, saves on change; lifetime label shows one decimal (invariant culture) - MatchManager: ApplyHostMatchSettings() on StartMatch (clamped); phase message now carries ping interval so clients tick correctly Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1468921ca2
commit
270c42d85b
4 files changed
+200
-14
No files matched your search
@@ -0,0 +1,100 @@
|
||||
using System.Globalization;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
/// Binds a UI Slider (and optional value label) to one host-side match rule persisted via
|
||||
/// GameSettings (Settings.json). The slider's range / whole-numbers are authored in the inspector;
|
||||
/// this only reads/writes the value. The machine that starts the match applies the stored values
|
||||
/// over the MatchManager's inspector defaults (MatchManager.ApplyHostMatchSettings).
|
||||
public class MatchSettingSlider : MonoBehaviour
|
||||
{
|
||||
public enum Setting
|
||||
{
|
||||
Mode,
|
||||
HunterCount,
|
||||
HideDuration,
|
||||
SeekDuration,
|
||||
ResolveDuration,
|
||||
RadarPingInterval,
|
||||
RadarPingLifetime,
|
||||
}
|
||||
|
||||
[Tooltip("Which persisted match setting this slider edits.")]
|
||||
[SerializeField] private Setting setting;
|
||||
[SerializeField] private Slider slider;
|
||||
[Tooltip("Optional TMP label showing the current value (mode name / count / seconds).")]
|
||||
[SerializeField] private TMP_Text valueLabel;
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
if (!slider) return;
|
||||
float value = ReadSetting();
|
||||
slider.SetValueWithoutNotify(value);
|
||||
UpdateLabel(value);
|
||||
slider.onValueChanged.AddListener(OnSliderChanged);
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
if (slider) slider.onValueChanged.RemoveListener(OnSliderChanged);
|
||||
}
|
||||
|
||||
void OnSliderChanged(float value)
|
||||
{
|
||||
WriteSetting(value);
|
||||
UpdateLabel(value);
|
||||
}
|
||||
|
||||
private float ReadSetting()
|
||||
{
|
||||
switch (setting)
|
||||
{
|
||||
case Setting.Mode: return GameSettings.MatchModeIndex;
|
||||
case Setting.HunterCount: return GameSettings.MatchHunterCount;
|
||||
case Setting.HideDuration: return GameSettings.MatchHideDuration;
|
||||
case Setting.SeekDuration: return GameSettings.MatchSeekDuration;
|
||||
case Setting.ResolveDuration: return GameSettings.MatchResolveDuration;
|
||||
case Setting.RadarPingInterval: return GameSettings.MatchRadarPingInterval;
|
||||
case Setting.RadarPingLifetime: return GameSettings.MatchRadarPingLifetime;
|
||||
default: return 0f;
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteSetting(float value)
|
||||
{
|
||||
switch (setting)
|
||||
{
|
||||
case Setting.Mode: GameSettings.MatchModeIndex = Mathf.RoundToInt(value); break;
|
||||
case Setting.HunterCount: GameSettings.MatchHunterCount = Mathf.RoundToInt(value); break;
|
||||
case Setting.HideDuration: GameSettings.MatchHideDuration = value; break;
|
||||
case Setting.SeekDuration: GameSettings.MatchSeekDuration = value; break;
|
||||
case Setting.ResolveDuration: GameSettings.MatchResolveDuration = value; break;
|
||||
case Setting.RadarPingInterval: GameSettings.MatchRadarPingInterval = value; break;
|
||||
case Setting.RadarPingLifetime: GameSettings.MatchRadarPingLifetime = value; break;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateLabel(float value)
|
||||
{
|
||||
if (!valueLabel) return;
|
||||
switch (setting)
|
||||
{
|
||||
case Setting.Mode:
|
||||
valueLabel.text = ((MatchManager.MatchMode)Mathf.RoundToInt(value)).ToString();
|
||||
break;
|
||||
case Setting.HunterCount:
|
||||
valueLabel.text = Mathf.RoundToInt(value).ToString();
|
||||
break;
|
||||
case Setting.RadarPingInterval when value <= 0f:
|
||||
valueLabel.text = "Off";
|
||||
break;
|
||||
case Setting.RadarPingLifetime:
|
||||
valueLabel.text = $"{value.ToString("0.0", CultureInfo.InvariantCulture)} s";
|
||||
break;
|
||||
default:
|
||||
valueLabel.text = $"{Mathf.RoundToInt(value)} s";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f6702d8b80bc1b44aa4723a64e0e529a
|
||||
@@ -18,6 +18,18 @@ public static class GameSettings
|
||||
public float mouseSensitivity = global::MouseSensitivity.Default;
|
||||
public float masterVolume = global::MasterVolume.Default;
|
||||
public string playerName = "";
|
||||
|
||||
// Host-side match rules (Esc-menu Match Panel). Ranges are authored on the panel sliders; the
|
||||
// values are applied server-side in MatchManager.StartMatch (with defensive clamps there).
|
||||
// Defaults mirror the Main scene's authored MatchManager values, so a fresh Settings.json
|
||||
// starts matches exactly like before the panel existed.
|
||||
public int matchMode = 0; // MatchManager.MatchMode index (0 = Classic, 1 = Conversion)
|
||||
public int matchHunterCount = 1;
|
||||
public float matchHideDuration = 120f;
|
||||
public float matchSeekDuration = 300f;
|
||||
public float matchResolveDuration = 50f;
|
||||
public float matchRadarPingInterval = 30f;
|
||||
public float matchRadarPingLifetime = 1f;
|
||||
}
|
||||
|
||||
private static Data _data;
|
||||
@@ -60,6 +72,55 @@ public static class GameSettings
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>MatchManager.MatchMode enum index the host starts matches with (0 = Classic, 1 = Conversion).</summary>
|
||||
public static int MatchModeIndex
|
||||
{
|
||||
get => Store.matchMode;
|
||||
set { if (Store.matchMode != value) { Store.matchMode = value; Save(); } }
|
||||
}
|
||||
|
||||
/// <summary>How many randomly chosen players become Hunters at match start.</summary>
|
||||
public static int MatchHunterCount
|
||||
{
|
||||
get => Store.matchHunterCount;
|
||||
set { if (Store.matchHunterCount != value) { Store.matchHunterCount = value; Save(); } }
|
||||
}
|
||||
|
||||
/// <summary>HIDE phase length in seconds.</summary>
|
||||
public static float MatchHideDuration
|
||||
{
|
||||
get => Store.matchHideDuration;
|
||||
set { if (!Mathf.Approximately(Store.matchHideDuration, value)) { Store.matchHideDuration = value; Save(); } }
|
||||
}
|
||||
|
||||
/// <summary>SEEK phase length in seconds.</summary>
|
||||
public static float MatchSeekDuration
|
||||
{
|
||||
get => Store.matchSeekDuration;
|
||||
set { if (!Mathf.Approximately(Store.matchSeekDuration, value)) { Store.matchSeekDuration = value; Save(); } }
|
||||
}
|
||||
|
||||
/// <summary>RESOLUTION (result screen) phase length in seconds.</summary>
|
||||
public static float MatchResolveDuration
|
||||
{
|
||||
get => Store.matchResolveDuration;
|
||||
set { if (!Mathf.Approximately(Store.matchResolveDuration, value)) { Store.matchResolveDuration = value; Save(); } }
|
||||
}
|
||||
|
||||
/// <summary>Seconds between radar ping sweeps during Seek (0 = pings disabled).</summary>
|
||||
public static float MatchRadarPingInterval
|
||||
{
|
||||
get => Store.matchRadarPingInterval;
|
||||
set { if (!Mathf.Approximately(Store.matchRadarPingInterval, value)) { Store.matchRadarPingInterval = value; Save(); } }
|
||||
}
|
||||
|
||||
/// <summary>Seconds each hider radar blip stays alive before it fades.</summary>
|
||||
public static float MatchRadarPingLifetime
|
||||
{
|
||||
get => Store.matchRadarPingLifetime;
|
||||
set { if (!Mathf.Approximately(Store.matchRadarPingLifetime, value)) { Store.matchRadarPingLifetime = value; Save(); } }
|
||||
}
|
||||
|
||||
private static Data Load()
|
||||
{
|
||||
string path = SavePath();
|
||||
|
||||
@@ -198,6 +198,7 @@ public class MatchManager : MonoBehaviour
|
||||
private float _phaseEndTime; // Time.time when the current sub-phase ends (for the HUD countdown)
|
||||
private float _seekStartTime; // Time.time Seek began locally (anchors the ping-countdown cadence, START-aligned)
|
||||
private MatchMode _localMode = MatchMode.Classic; // mirror of the server's matchMode, carried in the phase message
|
||||
private float _localPingInterval; // mirror of the server's radarPingInterval, carried in the phase message (seeded from the inspector value in Awake)
|
||||
|
||||
private const string PreMatchTag = "PreMatch"; // scene props destroyed when seek starts
|
||||
|
||||
@@ -210,10 +211,11 @@ public class MatchManager : MonoBehaviour
|
||||
/// <summary>Seconds left in the current sub-phase for the HUD (clamped to 0; cosmetic).</summary>
|
||||
public float LocalPhaseRemaining => Mathf.Max(0f, _phaseEndTime - Time.time);
|
||||
|
||||
/// <summary>Seconds between radar ping sweeps (0 = pings disabled). Baked identically on every client
|
||||
/// (persistent MatchManager, no replication - like <see cref="EmbeddedRevealPercent"/>), so the HUD can
|
||||
/// drive the ping countdown locally.</summary>
|
||||
public float RadarPingInterval => radarPingInterval;
|
||||
/// <summary>Seconds between radar ping sweeps (0 = pings disabled). Mirror of the SERVER's value, carried
|
||||
/// in the phase message - the host overrides its inspector default from the Match Panel settings on match
|
||||
/// start, so the value can no longer be assumed identical on every install. The HUD drives the ping
|
||||
/// countdown locally from this mirror.</summary>
|
||||
public float RadarPingInterval => _localPingInterval;
|
||||
|
||||
/// <summary>Seconds until the next radar ping sweep for the HUD (counts down from <see cref="RadarPingInterval"/>
|
||||
/// and repeats). 0 outside Seek or when pings are disabled. Computed locally from the local Seek-start +
|
||||
@@ -223,9 +225,9 @@ public class MatchManager : MonoBehaviour
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_subPhase != MatchSubPhase.Seek || radarPingInterval <= 0f) return 0f;
|
||||
float intoInterval = (Time.time - _seekStartTime) % radarPingInterval;
|
||||
return radarPingInterval - intoInterval;
|
||||
if (_subPhase != MatchSubPhase.Seek || _localPingInterval <= 0f) return 0f;
|
||||
float intoInterval = (Time.time - _seekStartTime) % _localPingInterval;
|
||||
return _localPingInterval - intoInterval;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,8 +258,9 @@ public class MatchManager : MonoBehaviour
|
||||
// the other client->server ("please end the match"); the server only honors it from the authority id.
|
||||
private const string MsgAuthorityGranted = "Match_AuthorityGranted";
|
||||
private const string MsgRequestEndMatch = "Match_RequestEndMatch";
|
||||
// Server -> all clients: the sub-phase changed (byte phase + float duration). Client -> server: a hunter
|
||||
// reports a hit on a hider (ulong target client id); the server validates and marks the hider found.
|
||||
// Server -> all clients: the sub-phase changed (byte phase + float duration + byte mode + float radar ping
|
||||
// interval). Client -> server: a hunter reports a hit on a hider (ulong target client id); the server
|
||||
// validates and marks the hider found.
|
||||
private const string MsgPhase = "Match_Phase";
|
||||
private const string MsgReportHit = "Match_ReportHit";
|
||||
// Client -> server, right after the client connects: the sender's persistent per-install id (survives its
|
||||
@@ -346,6 +349,7 @@ public class MatchManager : MonoBehaviour
|
||||
{
|
||||
if (Instance && Instance != this) { Destroy(this); return; }
|
||||
Instance = this;
|
||||
_localPingInterval = radarPingInterval; // pre-match HUD default; overwritten by every phase message
|
||||
_net = GetComponent<NetworkManager>();
|
||||
if (!_net) _net = FindFirstObjectByType<NetworkManager>();
|
||||
|
||||
@@ -481,7 +485,7 @@ public class MatchManager : MonoBehaviour
|
||||
_hasAuthorityClient = false;
|
||||
_isLocalAuthority = false;
|
||||
Phase = MatchPhase.Lobby;
|
||||
ApplyPhaseLocal(MatchSubPhase.None, 0f, _localMode);
|
||||
ApplyPhaseLocal(MatchSubPhase.None, 0f, _localMode, _localPingInterval);
|
||||
_pendingHiders.Clear();
|
||||
_persistentIds.Clear();
|
||||
// Drop any in-flight loading-barrier state (StopAllCoroutines above already killed the polls).
|
||||
@@ -676,6 +680,8 @@ public class MatchManager : MonoBehaviour
|
||||
var ids = new List<ulong>(_net.ConnectedClientsIds);
|
||||
if (ids.Count == 0) return;
|
||||
|
||||
ApplyHostMatchSettings();
|
||||
|
||||
Phase = MatchPhase.Transitioning;
|
||||
|
||||
// Roll N distinct random Hunters via partial Fisher-Yates (swap a random not-yet-picked id into
|
||||
@@ -695,6 +701,20 @@ public class MatchManager : MonoBehaviour
|
||||
StartCoroutine(MatchSequence(hunterIds));
|
||||
}
|
||||
|
||||
// Server-only, called once per match start. Pulls the host-authored match rules (Esc-menu Match Panel,
|
||||
// persisted via GameSettings/Settings.json) over the inspector defaults, so the machine that starts the
|
||||
// match decides the rules. Defensively clamped - the JSON is hand-editable.
|
||||
private void ApplyHostMatchSettings()
|
||||
{
|
||||
matchMode = GameSettings.MatchModeIndex == (int)MatchMode.Conversion ? MatchMode.Conversion : MatchMode.Classic;
|
||||
hunterCount = Mathf.Max(0, GameSettings.MatchHunterCount);
|
||||
hideDuration = Mathf.Max(0f, GameSettings.MatchHideDuration);
|
||||
seekDuration = Mathf.Max(0f, GameSettings.MatchSeekDuration);
|
||||
resolveDuration = Mathf.Max(0f, GameSettings.MatchResolveDuration);
|
||||
radarPingInterval = Mathf.Max(0f, GameSettings.MatchRadarPingInterval);
|
||||
radarPingLifetime = Mathf.Max(0f, GameSettings.MatchRadarPingLifetime);
|
||||
}
|
||||
|
||||
private IEnumerator MatchSequence(HashSet<ulong> hunterIds)
|
||||
{
|
||||
_conversionRecords.Clear(); // fresh match inherits no stale statue captures (EndMatch also clears)
|
||||
@@ -1027,17 +1047,18 @@ public class MatchManager : MonoBehaviour
|
||||
|
||||
private void SetPhaseServer(MatchSubPhase phase, float duration)
|
||||
{
|
||||
ApplyPhaseLocal(phase, duration, matchMode);
|
||||
ApplyPhaseLocal(phase, duration, matchMode, radarPingInterval);
|
||||
BroadcastPhase(phase, duration, matchMode, 0, true);
|
||||
}
|
||||
|
||||
// Shared by the server's SetPhaseServer and a client's OnPhaseMessage handler.
|
||||
private void ApplyPhaseLocal(MatchSubPhase phase, float duration, MatchMode mode)
|
||||
private void ApplyPhaseLocal(MatchSubPhase phase, float duration, MatchMode mode, float pingInterval)
|
||||
{
|
||||
bool changed = _subPhase != phase;
|
||||
_subPhase = phase;
|
||||
_phaseEndTime = Time.time + duration;
|
||||
_localMode = mode;
|
||||
_localPingInterval = pingInterval;
|
||||
// Props staged only for the hide phase are gone the instant seek begins; anchor the local ping-countdown
|
||||
// cadence to the same moment (START-aligned, matching the server's sweep timer).
|
||||
if (changed && phase == MatchSubPhase.Seek)
|
||||
@@ -1064,11 +1085,12 @@ public class MatchManager : MonoBehaviour
|
||||
{
|
||||
var cmm = _net ? _net.CustomMessagingManager : null;
|
||||
if (cmm == null) return;
|
||||
using (var writer = new FastBufferWriter(sizeof(byte) + sizeof(float) + sizeof(byte), Allocator.Temp))
|
||||
using (var writer = new FastBufferWriter(sizeof(byte) + sizeof(float) + sizeof(byte) + sizeof(float), Allocator.Temp))
|
||||
{
|
||||
writer.WriteValueSafe((byte)phase);
|
||||
writer.WriteValueSafe(duration);
|
||||
writer.WriteValueSafe((byte)mode);
|
||||
writer.WriteValueSafe(radarPingInterval);
|
||||
if (toAll)
|
||||
{
|
||||
_phaseRecipients.Clear();
|
||||
@@ -1090,7 +1112,8 @@ public class MatchManager : MonoBehaviour
|
||||
reader.ReadValueSafe(out byte phase);
|
||||
reader.ReadValueSafe(out float duration);
|
||||
reader.ReadValueSafe(out byte mode);
|
||||
ApplyPhaseLocal((MatchSubPhase)phase, duration, (MatchMode)mode);
|
||||
reader.ReadValueSafe(out float pingInterval);
|
||||
ApplyPhaseLocal((MatchSubPhase)phase, duration, (MatchMode)mode, pingInterval);
|
||||
}
|
||||
|
||||
// Server-only. One radar sweep during Seek: registers every still-un-found hider as a blip on each HUNTER's
|
||||
|
||||
Reference in new issue
Block a user