Server programming

05_03_유니티와 서버 연동 방법 #3

devRiripong 2023. 5. 21.
반응형

개요

MMO 게임 서버와 클라이언트 간 통신 프로토콜을 만드는 과정을 간략하게 설명하고 있습니다. 유니티 기반 클라이언트와 C#으로 작성된 서버 간 통신을 위해 필요한 패킷 형식을 XML을 통해 정의하고, 이를 처리하는 로직을 서버와 클라이언트에 추가하는 방식입니다.

중요한 부분은 다음과 같습니다:

1. 패킷 정의: XML 파일을 통해 각 패킷의 형식을 정의합니다. 패킷에는 플레이어의 입장 및 퇴장, 플레이어의 이동 등에 대한 정보가 포함됩니다. 이 정보는 서버와 클라이언트 모두가 이해할 수 있는 형식으로 작성되어야 합니다.

 

2. 패킷 처리: 서버와 클라이언트 모두 각 패킷을 처리하는 로직을 구현합니다. 이 로직에는 패킷을 받아서 적절한 작업을 수행하거나, 필요한 정보를 패킷으로 만들어 보내는 작업이 포함됩니다.

 

3. 테스트: 서버와 클라이언트를 실행하여 실제로 패킷이 잘 처리되는지 확인합니다. 이때, 각 패킷에 대한 처리 결과를 로그로 출력하여 테스트의 편의성을 높입니다.


이러한 과정을 통해 실시간 멀티플레이어 게임에서 필요한 네트워킹 기능을 구현할 수 있습니다. 다만, 실제 상용 게임에서는 보안, 네트워크 효율성, 오류 처리 등에 대한 추가적인 고려가 필요합니다.

 

 

 

미니 프로젝트르 만들어 테스트를 해 보기 위해 선행되어야 할 작업을 수행한다.

1.채팅에서 MMO 버전으로 패킷 파일 수정

기존의 패킷 내용을 삭제하고 패킷을 MMO에 맞게 다시 만들어 본다.

 

채팅 서버를 만들 때는 패킷이 2개만 있었다. PDL를 보자.

<?xml version="1.0" encoding="utf-8" ?>
<PDL>
  <packet name ="C_Chat">
    <string name ="chat"/>  
  </packet>
  <packet name ="S_Chat">   
    <int name ="playerId"/>
    <string name ="chat"/>  
  </packet>
</PDL>

C_Chat으로 채팅 매시지를 보내면 S_Chat이라는 서버쪽 답변 메시지 패킷을 이용해 메시지 내용을 뿌리는 간단한 프로그램이었다.

MMO에 대한 개념이 와닿을 수 있게 유니티에서 실린더 모양의 플레이어르 생성해 움직이는 것을 유니티 상에서 확인 할 수 있는 미니프로젝트를 만들어 본다. 

 

ClientSession의 OnConnected에서 Room에 Enter를 실행하는 람다를 Push하고, GameRoom의 Enter에서는 ClientSession session을 _sessions에 넣어주는 작업을 하고 끝내고 있었어. 이유는 Broadcast에서 S_Chat으로 모든 사용자에게 뿌리면 됐기 떄문이야.

하지만 MMO에서는 유저가 방이나 필드 존에 들어왔으면 주변의 유저들에게 보일 수 있게 알려야 해. Enter에 이 내용이 들어가면 된다. 반대로 방금 접속한 유저한테도 주변 사람들의 목록을 전송해 줘야 주변 유저들을 유니티상에 그릴 수 있다. 하나씩 만들어 본다.

네이밍 컨벤션으로 모두한테 보낼 건 Broadcast를 붙인다.

 

1_1. 플레이어가 GameRoom의 Enter로 들어왔을 때 주위 사람들에게 뿌리는 용도

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

 

1_2. 클라이언트에서 나갈게

<packet name ="C_LeaveGame">
  </packet>

 

1_3.모든 유저들한테 플레이어 몇 번 나간대

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

 

1_4.새로 들어온 사람한테 기존 유저들 목록 전달해줄 때

<packet name ="S_PlayerList">
    <list name="player">
      <bool name ="isSelf"/>
      <int name ="playerId"/>
      <float name ="posX"/>
      <float name ="posY"/>
      <float name ="posZ"/>
  </list>"
</packet>

 

1_5.목적지가 어딘지 크라이언트가 요청

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

 

1_6.방에 있는 모든 유저들에게 이동 좌표 쏴줄 때

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

