공부/인프런 - Rookiss

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

셩잇님 2024. 1. 8. 12:32
반응형

 

 

유니티 연동

 

 지난 시간에는 지금까지 구축해왔던 클라이언트-서버간 연결 프로젝트를 유니티에 연동하여, 유니티 내에 3D Obejct 실린더로 만든 임시 객체 Player를 3초마다 찾는 실습을 진행해보았다. 이 때 서브 쓰레드의 작동으로 인해 Player를 정상적으로 찾지 못하였다. 따라서 메인 쓰레드에서 함수가 실행되어 플레이어를 찾을 수 있도록 설정해주었다. 또한 과거 Command 패턴 학습방법을 이용해 큐를 생성하여 일감을 던져주는 형태로 마찬가지로 변경하여 정상적으로 Player를 메인 쓰레드에서 찾을 수 있도록 설정하였다.

 


 

🦀 채팅을 넘어, 움직임까지

 

 이전 유니티 연동 시간에서는 단순히 게임을 실행하면, Player를 찾았다는 로그만 덜렁나타나서 이것만 가지고서는 MMO의 개념을 명확하게 보여줄 수 없다. 따라서 이번 시간에는 실질적으로 플레이어가 생성이 되고, 움직이는 것 까지 진행해 볼 예정이다.

 

🧩 패킷 재설계

 

 PDL에 돌아가 패킷을 설계하는 것 부터 다시 시작해보도록 하자. 게임에 입장, 퇴장과 현재 플레이어 목록, 이동과 같은 패킷을 다음과 같이 구현한다.

 

1. 입장

  <packet name="S_BroadcastEnterGame">
    <int name="playerId"/>
    <float name="posX"/>
    <float name="posY"/>
    <float name="posZ"/>
  </packet>

 

2. 퇴장

  <packet name ="C_LeaveGame">
  </packet>
  <packet name="S_BroadcastLeaveGame">
    <int name="playerId"/>
  </packet>

 

3. 현재 플레이어 목록

  <packet name="S_PlayerList">
    <list name="player">
      <bool name="isSelf"/> <!-- 자기 자신인지 판단한다. -->
      <int name="playerId"/>
      <float name="posX"/>
      <float name="posY"/>
      <float name="posZ"/>
    </list>
  </packet>

 

4. 이동

  <packet name="C_Move">
    <float name="posX"/>
    <float name="posY"/>
    <float name="posZ"/>
  </packet>
  <packet name="S_BroadcastMove">
    <int name="playerId"/>
    <float name="posX"/>
    <float name="posY"/>
    <float name="posZ"/>
  </packet>

 

 각각의 패킷은 서버 → 클라에서 보내는 형태와 클라 → 서버에서 보내는 형태로 나뉘어 진다. 아래는 PDL.xml 파일의 전문이다.

 

<?xml version="1.0" encoding="utf-8" ?>
<PDL>
  <packet name="S_BroadcastEnterGame">
    <int name="playerId"/>
    <float name="posX"/>
    <float name="posY"/>
    <float name="posZ"/>
  </packet>
  <packet name ="C_LeaveGame">
  </packet>
  <packet name="S_BroadcastLeaveGame">
    <int name="playerId"/>
  </packet>
  <packet name="S_PlayerList">
    <list name="player">
      <bool name="isSelf"/>
      <int name="playerId"/>
      <float name="posX"/>
      <float name="posY"/>
      <float name="posZ"/>
    </list>
  </packet>
  <packet name="C_Move">
    <float name="posX"/>
    <float name="posY"/>
    <float name="posZ"/>
  </packet>
  <packet name="S_BroadcastMove">
    <int name="playerId"/>
    <float name="posX"/>
    <float name="posY"/>
    <float name="posZ"/>
  </packet>
</PDL>

 

 위와 같이 작성해주고 난 이후에, PacketGenerator를 빌드한 후, GenPacket 배치파일을 실행하여 스크립트를 최신화 해 주도록 한다.

 


 

🧩 서버 로직 수정

 

 Server - Session - ClientSession 스크립트로 돌아가 ClientSession 클래스에 이동 좌표의 값을 저장할 수 있도록 posX, Y, Z를 새롭게 추가하여 준다.

 

