From 7cec95b8a0e73e0e9b8c872a5f0c5f44a10e530a Mon Sep 17 00:00:00 2001 From: Marek Sorokin Date: Sun, 5 Oct 2025 19:15:37 +0200 Subject: [PATCH] online game working --- .../Networking/LobbyGameSceneCoordinator.cs | 252 ++++++++++++++++++ .../LobbyGameSceneCoordinator.cs.meta | 2 + Settings/Build Profiles.meta | 8 + Settings/Build Profiles/Linux.asset | 44 +++ Settings/Build Profiles/Linux.asset.meta | 8 + UI/Scripts/UGUIMultiplayerLobbyController.cs | 34 ++- 6 files changed, 344 insertions(+), 4 deletions(-) create mode 100644 Game/Scripts/Networking/LobbyGameSceneCoordinator.cs create mode 100644 Game/Scripts/Networking/LobbyGameSceneCoordinator.cs.meta create mode 100644 Settings/Build Profiles.meta create mode 100644 Settings/Build Profiles/Linux.asset create mode 100644 Settings/Build Profiles/Linux.asset.meta diff --git a/Game/Scripts/Networking/LobbyGameSceneCoordinator.cs b/Game/Scripts/Networking/LobbyGameSceneCoordinator.cs new file mode 100644 index 0000000..0b24851 --- /dev/null +++ b/Game/Scripts/Networking/LobbyGameSceneCoordinator.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.SceneManagement; +using MegaKoop.Game; + +#if STEAMWORKSNET +using Steamworks; +#endif + +namespace MegaKoop.Game.Networking +{ + /// + /// Handles transitioning from the lobby into the character scene and spawning player avatars. + /// Lives alongside the Steam services object so it survives scene loads. + /// + [DefaultExecutionOrder(-500)] + public class LobbyGameSceneCoordinator : MonoBehaviour + { + [SerializeField] private string characterSceneName = "CharacterScene"; + [SerializeField] private float spawnRadius = 3f; + [SerializeField] private float minimumSpawnSpacing = 2.5f; + + private readonly List pendingPlayers = new(); + private string localSteamId = string.Empty; + private bool loadPending; + private bool hasSpawned; + + private void Awake() + { + DontDestroyOnLoad(gameObject); + SceneManager.sceneLoaded += HandleSceneLoaded; + } + + private void OnDestroy() + { + SceneManager.sceneLoaded -= HandleSceneLoaded; + } + + /// + /// Captures the current lobby membership and moves everyone into the character scene. + /// + 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). + 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 + }); + } + } + + 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; + + Vector3 basePosition = template.transform.position; + Quaternion baseRotation = template.transform.rotation; + Transform parent = template.transform.parent; + + // Hide template so only spawned copies are visible. + 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); + + clone.SetActive(true); + } + } + + 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(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) + { + var controller = clone.GetComponent(); + if (controller != null) + { + controller.enabled = info.IsLocal; + } + + // Local player keeps the camera and audio; remote avatars are visual only. + var cameras = clone.GetComponentsInChildren(true); + foreach (var camera in cameras) + { + camera.enabled = info.IsLocal; + if (!info.IsLocal) + { + camera.gameObject.SetActive(false); + } + } + + var listeners = clone.GetComponentsInChildren(true); + foreach (var listener in listeners) + { + listener.enabled = info.IsLocal; + if (!info.IsLocal) + { + listener.gameObject.SetActive(false); + } + } + + var thirdPersonCamera = clone.GetComponentInChildren(true); + if (thirdPersonCamera != null) + { + thirdPersonCamera.enabled = info.IsLocal; + if (info.IsLocal) + { + thirdPersonCamera.SetTarget(clone.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"; + } + + [Serializable] + private struct LobbyPlayerInfo + { + public string SteamId; + public string DisplayName; + public bool IsLocal; + } + } +} diff --git a/Game/Scripts/Networking/LobbyGameSceneCoordinator.cs.meta b/Game/Scripts/Networking/LobbyGameSceneCoordinator.cs.meta new file mode 100644 index 0000000..107f646 --- /dev/null +++ b/Game/Scripts/Networking/LobbyGameSceneCoordinator.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 46f2c531c179d29e39aa831ff5290c20 \ No newline at end of file diff --git a/Settings/Build Profiles.meta b/Settings/Build Profiles.meta new file mode 100644 index 0000000..5f540be --- /dev/null +++ b/Settings/Build Profiles.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: eda40d14141ed13b99eb74bd3cbb962b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Settings/Build Profiles/Linux.asset b/Settings/Build Profiles/Linux.asset new file mode 100644 index 0000000..7417bc7 --- /dev/null +++ b/Settings/Build Profiles/Linux.asset @@ -0,0 +1,44 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 15003, guid: 0000000000000000e000000000000000, type: 0} + m_Name: Linux + m_EditorClassIdentifier: UnityEditor.dll::UnityEditor.Build.Profile.BuildProfile + m_AssetVersion: 1 + m_BuildTarget: 24 + m_Subtarget: 2 + m_PlatformId: cb423bfea44b4d658edb8bc5d91a3024 + m_PlatformBuildProfile: + rid: 8527041553903386862 + m_OverrideGlobalSceneList: 0 + m_Scenes: [] + m_ScriptingDefines: [] + m_PlayerSettingsYaml: + m_Settings: [] + references: + version: 2 + RefIds: + - rid: 8527041553903386862 + type: {class: LinuxPlatformSettings, ns: UnityEditor.LinuxStandalone, asm: UnityEditor.LinuxStandalone.Extensions} + data: + m_Development: 0 + m_ConnectProfiler: 0 + m_BuildWithDeepProfilingSupport: 0 + m_AllowDebugging: 0 + m_WaitForManagedDebugger: 0 + m_ManagedDebuggerFixedPort: 0 + m_ExplicitNullChecks: 0 + m_ExplicitDivideByZeroChecks: 0 + m_ExplicitArrayBoundsChecks: 0 + m_CompressionType: -1 + m_InstallInBuildFolder: 0 + m_InsightsSettingsContainer: + m_BuildProfileEngineDiagnosticsState: 2 diff --git a/Settings/Build Profiles/Linux.asset.meta b/Settings/Build Profiles/Linux.asset.meta new file mode 100644 index 0000000..aa07c6b --- /dev/null +++ b/Settings/Build Profiles/Linux.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 05f210c1ba43a51c6a548f8030a4cfec +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/UI/Scripts/UGUIMultiplayerLobbyController.cs b/UI/Scripts/UGUIMultiplayerLobbyController.cs index d4ff621..f3b0064 100644 --- a/UI/Scripts/UGUIMultiplayerLobbyController.cs +++ b/UI/Scripts/UGUIMultiplayerLobbyController.cs @@ -5,6 +5,7 @@ using UnityEngine.UI; using UnityEngine.EventSystems; using MegaKoop.Steam; using MegaKoop.Networking; +using MegaKoop.Game.Networking; using TMPro; namespace MegaKoop.UI @@ -77,6 +78,7 @@ namespace MegaKoop.UI // Steam service private SteamLobbyService steam; + private LobbyGameSceneCoordinator lobbyGameCoordinator; // Local state cache private bool IsInLobby => steam != null && steam.IsInLobby; @@ -247,6 +249,7 @@ namespace MegaKoop.UI if (steam.IsStartSignaled() && !clientStartedFromSignal) { clientStartedFromSignal = true; + lobbyGameCoordinator?.BeginGame(steam); #if UNITY_NETCODE || NETCODE_PRESENT // Configure transport with host SteamID before starting client var adapter = GetSteamAdapter(); @@ -327,6 +330,8 @@ namespace MegaKoop.UI #region Steam private void EnsureSteamServices() { + GameObject servicesRoot = null; + if (steam == null) { // Unity 2023+: use FindFirstObjectByType; older: FindObjectOfType @@ -335,14 +340,34 @@ namespace MegaKoop.UI #else steam = Object.FindObjectOfType(); #endif + if (steam != null) + { + servicesRoot = steam.gameObject; + } + if (steam == null) { - var go = GameObject.Find("SteamServices") ?? new GameObject("SteamServices"); - if (go.GetComponent() == null) go.AddComponent(); - steam = go.GetComponent() ?? go.AddComponent(); - DontDestroyOnLoad(go); + servicesRoot = GameObject.Find("SteamServices") ?? new GameObject("SteamServices"); + if (servicesRoot.GetComponent() == null) + { + servicesRoot.AddComponent(); + } + steam = servicesRoot.GetComponent() ?? servicesRoot.AddComponent(); + DontDestroyOnLoad(servicesRoot); } } + + if (steam != null) + { + servicesRoot ??= steam.gameObject; + + if (servicesRoot.GetComponent() == null) + { + servicesRoot.AddComponent(); + } + + lobbyGameCoordinator = servicesRoot.GetComponent() ?? servicesRoot.AddComponent(); + } } private void RegisterSteamEvents() @@ -625,6 +650,7 @@ namespace MegaKoop.UI return; } steam?.StartGameSignal(); + lobbyGameCoordinator?.BeginGame(steam); #if UNITY_NETCODE || NETCODE_PRESENT // Optionally start host and load scene var adapter = GetSteamAdapter();