Files
Learning-UnityNGO/Assets/CONTENT/FEATURES/DEV-TEST/Scripts/RoundManager.cs
T
2026-07-02 17:04:29 +03:00

290 lines
9.0 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using Unity.Netcode;
using UnityEngine;
public struct PlayerScore : INetworkSerializable, System.IEquatable<PlayerScore>
{
public ulong ClientId;
public int Score;
public bool Equals(PlayerScore _other)
{
return ClientId == _other.ClientId && Score == _other.Score;
}
public void NetworkSerialize<T>(BufferSerializer<T> 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<float> roundDuration = new NetworkVariable<float>(180f);
private NetworkVariable<float> roundTimeRemaining = new NetworkVariable<float>(0f);
private NetworkVariable<int> currentRound = new NetworkVariable<int>(0);
private NetworkVariable<bool> isRoundActive = new NetworkVariable<bool>(false);
private List<PlayerHealth> alivePlayers = new List<PlayerHealth>();
private NetworkList<PlayerScore> playerScores = new NetworkList<PlayerScore>();
// pitDefenders[i] = ClientId игрока, которому принадлежит яма с pitIndex == i
private ulong[] pitDefenders;
private const ulong NoWinner = ulong.MaxValue;
public event Action<float> TimerChanged;
public event Action<int> 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<PlayerHealth>(FindObjectsSortMode.None);
// Только живые игроки: у клона тоже есть PlayerHealth,
// но ему нельзя давать яму и спавн-поинт
List<PlayerHealth> _players = new List<PlayerHealth>();
foreach (PlayerHealth _p in _found)
{
if (_p.GetComponent<GhostPlayer>() != 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<PlayerHealth>(FindObjectsSortMode.None);
int _count = 0;
foreach (PlayerHealth _p in _found)
if (_p.GetComponent<GhostPlayer>() == 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<PlayerScore> _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)
{
}
}