Inflearn - '[C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part9: MMO 컨텐츠 구현 (DB, 대형구조, 라이브 준비)'를 스터디하며 정리한 글입니다.
현재 몬스터들도 일괄적으로 한꺼번애 Update 되고 있음
-> 자기가 각각 필요한 틱에 따라서 Update 될 수 있게 개선
현재 사용 중인 몬스터의 Update 방식은 여러 틱을 사용함
단순하게 Push(~) Job 방식으로 바꿔주기만 하면 꼬일 위험이 큼
IJob
을 return 하도록 처리JobSerializer.cs
public IJob PushAfter(int tickAfter, IJob job)
{
_timer.Push(job, tickAfter);
return job;
}
public IJob PushAfter(int tickAfter, Action action) { return PushAfter(tickAfter, new Job(action)); }
...
Execute()
가 실행되지 않게 됨Job.cs
public abstract class IJob
{
// 실행
public abstract void Execute();
public bool Cancel { get; set; }
}
// 인자가 없는 Job
public class Job : IJob
{
Action _action;
public Job(Action action)
{
_action = action;
}
public override void Execute()
{
if(Cancel == false)
_action.Invoke();
}
}
...
Monster.cs
IJob _job;
// FSM (Finite State Machine)
public override void Update()
{
switch(State)
{
case CreatureState.Idle:
UpdateIdle();
break;
case CreatureState.Moving:
UpdateMoving();
break;
case CreatureState.Skill:
UpdateSkill();
break;
case CreatureState.Dead:
UpdateDead();
break;
}
// 5프레임 (0.2 초마다 한번씩 Update)
if (Room != null)
_job = Room.PushAfter(200, Update);
}
public override void OnDead(GameObject attacker)
{
// 죽었다면 예약해둔 일감 취소
if(_job != null)
{
_job.Cancel = true;
_job = null;
}
...
}
대현 구조 관리를 위한 이론
현재까지 하나의 방에서의 멀티 플레이 동기화 OK
N^N
으로 늘어나기 때문에유저가 두 영역을 계속 넘나든다면
두 플레이어가 전투를 한다고 가정
공간 단위로 따로 처리
JobSerializer 모든 객체에 둠 - Object 하나하나가 GameRoom이 됨
GameRoom보다 더 넓은 영역을 관리할 수 있도록 서버의 구조를 변경
현재 사용자들이 뭉쳤을 때 부하가 굉장히 커질 수 있는 부분
쓰레드 분배 상황
1. Recv (N개) - 서빙
2. RoomManager (1개) - 요리사
3. DB (1개) - 결제/장부
RoomManager.cs -> GameLogic.cs
GameLogic.cs
public class GameLogic : JobSerializer
{
public static GameLogic Instance { get; } = new GameLogic();
Dictionary<int, GameRoom> _rooms = new Dictionary<int, GameRoom>();
int _roomId = 1;
public void Update()
{
Flush();
// 가진 모든 방을 돌면서 Update
foreach(GameRoom room in _rooms.Values)
{
room.Update();
}
}
// GameRoom 생성
public GameRoom Add(int mapId)
{
GameRoom gameRoom = new GameRoom();
//gameRoom.Init(mapId);
gameRoom.Push(gameRoom.Init, mapId);
gameRoom.RoomId = _roomId;
_rooms.Add(_roomId, gameRoom);
_roomId++;
return gameRoom;
}
// GameRoom 제거
public bool ReMove(int roomId)
{
return _rooms.Remove(roomId);
}
// GameRoom 찾음
public GameRoom Find(int roomId)
{
GameRoom room = null;
if (_rooms.TryGetValue(roomId, out room))
return room;
return null;
}
}
->
ClientSession_PreGame.cs
public void HandleEnterGame(C_EnterGame enterGamePacket)
{
...
// GameLogic을 담당하는 Thread에게 일감을 넘김
GameLogic.Instance.Push(() =>
{
// 1번방에 플레이어 입장
GameRoom room = GameLogic.Instance.Find(1);
room.Push(room.EnterGame, MyPlayer);
});
}
GameLogic을 하나의 Thread로만 관리하기 때문에 일감이 밀릴 수 있는 부분이 있으면 모든 로직이 밀릴 수 있음
ClientSession.cs
List<ArraySegment<byte>> _reserveQueue = new List<ArraySegment<byte>>();
// Send할 목록을 예약만 함
public void Send(IMessage packet)
{
...
lock(_lock)
{
// 일단 예약만 하고 넘겨줌
_reserveQueue.Add(sendBuffer);
}
//Send(new ArraySegment<byte>(sendBuffer));
}
// 실제 Network IO 보내는 부분
public void FlushSend()
{
List<ArraySegment<byte>> sendList = null;
lock(_lock)
{
// 복사만 해주고 초기화함
if (_reserveQueue.Count == 0)
return;
sendList = _reserveQueue;
_reserveQueue = new List<ArraySegment<byte>>();
}
Send(sendList);
}
Program.cs
static void GameLogicTask()
{
while(true)
{
GameLogic.Instance.Update();
Thread.Sleep(0);
}
}
static void DbTask()
{
while (true)
{
// 일단 무식하게 무한루프로 Flush
DbTransaction.Instance.Flush();
// 실행권을 잠시 넘겨줘서 CPU 낭비 방지
Thread.Sleep(0);
}
}
static void NetworkTask()
{
while(true)
{
List<ClientSession> sessions = SessionManager.Instance.GetSessions();
foreach (ClientSession session in sessions)
{
session.FlushSend();
}
Thread.Sleep(0);
}
}
static void Main(string[] args)
{
...
Console.WriteLine("Listening...");
// GameLogicTask
{
// 쓰레드 풀링X -> 별도로 하나를 더 파줌
Task gameLogicTask = new Task(GameLogicTask, TaskCreationOptions.LongRunning);
gameLogicTask.Start();
}
// NetworkTask
{
// 쓰레드 풀링X -> 별도로 하나를 더 파줌
Task networkTask = new Task(NetworkTask, TaskCreationOptions.LongRunning);
networkTask.Start();
}
DbTask();
}
Program.cs
static void Main(string[] args)
{
...
Console.WriteLine("Listening...");
// DbTask
{
Thread t = new Thread(DbTask);
t.Name = "DB";
t.Start();
}
// NetworkTask
{
Thread t = new Thread(NetworkTask);
t.Name = "Network Send";
t.Start();
}
// GameLogic
Thread.CurrentThread.Name = "GameLogic";
GameLogicTask();
}
Thread 분배 상황
+ Monster AI (1개) 분배 필요
기존
Map.cs
의FindPath()
가 맵이 어마어마하게 커질 경우 부하가 엄청나게 됨
HashSet
or Dictionary
로 변경HashSet<Pos>
의 경우 Pos에 대한 비교가 필요하게 됨
Map.cs
public struct Pos
{
public Pos(int y, int x) { Y = y; X = x; }
public int Y;
public int X;
public static bool operator==(Pos lhs, Pos rhs)
{
return lhs.Y == rhs.Y && lhs.X == rhs.X;
}
public static bool operator!=(Pos lhs, Pos rhs)
{
return !(lhs == rhs);
}
public override bool Equals(object obj)
{
return (Pos)obj == this;
}
public override int GetHashCode()
{
// X와 Y가 같은 경우 같은 HashCode를 return해야함
// X와 Y를 합침
long value = (Y << 32) | X;
return value.GetHashCode();
}
public override string ToString()
{
return base.ToString();
}
}
public List<Vector2Int> FindPath(Vector2Int startCellPos, Vector2Int destCellPos, bool checkObject = false)
{
List<Pos> path = new List<Pos>();
// 점수 매기기
// F = G + H
// F = 최종 점수 (작을 수록 좋음, 경로에 따라 달라짐)
// G = 시작점에서 해당 좌표까지 이동하는데 드는 비용 (작을 수록 좋음, 경로에 따라 달라짐)
// H = 목적지에서 얼마나 가까운지 (작을 수록 좋음, 고정)
// (y, x) 이미 방문했는지 여부 (방문 = closed 상태)
//bool[,] closed = new bool[SizeY, SizeX]; // CloseList
HashSet<Pos> closeList = new HashSet<Pos>(); // CloseList
// (y, x) 가는 길을 한 번이라도 발견했는지
// 발견X => MaxValue
// 발견O => F = G + H
//int[,] open = new int[SizeY, SizeX]; // OpenList
Dictionary<Pos, int> openList = new Dictionary<Pos, int>(); // OpenList
/*for (int y = 0; y < SizeY; y++)
for (int x = 0; x < SizeX; x++)
open[y, x] = Int32.MaxValue;*/
//Pos[,] parent = new Pos[SizeY, SizeX];
Dictionary<Pos, Pos> parent = new Dictionary<Pos, Pos>();
// 오픈리스트에 있는 정보들 중에서, 가장 좋은 후보를 빠르게 뽑아오기 위한 도구
PriorityQueue<PQNode> pq = new PriorityQueue<PQNode>();
// CellPos -> ArrayPos
Pos pos = Cell2Pos(startCellPos);
Pos dest = Cell2Pos(destCellPos);
// 시작점 발견 (예약 진행)
//open[pos.Y, pos.X] = 10 * (Math.Abs(dest.Y - pos.Y) + Math.Abs(dest.X - pos.X));
openList.Add(pos, 10 * (Math.Abs(dest.Y - pos.Y) + Math.Abs(dest.X - pos.X)));
pq.Push(new PQNode() { F = 10 * (Math.Abs(dest.Y - pos.Y) + Math.Abs(dest.X - pos.X)), G = 0, Y = pos.Y, X = pos.X });
//parent[pos.Y, pos.X] = new Pos(pos.Y, pos.X);
parent.Add(pos, pos);
while (pq.Count > 0)
{
// 제일 좋은 후보를 찾는다
//PQNode node = pq.Pop();
PQNode pqNode = pq.Pop();
Pos node = new Pos(pqNode.Y, pqNode.X);
// 동일한 좌표를 여러 경로로 찾아서, 더 빠른 경로로 인해서 이미 방문(closed)된 경우 스킵
if (closeList.Contains(node))
continue;
// 방문한다
//closed[node.Y, node.X] = true;
closeList.Add(node);
// 목적지 도착했으면 바로 종료
if (node.Y == dest.Y && node.X == dest.X)
break;
// 상하좌우 등 이동할 수 있는 좌표인지 확인해서 예약(open)한다
for (int i = 0; i < _deltaY.Length; i++)
{
Pos next = new Pos(node.Y + _deltaY[i], node.X + _deltaX[i]);
// 유효 범위를 벗어났으면 스킵
// 벽으로 막혀서 갈 수 없으면 스킵
if (next.Y != dest.Y || next.X != dest.X)
{
if (CanGo(Pos2Cell(next), checkObject) == false) // CellPos
continue;
}
// 이미 방문한 곳이면 스킵
if (closeList.Contains(next))
continue;
// 비용 계산
int g = 0;// node.G + _cost[i];
int h = 10 * ((dest.Y - next.Y) * (dest.Y - next.Y) + (dest.X - next.X) * (dest.X - next.X));
// 다른 경로에서 더 빠른 길 이미 찾았으면 스킵
/*if (open[next.Y, next.X] < g + h)
continue;*/
int value = 0;
if (openList.TryGetValue(next, out value) == false)
value = Int32.MaxValue;
if (value < g + h)
continue;
// 예약 진행
//open[next.Y, next.X] = g + h;
if (openList.TryAdd(next, g + h) == false)
openList[next] = g + h;
pq.Push(new PQNode() { F = g + h, G = g, Y = next.Y, X = next.X });
//parent[next.Y, next.X] = new Pos(node.Y, node.X);
if (parent.TryAdd(next, node) == false)
parent[next] = node;
}
}
return CalcCellPathFromParent(parent, dest);
}
GameRoom을 Zone 단위로 나눠서 보다 효율적으로 Broadcasting 할 수 있도록 작업
GameRoom이 어마어마하게 커졌다고 했을 때 Broadcasting 범위를 어떻게 설정해야 할까
노란색 박스 - 자신의 영역(관심 영역)
자신의 영역 내에 있을 경우 서로 Broadcasting을 주고 받아야 함
플레이어가 움직일 때마다 자신과 모든 상대방의 위치를 비교하여 영역 안에 있을 경우 통신해야 함
-> 굉장히 비효율적
GameRoom을 더 작은 가상 단위인 Zone으로 쪼개서 관리
다수의 존에 걸칠 경우
-> 최대 4개까지 검사하긴 해야 함
-> 4개 이상은 겹치지 못하도록 영역과 Zone의 크기를 조정
Zone.cs
public class Zone
{
public int IndexY { get; private set; }
public int IndexX { get; private set; }
// Zone에 있는 플레이어들
public HashSet<Player> Players { get; set; } = new HashSet<Player>();
public Zone(int y, int x)
{
IndexY = y;
IndexX = x;
}
// 조건에 맞는 한 플레이어 서치
public Player FindOne(Func<Player, bool> condition)
{
foreach(Player player in Players)
{
if (condition.Invoke(player))
return player;
}
return null;
}
// 조건에 맞는 모든 플레이어 서치
public List<Player> FindAll(Func<Player, bool> condition)
{
List<Player> findList = new List<Player>();
foreach (Player player in Players)
{
if (condition.Invoke(player))
findList.Add(player);
}
return findList;
}
}
GameRoom.cs
public Zone[,] Zones { get; private set; }
// 하나의 존의 범위
public int ZoneCells { get; private set; }
public Zone GetZone(Vector2Int cellPos)
{
// Cell 좌표에서 Grid 좌표로 변환
// Grid 좌표에서 ZoneCell 단위로 변환
int x = (cellPos.x - Map.MinX) / ZoneCells;
int y = (Map.MaxY - cellPos.y) / ZoneCells;
if (x < 0 || x >= Zones.GetLength(1))
return null;
if (y < 0 || y >= Zones.GetLength(0))
return null;
return Zones[y, x];
}
public void Init(int mapId,int zoneCells)
{
Map.LoadMap(mapId,"../../../../../Common/MapData");
// Zone 초기화
ZoneCells = zoneCells;
// Ex) 10 zoneCells
// 1~10칸 = 1존
// 11~20칸 = 2존
// 21~30칸 = 3존
int countY = (Map.SizeY + zoneCells - 1) / zoneCells;
int countX = (Map.SizeX + zoneCells - 1) / zoneCells;
Zones = new Zone[countY, countX];
for(int y = 0; y < countY; y++)
{
for(int x = 0; x<countX; x++)
{
Zones[y, x] = new Zone(y, x);
}
}
...
}
EnterGame()
: 처음 위치 맞는 Zone에 플레이어 배치 필요
LeaveGame()
: 플레이어가 있던 Zone에서 제거 필요
Broadcast()
: 내 영역의 사각형 네 모서리가 속한 Zone에 대해서만 전송
public void Broadcast(Vector2Int pos,IMessage packet)
{
List<Zone> zones = GetAdjacentZones(pos);
foreach(Zone zone in zones)
{
foreach(Player p in zone.Players)
{
p.Session.Send(packet);
}
}
or
foreach(Player p in zones.SelectMany( z => z.Players))
{
p.Session.Send(packet);
}
}
// 내 영역에 속하는 Zone 리스트
public List<Zone> GetAdjacentZones(Vector2Int cellPos, int cells = 5) // 반경
{
HashSet<Zone> zones = new HashSet<Zone>();
int[] delta = new int[2] { -cells, +cells };
foreach(int dy in delta)
{
foreach(int dx in delta)
{
int y = cellPos.y + dy;
int x = cellPos.x + dx;
Zone zone = GetZone(new Vector2Int(x, y));
if (zone == null)
continue;
zones.Add(zone);
}
}
return zones.ToList();
}
Map.cs
public bool ApplyMove(GameObject gameObject, Vector2Int dest)
{
// 오브젝트가 원래 있던 위치 비워줌
...
// 목표 위치로 이전
...
// 이동 위치에 맞는 Zone 설정
Player p = gameObject as Player;
if(p != null)
{
Zone now = gameObject.Room.GetZone(p.CellPos);
Zone after = gameObject.Room.GetZone(dest);
if(now != after)
{
if (now != null)
now.Players.Remove(p);
if (after != null)
after.Players.Add(p);
}
}
// 실제 좌표 변경
posInfo.PosX = dest.x;
posInfo.PosY = dest.y;
return true;
}
플레이어의 시야각 안에 있는지에 따라 몬스터와 투사체도 보이거나 보이지 않아야 함
플레이어의 시야를 계속해서 추적하는 시야 클래스
로 관리가 필요함
플레이어마다
VisionCube
로 직전에 보였던 객체들과 현재 보이는 객체를 계속해서 검사함
기존 Zone에서 이제 Player 목록 외에 Monster, Projectile 목록도 필요함
Zone.cs
// Zone에 있는 플레이어들
public HashSet<Player> Players { get; set; } = new HashSet<Player>();
// Zone에 있는 몬스터들
public HashSet<Monster> Monsters { get; set; } = new HashSet<Monster>();
// Zone에 있는 투사체들
public HashSet<Projecttile> Projecttiles { get; set; } = new HashSet<Projecttile>();
Player.cs
public VisionCube Vision { get; private set; }
...
public Player()
{
ObjectType = GameObjectType.Player;
Vision = new VisionCube(this);
}
...
VisionCube.cs
public Player Owner { get; private set; }
// 이전 시야에서 보인 객체들
public HashSet<GameObject> PreviousObjects { get; private set; } = new HashSet<GameObject>();
public VisionCube(Player owner)
{
Owner = owner;
}
// 현재 시야각의 객체들 긁어옴
public HashSet<GameObject> GatherObjects()
{
if (Owner == null || Owner.Room == null)
return null;
HashSet<GameObject> objects = new HashSet<GameObject>();
// 현재 Zone을 특정
Vector2Int cellPos = Owner.CellPos;
List<Zone> zones = Owner.Room.GetAdjacentZones(cellPos);
foreach(Zone zone in zones)
{
foreach(Player player in zone.Players)
{
int dx = cellPos.x - player.CellPos.x;
int dy = cellPos.y - player.CellPos.y;
// 시야각 밖에 위치
if (Math.Abs(dx) > GameRoom.VisionCells)
continue;
if (Math.Abs(dy) > GameRoom.VisionCells)
continue;
objects.Add(player);
}
foreach (Monster monster in zone.Monsters)
{...
}
foreach (Projecttile projecttile in zone.Projecttiles)
{...
}
}
return objects;
}
despawn
spawn
VisionCube.cs
// 이전과 현재 시야각 객체들 비교
public void Update()
{
if (Owner == null || Owner.Room == null)
return;
HashSet<GameObject> currentObjects = GatherObjects();
// 기존엔 없었는데 새로 생긴 애들 Spawn 처리
List<GameObject> added = currentObjects.Except(PreviousObjects).ToList();
if(added.Count > 0)
{
S_Spawn spawnPacket = new S_Spawn();
foreach(GameObject gameObject in added)
{
ObjectInfo info = new ObjectInfo();
info.MergeFrom(gameObject.Info);
spawnPacket.Objects.Add(info);
}
Owner.Session.Send(spawnPacket);
}
// 기존엔 있었는데 사라진 애들 Despawn 처리
List<GameObject> removed = PreviousObjects.Except(currentObjects).ToList();
if (removed.Count > 0)
{
S_Despawn despawnPacket = new S_Despawn();
foreach (GameObject gameObject in removed)
{
despawnPacket.ObjectIds.Add(gameObject.Id);
}
Owner.Session.Send(despawnPacket);
}
PreviousObjects = currentObjects;
// 0.1초에 한번씩 검사
Owner.Room.PushAfter(100, Update);
}
Map.ApplyMove()
: 타입별로 구분하여 이동 위치에 맞는 Zone 설정
Map.cs
public bool ApplyMove(GameObject gameObject, Vector2Int dest)
{
...
// 이동 위치에 맞는 Zone 설정
if (type == GameObjectType.Player)
{
Player player = (Player)gameObject;
if (player != null)
{
Zone now = gameObject.Room.GetZone(player.CellPos);
Zone after = gameObject.Room.GetZone(dest);
if (now != after)
{
now.Players.Remove(player);
after.Players.Add(player);
}
}
}
else if(type == GameObjectType.Monster)
{
Monster monster = (Monster)gameObject;
if (monster != null)
{
Zone now = gameObject.Room.GetZone(monster.CellPos);
Zone after = gameObject.Room.GetZone(dest);
if (now != after)
{
now.Monsters.Remove(monster);
after.Monsters.Add(monster);
}
}
}
else if(type == GameObjectType.Projecttile)
{
Projecttile projecttile = (Projecttile)gameObject;
if (projecttile != null)
{
Zone now = gameObject.Room.GetZone(projecttile.CellPos);
Zone after = gameObject.Room.GetZone(dest);
if (now != after)
{
now.Projecttiles.Remove(projecttile);
after.Projecttiles.Add(projecttile);
}
}
}
// 실제 좌표 변경
posInfo.PosX = dest.x;
posInfo.PosY = dest.y;
return true;
}
EnterGame()
: 처음 위치 맞는 Zone에 몬스터, 투사체 배치 필요
LeaveGame()
: 몬스터, 투사체가 있던 Zone에서 제거 필요
이제
VisionCube
에서 Spawn, Despawn 모두 보내줌
EnterGame()
에서 S_Spawn 패킷을 보내줄 필요가 없어짐 -> 삭제
LeaveGame()
에서 S_Despawn 패킷을 보내줄 필요가 없어짐 -> 삭제
public void Broadcast(Vector2Int pos,IMessage packet)
{
List<Zone> zones = GetAdjacentZones(pos);
foreach (Player p in zones.SelectMany(z => z.Players))
{
int dx = pos.x - p.CellPos.x;
int dy = pos.y - p.CellPos.y;
// 시야각 밖에 위치
if (Math.Abs(dx) > GameRoom.VisionCells)
continue;
if (Math.Abs(dy) > GameRoom.VisionCells)
continue;
p.Session.Send(packet);
}
}
버그 리스트
시작점 근처에서 몬스터가 죽으면 다시 부활하면서
VisionCube
에서 Update로 pre와 current의 예외처리가 되지 않음
원인 :
EnterGame()
에서 S_Spawn 패킷을 보내줄 필요가 없어짐 -> 삭제
LeaveGame()
에서 S_Despawn 패킷을 보내줄 필요가 없어짐 -> 삭제
Client.ObjectManager.cs
public void Add(ObjectInfo info, bool myPlayer = false)
{
if (MyPlayer != null && MyPlayer.Id == info.ObjectId)
return;
if (_objects.ContainsKey(info.ObjectId))
return;
...
}
public void Remove(int id)
{
if (MyPlayer != null && MyPlayer.Id == id)
return;
if (_objects.ContainsKey(id) == false)
return;
...
}
Arrow는
ApplyMove()
를 통한 이동이 아니라 Update에서 직접 이동시키고 있었음
-> 충돌에 포함되지 않기 때문
VisionCube
에서 계속 added에만 속하기 때문에 사라지지 않음화살도 ApplyMove()
로 이동하도록 처리
Arrow.cs
public override void Update()
{
if (Data == null || Data.projecttile == null || Owner == null || Room == null)
return;
int tick = (int)(1000 / Data.projecttile.speed);
Room.PushAfter(tick, Update);
Vector2Int destPos = GetFrontCellPos();
if(Room.Map.ApplyMove(this,destPos, collision : false))
{
S_Move movePacket = new S_Move();
movePacket.ObjectId = Id;
movePacket.PosInfo = PosInfo;
Room.Broadcast(CellPos,movePacket);
}
...
}
Map.cs
public bool ApplyMove(GameObject gameObject, Vector2Int dest, bool checkObjects = true, bool collision = true)
{
if (gameObject.Room == null)
return false;
if (gameObject.Room.Map != this)
return false;
PositionInfo posInfo = gameObject.Info.PosInfo;
if (CanGo(dest, checkObjects) == false)
return false;
// 목표 위치로 이전
if(collision) // 화살은 제외
{
// 오브젝트가 원래 있던 위치 비워줌
{
int x = posInfo.PosX - MinX;
int y = MaxY - posInfo.PosY;
if (_objects[y, x] == gameObject)
_objects[y, x] = null;
}
{
int x = dest.x - MinX;
int y = MaxY - dest.y;
_objects[y, x] = gameObject;
}
}
...
}
LeaveGame()
: 몬스터, 투사체가 있던 Zone에서 제거 필요
-> Map.ApplyLeave()
에서 관리될 수 있도록 변경
Map.cs
public bool ApplyLeave(GameObject gameObject)
{
...
// Zone
Zone zone = gameObject.Room.GetZone(gameObject.CellPos);
zone.Remove(gameObject);
...
}
Zone.cs
public void Remove(GameObject gameObject)
{
GameObjectType type = ObjectManager.GetObjectTypeById(gameObject.Id);
switch (type)
{
case GameObjectType.Player:
Players.Remove((Player)gameObject);
break;
case GameObjectType.Monster:
Monsters.Remove((Monster)gameObject);
break;
case GameObjectType.Projecttile:
Projecttiles.Remove((Projecttile)gameObject);
break;
}
}
부활할 때 (0,0)에 누군가 있다면
ApplyMove
에서 return이 돼서 충돌, Zone 처리 모두 실행되지 않게 됨
GameRoom.cs
Random _rand = new Random();
public void EnterGame(GameObject gameObject, bool randomPos)
{
if (gameObject == null)
return;
if(randomPos)
{
Vector2Int respawnPos;
while (true)
{
respawnPos.x = _rand.Next(Map.MinX, Map.MaxX + 1);
respawnPos.y = _rand.Next(Map.MinY, Map.MaxY + 1);
if (Map.Find(respawnPos) == null)
{
gameObject.CellPos = respawnPos;
break;
}
}
}
...
}
더미 클라이언트 테스트을 위한 사전 작업
GameRoom.cs
public void Init(int mapId,int zoneCells)
{
...
// Test
for(int i=0;i<500;i++)
{
Monster monster = ObjectManager.Instance.Add<Monster>();
monster.Init(1);
EnterGame(monster, randomPos: true);
}
}
Map.FindPath
의 충돌 검사 여부를 켜줌Monster.UpdateMoving()
...
List<Vector2Int> path = Room.Map.FindPath(CellPos, _target.CellPos,checkObject: true);
if(path.Count < 2 || path.Count > _chaseCellDist)
{
_target = null;
State = CreatureState.Idle;
BroadcastMove();
return;
}
...
Map.cs
List<Vector2Int> CalcCellPathFromParent(Dictionary<Pos,Pos> parent, Pos dest)
{
List<Vector2Int> cells = new List<Vector2Int>();
// Player에게 갈 수 있는 길이 없는 경우
if(parent.ContainsKey(dest) == false)
{
// 갈 수 있는 곳 중 그나마 플레이어에게 가장 가까운 곳으로 이동
Pos best = new Pos();
int bestDist = Int32.MaxValue;
foreach(Pos pos in parent.Keys)
{
int dist = Math.Abs(dest.X - pos.X) + Math.Abs(dest.Y - pos.Y);
// 제일 우수한 후보를 뽑는다
if(dist < bestDist)
{
best = pos;
bestDist = dist;
}
}
// 목적지를 우수한 후보로 변경
dest = best;
}
{
Pos pos = dest;
while (parent[pos] != pos)
{
cells.Add(Pos2Cell(pos));
pos = parent[pos];
}
cells.Add(Pos2Cell(pos));
cells.Reverse();
return cells;
}
}
클라가 강제 종료를 했을 때, 서버는 클라가 활동 중인지 연결이 끊긴 것인지 확인이 불가함
S_Ping
C_Pong
Protocol.proto
enum MsgId {
...
S_PING = 21;
C_PONG = 22;
}
// 내용은 없어도 됨
message S_Ping{
}
message C_Pong{
}
ClientSession.cs
...
// 클라에서 최근 마지막으로 응답을 준 시간
long _pingpongTick = 0;
public void Ping()
{
if(_pingpongTick > 0)
{
long delta = (System.Environment.TickCount64 - _pingpongTick);
// 마지막 응답부터 30초가 지남
if(delta > 30 * 1000)
{
Console.WriteLine("Disconnected by PingCheck");
Disconnect();
return;
}
}
// 응답 통과
S_Ping pingPacket = new S_Ping();
Send(pingPacket);
GameLogic.Instance.PushAfter(5000, Ping);
}
public void HandlePong()
{
_pingpongTick = System.Environment.TickCount64;
}
...
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
// 클라에게 연결 됐다고 알려줌
{
S_Connected connectedPacket = new S_Connected();
Send(connectedPacket);
}
GameLogic.Instance.PushAfter(5000, Ping);
}
Server.PacketHandler.cs
public static void C_PongHandler(PacketSession session, IMessage packet)
{
ClientSession clientSession = (ClientSession)session;
clientSession.HandlePong();
}
Client.PacketHandler.cs
public static void S_PingHandler(PacketSession session, IMessage packet)
{
C_Pong pongPacket = new C_Pong();
Debug.Log("[Server] PingCheck");
Managers.Network.Send(pongPacket);
}