TIL

[멋쟁이사자처럼 부트캠프 TIL회고] Unity게임개발 3기 0305 : 유니티와 서버

Cadi 2025. 3. 5. 17:25

오늘 배운 것

1. 설치 & InputField

2. Unity와 서버 연결

 

 

01. 설치 & InputField

SocketIO와 ParrelSynce를 설치해준다.

 

유니티 소켓 IO 설치
https://github.com/itisnajim/SocketIOUnity

 

ParrelSync 설치

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

 

Socket.IO

  • 웹 소켓 기반으로 클라이언트와 서버 간의 실시간 양방향 통신을 가능하게 하는 라이브러리

ParrelSync

  • 유니티 다중 인스턴스 실행을 가능하게 해 주는 도구

설치 후 상단의 ParrelSynnc - ClonesManager  - new clone

허브에도 뜨지만 그냥 원본 소스를 수정하고 ParrelSync로 접속하도록 하자. 

 


InputField에 대하여 (Feat. 로그인 패널 제작)

 

InputField

  • 사용자가 텍스트를 입력할 수 있도록 하는 UI 요소
  • 로그인, 채팅 등에 자주 쓰임

OnvalueChanged  : InputField의 텍스트 값이 변경될 때마다 발동

On Edit End : InputField의 편집이 종료될 때 발동

On Select : InputField가 선택되었을 때 발동

On Deslect : InputField의 선택이 해제되었을 때 발동

 

Content Type : 입력 필드에 입력되는 콘텐츠의 종류를 정의하는 속성

  • Standard : 어떤 문자든 입력할 수 있는 기본 설정
  • Autocorrected : 자동 수정 기능을 활성화, 맞춤법 자동 수정
  • Integer Number : 정수만 입력할 수 있도록 제한
  • Decimal Number :  소숫점 숫자를 포함한 숫자만 입력할 수 있도록 제한
  • Alphanumeric : 영문자와 숫자만 입력할 수 있도록 제한
  • Name : 이름 입력에 최적화된 설정 제공
  • Email Address : 이메일 주소 형식에 맞는 입력만 허용, 모바일 키보드를 이메일 입력에 최적화
  • Password : 입력된 문자를 '*'로 숨겨 비밀번호 입력에 적합하게 만듬
  • Pin : Password와 유사하지만 숫자만 입력 가능
  • Custom : 사용자가 직접 사용자 정의 가능한 타입

 

https://docs.unity3d.com/kr/2018.4/Manual/script-InputField.html

 

입력 필드 - Unity 매뉴얼

입력 필드(Input Field) 를 통해 텍스트 컨트롤의 텍스트를 수정할 수 있습니다. 다른 상호작용하는 컨트롤과 비슷하게, 그 자체로는 시각적 UI 요소가 아니므로 한 개 이상의 시각적 UI 요소와 결합

docs.unity3d.com

 

이제 회원가입 창을 만들어준다. 

아래 화면에 키패드가 나올 것을 대비해 살짝 올려서 배치한다. 

 

 

스크립트를 통해 SignupPanel를 컨트롤할 것이다. 

using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.Networking;

public struct SignupData
{
    public string username;
    public string password;
    public string nickname;
}

public class SignupPanelController : MonoBehaviour
{
    [SerializeField] private TMP_InputField _usernameInputField;
    [SerializeField] private TMP_InputField _passwordInputField;
    [SerializeField] private TMP_InputField _nicknameInputField;
    [SerializeField] private TMP_InputField _confirmPasswordInputField;

    private const string ServerURL = "http://localhost:3000";

