게임을 플레이 할 방을 들어가는 것까지의 게임 구조도를 정리해보았다.
현재 구현된 것은 Loading에서 Home으로 가는 것, 종료, 서버 접속까지이다.
기존에 만들어놓은 Scene과 기능들이 따로 놀고 있는 느낌이 있어, 위의 구조도를 기반으로 재정렬하자.
Loading에서 플레이어의 데이터를 검사하고, 없다면 새로운 계정을 생성한다. 이 과정은 데이터베이스가 만들어지고 나서 구현할 문제이므로, 일단은 플레이할 때마다 임시 프로필을 부여받아 Home에 접속하는 것으로 하자. 그 이후 Home에서 서버 접속을 시도한다.
처음 로딩 화면이다. 지금은 다음 Scene을 로딩하는 기능밖에 없지만, 나중에 플레이어의 데이터를 불러오고, 데이터가 없다면 플레이어 데이터를 새로 생성하는 장면이다.
그 다음은 Home. 여기서 다른 곳으로 쭉 갈라지게 된다. 오른쪽 아래에는 Player 이름이 업데이트된다.
Create Room은 방을 생성한다.
Join Room은 생성된 방에 접속한다.
Tutorial은 미리 설정된 진행 방식으로 게임의 규칙을 파악한다.
Profile은 사용자 데이터를 볼 수 있다.
Setting은 소리와 같은 설정을 변경할 수 있다.
Exit는 게임을 종료한다.
다음은 사용된 스크립트들의 정리다.
BtnExitParents - 팝업 UI 아래에 놓는 비활성화 X 버튼
GameManager - Scene 전반 관리
NetworkManager - 참고용, 추후 삭제
Player - 플레이어 정보를 담고 있음
SceneHome - Scene(Home) 관리 스크립트
ServerManager - 서버
ClientManager - 클라이언트
UILoading - 로딩바, 로딩 완료시 시작이 들어 있음
SoundManager - 소리 관련 - 작성 내용 없음
UIManager - 팝업 UI 관리 - 작성 내용 없음
추후 SoundManager는 소리 세팅에 관하여, UIManager는 팝업 형태로 나올 UI를 관리하는데 사용될 것이다.
일단, 순서도 상에서 서버 접속을 시도해야하므로 InpurField로 IP와 Port를 받는 패널에 스크립트를 삽입해주자.
Creat Room을 누르면 서버를 생성하는 용도로, Join Room을 누르면 서버에 접속하는 클라이언트로 사용될 것이다.
[SerializeField]
private TMP_InputField ipInputField;
[SerializeField]
private TMP_InputField portInputField;
[SerializeField]
private Button btnGo;
public bool isCreateMode;
public bool isJoinMode;
InputPanel에서 사용될 코드로, ip와 port를 입력하는 InputField와 시작 버튼을 엔진 내에서 할당한다.
그리고 CreatRoom을 눌러서 패널을 띄웠다면 isCreateMode를, JoinRoom을 눌러 패널을 띄웠다면 isJoinMode를 활성화시킨다. 위의 두 bool값을 통해서 서버를 생성할 지, 들어갈 지를 결정하는 것이다.
그를 위해서 Home Scene에 서버와 클라이언트 코드를 넣어놓은(저번에 짠) 오브젝트를 만들어놓자. 이 두 오브젝트가 서버와 클라이언트 사이의 데이터를 전송해줄 것이다.
private void PressGameStartButton()
{
if(isCreateMode && !isJoinMode)
{
//방 생성
}
else if(!isCreateMode && isJoinMode)
{
//방 들어가기
}
else
{
Debug.Log("Creat와 Join 중 선택이 필요합니다.");
}
}
public void SetCreateMode()
{
isCreateMode = true;
isJoinMode = false;
}
public void SetJoinMode()
{
isCreateMode = false;
isJoinMode = true;
}
그럴 목적으로 짠 코드는 위와 같다. Create를 누를 때와 Join을 누를 때, 어떤 동작을 할 것인지 bool 값으로 지정해준다. 그 이후 버튼을 눌렀을 때 서버와 클라이언트 매니저를 사용해서 접속하는 코드를 작성하면 된다.
우선은 if(isCreateMode && !isJoinMode) 쪽에 들어갈 서버를 여는 코드부터 작성해보도록 하자.
if(isCreateMode && !isJoinMode)
{
ServerManager.Instance.StartServer();
}
가 된다. 여기서 Instance를 보면 알겠지만, ServerManager도 따로 오브젝트를 만들어서 스크립트를 넣어주는 방식으로 구현했다.(싱글톤 패턴 사용. 아래에 코드 있음)
public static ServerManager Instance { get; private set; };
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
이런 식으로 서버 매니저와 클라이언트 매니저를 각각 만들어주어, 서버 역할을 할 때는 서버를, 클라이언트 역할을 할 때는 클라이언트 매니저를 사용하도록 하자.
※ Static Class로 만들어서 사용하는 방법도 있다고 한다. 이 경우, Monobehaviour을 상속받아 사용하는 것보다 나은 점이 여럿 있다. 문제점으로는 서버 작동이 오브젝트에 할당되어 있기 때문에 프레임단위 제한을 받는다는 것(확실한지는 모름)과 오브젝트에 의존하게 된다는 것 등을 해결할 수 있다고 한다. 나중에 어떤 점이 더 나은지 알아보고 확인할 것.
public void StartServer(string inputIpAddressStr, string inputPort)
{
//받은 입력으로 초기화
ipaddress = IPAddress.Parse(inputIpAddressStr);
port = int.Parse(inputPort);
//서버 소켓 생성
serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//IP 주소와 포트 설정. 그리고 바인드
endpoint = new IPEndPoint(ipaddress, port);
serverSocket.Bind(endpoint);
//Listen, 클라이언트 연결 요청 대기.
serverSocket.Listen(5); // 대기 큐 크기를 지정
Debug.Log("서버 시작. 접속을 기다립니다...");
//Thread로 클라이언트 소켓 보낸다.
while (true)
{
Socket clientSocket = serverSocket.Accept(); //Todo, 비동기적 메서드인 BeginAccept와 비교
Debug.Log("새 사용자 접속!");
Thread clientThread = new Thread(() => HandleClient(clientSocket));
clientThread.Start();
}
}
그럼, StartServer 함수를 약간 수정하자.
원래 인수는 없었는데, InputField로 IP와 Port를 입력받을 수 있으니 받아오도록 한다.
그 이후 받아온 ipaddress와 port로 IPEndPoint를 설정, 바인드를 실행한다.
그럼 당연히 서버 패널에서의 실행 함수도 ServerManager.Instance.StartServer(ipInputField.text, portInputField.text);로 수정되어야 옳다.
그런데 막상 이 코드를 실행시키면 문제가 있는데, 바로 클라이언트의 접속을 무한정 기다리는 상태에 빠진다는 점이다. -> 메인 실행 쓰레드가 응답상태로 계속 기다리므로 다른 행동을 하지 못한다.
따라서 위의 서버는 비동기로 실행하거나, 혹은 따로 쓰레드를 지정해줄 필요가 있다.(클라이언트 접속만 비동기로 해놓았던 과거의 잘못)
StartServer를 함수에서 코루틴으로 변경해주자.
그러면 해결인 줄 알았으나, 문제는 그 점이 아니었다. 아래는 무식하게 일일이 검사하며 때려박은 코드다.
public IEnumerator StartServer()
{
Debug.Log("Test2");
yield return new WaitForSeconds(1);
//서버 소켓 생성
serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
Debug.Log("Test3");
yield return new WaitForSeconds(1);
//IP 주소와 포트 설정. 그리고 바인드
endpoint = new IPEndPoint(ipaddress, port);
Debug.Log("Test4");
yield return new WaitForSeconds(1);
serverSocket.Bind(endpoint);
Debug.Log("Test5");
yield return new WaitForSeconds(1);
//Listen, 클라이언트 연결 요청 대기.
serverSocket.Listen(5); // 대기 큐 크기를 지정
Debug.Log("서버 시작. 접속을 기다립니다...");
yield return new WaitForSeconds(1);
//Thread로 클라이언트 소켓 보낸다.
//Check Infinite Loop
int loopNum = 0;
Debug.Log("Test6");
while (true)
{
Debug.Log("Test7");
yield return new WaitForSeconds(1);
Debug.Log("Test8");
yield return new WaitForSeconds(1);
Socket clientSocket = serverSocket.Accept(); //Todo, 비동기적 메서드인 BeginAccept와 비교
yield return new WaitForSeconds(1);
Debug.Log("새 사용자 접속!");
yield return new WaitForSeconds(1);
Thread clientThread = new Thread(() => HandleClient(clientSocket));
clientThread.Start();
if (loopNum++ > 10000)
throw new Exception("Infinite Loop");
yield return null;
}
}
무한루프에 빠진 줄 알아서 Infinite Loop 테스트도 돌려봤지만 아니었고, 결국 Debug.Log와 WaitForSeconds를 다 때려박아서 어디서 멈추는지를 알아봤다. 멈추는 구간은 Test8이후.
Socket clientSocket = serverSocket.Accept();이다.
이 부분에 내가 '비동기적 메서드인 BeginAccept와 비교' 라고 적어놨었는데, 이게 여기서 발목을 잡을 줄은 몰랐다. 나중에 TCP 공부를 이론으로 병행하는게 역시 필요할 것 같다.
아무튼, 이 부분에서 클라이언트의 접속을 무한정 기다리기 때문에 일어나는 문제같다.(그런데 코루틴으로 실행시켰는데도 이런 문제가 발생하는건가?)
그러니 BeginAccept로 변경할 시간이 찾아온 것이다.
혹은 다른 선택지도 있다. 여기서 Git에서 branch를 나눠서 여러가지를 시도해보는 것도 좋을 것이다.
1. 메인 쓰레드만 사용 -> BeginAccept와 같은 비동기 네트워크 함수 사용.
2. 별도의 쓰레드를 사용 -> new StartThread로 안에 기존의 동기 네트워크 함수를 때려넣기.
3. 별도의 쓰레드를 사용하면서 쓰레드 안에서 코루틴을 사용하기(이 코루틴이 결국은 메인 쓰레드에서 실행되는가?)
4. 유니티의 Job System을 사용해보기 (유니티의 언급 -> If you want to use multi-threaded code within Unity, consider the C# Job System.)
그렇지만, 결국에는 1번이 제일 나을 것이라는 생각이 든다. 공식 메뉴얼(https://docs.unity3d.com/Manual/Coroutines.html)에서 HTTP 통신 같은 것도 결국은 코루틴 사용이 제일 낫다고 했기 때문...
그리고 Job System도 대량의 오브젝트 관리나, Work 적인 부분의 부담을 줄이기 위한 것인 것 같으니 아마 잘 안맞을 것 같다.
'IndianPoker' 카테고리의 다른 글
[10] 게임 구조 설계 겸 튜토리얼 장면 만들기 (0) | 2023.11.15 |
---|---|
[09] Coroutine과 Thread의 차이점 (공부 후 재작성) (0) | 2023.11.10 |
[07] 채팅창 UI 만들기, TMP로 한글 쓰기 (0) | 2023.11.01 |
[06] TCP, 클라이언트 만들기 (1) | 2023.10.28 |
[05] Thread, Handle Client (1) | 2023.10.28 |