392 lines
14 KiB
C#
392 lines
14 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>();
|
|
|
|
// Точки спавна, назначенные игрокам в этом раунде (параллельно alivePlayers).
|
|
// Нужны, чтобы дождаться, пока серверная позиция догонит телепорт, перед стартом записи.
|
|
private List<Vector3> spawnAssignments = new List<Vector3>();
|
|
|
|
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()
|
|
{
|
|
// Рестарт матча по R — любой игрок, запрос уходит на сервер
|
|
if (Input.GetKeyDown(KeyCode.R))
|
|
RequestRestartServerRpc();
|
|
|
|
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])
|
|
{
|
|
Debug.Log($"[ROUND] pit{_pitIndex} own pit by {_enteringClientId} — no score");
|
|
return;
|
|
}
|
|
|
|
Debug.Log($"[ROUND] WIN pit{_pitIndex} by client={_enteringClientId} (defender={pitDefenders[_pitIndex]}) round={currentRound.Value} timeLeft={roundTimeRemaining.Value:F1}");
|
|
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());
|
|
// }
|
|
}
|
|
|
|
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
|
private void RequestRestartServerRpc()
|
|
{
|
|
RestartMatch();
|
|
}
|
|
|
|
// Полный рестарт: стоп текущего раунда и клонов, очистка архива и счёта, раунд с нуля
|
|
private void RestartMatch()
|
|
{
|
|
if (!IsServer) return;
|
|
|
|
StopAllCoroutines();
|
|
|
|
if (GhostRoundManager.Current != null)
|
|
{
|
|
GhostRoundManager.Current.ServerEndRound();
|
|
GhostRoundManager.Current.ClearArchive();
|
|
}
|
|
|
|
for (int _i = 0; _i < playerScores.Count; _i++)
|
|
{
|
|
playerScores[_i] = new PlayerScore
|
|
{
|
|
ClientId = playerScores[_i].ClientId,
|
|
Score = 0
|
|
};
|
|
}
|
|
|
|
isRoundActive.Value = false;
|
|
roundTimeRemaining.Value = 0f;
|
|
currentRound.Value = 0;
|
|
|
|
StartCoroutine(StartRoundWithDelay());
|
|
}
|
|
|
|
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)
|
|
{
|
|
Debug.Log($"[ROUND] EndRound round={currentRound.Value} winner={_winnerClientId} timeLeft={roundTimeRemaining.Value:F1}");
|
|
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();
|
|
spawnAssignments.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;
|
|
|
|
// Диагностика: спавн-поинт не должен быть внутри триггера ямы,
|
|
// иначе клон заспавнится в яме и мгновенно "победит".
|
|
Collider[] _overlaps = Physics.OverlapSphere(_spawnPosition, 0.5f, ~0, QueryTriggerInteraction.Collide);
|
|
foreach (Collider _c in _overlaps)
|
|
if (_c.GetComponentInParent<PitZone>() != null)
|
|
Debug.LogError($"[ROUND] SPAWN {_q} at {_spawnPosition} INSIDE PIT '{_c.name}' для client={_clientId} — это причина мгновенной победы клона!");
|
|
|
|
_players[_q].Respawn(_spawnPosition);
|
|
|
|
alivePlayers.Add(_players[_q]);
|
|
spawnAssignments.Add(_spawnPosition);
|
|
}
|
|
}
|
|
|
|
// Ждём, пока серверная позиция каждого игрока догонит телепорт на спавн.
|
|
// Иначе первый сэмпл записи (positions[0]) окажется там, где игрок умер (в яме),
|
|
// и клон следующего раунда заспавнится прямо в яме → мгновенная ложная победа.
|
|
private IEnumerator WaitForPlayersAtSpawn()
|
|
{
|
|
float _timeout = Time.time + 3f;
|
|
float _start = Time.time;
|
|
|
|
while (Time.time < _timeout)
|
|
{
|
|
bool _allAtSpawn = true;
|
|
|
|
for (int _i = 0; _i < alivePlayers.Count; _i++)
|
|
{
|
|
if (alivePlayers[_i] == null) continue;
|
|
|
|
float _dist = Vector3.Distance(alivePlayers[_i].transform.position, spawnAssignments[_i]);
|
|
if (_dist > 1.5f)
|
|
{
|
|
_allAtSpawn = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (_allAtSpawn)
|
|
{
|
|
Debug.Log($"[ROUND] players at spawn after {Time.time - _start:F2}s");
|
|
yield break;
|
|
}
|
|
yield return null;
|
|
}
|
|
|
|
Debug.LogWarning($"[ROUND] WaitForPlayersAtSpawn TIMEOUT after 3s — positions may still be stale:");
|
|
for (int _i = 0; _i < alivePlayers.Count; _i++)
|
|
if (alivePlayers[_i] != null)
|
|
Debug.LogWarning($"[ROUND] player {alivePlayers[_i].OwnerClientId} pos={alivePlayers[_i].transform.position} target={spawnAssignments[_i]} dist={Vector3.Distance(alivePlayers[_i].transform.position, spawnAssignments[_i]):F2}");
|
|
}
|
|
|
|
private IEnumerator StartRoundWithDelay()
|
|
{
|
|
yield return new WaitForSeconds(1f);
|
|
|
|
yield return new WaitUntil(() => CountRealPlayers() >= 2);
|
|
|
|
RespawnAllPlayers();
|
|
|
|
// КРИТИЧНО: ждём, пока серверная позиция игроков реально станет спавном,
|
|
// прежде чем начинать запись — иначе клоны будут спавниться в яме (см. WaitForPlayersAtSpawn).
|
|
yield return WaitForPlayersAtSpawn();
|
|
|
|
// Спавним клонов прошлых раундов и включаем запись у живых игроков
|
|
if (GhostRoundManager.Current != null)
|
|
GhostRoundManager.Current.ServerStartRound();
|
|
|
|
roundTimeRemaining.Value = roundDuration.Value;
|
|
isRoundActive.Value = true;
|
|
|
|
Debug.Log($"[ROUND] START round={currentRound.Value} players={alivePlayers.Count}");
|
|
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)
|
|
{
|
|
|
|
}
|
|
} |