    public void OnClickConfirmButton()
    {
        Debug.Log(_confirmPasswordInputField.text);
        var username = _usernameInputField.text;
        var password = _passwordInputField.text;
        var nickname = _nicknameInputField.text;
        var confirmPassword = _confirmPasswordInputField.text;

        if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password) ||
            string.IsNullOrEmpty(nickname) || string.IsNullOrEmpty(confirmPassword))
        {
            //TODO: 입력창이 비어 있음을 알리는 팝업창 표시

            return;
        }

        if (password.Equals(confirmPassword))
        {
            SignupData signupData = new SignupData();
            signupData.nickname = nickname;
            signupData.username = username;
            signupData.password = password;

            // 서버로 SignupData 전달하며 회원가입 진행
            StartCoroutine(Signup(signupData));
        }
    }

    IEnumerator Signup(SignupData signupData)
    {
        string jsonString = JsonUtility.ToJson(signupData);
        byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonString);

        //오타가 발생할 수 있기 때문에 정해진 문자열을 사용
        using (UnityWebRequest www = new UnityWebRequest(ServerURL + "/users/signup", UnityWebRequest.kHttpVerbPOST))
        {
            www.uploadHandler = (UploadHandler)new UploadHandlerRaw(bodyRaw);
            www.downloadHandler = (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)
                {
                    //중복 사용자 생성 팝업 표시
                    Debug.Log("중복사용자");
                    GameManager.Instance.OpenConfirmPanel("이미 존재하는 사용자입니다", () =>
                    {
                        _usernameInputField.text = string.Empty;
                        _passwordInputField.text = string.Empty;
                        _nicknameInputField.text = string.Empty;
                        _confirmPasswordInputField.text = string.Empty;
                    });
                }
            }
            else
            {
                //회원가입 성공 팝업 표시
                var result = www.downloadHandler.text;
                Debug.Log("Result " + result);
                
                GameManager.Instance.OpenConfirmPanel("회원 가입이 완료되었습니다", () =>
                {
                    Destroy(gameObject);
                });
            }
        }
    }

    public void OnClickCancelButton()
    {
        Destroy(gameObject);
    }
}

 

 

Stuct 구조체를 사용해 전달할 데이터를 정의하고 묶어둔다. 

이후 InputField에 들어온 string 값들을 통해 유효성 검사를 진행하고, 유효하다면 코루틴을 사용해서 데이터를 전송한다. 

 

전송되는 데이터는 다음의 과정을 거쳐 변환되는데, 이는 HTTP 요청을 통해 데이터를 전송하기 위한 과정이다. 

string jsonString = JsonUtility.ToJson(signupData);
byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonString);

 

JsonUtility.ToJson(signupData) 

: signupData 객체를 JSON 형식의 문자열로 변환한다.

 signupData은 c# 객체이며, HTTP 요청의 본문(body)에 직접 객체를 담아 전송할 수 없기 때문이다. 

 즉, C# 객체를 JSON 문자열로 직렬화하여, 네트워크를 통해 데이터를 전송하기 적합한 형태로 변환하는

 것이며 이를 직렬화라고 한다. 

 

System.Text.Encoding.UTF8.GetBytes(jsonString)

: JSON 문자열을 UTF-8 인코딩된 바이트 배열로 변환한다.

 HTTP 요청의 본문은 바이트 스트림 형태로 전송되어야하기 때문이다. 

 

* UTF-8 : 유니코드 문자를 인코딩하는 방식 중 하나

*바이트 스트림(Byte Stream) : 연속된 바이트들의 흐름, 컴퓨터에서 모든 데이터는 결국 바이트 형태로 표현되며,

                                                  이러한 바이트들이 연속적으로 전송되거나 처리되는 것이 바이트 스트림.

 

 

위 과정을 거쳐 데이터를 변환하고, WepRequest에 넣어 전송한다.

using (UnityWebRequest www =
       new UnityWebRequest(Constants.ServerURL + "/users/signup", UnityWebRequest.kHttpVerbPOST))

 

 

UnityWebRequest 객체는, 요청과 답변이 끝난 후 삭제되어야 한다. 

여러가지 방법이 있겠지만 위에서 사용한 방법은 using으로 전체 코드를 감싸준 것이다. 

이렇게 하면 끝난 후 자연스럽게 Dispose() 된다. 

 

www = null;

 

로 처리하고, GC한테 맡길 수도 있지만, 즉시 해제되지 않고 언제 GC가 실행될지 보장되지 ㅇ낳기 때문에

명시적으로 Dispose()를 호출하여 리소스를 해제하는 것이 중요하다. 

www.Dispose(); 

 

 

혹은, using 문으로 감싸주는, 위에서 사용한 방식을 사용한다.

using (UnityWebRequest www = new UnityWebRequest(ServerURL + "users/signup", UnityWebRequest.kHttpVerbPOST))
{
    
}

 

 

 

https://docs.unity3d.com/kr/2022.3/Manual/UnityWebRequest.html

 

UnityWebRequest - Unity 매뉴얼

