지금까지 작업한 서버를 유니티와 연동해보겠다.
우리가 지금까지 만든 서버가 유니티와 100퍼 연동이 가능한것은 아니다. 유니티 자체적으로 정책이 정해져있으니 그것때문에 span 등등을 사용할수 없어 그 부분은 수정해야 할 것이다.
프로젝트 생성 후 클라이언트와 연동
프로젝트 명은 Client라고 해주고 위치는 Server가 있는 곳에 해주었다.
유니티에 dll을 바로 가져다 사용하게 되면 편할 수 있긴 하겠지만 그렇게 할 경우 디버깅이 매우 힘들어진다. 그렇기 때문에 이렇게 쌓은 서버코어 코드들을 유니티에 전부 복사한 다음 걸러내는 과정이 필요하다.
서버 코어에 있는 코드를 유니티로 복사 한 후 확인해보자.
Network에서는 이정도만 사용할 것같다. 만약 필요한게 있다면 그때 추가하도록 하자. 문제는 GenPackets에 있다. 유니티 버전에 따라 다르겠지만 ReadOnlySpan 같은 Span 계열들은 유니티에서 사용할 수 없기 때문에 이부분을 수정해줘야 한다.(2021년 버전 이상이면 전부 사용가능 하다고 한다! 필자는 2022년 버전을 사용하기에 수정하지 않는다.)
이렇게 bat 파일을 이용해 GenPacket의 경로를 유니티에 추가해보자.
그리고 테스트를 해보기 위해 NetworkManager.cs를 만들어 서버와 연결을 해보자.
using DummyClient;
using ServerCore;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using UnityEngine;
public class NetworkManager : MonoBehaviour
{
ServerSession _session = new ServerSession();
// Start is called before the first frame update
void Start()
{
// DNS (Domain Name System)
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 _session; },
1);
}
// Update is called once per frame
void Update()
{
}
}
이제 서버를 실행하고 유니티를 실행해보자.
정상적으로 작동하는 모습을 볼 수 있다.
추가적인 채팅 액션
지금은 여러 더미클라이언트들이 서버로 데이터를 보내면 서버가 다시 클라이언트로 그 데이터를 전송하는걸 유니티에서 로그를 찍어본 모습이다. 이번에는 로그만 찍고 끝나는 것이 아니라 무언가 추가적인 액션을 해보자.
온라인게임이라면 채팅서버가 아닌 환경에서는 스킬을 쓰거나 공격을 하거나 움직임을 처리해야할 것이다.
유니티에 Player라는 임의의 객체를 만들고 다음과 같은 코드로 테스트를 해보자.
class PacketHandler
{
public static void S_ChatHandler(PacketSession session, IPacket packet)
{
S_Chat chatPacket = packet as S_Chat;
ServerSession serverSession = session as ServerSession;
if (chatPacket.playerId == 1)
{
Debug.Log(chatPacket.chat);
GameObject go = GameObject.Find("Player");
if (go == null)
{
Debug.Log("Player not found");
}
else
{
Debug.Log("Player found");
}
}
}
}
자 그러면 코드상으로는 플레이어를 찾던 못 찾던 로그가 찍혀야할 것이다. 하지만 막상 실행해보면 Hello world Iam1 까지만 찍히고 그 뒤는 찍히지 않는다. 딱히 크래쉬가 나지 않는다.(버전에 따라 다르다)
이 이유는 유니티 정책과 메인쓰레드와 관련이 있다.
우리가 이전에 Session을 짠 코드를 보면 Async 계열의 함수를 사용하고 있다. 그렇기 때문에 클라이언트들이 접속할 때 여러 쓰레드들을 쓰레드풀에서 가져와 동작하게 된다.
문제는 유니티 정책 상 게임을 구동하는 쓰레드가 아닌 다른 쓰레드가 Game과 관련된 코드를 사용하지 못하게 되어있기 때문이다.
그렇다면 어떤 방법이 있을까? 바로 S_ChatHandler를 유니티 메인 쓰레드에서 실행하게 하면 된다. 유니티 메인쓰레드로 일감을 옮겨주는 것이다.
패킷을 만들긴 하되, Queue를 만들어서 유니티에서 실행해야할 패킷들을 전부 모아서 하나씩 처리하게끔 하면된다.
using System.Collections.Generic;
public class PacketQueue
{
public static PacketQueue Instance { get; } = new PacketQueue();
Queue<IPacket> _packetQueue = new Queue<IPacket>();
object _lock = new object();
public void Push(IPacket packet)
{
lock (_lock)
{
_packetQueue.Enqueue(packet);
}
}
public IPacket Pop()
{
lock (_lock)
{
if (_packetQueue.Count == 0)
return null;
return _packetQueue.Dequeue();
}
}
}
패킷 매니저에서 패킷을 만들고 핸들러를 호출하고 있는데 패킷을 만드는 것과 핸들러를 호출하는 것을 나눠 그 사이에 Queue를 집어넣으면 될거 같다는 생각이 든다.
using ServerCore;
using System;
using System.Collections.Generic;
using Unity.VisualScripting;
class PacketManager
{
#region Singleton
static PacketManager _instance = new PacketManager();
public static PacketManager Instance { get { return _instance; } }
#endregion
PacketManager()
{
Register();
}
Dictionary<ushort, Func<PacketSession, ArraySegment<byte>, IPacket>> _makeFunc = new Dictionary<ushort, Func<PacketSession, ArraySegment<byte>, IPacket>>();
Dictionary<ushort, Action<PacketSession, IPacket>> _handler = new Dictionary<ushort, Action<PacketSession, IPacket>>();
public void Register()
{
_makeFunc.Add((ushort)PacketID.S_Chat, MakePacket<S_Chat>);
_handler.Add((ushort)PacketID.S_Chat, PacketHandler.S_ChatHandler);
}
public void OnRecvPacket(PacketSession session, ArraySegment<byte> buffer, Action<PacketSession,IPacket> onRecvCallback = null)
{
ushort count = 0;
ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
count += 2;
ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
count += 2;
Func<PacketSession, ArraySegment<byte>, IPacket> func = null;
if (_makeFunc.TryGetValue(id, out func))
{
IPacket packet = func.Invoke(session, buffer);
if (onRecvCallback != null)
onRecvCallback.Invoke(session, packet);
else
HandlerPacket(session, packet);
}
}
T MakePacket<T>(PacketSession session, ArraySegment<byte> buffer) where T : IPacket, new()
{
T pkt = new T();
pkt.Read(buffer);
return pkt;
}
public void HandlerPacket(PacketSession session, IPacket packet)
{
Action<PacketSession, IPacket> action = null;
if (_handler.TryGetValue(packet.Protocol, out action))
action.Invoke(session, packet);
}
}
네트워크 매니저 Update에서 Queue를 꺼내와 HandlerPacket를 실행하면 다음과 같이 동작한다.
패킷을 받는거까지는 됐으니 패킷을 보내는 것도 확인해보자.
using DummyClient;
using ServerCore;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using UnityEngine;
public class NetworkManager : MonoBehaviour
{
ServerSession _session = new ServerSession();
// Start is called before the first frame update
void Start()
{
// DNS (Domain Name System)
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 _session; },
1);
StartCoroutine("CoSendPacket");
}
// Update is called once per frame
void Update()
{
IPacket packet = PacketQueue.Instance.Pop();
if (packet != null)
{
PacketManager.Instance.HandlerPacket(_session, packet);
}
}
IEnumerator CoSendPacket()
{
while (true)
{
yield return new WaitForSeconds(3.0f);
C_Chat chatPacket = new C_Chat();
chatPacket.chat = "Hello Unity";
ArraySegment<byte> segment = chatPacket.Write();
_session.Send(segment);
}
}
}
제대로 확인이 된다!
미니 프로젝트(테스트 용)
기존에 존재하는 패킷은 그냥 단순히 클라이언트가 채팅을 뿌리고 서버가 받아서 모든 클라에게 다시 전달해 주는 아주 간단한 패킷만 존재했다.
그래서 조금 더 감을 잡기위해서 유니티에서 유저들이 접속할 때 플레이어가 생성되고 움직이게 해보자.
먼저 유저가 입장할 때, 이미 접속되어 있는 유저들에게 새로운 유저가 있다고 알려줘야한다.
또한 입장한 유저는 기존 유저의 위치를 전부 받아야한다.
따라서 패킷 구성은 다음과 같이 한다. S로 시작하는 패킷은 서버에서 클라로, C는 클라에서 서버로 전송하는 로직이다.
<?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>
그리고 클라에서 서버로 요청하는 것을 처리하기 위해 다음과 같이 핸들러를 구성한다.
using Server;
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;
class PacketHandler
{
public static void C_LeaveGameHandler(PacketSession session, IPacket packet)
{
C_LeaveGame leavePacket = packet as C_LeaveGame;
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;
GameRoom room = clientSession.Room;
room.Push(
() => room.Move(clientSession, movePacket)
);
}
}
그리고 이제 클라의 요청을 들어주기 위해 컨텐츠단은 다음과 같이 구성한다.
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;
namespace Server
{
class GameRoom : IJobQueue
{
List<ClientSession> _sessions = new List<ClientSession>();
JobQueue _jobQueue = new JobQueue();
List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
public void Push(Action job)
{
_jobQueue.Push(job);
}
public void Flush()
{
// N ^ 2
foreach (ClientSession s in _sessions)
s.Send(_pendingList);
Console.WriteLine($"Flushed {_pendingList.Count} items");
_pendingList.Clear();
}
public void Broadcast(ArraySegment<byte> segment)
{
_pendingList.Add(segment);
}
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());
}
public void Leave(ClientSession session)
{
// 플레이어 제거하고
_sessions.Remove(session);
// 모두에게 알린다.
S_BroadcastLeaveGame leave = new S_BroadcastLeaveGame();
leave.playerId = session.SessionId;
Broadcast(leave.Write());
}
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 = packet.posX;
move.posY = packet.posY;
move.posZ = packet.posZ;
Broadcast(move.Write());
}
}
}
이어서 유니티 작업을 하겠다.
내가 직접 조종하는 Player와 일반적인 Player들이 있다.
그렇기 때문에 MyPlayer라는 클래스는 내가 조종하는것 그 이외의 모든 플레이어는 Player 클래스에서 담당하겠다.
using ServerCore;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyPlayer : Player
{
// Start is called before the first frame update
NetworkManager _network;
void Start()
{
StartCoroutine("CoSendPacket");
_network = GameObject.Find("NetworkManager").GetComponent<NetworkManager>();
}
// Update is called once per frame
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());
}
}
}
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using static S_PlayerList;
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 (var 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);
}
}
}
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);
}
else
{
Player player = null;
if(_players.TryGetValue(packet.playerId, out player))
{
GameObject.Destroy(player.gameObject);
_players.Remove(packet.playerId);
}
}
}
}
'Unity > 온라인 RPG' 카테고리의 다른 글
[데이터베이스] SQL 입문 - SSMS 다루기와 각종 문법 (1) | 2024.02.14 |
---|---|
[데이터베이스] SQL 입문 - 세팅 (0) | 2024.02.13 |
[게임 서버] JobQueue - 서버 과부화를 줄이기 위한 패킷 처리 방법 (0) | 2024.02.05 |
[게임 서버] Job Queue - 채팅 테스트 (0) | 2024.02.01 |
[게임 서버] 패킷 직렬화 - Packet Generator (0) | 2024.01.30 |