[멋쟁이사자처럼 부트캠프 TIL회고] Unity 게임개발 0307 : 채팅, 멀티플레이
오늘 과제
1. 채팅 구현
2. 멀티 플레이 구현
01. 채팅 구현
Dynamic string에 있는 것으로 해야 한다.
웹소켓(SocketIO)를 사용할 것이다.
//이렇게 람다로 만들어도 되고, 새로운 함수(밑)으로 만들어도 된다.
_socket.On("createRoom", (data) =>
{
});
}
// 항상 매개변수는 있어야 한다. 받을지 말지 모르기 떄문에
private void CreateRoom(SocketIOResponse response)
{
}
* ParrelSCyn 이용시 주의점 : 변경사항들을 공유한다, 따로 오가며 수정해줄 필요는 없지만 가끔 클릭 등으로 동기화 해 주어야 한다.
첫 번째 MultiplayerManager클래스이다.
SocketIO를 이용해서 통신하고, Action을 통해 받아온 함수들을 실행하는 역할을 할 것이다.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using SocketIOClient;
using Newtonsoft.Json;
public class RoomData
{
[JsonProperty("roomId")] public string roomId { get; set; }
}
public class UserData
{
[JsonProperty("userId")] public string userId { get; set; }
}
public class MessageData
{
[JsonProperty("nickName")] public string nickName { get; set; }
[JsonProperty("message")] public string message { get; set; }
}
public class MultiplayManager : IDisposable
{
private SocketIOUnity _socket;
//상태를 전달해줘야 할 것 rkxdma
public event Action<Constants.MultiplayManagerState, string> _onMultiplayChanged;
// 채팅을 붙였다 뗏다 하기 위한 목적을 분리를 해 놨음(할당했다가 분리했다가 할 용도)
public Action<MessageData> OnReceivedMessage;
// 생성자, Action을 받아서 등록해놓는.
public MultiplayManager(Action<Constants.MultiplayManagerState, string> onMultiplayChanged)
{
var uri = new Uri(Constants.GameServerURL);
_onMultiplayChanged = onMultiplayChanged;
_socket = new SocketIOUnity(uri, new SocketIOOptions()
{
Transport = SocketIOClient.Transport.TransportProtocol.WebSocket
});
//람다로 만들어도 되고, 새로운 함수(밑)으로 만들어도 된다.
_socket.On("createRoom", CreateRoom);
_socket.On("joinRoom", JoinRoom);
_socket.On("startGame", StartGame);
_socket.On("gameEnded", GameEnded);
_socket.On("receiveMessage", ReceivedMessage);
//연결됨
_socket.Connect();
}
// 항상 매개변수는 있어야 한다. 받을지 말지 모르기 떄문에
private void CreateRoom(SocketIOResponse response)
{
var data = response.GetValue<RoomData>();
_onMultiplayChanged?.Invoke(Constants.MultiplayManagerState.CreateRoom, data.roomId);
}
private void JoinRoom(SocketIOResponse response)
{
var data = response.GetValue<RoomData>();
UnityThread.executeInUpdate(() =>
_onMultiplayChanged?.Invoke(Constants.MultiplayManagerState.JoinRoom, data.roomId));
}
private void StartGame(SocketIOResponse response)
{
var data = response.GetValue<UserData>();
UnityThread.executeInUpdate(() =>
_onMultiplayChanged?.Invoke(Constants.MultiplayManagerState.StartGame, data.userId));
}
private void GameEnded(SocketIOResponse response)
{
var data = response.GetValue<UserData>();
_onMultiplayChanged?.Invoke(Constants.MultiplayManagerState.EndGame, data.userId);
}
private void ReceivedMessage(SocketIOResponse response)
{
Debug.Log("메세지 받음");
var data = response.GetValue<MessageData>();
OnReceivedMessage?.Invoke(data);
}
public void SendMessage(string roomId, string nickName, string message)
{
_socket.Emit("sendMessage", new { roomId, nickName, message });
}
public void Dispose()
{
if (_socket != null)
{
_socket.Disconnect();
_socket.Dispose();
_socket = null;
}
}
}
우선 변수들을 정의해 준다.
private SocketIOUnity _socket;
//상태를 전달해줘야 할 것 rkxdma
public event Action<Constants.MultiplayManagerState, string> _onMultiplayChanged;
// 채팅을 붙였다 뗏다 하기 위한 목적을 분리를 해 놨음(할당했다가 분리했다가 할 용도)
public Action<MessageData> OnReceivedMessage;
Socket 변수, 전달받아 실행할 Action _onMultiplayChanged (상태가 변했다는 메세지 받으면 실행),
그리고 채팅을 받을 수 있게 만들어주는 OnReceivedMessage 함수이다.
이것을 따로 분리한 이유는, OnReceivedMessage 대리자를 할당했다 분리했다 하면서 채팅을 받을 수 있는 상태와
그렇지 않은 상태를 구분하기 위해서이다.
다음은 생성자 파트이다.
// 생성자, Action을 받아서 등록해놓는.
public MultiplayManager(Action<Constants.MultiplayManagerState, string> onMultiplayChanged)
{
var uri = new Uri(Constants.GameServerURL);
_onMultiplayChanged = onMultiplayChanged;
_socket = new SocketIOUnity(uri, new SocketIOOptions()
{
Transport = SocketIOClient.Transport.TransportProtocol.WebSocket
});
//람다로 만들어도 되고, 새로운 함수(밑)으로 만들어도 된다.
_socket.On("createRoom", CreateRoom);
_socket.On("joinRoom", JoinRoom);
_socket.On("startGame", StartGame);
_socket.On("gameEnded", GameEnded);
_socket.On("receiveMessage", ReceivedMessage);
//연결됨
_socket.Connect();
}
생성할 때 상태에 따라 다르게 실행되는 변수(함수)를 매개변수로 받고 설정한다.
필요한 uri를 받고 설정한다.
소켓 또한 새로운 소켓을 만들어 설정한다.
그리고 만든 소켓에 특정 신호를 받으면 다음 함수들이 실행되게 한 후 연결한다.
private void JoinRoom(SocketIOResponse response)
{
var data = response.GetValue<RoomData>();
UnityThread.executeInUpdate(() =>
_onMultiplayChanged?.Invoke(Constants.MultiplayManagerState.JoinRoom, data.roomId));
}
private void StartGame(SocketIOResponse response)
{
var data = response.GetValue<UserData>();
UnityThread.executeInUpdate(() =>
_onMultiplayChanged?.Invoke(Constants.MultiplayManagerState.StartGame, data.userId));
}
private void GameEnded(SocketIOResponse response)
{
var data = response.GetValue<UserData>();
_onMultiplayChanged?.Invoke(Constants.MultiplayManagerState.EndGame, data.userId);
}
private void ReceivedMessage(SocketIOResponse response)
{
Debug.Log("메세지 받음");
var data = response.GetValue<MessageData>();
OnReceivedMessage?.Invoke(data);
}
public void SendMessage(string roomId, string nickName, string message)
{
_socket.Emit("sendMessage", new { roomId, nickName, message });
}
각각 매개변수로 response를 받고, 그 response를 바탕으로 등록해둔 Action을 실행한다.
추후에 보면 알겠지만 상태를 넣어주는데, 이 상태에 따라 다른 기능들이 실행된다.
추가 개념 공부 1 : GetValue<T>();
SocketIOResponse.GetValue<T>() 메서드는 Socket.IO 클라이언트 라이브러리에서 제공하는 기능,
JSON 데이터를 C#의 타입 (T) 객체로 변환(역직렬화) 하는 역할을 함.
다음은 받은 데이터를 갖고 실행하는 부분이다.
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEditor.VersionControl;
using UnityEngine;
using UnityEngine.Serialization;
using UnityEngine.UI;
public class ChattingPanelController : MonoBehaviour
{
[SerializeField] private TMP_InputField messageInputField;
[SerializeField] private GameObject messagePrefab;
[SerializeField] private Transform messageTextParent;
private MultiplayManager multiplayerManager;
private string _roomId;
public void OnEndEidtInputField(string messageText)
{
var messageTextObject = Instantiate(messagePrefab, messageTextParent);
messageTextObject.GetComponent<TMP_Text>().text = messageText;
messageInputField.text = "";
Debug.Log(_roomId + ": " + multiplayerManager);
if (_roomId != null && multiplayerManager != null)
{
multiplayerManager.SendMessage(_roomId, "Cadi", messageText);
}
}
void Start()
{
messageInputField.interactable = false;
multiplayerManager = new MultiplayManager((state, id) =>
{
switch(state)
{
case Constants.MultiplayManagerState.CreateRoom:
_roomId = id;
Debug.Log("## Create Room ##");
break;
case Constants.MultiplayManagerState.JoinRoom:
messageInputField.interactable = true;
Debug.Log("## Joining Room ##");
_roomId = id;
Debug.Log(_roomId);
break;
case Constants.MultiplayManagerState.StartGame:
messageInputField.interactable = true;
Debug.Log("## Start Game ##");
break;
case Constants.MultiplayManagerState.EndGame:
Debug.Log("## End Game ##");
break;
}
});
multiplayerManager.OnReceivedMessage = OnReceiveMessage;
}
// 이 메서드 같은 경우 메인스레드에서 동작하게 바꿔주어야 함
private void OnReceiveMessage(MessageData messageData)
{
UnityThread.executeInUpdate(() =>
{
var messageTextObject = Instantiate(messagePrefab, messageTextParent);
messageTextObject.GetComponent<TMP_Text>().text = messageData.nickName + " : " + messageData.message;
});
}
private void OnApplicationQuit(){
multiplayerManager.Dispose();
}
}
public void OnEndEidtInputField(string messageText)
{
var messageTextObject = Instantiate(messagePrefab, messageTextParent);
messageTextObject.GetComponent<TMP_Text>().text = messageText;
messageInputField.text = "";
Debug.Log(_roomId + ": " + multiplayerManager);
if (_roomId != null && multiplayerManager != null)
{
multiplayerManager.SendMessage(_roomId, "Cadi", messageText);
}
}
이 부분은 InputField의 OnEditEnd에 물려 있어 엔터를 치면 실행된다.
중요한 부분은 SendMessage하는 부분이다. 설정된 roomID로 메세지를 보낸다.
룸 아이디와 다른 것들은 밑의 코드에서 설정하게 된다.
void Start()
{
messageInputField.interactable = false;
multiplayerManager = new MultiplayManager((state, id) =>
{
switch(state)
{
case Constants.MultiplayManagerState.CreateRoom:
_roomId = id;
Debug.Log("## Create Room ##");
break;
case Constants.MultiplayManagerState.JoinRoom:
messageInputField.interactable = true;
Debug.Log("## Joining Room ##");
_roomId = id;
Debug.Log(_roomId);
break;
case Constants.MultiplayManagerState.StartGame:
messageInputField.interactable = true;
Debug.Log("## Start Game ##");
break;
case Constants.MultiplayManagerState.EndGame:
Debug.Log("## End Game ##");
break;
}
});
multiplayerManager.OnReceivedMessage = OnReceiveMessage;
}
채팅 패널에 멀티플레이 매니저 객체가 컴포넌트로 있게 되고, 상태에 따라 다른 행동을 하게 된다.
(상태가 변하면, 즉 신호를 받으면 Inovoke 되는 것을 봤음)
보내는 것 뿐만 아니라 받는 것도 있어야 한다.
private void OnReceiveMessage(MessageData messageData)
{
UnityThread.executeInUpdate(() =>
{
var messageTextObject = Instantiate(messagePrefab, messageTextParent);
messageTextObject.GetComponent<TMP_Text>().text = messageData.nickName + " : " + messageData.message;
});
}
private void OnApplicationQuit(){
multiplayerManager.Dispose();
}
그리고 끝났을 때는 자연스럽게 Dispose() 해 주면 된다.
헤맸던 부분 : 서브스레드에서 UI 요소 동작 불가
private void JoinRoom(SocketIOResponse response)
{
var data = response.GetValue<RoomData>();
UnityThread.executeInUpdate(() =>
_onMultiplayChanged?.Invoke(Constants.MultiplayManagerState.JoinRoom, data.roomId));
}
private void StartGame(SocketIOResponse response)
{
var data = response.GetValue<UserData>();
UnityThread.executeInUpdate(() =>
_onMultiplayChanged?.Invoke(Constants.MultiplayManagerState.StartGame, data.userId));
}
요 부분이다, 원래는 UnityThread.executeInUpdat()로 묶어주지 않아 그냥 실행되었는데,
On으로 받아온 신호는 서브스레드에서 실행되기에 유니티의 UI에 직접적인 영향을 가하려고 하면 오류가 나게 된다.
오류가 나서 그 밑의 코드가 실행되지 않았고, 따라서 roomId가 제대로 설정되지 않아, 처음에 룸을 만든 클라이언트는 정상 작동했지만 이후 join으로 참여한 클라이언트는 정상적으로 메세지를 보낼 수 없는(roomId가 null이므로) 문제가 발생했다. 한 두 시간 걸려서 찾았는데 다른 분들도 똑같은 문제가 있었고 이미 해결된 문제였다.
그래도 앞으로는 '감히' 서브스레드에서 UI를 건드는 짓을 안하게 된 것 같으니 한 잔 해 ~
02. 멀티 플레이
일단 코드를 보자면
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using SocketIOClient;
using Newtonsoft.Json;
public class RoomData
{
[JsonProperty("roomId")] public string roomId { get; set; }
}
public class UserData
{
[JsonProperty("userId")] public string userId { get; set; }
}
public class MessageData
{
[JsonProperty("nickName")] public string nickName { get; set; }
[JsonProperty("message")] public string message { get; set; }
}
public class BlockData
{
public int turn { get; set; }
public int x { get; set; }
public int y { get; set; }
}
public class MultiplayManager : IDisposable
{
private SocketIOUnity _socket;
//상태를 전달해줘야 할 것 rkxdma
public event Action<Constants.MultiplayManagerState, string> _onMultiplayChanged;
// 채팅을 붙였다 뗏다 하기 위한 목적을 분리를 해 놨음(할당했다가 분리했다가 할 용도)
public Action<MessageData> OnReceivedMessage;
// 생성자, Action을 받아서 등록해놓는.
public MultiplayManager(Action<Constants.MultiplayManagerState, string> onMultiplayChanged)
{
var uri = new Uri(Constants.GameServerURL);
_onMultiplayChanged = onMultiplayChanged;
_socket = new SocketIOUnity(uri, new SocketIOOptions()
{
Transport = SocketIOClient.Transport.TransportProtocol.WebSocket
});
//람다로 만들어도 되고, 새로운 함수(밑)으로 만들어도 된다.
_socket.On("createRoom", CreateRoom);
_socket.On("joinRoom", JoinRoom);
_socket.On("startGame", StartGame);
_socket.On("gameEnded", GameEnded);
_socket.On("receiveMessage", ReceivedMessage);
_socket.On("blockChanged", BlockChanged);
//연결됨
_socket.Connect();
}
// 항상 매개변수는 있어야 한다. 받을지 말지 모르기 떄문에
private void CreateRoom(SocketIOResponse response)
{
var data = response.GetValue<RoomData>();
_onMultiplayChanged?.Invoke(Constants.MultiplayManagerState.CreateRoom, data.roomId);
}
private void JoinRoom(SocketIOResponse response)
{
var data = response.GetValue<RoomData>();
UnityThread.executeInUpdate(() =>
_onMultiplayChanged?.Invoke(Constants.MultiplayManagerState.JoinRoom, data.roomId));
}
private void StartGame(SocketIOResponse response)
{
var data = response.GetValue<UserData>();
UnityThread.executeInUpdate(() =>
_onMultiplayChanged?.Invoke(Constants.MultiplayManagerState.StartGame, data.userId));
}
private void GameEnded(SocketIOResponse response)
{
var data = response.GetValue<UserData>();
_onMultiplayChanged?.Invoke(Constants.MultiplayManagerState.EndGame, data.userId);
}
private void ReceivedMessage(SocketIOResponse response)
{
var data = response.GetValue<MessageData>();
OnReceivedMessage?.Invoke(data);
}
public void SendMessage(string roomId, string nickName, string message)
{
_socket.Emit("sendMessage", new { roomId, nickName, message });
}
private void BlockChanged(SocketIOResponse response)
{
var data = response.GetValue<BlockData>();
Debug.Log(data);
string data1 = data.turn.ToString();
data1 += data.x.ToString();
data1 += data.y.ToString();
Debug.Log($"data1 = {data1}");
_onMultiplayChanged.Invoke(Constants.MultiplayManagerState.BlockChanged, data1);
}
public void ChangeBlock(string roomId, GameManager.TurnType turnType, int x, int y)
{
Debug.Log($"ChangeBlock으로 들어온 x, x : {x}, y : {y}");
int turn = (turnType == GameManager.TurnType.PlayerB) ? 1 : 0;
var data = new Dictionary<string, object>
{
{ "roomId", roomId },
{ "turn", turn },
{ "x", x },
{ "y", y }
};
Debug.Log($"data = {JsonConvert.SerializeObject(data)}");
_socket.Emit("changeBlock", data);
}
public void Dispose()
{
if (_socket != null)
{
_socket.Disconnect();
_socket.Dispose();
_socket = null;
}
}
}
새로운 코드를 만들어도 괜찮지만(오히려 그쪽이 더 깔끔하겠지만) 이용하고 싶어서 우겨넣었다.
그래서 데이터를 사용할 때 string값으로 변환하고 사용하는 문제가.. 있긴 하다.
private void StartGame()
{
// _board 초기화
_board = new PlayerType[3, 3];
// 블록 초기화
_blockController.InitBlocks();
// Game UI 초기화
_gameUIController.SetGameUIMode(GameUIController.GameUIMode.Init);
//듀얼모드라면 새롭게 듀얼플레이매니저를 만든다.
if (_gameType == GameType.DualPlayer)
{
_multiplayManager = new MultiplayManager((state, id) =>
{
switch (state)
{
case Constants.MultiplayManagerState.CreateRoom:
_roomId = id;
_myTurnType = TurnType.PlayerA;
Debug.Log("## Create Room ##");
break;
case Constants.MultiplayManagerState.JoinRoom:
Debug.Log("## Joining Room ##");
_roomId = id;
_myTurnType = TurnType.PlayerB;
break;
case Constants.MultiplayManagerState.StartGame:
Debug.Log("## Start Game ##");
break;
case Constants.MultiplayManagerState.EndGame:
Debug.Log("## End Game ##");
break;
case Constants.MultiplayManagerState.BlockChanged:
Debug.Log("## Set Block ##");
int turn = (int)char.GetNumericValue(id[0]);
int x = (int)char.GetNumericValue(id[1]);
int y = (int)char.GetNumericValue(id[2]);
UnityThread.executeInUpdate(() =>
{
_blockController.PlaceMarker(turn <1 ? Block.MarkerType.O: Block.MarkerType.X, x, y);
SetNewBoardValue(turn < 1 ? PlayerType.PlayerA : PlayerType.PlayerB ,x, y);
SetTurn( turn < 1 ? TurnType.PlayerB : TurnType.PlayerA );
});
break;
}
});
}
// 턴 시작
SetTurn(TurnType.PlayerA);
}
startgame 부분에서 다음과 같이 새로운 매니저를 만들어주고 할당해준다.
다음은 SetTurn의 일부분이다 (위쪽은 Singleplay일때 )
else
{
_blockController.OnBlockClickedDelegate = null;
var checkGameResult = CheckGameResult();
if (checkGameResult != GameResult.None)
{
EndGame(checkGameResult);
return;
}
switch (turnType)
{
case TurnType.PlayerA:
{
if (_myTurnType == TurnType.PlayerA)
{
_blockController.OnBlockClickedDelegate = (row, col) =>
{
if (SetNewBoardValue(PlayerType.PlayerA, row, col))
{
_multiplayManager.ChangeBlock(_roomId,TurnType.PlayerA, row, col);
var gameResult = CheckGameResult();
if (gameResult == GameResult.None)
{
SetTurn(TurnType.PlayerB);
_blockController.OnBlockClickedDelegate = null;
}
else
EndGame(gameResult);
}
};
}
break;
}
case TurnType.PlayerB:
{
if (_myTurnType == TurnType.PlayerB)
{
_blockController.OnBlockClickedDelegate = (row, col) =>
{
if (SetNewBoardValue(PlayerType.PlayerB, row, col))
{
_multiplayManager.ChangeBlock(_roomId,TurnType.PlayerB, row, col);
var gameResult = CheckGameResult();
if (gameResult == GameResult.None)
{
SetTurn(TurnType.PlayerA);
_blockController.OnBlockClickedDelegate = null;
}
else
EndGame(gameResult);
}
};
}
}
break;
}
}
이런 식으로 자신의 타입과 같을 때만 동작하도록 만들어준다.
그리고 블럭을 놨다면, 상대방한테도 그 정보를 보내주는 것이다.
헤맸던 부분 : 데이터 파싱 문제
오히려 코드를 짜는 부분보다 이 부분이 시간이 오래걸렸다.
파싱이제대로안되었다, 처음엔 보내는 것도 안되었고, 두 번째는 가져오는 것도 제대로 안되었다.
이건 따로 포스팅해서 JSON 데이터와 변환에 관해 써야겠다.
일단 정답만 써 보자면
public class BlockData
{
public int turn { get; set; }
public int x { get; set; }
public int y { get; set; }
}
블럭데이터를 다음과 같은 형태로 Set 할 수 있게 만들어야 했고
public void ChangeBlock(string roomId, GameManager.TurnType turnType, int x, int y)
{
Debug.Log($"ChangeBlock으로 들어온 x, x : {x}, y : {y}");
int turn = (turnType == GameManager.TurnType.PlayerB) ? 1 : 0;
var data = new Dictionary<string, object>
{
{ "roomId", roomId },
{ "turn", turn },
{ "x", x },
{ "y", y }
};
Debug.Log($"data = {JsonConvert.SerializeObject(data)}");
_socket.Emit("changeBlock", data);
}
Dic형태로 만들어 전달해 주어야 했다. 익명은 전달이 안될수도 있다고 한다.
자세한 이야기는 내일 포스팅에서 !
오늘의 목표
01. 동아리 지원서 작성
02. 코테 풀기 ( 하노이 탑 )