PDL파일을 이렇게 수정하고,

 

1_7. 배치 파일을 돌리기 전에 class PacketFormat에 가서 PacketManaer와 class {0} : IPacket에 public을 붙인다.

PacketGenerator를 빌드하고, GenPackets.bat을 실행한다.

 

2.서버쪽 작업

 

2_1. Server의 ClientSession에서 좌표를 추가한다.

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; }

 

2_2. GameRoom

 

2_2_1. GameRoom의 Flush에서 메시지 출력하는 부분 주석처리

 

2_2_2. GameRoom의 Enter에서 플레이어 추가하고,

신입에게 다른 플레이어의 목록을 전송하고,

신입 입장을 다른 플레이어에게 알린다.

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());
}

 

2_2_3. GameRoom의 Broadcast를 AraySegment를 받아서 _pendingList에 넣기만 하게 수정한다.

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

나중에 Flush에서 Send한다.

 

2_2_4. GameRoom의 Leave에 플레이어를 제거하고 모두에게 알리는 코드를 넣어준다.

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

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

 

2_3. packetHandler

로그아웃 버튼을 누르거나, 이동 할 때

 

2_3_1. C_ChatHandler러를 C_LeaveGameHandler로 바꾼다.

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));
}

 

2_3_2. C_MoveHandler추가

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;

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

 

 2_3_2_1. GameRoom의 Move를 만들어, 좌표를 바꿔주고, 모두에게 알린다.

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()); 
}

 

2_3. Server만 빌드한다.

 

3.DummyClient쪽 작업

일단 빌드를 해보고 에러가 나는 부분을 보며 작업한다. 유니티 클라이언트에서 클라이언트 작업을 해줄 것이기 떄문에 여기서는 빌드를 통과할 수 있게 함수만 만들어 주고 별다른 작업은 안한다.

 

3_1. PacketHandler

PDL를 보면서 맞춰준다.

 

3_1_1. S_ChatHandler를 S_BroadcastEnterGameHandler로 바꿔준다.

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

 

3_1_2. S_PlayerListHandler

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

 

3_1_3. S_BroadcastMoveHandler

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

이렇게 해주면 ClientPacketManager.cs의

	public void Register()
	{
		_makeFunc.Add((ushort)PacketID.S_BroadcastEnterGame, MakePacket<S_BroadcastEnterGame>);
		_handler.Add((ushort)PacketID.S_BroadcastEnterGame, PacketHandler.S_BroadcastEnterGameHandler);
		_makeFunc.Add((ushort)PacketID.S_BroadcastLeaveGame, MakePacket<S_BroadcastLeaveGame>);
		_handler.Add((ushort)PacketID.S_BroadcastLeaveGame, PacketHandler.S_BroadcastLeaveGameHandler);
		_makeFunc.Add((ushort)PacketID.S_PlayerList, MakePacket<S_PlayerList>);
		_handler.Add((ushort)PacketID.S_PlayerList, PacketHandler.S_PlayerListHandler);
		_makeFunc.Add((ushort)PacketID.S_BroadcastMove, MakePacket<S_BroadcastMove>);
		_handler.Add((ushort)PacketID.S_BroadcastMove, PacketHandler.S_BroadcastMoveHandler);
	}

여기서 발생하던 문제가 해결된다.

 

3_2. SessionManager

Generate에서 Session을 만들어서 _sessions에 넣어준 다음에 SendForEach에서 segment를 만들어서 Send해주고 있었는데 C_Chat 패킷이 아니라 C_Move 이동 패킷을 만들어 보낸다.

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());
        }            
    }
}

그리고 Program에서 SendForEach를 호출합니다.

namespace DummyClient
{   
    internal class Program
    {
        static void Main(string[] args)
        {
            // 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 SessionManager.Instance.Generate(); }, 
                10); 

            while(true)
            {
                try
                {
                    SessionManager.Instance.SendForEach();
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.ToString());
                }

                Thread.Sleep(250);
            }          
        }
    }
}

여기까지 빌드를 해보면 빌드가 잘 된다.

 

4.테스트

서버의 PacketHandler에 여기 C_MoveHandler에서 어떤 좌표를 보내주고 있는지 로그를 출력해본다.

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($"{movePacket.posX}, {movePacket.posY}, {movePacket.posZ}");

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

서버와 클라이언트를 실행해보면 Server에서 좌표가 잘 출력되는 것을 볼 수 있다.

 

반응형

댓글