syntax = "proto3";
package Protocol;
import "google/protobuf/timestamp.proto";
option csharp_namespace = "Google.Protobuf.Protocol";
enum MsgId {
S_ENTER_GAME = 0;
S_LEAVE_GAME = 1;
S_SPAWN = 2; // 주변 플레이어가 S_SPAWN 알림을 받으면 그 플레이어의 클라에서 방금 스폰된 플레이어 만들어줌 (스폰은 플레이어 뿐만 아니라 모든 오브젝트 대상(화살, 몬스터 등))
S_DESPAWN = 3;
C_MOVE = 4; // 클라가 서버로 이동하고 싶다는 패킷 전송
S_MOVE = 5; // 서버가 주변 플레이어들한테 누가 이동하고 있는지 알림
}
message S_EnterGame {
PlayerInfo player = 1;
}
message S_LeaveGame { // 딱히 인자 필요 없음
}
message S_Spawn {
repeated PlayerInfo players = 1; // 여러명이 등장할 수 있으니 list형태 즉 repeated
}
message S_Despawn {
repeated int32 playerIds = 1; // 어떤 애가 없어질지만 알려주면 되니까 사라질 플레이어들의 id
}
message C_Move {
int32 posX = 1;
int32 posY = 2;
}
message S_Move { // 어떤 유저(id)가 어디로 이동하고 싶다는 정보를 인자로.
int32 playerId =1;
int32 posX = 2;
int32 posY = 3;
}
message PlayerInfo {
int32 playerId = 1; // DB에 들어가는 아이디가 아닌, 공격했을 때 구분하는 구분자
string name = 2;
int32 posX = 3;
int32 posY = 4;
}
public class GameRoom
{
object _lock = new object();
public int RoomId { get; set; }
List<Player> _players = new List<Player>();
public void EnterGame(Player newPlayer) { ... }
public void LeaveGame(int playerId) { ... }
}
public void EnterGame(Player newPlayer)
{
if (newPlayer == null)
return;
lock (_lock)
{
_players.Add(newPlayer);
newPlayer.Room = this;
// 본인한테 정보 전송
{
S_EnterGame enterPacket = new S_EnterGame();
enterPacket.Player = newPlayer.Info;
newPlayer.Session.Send(enterPacket);
S_Spawn spawnPacket = new S_Spawn();
foreach (Player p in _players)
{
if (newPlayer != p) // 타인의 정보만
spawnPacket.Players.Add(p.Info);
}
newPlayer.Session.Send(spawnPacket);
}
// 타인한테 정보 전송
{
S_Spawn spawnPacket = new S_Spawn();
spawnPacket.Players.Add(newPlayer.Info);
foreach (Player p in _players)
{
if (newPlayer != p)
p.Session.Send(spawnPacket);
}
}
}
}
S_EnterGame 패킷을 만들고 새로 들어온 플레이어의 정보를 넣어준 후 새로 들어온 플레이어 입장에서 패킷을 서버로 보내준다.
그리고 S_Spawn 패킷을 만들고 현재 GameRoom에 참가 중인 자신을 제외한 모든 플레이어들을 뽑아와서 spawnPacket 내부에 Players 리스트에 정보들을 뽑아 넣어주고, 새로운 플레이어 입장에서 넣어준 S_Spawn 패킷을 쏘아 보내준다.
( 게임룸에 들어왔는데 제 주변에 누가누가 있으니 서버 측에서 제가 보내준 리스트에 따라 제 화면에 보이도록 해주세요!!)
그리고 S_Spawn 패킷을 다시 만들어서 이번에는 새로운 플레이어의 정보를 넣어준다. 그리고 룸에 참가중인 새로운 플레이어를 제외한 모든 플레이어의 입장에서 새로운 플레이어의 정보를 담은 S_Spawn 패킷을 서버로 보낸다. 즉 브로드캐스팅
(각 플레이어마다 새로 들어온 플레이어 정보를 담은 패킷을 쏘는 이유도 마찬가지로 서버한테 화면에 뿌려달라는 요청)
public void LeaveGame(int playerId)
{
lock (_lock)
{
Player player = _players.Find(p => p.Info.PlayerId == playerId);
if (player == null)
return;
_players.Remove(player);
player.Room = null;
// 본인한테 정보 전송
{
S_LeaveGame leavePacket = new S_LeaveGame();
player.Session.Send(leavePacket);
}
// 타인한테 정보 전송
{
S_Despawn despawnPacket = new S_Despawn();
despawnPacket.PlayerIds.Add(player.Info.PlayerId);
foreach (Player p in _players)
{
if (player != p)
p.Session.Send(despawnPacket);
}
}
}
}
플레이어가 나갈 때는 S_Despawn 패킷을 하나 만들고 그 패킷 안에 나갈 플레이어의 아이디를 add한다. (이 코드에서는 한명만 나감) 그리고 현재 나갈 플레이어를 제외한 게임룸에 참가중인 모든 플레이어들 입장에서 Send 함수를 호출하여 despawnPacket을 보내준다.
class PacketManager
{
#region Singleton
static PacketManager _instance = new PacketManager();
public static PacketManager Instance { get { return _instance; } }
#endregion
PacketManager()
{
Register();
}
Dictionary<ushort, Action<PacketSession, ArraySegment<byte>, ushort>> _onRecv = new Dictionary<ushort, Action<PacketSession, ArraySegment<byte>, ushort>>();
Dictionary<ushort, Action<PacketSession, IMessage>> _handler = new Dictionary<ushort, Action<PacketSession, IMessage>>();
public void Register()
{
_onRecv.Add((ushort)MsgId.SEnterGame, MakePacket<S_EnterGame>);
_handler.Add((ushort)MsgId.SEnterGame, PacketHandler.S_EnterGameHandler);
_onRecv.Add((ushort)MsgId.SLeaveGame, MakePacket<S_LeaveGame>);
_handler.Add((ushort)MsgId.SLeaveGame, PacketHandler.S_LeaveGameHandler);
_onRecv.Add((ushort)MsgId.SSpawn, MakePacket<S_Spawn>);
_handler.Add((ushort)MsgId.SSpawn, PacketHandler.S_SpawnHandler);
_onRecv.Add((ushort)MsgId.SDespawn, MakePacket<S_Despawn>);
_handler.Add((ushort)MsgId.SDespawn, PacketHandler.S_DespawnHandler);
_onRecv.Add((ushort)MsgId.SMove, MakePacket<S_Move>);
_handler.Add((ushort)MsgId.SMove, PacketHandler.S_MoveHandler);
}
...
class PacketHandler
{
public static void S_EnterGameHandler(PacketSession session, IMessage packet)
{
S_EnterGame enterGamePacket = packet as S_EnterGame;
ServerSession serverSession = session as ServerSession;
Debug.Log("S_EnterGameHandler");
Debug.Log(enterGamePacket.Player);
}
public static void S_LeaveGameHandler(PacketSession session, IMessage packet)
{
S_LeaveGame leaveGameHandler = packet as S_LeaveGame;
ServerSession serverSession = session as ServerSession;
Debug.Log("S_LeaveGameHandler");
}
public static void S_SpawnHandler(PacketSession session, IMessage packet)
{
S_Spawn spawnPacket = packet as S_Spawn;
ServerSession serverSession = session as ServerSession;
Debug.Log("S_SpawnHandler");
Debug.Log(spawnPacket.Players);
}
public static void S_DespawnHandler(PacketSession session, IMessage packet)
{
S_Despawn despawnPacket = packet as S_Despawn;
ServerSession serverSession = session as ServerSession;
Debug.Log("S_DespawnHandler");
}
public static void S_MoveHandler(PacketSession session, IMessage packet)
{
S_Move movePacket = packet as S_Move;
ServerSession serverSession = session as ServerSession;
Debug.Log("S_MoveHandler");
}
}
받아온 패킷을 처리하는 함수들을 정의했다.
public class Player
{
public PlayerInfo Info { get; set; } = new PlayerInfo();
public GameRoom Room { get; set; }
public ClientSession Session { get; set; }
}
public class PlayerManager
{
public static PlayerManager Instance { get; } = new PlayerManager();
object _lock = new object();
Dictionary<int, Player> _players = new Dictionary<int, Player>();
int _playerId = 1; // TODO
public Player Add()
{
Player player = new Player();
lock (_lock)
{
player.Info.PlayerId = _playerId;
_players.Add(_playerId, player);
_playerId++;
}
return player;
}
public bool Remove(int playerId)
{
lock (_lock)
{
return _players.Remove(playerId);
}
}
public Player Find(int playerId)
{
lock (_lock)
{
Player player = null;
if (_players.TryGetValue(playerId, out player))
return player;
return null;
}
}
}
public class RoomManager
{
public static RoomManager Instance { get; } = new RoomManager();
object _lock = new object();
Dictionary<int, GameRoom> _rooms = new Dictionary<int, GameRoom>();
int _roomId = 1;
public GameRoom Add()
{
GameRoom gameRoom = new GameRoom();
lock (_lock)
{
gameRoom.RoomId = _roomId;
_rooms.Add(_roomId, gameRoom);
_roomId++;
}
return gameRoom;
}
public bool Remove(int roomId)
{
lock (_lock)
{
return _rooms.Remove(roomId);
}
}
public GameRoom Find(int roomId)
{
lock (_lock)
{
GameRoom room = null;
if (_rooms.TryGetValue(roomId, out room))
return room;
return null;
}
}
}
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
MyPlayer = PlayerManager.Instance.Add();
{
MyPlayer.Info.Name = $"Player_{MyPlayer.Info.PlayerId}";
MyPlayer.Info.PosX = 0;
MyPlayer.Info.PosY = 0;
MyPlayer.Session = this;
}
RoomManager.Instance.Find(1).EnterGame(MyPlayer);
}
public override void OnRecvPacket(ArraySegment<byte> buffer)
{
PacketManager.Instance.OnRecvPacket(this, buffer);
}
public override void OnDisconnected(EndPoint endPoint)
{
RoomManager.Instance.Find(1).LeaveGame(MyPlayer.Info.PlayerId);
SessionManager.Instance.Remove(this);
Console.WriteLine($"OnDisconnected : {endPoint}");
}
RoomManager는 GameRoom들을 관리하는 클래스이다. Client와 연결이 되었을 때 ClientSession에서 OnConnected() 함수를 통해 플레이어를 하나 생성하고 RoomManager의 Find(1) 즉 1번 룸(이 예제에선 게임룸 하나 밖에 없음. 각 게임룸은 1번부터 고유 아이디를 가짐)의 EnterGame에 인자로 넘겨준다.
ClientSession의 멤버변수로 MyPlayer가 선언되어 있어서 각 클라이언트 세션마다 하나의 플레이어가 할당되어 있다는 것을 알 수 있고, 룸을 나갈 때도 MyPlayer 변수를 이용하여 PlayerId를 뽑아 LeaveGame 함수의 인자로 전달해준다.