645 lines
15 KiB
C#
645 lines
15 KiB
C#
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<SpawnPoint> SpawnPoints => _allSpawnPoints;
|
|
public ShrinkingArea ShrinkingArea => _shrinkingArea;
|
|
|
|
public Action<PlayerRef> OnPlayerJoinedGame;
|
|
public Action<string> OnPlayerLeftGame;
|
|
public Action<KillData> OnAgentDeath;
|
|
public Action<PlayerRef> 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<RespawnRequest> _respawnRequests = new Queue<RespawnRequest>(16);
|
|
|
|
private List<SpawnPoint> _allSpawnPoints = new List<SpawnPoint>();
|
|
private List<SpawnPoint> _availableSpawnPoints = new List<SpawnPoint>();
|
|
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<ShrinkingArea>();
|
|
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<PlayerStatistics> 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<Agent>();
|
|
|
|
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<PlayerStatistics>(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<PlayerStatistics>
|
|
{
|
|
public int Compare(PlayerStatistics x, PlayerStatistics y)
|
|
{
|
|
return y.Score.CompareTo(x.Score);
|
|
}
|
|
}
|
|
}
|
|
}
|