공부/인프런 - Rookiss

Part 4-6-2. 유니티 연동 : 유니티 연동 #2

셩잇님 2024. 1. 3. 15:19
반응형

 

 

유니티 연동

 

 지난 시간에는 유니티 연동 실습을 위해 프로젝트를 하나 새롭게 생성하고, 기존에 사용했던 스크립트들와 새롭게 생성한  NetworkManager를 통해 유니티에서 실제로 연동이 되는지 확인하는 시간을 가져 보았다. 오늘도 이어서 추가적으로 연동하는 법을 배워보도록 하자.

 


 

👢 비동기 처리로 인한 문제

 

📜 PacketHandler.cs

 

 이전 시간에는 PacketHandler 스크립트 내부에 채팅 핸들러로 들어왔을 때 로그만 찍어줬다면 오늘은 이 부분을 수정하여 실질적인 액션을 취할 수 있도록 수정해보자. 따라서 유니티 내부에서 실린더 오브젝트를 새롭게 만들어주고 이름을 "Player"로 설정하도록 하자.

 

using DummyClient;
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;

class PacketHandler
{
	public static void S_ChatHandler(PacketSession session, IPacket packet)
	{
		S_Chat chatPacket = packet as S_Chat;
		ServerSession serverSession = session as ServerSession;
        
		if (chatPacket.playerId == 1)
		{
            Console.WriteLine(chatPacket.chat);

			GameObject go = GameObject.Find("Player");
            if (go == null)
            {
                Debug.Log("Player not found");
            }
			else
			{
                Debug.Log("Player found");
            }
        }
    }
}

 

 위 로직을 살펴보면 GameObjetct.Find 메서드를 통해 Player를 찾는 로직이다. 만약 플레이어를 찾을 경우, Player Found가 나타나고, 그렇지 않을경우 Player not found가 나타날 것이다. 이제 작업이 정상적으로 동작하는지 확인해보자. 서버 솔루션 프로젝트를 동작하여 서버를 켜주고, 유니티 엔진으로 돌아가 게임을 실행하면 된다.

 

게임 실행 결과. Find 이후가 정상적으로 나타나지 않는다.

 

 

 그러나 막상 실행해보면 Hello Server, I am 1 까지 나타나는 것을 볼 수 있으나, 정작 그 이상으로는 메시지가 처리되지 않는 것을 볼 수 있다. 왜 로그 메시지가 안나타나는 것일까? 바로 이 부분이 메인 스레드와 관련된 문제이다.

 

 기존의 우리가 작성하던 Server 솔루션의 로직은 비동기로 네트워크 통신을 하고 있었다. 따라서 유니티에서 지금 구동하고 있는 메인 스레드가 네트워크 패킷을 실행하는 것이 아닌, 스레드 풀에 있던 스레드가 패킷 핸들러를 실행하여 문제가 되는 것이다. 그러나 유니티는 다른 스레드에서 게임과 관련된 부분을 접근해서 실행하는 것을 원천적으로 차단을 해놓았다. 따라서 코드가 실행되고 있지 않는 것이다.

 


 

☠️ 어떻게 해결해야 할까?

 

 그렇다면 우리가 여태까지 작업한 코드들은 모두 쓸모없어지는 것일까? 그건 아니다. 바로 PacketHandler 클래스를 서브 스레드가 아닌 메인 스레드로 동작하여 실행하게끔 하면 된다. 따라서 S_ChatHandler에서 로직을 처리하는 것이 아닌 일전에 JobQueue와 같이 큐를 하나 생성해주어 일감을 등록하고, 처리하는 로직으로 구분지어 사용하는 방법으로 나누어준다.

 

📜 PacketQueue.cs

 

 유니티로 돌아가 새로운 스크립트인 PacketQueue 클래스를 만들어준다. PacketQueue는 메인 스레드와 백그라운드 스레드 네트워크를 처리하는 통로라는 개념으로 이해하면 된다. 따라서 Pop, Push 메서드를 통해 메인 쓰서드에서는 Pop 메서드를 통해 일감을 꺼내와서 메인 스레드에서 처리하고, 백그라운드 스레드는 일감을 PacketQueue의 Push를 통해 일감을 등록해준다. 물론 이 때에도 멀티스레드 환경을 고려해야 하기 때문에 lock을 이용하여 작업을 처한다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PacketQueue
{
    public static PacketQueue instance { get; } = new PacketQueue();

    Queue<IPacket> _packetQueue = new Queue<IPacket>();
    object _lock = new object();

    public void Push(IPacket packet)
    {
        lock (_lock)
        {
            _packetQueue.Enqueue(packet);
        }
    }

    public IPacket Pop()
    {
        lock ( _lock)
        {
            if (_packetQueue.Count == 0)
                return null;

            return _packetQueue.Dequeue();
        }
    }
}

 


 

🫨 일감 등록

 

 그렇다면 일감을 어디에서 등록을 해줘야할까? 만약 PacketHandler 내부에서 처리한다면 자동화해서 처리하는 로직이 의미가 없어지기 때문이다. 따라서 보다 윗 단계인 ClientPacketManager 에서 이를 처리하도록 한다. OnRecvPacket 메서드를 살펴보면 패킷을 만들어 준 다음에 핸들러를 호출해주는 부분이 있는데, 해당 부분을 분리하여 처리하도록 하자.

 

 

 왼쪽에 빨간색 영역이 기존과 같은 코드인데, action을 TryGetValue하여 값이 있을 경우 Invoke 해주는 것을 볼 수 있다. 따라서 해당 영역을 패킷을 만들기는 하지만, 당장 처리하지 않고 패킷 큐에 넣어주는 작업으로 변경한다. 이렇게 될 경우 나중에 NetworkManager에서 Update 문을 순회하며 패킷 큐에 넣어준 작업을 꺼내 핸들러에게 전달해주면 동작이 보다 수월해 질 것이다. 

 

