using System; using System.Collections; using System.Collections.Generic; using TMPro; using Unity.Netcode; using UnityEngine; public struct PlayerScore : INetworkSerializable, System.IEquatable { public ulong ClientId; public int Score; public bool Equals(PlayerScore _other) { return ClientId == _other.ClientId && Score == _other.Score; } public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { serializer.SerializeValue(ref ClientId); serializer.SerializeValue(ref Score); } } public class RoundManager : NetworkBehaviour { public static RoundManager Current { get; private set; } [SerializeField] private float roundStartDelay = 3f; [SerializeField] private Transform[] spawnPoints; private NetworkVariable roundDuration = new NetworkVariable(180f); private NetworkVariable roundTimeRemaining = new NetworkVariable(0f); private NetworkVariable currentRound = new NetworkVariable(0); private NetworkVariable isRoundActive = new NetworkVariable(false); private List alivePlayers = new List(); private NetworkList playerScores = new NetworkList(); // pitDefenders[i] = ClientId игрока, которому принадлежит яма с pitIndex == i private ulong[] pitDefenders; private const ulong NoWinner = ulong.MaxValue; public event Action TimerChanged; public event Action RoundChanged; public event Action ScoresChanged; public override void OnNetworkSpawn() { currentRound.OnValueChanged += OnRoundChanged; isRoundActive.OnValueChanged += OnRoundStateChanged; roundTimeRemaining.OnValueChanged += OnRoundTimeChanged; playerScores.OnListChanged += OnScoresChanged; if (IsServer) { currentRound.Value = 0; StartCoroutine(StartRoundWithDelay()); } } private void Update() { if (!IsServer) return; if (!IsRoundActive) return; roundTimeRemaining.Value = Mathf.Max(0f, roundTimeRemaining.Value - Time.deltaTime); if (roundTimeRemaining.Value <= 0f) { StartCoroutine(EndRound(NoWinner)); } } void Awake() { Current = this; } public void OnPlayerEnteredPit(int _pitIndex, ulong _enteringClientId) { if (!IsServer) return; if (!isRoundActive.Value) return; if (pitDefenders == null) return; if (_pitIndex < 0 || _pitIndex >= pitDefenders.Length) return; // Своя яма — не считается, побеждают только за попадание в ЧУЖУЮ яму. // Для клона сюда приходит ClientId его ОРИГИНАЛА (см. PitZone) — правило работает так же. if (_enteringClientId == pitDefenders[_pitIndex]) return; AddScore(_enteringClientId); StartCoroutine(EndRound(_enteringClientId)); } private void AddScore(ulong _clientId) { for (int _i = 0; _i < playerScores.Count; _i++) { if (playerScores[_i].ClientId != _clientId) continue; playerScores[_i] = new PlayerScore { ClientId = _clientId, Score = playerScores[_i].Score + 1 }; return; } } public void OnPlayerDied(PlayerHealth _playerHealth) { // if (!IsServer) return; // alivePlayers.Remove(_playerHealth); // if (alivePlayers.Count <= 1) // { // StartCoroutine(EndRound()); // } } public void RegisterPlayer(ulong _clientId) { if (!IsServer) return; foreach (var _s in playerScores) if (_s.ClientId == _clientId) return; playerScores.Add(new PlayerScore { ClientId = _clientId, Score = 0 }); } private IEnumerator EndRound(ulong _winnerClientId) { isRoundActive.Value = false; // Сразу: стоп клонов + запись раунда в архив. // Обязательно ДО WaitForSeconds — иначе клоны бегают и стреляют на экране победы. if (GhostRoundManager.Current != null) GhostRoundManager.Current.ServerEndRound(); string _winner = _winnerClientId == NoWinner ? "No winner (time up)" : $"Player {_winnerClientId} wins!"; RoundEndedClientRPC(_winner); yield return new WaitForSeconds(roundStartDelay); currentRound.Value++; StartCoroutine(StartRoundWithDelay()); } private void RespawnAllPlayers() { if (!IsServer) return; if (spawnPoints == null || spawnPoints.Length == 0) return; PlayerHealth[] _found = FindObjectsByType(FindObjectsSortMode.None); // Только живые игроки: у клона тоже есть PlayerHealth, // но ему нельзя давать яму и спавн-поинт List _players = new List(); foreach (PlayerHealth _p in _found) { if (_p.GetComponent() != null) continue; _players.Add(_p); } // Детерминированный порядок раздачи ям: клон слепо повторяет прошлый раунд, // значит "чья яма где" должно совпадать из раунда в раунд. // FindObjectsByType порядок не гарантирует — сортируем по ClientId. _players.Sort((_a, _b) => _a.OwnerClientId.CompareTo(_b.OwnerClientId)); alivePlayers.Clear(); // Одна яма на игрока: игрок с индексом _q защищает яму pitIndex == _q pitDefenders = new ulong[_players.Count]; for (int _q = 0; _q < _players.Count; _q++) { ulong _clientId = _players[_q].OwnerClientId; RegisterPlayer(_clientId); pitDefenders[_q] = _clientId; Vector3 _spawnPosition = spawnPoints[_q % spawnPoints.Length].position; _players[_q].Respawn(_spawnPosition); alivePlayers.Add(_players[_q]); } } private IEnumerator StartRoundWithDelay() { yield return new WaitForSeconds(1f); yield return new WaitUntil(() => CountRealPlayers() >= 2); RespawnAllPlayers(); // Спавним клонов прошлых раундов и включаем запись у живых игроков if (GhostRoundManager.Current != null) GhostRoundManager.Current.ServerStartRound(); roundTimeRemaining.Value = roundDuration.Value; isRoundActive.Value = true; RoundStartedClientRPC(currentRound.Value); } // Считаем только настоящих игроков — у клонов тоже есть PlayerHealth private int CountRealPlayers() { PlayerHealth[] _found = FindObjectsByType(FindObjectsSortMode.None); int _count = 0; foreach (PlayerHealth _p in _found) if (_p.GetComponent() == null) _count++; return _count; } [ClientRpc] private void RoundStartedClientRPC(int _round) { Debug.Log($"Round {_round} started"); } [ClientRpc] private void RoundEndedClientRPC(string _winnerText) { Debug.Log($"Round ended: {_winnerText}"); } public bool IsRoundActive => isRoundActive.Value; public int CurrentRound => currentRound.Value; public float RoundTimeRemaining => roundTimeRemaining.Value; public int GetScore(ulong _clientId) { foreach (PlayerScore _s in playerScores) if (_s.ClientId == _clientId) return _s.Score; return 0; } // Для 2 игроков: счёт первого игрока, чей ClientId не равен локальному public int GetEnemyScore(ulong _localClientId) { foreach (PlayerScore _s in playerScores) if (_s.ClientId != _localClientId) return _s.Score; return 0; } public override void OnNetworkDespawn() { if (IsServer && Current == this) Current = null; currentRound.OnValueChanged -= OnRoundChanged; isRoundActive.OnValueChanged -= OnRoundStateChanged; roundTimeRemaining.OnValueChanged -= OnRoundTimeChanged; playerScores.OnListChanged -= OnScoresChanged; } private void OnScoresChanged(NetworkListEvent _event) { ScoresChanged?.Invoke(); } private void OnRoundTimeChanged(float _previousValue, float _newValue) { TimerChanged?.Invoke(_newValue); } private void OnRoundChanged(int _previousValue, int _newValue) { RoundChanged?.Invoke(_newValue); } private void OnRoundStateChanged(bool _previousValue, bool _newValue) { } }