class ClientSession : PacketSession
{
	public int SessionId { get; set; }
	public GameRoom Room { get; set; }
	public float posX { get; set; }
	public float posY { get; set; }
	public float posZ { get; set; }

	public override void OnConnected(EndPoint endPoint)
	{
		Console.WriteLine($"OnConnected : {endPoint}");
	
		Program.Room.Push(() => Program.Room.Enter(this));
	}
    
    ...
}

 

 Server - GameRoom 스크립트로 돌아가 Enter 메서드를 수정해주도록 한다. 해당 함수에서 해야 할 일은 다음과 같다.

 

1. 플레이어를 추가하고,

2. 신입생한테 모든 플레이어의 목록을 전송한다.

3. 이 후, 모든 플레이어에게 신입생의 입장을 알려준다.

 

        public void Enter(ClientSession session)
        {
            // 플레이어 추가하고
            _sessions.Add(session);
            session.Room = this;

            // 신입생한테 모든 플레이어 목록 전송
            S_PlayerList players = new S_PlayerList();
            foreach (ClientSession s in _sessions)
            {
                players.players.Add(new S_PlayerList.Player()
                {
                    isSelf = (s == session),
                    playerId = s.SessionId,
                    posX = s.posX,
                    posY = s.posY,
                    posZ = s.posZ
                });
            }
            session.Send(players.Write());

            // 신입생 입장을 모든 플레이어에게 알린다.
            S_BroadcastEnterGame enter = new S_BroadcastEnterGame();
            enter.playerId = session.SessionId;
            enter.posX = 0;
            enter.posY = 0;
            enter.posZ = 0;
            Broadcast(enter.Write());
        }

 

 이 때 Broadcast 메서드에서 에러가 발생하는데, 기존의 Broadcast 메서드는 오로지 채팅 패킷만을 보내라고 하드 코딩이 되어 있는데, Broadcast 메서드는 굉장히 다양하게 사용될 것이기에 이 부분을 모든 세션에서 다 적용해도 문제 없이 작동할 수 있도록 수정해준다.

 

        public void Broadcast(ArraySegment<byte> segment)
        {
            _pendingList.Add(segment);
        }

 

 이제 플레이어가 들어온 엔터를 만들었으니, 나갔을 때를 관리하기 위해 Leave 메서드를 새롭게 만들어주자. 마찬가지로 해당 메서드에서 해야할 일는 다음과 같다.

 

1. 플레이어를 제거하고

2. 모두에게 이를 알려준다.

 

        public void Leave(ClientSession session)
        {
            // 플레이어를 제거하고
            _sessions.Remove(session);

            // 모두에게 알린다.
            S_BroadcastLeaveGame leave = new S_BroadcastLeaveGame();
            leave.playerId = session.SessionId;
            Broadcast(leave.Write());
        }

 

 마지막으로 Move 메서드를 만들어주어야 하는데, 해당 함수는 인터페이스를 어떻게 처리해주어야 할 지 의문이다. 기존 Enter와 Leave 같은 경우는 클라이언트 세션이 접속되거나, 끊겼을 때 메서드가 실행되도록 처리했기 때문에 문제가 없었기 때문이다.

 

 그러나 자세히 살펴보면, 우리는 Server - Packet - PacketHandler에서 이를 처리하고 있었기 때문에 해당 스크립트를 열어 수정하면 된다. 🤣 그러므로 PacketHandler에서 Leave와 Move의 동작을 처리하도록 하자.

 

using Server;
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;

class PacketHandler
{
	public static void C_LeaveGameHandler(PacketSession session, IPacket packet)
	{
		ClientSession clientSession = session as ClientSession;

		if (clientSession.Room == null)
		{
            return;
		}

		GameRoom room = clientSession.Room;
		room.Push(() => room.Leave(clientSession));
	}

    public static void C_MoveHandler(PacketSession session, IPacket packet)
    {
        C_Move movePacket = packet as C_Move;
        ClientSession clientSession = session as ClientSession;

        if (clientSession.Room == null)
        {
            return;
        }

        Console.WriteLine($"C_Move ({movePacket.posX}, {movePacket.posY}, {movePacket.posZ})");

        GameRoom room = clientSession.Room;
        room.Push(() => room.Move(clientSession, movePacket));
    }
}

 

 PacketHandler를 처리했다면, 다시 GameRoom 스크립트로 돌아가 Move 메서드 내부를 채워주도록 하자. Move 메서드에서 해야할 일은 다음과 같다.

 