변경 후 스크립트

Dictionary<ushort, Func<PacketSession, ArraySegment<byte>, IPacket>> _makeFunc = new Dictionary<ushort, Func<PacketSession, ArraySegment<byte>, IPacket>>();

public void OnRecvPacket(PacketSession session, ArraySegment<byte> buffer, Action<PacketSession, IPacket> onRecvCallback = null)
	{
		ushort count = 0;

		ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
		count += 2;
		ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
		count += 2;

        Func<PacketSession, ArraySegment<byte>, IPacket> func = null;
		if (_makeFunc.TryGetValue(id, out func))
		{
            IPacket packet = func.Invoke(session, buffer);

			if (onRecvCallback != null)
                onRecvCallback.Invoke(session, packet);
            else
                HandlePacket(session, packet);
		}
	}

	T MakePacket<T>(PacketSession session, ArraySegment<byte> buffer) where T : IPacket, new()
	{
		T pkt = new T();
		pkt.Read(buffer);
		return pkt;
	}

	public void HandlePacket(PacketSession session, IPacket packet)
	{
        Action<PacketSession, IPacket> action = null;
        if (_handler.TryGetValue(packet.Protocol, out action))
		{
            action.Invoke(session, packet);
		}
    }

 

 이 떄 MakePacket은 void형을 반환했었는데, 이제는 패킷을 반환해야 하므로 제네릭 형식의 T로 변경해주고, 변경된 형태에 따라 더이상 Action으로 처리하는 것이 아닌 Func 메서드로 처리하는 형태로 변경하여 준다. 따라서 Dictionary _onRecv도 이름을 변경해주고 IPacket을 추가하여 이를 받아 처리할 수 있도록 한다.

 

📜 SeverSession.cs

 

 변경된 OnRecvPacket 메서드의 인자 값에 따라 ServerSession 에서도 해당 메서드를 때 호출하는 인자 값을 수정해주어야 한다. 다음과 같이 수정한다.

 

        public override void OnRecvPacket(ArraySegment<byte> buffer)
        {
            PacketManager.Instance.OnRecvPacket(this, buffer, (s, p) => PacketQueue.instance.Push(p));
        }

 


 

🫨 일감 처리

 

 그렇다면 일감은 어디에서 처리해주어야 할까? 이는 간단하다. 현재 유니티에서 사용하고 있는 매니저인 NetworkManager에서 이를 처리하면 된다. NetworkManager는 MonoBehavior를 상속받고 있는 상태기 때문에 메인 스레드에서 구동하기 때문이다. 

 

 

 NetworkManager Update를 다음과 같이 작성한 후, 정상적으로 잘 동작하는지 다시한번 테스트 해보자. Server 솔루션에서 서버를 실행시키고 유니티로 돌아와 프로그램을 실행시키면 다음과 같이 정상적으로 작동하는 것을 볼 수 있다.

 

 


 

📜 PacketFormat.cs

 

 ClientPacketManager를 변경했기 때문에 오늘도 PacketFormat을 수정해주어야 한다.. 😥 수정해주어야 할 부분은 managerFormat, managerRegisterFormat, fileFormat 부분이다. 열심히.. 자동화를 위해 수정해주자..

 

 모두 수정했다면 다시한번 PacketGenerator를 빌드하여, GenPacket 배치 파일을 실행하여 파일들을 최신화 하도록 하자.

 


 

실행결과 😎

 

📜 NetworkManager.cs

using DummyClient;
using ServerCore;
using System;
using System.Collections;
using System.Net;
using UnityEngine;

public class NetworkManager : MonoBehaviour
{
    ServerSession _session = new ServerSession();

    void Start()
    {
        string host = Dns.GetHostName();
        IPHostEntry ipHost = Dns.GetHostEntry(host);
        IPAddress ipAddr = ipHost.AddressList[0];
        IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);

        Connector connector = new Connector();
        connector.Connect(endPoint,
            () => { return _session; },
            1);

        StartCoroutine("CoSendPacket");
    }

    void Update()
    {
        IPacket packet = PacketQueue.instance.Pop();
        if (packet != null)
        {
            PacketManager.Instance.HandlePacket(_session, packet);
        }
    }

    IEnumerator CoSendPacket()
    {
        while (true)
        {
            yield return new WaitForSeconds(3.0f);

            C_Chat chatPacket = new C_Chat();
            chatPacket.chat = $"Hello Unity!";
            ArraySegment<byte> segment = chatPacket.Write();
            _session.Send(segment);
        }
    }
}

 

 NetworkManager에 Start문에 코루틴을 실행시키도록 하여 3초마다 패킷 전송이 일어나게 해보자. 위와 같이 스크립트를 작성하여 준다. 

 

 또한, SendBuffer 클래스에서 정해준 ChunkSize의 크기를 65535로 변경하고, Server 솔루션에서 여태까지는 DummyClient 와 Server가 같이 실행되었던 시작 프로그램 구성도 이제는 Server만 동작하도록 Server 프로젝트를 우클릭 하여 시작 프로젝트로 구성한다.

 

 이제 다시 Server 솔루션에서 프로그램을 동작시켜 서버를 실행시키고, 유니티를 실행하면 정상적으로 로그가 뜨는 것을 볼 수 있다.

 

 

 

 

반응형