목차
Tic Tac Toe 서버 만들기
25.03.07
채팅 구현
> 키보드가 올라오면 Input Field도 위로 올라오도록 구현해야 함
>> Input Field 이름을 'Message Input Field'로 변경
: Input Field와 Text의 이름에 있는 (TMP) 전부 삭제 --> 과거에는 Legacy와 혼용되어 사용되었기 때문에 구분하기 위해 썼지만 지금은 TMP만 사용하므로 구분할 필요가 없음
>> 'Content'의 자식으로 있는 Text 이름을 'Message Text'로 변경 후, Prefab화
: 이후 Hierarchy에 있는 Message Text 삭제
>> ChattingPanelController.cs 생성
>> Chatting Panel에 ChattingPanelController.cs 추가 및 바인딩
>> Message InputField의 OnEndEdit(string)에 함수 추가 및 바인딩
멀티 플레이로 채팅 예제 만들기
: SocketIO를 활용
>> MultiPlayManager.cs 생성
※ 테스트 하기 위해 ParrelSync를 이용해 Clone을 만드는 중 발생한 오류
: ParrelSync로 Clone을 만드는 과정에서 자꾸 에러가 떠서 Clone이 안 된다.
--> 강사님 답변 : Build를 하거나 프로젝트를 2개로 띄우는 다른 방법을 찾아서 테스트 해볼 것
--> 수강생님이 알려주신 방법 : Unity6에서 공식지원하는 'Multiplayer Play Mode' 를 Package Managre Pre-release를 허용하여 가져올 수 있다.
--> 근데 Package Manager에 Mulitplayer Play Mode가 검색해도 보이질 않는 문제...
>> 해결 방법
: ChatGPT에 물어봤을 때, 관리자 권한의 문제였다는 것을 알게됐고 프로젝트를 종료한 다음 UnityHub를 관리자 권한으로 실행하여 프로젝트를 다시 열어서 ParrelSync로 Clone을 만드니까 성공했다.
※ Unity의 Thread 관련
: Unity는 기본적으로 싱글 스레드 환경에서 동작한다. 즉, 대부분의 경우 메인 스레드에서 동작한다. 하지만 Socket.IO는 네트워크 이벤트를 처리할 때 별도의 네트워크 스레드에서 실행되기 때문에 Socket.IO 내에서 Unity의 메인 스레드에서만 실행 가능한 작업(예: UI 업데이트, GameObject 변경 등)을 실행하면 문제가 발생할 수 있다.
--> 따라서 별도의 네트워크 스레드에서 실행된 콜백을 Unity의 메인 스레드에서 실행하도록 다음과 같은 두 가지 방법으로 작업을 옮겨줘야 한다.
- OnUnityThread() : 다른 스레드에서 실행된 코드를 Unity의 메인 스레드에서 '즉시' 실행되도록 한다.
- executeInUpdate() : Unity의 Update()에서 실행되도록 예약하는 방법으로, 특정 작업을 '다음 프레임'에서 실행할 수 있도록 큐에 추가한다. --> OnUnityThread()와 비슷하지만, 단순히 Update()에서 실행되도록 보장하는 점이 다르다.
※ Postman으로 서버 테스트를 할 수 있다.
활동
: Tic Tac Toe 멀티플레이 만들기
- 멀티플레이가 가능한 Tic Tac Toe를 만들어서 영상을 올려주세요
최종 코드
>> ChattingPanelController.cs
using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
public class ChattingPanelController : MonoBehaviour
{
[SerializeField] private TMP_InputField messageInputField;
[SerializeField] private GameObject messageTextPrefab;
[SerializeField] private Transform messageTextParent;
private MultiplayManager _multiplayManager;
private string _roomId;
public void OnEndEditInputField(string messageText)
{
var messageTextObject = Instantiate(messageTextPrefab, messageTextParent);
messageTextObject.GetComponent<TMP_Text>().text = messageText;
messageInputField.text = ""; // message를 보내고 나면 InputField를 비워서 다음 message를 받을 수 있도록
if (_roomId != null && _multiplayManager != null)
{
// TODO: 임의로 넣은 "홍길동" 대신 로그인할 때 받아온 User의 nickName을 넣어주기
_multiplayManager.SendMessage(_roomId, "홍길동", messageText);
}
}
private void Start()
{
messageInputField.interactable = false;
_multiplayManager = new MultiplayManager((state, id) =>
{
switch (state)
{
case Constants.MultiplayManagerState.CreateRoom:
Debug.Log("## Create Room ##");
_roomId = id;
break;
case Constants.MultiplayManagerState.JoinRoom:
Debug.Log("## Join Room ##");
_roomId = id;
UnityThread.executeInUpdate(() => messageInputField.interactable = true);
break;
case Constants.MultiplayManagerState.StartGame:
Debug.Log("## Start Game ##");
UnityThread.executeInUpdate(() => messageInputField.interactable = true);
break;
case Constants.MultiplayManagerState.EndGame:
Debug.Log("## End Game ##");
break;
}
});
_multiplayManager.OnReceivedMessage = OnReceiveMessage;
}
private void OnReceiveMessage(MessageData messageData)
{
UnityThread.executeInUpdate(() =>
{
var messageTextObject = Instantiate(messageTextPrefab, messageTextParent);
messageTextObject.GetComponent<TMP_Text>().text = messageData.nickName + " : " + messageData.message;
});
}
private void OnApplicationQuit()
{
_multiplayManager.Dispose();
}
}
>> MultiplayManager.cs
using System;
using System.Collections;
using System.Collections.Generic;
using Newtonsoft.Json;
using UnityEngine;
using SocketIOClient;
// game.js 에서 보내는 값을 받음
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;
private event Action<Constants.MultiplayManagerState, string> _onMultiplayStateChanged;
public Action<MessageData> OnReceivedMessage;
public MultiplayManager(Action<Constants.MultiplayManagerState, string> onMultiplayStateChanged)
{
_onMultiplayStateChanged = onMultiplayStateChanged;
var uri = new Uri(Constants.GameServerURL);
_socket = new SocketIOUnity(uri, new SocketIOOptions
{
Transport = SocketIOClient.Transport.TransportProtocol.WebSocket
});
// 서버가 보낼 event(message)에 따라 작동
// "createRoom"이라는 event가 왔을 때 CreateRoom이 작동하도록 CreateRoom()이라고 적지 않는다.
_socket.On("createRoom", CreateRoom);
_socket.On("joinRoom", JoinRoom);
_socket.On("startGame", StartGame);
_socket.On("gameEnded", GameEnded);
_socket.On("receiveMessage", ReceiveMessage);
_socket.Connect();
}
private void CreateRoom(SocketIOResponse response) // 매개변수로 SocketIOResponse가 필수로 있어야 한다.
{
var data = response.GetValue<RoomData>();
_onMultiplayStateChanged?.Invoke(Constants.MultiplayManagerState.CreateRoom, data.roomId);
}
private void JoinRoom(SocketIOResponse response)
{
var data = response.GetValue<RoomData>();
_onMultiplayStateChanged?.Invoke(Constants.MultiplayManagerState.JoinRoom, data.roomId);
}
private void StartGame(SocketIOResponse response)
{
var data = response.GetValue<UserData>();
_onMultiplayStateChanged?.Invoke(Constants.MultiplayManagerState.StartGame, data.userId);
}
private void GameEnded(SocketIOResponse response)
{
var data = response.GetValue<UserData>();
_onMultiplayStateChanged?.Invoke(Constants.MultiplayManagerState.EndGame, data.userId);
}
// 서버로부터 Message를 받음
private void ReceiveMessage(SocketIOResponse response)
{
var data = response.GetValue<MessageData>();
OnReceivedMessage?.Invoke(data);
}
// 서버로 Message를 보냄
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;
}
}
}
>> Constants.cs
public class Constants
{
public const string ServerURL = "http://localhost:3000";
public const string GameServerURL = "ws://localhost:3000";
public enum MultiplayManagerState
{
CreateRoom,
JoinRoom,
StartGame,
EndGame
};
}
>> game.js
'Development > C#' 카테고리의 다른 글
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 68일차 (0) | 2025.03.06 |
---|---|
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 67일차 (0) | 2025.03.05 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 66일차 (0) | 2025.03.05 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 65일차 (0) | 2025.03.05 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 64일차 (0) | 2025.02.27 |