개요
플레이어의 이동을 서버에 보내는 코드, 클라이언트에서 플레이어를 제어하는 코드, 그리고 서버에서 클라이언트로 데이터를 전송하는 방법을 보여줍니다.
특히 이 예제에서는 네트워크를 통해 플레이어의 위치를 업데이트하고, 여러 플레이어가 게임에서 움직일 수 있도록 만드는 방법에 대해 설명하고 있습니다. 또한, 클라이언트는 서버로부터 플레이어 리스트를 받아오고, 이를 기반으로 새로운 플레이어를 생성하거나 기존 플레이어를 업데이트합니다.
마지막으로, 이 예제는 다수의 플레이어(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
'Server programming' 카테고리의 다른 글
05_03_유니티와 서버 연동 방법 #3 (0) | 2023.05.21 |
---|---|
05_02_유니티와 서버 연동 방법#2 (0) | 2023.05.18 |
05_01_유니티 연동_유니티 연동#1 (0) | 2023.05.17 |
04_07_Job Queue_JobTimer (0) | 2023.05.17 |
04_06_Job Queue_패킷 모아 보내기 (0) | 2023.05.16 |
댓글