공부/인프런 - Rookiss

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

셩잇님 2024. 1. 2. 18:07
반응형

 

 

유니티 연동

 

 지난 시간에는 우선 순위 큐를 이용하여 JobTimer Class를 새로 만들어 주었다. 이 후, Push와 Flush 메서드를 통해 일감들을 등록하고 관리하는 시간을 가졌다. 오늘은 여태까지 만든 이 모든 것들을 유니티를 통해 실제로 연동이 정상적으로 잘 되는지 확인해보며 학습하는 시간을 가져본다.

 


 

🙎‍♂️ 실습에 들어가기 앞서

 

 먼저 좋은 소식과 나쁜 소식이 하나 있다. 좋은 소식은 네트워크 통신을 구현하기 위해 사용했던 스크립트들을 재 사용할 수 있다는 것이고, 나쁜 소식은 유니티의 정책상 C#에서도 허용되는 문법과 그렇지 않은 문법이 있어 GenPacket 스크립트에서 사용한 ReadOnlySpan, TryWriteBytes와 같은 것들을 사용하지 못한다는 것이다.

 

 그러나 24.01.02 기준 강의를 시청하면서 따라해 본 결과 ReadOnlySpan, TryWriteBytes 모두 2021버전 이후 엔진에서 지원하기 때문에 위 나쁜소식은 해당 되지 않는다. 그러나, 강의를 찍고있는 루키스님 시점에서는 위 두 문법이 허용되지 않아, 스크립트의 내용을 수정하시는데 나 또한 위 두 문법을 사용가능함에도 불구하고 루키스님처럼 수정하며 따라하는 방식으로 진행할 것이다.

 


 

🌝 프로젝트 생성

 

 유니티 허브를 통해 새로운 프로젝트를 하나 만들어준다. 폴더의 경로는 여태까지 실습해주었던 프로젝트의 경로로 설정하고, 이름은 Client로 만들어준다. 이 후 스크립트 전용 폴더를 새롭게 생성해준다.

 

👾 파일 복사

 

1. ServerCore 폴더 내에 프로젝트 파일을 제외한 스크립트 파일을 유니티로 복사, 붙여넣기 해준다. 

2. DummyClient 폴더 내에 ServerSession 스크립트와 Packet 폴더를 복사하여 유니티로 붙여넣기 해준다.

3. JobQueue, Listner, Priority Queue 같은 불필요한 스크립트를 삭제하여 준다.

 

따라서 남은 스크립트는 Connector, RecvBuffer, SendBuffer, ServerSession, Session 스크립트와 Packet 폴더 내 ClientPacketManager, GenPacket, PacketHandler 스크립트이다. 이 후, Packet 폴더 내에 있는 스크립트가 아닌 5개의 스크립트를 Network 폴더를 만들어 해당 폴더 산하에 넣어주도록 한다.

 

정리된 스크립트 목록

 

👾 에러 수정

 

 위에서 언급한바와 같이 원래같았으면 GenPacket 내부에 ReadOnlySpan 문법이 에러가 나야 하는데, 나는 비교적 최신 버전 유니티를 사용하고 있는 바람에 해당 문법이 정상적으로 지원이 되서 에러가 나질 않았다. 하지만 그럼에도 불구하고 에러가 난 것처럼 강의 영상과 같이 따라하는 시간을 가졌다.

 


 

📜 GenPacket.cs

 

Read()

 

 GenPacket 내부 스크립트 파일의 C_Chat 클래스의 Read 메서드를 아래와 같이 수정한다. 빨간색으로 보였던 영역이 기존의 소스코드인데, ReadOnlySpan 문법을 이용하여 처리하였던 것을 초록색으로 보이는 영역과 같이 수정하여 준다. 이는 기존에 학습했지만 Byte[] 배열을 복사해서 사용해 메모리를 더 잡아먹는 문제점이 있던 BitConverter.ToUint16을 이용하는 방법이다.

 

 

Write()

 

 Write 또한 Read 메서드와 마찬가지로 빨간색으로 보였던 기존의 소스코드를 초록색으로 보이는 영역과 같이 수정하여 준다.

 

 

 위와 같이 스크립트를 모두 수정하였다면 다시 PacketFormat.cs로 돌아가 수정된 스크립트의 내용에 따라 다시 수정해주어야 한다.. 얼마나 수정하는거니 😭

 


 

📜 PacketFormat.cs

 

 수정해주어야 할 String 변수명은 다음과 같다.

 

packetFormat, memberListFormat, readFormat, readStringFormat, readListFormat, writeFormat, writeStringFormat, writeListFormat

 

