You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
279 lines
11 KiB
C#
279 lines
11 KiB
C#
5 days ago
|
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
|
||
|
{
|
||
|
/// <summary>
|
||
|
/// Server specialization of core BossRoom game logic.
|
||
|
/// </summary>
|
||
|
[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<Transform> 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;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Has the ServerBossRoomState already hit its initial spawn? (i.e. spawned players following load from character select).
|
||
|
/// </summary>
|
||
|
public bool InitialSpawnDone { get; private set; }
|
||
|
|
||
|
/// <summary>
|
||
|
/// Keeping the subscriber during this GameState's lifetime to allow disposing of subscription and re-subscribing
|
||
|
/// when despawning and spawning again.
|
||
|
/// </summary>
|
||
|
[Inject] ISubscriber<LifeStateChangedEventMessage> 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<SessionPlayerData>.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<ulong> clientsCompleted, List<ulong> 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<Transform>(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<ServerCharacter>();
|
||
|
|
||
|
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<SessionPlayerData>.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<NetworkGuid>(persistentPlayer.NetworkAvatarGuidState.AvatarGuid.Value);
|
||
|
|
||
|
// pass name from persistent player to avatar
|
||
|
if (newPlayer.TryGetComponent(out NetworkNameState networkNameState))
|
||
|
{
|
||
|
networkNameState.Name = new NetworkVariable<FixedPlayerName>(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);
|
||
|
}
|
||
|
}
|
||
|
}
|