1. 좌표를 변경한다

2. 이를 모두에게 알린다.

 

        public void Move(ClientSession session, C_Move packet)
        {
            // 좌표를 변경하고
            session.posX = packet.posX;
            session.posY = packet.posY;
            session.posZ = packet.posZ;

            // 모두에게 알린다.
            S_BroadcastMove move = new S_BroadcastMove();
            move.playerId = session.SessionId;
            move.posX = session.posX;
            move.posY = session.posY;
            move.posZ = session.posZ;
            Broadcast(move.Write());
        }

 

서버 로직 수정 끝! 😎

 


 

🧩 클라이언트 로직 수정

 

 클라이언트 로직은 서버 로직과 같이 맞춰주면서 작업해주면 된다. 먼저 DummyClient - Packet - PacketHandler 스크립트를 열어준다. 해당 Handler 에서는 빌드가 통과할 수 있도록 함수만 만들어줄 뿐, 실질적인 작업은 유니티 내부에서 처리할 예정이다.

 

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

class PacketHandler
{
    // 빌드가 통과할 수 있도록 함수만 만들어주고, 별다른 작업은 하지 않는다.
    // 실질적으로는 유니티에서 처리할 예정

    public static void S_BroadcastEnterGameHandler(PacketSession session, IPacket packet)
	{
		S_BroadcastEnterGame pkt = packet as S_BroadcastEnterGame;
		ServerSession serverSession = session as ServerSession;
    }

    public static void S_BroadcastLeaveGameHandler(PacketSession session, IPacket packet)
    {
        S_BroadcastLeaveGame pkt = packet as S_BroadcastLeaveGame;
        ServerSession serverSession = session as ServerSession;
    }

    public static void S_PlayerListHandler(PacketSession session, IPacket packet)
    {
        S_PlayerList pkt = packet as S_PlayerList;
        ServerSession serverSession = session as ServerSession;
    }

    public static void S_BroadcastMoveHandler(PacketSession session, IPacket packet)
    {
        S_BroadcastMove pkt = packet as S_BroadcastMove;
        ServerSession serverSession = session as ServerSession;
    }
}

 

 위와 같이 스크립트를 수정하면, 비주얼 스튜디오에 나타나는 에러들은 거의 다 사라질 것이다. 그러나 DummyClient - SessionManager에서 에러가 날 것인데 SendForEach 부분에서 채팅 패킷을 보내고 있었기 때문이다. 따라서 해당 메서드에서도 채팅 패킷을 보내는 것이 아니라, 이동 패킷을 보내도록 수정한다.

 

class SessionManager
{
    static SessionManager _session = new SessionManager();
    public static SessionManager Instance { get { return _session; } }

    List<ServerSession> _sessions = new List<ServerSession>();
    object _lock = new object();
    Random _rand = new Random(); ⭕

    public void SendForEach()
    {
        lock (_lock)
        {
            foreach (ServerSession session in _sessions)
            {
                C_Move movePacket = new C_Move();
                movePacket.posX = _rand.Next(-50, 50);
                movePacket.posY = 0;
                movePacket.posZ = _rand.Next(-50, 50);
                session.Send(movePacket.Write());
            }
        }
    }
    
    ...
    
}

 

 Random 함수를 이용하여 movePacket의 값을 -50 ~ 50 사이의 값으로 설정해주도록 한다. 이 후, session.Send에 movePacket을 넣어주어 DummyClient - Program 스크립트에서 0.25초씩 자기 좌표를 갱신해 랜덤으로 값을 보내도록 설정한다.

 


 

 

실행 결과 😎

 

 정상적으로 콘솔창에서 로그가 뜨는 것을 볼 수 있다. 😍

 

 

 다음 시간에는 콘솔에서 로그를 보는 것이 아닌, 실질적으로 유니티에서 임시 객체(실린더 형태)로 실습하도록 하자. 

 

 

반응형