본문 바로가기
Development/C#

멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 67일차

by Mobics 2025. 3. 5.

 

목차


    Tic Tac Toe 서버 만들기

    25.03.05

    Unity로 프로그램 설치

    >> SocketIOUnity

    : 아래 링크에서 Installation을 참고하여 설치

    https://github.com/itisnajim/SocketIOUnity

     

    GitHub - itisnajim/SocketIOUnity: A Wrapper for socket.io-client-csharp to work with Unity.

    A Wrapper for socket.io-client-csharp to work with Unity. - itisnajim/SocketIOUnity

    github.com

     

    >> 설치 과정

     

    >> 설치 Git 링크

    https://github.com/itisnajim/SocketIOUnity.git

     

    >> ParrelSync

    : 아래 링크에서 Installation을 참고하여 설치

    https://github.com/VeriorPies/ParrelSync

     

    GitHub - VeriorPies/ParrelSync: (Unity3D) Test multiplayer without building

    (Unity3D) Test multiplayer without building. Contribute to VeriorPies/ParrelSync development by creating an account on GitHub.

    github.com

     

    >> 설치 과정

     

    >> 설치 Git 링크

    https://github.com/VeriorPies/ParrelSync.git?path=/ParrelSync

     

    >> ParrelSync로 Clone 만들기

    --> 추가 생성된 Clone은 종료

     

    회원가입 만들기

    >> 'Main Panel'의 자식으로 Signup Panel 생성

    : UI-Panel로 생성

    --> Color는 Main Panel의 Color를 가져옴

     

    >> 'Signup Panel'의 자식으로 Title Text 생성

    : UI-Text로 생성

    --> Anchor는 Alt + Shift

     

    >> 'Signup Panel'의 자식으로 'Username Input Field' 생성

    : UI-Input Field로 생성

     

    >> 빈 게임 오브젝트로 'InputFields'를 만들어서 Vertical Layout Group 추가

    : 이후 Username InputField를 자식으로 넣기

    --> Anchor는 Alt + Shift

     

    >> 'Username Input Field'를 복사하여 여러 'Input Field' 생성

    : PlaceHolder의 Text Input은 각자에 맞게 수정

    • Nickname Input Field
    • Password Input Field --> Content Type을 Password로 설정
    • Confirm Password Input Field --> Content Type을 Password로 설정

     

    --> InputFields의 모든 Text들의 Alignment를 Middle로 세팅

     

    >> 'Signup Panel'의 자식으로 Confirm Button 생성

    : Source Image인 Button-bg를 Slice한 후, 설정

     

    --> Button의 Text도 설정

     

    >> 'Signup Panel'의 자식으로 빈 게임 오브젝트로 'Buttons'를 만들어서 Vertical Layout Group 추가

    : 이후 'Confirm Button'을 자식으로 넣기

     

    >> 'Confirm Button'을 복사하여 'Cancel Button' 만들기

    : Button의 Text Input을 "취소"로 변경

     

    >> SignupPanelController.cs 생성

    : 추가 및 바인딩

     

    --> 각 Button에 맞게 OnClick() 바인딩

     

    >> Signup Panel Prefab화

     

    로그인 만들기

    >> Hierarchy에 있는 'Signup Panel'을 다시 Prefab 해제한 다음 'Signin Panel'로 변경

     

    >> Input Fields의 Height 수정 및 'Nickname Input Field', 'Confirm Password Input Field' 삭제

     

    >> Button들 이름 및 Text 변경

     

    >> 'Signin Panel'의 'SignupPanelController.cs' 삭제, 'SigninPanelController.cs', 'Constants.cs' 생성

    : 추가 및 바인딩

     

    >> 각 Button의 OnClick()에 함수 바인딩

     

    >> Signin Panel Prefab화 후 Hierarchy 에서 삭제

     

    구조 개선

    >> Panel Open 구현

    : GameManager.cs에 바인딩

     

    >> NetworkManager.cs 생성

    : Signin, Signup, 점수 추가 및 불러오기, 회원 정보 불러오기 등등 구현할 예정

     

    점수 불러오기

    : 별도로 ID, Password를 넘기지 않아도 불러올 수 있는 이유는 Cookie값을 보내기 때문

    --> 코드로 구현할 때도 마찬가지로 Cookie값을 저장했다가 불러오는 방식으로 구현

     

    >> 'Main Panel'의 'Buttons'의 자식으로 임시로 Score Button 생성

    : 'SettingsButton'을 복붙하여 생성, Text만 "점수 보기"로 수정

     

    --> Buttons의 Height 수정

     

    >> 'Score Button'의 OnClick()에 함수 바인딩

    : 'MainPanel'은 기존에 이미 바인딩 되어 있을 것, 함수만 바꿔주자

     

    활동

    : Tic Tac Toe 자동 로그인 만들기

    • 점수 불러오기 기능과 로그인 기능을 이용해 자동 로그인 기능을 만들어 보세요.

    --> 한번 로그인을 하면 자동으로 로그인이 되지만, 아직 안한 상태라면 로그인 창이 뜨도록

     

    ※ 자동 로그인과 점수 불러오기의 상관관계 (다른 수강생님 답변)

    : 세션 아이디로 점수 불러오기가 성공하면 로그인 되어 있는거고, 해당 세션 아이디가 없거나 서버의 것과 일치하지 않으면 로그인이 안 되어있는 것으로 생각

     

    최종 코드

    >> GameManager.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    
    public class GameManager : Singleton<GameManager>
    {
        [SerializeField] private GameObject settingsPanel;
        [SerializeField] private GameObject confirmPanel;
        [SerializeField] private GameObject signinPanel;
        [SerializeField] private GameObject signupPanel;
        
        private BlockController _blockController;
        private GameUIController _gameUIController;
        private Canvas _canvas;
    
        public enum PlayerType { None, PlayerA, PlayerB }
        private PlayerType[,] _board;
        
        public enum TurnType { PlayerA, PlayerB }
        private enum GameResult { None, Win, Lose, Draw }
        
        public enum GameType { SinglePlayer, DualPlayer }
        private GameType _gameType;
    
        private void Start()
        {
            // 로그인
            OpenSigninPanel();
        }
    
        public void ChangeToGameScene(GameType gameType)
        {
            _gameType = gameType;
            SceneManager.LoadScene("Game");
        }
    
        public void ChangeToMainScene()
        {
            SceneManager.LoadScene("Main");
        }
    
        public void OpenSettingsPanel()
        {
            if (_canvas != null)
            {
                var settingsPanelObject = Instantiate(settingsPanel, _canvas.transform);
                settingsPanelObject.GetComponent<PanelController>().Show();
            }
        }
    
        public void OpenConfirmPanel(string message, ConfirmPanelController.OnConfirmButtonClick onConfirmButtonClick)
        {
            if (_canvas != null)
            {
                var confirmPanelObject = Instantiate(confirmPanel, _canvas.transform);
                confirmPanelObject.GetComponent<ConfirmPanelController>()
                    .Show(message, onConfirmButtonClick);
            }
        }
    
        public void OpenSigninPanel()
        {
            if (_canvas != null)
            {
                var signinPanelObject = Instantiate(signinPanel, _canvas.transform);
            }
        }
    
        public void OpenSignupPanel()
        {
            if (_canvas != null)
            {
                var signupPanelObject = Instantiate(signupPanel, _canvas.transform);
            }
        }
    
        /// <summary>
        /// 게임 시작
        /// </summary>
        private void StartGame()
        {
            // _board 초기화
            _board = new PlayerType[3, 3];
            
            // Block 초기화
            _blockController.InitBlocks();
            
            // Game UI 초기화
            _gameUIController.SetGameUIMode(GameUIController.GameUIMode.Init);
            
            //panelManager.ShowPanel(PanelManager.PanelType.BattlePanel);
            
            // 턴 시작
            SetTurn(TurnType.PlayerA);
        }
        
        /// <summary>
        /// 게임 오버 시, 호출되는 함수
        /// gameResult에 따라 결과 출력
        /// </summary>
        /// <param name="gameResult">win, lose, draw</param>
        private void EndGame(GameResult gameResult)
        {
            // 게임오버 표시
            _gameUIController.SetGameUIMode(GameUIController.GameUIMode.GameOver);
            _blockController.OnBlockClickedDelegate = null; // 게임이 끝났을 때 board를 Click해도 바뀌지 않도록
            
            // TODO: 나중에 구현!
            switch (gameResult)
            {
                case GameResult.Win:
                    break;
                case GameResult.Lose:
                    break;
                case GameResult.Draw:
                    break;
            }
        }
    
        /// <summary>
        /// _board에 새로운 값을 할당하는 함수
        /// </summary>
        /// <param name="playerType">할당하고자 하는 플레이어 타입</param>
        /// <param name="row">Row</param>
        /// <param name="col">Col</param>
        /// <returns>False : 할당할 수 없음, True : 할당이 완료됨</returns>
        private bool SetNewBoardValue(PlayerType playerType, int row, int col)
        {
            if (_board[row, col] != PlayerType.None) return false; // 중복 체크 방지
            
            if (playerType == PlayerType.PlayerA)
            {
                _board[row, col] = playerType;
                _blockController.PlaceMarker(Block.MarkerType.O, row, col);
                return true;
            }
            else if (playerType == PlayerType.PlayerB)
            {
                _board[row, col] = playerType;
                _blockController.PlaceMarker(Block.MarkerType.X, row, col);
                return true;
            }
            return false;
        }
    
        private void SetTurn(TurnType turnType)
        {
            switch (turnType)
            {
                case TurnType.PlayerA:
                    _gameUIController.SetGameUIMode(GameUIController.GameUIMode.TurnA);
                    _blockController.OnBlockClickedDelegate = (row, col) =>
                    {
                        if (SetNewBoardValue(PlayerType.PlayerA, row, col))
                        {
                            var gameResult = CheckGameResult();
                            if (gameResult == GameResult.None)
                                SetTurn(TurnType.PlayerB);
                            else
                                EndGame(gameResult);
                        }
                    };
                    break;
                case TurnType.PlayerB:
                    _gameUIController.SetGameUIMode(GameUIController.GameUIMode.TurnB);
    
                    if (_gameType == GameType.SinglePlayer)
                    {
                        var result = MinimaxAIController.GetBestMove(_board);
                        if (result.HasValue)
                        {
                            if (SetNewBoardValue(PlayerType.PlayerB, result.Value.row, result.Value.col))
                            {
                                var gameResult = CheckGameResult();
                                if (gameResult == GameResult.None)
                                    SetTurn(TurnType.PlayerA);
                                else
                                    EndGame(gameResult);
                            }
                        }
                        else
                        {
                            EndGame(GameResult.Win);
                        }
                    }
                    else if (_gameType == GameType.DualPlayer)
                    {
                        _blockController.OnBlockClickedDelegate = (row, col) =>
                        {
                            if (SetNewBoardValue(PlayerType.PlayerB, row, col))
                            {
                                var gameResult = CheckGameResult();
                                if (gameResult == GameResult.None)
                                    SetTurn(TurnType.PlayerA);
                                else
                                    EndGame(gameResult);
                            }
                        };    
                    }
                    break;
            }
        }
    
        /// <summary>
        /// 게임 결과 확인 함수
        /// </summary>
        /// <returns>플레이어 기준 게임 결과</returns>
        private GameResult CheckGameResult()
        {
            if (CheckGameWin(PlayerType.PlayerA)) return GameResult.Win;
            if (CheckGameWin(PlayerType.PlayerB)) return GameResult.Lose;
            if (MinimaxAIController.IsAllBlocksPlaced(_board)) return GameResult.Draw;
            
            return GameResult.None;
        }
        
        // 게임의 승패를 판단하는 함수
        private bool CheckGameWin(PlayerType playerType)
        {
            // 가로로 마커가 일치하는지 확인 
            for (var row = 0; row < _board.GetLength(0); row++)
            {
                if (_board[row, 0] == playerType && _board[row, 1] == playerType && _board[row, 2] == playerType)
                {
                    (int, int)[] blocks = { (row, 0), (row, 1), (row, 2) };
                    _blockController.SetBlockColor(playerType, blocks);
                    return true;
                }
            }
            
            // 세로로 마커가 일치하는지 확인
            for (var col = 0; col < _board.GetLength(1); col++)
            {
                if (_board[0, col] == playerType && _board[1, col] == playerType && _board[2, col] == playerType)
                {
                    (int, int)[] blocks = { (0, col), (1, col), (2, col) };
                    _blockController.SetBlockColor(playerType, blocks);
                    return true;
                }
            }
            
            // 대각선으로 마커가 일치하는지 확인
            if (_board[0, 0] == playerType && _board[1, 1] == playerType && _board[2, 2] == playerType)
            {
                (int, int)[] blocks = { (0, 0), (1, 1), (2, 2) };
                _blockController.SetBlockColor(playerType, blocks);
                return true;
            }
    
            if (_board[0, 2] == playerType && _board[1, 1] == playerType && _board[2, 0] == playerType)
            {
                (int, int)[] blocks = { (0, 2), (1, 1), (2, 0) };
                _blockController.SetBlockColor(playerType, blocks);
                return true;
            }
            
            return false;
        }
    
        protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
        {
            if (scene.name == "Game")
            {
                _blockController = GameObject.FindObjectOfType<BlockController>();
                _gameUIController = GameObject.FindObjectOfType<GameUIController>();
    
                // 게임 시작 --> GameScene에 들어왔을 때만 실행
                StartGame();
            }
    
            // Canvas는 Main과 Game 모두 필요하기 때문에 if 밖에서 찾음
            _canvas = GameObject.FindObjectOfType<Canvas>();
        }
    }

     

    >> SignupPanelController.cs

    using System.Collections;
    using System.Collections.Generic;
    using TMPro;
    using UnityEngine;
    using UnityEngine.Networking;
    
    public struct SignupData
    {
        public string username;
        public string nickname;
        public string password;
    }
    
    public class SignupPanelController : MonoBehaviour
    {
        [SerializeField] private TMP_InputField usernameInputField;
        [SerializeField] private TMP_InputField nicknameInputField;
        [SerializeField] private TMP_InputField passwordInputField;
        [SerializeField] private TMP_InputField confirmPasswordInputField;
        
        public void OnClickConfirmButton()
        {
            var username = usernameInputField.text;
            var nickname = nicknameInputField.text;
            var password = passwordInputField.text;
            var confirmPassword = confirmPasswordInputField.text;
    
            if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(nickname) || 
                string.IsNullOrEmpty(password) || string.IsNullOrEmpty(confirmPassword)) 
            {
                // 입력값이 비어있음을 알리는 팝업창 표시
                GameManager.Instance.OpenConfirmPanel("입력 내용이 누락되었습니다.", () =>
                {
                    
                });
                return;
            }
    
            if (password.Equals(confirmPassword))
            {
                SignupData signupData = new SignupData();
                signupData.username = username;
                signupData.nickname = nickname;
                signupData.password = password;
                
                // 서버로 SignupData 전달하면서 회원가입 진행
                StartCoroutine(NetworkManager.Instance.Signup(signupData, () =>
                {
                    Destroy(gameObject);
                }, () =>
                {
                    usernameInputField.text = "";
                    nicknameInputField.text = "";
                    passwordInputField.text = "";
                    confirmPasswordInputField.text = "";
                }));
            }
            else
            {
                GameManager.Instance.OpenConfirmPanel("비밀번호가 서로 다릅니다.", () =>
                {
                    passwordInputField.text = "";
                    confirmPasswordInputField.text = "";
                });
            }
        }
        
        public void OnClickCancelButton()
        {
            Destroy(gameObject);
        }
    }

     

    >> SigninPanelController.cs

    using TMPro;
    using UnityEngine;
    
    public struct SigninData
    {
        public string username;
        public string password;
    }
    
    public struct SigninResult
    {
        public int result;
    }
    
    public struct ScoreResult
    {
        public string id;
        public string username;
        public string nickname;
        public int score;
    
    }
    
    public class SigninPanelController : MonoBehaviour
    {
        [SerializeField] private TMP_InputField usernameInputField;
        [SerializeField] private TMP_InputField passwordInputField;
    
        public void OnClickSigninButton()
        {
            string username = usernameInputField.text;
            string password = passwordInputField.text;
    
            if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
            {
                // TODO: 누락된 값 입력 요청 팝업 표시
                return;
            }
    
            var signinData = new SigninData();
            signinData.username = username;
            signinData.password = password;
    
            StartCoroutine(NetworkManager.Instance.Signin(signinData, () =>
            {
                Destroy(gameObject);
            }, result =>
            {
                if (result == 0)
                {
                    usernameInputField.text = "";
                }
                else if (result == 1)
                {
                    passwordInputField.text = "";
                }
            }));
        }
    
        public void OnClickSignupButton()
        {
            GameManager.Instance.OpenSignupPanel();
        }
    }

     

    >> Constants.cs

    : NetworkManager.cs에서만 사용하기 때문에 사실 이렇게 따로 선언할 필요는 없지만, 그럼에도 상수값들을 한 곳에 모아두면 용이하기 때문에 이렇게 처리함

    public class Constants
    {
        public const string ServerURL = "http://localhost:3000";
    }

     

    >> NetworkManager.cs

    using System;
    using System.Collections;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    using UnityEngine.Networking;
    
    public class NetworkManager : Singleton<NetworkManager>
    {
        protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
        {
            
        }
        
        public IEnumerator Signup(SignupData signupData, Action success, Action failure)
        {
            string jsonString = JsonUtility.ToJson(signupData);
            byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonString);
    
            // () 안의 객체가 {}를 벗어나면 Disposing되는 문법
            using (UnityWebRequest www =
                   new UnityWebRequest(Constants.ServerURL + "/users/signup", UnityWebRequest.kHttpVerbPOST))
            {
                www.uploadHandler = new UploadHandlerRaw(bodyRaw);
                www.downloadHandler = new DownloadHandlerBuffer();
                www.SetRequestHeader("Content-Type", "application/json");
                
                yield return www.SendWebRequest();
    
                if (www.result == UnityWebRequest.Result.ConnectionError || 
                    www.result == UnityWebRequest.Result.ProtocolError)
                {
                    Debug.Log("Error : " + www.error);
    
                    if (www.responseCode == 409) // 중복 사용자
                    {
                        // TODO: 중복 사용자 생성 팝업 표시
                        Debug.Log("중복 사용자");
                        GameManager.Instance.OpenConfirmPanel("이미 존재하는 사용자입니다.", () =>
                        {
                            failure?.Invoke();
                        });
                    }
                }
                else
                {
                    var result = www.downloadHandler.text;
                    Debug.Log("Result : " + result);
                    
                    // 회원가입 성공 팝업 표시
                    GameManager.Instance.OpenConfirmPanel("회원가입이 완료 되었습니다.", () =>
                    {
                        success?.Invoke();
                    });
                }
            }
        }
        
        public IEnumerator Signin(SigninData signinData, Action success, Action<int> failure)
        {
            string jsonString = JsonUtility.ToJson(signinData);
            byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonString);
    
            using (UnityWebRequest www =
                   new UnityWebRequest(Constants.ServerURL + "/users/signin", UnityWebRequest.kHttpVerbPOST))
            {
                www.uploadHandler = new UploadHandlerRaw(bodyRaw);
                www.downloadHandler = new DownloadHandlerBuffer();
                www.SetRequestHeader("Content-Type", "application/json");
                
                yield return www.SendWebRequest();
    
                if (www.result == UnityWebRequest.Result.ConnectionError ||
                    www.result == UnityWebRequest.Result.ProtocolError)
                {
                    
                }
                else
                {
                    // 점수 불러오기 기능을 구현하기 위해 cookie 값 저장 (수강생님 코드 참고)
                    var cookie = www.GetResponseHeader("set-cookie");
                    if (!string.IsNullOrEmpty(cookie))
                    {
                        //int lastIndex = cookie.LastIndexOf(";");
                        //string sid = cookie.Substring(0, lastIndex);
                        //PlayerPrefs.SetString("sid", sid);
                        
                        int lastIndex = cookie.IndexOf('='); // 첫 번째 '='의 위치 찾기
                        if (lastIndex != -1 && lastIndex + 1 < cookie.Length) // '='이 존재하고, 뒤에 값이 있을 경우
                        {
                            string sid = cookie.Substring(lastIndex + 1).Split(';')[0]; // '=' 다음부터 ';' 이전까지 추출
                            Debug.Log("Session ID : " + sid);
                            PlayerPrefs.SetString("sid", sid);
                        }
                        else
                        {
                            // 올바른 쿠키 형식이 아님
                        }
                    }
                    else
                    {
                        Debug.Log("쿠키가 비어있습니다.");
                    }
                    
                    var resultString = www.downloadHandler.text;
                    var result = JsonUtility.FromJson<SigninResult>(resultString);
    
                    if (result.result == 0)
                    {
                        // username이 유효하지 않음
                        GameManager.Instance.OpenConfirmPanel("Username이 유효하지 않습니다.", () =>
                        {
                            failure?.Invoke(0);
                        });
                    }
                    else if (result.result == 1)
                    {
                        // password가 유효하지 않음
                        GameManager.Instance.OpenConfirmPanel("Password가 유효하지 않습니다.", () =>
                        {
                            failure?.Invoke(1);
                        });
                    }
                    else if (result.result == 2)
                    {
                        // 성공
                        GameManager.Instance.OpenConfirmPanel("로그인에 성공하였습니다.", () =>
                        {
                            success?.Invoke();
                        });
                    }
                }
            }
        }
    
        public IEnumerator GetScore(Action<ScoreResult> success, Action failure)
        {
            using (UnityWebRequest www = 
                   new UnityWebRequest(Constants.ServerURL + "/users/score", UnityWebRequest.kHttpVerbGET))
            {
                www.downloadHandler = new DownloadHandlerBuffer();
                
                string sid = PlayerPrefs.GetString("sid", "");
                if (!string.IsNullOrEmpty(sid))
                {
                    www.SetRequestHeader("Cookie", sid);
                }
                
                yield return www.SendWebRequest();
    
                if (www.result == UnityWebRequest.Result.ConnectionError || 
                    www.result == UnityWebRequest.Result.ProtocolError)
                {
                    if (www.responseCode == 403)
                    {
                        Debug.Log("로그인이 필요합니다.");
                    }
                    
                    failure?.Invoke();
                }
                else
                {
                    var result = www.downloadHandler.text;
                    var userScore = JsonUtility.FromJson<ScoreResult>(result);
                    
                    Debug.Log(userScore.score);
                    
                    success?.Invoke(userScore);
                }
            }
        }
    }