Room Manager
- 현재 서버에서 구동되고 있는 GameRoom을 관리한다.
📄 RoomManager.cs
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(int mapId)
{
GameRoom gameRoom = new GameRoom();
gameRoom.Push(gameRoom.Init, mapId);
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;
}
}
}
}
Game Room
- JobSerializer의 자식 클래스이다.
- 한 지역의 Map과 GameObject를 모두 관리한다.
- 게임 내용과 관련있는 패킷을 처리한다.
- GameRoom 내부의 모든 클리언트에게 Broadcast하는 함수를 제공한다.
- GameObject 입장/퇴장 시 Broadcast한다.
📄 GameRoom.cs
using Google.Protobuf;
using Google.Protobuf.Protocol;
using Server.Data;
using System;
using System.Collections.Generic;
using System.Text;
namespace Server.Game
{
public class GameRoom : JobSerializer
{
public int RoomId { get; set; }
Dictionary<int, Player> _players = new Dictionary<int, Player>();
Dictionary<int, Monster> _monsters = new Dictionary<int, Monster>();
Dictionary<int, Projectile> _projectiles = new Dictionary<int, Projectile>();
public Map Map { get; private set; } = new Map();
public void Init(int mapId)
{
Map.LoadMap(mapId);
Monster monster = ObjectManager.Instance.Add<Monster>();
monster.Info.Name = $"Monster_{monster.Info.ObjectId}";
monster.CellPos = new Vector2Int(5, 5);
EnterGame(monster);
}
public void Update()
{
foreach (Monster monster in _monsters.Values)
{
monster.Update();
}
foreach (Projectile projectile in _projectiles.Values)
{
projectile.Update();
}
Flush();
}
public void EnterGame(GameObject gameObject)
{
if (gameObject == null)
return;
GameObjectType type = ObjectManager.GetObjectTypeById(gameObject.Id);
if (type == GameObjectType.Player)
{
Player player = gameObject as Player;
_players.Add(gameObject.Id, player);
player.Room = this;
Map.ApplyMove(player, new Vector2Int(player.CellPos.x, player.CellPos.y));
{
S_EnterGame enterPacket = new S_EnterGame();
enterPacket.Player = player.Info;
player.Session.Send(enterPacket);
S_Spawn spawnPacket = new S_Spawn();
foreach (Player p in _players.Values)
{
if (player != p)
spawnPacket.Objects.Add(p.Info);
}
foreach (Monster m in _monsters.Values)
spawnPacket.Objects.Add(m.Info);
foreach (Projectile p in _projectiles.Values)
spawnPacket.Objects.Add(p.Info);
player.Session.Send(spawnPacket);
}
}
else if (type == GameObjectType.Monster)
{
Monster monster = gameObject as Monster;
_monsters.Add(gameObject.Id, monster);
monster.Room = this;
Map.ApplyMove(monster, new Vector2Int(monster.CellPos.x, monster.CellPos.y));
}
else if (type == GameObjectType.Projectile)
{
Projectile projectile = gameObject as Projectile;
_projectiles.Add(gameObject.Id, projectile);
projectile.Room = this;
}
{
S_Spawn spawnPacket = new S_Spawn();
spawnPacket.Objects.Add(gameObject.Info);
foreach (Player p in _players.Values)
{
if (p.Id != gameObject.Id)
p.Session.Send(spawnPacket);
}
}
}
public void LeaveGame(int objectId)
{
GameObjectType type = ObjectManager.GetObjectTypeById(objectId);
if (type == GameObjectType.Player)
{
Player player = null;
if (_players.Remove(objectId, out player) == false)
return;
Map.ApplyLeave(player);
player.Room = null;
{
S_LeaveGame leavePacket = new S_LeaveGame();
player.Session.Send(leavePacket);
}
}
else if (type == GameObjectType.Monster)
{
Monster monster = null;
if (_monsters.Remove(objectId, out monster) == false)
return;
Map.ApplyLeave(monster);
monster.Room = null;
}
else if (type == GameObjectType.Projectile)
{
Projectile projectile = null;
if (_projectiles.Remove(objectId, out projectile) == false)
return;
projectile.Room = null;
}
{
S_Despawn despawnPacket = new S_Despawn();
despawnPacket.ObjectIds.Add(objectId);
foreach (Player p in _players.Values)
{
if (p.Id != objectId)
p.Session.Send(despawnPacket);
}
}
}
public void HandleMove(Player player, C_Move movePacket)
{
if (player == null)
return;
PositionInfo movePosInfo = movePacket.PosInfo;
ObjectInfo info = player.Info;
if (movePosInfo.PosX != info.PosInfo.PosX || movePosInfo.PosY != info.PosInfo.PosY)
{
if (Map.CanGo(new Vector2Int(movePosInfo.PosX, movePosInfo.PosY)) == false)
return;
}
info.PosInfo.State = movePosInfo.State;
info.PosInfo.MoveDir = movePosInfo.MoveDir;
Map.ApplyMove(player, new Vector2Int(movePosInfo.PosX, movePosInfo.PosY));
S_Move resMovePacket = new S_Move();
resMovePacket.ObjectId = player.Info.ObjectId;
resMovePacket.PosInfo = movePacket.PosInfo;
Broadcast(resMovePacket);
}
public void HandleSkill(Player player, C_Skill skillPacket)
{
if (player == null)
return;
ObjectInfo info = player.Info;
if (info.PosInfo.State != CreatureState.Idle)
return;
info.PosInfo.State = CreatureState.Skill;
S_Skill skill = new S_Skill() { Info = new SkillInfo() };
skill.ObjectId = info.ObjectId;
skill.Info.SkillId = skillPacket.Info.SkillId;
Broadcast(skill);
Data.Skill skillData = null;
if (DataManager.SkillDict.TryGetValue(skillPacket.Info.SkillId, out skillData) == false)
return;
switch (skillData.skillType)
{
case SkillType.SkillAuto:
{
Vector2Int skillPos = player.GetFrontCellPos(info.PosInfo.MoveDir);
GameObject target = Map.Find(skillPos);
if (target != null)
{
Console.WriteLine("Hit GameObject !");
}
}
break;
case SkillType.SkillProjectile:
{
Arrow arrow = ObjectManager.Instance.Add<Arrow>();
if (arrow == null)
return;
arrow.Owner = player;
arrow.Data = skillData;
arrow.PosInfo.State = CreatureState.Moving;
arrow.PosInfo.MoveDir = player.PosInfo.MoveDir;
arrow.PosInfo.PosX = player.PosInfo.PosX;
arrow.PosInfo.PosY = player.PosInfo.PosY;
arrow.Speed = skillData.projectile.speed;
Push(EnterGame, arrow);
}
break;
}
}
public Player FindPlayer(Func<GameObject, bool> condition)
{
foreach (Player player in _players.Values)
{
if (condition.Invoke(player))
return player;
}
return null;
}
public void Broadcast(IMessage packet)
{
foreach (Player p in _players.Values)
{
p.Session.Send(packet);
}
}
}
}
Map
- 현재 Map의 맵 사이즈, 충돌 지형 타일, GameObject 실제 좌표 타일 등의 데이터를 관리한다.
- GameObject의 특정 위치로의 이동 가능 여부를 체크하는 함수를 제공한다.
- GameObject의 Map에서의 이동과 소멸을 처리하는 함수를 제공한다.
- A* 길찾기 알고리즘을 제공한다.
📄 Map.cs
using Google.Protobuf.Protocol;
using ServerCore;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace Server.Game
{
public struct Pos
{
public Pos(int y, int x) { Y = y; X = x; }
public int Y;
public int X;
}
public struct PQNode : IComparable<PQNode>
{
public int F;
public int G;
public int Y;
public int X;
public int CompareTo(PQNode other)
{
if (F == other.F)
return 0;
return F < other.F ? 1 : -1;
}
}
public struct Vector2Int
{
public int x;
public int y;
public Vector2Int(int x, int y) { this.x = x; this.y = y; }
public static Vector2Int up { get { return new Vector2Int(0, 1); } }
public static Vector2Int down { get { return new Vector2Int(0, -1); } }
public static Vector2Int left { get { return new Vector2Int(-1, 0); } }
public static Vector2Int right { get { return new Vector2Int(1, 0); } }
public static Vector2Int operator +(Vector2Int a, Vector2Int b)
{
return new Vector2Int(a.x + b.x, a.y + b.y);
}
public static Vector2Int operator -(Vector2Int a, Vector2Int b)
{
return new Vector2Int(a.x - b.x, a.y - b.y);
}
public float magnitude { get { return (float)Math.Sqrt(sqrMagnitude); } }
public int sqrMagnitude { get { return (x * x + y * y); } }
public int cellDistFromZero { get { return Math.Abs(x) + Math.Abs(y); } }
}
public class Map
{
public int MinX { get; set; }
public int MaxX { get; set; }
public int MinY { get; set; }
public int MaxY { get; set; }
public int SizeX { get { return MaxX - MinX + 1; } }
public int SizeY { get { return MaxY - MinY + 1; } }
bool[,] _collision;
GameObject[,] _objects;
public bool CanGo(Vector2Int cellPos, bool checkObjects = true)
{
if (cellPos.x < MinX || cellPos.x > MaxX)
return false;
if (cellPos.y < MinY || cellPos.y > MaxY)
return false;
int x = cellPos.x - MinX;
int y = MaxY - cellPos.y;
return !_collision[y, x] && (!checkObjects || _objects[y, x] == null);
}
public GameObject Find(Vector2Int cellPos)
{
if (cellPos.x < MinX || cellPos.x > MaxX)
return null;
if (cellPos.y < MinY || cellPos.y > MaxY)
return null;
int x = cellPos.x - MinX;
int y = MaxY - cellPos.y;
return _objects[y, x];
}
public bool ApplyLeave(GameObject gameObject)
{
if (gameObject.Room == null)
return false;
if (gameObject.Room.Map != this)
return false;
PositionInfo posInfo = gameObject.PosInfo;
if (posInfo.PosX < MinX || posInfo.PosX > MaxX)
return false;
if (posInfo.PosY < MinY || posInfo.PosY > MaxY)
return false;
{
int x = posInfo.PosX - MinX;
int y = MaxY - posInfo.PosY;
if (_objects[y, x] == gameObject)
_objects[y, x] = null;
}
return true;
}
public bool ApplyMove(GameObject gameObject, Vector2Int dest)
{
ApplyLeave(gameObject);
if (gameObject.Room == null)
return false;
if (gameObject.Room.Map != this)
return false;
PositionInfo posInfo = gameObject.PosInfo;
if (CanGo(dest, true) == false)
return false;
{
int x = dest.x - MinX;
int y = MaxY - dest.y;
_objects[y, x] = gameObject;
}
posInfo.PosX = dest.x;
posInfo.PosY = dest.y;
return true;
}
public void LoadMap(int mapId, string pathPrefix = "../../../../../Common/MapData")
{
string mapName = "Map_" + mapId.ToString("000");
string text = File.ReadAllText($"{pathPrefix}/{mapName}.txt");
StringReader reader = new StringReader(text);
MinX = int.Parse(reader.ReadLine());
MaxX = int.Parse(reader.ReadLine());
MinY = int.Parse(reader.ReadLine());
MaxY = int.Parse(reader.ReadLine());
int xCount = MaxX - MinX + 1;
int yCount = MaxY - MinY + 1;
_collision = new bool[yCount, xCount];
_objects = new GameObject[yCount, xCount];
for (int y = 0; y < yCount; y++)
{
string line = reader.ReadLine();
for (int x = 0; x < xCount; x++)
{
_collision[y, x] = (line[x] == '1' ? true : false);
}
}
}
#region A* PathFinding
int[] _deltaY = new int[] { 1, -1, 0, 0 };
int[] _deltaX = new int[] { 0, 0, -1, 1 };
int[] _cost = new int[] { 10, 10, 10, 10 };
public List<Vector2Int> FindPath(Vector2Int startCellPos, Vector2Int destCellPos, bool checkObjects = true)
{
List<Pos> path = new List<Pos>();
bool[,] closed = new bool[SizeY, SizeX];
int[,] open = new int[SizeY, SizeX];
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];
PriorityQueue<PQNode> pq = new PriorityQueue<PQNode>();
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));
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);
while (pq.Count > 0)
{
PQNode node = pq.Pop();
if (closed[node.Y, node.X])
continue;
closed[node.Y, node.X] = true;
if (node.Y == dest.Y && node.X == dest.X)
break;
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), checkObjects) == false)
continue;
}
if (closed[next.Y, next.X])
continue;
int g = 0;
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;
open[dest.Y, dest.X] = 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);
}
}
return CalcCellPathFromParent(parent, dest);
}
List<Vector2Int> CalcCellPathFromParent(Pos[,] parent, Pos dest)
{
List<Vector2Int> cells = new List<Vector2Int>();
int y = dest.Y;
int x = dest.X;
while (parent[y, x].Y != y || parent[y, x].X != x)
{
cells.Add(Pos2Cell(new Pos(y, x)));
Pos pos = parent[y, x];
y = pos.Y;
x = pos.X;
}
cells.Add(Pos2Cell(new Pos(y, x)));
cells.Reverse();
return cells;
}
Pos Cell2Pos(Vector2Int cell)
{
return new Pos(MaxY - cell.y, cell.x - MinX);
}
Vector2Int Pos2Cell(Pos pos)
{
return new Vector2Int(pos.X + MinX, MaxY - pos.Y);
}
#endregion
}
}