멀티플레이 환경 구성
이제부터는 유니티를 멀티 플레이 환경에서 처리하기 위해 작업을 할 것이다. 이전 포스팅에서 작업했던 서버를 가져오는 과정에서 이미 goole protobuf를 이용해 어느정도 패킷을 손쉽게 처리 해놓은 상태임을 감안하자.
일단 패킷을 만들고 보내는 것을 자동화부터 한 후에 멀티플레이 환경을 작업하자.
public void Send(IMessage packet)
{
string msgName = packet.Descriptor.Name.Replace("_", string.Empty);
MsgId msgId = (MsgId)Enum.Parse(typeof(MsgId), msgName);
ushort size = (ushort)packet.CalculateSize();
byte[] sendBuffer = new byte[size + 4];
Array.Copy(BitConverter.GetBytes(size + 4), 0, sendBuffer, 0, sizeof(ushort));
Array.Copy(BitConverter.GetBytes((ushort)msgId), 0, sendBuffer, 2, sizeof(ushort));
Array.Copy(packet.ToByteArray(), 0, sendBuffer, 4, size);
Send(new ArraySegment<byte>(sendBuffer));
}
클라 세션쪽에 Send라는 인터페이스를 하나 더 만들어서 패킷에 맞는 msgId를 찾아내 전송하게 끔 수정했다.
이제 클라쪽에서 멀티 플레이 환경을 구성해보겠다.
유니티에서 클라이언트를 2개 이상 실행하고 싶을땐 어떻게 할 수 있을까? 유니티 정책상 같은 프로젝트는 한 개 밖에 실행이 안되게끔 되어있다.
그래서 대부분 빌드를 한 후에 여러 클라이언트를 틀어서 확인하는 경우를 채택하곤 한다.
그러면 이게 최선일까?? 매번 테스트를 하려면 코드를 바꿀 때마다 빌드를 다시해서 클라이언트를 여러개 트는것은 매우 비효율적이다.
우리가 유니티를 다루면서 가장 잊지 말아야하는 1순위는 툴로 가능한 모든 건 코드로도 가능하다 이다.
그렇기에 에디터에 빌드하고 여러개의 클라를 한번에 틀어주는 기능을 만들어보자.
MultiplayersBuildAndRun 이라는 c# 파일을 만들어서 작업한다.
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
public class MultiplayersBuildAndRun
{
[MenuItem("Tools/Run Multiplayer/2 Players")]
static void PerformWin64Build2()
{
PerformWin64Build(2);
}
[MenuItem("Tools/Run Multiplayer/3 Players")]
static void PerformWin64Build3()
{
PerformWin64Build(3);
}
[MenuItem("Tools/Run Multiplayer/4 Players")]
static void PerformWin64Build4()
{
PerformWin64Build(4);
}
static void PerformWin64Build(int playerCount)
{
EditorUserBuildSettings.SwitchActiveBuildTarget(
BuildTargetGroup.Standalone, BuildTarget.StandaloneWindows
);
for ( int i = 0; i < playerCount; i++ )
{
BuildPipeline.BuildPlayer(GetScenePaths(),
"Builds/Win64/" + GetProjectName() + i.ToString() + "/" + GetProjectName() + i.ToString() + ".exe",
BuildTarget.StandaloneWindows64, BuildOptions.AutoRunPlayer
);
}
}
static string GetProjectName()
{
string[] s = Application.dataPath.Split('/');
return s[s.Length - 2];
}
static string[] GetScenePaths()
{
string[] scenes = new string[EditorBuildSettings.scenes.Length];
for (int i = 0; i < scenes.Length; i++)
{
scenes[i] = EditorBuildSettings.scenes[i].path;
}
return scenes;
}
}
2,3,4명의 플레이어를 조절해 자동으로 빌드 후 실행하는걸 확인할 수 있다.
게임 입장
게임에 유저가 입장하고 그 입장했다는걸 서버에 보낸후 다시 모든 유저에게 입장했다는 사실을 알려줘야한다. 그 뒤로부터는 이동하는 패킷이라던지 공격하는 패킷을 다뤄볼 것이다.
일단 패킷부터 설계하는 것이 조금 더 효율적이긴하다.
일단 생각해보면 플레이어가 입장, 퇴장 하는 패킷이 필요하고, 플레이어나 몬스터가 스폰 또는 디스폰 되는 패킷이 필요하다. 그리고 클라이언트에서 움직이는 것과 서버에서 움직이는 패킷이 필요하기에 총 6 개의 패킷이 필요하다.
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_DESPAWN = 3;
C_MOVE = 4;
S_MOVE = 5;
}
message S_EnterGame {
PlayerInfo player = 1;
}
message S_LeaveGame {
}
message S_Spawn {
repeated PlayerInfo players = 1;
}
message S_Despawn {
repeated int32 playerIds = 1;
}
message C_MOVE {
int32 posX = 1;
int32 posY = 2;
}
message S_MOVE{
int32 playerId =1;
int32 posX = 2;
int32 posY = 3;
}
message PlayerInfo {
int32 playerId = 1;
string name = 2;
int32 posX = 3;
int32 posY = 4;
}
이게 검증된 패킷은 아니지만 대략적으로 이런 느낌을 가지고 시작을 해보자. 불필요한 부분이나 필요한 부분이 있다면 그때마다 수정하도록 하겠다.
이제 ClientSession에서 플레이가 OnConnected가 됐을 때, 맵에 입장시켜주어야한다.
그러기 위해서는 플레이어를 만들어주는 팩토리가 필요하기에 따라서 4개의 스크립트를 만든다.
using Google.Protobuf.Protocol;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace Server.Game
{
public class GameRoom
{
object _lock = new object();
public int RoomId { get; set; }
List<Player> _players = new List<Player>();
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);
}
}
}
}
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);
}
}
}
}
}
}
using Google.Protobuf.Protocol;
using System;
using System.Collections.Generic;
using System.Text;
namespace Server.Game
{
public class Player
{
public PlayerInfo Info { get; set; } = new PlayerInfo();
public GameRoom Room { get; set; }
public ClientSession Session { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace Server.Game
{
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;
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 roomId)
{
lock (_lock)
{
Player player = null;
if (_players.TryGetValue(roomId, out player))
{
return player;
}
return null;
}
}
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace Server.Game
{
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 OnDisconnected(EndPoint endPoint)
{
RoomManager.Instance.Find(1).LeaveGame(MyPlayer.Info.PlayerId);
SessionManager.Instance.Remove(this);
Console.WriteLine($"OnDisconnected : {endPoint}");
}
이제 세션이 서버와 접속을 했을 때 플레이어를 생성할 수 있게 서버쪽 작업은 끝났다. 사실 OnConnected가 됐을 때 플레이어에 대한 정보나 각종 정보를 클라쪽에게 보내 로딩 시키고 클라쪽에서 로딩이 완료됐다는 패킷을 서버에 보내면 그때 플레이어를 접속시키는게 올바른 방식이지만 우리는 플레이어에 대한 정보를 Db를 이용해서 관리하는 것이 아니고 그냥 테스트 용도로 확인하는거기 때문에 연결이 되자마자 플레이어를 생성하는 식으로 진행했다.
이제 클라이언트 쪽에서 로그를 찍어보면 된다.
위에서 만든 멀티플레이 환경 구성에서 실행한 상태이다. 이제 유니티에서 실행을 한 화면을 보자.
플레이어가 입장했을 때, 자신의 정보를 서버로 부터 받았고, 주변에 어떤 플레이어가 있는지 즉, 총 2명의 플레이어가 이미 방안에 있었다고 spawn 패킷을 받은 모습이다.
이때 한 플레이어가 나가면 despawn이 될것이다.
'Unity > 온라인 RPG' 카테고리의 다른 글
[Unity 2D] 서버 연동 - 스킬 동기화와 히트 판정 (0) | 2024.03.11 |
---|---|
[Unity 2D] 서버 연동 - MyPlayer 분리 및 이동 동기화 (0) | 2024.03.08 |
[Unity 2D] 컨텐츠 준비 - Monster AI(Patrol AI, Search AI, Skill AI) (0) | 2024.03.05 |
[Unity 2D] 컨텐츠 준비 - 스킬(평타, 화살) 사용하기 (1) | 2024.03.05 |
[Unity 2D] 컨텐츠 준비 - MapManager, Controller 정리, ObjectManager (0) | 2024.03.04 |