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,