위 변수들은 Read, Wirte 메서드에서 수정해 준것과 같이 마찬가지로 변경해주어야 한다. 이를 일일히 나열하기에는 힘드므로 아래 더보기란에 코드 전문을 추가한다.

 

더보기

using System;
using System.Collections.Generic;
using System.Text;

namespace PacketGenerator
{
    class PacketFormat
    {
        // {0} 패킷 등록
        public static string managerFormat =
@"using ServerCore;
using System;
using System.Collections.Generic;

class PacketManager
{{
#region Singleton
static PacketManager _instance = new PacketManager();
public static PacketManager Instance {{ get {{ return _instance; }} }}
#endregion

PacketManager()
{{
        Register();
    }}

Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>> _onRecv = new Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>>();
Dictionary<ushort, Action<PacketSession, IPacket>> _handler = new Dictionary<ushort, Action<PacketSession, IPacket>>();

public void Register()
{{
{0}
}}

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

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

Action<PacketSession, ArraySegment<byte>> action = null;
if (_onRecv.TryGetValue(id, out action))
action.Invoke(session, buffer);
}}

void MakePacket<T>(PacketSession session, ArraySegment<byte> buffer) where T : IPacket, new()
{{
T pkt = new T();
pkt.Read(buffer);
Action<PacketSession, IPacket> action = null;
if (_handler.TryGetValue(pkt.Protocol, out action))
action.Invoke(session, pkt);
}}
}}";

        // {0} 패킷 이름
        public static string managerRegisterFormat =
@" _onRecv.Add((ushort)PacketID.{0}, MakePacket<{0}>);
_handler.Add((ushort)PacketID.{0}, PacketHandler.{0}Handler);";

        // {0} 패킷 이름/번호 목록
        // {1} 패킷 목록
        public static string fileFormat =
@"using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using ServerCore;

public enum PacketID
{{
{0}
}}

interface IPacket
{{
ushort Protocol {{ get; }}
void Read(ArraySegment<byte> segment);
ArraySegment<byte> Write();
}}

{1}
";

        // {0} 패킷 이름
        // {1} 패킷 번호
        public static string packetEnumFormat =
@"{0} = {1},";


        // {0} 패킷 이름
        // {1} 멤버 변수들
        // {2} 멤버 변수 Read
        // {3} 멤버 변수 Write
        public static string packetFormat =
@"
class {0} : IPacket
{{
{1}

public ushort Protocol {{ get {{ return (ushort)PacketID.{0}; }} }}

public void Read(ArraySegment<byte> segment)
{{
ushort count = 0;
count += sizeof(ushort);
count += sizeof(ushort);
{2}
}}

public ArraySegment<byte> Write()
{{
ArraySegment<byte> segment = SendBufferHelper.Open(4096);
ushort count = 0;

count += sizeof(ushort);
Array.Copy(BitConverter.GetBytes((ushort)PacketID.{0}), 0, segment.Array, segment.Offset + count, sizeof(ushort));
count += sizeof(ushort);
{3}

Array.Copy(BitConverter.GetBytes(count), 0, segment.Array, segment.Offset, sizeof(ushort));

return SendBufferHelper.Close(count);
}}
}}
";
        // {0} 변수 형식
        // {1} 변수 이름
        public static string memberFormat =
@"public {0} {1};";

        // {0} 리스트 이름 [대문자]
        // {1} 리스트 이름 [소문자]
        // {2} 멤버 변수들
        // {3} 멤버 변수 Read
        // {4} 멤버 변수 Write
        public static string memberListFormat =
@"public class {0}
{{
{2}

public void Read(ArraySegment<byte> segment, ref ushort count)
{{
{3}
}}

public bool Write(ArraySegment<byte> segment, ref ushort count)
{{
bool success = true;
{4}
return success;
}}
}}
public List<{0}> {1}s = new List<{0}>();";

        // {0} 변수 이름
        // {1} To~ 변수 형식
        // {2} 변수 형식
        public static string readFormat =
@"this.{0} = BitConverter.{1}(segment.Array, segment.Offset + count);
count += sizeof({2});";

        // {0} 변수 이름
        // {1} 변수 형식
        public static string readByteFormat =
@"this.{0} = ({1})segment.Array[segment.Offset + count];
count += sizeof({1});";

        // {0} 변수 이름
        public static string readStringFormat =
@"ushort {0}Len = BitConverter.ToUInt16(segment.Array, segment.Offset + count);
count += sizeof(ushort);
this.{0} = Encoding.Unicode.GetString(segment.Array, segment.Offset + count, {0}Len);
count += {0}Len;";

        // {0} 리스트 이름 [대문자]
        // {1} 리스트 이름 [소문자]
        public static string readListFormat =
