유니티 연동
지난 시간에는 단순 채팅이 아니라, 실제로 플레이어가 움직이는 것과 같이 이동하는 메서드를 콘솔에서 구현하였다. 이 때에 이동하는 패킷을 만들기 위해 PDL 파일을 수정하고, 그에따라 클라이언트/서버에서 추가적으로 로직을 수정해 주었다. 이 떄 Enter, Leave, Move, Player List 등 다양한 메서드들 또한 같이 구현하는 시간을 가져보았다. 오늘은 유니티 연동의 마지막 강의로 콘솔을 이용해 구현하는 것이 아닌 실제로 유니티를 이용해 구현하는 시간을 가진다.
🐛 유니티 버그 잡기
유니티를 구동해서 실행하면, 아래 이미지와 같이 콘솔창에 여러 에러가 우리를 반겨줄 것이다.
하나씩 에러 메시지를 클릭해보며, 이를 수정해보자. 😥
📜 ClientPacketManager.cs
ClientPacketManager의 Register 함수를 확인하면, 패킷 핸들러에서 에러가 나타나면서 불만을 표시하고 있다. 따라서 PacketHandler 스크립트로 넘어가 이를 수정해주자. 해당 부분은 Server 솔루션의 DummyPacket 프로젝트 내 PacketHandler와 미러링되는 부분이다. 따라서 DummyPacket의 PacketHandler에 있는 내용을 통으로 복사하여 유니티 내부 스크립트인 PacketHandle에 붙여넣기 해주자.
using DummyClient;
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
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;
}
}
⛱️ 플레인
이동하는 모습을 보여주기 위해 땅이 필요하다. Plane을 새로 생성해주고, Scale은 모두 10으로 설정해준다. 그리고 색상을 입히기 위해 유니티 프로젝트 뷰에서 우클릭을 해 Create - Material을 생성해준다. 이 후, 생성해준 Material을 클릭하여 Albedo의 값을 5A9EE5로 설정해준다. 색까지 변경한 Material을 드래그 드롭하여 Plane에 설정하면, 바닥 색상이 변경된다. 🫡
🧍♂️Player
플레이어라는 객체가 움직이기 위해서는 플레이어 스크립트가 필요하다. 따라서 Player와 MyPlayer 스크립트를 새롭게 생성해준다. Player는 내가 아닌 다른 플레이어를 의미하고, MyPlayer는 내 캐릭터를 의미한다.
📜 Player.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
public int PlayerId { get; set; }
void Start()
{
}
void Update()
{
}
}
📜 MyPlayer.cs
using ServerCore;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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());
}
}
}
Player와 MyPlayer 스크립트를 생성 후, 작성하다보니 각자의 스크립트에서 Etner, Leave, Move 등 공통으로 구현해야 하는 메서드를 각자의 스크립트에서 또 다시 구현해야 하는 것을 알 수 있다. 따라서 이를 보다 편하고 공통적으로 관리할 수 있도록 새로운 스크립트 PlayerManager 스크립트를 새로 생성한다.
📜 PlayerManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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);
}
}
}
}
위와 같이 구현을 모두 완료했으면, 다시 ClientPacketManager 스크립트로 돌아가, 매니저를 통해 구현한 플레이어를 추가 내용을 작성해주도록 하자.
📜 ClientPacketManager.cs
using DummyClient;
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
class PacketHandler
{
...
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); ⭕
}
...
}
해당 스크립트를 위와 같이 수정하면, 해당 스크립트를 방문한 김에 Enter, Leave, Move 등과 같은 다른 함수들도 생성해주고, PlayerManager에서 다시 구현하도록 하자.
using DummyClient;
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
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 스크립트로 다시 돌아가 구현하도록 하자.
📜 PlayerManager.cs
public void Move(S_BroadcastMove packet)
{
// 이동 동기화는 어려운 부분 중 하나. 이는 두 가지 방법으로 나뉜다.
// 1. 서버쪽에서 허락 패킷이 왔을 때 이동하는 방법
// 2. 클라이언트쪽에서 이동을 하고 서버에게 응답이 올 경우 보정하는 방법
// 현재 구현 방식은 1번 방식을 따른다.
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)
{
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;
// playerId가 진짜 있는걸까요?
if (_players.TryGetValue(packet.playerId, out player))
{
// 찾았으면 삭제를 해줄게요.
Object.Destroy(player.gameObject);
_players.Remove(packet.playerId);
}
}
}
🐪 일감 처리 방식 수정
기존의 NetworkManager의 Update 메서드를 보면, 매 프레임마다 Pop을 한 번만 처리하도록 로직이 작성되어 있다. 이 부분을 프레임마다 모든 일감을 처리하는 방식으로 수정하도록 하자.
📜 PacketQueue.cs
public List<IPacket> PopAll()
{
List<IPacket> list = new List<IPacket>();
lock (_lock)
{
while (_packetQueue.Count > 0)
{
list.Add(_packetQueue.Dequeue());
}
}
return list;
}
📜 NetworkManager.cs
void Update()
{
List<IPacket> list = PacketQueue.instance.PopAll();
foreach (IPacket packet in list)
{
PacketManager.Instance.HandlePacket(_session, packet);
}
}
실행 결과 😎
일반 Player 들은 모두 움직이고 있는데, MyPlayer 스크립트 컴포넌트들 붙인 객체는 움직이지 않는다.. 뭐가 문제일까..? 🤔
📜 PlayerManager.cs
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);
}
}
}
PlayerManager에서 Add를 해줄 때, PlayerId를 등록하지 않아서 생긴 문제였다. Move 메서드에서 PlayerId에 따라서 움직임을 설정해주고 있는데, 아이디를 설정하지 않아서 정상적으로 이동하지 못하는 문제였다.
그런데 위와 같이 수정하니 Player, MyPlayer 모두 움직이지 않는다.. 뭐가 또 문제인가 찾아보니, PlayerManager 스크립트에서 EnterGame 메서드 내부에서 예외처리를 하지 않아 생기는 문제이다. 아래와 같이 다시 수정해주도록 하자.
public void EnterGame(S_BroadcastEnterGame packet)
{
// ⭕
if (_myplayer.PlayerId == packet.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);
}
마지막으로 Server 프로젝트의 DummyClient의 개수를 10이 아닌 500으로 수정해서 정상 작동하는 것을 확인해보자.
📜 Program.cs
using ServerCore;
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace DummyClient
{
class Program
{
static void Main(string[] args)
{
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 SessionManager.Instance.Generate(); },
500); ⭕
while (true)
{
try
{
SessionManager.Instance.SendForEach();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
Thread.Sleep(250);
}
}
}
}
사진이라 500명의 플레이어의 움직임이 다 보이진 않지만, 500명의 플레이어를 정상적으로 소환하고, 또 플레이어들이 정상적으로 움직이는 것을 볼 수 있다.
이상으로 Part 4 게임 서버의 모든 강의를 다 수강하였다. 네트워크간 통신을 직접 구현하고, 구현하면서 생기는 문제를 또 수정하는 시간을 가졌다. 아울러 네트워크 통신에 필요한 최적화 기법들 또한 새롭게 배우는 시간을 가졌다. 새롭고 낯선 개념이 많이 나타나서 조금 어렵긴 했지만, 유니티를 통해 직접 실습해서 통신되는 모습을 보니 정말 신기하다. 앞으로 남은 Part 5 데이터베이스도 열심히 들어보자. 화이팅! 😎
'공부 > 인프런 - Rookiss' 카테고리의 다른 글
Part 5-1-1. 개론 : OT 및 환경 설정 (0) | 2024.01.16 |
---|---|
Part 5. 데이터베이스 (0) | 2024.01.11 |
Part 4-6-3. 유니티 연동 : 유니티 연동 #3 (1) | 2024.01.08 |
Part 4-6-2. 유니티 연동 : 유니티 연동 #2 (1) | 2024.01.03 |
Part 4-6-1. 유니티 연동 : 유니티 연동 #1 (1) | 2024.01.02 |