using Fusion; using System; namespace TPSBR { using System.Collections; using System.Collections.Generic; using UnityEngine; public struct KillData : INetworkStruct { public PlayerRef KillerRef; public PlayerRef VictimRef; public EHitType HitType; public bool Headshot { get { return _flags.IsBitSet(0); } set { _flags.SetBit(0, value); } } private byte _flags; } public struct RespawnRequest { public PlayerRef PlayerRef; public TickTimer Timer; } public enum EGameplayType { None, Deathmatch, Elimination, BattleRoyale, } public abstract class GameplayMode : ContextBehaviour { public enum EState { None, Active, Finished, } public string GameplayName; public int MaxPlayers; public short ScorePerKill; public short ScorePerDeath; public short ScorePerSuicide; public float RespawnTime; public float TimeLimit; public float BackfillTimeLimit; public Announcement[] Announcements; // PUBLIC MEMBERS public EGameplayType Type => _type; public float Time => (Runner.Tick - _startTick) * Runner.DeltaTime; public float RemainingTime => _endTimer.IsRunning == true ? _endTimer.RemainingTime(Runner).Value : 0f; [Networked, HideInInspector] public EState State { get; private set; } public List SpawnPoints => _allSpawnPoints; public ShrinkingArea ShrinkingArea => _shrinkingArea; public Action OnPlayerJoinedGame; public Action OnPlayerLeftGame; public Action OnAgentDeath; public Action OnPlayerEliminated; // PROTECTED MEMBERS [Networked, HideInInspector] protected int _startTick { get; set; } [Networked, HideInInspector] protected TickTimer _endTimer { get; private set; } [Networked, HideInInspector] protected ShrinkingArea _shrinkingArea { get; set; } // PRIVATE MEMBERS [SerializeField] private EGameplayType _type; [SerializeField] private bool _useShrinkingArea; [SerializeField] private float _maxKillInRowDelay = 3.5f; private Queue _respawnRequests = new Queue(16); private List _allSpawnPoints = new List(); private List _availableSpawnPoints = new List(); private DefaultPlayerComparer _playerComparer = new DefaultPlayerComparer(); private float _backfillTimerS; // PUBLIC METHODS public void Activate() { if (Runner.IsServer == false) return; if (State != EState.None) return; _startTick = Runner.Tick; if (TimeLimit > 0f) { _endTimer = TickTimer.CreateFromSeconds(Runner, TimeLimit); } if (_useShrinkingArea == true && HasStateAuthority == true) { _shrinkingArea = Runner.SimulationUnityScene.GetComponent(); if (_shrinkingArea != null && HasStateAuthority == true) { _shrinkingArea.Activate(); _shrinkingArea.ShrinkingAnnounced += OnShrinkingAreaAnnounced; } } Runner.SimulationUnityScene.GetComponents(_allSpawnPoints); for (int i = 0; i < _allSpawnPoints.Count; i++) { if (_allSpawnPoints[i].SpawnEnabled == true) { _availableSpawnPoints.Add(_allSpawnPoints[i]); } } if (_shrinkingArea != null && HasStateAuthority == true) { OnShrinkingAreaAnnounced(_shrinkingArea.Center, _shrinkingArea.Radius); } State = EState.Active; OnActivate(); } public void AgentDeath(Agent victim, HitData hitData) { if (Runner.IsServer == false) return; if (State != EState.Active) return; var victimRef = victim.Object.InputAuthority; var victimPlayer = Context.NetworkGame.GetPlayer(victimRef); var victimStatistics = victimPlayer != null ? victimPlayer.Statistics : default; var respawnTime = GetRespawnTime(victimStatistics); if (respawnTime >= 0f) { var respawnTimer = TickTimer.CreateFromSeconds(Runner, respawnTime); victimStatistics.RespawnTimer = respawnTimer; _respawnRequests.Enqueue(new RespawnRequest() { PlayerRef = victimRef, Timer = respawnTimer, }); } else { victimStatistics.IsEliminated = true; } victimStatistics.IsAlive = false; victimStatistics.Deaths += 1; victimStatistics.Score += ScorePerDeath; victimStatistics.KillsWithoutDeath = 0; var killerRef = hitData.InstigatorRef; var killerPlayer = killerRef.IsRealPlayer == true ? Context.NetworkGame.GetPlayer(killerRef) : default; var killerStatistics = killerPlayer != null ? killerPlayer.Statistics : default; if (killerRef == victimRef) { victimStatistics.Score += ScorePerSuicide; } else { killerStatistics.Kills += 1; killerStatistics.Score += ScorePerKill; killerStatistics.KillsWithoutDeath += 1; if (killerStatistics.KillsInRowCooldown.Expired(Runner) == false) { killerStatistics.KillsInRow += 1; } else { killerStatistics.KillsInRow = 1; } killerStatistics.KillsInRowCooldown = TickTimer.CreateFromSeconds(Runner, _maxKillInRowDelay); } AgentDeath(ref victimStatistics, ref killerStatistics); if (victimPlayer != null) { victimPlayer.UpdateStatistics(victimStatistics); } if (killerPlayer != null && killerPlayer != victimPlayer) { killerPlayer.UpdateStatistics(killerStatistics); } RecalculatePositions(); var killData = new KillData() { KillerRef = killerStatistics.PlayerRef, VictimRef = victimRef, Headshot = hitData.IsCritical, HitType = hitData.HitType, }; RPC_AgentDeath(killData); if (victimStatistics.IsEliminated == true) { RPC_PlayerEliminated(victimRef); } CheckWinCondition(); } public Transform GetRandomSpawnPoint(float minDistanceFromAgents) { if (_availableSpawnPoints.SafeCount() == 0) return null; while (minDistanceFromAgents > 1.0f) { float minSqrDistanceFromAgents = minDistanceFromAgents * minDistanceFromAgents; for (int i = 0, count = Mathf.Min(5 + _availableSpawnPoints.Count, 25); i < count; ++i) { Transform spawnPoint = _availableSpawnPoints.GetRandom().transform; bool isValid = true; foreach (var player in Context.NetworkGame.ActivePlayers) { if (player == null) continue; var agent = player.ActiveAgent; if (agent == null) continue; if (Vector3.SqrMagnitude(agent.transform.position - spawnPoint.position) < minSqrDistanceFromAgents) { isValid = false; break; } } if (isValid == true) return spawnPoint; } minDistanceFromAgents *= 0.5f; } return _availableSpawnPoints.GetRandom().transform; } public void ChangeSpectatorTarget(bool next) { var observedPlayerRef = Context.ObservedPlayerRef; int playerIndex = 0; int maxPlayerIndex = 1000; while (playerIndex < maxPlayerIndex) { ++playerIndex; if (observedPlayerRef.AsIndex > maxPlayerIndex) { observedPlayerRef = PlayerRef.None; } else if (observedPlayerRef.AsIndex < PlayerRef.None.AsIndex) { observedPlayerRef = PlayerRef.FromIndex(maxPlayerIndex); } observedPlayerRef = PlayerRef.FromIndex(observedPlayerRef.AsIndex + (next == true ? 1 : -1)); Player observedPlayer = Context.NetworkGame.GetPlayer(observedPlayerRef); if (observedPlayer == null) continue; if (observedPlayer.Statistics.IsEliminated == true) continue; break; } var localPlayer = Context.NetworkGame.GetPlayer(Context.LocalPlayerRef); localPlayer.SetObservedPlayer(observedPlayerRef); } public void PlayerJoined(Player player) { var statistics = player.Statistics; PreparePlayerStatistics(ref statistics); player.UpdateStatistics(statistics); if (statistics.IsEliminated == false) { TrySpawnAgent(player); } RecalculatePositions(); Context.Backfill.PlayerJoined(player); RPC_PlayerJoinedGame(player.Object.InputAuthority); } public void PlayerLeft(Player player) { if (Runner.IsServer == false) return; if (State == EState.Finished) return; player.DespawnAgent(); RecalculatePositions(); Context.Backfill.PlayerLeft(player); string nickname = player.Nickname; if (nickname.HasValue() == false) { nickname = "Unknown"; } RPC_PlayerLeftGame(player.Object.InputAuthority, nickname); CheckWinCondition(); } public void StopGame() { if (HasStateAuthority == false) { Global.Networking.StopGame(); return; } StartCoroutine(StopGameCoroutine()); } // NetworkBehaviour INTERFACE public override void Spawned() { Context.GameplayMode = this; } public override void Despawned(NetworkRunner runner, bool hasState) { if (_shrinkingArea != null) { _shrinkingArea.ShrinkingAnnounced -= OnShrinkingAreaAnnounced; } } public override void FixedUpdateNetwork() { if (HasStateAuthority == false) return; switch (State) { case EState.Active: FixedUpdateNetwork_Active(); break; case EState.Finished: FixedUpdateNetwork_Finished(); break; } } // GameplayMode INTERFACE protected virtual void OnActivate() { } protected virtual void TrySpawnAgent(Player player) { Transform spawnPoint = GetRandomSpawnPoint(100.0f); var spawnPosition = spawnPoint != null ? spawnPoint.position : Vector3.zero; var spawnRotation = spawnPoint != null ? spawnPoint.rotation : Quaternion.identity; SpawnAgent(player.Object.InputAuthority, spawnPosition, spawnRotation); } protected virtual void AgentDeath(ref PlayerStatistics victimStatistics, ref PlayerStatistics killerStatistics) { } protected virtual void PreparePlayerStatistics(ref PlayerStatistics playerStatistics) { } protected virtual void SortPlayers(List allStatistics) { allStatistics.Sort(_playerComparer); } protected virtual float GetRespawnTime(PlayerStatistics playerStatistics) { return RespawnTime; } protected abstract void CheckWinCondition(); // PROTECTED METHODS protected Agent SpawnAgent(PlayerRef playerRef, Vector3 position, Quaternion rotation) { var player = Context.NetworkGame.GetPlayer(playerRef); if (player.AgentPrefab.IsValid == false) { throw new InvalidOperationException(nameof(player.AgentPrefab)); } var agentObject = Runner.Spawn(player.AgentPrefab, position, rotation, playerRef); var agent = agentObject.GetComponent(); Runner.SetPlayerAlwaysInterested(playerRef, agentObject, true); var statistics = player.Statistics; statistics.IsAlive = true; statistics.RespawnTimer = default; player.UpdateStatistics(statistics); player.SetActiveAgent(agent); return agent; } protected void FinishGameplay() { if (State != EState.Active) return; if (Runner.IsServer == false) return; State = EState.Finished; Runner.SessionInfo.IsOpen = false; Context.Backfill.BackfillEnabled = false; if (Application.isBatchMode == true) { StartCoroutine(ShutdownCoroutine()); } } protected void SetSpectatorTargetToBestPlayer(Player spectatorPlayer) { var bestPlayer = PlayerRef.None; int bestPosition = int.MaxValue; foreach (var player in Context.NetworkGame.ActivePlayers) { if (player == null) continue; var statistics = player.Statistics; if (statistics.IsEliminated == true) continue; int position = statistics.Position > 0 ? statistics.Position : 1000; if (position < bestPosition) { bestPlayer = statistics.PlayerRef; bestPosition = position; } } spectatorPlayer.SetObservedPlayer(bestPlayer); } // PRIVATE METHODS private void FixedUpdateNetwork_Active() { while (_respawnRequests.Count > 0) { var respawnRequest = _respawnRequests.Peek(); if (respawnRequest.Timer.Expired(Runner) == false) break; _respawnRequests.Dequeue(); Respawn(respawnRequest.PlayerRef); } _backfillTimerS += UnityEngine.Time.deltaTime; if (_backfillTimerS > BackfillTimeLimit) { Context.Backfill.BackfillEnabled = false; } if (_endTimer.Expired(Runner) == true) { FinishGameplay(); } } private void FixedUpdateNetwork_Finished() { } private void Respawn(PlayerRef playerRef) { var player = Context.NetworkGame.GetPlayer(playerRef); if (player == null) return; // Player is not present anymore player.DespawnAgent(); TrySpawnAgent(player); } private void RecalculatePositions() { var allStatistics = ListPool.Get(byte.MaxValue); foreach (var player in Context.NetworkGame.ActivePlayers) { if (player == null) continue; var statistics = player.Statistics; if (statistics.IsValid == false) continue; allStatistics.Add(statistics); } SortPlayers(allStatistics); for (int i = 0; i < allStatistics.Count; i++) { var statistics = allStatistics[i]; statistics.Position = (byte)(i + 1); Context.NetworkGame.GetPlayer(statistics.PlayerRef).UpdateStatistics(statistics); } ListPool.Return(allStatistics); } private void OnShrinkingAreaAnnounced(Vector3 center, float radius) { _availableSpawnPoints.Clear(); var radiusSqr = radius * radius; foreach (var spawnPoint in _allSpawnPoints) { if (spawnPoint.SpawnEnabled == false) continue; var direction = spawnPoint.transform.position - center; direction.y = 0f; if (direction.sqrMagnitude <= radiusSqr) { _availableSpawnPoints.Add(spawnPoint); } } } private IEnumerator StopGameCoroutine() { RPC_StopGame(); Context.Backfill.BackfillEnabled = false; yield return new WaitForSecondsRealtime(0.25f); Global.Networking.StopGame(); } private IEnumerator ShutdownCoroutine() { yield return new WaitForSecondsRealtime(20.0f); Debug.LogWarning("Shutting down..."); Application.Quit(); } // RPCs [Rpc(RpcSources.StateAuthority, RpcTargets.All, Channel = RpcChannel.Reliable)] private void RPC_AgentDeath(KillData killData) { OnAgentDeath?.Invoke(killData); } [Rpc(RpcSources.StateAuthority, RpcTargets.All, Channel = RpcChannel.Reliable)] private void RPC_PlayerEliminated(PlayerRef playerRef) { OnPlayerEliminated?.Invoke(playerRef); } [Rpc(RpcSources.StateAuthority, RpcTargets.All, Channel = RpcChannel.Reliable)] private void RPC_PlayerJoinedGame(PlayerRef playerRef) { OnPlayerJoinedGame?.Invoke(playerRef); } [Rpc(RpcSources.StateAuthority, RpcTargets.All, Channel = RpcChannel.Reliable)] private void RPC_PlayerLeftGame(PlayerRef playerRef, string nickname) { OnPlayerLeftGame?.Invoke(nickname); } [Rpc(RpcSources.StateAuthority, RpcTargets.Proxies, Channel = RpcChannel.Reliable)] private void RPC_StopGame() { Global.Networking.StopGame(Networking.STATUS_SERVER_CLOSED); } // HELPERS private class DefaultPlayerComparer : IComparer { public int Compare(PlayerStatistics x, PlayerStatistics y) { return y.Score.CompareTo(x.Score); } } } }