From b8c6c1bf740ca73bdd82af9912af447a8c7cf458 Mon Sep 17 00:00:00 2001 From: Jay <96156114+jaydev00a@users.noreply.github.com> Date: Wed, 26 Mar 2025 02:06:46 +0900 Subject: [PATCH] =?UTF-8?q?DO-58=20[Feat]=20=EB=A9=80=ED=8B=B0=20=ED=94=8C?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=20=ED=85=9C=ED=94=8C=EB=A6=BF=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20ParrelSync=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/Plugins/ParrelSync.meta | 8 + .../Plugins/ParrelSync/ScriptableObjects.meta | 8 + .../ParrelSyncProjectSettings.asset | 15 ++ .../ParrelSyncProjectSettings.asset.meta | 8 + .../Utility/UnityMainThreadDispatcher.prefab | 46 ++++ .../UnityMainThreadDispatcher.prefab.meta | 7 + Assets/Scenes/Game.unity | 58 +++++ Assets/Script/Common/AudioManager.cs | 10 +- Assets/Script/Common/Constants.cs | 10 + .../Common/UnityMainThreadDispatcher.cs | 121 ++++++++++ .../Common/UnityMainThreadDispatcher.cs.meta | 3 + Assets/Script/Game/GameLogic.cs | 214 +++++++++++++++++- Assets/Script/Game/GameManager.cs | 3 + Assets/Script/Game/MultiplayManager.cs | 195 ++++++++++++++++ Assets/Script/Game/MultiplayManager.cs.meta | 3 + Assets/Script/Main/LoadingPanelController.cs | 3 +- Assets/Script/Main/MainPanelController.cs | 3 +- .../Script/UI/PanelController/PanelManager.cs | 23 +- Packages/manifest.json | 2 + Packages/packages-lock.json | 23 ++ 20 files changed, 752 insertions(+), 11 deletions(-) create mode 100644 Assets/Plugins/ParrelSync.meta create mode 100644 Assets/Plugins/ParrelSync/ScriptableObjects.meta create mode 100644 Assets/Plugins/ParrelSync/ScriptableObjects/ParrelSyncProjectSettings.asset create mode 100644 Assets/Plugins/ParrelSync/ScriptableObjects/ParrelSyncProjectSettings.asset.meta create mode 100644 Assets/Resources/Prefabs/Utility/UnityMainThreadDispatcher.prefab create mode 100644 Assets/Resources/Prefabs/Utility/UnityMainThreadDispatcher.prefab.meta create mode 100644 Assets/Script/Common/UnityMainThreadDispatcher.cs create mode 100644 Assets/Script/Common/UnityMainThreadDispatcher.cs.meta create mode 100644 Assets/Script/Game/MultiplayManager.cs create mode 100644 Assets/Script/Game/MultiplayManager.cs.meta diff --git a/Assets/Plugins/ParrelSync.meta b/Assets/Plugins/ParrelSync.meta new file mode 100644 index 0000000..e9be2ff --- /dev/null +++ b/Assets/Plugins/ParrelSync.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 49e291f62c68bcb4b8ae5c0b9a87f98d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/ParrelSync/ScriptableObjects.meta b/Assets/Plugins/ParrelSync/ScriptableObjects.meta new file mode 100644 index 0000000..2aac655 --- /dev/null +++ b/Assets/Plugins/ParrelSync/ScriptableObjects.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a7530dff3f8b9a74081055b376b00dc7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/ParrelSync/ScriptableObjects/ParrelSyncProjectSettings.asset b/Assets/Plugins/ParrelSync/ScriptableObjects/ParrelSyncProjectSettings.asset new file mode 100644 index 0000000..0ec926c --- /dev/null +++ b/Assets/Plugins/ParrelSync/ScriptableObjects/ParrelSyncProjectSettings.asset @@ -0,0 +1,15 @@ +%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: 11500000, guid: c0011418c9d75434988a06b6df93b283, type: 3} + m_Name: ParrelSyncProjectSettings + m_EditorClassIdentifier: + m_OptionalSymbolicLinkFolders: [] diff --git a/Assets/Plugins/ParrelSync/ScriptableObjects/ParrelSyncProjectSettings.asset.meta b/Assets/Plugins/ParrelSync/ScriptableObjects/ParrelSyncProjectSettings.asset.meta new file mode 100644 index 0000000..fde75d0 --- /dev/null +++ b/Assets/Plugins/ParrelSync/ScriptableObjects/ParrelSyncProjectSettings.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 87e010131d20c0f4f8c3a9fe6d9bcdfb +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Resources/Prefabs/Utility/UnityMainThreadDispatcher.prefab b/Assets/Resources/Prefabs/Utility/UnityMainThreadDispatcher.prefab new file mode 100644 index 0000000..0eb94f6 --- /dev/null +++ b/Assets/Resources/Prefabs/Utility/UnityMainThreadDispatcher.prefab @@ -0,0 +1,46 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &6968746470133643514 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2544285477164602481} + - component: {fileID: 450855533396324322} + m_Layer: 0 + m_Name: UnityMainThreadDispatcher + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &2544285477164602481 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6968746470133643514} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &450855533396324322 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6968746470133643514} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e7461fd0f3834d9283d0ea00daaaea3b, type: 3} + m_Name: + m_EditorClassIdentifier: diff --git a/Assets/Resources/Prefabs/Utility/UnityMainThreadDispatcher.prefab.meta b/Assets/Resources/Prefabs/Utility/UnityMainThreadDispatcher.prefab.meta new file mode 100644 index 0000000..ea589bc --- /dev/null +++ b/Assets/Resources/Prefabs/Utility/UnityMainThreadDispatcher.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 120a1b7daa97a4247ae154ca3321d3b8 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scenes/Game.unity b/Assets/Scenes/Game.unity index 254abaa..bd4adef 100644 --- a/Assets/Scenes/Game.unity +++ b/Assets/Scenes/Game.unity @@ -47216,6 +47216,63 @@ SpriteRenderer: m_WasSpriteAssigned: 0 m_MaskInteraction: 0 m_SpriteSortPoint: 0 +--- !u!1001 &8064471184320700382 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 2544285477164602481, guid: 120a1b7daa97a4247ae154ca3321d3b8, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2544285477164602481, guid: 120a1b7daa97a4247ae154ca3321d3b8, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2544285477164602481, guid: 120a1b7daa97a4247ae154ca3321d3b8, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2544285477164602481, guid: 120a1b7daa97a4247ae154ca3321d3b8, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 2544285477164602481, guid: 120a1b7daa97a4247ae154ca3321d3b8, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2544285477164602481, guid: 120a1b7daa97a4247ae154ca3321d3b8, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2544285477164602481, guid: 120a1b7daa97a4247ae154ca3321d3b8, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2544285477164602481, guid: 120a1b7daa97a4247ae154ca3321d3b8, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2544285477164602481, guid: 120a1b7daa97a4247ae154ca3321d3b8, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2544285477164602481, guid: 120a1b7daa97a4247ae154ca3321d3b8, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6968746470133643514, guid: 120a1b7daa97a4247ae154ca3321d3b8, type: 3} + propertyPath: m_Name + value: UnityMainThreadDispatcher + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 120a1b7daa97a4247ae154ca3321d3b8, type: 3} --- !u!4 &8068830904452262562 Transform: m_ObjectHideFlags: 0 @@ -54561,3 +54618,4 @@ SceneRoots: - {fileID: 2953084326979075905} - {fileID: 873369031} - {fileID: 1657305406} + - {fileID: 8064471184320700382} diff --git a/Assets/Script/Common/AudioManager.cs b/Assets/Script/Common/AudioManager.cs index a264c4d..f635733 100644 --- a/Assets/Script/Common/AudioManager.cs +++ b/Assets/Script/Common/AudioManager.cs @@ -46,11 +46,17 @@ public class AudioManager : MonoBehaviour public void PlayClickSound() { - audioSource.PlayOneShot(clickSound, sfxVolume); + if (audioSource != null) + { + audioSource.PlayOneShot(clickSound, sfxVolume); + } } public void PlayCloseSound() { - audioSource.PlayOneShot(closeSound, sfxVolume); + if (audioSource != null) + { + audioSource.PlayOneShot(closeSound, sfxVolume); + } } } diff --git a/Assets/Script/Common/Constants.cs b/Assets/Script/Common/Constants.cs index ae029e0..84d60dd 100644 --- a/Assets/Script/Common/Constants.cs +++ b/Assets/Script/Common/Constants.cs @@ -10,4 +10,14 @@ public const int RAING_POINTS = 10; public string[] AI_NAMIES = { "이세돌", "신사동호랭이","진짜인간임","종로3가짱돌","마스터김춘배","62세황순자","고준일 강사님"}; + + public enum MultiplayManagerState + { + CreateRoom, // 방 생성 + JoinRoom, // 생성된 방에 참여 + StartGame, // 생성한 방에 다른 유저가 참여해서 게임 시작 + SwitchAI, // 15초 후 매칭 실패 시 AI 플레이로 전환 알림 + ExitRoom, // 자신이 방을 빠져 나왔을 때 + EndGame // 상대방이 접속을 끊거나 방을 나갔을 때 + }; } \ No newline at end of file diff --git a/Assets/Script/Common/UnityMainThreadDispatcher.cs b/Assets/Script/Common/UnityMainThreadDispatcher.cs new file mode 100644 index 0000000..9e9e932 --- /dev/null +++ b/Assets/Script/Common/UnityMainThreadDispatcher.cs @@ -0,0 +1,121 @@ +/* +Copyright 2015 Pim de Witte All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +using UnityEngine; +using System.Collections; +using System.Collections.Generic; +using System; +using System.Threading.Tasks; + +namespace PimDeWitte.UnityMainThreadDispatcher { + /// Author: Pim de Witte (pimdewitte.com) and contributors, https://github.com/PimDeWitte/UnityMainThreadDispatcher + /// + /// A thread-safe class which holds a queue with actions to execute on the next Update() method. It can be used to make calls to the main thread for + /// things such as UI Manipulation in Unity. It was developed for use in combination with the Firebase Unity plugin, which uses separate threads for event handling + /// + public class UnityMainThreadDispatcher : MonoBehaviour { + + private static readonly Queue _executionQueue = new Queue(); + + public void Update() { + lock(_executionQueue) { + while (_executionQueue.Count > 0) { + _executionQueue.Dequeue().Invoke(); + } + } + } + + /// + /// Locks the queue and adds the IEnumerator to the queue + /// + /// IEnumerator function that will be executed from the main thread. + public void Enqueue(IEnumerator action) { + lock (_executionQueue) { + _executionQueue.Enqueue (() => { + StartCoroutine (action); + }); + } + } + + /// + /// Locks the queue and adds the Action to the queue + /// + /// function that will be executed from the main thread. + public void Enqueue(Action action) + { + Enqueue(ActionWrapper(action)); + } + + /// + /// Locks the queue and adds the Action to the queue, returning a Task which is completed when the action completes + /// + /// function that will be executed from the main thread. + /// A Task that can be awaited until the action completes + public Task EnqueueAsync(Action action) + { + var tcs = new TaskCompletionSource(); + + void WrappedAction() { + try + { + action(); + tcs.TrySetResult(true); + } catch (Exception ex) + { + tcs.TrySetException(ex); + } + } + + Enqueue(ActionWrapper(WrappedAction)); + return tcs.Task; + } + + + IEnumerator ActionWrapper(Action a) + { + a(); + yield return null; + } + + + private static UnityMainThreadDispatcher _instance = null; + + public static bool Exists() { + return _instance != null; + } + + public static UnityMainThreadDispatcher Instance() { + if (!Exists ()) { + throw new Exception ("UnityMainThreadDispatcher could not find the UnityMainThreadDispatcher object. Please ensure you have added the MainThreadExecutor Prefab to your scene."); + } + return _instance; + } + + + void Awake() { + if (_instance == null) { + _instance = this; + DontDestroyOnLoad(this.gameObject); + } + } + + void OnDestroy() { + _instance = null; + } + + + } +} \ No newline at end of file diff --git a/Assets/Script/Common/UnityMainThreadDispatcher.cs.meta b/Assets/Script/Common/UnityMainThreadDispatcher.cs.meta new file mode 100644 index 0000000..9c2b213 --- /dev/null +++ b/Assets/Script/Common/UnityMainThreadDispatcher.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e7461fd0f3834d9283d0ea00daaaea3b +timeCreated: 1742916255 \ No newline at end of file diff --git a/Assets/Script/Game/GameLogic.cs b/Assets/Script/Game/GameLogic.cs index 36e120c..2d59f55 100644 --- a/Assets/Script/Game/GameLogic.cs +++ b/Assets/Script/Game/GameLogic.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; +using PimDeWitte.UnityMainThreadDispatcher; public abstract class BasePlayerState { @@ -54,10 +55,32 @@ public class PlayerState : BasePlayerState { private Enums.PlayerType _playerType; private bool _isFirstPlayer; + + private MultiplayManager _multiplayManager; + private string _roomId; + private bool _isMultiplay; + public PlayerState(bool isFirstPlayer) { _isFirstPlayer = isFirstPlayer; _playerType = isFirstPlayer ? Enums.PlayerType.PlayerA : Enums.PlayerType.PlayerB; + _isMultiplay = false; + } + + public PlayerState(bool isFirstPlayer, MultiplayManager multiplayManager, JoinRoomData data) + : this(isFirstPlayer) + { + _multiplayManager = multiplayManager; + _roomId = data.roomId; + _isMultiplay = true; + } + + public PlayerState(bool isFirstPlayer, MultiplayManager multiplayManager, string roomId) + : this(isFirstPlayer) + { + _multiplayManager = multiplayManager; + _roomId = roomId; + _isMultiplay = true; } public override void OnEnter(GameLogic gameLogic) @@ -88,6 +111,11 @@ public class PlayerState : BasePlayerState public override void HandleMove(GameLogic gameLogic, int row, int col) { gameLogic.SetStoneSelectedState(row, col); + if (_isMultiplay) + { + Debug.Log("row: " + row + "col: " + col); + _multiplayManager.SendPlayerMove(_roomId, new Vector2Int(row, col)); + } } public override void HandleNextTurn(GameLogic gameLogic) @@ -132,24 +160,64 @@ public class AIState: BasePlayerState } public class MultiPlayerState: BasePlayerState { + private Enums.PlayerType _playerType; + private bool _isFirstPlayer; + + private MultiplayManager _multiplayManager; + + public MultiPlayerState(bool isFirstPlayer, MultiplayManager multiplayManager) + { + _isFirstPlayer = isFirstPlayer; + _playerType = isFirstPlayer ? Enums.PlayerType.PlayerA : Enums.PlayerType.PlayerB; + _multiplayManager = multiplayManager; + } + public override void OnEnter(GameLogic gameLogic) { gameLogic.fioTimer.StartTimer(); + //TODO: 첫번째 플레이어면 렌주 룰 확인 + #region Renju Turn Set + // 턴이 변경될 때마다 금수 위치 업데이트 + gameLogic.UpdateForbiddenMoves(); + #endregion + + // gameLogic.currentTurn = _playerType; + // gameLogic.stoneController.OnStoneClickedDelegate = (row, col) => + // { + // HandleMove(gameLogic, row, col); + // }; + _multiplayManager.OnOpponentMove = moveData => + { + var row = moveData.position.x; + var col = moveData.position.y; + UnityThread.executeInUpdate(() => + { + HandleMove(gameLogic, row, col); + }); + }; } public override void OnExit(GameLogic gameLogic) { gameLogic.fioTimer.InitTimer(); + _multiplayManager.OnOpponentMove = null; } public override void HandleMove(GameLogic gameLogic, int row, int col) { - + ProcessMove(gameLogic, _playerType, row, col); } public override void HandleNextTurn(GameLogic gameLogic) { - + if (_isFirstPlayer) + { + gameLogic.SetState(gameLogic.secondPlayerState); + } + else + { + gameLogic.SetState(gameLogic.firstPlayerState); + } } } @@ -166,6 +234,7 @@ public class GameLogic : MonoBehaviour public BasePlayerState firstPlayerState; public BasePlayerState secondPlayerState; private BasePlayerState _currentPlayerState; + //타이머 public FioTimer fioTimer; @@ -176,6 +245,9 @@ public class GameLogic : MonoBehaviour private int _lastRow; private int _lastCol; + private MultiplayManager _multiplayManager; + private string _roomId; + #region Renju Members // 렌주룰 금수 검사기 private RenjuForbiddenMoveDetector _forbiddenDetector; @@ -242,14 +314,148 @@ public class GameLogic : MonoBehaviour SetState(firstPlayerState); break; case Enums.GameType.MultiPlay: - //TODO: 멀티 구현 필요 - ReplayManager.Instance.InitReplayData("PlayerA","nicknameB"); + // 메인 스레드에서 실행 - UI 업데이트는 메인 스레드에서 실행 필요 + UnityMainThreadDispatcher.Instance().Enqueue(() => + { + GameManager.Instance.panelManager.OpenLoadingPanel(true, true); + }); + _multiplayManager = new MultiplayManager((state, data) => + { + switch (state) + { + case Constants.MultiplayManagerState.CreateRoom: + Debug.Log("## Create Room"); + _roomId = data as string; + break; + case Constants.MultiplayManagerState.JoinRoom: + Debug.Log("## Join Room"); + var joinRoomData = data as JoinRoomData; + if (joinRoomData == null) + { + Debug.Log("Join Room 응답값이 null 입니다"); + return; + } + + // TODO: 선공, 후공 처리 + if (joinRoomData.isBlack) + { + } + + firstPlayerState = new MultiPlayerState(true, _multiplayManager); + secondPlayerState = new PlayerState(false, _multiplayManager, joinRoomData); + + // 메인 스레드에서 실행 - UI 업데이트는 메인 스레드에서 실행 필요 + UnityMainThreadDispatcher.Instance().Enqueue(() => + { + GameManager.Instance.InitPlayersName(UserManager.Instance.Nickname, joinRoomData.opponentNickname); + GameManager.Instance.InitProfileImages(UserManager.Instance.imageIndex, joinRoomData.opponentImageIndex); + + // 리플레이 데이터 업데이트 + ReplayManager.Instance.InitReplayData(UserManager.Instance.Nickname, joinRoomData.opponentNickname, UserManager.Instance.imageIndex, joinRoomData.opponentImageIndex); + + // 로딩 패널 열려있으면 닫기 + GameManager.Instance.panelManager.CloseLoadingPanel(); + + // 게임 시작 + SetState(firstPlayerState); + }); + break; + case Constants.MultiplayManagerState.SwitchAI: + Debug.Log("## Switching to AI Mode"); + SwitchToSinglePlayer(); + break; + case Constants.MultiplayManagerState.StartGame: + Debug.Log("## Start Game"); + var startGameData = data as StartGameData; + if (startGameData == null) + { + Debug.Log("Start Game 응답값이 null 입니다"); + return; + } + + // TODO: 선공, 후공 처리 + if (startGameData.isBlack) + { + } + firstPlayerState = new PlayerState(true, _multiplayManager, _roomId); + secondPlayerState = new MultiPlayerState(false, _multiplayManager); + + // 메인 스레드에서 실행 - UI 업데이트는 메인 스레드에서 실행 필요 + UnityMainThreadDispatcher.Instance().Enqueue(() => + { + GameManager.Instance.InitPlayersName(UserManager.Instance.Nickname, startGameData.opponentNickname); + GameManager.Instance.InitProfileImages(UserManager.Instance.imageIndex, startGameData.opponentImageIndex); + + // 리플레이 데이터 업데이트 + ReplayManager.Instance.InitReplayData(UserManager.Instance.Nickname, startGameData.opponentNickname, UserManager.Instance.imageIndex, startGameData.opponentImageIndex); + + // 로딩 패널 열려있으면 닫기 + GameManager.Instance.panelManager.CloseLoadingPanel(); + + // 게임 시작 + SetState(firstPlayerState); + }); + break; + case Constants.MultiplayManagerState.ExitRoom: + Debug.Log("## Exit Room"); + // TODO: Exit Room 처리 + break; + case Constants.MultiplayManagerState.EndGame: + Debug.Log("## End Game"); + // TODO: End Room 처리 + break; + } + ReplayManager.Instance.InitReplayData(UserManager.Instance.Nickname,"nicknameB"); + + }); + _multiplayManager.RegisterPlayer(UserManager.Instance.Nickname, UserManager.Instance.Rating, UserManager.Instance.imageIndex); break; case Enums.GameType.Replay: //TODO: 리플레이 구현 break; } } + + public void SwitchToSinglePlayer() + { + _multiplayManager?.Dispose(); + + // 기존 멀티플레이 상태 초기화 + _multiplayManager = null; + _roomId = null; + + // 싱글 플레이 상태로 변경 + firstPlayerState = new PlayerState(true); + secondPlayerState = new AIState(); + // AI 난이도 설정(급수 설정) + OmokAI.Instance.SetRating(UserManager.Instance.Rating); + + // 메인 스레드에서 실행 - UI 업데이트는 메인 스레드에서 실행 필요 + UnityMainThreadDispatcher.Instance().Enqueue(() => + { + // 스레드 확인 로그: 추후 디버깅 시 필요할 수 있을 것 같아 남겨둡니다 + // Debug.Log($"[UnityMainThreadDispatcher] 실행 스레드: {System.Threading.Thread.CurrentThread.ManagedThreadId}"); + // UI 업데이트 + GameManager.Instance.InitPlayersName(UserManager.Instance.Nickname, "AIPlayer"); + GameManager.Instance.InitProfileImages(UserManager.Instance.imageIndex, 1); + + // 리플레이 데이터 업데이트 + ReplayManager.Instance.InitReplayData(UserManager.Instance.Nickname, "PlayerAI", UserManager.Instance.imageIndex, 1); + + // 로딩 패널 열려있으면 닫기 + GameManager.Instance.panelManager.CloseLoadingPanel(); + + // 첫 번째 플레이어(유저)부터 시작 + SetState(firstPlayerState); + }); + } + + public void Dispose() + { + _multiplayManager?.LeaveRoom(_roomId); + _multiplayManager?.Dispose(); + } + //돌 카운터 증가 함수 public void CountStoneCounter() { diff --git a/Assets/Script/Game/GameManager.cs b/Assets/Script/Game/GameManager.cs index e37aca3..677308b 100644 --- a/Assets/Script/Game/GameManager.cs +++ b/Assets/Script/Game/GameManager.cs @@ -54,6 +54,7 @@ public class GameManager : Singleton } } + // 멀티 플레이를 위한 코드 public void ChangeToGameScene(Enums.GameType gameType) { _gameType = gameType; @@ -63,6 +64,8 @@ public class GameManager : Singleton public void ChangeToMainScene() { _gameType = Enums.GameType.None; + // TODO: 추후 혹시 모를 존재하는 socket 통신 종료 필요 - _gameLogic?.Dispose에서 LeaveRoom 호출하긴 하는데 서버에서 이미 해당 방을 삭제했을 경우 동작 확인 필요 + // _gameLogic?.Dispose(); SceneManager.LoadScene("Main"); } diff --git a/Assets/Script/Game/MultiplayManager.cs b/Assets/Script/Game/MultiplayManager.cs new file mode 100644 index 0000000..4c81e88 --- /dev/null +++ b/Assets/Script/Game/MultiplayManager.cs @@ -0,0 +1,195 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; +using SocketIOClient; +using UnityEngine; + +public class CreateRoomData +{ + [JsonProperty("roomId")] + public string roomId { get; set; } +} + +public class JoinRoomData +{ + [JsonProperty("roomId")] + public string roomId { get; set; } + [JsonProperty("opponentRating")] + public int opponentRating { get; set; } + [JsonProperty("opponentNickname")] + public string opponentNickname { get; set; } + [JsonProperty("opponentImageIndex")] + public int opponentImageIndex { get; set; } + [JsonProperty("isBlack")] + public Boolean isBlack { get; set; } +} + +public class StartGameData +{ + [JsonProperty("opponentId")] + public string opponentId { get; set; } + [JsonProperty("opponentRating")] + public int opponentRating { get; set; } + [JsonProperty("opponentNickname")] + public string opponentNickname { get; set; } + [JsonProperty("opponentImageIndex")] + public int opponentImageIndex { get; set; } + [JsonProperty("isBlack")] + public Boolean isBlack { get; set; } +} + + +public class PositionData +{ + [JsonProperty("x")] + public int x { get; set; } + + [JsonProperty("y")] + public int y { get; set; } +} + +public class MoveData +{ + [JsonProperty("position")] + public PositionData position { get; set; } +} + + +public class MessageData +{ + [JsonProperty("message")] + public string message { get; set; } +} + +public class MultiplayManager : IDisposable +{ + private SocketIOUnity _socket; + private event Action _onMultiplayStateChanged; + public Action OnOpponentMove; + + public MultiplayManager(Action onMultiplayStateChanged) + { + _onMultiplayStateChanged = onMultiplayStateChanged; + try + { + var serverUrl = new Uri(Constants.GameServerURL); + + _socket = new SocketIOUnity(serverUrl, new SocketIOOptions + { + Transport = SocketIOClient.Transport.TransportProtocol.WebSocket + }); + + _socket.On("createRoom", CreateRoom); + _socket.On("joinRoom", JoinRoom); + _socket.On("startGame", StartGame); + _socket.On("switchAI", SwitchAI); + _socket.On("exitRoom", ExitRoom); + _socket.On("endGame", EndGame); + _socket.On("doOpponent", DoOpponent); + + _socket.Connect(); + } + catch (Exception e) + { + Debug.LogError("MultiplayManager 생성 중 오류 발생: " + e.Message); + } + } + + public async void RegisterPlayer(string nickname, int rating, int imageIndex) + { + // 연결될 때까지 대기 + while (!_socket.Connected) + { + Debug.Log("소켓 연결 대기 중..."); + await Task.Delay(100); // 0.1초 대기 후 다시 확인 + } + _socket.Emit("registerPlayer", new { nickname, rating, imageIndex }); + } + + private void CreateRoom(SocketIOResponse response) + { + var data = response.GetValue(); + _onMultiplayStateChanged?.Invoke(Constants.MultiplayManagerState.CreateRoom, data.roomId); + } + + private void JoinRoom(SocketIOResponse response) + { + var data = response.GetValue(); + Debug.Log($"룸에 참여: 룸 ID - {data.roomId}, 상대방 등급 - {data.opponentRating}, 상대방 이름 - {data.opponentNickname}, 흑/백 여부 - {data.isBlack}, 상대방 이미지 인덱스 - {data.opponentImageIndex}"); + + // 필요한 데이터 사용 + _onMultiplayStateChanged?.Invoke(Constants.MultiplayManagerState.JoinRoom, data); + } + + private void SwitchAI(SocketIOResponse response) + { + var data = response.GetValue(); + Debug.Log("switchAI: " + data.message); + _onMultiplayStateChanged?.Invoke(Constants.MultiplayManagerState.SwitchAI, data.message); + } + + private void StartGame(SocketIOResponse response) + { + var data = response.GetValue(); + Debug.Log($"게임 시작: 상대방 ID - {data.opponentId}, 상대방 등급 - {data.opponentRating}, 상대방 이름 - {data.opponentNickname}, 흑/백 여부 - {data.isBlack}, 상대방 이미지 인덱스 - {data.opponentImageIndex}"); + + // 필요한 데이터 사용 + _onMultiplayStateChanged?.Invoke(Constants.MultiplayManagerState.StartGame, data); + } + + + // 서버로 부터 상대방의 마커 정보를 받기 위한 메서드 + private void DoOpponent(SocketIOResponse response) + { + var data = response.GetValue(); + + if (data != null && data.position != null) + { + Vector2Int opponentPosition = new Vector2Int(data.position.x, data.position.y); + Debug.Log($"상대방의 위치: {opponentPosition}"); + OnOpponentMove?.Invoke(new MoveData { position = data.position }); + } + else + { + Debug.LogError("DoOpponent: 데이터가 올바르지 않습니다."); + } + } + + + // 플레이어의 마커 위치를 서버로 전달하기 위한 메서드 + public void SendPlayerMove(string roomId, Vector2Int position) + { + Debug.Log($"내 위치: {position}"); + _socket.Emit("doPlayer", new + { + roomId, + position = new { x = position.x, y = position.y } + }); + } + + + private void ExitRoom(SocketIOResponse response) + { + _onMultiplayStateChanged?.Invoke(Constants.MultiplayManagerState.ExitRoom, null); + } + + private void EndGame(SocketIOResponse response) + { + _onMultiplayStateChanged?.Invoke(Constants.MultiplayManagerState.EndGame, null); + } + + public void LeaveRoom(string roomId) + { + _socket.Emit("leaveRoom", new { roomId }); + } + + public void Dispose() + { + if (_socket != null) + { + _socket.Disconnect(); + _socket.Dispose(); + _socket = null; + } + } +} diff --git a/Assets/Script/Game/MultiplayManager.cs.meta b/Assets/Script/Game/MultiplayManager.cs.meta new file mode 100644 index 0000000..b167786 --- /dev/null +++ b/Assets/Script/Game/MultiplayManager.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 18c2446fa3d545d289abeca18341ef81 +timeCreated: 1742865818 \ No newline at end of file diff --git a/Assets/Script/Main/LoadingPanelController.cs b/Assets/Script/Main/LoadingPanelController.cs index 1388551..48def94 100644 --- a/Assets/Script/Main/LoadingPanelController.cs +++ b/Assets/Script/Main/LoadingPanelController.cs @@ -120,6 +120,7 @@ public class LoadingPanelController : MonoBehaviour { cancellationTokenSource.Cancel(); } - gameObject.SetActive(false); + + if (gameObject.activeSelf) gameObject.SetActive(false); } } diff --git a/Assets/Script/Main/MainPanelController.cs b/Assets/Script/Main/MainPanelController.cs index c352ce7..f4b22ad 100644 --- a/Assets/Script/Main/MainPanelController.cs +++ b/Assets/Script/Main/MainPanelController.cs @@ -85,8 +85,7 @@ public class MainPanelController : MonoBehaviour //코인 차감 후 게임 씬 로드 GameManager.Instance.panelManager.RemoveCoinsPanelUI((() => { - GameManager.Instance.ChangeToGameScene(Enums.GameType.SinglePlay); - //Todo: 게임 타입에 따라 다른 Scene 호출 + GameManager.Instance.ChangeToGameScene(Enums.GameType.MultiPlay); })); } diff --git a/Assets/Script/UI/PanelController/PanelManager.cs b/Assets/Script/UI/PanelController/PanelManager.cs index cdda752..f66194f 100644 --- a/Assets/Script/UI/PanelController/PanelManager.cs +++ b/Assets/Script/UI/PanelController/PanelManager.cs @@ -14,6 +14,7 @@ public class PanelManager : MonoBehaviour private Canvas _canvas; private CoinsPanelController _coinsPanel; private LoadingPanelController loadingPanelController; + private GameObject loadingPanelObject; private Dictionary panelPrefabs = new Dictionary(); @@ -71,11 +72,17 @@ public class PanelManager : MonoBehaviour public void OpenLoadingPanel(bool rotateImage = false, bool animatedText = false, bool flipImage = false) { + SetCanvas(); if (_canvas != null) { - var loadingPanelObject = GetPanel("Loading Panel"); + if (loadingPanelObject != null && loadingPanelObject.activeSelf) + { + // 기존 로딩 패널이 활성화되어 있으면 먼저 닫기 + CloseLoadingPanel(); + } + + loadingPanelObject = GetPanel("Loading Panel"); - // 로딩 화면이 생성된 후, 원하는 애니메이션 활성화 loadingPanelController = loadingPanelObject.GetComponent(); if (loadingPanelController != null) @@ -84,6 +91,18 @@ public class PanelManager : MonoBehaviour } } } + + public void CloseLoadingPanel() + { + if (loadingPanelObject != null && loadingPanelObject.activeSelf && loadingPanelController != null) + { + loadingPanelController.StopLoading(); + } + else + { + Debug.Log("로딩 패널이 이미 닫혔거나 비활성화 상태입니다."); + } + } public void OpenSigninPanel() { diff --git a/Packages/manifest.json b/Packages/manifest.json index 03e5328..93555b6 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -1,5 +1,6 @@ { "dependencies": { + "com.itisnajim.socketiounity": "https://github.com/itisnajim/SocketIOUnity.git", "com.unity.collab-proxy": "2.7.1", "com.unity.feature.2d": "2.0.1", "com.unity.ide.rider": "3.0.34", @@ -10,6 +11,7 @@ "com.unity.timeline": "1.7.6", "com.unity.ugui": "1.0.0", "com.unity.visualscripting": "1.9.4", + "com.veriorpies.parrelsync": "https://github.com/VeriorPies/ParrelSync.git?path=/ParrelSync", "com.unity.modules.ai": "1.0.0", "com.unity.modules.androidjni": "1.0.0", "com.unity.modules.animation": "1.0.0", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index dd254ab..dd05142 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -1,5 +1,14 @@ { "dependencies": { + "com.itisnajim.socketiounity": { + "version": "https://github.com/itisnajim/SocketIOUnity.git", + "depth": 0, + "source": "git", + "dependencies": { + "com.unity.nuget.newtonsoft-json": "3.0.2" + }, + "hash": "c9e06b15391449ad42fd9b0f39fea48054751bcd" + }, "com.unity.2d.animation": { "version": "9.1.3", "depth": 1, @@ -168,6 +177,13 @@ "dependencies": {}, "url": "https://packages.unity.com" }, + "com.unity.nuget.newtonsoft-json": { + "version": "3.2.1", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, "com.unity.recorder": { "version": "4.0.3", "depth": 0, @@ -228,6 +244,13 @@ }, "url": "https://packages.unity.com" }, + "com.veriorpies.parrelsync": { + "version": "https://github.com/VeriorPies/ParrelSync.git?path=/ParrelSync", + "depth": 0, + "source": "git", + "dependencies": {}, + "hash": "610157ad762084380380148ba8ce14e266a6da97" + }, "com.unity.modules.ai": { "version": "1.0.0", "depth": 0,