365 lines
13 KiB
C#
365 lines
13 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
using MegaKoop.Game;
|
|
|
|
#if STEAMWORKSNET
|
|
using Steamworks;
|
|
#endif
|
|
|
|
namespace MegaKoop.Game.Networking
|
|
{
|
|
/// <summary>
|
|
/// Handles transitioning from the lobby into the character scene and spawning player avatars.
|
|
/// Lives alongside the Steam services object so it survives scene loads.
|
|
/// </summary>
|
|
[DefaultExecutionOrder(-500)]
|
|
public class LobbyGameSceneCoordinator : MonoBehaviour
|
|
{
|
|
private static LobbyGameSceneCoordinator Instance;
|
|
|
|
[SerializeField] private string characterSceneName = "CharacterScene";
|
|
[SerializeField] private float spawnRadius = 3f;
|
|
[SerializeField] private float minimumSpawnSpacing = 2.5f;
|
|
|
|
private readonly List<LobbyPlayerInfo> pendingPlayers = new();
|
|
private string localSteamId = string.Empty;
|
|
private bool loadPending;
|
|
private bool hasSpawned;
|
|
|
|
private void Awake()
|
|
{
|
|
// Singleton to avoid double spawners (e.g., one from DontDestroyOnLoad and one from scene contents)
|
|
if (Instance != null && Instance != this)
|
|
{
|
|
// Another coordinator already exists and persists; remove this duplicate.
|
|
Destroy(gameObject);
|
|
return;
|
|
}
|
|
Instance = this;
|
|
DontDestroyOnLoad(gameObject);
|
|
SceneManager.sceneLoaded += HandleSceneLoaded;
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
SceneManager.sceneLoaded -= HandleSceneLoaded;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Captures the current lobby membership and moves everyone into the character scene.
|
|
/// </summary>
|
|
public void BeginGame(MegaKoop.Steam.SteamLobbyService steam)
|
|
{
|
|
if (steam == null)
|
|
{
|
|
Debug.LogWarning("[LobbyGameSceneCoordinator] SteamLobbyService missing; cannot begin game.");
|
|
return;
|
|
}
|
|
|
|
CapturePlayers(steam);
|
|
|
|
if (!string.Equals(SceneManager.GetActiveScene().name, characterSceneName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
loadPending = true;
|
|
hasSpawned = false;
|
|
SceneManager.LoadScene(characterSceneName);
|
|
}
|
|
else
|
|
{
|
|
// Already in the target scene (e.g., during hot reload). Avoid double-spawn.
|
|
if (hasSpawned)
|
|
{
|
|
Debug.Log("[LobbyGameSceneCoordinator] BeginGame called but players already spawned; ignoring.");
|
|
return;
|
|
}
|
|
loadPending = false;
|
|
hasSpawned = false;
|
|
SpawnPlayersInScene(SceneManager.GetActiveScene());
|
|
}
|
|
}
|
|
|
|
private void CapturePlayers(MegaKoop.Steam.SteamLobbyService steam)
|
|
{
|
|
pendingPlayers.Clear();
|
|
localSteamId = steam.LocalSteamIdString ?? string.Empty;
|
|
|
|
var members = steam.GetMembers();
|
|
if (members != null)
|
|
{
|
|
foreach (var member in members)
|
|
{
|
|
pendingPlayers.Add(new LobbyPlayerInfo
|
|
{
|
|
SteamId = member.steamId,
|
|
DisplayName = string.IsNullOrWhiteSpace(member.name) ? "Player" : member.name,
|
|
IsLocal = member.steamId == localSteamId
|
|
});
|
|
}
|
|
}
|
|
|
|
// Ensure the local player is represented even if lobby data is empty (e.g., offline stub).
|
|
if (!pendingPlayers.Exists(p => p.IsLocal))
|
|
{
|
|
pendingPlayers.Add(new LobbyPlayerInfo
|
|
{
|
|
SteamId = localSteamId,
|
|
DisplayName = GetFallbackLocalName(),
|
|
IsLocal = true
|
|
});
|
|
}
|
|
|
|
pendingPlayers.Sort((a, b) => string.CompareOrdinal(a.SteamId ?? string.Empty, b.SteamId ?? string.Empty));
|
|
}
|
|
|
|
private void HandleSceneLoaded(Scene scene, LoadSceneMode mode)
|
|
{
|
|
if (!string.Equals(scene.name, characterSceneName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!loadPending && hasSpawned)
|
|
{
|
|
return;
|
|
}
|
|
|
|
loadPending = false;
|
|
hasSpawned = false;
|
|
SpawnPlayersInScene(scene);
|
|
}
|
|
|
|
private void SpawnPlayersInScene(Scene scene)
|
|
{
|
|
if (hasSpawned)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var template = FindWizardTemplate(scene);
|
|
if (template == null)
|
|
{
|
|
Debug.LogWarning("[LobbyGameSceneCoordinator] Wizard template not found in CharacterScene; cannot spawn players.");
|
|
return;
|
|
}
|
|
|
|
hasSpawned = true;
|
|
|
|
// Proactively remove any previously spawned character clones (e.g., from a duplicate coordinator)
|
|
DespawnExistingClones(scene, template);
|
|
|
|
Vector3 basePosition = template.transform.position;
|
|
Quaternion baseRotation = template.transform.rotation;
|
|
Transform parent = template.transform.parent;
|
|
|
|
template.SetActive(false);
|
|
|
|
int total = Mathf.Max(1, pendingPlayers.Count);
|
|
for (int i = 0; i < total; i++)
|
|
{
|
|
LobbyPlayerInfo info = i < pendingPlayers.Count
|
|
? pendingPlayers[i]
|
|
: new LobbyPlayerInfo { DisplayName = $"Player {i + 1}", IsLocal = i == 0 };
|
|
|
|
Vector3 spawnPosition = CalculateSpawnPosition(basePosition, i, total);
|
|
var clone = Instantiate(template, spawnPosition, baseRotation, parent);
|
|
clone.name = BuildPlayerName(info, i);
|
|
|
|
ConfigureCloneForPlayer(clone, info, i);
|
|
|
|
clone.SetActive(true);
|
|
}
|
|
}
|
|
|
|
private static void DespawnExistingClones(Scene scene, GameObject template)
|
|
{
|
|
// Destroy any GameObjects in the scene that look like spawned player avatars, excluding the template hierarchy.
|
|
foreach (var root in scene.GetRootGameObjects())
|
|
{
|
|
// Skip the template root itself
|
|
if (root == template) continue;
|
|
|
|
// Heuristic: if it has a SteamCharacterNetworkBridge or ThirdPersonCharacterController, treat it as a character clone
|
|
var bridge = root.GetComponentInChildren<SteamCharacterNetworkBridge>(true);
|
|
var tpc = root.GetComponentInChildren<ThirdPersonCharacterController>(true);
|
|
if (bridge != null || tpc != null)
|
|
{
|
|
// Avoid deleting the template if nested under a different root (shouldn't happen)
|
|
if (!template.transform.IsChildOf(root.transform) && !root.transform.IsChildOf(template.transform))
|
|
{
|
|
UnityEngine.Object.Destroy(root);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static GameObject FindWizardTemplate(Scene scene)
|
|
{
|
|
foreach (var root in scene.GetRootGameObjects())
|
|
{
|
|
if (root.name.Equals("Wizard", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return root;
|
|
}
|
|
|
|
var controller = root.GetComponentInChildren<ThirdPersonCharacterController>(true);
|
|
if (controller != null)
|
|
{
|
|
return controller.gameObject;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private Vector3 CalculateSpawnPosition(Vector3 center, int index, int total)
|
|
{
|
|
if (total <= 1)
|
|
{
|
|
return center;
|
|
}
|
|
|
|
float radiusForRing = Mathf.Max(spawnRadius, minimumSpawnSpacing) * Mathf.Ceil((index + 1) / 6f);
|
|
float angle = (Mathf.PI * 2f / Mathf.Max(1, Mathf.Min(total, 6))) * (index % 6);
|
|
Vector3 offset = new Vector3(Mathf.Cos(angle), 0f, Mathf.Sin(angle)) * radiusForRing;
|
|
return center + offset;
|
|
}
|
|
|
|
private void ConfigureCloneForPlayer(GameObject clone, LobbyPlayerInfo info, int index)
|
|
{
|
|
// Ensure network identity is deterministic across clients.
|
|
var identity = clone.GetComponent<NetworkIdentity>() ?? clone.AddComponent<NetworkIdentity>();
|
|
identity.SetNetworkId(index + 1);
|
|
|
|
var bridge = clone.GetComponent<SteamCharacterNetworkBridge>() ?? clone.AddComponent<SteamCharacterNetworkBridge>();
|
|
bridge.AssignOwner(ParseSteamId(info.SteamId), info.IsLocal);
|
|
|
|
var inputSender = clone.GetComponent<SteamLocalInputSender>() ?? clone.AddComponent<SteamLocalInputSender>();
|
|
inputSender.enabled = info.IsLocal;
|
|
|
|
// Local player keeps the camera and audio; remote avatars are visual only.
|
|
var cameras = clone.GetComponentsInChildren<Camera>(true);
|
|
foreach (var camera in cameras)
|
|
{
|
|
camera.enabled = info.IsLocal;
|
|
if (!info.IsLocal)
|
|
{
|
|
camera.gameObject.SetActive(false);
|
|
}
|
|
}
|
|
|
|
var listeners = clone.GetComponentsInChildren<AudioListener>(true);
|
|
foreach (var listener in listeners)
|
|
{
|
|
listener.enabled = info.IsLocal;
|
|
if (!info.IsLocal)
|
|
{
|
|
listener.gameObject.SetActive(false);
|
|
}
|
|
}
|
|
|
|
var thirdPersonCamera = clone.GetComponentInChildren<ThirdPersonCamera>(true);
|
|
if (thirdPersonCamera != null)
|
|
{
|
|
thirdPersonCamera.gameObject.SetActive(info.IsLocal);
|
|
thirdPersonCamera.enabled = info.IsLocal;
|
|
if (info.IsLocal)
|
|
{
|
|
thirdPersonCamera.SetTarget(clone.transform);
|
|
}
|
|
}
|
|
|
|
var controller = clone.GetComponent<ThirdPersonCharacterController>();
|
|
if (!info.IsLocal && controller != null)
|
|
{
|
|
controller.SetCameraTransform(null);
|
|
}
|
|
|
|
// Ensure only the local player's camera remains active and is MainCamera
|
|
if (info.IsLocal)
|
|
{
|
|
foreach (var cam in cameras)
|
|
{
|
|
cam.tag = "MainCamera";
|
|
cam.enabled = true;
|
|
cam.gameObject.SetActive(true);
|
|
}
|
|
|
|
// Disable any other active cameras that are not part of this player
|
|
foreach (var otherCam in UnityEngine.Object.FindObjectsOfType<Camera>())
|
|
{
|
|
if (!otherCam.transform.IsChildOf(clone.transform))
|
|
{
|
|
otherCam.enabled = false;
|
|
}
|
|
}
|
|
|
|
// Ensure there is exactly one enabled AudioListener (on this player)
|
|
foreach (var otherListener in UnityEngine.Object.FindObjectsOfType<AudioListener>())
|
|
{
|
|
if (!otherListener.transform.IsChildOf(clone.transform))
|
|
{
|
|
otherListener.enabled = false;
|
|
}
|
|
}
|
|
|
|
// Bind the character controller to this camera for input-relative movement
|
|
if (controller != null)
|
|
{
|
|
Transform camTransform = thirdPersonCamera != null ? thirdPersonCamera.transform : (Camera.main != null ? Camera.main.transform : null);
|
|
if (camTransform != null)
|
|
{
|
|
controller.SetCameraTransform(camTransform);
|
|
}
|
|
}
|
|
|
|
if (inputSender != null && thirdPersonCamera != null)
|
|
{
|
|
inputSender.SetCameraTransform(thirdPersonCamera.transform);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private static string BuildPlayerName(LobbyPlayerInfo info, int index)
|
|
{
|
|
string baseName = !string.IsNullOrWhiteSpace(info.DisplayName) ? info.DisplayName : $"Player {index + 1}";
|
|
return $"Wizard_{baseName}";
|
|
}
|
|
|
|
private static string GetFallbackLocalName()
|
|
{
|
|
#if STEAMWORKSNET
|
|
if (SteamBootstrap.IsInitialized)
|
|
{
|
|
return SteamFriends.GetPersonaName();
|
|
}
|
|
#endif
|
|
return "You";
|
|
}
|
|
|
|
private static ulong ParseSteamId(string steamId)
|
|
{
|
|
if (string.IsNullOrEmpty(steamId))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (ulong.TryParse(steamId, out var value))
|
|
{
|
|
return value;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
[Serializable]
|
|
private struct LobbyPlayerInfo
|
|
{
|
|
public string SteamId;
|
|
public string DisplayName;
|
|
public bool IsLocal;
|
|
}
|
|
}
|
|
}
|