Merge pull request #51 from Degulleo/DO-58-멀티플레이-템플릿-구현

DO-58 [Feat] 멀티 플레이 템플릿 구현 및 ParrelSync 추가
This commit is contained in:
Jay 2025-03-26 10:32:47 +09:00 committed by GitHub
commit 0c32f7665a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 757 additions and 11 deletions

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 49e291f62c68bcb4b8ae5c0b9a87f98d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a7530dff3f8b9a74081055b376b00dc7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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: []

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 87e010131d20c0f4f8c3a9fe6d9bcdfb
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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:

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 120a1b7daa97a4247ae154ca3321d3b8
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -47216,6 +47216,63 @@ SpriteRenderer:
m_WasSpriteAssigned: 0 m_WasSpriteAssigned: 0
m_MaskInteraction: 0 m_MaskInteraction: 0
m_SpriteSortPoint: 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 --- !u!4 &8068830904452262562
Transform: Transform:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@ -54561,3 +54618,4 @@ SceneRoots:
- {fileID: 2953084326979075905} - {fileID: 2953084326979075905}
- {fileID: 873369031} - {fileID: 873369031}
- {fileID: 1657305406} - {fileID: 1657305406}
- {fileID: 8064471184320700382}

View File

@ -76,13 +76,19 @@ public class AudioManager : Singleton<AudioManager>
// 클릭 사운드(SFX) 재생 // 클릭 사운드(SFX) 재생
public void PlayClickSound() public void PlayClickSound()
{ {
sfxAudioSource.PlayOneShot(clickSound, sfxVolume); if (sfxAudioSource != null)
{
sfxAudioSource.PlayOneShot(clickSound, sfxVolume);
}
} }
// 닫기 사운드(SFX) 재생 // 닫기 사운드(SFX) 재생
public void PlayCloseSound() public void PlayCloseSound()
{ {
sfxAudioSource.PlayOneShot(closeSound, sfxVolume); if (sfxAudioSource != null)
{
sfxAudioSource.PlayOneShot(closeSound, sfxVolume);
}
} }
// 씬이 로드될 때마다 호출되는 OnSceneLoaded 메서드 // 씬이 로드될 때마다 호출되는 OnSceneLoaded 메서드

View File

@ -10,4 +10,14 @@
public const int RAING_POINTS = 10; public const int RAING_POINTS = 10;
public string[] AI_NAMIES = { "이세돌", "신사동호랭이","진짜인간임","종로3가짱돌","마스터김춘배","62세황순자","고준일 강사님"}; public string[] AI_NAMIES = { "이세돌", "신사동호랭이","진짜인간임","종로3가짱돌","마스터김춘배","62세황순자","고준일 강사님"};
public enum MultiplayManagerState
{
CreateRoom, // 방 생성
JoinRoom, // 생성된 방에 참여
StartGame, // 생성한 방에 다른 유저가 참여해서 게임 시작
SwitchAI, // 15초 후 매칭 실패 시 AI 플레이로 전환 알림
ExitRoom, // 자신이 방을 빠져 나왔을 때
EndGame // 상대방이 접속을 끊거나 방을 나갔을 때
};
} }

View File

