1 분 소요

Intro

이번 포스팅에서는 채팅 시스템을 구현해보았습니다.

image

  • 멀티씬 입장시 채팅활성화

  • 엔터키입력으로 채팅 입력모드 ON/OFF

  • 새로운 채팅을 포커싱(스크롤가능)

GUI

채팅창 UI의 전체 구성입니다.

image

image

  • 스크롤뷰를 사용하여 위아래로 스크롤이 가능한 형태의 채팅창을 만들었습니다.

  • InputField에 입력된 채팅을 Content에 표시하게됩니다.
    • Content 오브젝트에는 Virtical Layout Group, Content Size Fitter 컴포넌트를 추가하였습니다.
  • ChatLog 프리팹을 생성하여 플레이어가 채팅을 입력할때마다 채팅창에 아래방향으로 쌓아가는 형태로 구현하였습니다
    • ChatLog 프리팹은 Text UI입니다.

구현

ChatManager 클래스는 채팅시스템을 담당하는 클래스입니다.

public class ChatManager : MonoBehaviourPunCallbacks
{
    [SerializeField] private TMP_InputField inputField;             // 채팅 입력란
    [SerializeField] private ScrollRect scrollRect;                 // 채팅 스크롤뷰

    [SerializeField] private GameObject chatLogPrefab;              // 채팅로그 프리팹
    [SerializeField] private Transform contentTransform;            // 채팅로그가 생성될 위치

    private Queue<GameObject> messageQueue = new();
    private int maxMessages = 10;                           // 최대 10개 메세지 표시
    private bool isInputActive = false;                     // 입력모드인지 여부

    private void Start()
    {
        // 플레이어 입장 메세지
        string temp = $"<color=green>{PhotonNetwork.NickName} Joined the chat room.</color>";
        photonView.RPC("ReceiveMessage",RpcTarget.All, temp);

        // 엔터키 입력
        inputField.onSubmit.AddListener(_ => SendChatMessage());
        inputField.DeactivateInputField();
    }

    private void Update()
    {
        if(Input.GetKeyDown(KeyCode.Return))
        {
            // 입력모드가 아닐 경우 -> 입력시작
            if(!isInputActive)
            {
                isInputActive = true;
                GameManager.Instance.isChatting = true;
                inputField.ActivateInputField();
            }
            else
            {
                // 입력모드일 때 -> 텍스트가 없으면 포커스해제
                if(string.IsNullOrWhiteSpace(inputField.text))
                {
                    isInputActive = false;
                    GameManager.Instance.isChatting = true;
                    inputField.DeactivateInputField();
                    EventSystem.current.SetSelectedGameObject(null);        // 포커스해제
                }
            }
        }

    }

    // 채팅 전송
    private void SendChatMessage()
    {
        if(!string.IsNullOrEmpty(inputField.text))
        {
            // Player : ~~~
            string message = PhotonNetwork.NickName + ": " + inputField.text;
            photonView.RPC("ReceiveMessage", RpcTarget.All, message);
            inputField.text = "";               // 입력창 초기화

            // 포커스 유지
            inputField.ActivateInputField();
        }
    }

    [PunRPC]
    void ReceiveMessage(string message)
    {
        AppendMessage(message);
    }

    void AppendMessage(string message)
    {
        // 채팅로그 생성
        GameObject go = Instantiate(chatLogPrefab, contentTransform);
        TMP_Text text = go.GetComponent<TMP_Text>();

        text.text = "";
        StartCoroutine(AssignMessageDelayed(text, message));
        
        messageQueue.Enqueue(go);
        if(messageQueue.Count > maxMessages)
        {
            GameObject oldestMessage = messageQueue.Dequeue();
            Destroy(oldestMessage);
        }

        ScrollToBottom();
    }

    // 새로운 채팅으로 포커싱
    void ScrollToBottom()
    {
        StartCoroutine(ScrollToBottomCoroutine());
    }

    private IEnumerator ScrollToBottomCoroutine()
    {
        yield return new WaitForEndOfFrame();

        // 아래로 스크롤
        scrollRect.verticalNormalizedPosition = 0f;

        // Canvas 갱신
        Canvas.ForceUpdateCanvases();
    }

    // 이전 메세지 출력 방지
    private IEnumerator AssignMessageDelayed(TMP_Text text, string message)
    {
        yield return null;
        text.SetText(message);
    }
}
  • 주요로직은 AppendMessage 메서드입니다.
    • 플레이어가 채팅을 치고 엔터키 입력을하면 채팅로그 프리팹을 생성하여 채팅창(Content영역)에 출력합니다.
    • 엔터키 입력을 받기위해 inputField의 onSubmit 이벤트를 활용하였습니다.
  • ScrollToBottom() 메서드는 새로운 채팅로그가 생겼을 때 거기에 포커싱하기위한 메서드입니다.

  • AssignMessageDelayed() 코루틴은 채팅로그의 생성직후 레이아웃등의 계산이 완료되지 않았을 수 있기때문에 한 프레임 뒤 텍스트를 할당하여 문제를 방지합니다.
    • 없이도 정상동작한다면 필요없습니다.

댓글남기기