UnityWebRequest는 HTTP 요청을 구성하고 HTTP 리스폰스를 처리하기 위한 모듈식 시스템을 제공합니다. UnityWebRequest 시스템의 주요 목표는 Unity 게임이 최신 웹 브라우저 백 엔드와 상호작용할 수 있도

docs.unity3d.com

 

 

* UnityWebRequest.kHttpVerbGet 과 같은 것은 실수를 방지하기 위해 문자열을 저장해둔 것이다.

 

 

 

 

 

 

 

 

 

성공적으로 사용자가 생성된 것을 볼 수 있다. 

MongoDB에서 확인한 내역

 

 

Unity로 로그인 / 로그아웃 / 회원가입 / 점수보기 제작

위에서 만들었던 패널을 프리팹으로 만들어서 저장하고, 비슷한 기능을 하는 새로운 패널을 제작할 것이다.

 새로운 화면 (로그인)을 만들기 위해 Unpack Compeletely (재사용하기 위함, 원래 패널은 프리팹으로 이미 저장)

 

패널을 만드는 것은 생략한다. 다음은 SignIn(로그인) 코드이다. 

using System.Collections;
using System.Collections.Generic;
using System.Text;
using TMPro;
using UnityEngine;
using UnityEngine.Networking;

public struct SignInData
{
   public string username;
   public string password;
}

//서버로부터 내려받는 데이터의 형식
public struct SigninResult
{
   public int result;
}

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(SignIn(signInData));
      
      
   }

   IEnumerator SignIn(SignInData signInData)
   {
      string jsonString = JsonUtility.ToJson(signInData);
      byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonString);

      using (UnityWebRequest www =
             new UnityWebRequest(Constants.ServerURL + "/users/signin", 
                UnityWebRequest.kHttpVerbPOST))
      {
         www.uploadHandler = new UploadHandlerRaw(bodyRaw);       
         www.downloadHandler = (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
         {
            var resultString = www.downloadHandler.text;
            // FromJSon을 통해 변환한다.
            var result = JsonUtility.FromJson<SigninResult>(resultString);

            if (result.result == 0)
            {
             //유저 네임유효하지 않음  
             GameManager.Instance.OpenConfirmPanel("유저네임이 유효하지 않습니다.", () =>
             {
                _usernameInputField.text = "";
             });
            }
            else if (result.result == 1)
            {
               //패스워드가 유효하지 않음
               GameManager.Instance.OpenConfirmPanel("패스워드가 유효하지 않습니다.", () =>
               {
                  _passwordInputField.text = "";
               });
            }
            else if (result.result == 2)
            {
               //성공
               GameManager.Instance.OpenConfirmPanel("로그인에 성공하였습니다.", () =>
               {
                  Destroy(gameObject);
               });
            }

         }
      }
   }
   

   public void OnClickSignupButton()
   {
      
   }
   
}

 

 

이제, 게임과 결합시켜야 한다. 

