05_02_유니티와 서버 연동 방법#2
개요
유니티 클라이언트의 S_ChatHandler에서 뭔가 추가적인 액션을 하게 수정해봅니다.
플레이어를 찾아서 움직이거나 스킬을 쓰게 하기 위한 첫 걸음으로 유니티가 Player를 찾는 기능을 구현해 봅니다.
유니티가 인식하는 메인 스레드에서 명령이 실행되어야 하므로 패킷을 만드는 것과 핸들링 하는 것을 분리합니다.
패킷을 만들어 큐에 넣어 둔 다음에, 핸들링 하는 건 유니티에서 만든 스크립트에서 실행하게 합니다.
수정된 내용이 적용되어 코드가 자동 생성되게 PacketGenerator를 업데이트 합니다.
패킷을 받는 것 뿐만 아니라 보내는 것도 합니다.
1.유니티가 Player 찾게 하기
1_1. 시도
유니티로 돌아가 실린더를 만들고 이름은 Player로 한다.
유니티 Client의 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);
GameObject go = GameObject.Find("Player");
if (go == null)
Debug.Log("Player not found");
else
Debug.Log("Player found");
}
//if(chatPacket.playerId == 1)
// Console.WriteLine(chatPacket.chat);
}
}
서버를 구동하고, 유니티를 켜서 Play 한다. Console 창에 Player not found나 Player found가 나오지 않고 Hello Server I am 1만 나오고 멈춰있다. 뭔가 잘못되었다는 걸 알 수 있다.
1_2. 문제 원인
Session의 RegisterRecv를 보면 ReciveAsync로 비동기 함수로 받고 OnRecvCompleted를 실행하고 있다. pending이 true가 되면 OnRecvCompleted가 바로 실행되지 않고 나중에 다른 스레드가 이어서 OnRecvCompleted를 호출하게 된다. 그럼 그 메인 스레드가 아닌 스레드가 OnRecv, OnRecvPacket까지 실행하게 될테고 Handler를 실행하게 되어서 PacketHandler까지 메인 스레드가 아닌 스레드가 실행해 유니티 코드인 GameObject.Find를 실행하게 되는데 유니티는 지정한 메인 스레드가 아닌 스레드가 자신의 코드에 접근해 실행하는 걸 차단해 놨다.
S_ChatHandler를 메인 스레드에서 실행하게 조작을 해줘야 한다.
1_3. 해결
일감을 큐에 저장하고 밀어 넣어 준 뒤, 쓰레드에서 걔를 하나씩 꺼내서 시간이 될 때에 처리하게 한다.
1_3_1. 유니티의 Assets/Scripts에 PacketQueue라는 스크립트를 만든다.
MonoBehaviour을 일단은 제거
패킷을 밀어 넣고 뽑는 그런 용도로만 사용
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();
}
}
}
Packet/GenPackets의 interface IPacket을 public으로 해준다.
public interface IPacket
이걸 이용해 메인스레드가 Pop 해서 처리하면 될 거 같다.
1_3_2. PacketQueue에 다가 넣었다가 나중에 유니티에서 만든 스크립트(유니티가 인정한 메인 스레드)인 NetworkManager에서 꺼내 처리하게 한다.
1_3_2_1. MakePacket과 HandlePacket을 분리하고 HandlePacket전 PacketQueue에다 넣기만 한다.
ClientPacketManager에서 Dictionary인 _onRecv에 MakePacket<S_Chat>을 넣고, OnRecvPacket에서 id로 찾아 Invoke 하고 있었어. 그리고 MakePacket에서 S_ChatHandler도 호출해 주고 있었어.
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);
}
PacketQueue에 다가 넣었다가 나중에 유니티에서 만든 스크립트인 NetworkManager에서 핸들하기 위해 기존의 패킷을 만들고 바로 핸들하던 것을
패킷을 만드는 부분과 패킷을 핸들하는 부분을 나누기 위해
ClientPacketManager에 HandlePacket인터페이스를 만든다.
public void HandlePacket(PacketSession session, IPacket packet)
{
Action<PacketSession, IPacket> action = null;
if (_handler.TryGetValue(packet.Protocol, out action))
action.Invoke(session, packet);
}
MakePakcet이 이제 handle하는 걸 호출하지 않으니 만든 패킷을 반환하게 한다.
T MakePacket<T>(PacketSession session, ArraySegment<byte> buffer) where T : IPacket, new()
{
T pkt = new T();
pkt.Read(buffer);
return pkt;
}
Action의 경우 반환값을 받을 수 없는데 MakcePacket이 반환갑이 생겼기 때문에 MakePacket에서 에러가 난다.
Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>> _onRecv = new Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>>();
public void Register()
{
_onRecv.Add((ushort)PacketID.S_Chat, MakePacket<S_Chat>);
_handler.Add((ushort)PacketID.S_Chat, PacketHandler.S_ChatHandler);
}
그래서 Action 대신 Func로 바꿔주고, 반환 값도 받아 줄 수 있는 Dictionary 형태로 수정한다.
Dictionary<ushort, Func<PacketSession, ArraySegment<byte>, IPacket>> _makeFunc = new Dictionary<ushort, Func<PacketSession, ArraySegment<byte>, IPacket>>();
Dictionary<ushort, Action<PacketSession, IPacket>> _handler = new Dictionary<ushort, Action<PacketSession, IPacket>>();
public void Register()
{
_makeFunc.Add((ushort)PacketID.S_Chat, MakePacket<S_Chat>);
_handler.Add((ushort)PacketID.S_Chat, PacketHandler.S_ChatHandler);
}
OnRecvPacket에서 _makeFunc의 결과값 Ipacket packet으로 받게 한다.
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;
Func<PacketSession, ArraySegment<byte>, IPacket> func = null;
if (_makeFunc.TryGetValue(id, out func ))
{
IPacket packet = func.Invoke(session, buffer);
HandlePacket(session, packet);
}
}
지금 상태는 수정하기 전과 마찬가지로 바로 HandlePacket을 호출하는 거니까 OnRecvPacket의 인자에 Action을 하나 추가한다. Action이 있다면 그 액션에 따라 처리를 해주고, 없으면 그냥 바로 Handle한다.
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);
}
}
OnRecvPacket을 Nework/ServerSession에서 호출하고 있었어. 여기서 PacketQueue에다가 넣어 놓는 작업을 하라고 옵션을 넣어준다.
public override void OnRecvPacket(ArraySegment<byte> buffer)
{
PacketManager.Instance.OnRecvPacket(this, buffer, (s, p) => PacketQueue.Instance.Push(p));
}
s도 넣어준 건 인터페이스를 맞추기 위함이다.
1_3_2_2. 유니티에서 만든 메인 스레드 스크립트 파일 NetworkManager에서 Handle한다.
NetworkManager의 Update에 Handle작업을 넣어 준다.
void Update()
{
IPacket packet = PacketQueue.Instance.Pop();
if(packet != null)
{
PacketManager.Instance.HandlePacket(_session, packet);
}
}
그러면 PacketQueue에서 꺼낸 packet을 매개변수로 ClientPacketManager의 HandlePacket이 실행된다.
public void HandlePacket(PacketSession session, IPacket packet)
{
Action<PacketSession, IPacket> action = null;
if (_handler.TryGetValue(packet.Protocol, out action))
action.Invoke(session, packet);
}
그러면 S_ChatHandler가 실행 될텐데 유니티 메인 스레드를 사용하는 것이기 때문에
GameObject go = GameObject.Find("Player");
이 부분이 정상적으로 실행된다.
유니티의 로그에도 Player found가 출력되는 것을 볼 수 있다.
S_ChatHandler에서 이동이나 스킬을 사용하는 패킷을 처리하게 할 수 있다.
1_4. PacketGenerator 갱신하기
1_4_1. PacketFormat의 코드를 ClientPacketManager에 맞춰 수정해 준다.
1_4_2. PacketGenerator 프로젝트를 빌드를 하고, GenPacket.bat을 실행한다.
1_4_3. 구동을 하고 유니티 콘솔창에 잘 뜨는지 확인한다.
2.보내는 작업
2_1. 3초마다 보내는 것을 테스트 하기 위해 NetworkManager에 CoSendPacket함수를 정의한다.
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);
}
}
2_1.NetworkManager의 Start에 실행하는 코드를 넣는다.
void Start()
{
// DNS ( Domain Name System )
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
// ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호 }
// 식당 주소 찾는 부분은 똑같을 거야.
Connector connector = new Connector();
connector.Connect(endPoint,
() => { return _session; },
1);
StartCoroutine("CoSendPacket");
}
2_2. 버퍼가 너무 클 필요가 없으니 SendBuffer에서 버퍼의 크기를 수정한다.
public class SendBufferHelper
{
public static ThreadLocal<SendBuffer> CurrentBuffer = new ThreadLocal<SendBuffer>(() => { return null; });
// 전역이지만 나의 쓰레드에서만 고유하게 사용할 수 있는 전역
public static int ChunkSize { get; set; } = 66535;
2_3. PacketHandler에서 if (chatPacket.playerId == 1) 를 주석처리 해서 받는 메시지를 다 로그에 찍게 한다.
2_4. 서버만 실행하고, 유니티도 실행한다.
“Hello Unity! I am 숫자”가 3초마 뜨는 것을 확인할 수 있다