@"this.{1}s.Clear();
ushort {1}Len = BitConverter.ToUInt16(segment.Array, segment.Offset + count);
count += sizeof(ushort);
for (int i = 0; i < {1}Len; i++)
{{
{0} {1} = new {0}();
{1}.Read(s, ref count);
{1}s.Add({1});
}}";

        // {0} 변수 이름
        // {1} 변수 형식
        public static string writeFormat =
@"Array.Copy(BitConverter.GetBytes(this.{0}), 0, segment.Array, segment.Offset + count, sizeof({1}));
count += sizeof({1});";

        // {0} 변수 이름
        // {1} 변수 형식
        public static string writeByteFormat =
@"segment.Array[segment.Offset + count] = (byte)this.{0};
count += sizeof({1});";

        // {0} 변수 이름
        public static string writeStringFormat =
@"ushort {0}Len = (ushort)Encoding.Unicode.GetBytes(this.{0}, 0, this.{0}.Length, segment.Array, segment.Offset + count + sizeof(ushort));
Array.Copy(BitConverter.GetBytes({0}Len), 0, segment.Array, segment.Offset + count, sizeof(ushort));
count += sizeof(ushort);
count += {0}Len;";

        // {0} 리스트 이름 [대문자]
        // {1} 리스트 이름 [소문자]
        public static string writeListFormat =
@"
Array.Copy(BitConverter.GetBytes((ushort)this.{1}s.Count), 0, segment.Array, segment.Offset + count, sizeof(ushort));
count += sizeof(ushort);
foreach ({0} {1} in this.{1}s)
{1}.Write(segment, ref count);";

    }
}

 

PacketFormat 스크립트를 모두 수정했으면 PacketGenerator 프로젝트를 빌드하여, ClientPacketManager가 정상적으로 변경되는지 확인하여 준다.

 


 

📜 GenPacket.bat

 

 GenPacket 배치파일을 비주얼 스튜디오로 열어주어 배치파일을 실행할 때 유니티 또한 같이 변경될 수 있도록 추가적으로 아래와 같이 수정하여 준다.

 

START ../../PacketGenerator/bin/Debug/PacketGenerator.exe ../../PacketGenerator/PDL.xml
XCOPY /Y GenPackets.cs "../../DummyClient/Packet"
XCOPY /Y GenPackets.cs "../../Client/Assets/Scripts/Packet" ⭕
XCOPY /Y GenPackets.cs "../../Server/Packet"
XCOPY /Y ClientPacketManager.cs "../../DummyClient/Packet"
XCOPY /Y ClientPacketManager.cs "../../Client/Assets/Scripts/Packet" ⭕
XCOPY /Y ServerPacketManager.cs "../../Server/Packet"

 

 위와 같이 수정한 후, 배치파일을 실행시키면 유니티 프로젝트 내부에 있는 스크립트까지 정상적으로 변경되는 것을 확인할 수 있다.

 


 

📜 NetworkManager.cs

 

 이제는 유니티 프로젝트로 다시 돌아와 새로운 스크립트를 만들어준다. 새로운 스크립트는 NetworkManager 이름으로 생성해주고, 해당 스크립트의 Start 메서드의 Server 솔루션의 DummyClient 프로젝트에 가서 Main 문 내부에 있는 아래 영역을 복사해주어 붙여준다.

 

using DummyClient; ⭕
using ServerCore; ⭕
using System.Collections;
using System.Collections.Generic;
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);
    }
}

 

 이 후, System.Net, DummyClient, ServerCore 등 다양한 네임 스페이스를 붙여주고, ServerSession을 새롭게 만들어주어 Start 메서드 내부에서 _session으로 바로 접속할 수 있게끔 설정해준다. 아울러 커넥트 할때의 개수도 500이 아닌 1로 수정해준다. 이 후 유니티 내부에서 빈 오브젝트를 새롭게 만들어주고, NetworkManager 스크립트를 붙여준다.

 

📜 PacketHandler.cs

 

 PacketHandler 스크립트를 통해, 실질적으로 통신이 잘 이루어지는지 확인하기 위해 로그를 찍어준다. 👍

 

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)
			Debug.Log(chatPacket.chat);
    }
}

 


 

실행결과 😎

 

 테스트를 위해 Server 솔루션 프로젝트를 실행시켜 서버와 클라이언트를 구동시키고, 유니티로 돌아와 플레이 버튼을 누르면 콘솔창에 로그가 뜨는 것을 볼 수 있다. 😮

 

정상적으로 통신되어 로그가 나타난다.

 

매번 콘솔창에서 진행되었던 것들이 유니티를 통해서도 정상작동하는 모습을 보니 정~말 좋다. 역시 이론도 좋지만 실습이 훨씬 재미있는 것 같다.

 

 

 

반응형