using System; using System.Collections; using System.Collections.Generic; using Unity.BossRoom.ConnectionManagement; using Unity.BossRoom.Gameplay.GameplayObjects; using Unity.BossRoom.Gameplay.GameplayObjects.Character; using Unity.BossRoom.Gameplay.Messages; using Unity.BossRoom.Infrastructure; using Unity.BossRoom.Utils; using Unity.Multiplayer.Samples.BossRoom; using Unity.Multiplayer.Samples.Utilities; using Unity.Netcode; using UnityEngine; using UnityEngine.Assertions; using UnityEngine.SceneManagement; using UnityEngine.Serialization; using VContainer; using Random = UnityEngine.Random; namespace Unity.BossRoom.Gameplay.GameState { /// /// Server specialization of core BossRoom game logic. /// [RequireComponent(typeof(NetcodeHooks))] public class ServerBossRoomState : GameStateBehaviour { [FormerlySerializedAs("m_NetworkWinState")] [SerializeField] PersistentGameState persistentGameState; [SerializeField] NetcodeHooks m_NetcodeHooks; [SerializeField] [Tooltip("Make sure this is included in the NetworkManager's list of prefabs!")] private NetworkObject m_PlayerPrefab; [SerializeField] [Tooltip("A collection of locations for spawning players")] private Transform[] m_PlayerSpawnPoints; private List m_PlayerSpawnPointsList = null; public override GameState ActiveState { get { return GameState.BossRoom; } } // Wait time constants for switching to post game after the game is won or lost private const float k_WinDelay = 7.0f; private const float k_LoseDelay = 2.5f; /// /// Has the ServerBossRoomState already hit its initial spawn? (i.e. spawned players following load from character select). /// public bool InitialSpawnDone { get; private set; } /// /// Keeping the subscriber during this GameState's lifetime to allow disposing of subscription and re-subscribing /// when despawning and spawning again. /// [Inject] ISubscriber m_LifeStateChangedEventMessageSubscriber; [Inject] ConnectionManager m_ConnectionManager; [Inject] PersistentGameState m_PersistentGameState; protected override void Awake() { base.Awake(); m_NetcodeHooks.OnNetworkSpawnHook += OnNetworkSpawn; m_NetcodeHooks.OnNetworkDespawnHook += OnNetworkDespawn; } void OnNetworkSpawn() { if (!NetworkManager.Singleton.IsServer) { enabled = false; return; } m_PersistentGameState.Reset(); m_LifeStateChangedEventMessageSubscriber.Subscribe(OnLifeStateChangedEventMessage); NetworkManager.Singleton.OnClientDisconnectCallback += OnClientDisconnect; NetworkManager.Singleton.SceneManager.OnLoadEventCompleted += OnLoadEventCompleted; NetworkManager.Singleton.SceneManager.OnSynchronizeComplete += OnSynchronizeComplete; SessionManager.Instance.OnSessionStarted(); } void OnNetworkDespawn() { if (m_LifeStateChangedEventMessageSubscriber != null) { m_LifeStateChangedEventMessageSubscriber.Unsubscribe(OnLifeStateChangedEventMessage); } NetworkManager.Singleton.OnClientDisconnectCallback -= OnClientDisconnect; NetworkManager.Singleton.SceneManager.OnLoadEventCompleted -= OnLoadEventCompleted; NetworkManager.Singleton.SceneManager.OnSynchronizeComplete -= OnSynchronizeComplete; } protected override void OnDestroy() { if (m_LifeStateChangedEventMessageSubscriber != null) { m_LifeStateChangedEventMessageSubscriber.Unsubscribe(OnLifeStateChangedEventMessage); } if (m_NetcodeHooks) { m_NetcodeHooks.OnNetworkSpawnHook -= OnNetworkSpawn; m_NetcodeHooks.OnNetworkDespawnHook -= OnNetworkDespawn; } base.OnDestroy(); } void OnSynchronizeComplete(ulong clientId) { if (InitialSpawnDone && !PlayerServerCharacter.GetPlayerServerCharacter(clientId)) { //somebody joined after the initial spawn. This is a Late Join scenario. This player may have issues //(either because multiple people are late-joining at once, or because some dynamic entities are //getting spawned while joining. But that's not something we can fully address by changes in //ServerBossRoomState. SpawnPlayer(clientId, true); } } void OnLoadEventCompleted(string sceneName, LoadSceneMode loadSceneMode, List clientsCompleted, List clientsTimedOut) { if (!InitialSpawnDone && loadSceneMode == LoadSceneMode.Single) { InitialSpawnDone = true; foreach (var kvp in NetworkManager.Singleton.ConnectedClients) { SpawnPlayer(kvp.Key, false); } } } void OnClientDisconnect(ulong clientId) { if (clientId != NetworkManager.Singleton.LocalClientId) { // If a client disconnects, check for game over in case all other players are already down StartCoroutine(WaitToCheckForGameOver()); } } IEnumerator WaitToCheckForGameOver() { // Wait until next frame so that the client's player character has despawned yield return null; CheckForGameOver(); } void SpawnPlayer(ulong clientId, bool lateJoin) { Transform spawnPoint = null; if (m_PlayerSpawnPointsList == null || m_PlayerSpawnPointsList.Count == 0) { m_PlayerSpawnPointsList = new List(m_PlayerSpawnPoints); } Debug.Assert(m_PlayerSpawnPointsList.Count > 0, $"PlayerSpawnPoints array should have at least 1 spawn points."); int index = Random.Range(0, m_PlayerSpawnPointsList.Count); spawnPoint = m_PlayerSpawnPointsList[index]; m_PlayerSpawnPointsList.RemoveAt(index); var playerNetworkObject = NetworkManager.Singleton.SpawnManager.GetPlayerNetworkObject(clientId); var newPlayer = Instantiate(m_PlayerPrefab, Vector3.zero, Quaternion.identity); var newPlayerCharacter = newPlayer.GetComponent(); var physicsTransform = newPlayerCharacter.physicsWrapper.Transform; if (spawnPoint != null) { physicsTransform.SetPositionAndRotation(spawnPoint.position, spawnPoint.rotation); } var persistentPlayerExists = playerNetworkObject.TryGetComponent(out PersistentPlayer persistentPlayer); Assert.IsTrue(persistentPlayerExists, $"Matching persistent PersistentPlayer for client {clientId} not found!"); // pass character type from persistent player to avatar var networkAvatarGuidStateExists = newPlayer.TryGetComponent(out NetworkAvatarGuidState networkAvatarGuidState); Assert.IsTrue(networkAvatarGuidStateExists, $"NetworkCharacterGuidState not found on player avatar!"); // if reconnecting, set the player's position and rotation to its previous state if (lateJoin) { SessionPlayerData? sessionPlayerData = SessionManager.Instance.GetPlayerData(clientId); if (sessionPlayerData is { HasCharacterSpawned: true }) { physicsTransform.SetPositionAndRotation(sessionPlayerData.Value.PlayerPosition, sessionPlayerData.Value.PlayerRotation); } } // instantiate new NetworkVariables with a default value to ensure they're ready for use on OnNetworkSpawn networkAvatarGuidState.AvatarGuid = new NetworkVariable(persistentPlayer.NetworkAvatarGuidState.AvatarGuid.Value); // pass name from persistent player to avatar if (newPlayer.TryGetComponent(out NetworkNameState networkNameState)) { networkNameState.Name = new NetworkVariable(persistentPlayer.NetworkNameState.Name.Value); } // spawn players characters with destroyWithScene = true newPlayer.SpawnWithOwnership(clientId, true); } void OnLifeStateChangedEventMessage(LifeStateChangedEventMessage message) { switch (message.CharacterType) { case CharacterTypeEnum.Tank: case CharacterTypeEnum.Archer: case CharacterTypeEnum.Mage: case CharacterTypeEnum.Rogue: // Every time a player's life state changes to fainted we check to see if game is over if (message.NewLifeState == LifeState.Fainted) { CheckForGameOver(); } break; case CharacterTypeEnum.ImpBoss: if (message.NewLifeState == LifeState.Dead) { BossDefeated(); } break; default: throw new ArgumentOutOfRangeException(); } } void CheckForGameOver() { // Check the life state of all players in the scene foreach (var serverCharacter in PlayerServerCharacter.GetPlayerServerCharacters()) { // if any player is alive just return if (serverCharacter && serverCharacter.LifeState == LifeState.Alive) { return; } } // If we made it this far, all players are down! switch to post game StartCoroutine(CoroGameOver(k_LoseDelay, false)); } void BossDefeated() { // Boss is dead - set game won to true StartCoroutine(CoroGameOver(k_WinDelay, true)); } IEnumerator CoroGameOver(float wait, bool gameWon) { m_PersistentGameState.SetWinState(gameWon ? WinState.Win : WinState.Loss); // wait 5 seconds for game animations to finish yield return new WaitForSeconds(wait); SceneLoaderWrapper.Instance.LoadScene("PostGame", useNetworkSceneManager: true); } } }