public class GameManager : Singleton<GameManager>
{
    [SerializeField] private GameObject settingsPanel;
    [SerializeField] private GameObject confirmPanel;
    [SerializeField] private GameObject signinPanel;
    [SerializeField] private GameObject signupPanel;
public void OpenSigninPanel()
{
    if (_canvas != null)
    {
        var signinPanelObject = Instantiate(signinPanel, _canvas.transform);
    }
}

public void OpenSignupPanel()
{
    if (_canvas != null)
    {
        var signupPanelObject = Instantiate(signupPanel, _canvas.transform);
    }
}

 

 

이렇게 패널 하나하나마다 별도의 스크립트를 해도 좋고, 다른 방식으로는 새로운 클래스( 네트워크 관련 클래스)를 만들어 한 번에 관리하는 것도 좋은 방법이다.

 

 

NetWorkManager로 통합할 것이다. 

 

 

또한, 통합하려면 Action을 받아와서 각각의 패널들이 자신의 InputField를 변화시킬 수 있게 매개변수를 받는다.

public IEnumerator SignIn(SignInData signInData, Action sucess, Action failure)

Action<매개변수> failure 등으로 실패의 종류를 구별한다던지, 후에 Data를 받아서 출력한다던지 할 수 있다.

using System;
using System.Collections;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class NetworkManager : Singleton<NetworkManager>
{
   protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
   {
   }

   public IEnumerator SignIn(SignInData signInData, Action sucess, Action<int> failure)
   {
      string jsonString = JsonUtility.ToJson(signInData);
      byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonString);

      using (UnityWebRequest www =
             new UnityWebRequest(Constants.ServerURL + "/users/signin",
                UnityWebRequest.kHttpVerbPOST))
      {
         www.uploadHandler = new UploadHandlerRaw(bodyRaw);
         www.downloadHandler = (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
         {
            var resultString = www.downloadHandler.text;
            // FromJSon을 통해 변환한다.
            var result = JsonUtility.FromJson<SigninResult>(resultString);

            if (result.result == 0)
            {
               //유저 네임유효하지 않음  
               GameManager.Instance.OpenConfirmPanel("유저네임이 유효하지 않습니다.", () =>
               {
                  failure.Invoke(0);
               });
            }
            else if (result.result == 1)
            {
               //패스워드가 유효하지 않음
               GameManager.Instance.OpenConfirmPanel("패스워드가 유효하지 않습니다.", () =>
               {
                  failure.Invoke(1);
               });
            }
            else if (result.result == 2)
            {
               //성공
               sucess.Invoke();
               GameManager.Instance.OpenConfirmPanel("로그인에 성공하였습니다.", () => { Destroy(gameObject); });
            }

         }
      }
   }
   public IEnumerator Signup(SignupData signupData,Action sucess, Action failure)
   {
      string jsonString = JsonUtility.ToJson(signupData);
      byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonString);

      //오타가 발생할 수 있기 때문에 정해진 문자열을 사용
      using (UnityWebRequest www = new UnityWebRequest(Constants.ServerURL + "/users/signup", UnityWebRequest.kHttpVerbPOST))
      {
         www.uploadHandler = (UploadHandler)new UploadHandlerRaw(bodyRaw);
         www.downloadHandler = (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)
            {
               //중복 사용자 생성 팝업 표시
               Debug.Log("중복사용자");
               GameManager.Instance.OpenConfirmPanel("이미 존재하는 사용자입니다", () =>
               {
                  // _usernameInputField.text = string.Empty;
                  // _passwordInputField.text = string.Empty;
                  // _nicknameInputField.text = string.Empty;
                  // _confirmPasswordInputField.text = string.Empty;
                  failure?.Invoke();
               });
            }
         }
         else
         {
            //회원가입 성공 팝업 표시
            var result = www.downloadHandler.text;
            Debug.Log("Result " + result);
                
            GameManager.Instance.OpenConfirmPanel("회원 가입이 완료되었습니다", () =>
            {
               sucess?.Invoke();
               //Destroy(gameObject);
            });
         }
      }
   }
}
using System.Collections;
using System.Collections.Generic;
using System.Text;
using TMPro;
using UnityEngine;
using UnityEngine.Networking;

public struct SignInData
{
   public string username;
   public string password;
}

//서버로부터 내려받는 데이터의 형식
public struct SigninResult
{
   public int result;
}

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();
   }
   
}

 

 

스코어 기능까지 !

 

public IEnumerator GetScoreCoroutine(Action<ScoreResult> sucess, Action failure)
{
    // using (UnityWebRequest www = UnityWebRequest.Get("http://localhost:5000/users/score")) ;
    using (UnityWebRequest www =
           new UnityWebRequest(Constants.ServerURL + "/users/score", UnityWebRequest.kHttpVerbGET))
    {
        string sid = PlayerPrefs.GetString("sid");
        www.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();
        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);
            sucess?.Invoke(userScore);

            Debug.Log(userScore.score);
        }
    }
}

 

질문  - www.downloadhandeler >>>> > 왜 초기화 해 주어야 작동하는가 ? 선언되있는거 아닌가 ?

질문 : www.downloadHandler는 왜 초기화 해 주어야만 작동하는가 ? 미리 선언되어 있는 것이 아닌가 ?

UnityWebRequest는 객체를생성할 때, 기본적으로 다운로드 핸들러가 자동으로 할당되지 않는다.

따라서, 서버로부터 응답 데이터를 받기 위해서는 명시적으로 다운로드 핸들러를 설정해야 한다. 

 

 

이런 식으로 유니티에서 UnityWebRequest 객체를 생성하고, 서버로 URL주소를 통해 데이터를 주고받으면서 로그인,

로그아웃, 회원가입, 점수 보기 등의 기능을 구현할 수 있다.