@ -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
/// <summary>
/// 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
/// </summary>
public class UnityMainThreadDispatcher : MonoBehaviour {
private static readonly Queue<Action> _executionQueue = new Queue<Action>();
public void Update() {
lock(_executionQueue) {
while (_executionQueue.Count > 0) {
_executionQueue.Dequeue().Invoke();
}
}
}
/// <summary>
/// Locks the queue and adds the IEnumerator to the queue
/// </summary>
/// <param name="action">IEnumerator function that will be executed from the main thread.</param>
public void Enqueue(IEnumerator action) {
lock (_executionQueue) {
_executionQueue.Enqueue (() => {
StartCoroutine (action);
});
}
}
/// <summary>
/// Locks the queue and adds the Action to the queue
/// </summary>
/// <param name="action">function that will be executed from the main thread.</param>
public void Enqueue(Action action)
{
Enqueue(ActionWrapper(action));
}
/// <summary>
/// Locks the queue and adds the Action to the queue, returning a Task which is completed when the action completes
/// </summary>
/// <param name="action">function that will be executed from the main thread.</param>
/// <returns>A Task that can be awaited until the action completes</returns>
public Task EnqueueAsync(Action action)
{
var tcs = new TaskCompletionSource<bool>();
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;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e7461fd0f3834d9283d0ea00daaaea3b
timeCreated: 1742916255

View File

@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine; using UnityEngine;
using UnityEngine.SceneManagement; using UnityEngine.SceneManagement;
using PimDeWitte.UnityMainThreadDispatcher;
public abstract class BasePlayerState public abstract class BasePlayerState
{ {
@ -54,10 +55,32 @@ public class PlayerState : BasePlayerState
{ {
private Enums.PlayerType _playerType; private Enums.PlayerType _playerType;
private bool _isFirstPlayer; private bool _isFirstPlayer;
private MultiplayManager _multiplayManager;
private string _roomId;
private bool _isMultiplay;
public PlayerState(bool isFirstPlayer) public PlayerState(bool isFirstPlayer)
{ {
_isFirstPlayer = isFirstPlayer; _isFirstPlayer = isFirstPlayer;
_playerType = isFirstPlayer ? Enums.PlayerType.PlayerA : Enums.PlayerType.PlayerB; _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) public override void OnEnter(GameLogic gameLogic)
@ -88,6 +111,11 @@ public class PlayerState : BasePlayerState
public override void HandleMove(GameLogic gameLogic, int row, int col) public override void HandleMove(GameLogic gameLogic, int row, int col)
{ {
gameLogic.SetStoneSelectedState(row, 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) public override void HandleNextTurn(GameLogic gameLogic)
@ -132,24 +160,64 @@ public class AIState: BasePlayerState
} }
public class MultiPlayerState: 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) public override void OnEnter(GameLogic gameLogic)
{ {
gameLogic.fioTimer.StartTimer(); 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) public override void OnExit(GameLogic gameLogic)
{ {
gameLogic.fioTimer.InitTimer(); gameLogic.fioTimer.InitTimer();
_multiplayManager.OnOpponentMove = null;
} }
public override void HandleMove(GameLogic gameLogic, int row, int col) public override void HandleMove(GameLogic gameLogic, int row, int col)
{ {
ProcessMove(gameLogic, _playerType, row, col);
} }
public override void HandleNextTurn(GameLogic gameLogic) 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 firstPlayerState;
public BasePlayerState secondPlayerState; public BasePlayerState secondPlayerState;
private BasePlayerState _currentPlayerState; private BasePlayerState _currentPlayerState;
//타이머 //타이머
public FioTimer fioTimer; public FioTimer fioTimer;
@ -176,6 +245,9 @@ public class GameLogic : MonoBehaviour
private int _lastRow; private int _lastRow;
private int _lastCol; private int _lastCol;
private MultiplayManager _multiplayManager;
private string _roomId;
#region Renju Members #region Renju Members
// 렌주룰 금수 검사기 // 렌주룰 금수 검사기
private RenjuForbiddenMoveDetector _forbiddenDetector; private RenjuForbiddenMoveDetector _forbiddenDetector;
@ -227,6 +299,7 @@ public class GameLogic : MonoBehaviour
switch (gameType) switch (gameType)
{ {
// TODO: 현재 싱글 플레이로 바로 넘어가지 않기 때문에 미사용 중
case Enums.GameType.SinglePlay: case Enums.GameType.SinglePlay:
firstPlayerState = new PlayerState(true); firstPlayerState = new PlayerState(true);
secondPlayerState = new AIState(); secondPlayerState = new AIState();
@ -242,14 +315,152 @@ public class GameLogic : MonoBehaviour
SetState(firstPlayerState); SetState(firstPlayerState);
break; break;
case Enums.GameType.MultiPlay: case Enums.GameType.MultiPlay:
//TODO: 멀티 구현 필요 // 메인 스레드에서 실행 - UI 업데이트는 메인 스레드에서 실행 필요
ReplayManager.Instance.InitReplayData("PlayerA","nicknameB"); 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;
// TODO: 응답값 없을 때 서버에서 다시 받아오기 or AI 플레이로 넘기는 처리 필요
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;
// TODO: 응답값 없을 때 서버에서 다시 받아오기 or AI 플레이로 넘기는 처리 필요
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; break;
case Enums.GameType.Replay: case Enums.GameType.Replay:
//TODO: 리플레이 구현 //TODO: 리플레이 구현
break; 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() public void CountStoneCounter()
{ {

View File

@ -54,6 +54,7 @@ public class GameManager : Singleton<GameManager>
} }
} }
// 멀티 플레이를 위한 코드
public void ChangeToGameScene(Enums.GameType gameType) public void ChangeToGameScene(Enums.GameType gameType)
{ {
_gameType = gameType; _gameType = gameType;
@ -63,6 +64,8 @@ public class GameManager : Singleton<GameManager>
public void ChangeToMainScene() public void ChangeToMainScene()
{ {
_gameType = Enums.GameType.None; _gameType = Enums.GameType.None;
// TODO: 추후 혹시 모를 존재하는 socket 통신 종료 필요 - _gameLogic?.Dispose에서 LeaveRoom 호출하긴 하는데 서버에서 이미 해당 방을 삭제했을 경우 동작 확인 필요
// _gameLogic?.Dispose();
SceneManager.LoadScene("Main"); SceneManager.LoadScene("Main");
} }

View File

@ -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<Constants.MultiplayManagerState, object> _onMultiplayStateChanged;
public Action<MoveData> OnOpponentMove;
public MultiplayManager(Action<Constants.MultiplayManagerState, object> 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<CreateRoomData>();
_onMultiplayStateChanged?.Invoke(Constants.MultiplayManagerState.CreateRoom, data.roomId);
}
private void JoinRoom(SocketIOResponse response)
{
var data = response.GetValue<JoinRoomData>();
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<MessageData>();
Debug.Log("switchAI: " + data.message);
_onMultiplayStateChanged?.Invoke(Constants.MultiplayManagerState.SwitchAI, data.message);
}
private void StartGame(SocketIOResponse response)
{
var data = response.GetValue<StartGameData>();
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<MoveData>();
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;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 18c2446fa3d545d289abeca18341ef81
timeCreated: 1742865818

View File

@ -120,6 +120,7 @@ public class LoadingPanelController : MonoBehaviour
{ {
cancellationTokenSource.Cancel(); cancellationTokenSource.Cancel();
} }
gameObject.SetActive(false);
if (gameObject.activeSelf) gameObject.SetActive(false);
} }
} }

View File

@ -85,8 +85,7 @@ public class MainPanelController : MonoBehaviour
//코인 차감 후 게임 씬 로드 //코인 차감 후 게임 씬 로드
GameManager.Instance.panelManager.RemoveCoinsPanelUI((() => GameManager.Instance.panelManager.RemoveCoinsPanelUI((() =>
{ {
GameManager.Instance.ChangeToGameScene(Enums.GameType.SinglePlay); GameManager.Instance.ChangeToGameScene(Enums.GameType.MultiPlay);
//Todo: 게임 타입에 따라 다른 Scene 호출
})); }));
} }

View File

@ -14,6 +14,7 @@ public class PanelManager : MonoBehaviour
private Canvas _canvas; private Canvas _canvas;
private CoinsPanelController _coinsPanel; private CoinsPanelController _coinsPanel;
private LoadingPanelController loadingPanelController; private LoadingPanelController loadingPanelController;
private GameObject loadingPanelObject;
private Dictionary<string, GameObject> panelPrefabs = new Dictionary<string, GameObject>(); private Dictionary<string, GameObject> panelPrefabs = new Dictionary<string, GameObject>();
@ -71,10 +72,16 @@ public class PanelManager : MonoBehaviour
public void OpenLoadingPanel(bool rotateImage = false, bool animatedText = false, bool flipImage = false) public void OpenLoadingPanel(bool rotateImage = false, bool animatedText = false, bool flipImage = false)
{ {
SetCanvas();
if (_canvas != null) if (_canvas != null)
{ {
var loadingPanelObject = GetPanel("Loading Panel"); if (loadingPanelObject != null && loadingPanelObject.activeSelf)
{
// 기존 로딩 패널이 활성화되어 있으면 먼저 닫기
CloseLoadingPanel();
}
loadingPanelObject = GetPanel("Loading Panel");
// 로딩 화면이 생성된 후, 원하는 애니메이션 활성화 // 로딩 화면이 생성된 후, 원하는 애니메이션 활성화
loadingPanelController = loadingPanelObject.GetComponent<LoadingPanelController>(); loadingPanelController = loadingPanelObject.GetComponent<LoadingPanelController>();
@ -85,6 +92,18 @@ public class PanelManager : MonoBehaviour
} }
} }
public void CloseLoadingPanel()
{
if (loadingPanelObject != null && loadingPanelObject.activeSelf && loadingPanelController != null)
{
loadingPanelController.StopLoading();
}
else
{
Debug.Log("로딩 패널이 이미 닫혔거나 비활성화 상태입니다.");
}
}
public void OpenSigninPanel() public void OpenSigninPanel()
{ {
if (_canvas != null) if (_canvas != null)

View File

@ -1,5 +1,6 @@
{ {
"dependencies": { "dependencies": {
"com.itisnajim.socketiounity": "https://github.com/itisnajim/SocketIOUnity.git",
"com.unity.collab-proxy": "2.7.1", "com.unity.collab-proxy": "2.7.1",
"com.unity.feature.2d": "2.0.1", "com.unity.feature.2d": "2.0.1",
"com.unity.ide.rider": "3.0.34", "com.unity.ide.rider": "3.0.34",
@ -10,6 +11,7 @@
"com.unity.timeline": "1.7.6", "com.unity.timeline": "1.7.6",
"com.unity.ugui": "1.0.0", "com.unity.ugui": "1.0.0",
"com.unity.visualscripting": "1.9.4", "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.ai": "1.0.0",
"com.unity.modules.androidjni": "1.0.0", "com.unity.modules.androidjni": "1.0.0",
"com.unity.modules.animation": "1.0.0", "com.unity.modules.animation": "1.0.0",

View File

@ -1,5 +1,14 @@
{ {
"dependencies": { "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": { "com.unity.2d.animation": {
"version": "9.1.3", "version": "9.1.3",
"depth": 1, "depth": 1,
@ -168,6 +177,13 @@
"dependencies": {}, "dependencies": {},
"url": "https://packages.unity.com" "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": { "com.unity.recorder": {
"version": "4.0.3", "version": "4.0.3",
"depth": 0, "depth": 0,
@ -228,6 +244,13 @@
}, },
"url": "https://packages.unity.com" "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": { "com.unity.modules.ai": {
"version": "1.0.0", "version": "1.0.0",
"depth": 0, "depth": 0,