Server programming

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

devRiripong 2023. 5. 23.
반응형

개요

플레이어의 이동을 서버에 보내는 코드, 클라이언트에서 플레이어를 제어하는 코드, 그리고 서버에서 클라이언트로 데이터를 전송하는 방법을 보여줍니다.

특히 이 예제에서는 네트워크를 통해 플레이어의 위치를 업데이트하고, 여러 플레이어가 게임에서 움직일 수 있도록 만드는 방법에 대해 설명하고 있습니다. 또한, 클라이언트는 서버로부터 플레이어 리스트를 받아오고, 이를 기반으로 새로운 플레이어를 생성하거나 기존 플레이어를 업데이트합니다.

마지막으로, 이 예제는 다수의 플레이어(500개까지)를 동시에 처리할 수 있음을 보여줍니다. 이를 통해 Unity와 C#을 사용하여 대규모 멀티플레이어 게임을 개발할 수 있음을 알 수 있습니다.
 
 

1.유니티 작업에 들어가기 전에


1_1. 로그 안보이게

Server의 PacketHandler의

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

몇 백개 테스트를 하면 너무 로그가 많아질테니 로그 찍는 부분을 주석처리 한다.


1_2.유니티 에러 

유니티로 돌아가면 에러가 뜨고 있다.
DummyClient의 PacketHandler부분을 복사해서 유니티의 Client에 붙여넣기를 한다.
C_Chat은 없어졌기 때문에 에러가 발생 중이다.



2.유니티 작업

하고 싶은 건 Player 뿐만 아니라 다른 애들을 출력하고 이동하는 것을 구현하는 것이다.


2_1.오브젝트, 카메라 작업

유니티에서 플레인을 만들고 크기를 10으로 한다.
Assets에 마테리어를 만들고 이름을 Floor로 하고 파란색으로 한다. 플레인에 드래그 앤 드롭한다.
적당한 위치로 카메라를 조절하고 메인 카메라를 선택한 뒤 ctrl+shift+f를 누른다.


2_2. 스크립트 작업


2_2_1. Player, MyPlayer

Player, MyPlayer라는 스크립트를 생성한다.

public class Player : MonoBehaviour
{
    public int PlayerId { get; set; }

     void Start()
    {
        
    }

    void Update()
    {
        
    }
}

NetworkManager의 CoSendPacket과 StartCoroutine을 MyPlayer에 옮긴다.

public class MyPlayer : Player
{
     void Start()
    {
        StartCoroutine("CoSendPacket");
    }

    void Update()
    {
        
    }

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

            C_Move movePacket = new C_Move();
            movePacket.posX = UnityEngine.Random.Range(-50, 50); 
            movePacket.posY = 0; 
            movePacket.posZ = UnityEngine.Random.Range(-50, 50); 

            _session.Send(segment);
        }
    }
}

_session이 NetworkManager변수니 NetworkManager에 Send인터페이스를 만들어 준다.

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

    public void Send(ArraySegment<byte> sendBuff)
    {
        _session.Send(sendBuff);
    }

MonoBehaviour를 상속받은 NetworkManager는 유니티의 Object에 있어야 하는데 이걸 Find함수로 찾아서 빼오면 너무 느리니 MyPlayer의 Start에서 한번만 하게 한다.

public class MyPlayer : Player
{
		NetworkManager _network; 

     void Start()
    {
        StartCoroutine("CoSendPacket");
        _network = GameObject.Find("NetworkManager").GetComponent<NetworkManager>();
    }

그리고 만들어 줬던 Send 인터페이스를 이용하면 된다.

public class MyPlayer : Player
{
    NetworkManager _network; 

     void Start()
    {
        StartCoroutine("CoSendPacket");
        _network = GameObject.Find("NetworkManager").GetComponent<NetworkManager>();
    }

    void Update()
    {
        
    }

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

            C_Move movePacket = new C_Move();
            movePacket.posX = UnityEngine.Random.Range(-50, 50); 
            movePacket.posY = 0; 
            movePacket.posZ = UnityEngine.Random.Range(-50, 50);

            _network.Send(movePacket.Write());
        }
    }
}


2_2_2. S_PlayerList로 들어온 패킷을 PlayerManager에서 핸들링

Scripts에 PlayerManager 파일을 생성한다.
MonoBehaviour를 뗀다.

public class PlayerManager
{
    MyPlayer _player; 
    Dictionary<int, Player> _players = new Dictionary<int, Player>();

    public static PlayerManager Instance { get; } = new PlayerManager();
		
		public void Add(S_PlayerList packet)
    {

    }
}

Player의 패킷의 인터페이스를 맞춰주기 위해 Assets/Resources폴더를 만들어 주고 Player를 드래그앤 드랍해 프리팹으로 만들어준다. 그리고 이것을 사용하기 위해
PlayerManager에서 로드하고, instantiate해준다.

public class PlayerManager
{
    MyPlayer _myPlayer; 
    Dictionary<int, Player> _players = new Dictionary<int, Player>();

    public static PlayerManager Instance { get; } = new PlayerManager();

