이번 포스트에서는 이전에 만든 구조를 조금 더 개량해 간단한 프로젝트를 만들어보겠습니다.
우선, 패킷 구조를 변경하여 현실적인 패킷 구조를 만들어보겠습니다.
1) 패킷 구조 xml
<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>
총 6개의 패킷 구조로 변경되었습니다.
각각 다음과 같이 동작합니다.
S_BroadcastEnterGame 에서는
GameRoom에 입장한 신규 플레이어를 기존의 플레이어들에게 브로드캐스트하고
playerId를 기준으로 지정된 3차원 좌표계에 소환합니다.
C_LeaveGame의 경우 플레이어가 GameRoom을 떠나는 경우에 사용합니다.
S_BroadcastLeaveGame의 경우 플레이어가 GameRoom을 떠나는 경우 사용하고,
playerId를 기준으로 다른 플레이어들에게 브로드캐스트합니다.
S_PlayerList의 경우 현재 GameRoom에 존재하는 플레이어의 목록입니다.
C_Move의 경우 GameRoom에 있는 플레이어가 이동하고 싶은 경우
좌표를 설정하여 이동하는데 사용합니다.
S_BroadcastMove의 경우 GameRoom에 있는 플레이어가 이동하고 싶은 경우
좌표를 설정하여 이동하는데 사용하고 playerId를 기준으로 다른 플레이어들에게 브로드캐스트합니다.
2) ClientSession
class ClientSession : PacketSession
{
public int clientSessionID { get; set; }
public GameRoom Room { get; set; }
public float PosX { get; set; }
public float PosY { get; set; }
public float PosZ { get; set; }
.
.
.
}
서버에 존재하는 ClientSession의 경우, 클라이언트가 이동하고자 하는 경우
이동할 좌표를 받을 수 있도록 좌표계를 프로퍼티로 갖고 있도록 수정합니다.
3) GameRoom
class GameRoom : IJobQueue
{
List<ClientSession> _sessions = new List<ClientSession>();
object _lock = new object();
JobQueue _jobQueue = new JobQueue();
List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
public void Push(Action job)
{
_jobQueue.Push(job);
}
public void Flush()
{
foreach (ClientSession eachSession in _sessions)
eachSession.Send(_pendingList);
_pendingList.Clear();
}
public void Broadcast(ArraySegment<byte> segment)
{
_pendingList.Add(segment);
}
public void Enter(ClientSession session)
{
// 플레이어가 GameRoom에 입장하고
_sessions.Add(session);
session.Room = this;
// 입장한 플레이어에게 기존에 GameRoom에 존재하던 모든 플레이어를 알려줌
S_PlayerList players = new S_PlayerList();
foreach (ClientSession eachSession in _sessions)
{
players._playerList.Add(new S_PlayerList.Player()
{
isSelf = (eachSession == session),
playerId = eachSession.clientSessionID,
posX = eachSession.PosX,
posY = eachSession.PosY,
posZ = eachSession.PosZ,
});
}
session.Send(players.Write());
// 기존에 GameRoom에 존재하던 모든 플레이어에게 새로 입장한 플레이어를 알려줌
S_BroadcastEnterGame enter = new S_BroadcastEnterGame();
enter.playerId = session.clientSessionID;
enter.posX = 0;
enter.posY = 0;
enter.posZ = 0;
Broadcast(enter.Write());
}
public void Leave(ClientSession session)
{
// 플레이어가 퇴장
_sessions.Remove(session);
// GameRoom에 남아있는 모든 유저들에게 퇴장을 알림
S_BroadcastLeaveGame leave = new S_BroadcastLeaveGame();
leave.playerId = session.clientSessionID;
Broadcast(leave.Write());
}
public void Move(ClientSession session, C_Move movePacket)
{
// 좌표를 바꿔주고
session.PosX = movePacket.posX;
session.PosY = movePacket.posY;
session.PosZ = movePacket.posZ;
// 바꾼 좌표를 모두에게 알려줌
S_BroadcastMove move = new S_BroadcastMove();
move.playerId = session.clientSessionID;
move.posX = session.PosX;
move.posY = session.PosY;
move.posZ = session.PosZ;
Broadcast(move.Write());
}
}
GameRoom의 경우, 클라이언트의 입장 / 퇴장 / 이동과 관련된 로직을 수행하는 메서드를 추가합니다.
입장을 수행하는 Enter의 경우,
플레이어가 GameRoom에 입장하고 난후 새로 입장한 플레이어에게 기존의 플레이어들을 알려줍니다.
그 이후, 기존의 플레이어들에게 새로운 플레이어를 알려주고 새로운 플레이어를 원점에 배치합니다.
퇴장을 수행하는 Leave의 경우,더 간단하게 동작합니다.
그저 퇴장한 플레이어를 GameRoom에 남아있는 플레이어들에게 알려줍니다.
마지막으로 이동을 수행하는 Move의 경우
이동을 원하는 클라이언트를 이동시키고, 이동의 결과를 다른 플레이어들에게 알려줍니다.
4) 서버의 PacketHandler
class PacketHandler
{
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));
}
public static void C_MoveGameHandler(PacketSession session, IPacket packet)
{
C_Move movePacket = packet as C_Move;
ClientSession clientSession = session as ClientSession;
if (clientSession.Room == null)
return;
Console.WriteLine($"MovePacket : {movePacket.posX} / {movePacket.posY} / {movePacket.posZ}");
GameRoom room = clientSession.Room;
room.Push(() => room.Move(clientSession, movePacket));
}
}
서버의 패킷핸들러에서는 C_가 수식된 패킷들을 처리해야 합니다.
이들중에서 클라이언트의 퇴장을 처리하는 Leave와 클라이언트의 이동을 처리하는 Move를 처리합니다.
C_LeaveGameHandler 메서드에서는
클라이언트 세션이 캐싱한 GameRoom이 유효하다면
해당 클라이언트를 퇴장시킵니다.
C_MoveGameHandler 메서드에서도
마찬가지로 클라이언트 세션이 캐싱한 GameRoom이 유효하다면
해당 클라이언트가 이동할 좌표를 로그로 남기고 이동시킵니다.
5) 클라이언트의 PacketHandler
class PacketHandler
{
public static void S_BroadcastEnterGameHandler(PacketSession session, IPacket packet)
{
S_BroadcastEnterGame enterPacket = packet as S_BroadcastEnterGame;
ServerSession serverSession = session as ServerSession;
}
public static void S_BroadcastLeaveGameHandler(PacketSession session, IPacket packet)
{
S_BroadcastLeaveGame leavePacket = packet as S_BroadcastLeaveGame;
ServerSession serverSession = session as ServerSession;
}
public static void S_PlayerListHandler(PacketSession session, IPacket packet)
{
S_PlayerList listPacket = packet as S_PlayerList;
ServerSession serverSession = session as ServerSession;
}
public static void S_BroadcastMoveHandler(PacketSession session, IPacket packet)
{
S_BroadcastMove movePacket = packet as S_BroadcastMove;
ServerSession serverSession = session as ServerSession;
}
}
클라이언트의 패킷 핸들러에서는 S_가 수식된 패킷들을 처리합니다.
따라서 해당하는 패킷들을 처리하는 메서드를 추가하였습니다.
6) 클라이언트의 SessionManager
class SessionManager
{
.
.
.
Random _rand = new Random();
.
.
.
public void SendForEach()
{
lock (_lock)
{
foreach (ServerSession eachSession in _sessions)
{
C_Move movePacket = new C_Move();
movePacket.posX = _rand.Next(-50, 50);
movePacket.posY = 0;
movePacket.posZ = _rand.Next(-50, 50);
eachSession.Send(movePacket.Write());
}
}
}
}
클라이언트의 SessionManager에서 SendForEach 메서드를 위와 같이 설정하였습니다.
매 프레임마다 X와 Z 값을 -50에서 50 사이의 값을 정하여 패킷으로 전달하도록 설정하였습니다.
이제 최종실행결과를 알아보겠습니다.