멈추지 않고 끈질기게
[Unity][Photon][포트폴리오] 채팅 구현 본문
※ 해당 포스팅은 개인의 공부 정리용 글입니다. 틀린 내용이 있다면 추후 수정될 수 있습니다.
※ 해당 포스팅은 Unity 2021.3.15f1 / Pun 2.41 / Photon lib 4.1.6.17 버전을 기준으로 작성되었습니다.
※ 개발 중인 유니티 3D 포트폴리오의 소스 코드를 포함하고 있습니다.
1. UI 세팅
우선 멀티플레이 대기중인 유저들끼리 채팅할 수 있도록 채팅창을 멀티플레이 대기 UI에 추가하였습니다.
채팅 입력칸은 InputField(TMP)로 만들었고, 채팅 출력 영역에는 Vertical Layout Group을 붙여 자동 정렬되도록 했습니다. 정렬 기준은 Lower Left로 하여 새로운 채팅이 가장 밑으로 출력되고 이전 채팅은 위로 올라가도록 했습니다. 채팅 텍스트(TMP)의 가로길이는 채팅창 안에 들어가는 사이즈로 고정하고, Control Child Size에서 Height만 체크하여 채팅 길이에 따라 줄바꿈이 일어나도록 세팅하였습니다.
참고로 TextMeshPro에서는 띄어쓰기 단위로 줄바꿈이 일어나도록 설정할 수 있습니다. Assets\TextMeshPro\Resources 폴더의 TMP Settings 파일 선택 후, 인스펙터 창 최하단의 Korkean Language Option에서 Use Modren Line Breaking 옵션을 체크하면 됩니다.
2. Photon 방 설정
// GameManager
public void GameStart(GameMode mode)
{
switch (mode)
{
case GameMode.SingleGame:
StartSingleGame();
break;
case GameMode.MultiGame:
// 방 입장
NetworkManager.Instance.EnterRoom();
break;
}
}
// NetworkManager
using PN = Photon.Pun.PhotonNetwork;
public void EnterRoom()
{
// 방이 있다면 랜덤 입장, 없다면 생성
// 최대 인원수 2인 방에 참가, 없다면 최대 인원수 2인 방 생성
PN.JoinRandomOrCreateRoom(
expectedMaxPlayers: 2,
roomOptions: new RoomOptions() { MaxPlayers = 2 });
}
// 방 입장 시
// 대기중인 유저 모두 업데이트 하도록 RPC로 실행
public override void OnJoinedRoom()
{
PV.RPC(nameof(MultiUIUpdate), RpcTarget.All);
}
[PunRPC]
void MultiUIUpdate()
{
LobbyUIController.Instance.MultiWaitStart(CurUsers, MaxUsers);
}
멀티플레이 버튼 터치 시 GameManager에서 NetworkManager의 EnterRoom()을 호출하도록 했으며, EnterRoom()에서는 JoinRandomOrCreateRoom() 함수로 최대 인원수가 2인 방에 참여하거나, 없다면 생성하도록 했습니다. 방 입장 완료 후 자동으로 UI를 업데이트 하도록 OnJoinedRoom() 함수에서 UI 갱신 함수를 호출하도록 했으며, 새로운 유저 입장 시 방에 있는 다른 유저들의 UI도 갱신되어야 하므로 RPC로 호출하도록 했습니다.
3. 채팅 구현
우선 채팅 출력용 TMP들을 활성화/비활성화 하여 채팅 출력 및 삭제를 구현하도록 채팅 영역 하위에 TMP 오브젝트들을 넣어두었습니다.
채팅 로직은 LobbyUIController에 작성했으며, 로비 UI용 Input Action을 생성하고 Chat을 Enter 입력으로 정의하여, Enter 입력 시 OnChat()을 실행하도록 했습니다.
[Header("Chatting")]
[SerializeField] TMP_InputField inputField;
[SerializeField] TextMeshProUGUI[] chats;
[SerializeField] int chatRefreshInterval;
int curChatIdx;
int curChatTime;
string myName;
Coroutine chatRoutine;
// 포톤 네트워크
PhotonView PV;
public static LobbyUIController Instance { get; private set; }
void Awake()
{
Instance = this;
PV = GetComponent<PhotonView>();
}
#region 채팅
// Enter 입력 시
// Player Input으로 관리
void OnChat()
{
// 입력란이 활성화된 상태가 아니라면 활성화
if (!inputField.isFocused)
{
inputField.ActivateInputField();
return;
}
// 입력란이 공백 뿐이라면 비활성화
if (inputField.text.Trim() == "")
{
inputField.DeactivateInputField();
return;
}
// 채팅 입력 시 RPC로 전체 유저에게 출력
inputField.ActivateInputField();
PV.RPC(nameof(Chat), RpcTarget.All, myName + ": " + inputField.text);
inputField.text = "";
}
Enter 입력 시 InputField가 활성화된 상태가 아니라면 활성화하고, 이미 활성화된 상태여도 내용이 공백뿐이라면 비활성화 하도록 했습니다. 채팅 내용이 있는 경우에는 채팅 출력용 함수를 해당 내용을 방에 있는 모든 유저들에게 출력하도록 RPC로 실행하고 InputField는 초기화합니다. myName은 유저 ID 출력용 변수로, 우선은 player + 1000~9999사이의 랜덤 숫자 조합으로 생성했습니다.
// 채팅 전체 출력용 RPC 함수
// 채팅창 갱신 주기 초기화
[PunRPC]
void Chat(string chatValue)
{
// 채팅이 꽉 찬 경우
// 오래된 채팅 제거 로직 바로 실행
if(curChatIdx == chats.Length - 1)
RemoveOneChat();
else
curChatIdx++;
chats[curChatIdx].text = chatValue;
chats[curChatIdx].gameObject.SetActive(true);
curChatTime = 0;
// 채팅이 없는 상태였다면 갱신용 코루틴 실행
if (chatRoutine == null)
chatRoutine = StartCoroutine(RefreshChat());
}
Chat() 함수는 채팅 영역에 이미 세팅해둔 TMP 오브젝트들의 배열(chats)에서 현재 chatIdx번째 TMP를 전달받은 채팅 내용으로 갱신한 뒤 활성화하고, curChatTime을 0으로 초기화합니다. 해당 변수는 후술할 채팅창 갱신용 코루틴에서 사용하는 변수로, 새롭게 채팅이 올라오면 기존 채팅 삭제를 보류하기 위한 내용입니다. 갱신용 코루틴은 중복 실행을 막기 위해 실행 시 chatRoutine 변수에 저장하고, 해당 변수가 null일 때만 실행하도록 했습니다.
// 채팅창 갱신용 코루틴
IEnumerator RefreshChat()
{
while (curChatIdx >= 0)
{
yield return WfsManager.Instance.GetWaitForSeconds(1.0f);
curChatTime++;
if(curChatTime > chatRefreshInterval)
{
RemoveOneChat();
curChatTime = 0;
}
}
// 종료 알림
chatRoutine = null;
}
// 채팅 제거 및 끌어오기
void RemoveOneChat()
{
// 현재 채팅이 없다면 패스
if (curChatIdx < 0)
return;
// 채팅 한칸씩 끌어내리기
for(int i = 0; i < curChatIdx; i++)
{
if (!chats[i].gameObject.activeSelf)
break;
chats[i].text = chats[i + 1].text;
}
// 오래된 채팅 제거
chats[curChatIdx].gameObject.SetActive(false);
curChatIdx--;
}
#endregion
RefreshChat()은 curChatIdx가 0이상인 동안(채팅이 존재하는 동안) 1초 단위로 대기하고, curChatTime이 chatRefreshInterval에 도달했을 경우 채팅창 갱신 함수 RemoveOneChat()을 호출합니다. 루프가 끝난 후에는 chatRoutine 변수를 null로 초기화하여 이후 다시 채팅이 올라왔을 경우 해당 코루틴이 실행되도록 합니다. chatRefreshInterval 및 curChatTime은 처음에는 float로 선언하였으나, 매 프레임이나 0.1초 단위로 체크하는 것은 리소스 낭비라고 판단하여 1초 단위로 체크하도록 로직을 바꾸면서 int로 변경하였습니다.
RemoveOneChat() 함수는 현재 활성화된 채팅 내용을 한칸씩 밑으로 복사한 후, 마지막 채팅을 비활성화하여 끌어내리는 역할을 합니다. 사실 채팅 영역에 Vertical Layout Group이 있기 때문에 맨 처음 텍스트만 비활성화해도 동일한 결과를 보여주지만, 채팅이 계속되면 관리가 어려워지므로 위와 같이 구현했습니다.
다음은 위의 내용으로 구현한 채팅 테스트 영상입니다.
4. 남은 이슈들
현재 해결해야될 이슈들은 다음과 같습니다.
- 한글 입력 이슈
한글 입력 시 마지막에 띄어쓰기를 한번 하지 않으면 마지막 글자가 잘려서 출력됨(영상1 뒷부분 참조) - 유저 이름과 채팅 내용 구분
단순 줄바꿈 시 유저 이름 밑으로 채팅 내용이 출력되어 구분이 어려움
ex) player0308: 채팅 줄바꿈 테스트 → player0308: 채팅 줄바꿈 테스트
중입니다. 중입니다.
해당 내용들은 해결하는 대로 포스팅에 추가할 예정입니다.
'포트폴리오' 카테고리의 다른 글
[Unity][포트폴리오] 멀티플레이 관련 이슈 (0) | 2023.06.11 |
---|---|
[Unity][포트폴리오] 진짜 혹은 가짜(True or False) 발판 (0) | 2023.06.10 |
[Unity][포트폴리오] 태그 관리용 클래스 / WaitForSeconds 관리용 클래스 (0) | 2023.05.31 |
[Unity][포트폴리오] 회전 발판(Rotating Platform) (0) | 2023.05.23 |
[Unity][포트폴리오] 이동 발판(Moving Platform) (0) | 2023.05.21 |