    public void Add(S_PlayerList packet)
    {
        Object obj = Resources.Load("Player");

        foreach (S_PlayerList.Player p in packet.players)
        {
            GameObject go = Object.Instantiate(obj) as GameObject;

            if(p.isSelf)
            {
                MyPlayer myPlayer = go.AddComponent<MyPlayer>();
                myPlayer.transform.position = new Vector3(p.posX, p.posY, p.posZ);
                _myPlayer = myPlayer; 
            }
            else
            {
                Player player = go.AddComponent<Player>();
                player.transform.position = new Vector3(p.posX, p.posY, p.posZ);
                _players.Add(p.playerId, player); 
            }
        }
    }
}


2_2_3.유니티 클라이언트의 PacketHandler에서 연동

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

        PlayerManager.Instance.EnterGame(pkt);
    }

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

        PlayerManager.Instance.LeaveGame(pkt);
    }

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

        PlayerManager.Instance.Add(pkt); 
    }

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

        PlayerManager.Instance.Move(pkt);
    }
}

일단 이렇게 인터페이스를 적어주고
PlayerManager에서 Add에 이어 Move, EnterGame, LeaveGame구현

````public void Move(S_BroadcastMove packet)
    {
        if (_myPlayer.PlayerId == packet.playerId)
        {
            _myPlayer.transform.position = new Vector3(packet.posX, packet.posY, packet.posZ);
        }
        else
        {
            Player player = null; 
            if(_players.TryGetValue(packet.playerId, out player))
            {
                player.transform.position = new Vector3(packet.posX, packet.posY, packet.posZ);
            }
        }
    }

    public void EnterGame(S_BroadcastEnterGame packet)
    {
        if (packet.playerId == _myPlayer.PlayerId)
            return;

        Object obj = Resources.Load("Player");
        GameObject go = Object.Instantiate(obj) as GameObject;

        Player player = go.AddComponent<Player>();
        player.transform.position = new Vector3(packet.posX, packet.posY, packet.posZ);
        _players.Add(packet.playerId, player);
    }

    public void LeaveGame(S_BroadcastLeaveGame packet)
    {
        if(_myPlayer.PlayerId == packet.playerId)
        {
            GameObject.Destroy(_myPlayer.gameObject);
            _myPlayer = null; 
        }
        else
        {
            Player player = null;
            if(_players.TryGetValue(packet.playerId, out player))
            {
                GameObject.Destroy(player.gameObject);
                _players.Remove(packet.playerId); 
            }            
        }
    }
}


2_2_4.NetworkManager의 Update에서 Pop을 한 프레임에 한번 해주고 있던걸 PopAll할 수 있게 PacketQueue에 인터페이스 추가

public List<IPacket> PopAll()
    {
        List<IPacket> list = new List<IPacket>();

        lock(_lock)
        {
            while (_packetQueue.Count > 0)
                list.Add(_packetQueue.Dequeue()); 
        }

        return list; 
    }

NetworkManager의 Update에서 사용

void Update()
    {
        List<IPacket> list = PacketQueue.Instance.PopAll(); 
        foreach(IPacket packet in list)
            PacketManager.Instance.HandlePacket(_session, packet);        
    }


3.유니티 구동


3_1. 첫 시도

서버를 구동하고, 유니티를 구동한다. 10개의 실린더가 돌아다닌다.

DummyClient가 이동하는 거.
MyPlayer는 안움직이고 있어.


3_2.MyPlayer를 움직이게 하자.

PlayerManager의 Add에서 playerId를 세팅해주는 걸 추가한다.

public void Add(S_PlayerList packet)
    {
        Object obj = Resources.Load("Player");

        foreach (S_PlayerList.Player p in packet.players)
        {
            GameObject go = Object.Instantiate(obj) as GameObject;

            if(p.isSelf)
            {
                MyPlayer myPlayer = go.AddComponent<MyPlayer>();
                myPlayer.PlayerId = p.playerId; 
                myPlayer.transform.position = new Vector3(p.posX, p.posY, p.posZ);
                _myPlayer = myPlayer; 
            }
            else
            {
                Player player = go.AddComponent<Player>();
                player.PlayerId = p.playerId;
                player.transform.position = new Vector3(p.posX, p.posY, p.posZ);
                _players.Add(p.playerId, player); 
            }
        }
    }

그리고
Server의 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());
		}

신입생이 입장하면 그 신입을 제외해고 보내야 하는데
PlayerManager의 EnterGame에서

public void EnterGame(S_BroadcastEnterGame packet)
    {
        if (packet.playerId == _myPlayer.PlayerId)
            return;

        Object obj = Resources.Load("Player");
        GameObject go = Object.Instantiate(obj) as GameObject;

        Player player = go.AddComponent<Player>();
        player.transform.position = new Vector3(packet.posX, packet.posY, packet.posZ);
        _players.Add(packet.playerId, player);
    }

이렇게 자신이면 return을 해준다. 그러면 2중으로 보낸게 해결이 된다.


3_3.갯수를 500개로 실험한다.

connector.Connect(endPoint, 
				() => { return SessionManager.Instance.Generate(); },
				500);

 
 
여기까지 수고했다.
 
다시 첨부터 10번정도 반복하자.

출처: https://inf.run/KsPE

반응형

댓글