스킬 동기화
저번 포스팅에서는 플레이어의 움직임을 방향, 또는 좌표가 변했을 경우 패킷을 만들어 서버에 보냈고 서버는 그 값을 모든 방에 있는 플레이어에게 브로드캐스팅을 해주었다.
이번에는 우리가 저번에 만들었던 플레이어 스킬들을 공기화 해보자.
우리가 움직임을 동기화 할때는 상황에 따라 두가지 경우, 클라 선처리와 클라 후처리 방식이 있었다. 지금 우리의 방식은 클라 선처리에 해당되는데 움직이는 경우가 빈번하게 발생하기 때문에 서버가 연산 속도를 따라 오지 못할 수 있으니(MMO RPG의 경우) 클라이언트에서 움직임을 처리하고 그 결과를 서버에 통보하고 검증하는 과정을 거쳤다.
그렇다면 스킬도 클라에서 관리하고 서버에 통보하는 식으로 해도 괜찮을까?? 해도 괜찮긴 하다만 움직임보다는 스킬 사용이 빈번하게 일어나지 않기 때문에 그럴 필요가 전혀 없다. 오히려 스킬은 적 몬스터와의 전투를 위해 사용되는데 클라쪽에서 선처리해버리게되면 데이터를 변조(통칭 핵)하여 문제가 발생할 수 있다.
그러기 위해서 일단 패킷부터 설계를 해보자.
message C_Skill {
SkillInfo info = 1;
}
message S_Skill {
int32 playerId = 1;
SkillInfo info = 2;
}
message SkillInfo {
int32 skillId = 1;
}
추가적으로 스킬에 대한 정보가 필요한 경우(타겟 시전자 등등) SkillInfo에 추가적인 정보만 기입하면 된다.
public static void C_SkillHandler(PacketSession session, IMessage packet)
{
C_Skill skillPacket = packet as C_Skill;
ClientSession clientSession = session as ClientSession;
Player player = clientSession.MyPlayer;
if (player == null)
return;
GameRoom room = player.Room;
if (room == null)
return;
room.HandleSkill(player, skillPacket);
}
서버쪽 패킷 핸들러이다.
public void HandleSkill(Player player, C_Skill movePacket)
{
if (player == null)
return;
lock (_lock)
{
PlayerInfo 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.PlayerId = info.PlayerId;
skill.Info.SkillId = 1;
Broadcast(skill);
// 데미지 판정
}
}
이렇게 스킬에 대한 정보를 입력해 브로드캐스트해준다. 여기서 나중에 해야할 것은 클라이언트 치팅을 막기위해 스킬이 여러번 사용되는거나 데미지를 여러번 입히지 않게 체킹을 해야아한다.
이제 클라이언트에서 연동해보겠다.
public static void S_SkillHandler(PacketSession session, IMessage packet)
{
S_Skill skillPacket = packet as S_Skill;
GameObject go = Managers.Object.FindById(skillPacket.PlayerId);
if (go == null)
return;
PlayerController pc = go.GetComponent<PlayerController>();
if (pc != null)
{
pc.UseSkill(skillPacket.Info.SkillId);
}
}
그런데 만약 클라이언트에서 스페이스바를 연타했을 때 스킬 패킷을 계속 보내는건 당연히 말이 안될 것이다. 그러기에 스킬 쿨타임을 두어 그 사이에는 패킷이 전송되지 않게 딜레이를 주어야한다.
그럼 스킬 쿨타임을 서버에서 계산해야할까 아님 클라에서 해야할까? 정답은 둘 다 해야만한다.
IEnumerator CoStartPunch()
{
// 피격 판정은서버 쪽으로 이전
// 대기 시간
_rangedSkill = false;
State = CreatureState.Skill;
yield return new WaitForSeconds(0.5f);
State = CreatureState.Idle;
_coSkill = null;
CheckUpdatedFlag();
}
protected override void UpdateIdle()
{
// 이동 상태로 갈지 확인
if (Dir != MoveDir.None)
{
State = CreatureState.Moving;
return;
}
// 스킬 상태로 갈지 확인
if (Input.GetKey(KeyCode.Space))
{
Debug.Log("Skill");
C_Skill skill = new C_Skill() { Info = new SkillInfo() };
skill.Info.SkillId = 1;
Managers.Network.Send(skill);
_coSkillCooltime = StartCoroutine("CoInputCooltiome", 0.2f);
}
}
Coroutine _coSkillCooltime;
IEnumerator CoInputCooltiome(float time)
{
yield return new WaitForSeconds(time);
_coSkillCooltime = null;
}
이렇게 클라쪽에서는 0.2초마다 패킷을 보내게 설정하고 서버쪽에서는 Idle 상태에서만 스킬을 사용할 수 있게 설정함으로써 효율적이게 관리할 수 있다.
피격 판정
히트 판정은 조금 복잡한데 간략하게 설명하면 현재는 클라에서 맵에대한 정보를 추출하고 가지고 있었다. 이제는 이걸 서버도 추가적으로 정보를 가지고 있게 한 뒤 플레이어가 공격하려고 하는 좌표에 대해 플레이어를 가지고와서 처리하면 된다.
public void HandleSkill(Player player, C_Skill skillPacket)
{
if (player == null)
return;
lock (_lock)
{
PlayerInfo info = player.Info;
if (info.PosInfo.State != CreatureState.Idle)
return;
// TODO : 스킬 사용 가능 여부 체크
// 통과
info.PosInfo.State = CreatureState.Skill;
S_Skill skill = new S_Skill() { Info = new SkillInfo() };
skill.PlayerId = info.PlayerId;
skill.Info.SkillId = 1;
Broadcast(skill);
// TODO : 데미지 판정
Vector2Int skillPos = player.GetFrontCellPos(info.PosInfo.MoveDir);
Player target = _map.Find(skillPos);
if (target != null)
{
Console.WriteLine("Hit Player !");
}
}
}
화살
화살 같은 투사체를 발사했을 때, 과연 서버에서 그 객체에 대한 정보를 실시간으로 송수신 해야할까가 의문이다. 화살같은 투사체의 움직임을 계산하는 것 까진 서버와 클라가 동시에 진행해야하지만 서버가 값을 계산해서 모든 플레이어에게 송신하는 건 옳지 않다고 생각한다. 어마어마한 트래픽이 발생할 것 같기 때문이다.
using Google.Protobuf;
using Google.Protobuf.Protocol;
using Server.Game.Object;
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Text;
namespace Server.Game.Room
{
public class GameRoom
{
object _lock = new object();
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);
}
public void Update()
{
lock (_lock)
{
foreach(Projectile projectile in _projectiles.Values)
{
projectile.Update();
}
}
}
public void EnterGame(GameObject gameObject)
{
if (gameObject == null)
return;
GameObjectType type = ObjectManager.GetObjectTypeById(gameObject.Id);
lock (_lock)
{
if(type == GameObjectType.Player)
{
Player player = gameObject as Player;
_players.Add(gameObject.Id, player);
player.Room = this;
// 본인한테 정보 전송
{
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);
}
player.Session.Send(spawnPacket);
}
}
else if(type == GameObjectType.Monster)
{
Monster monster = gameObject as Monster;
_monsters.Add(gameObject.Id, monster);
monster.Room = this;
}
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);
lock (_lock)
{
if (type == GameObjectType.Player)
{
Player player = null;
if (_players.Remove(objectId, out player) == false)
return;
player.Room = null;
Map.ApplyLeave(player);
// 본인한테 정보 전송
{
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;
monster.Room = null;
Map.ApplyLeave(monster);
}
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.PlayerIds.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;
lock (_lock)
{
// TODO : 검증
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;
lock (_lock)
{
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);
if (skillPacket.Info.SkillId == 1)
{
Vector2Int skillPos = player.GetFrontCellPos(info.PosInfo.MoveDir);
GameObject target = Map.Find(skillPos);
if (target != null)
{
Console.WriteLine("Hit GameObject !");
}
}
else if (skillPacket.Info.SkillId == 2)
{
// 화살
Arrow arrow = ObjectManager.Instance.Add<Arrow>();
if (arrow == null)
return;
arrow.Owner = player;
arrow.PosInfo.State = CreatureState.Moving;
arrow.PosInfo.MoveDir = player.PosInfo.MoveDir;
arrow.PosInfo.PosX = player.PosInfo.PosX;
arrow.PosInfo.PosY = player.PosInfo.PosY;
EnterGame(arrow);
}
}
}
public void Broadcast(IMessage packet)
{
lock (_lock)
{
foreach (Player p in _players.Values)
{
p.Session.Send(packet);
}
}
}
}
}
using Google.Protobuf.Protocol;
using Server.Game.Room;
using System;
using System.Collections.Generic;
using System.Text;
namespace Server.Game.Object
{
public class Arrow : Projectile
{
public GameObject Owner { get; set; }
long _nextMoveTick = 0;
public override void Update()
{
if (Owner == null || Room == null)
return;
if (_nextMoveTick >= Environment.TickCount64)
return;
_nextMoveTick = Environment.TickCount64;
Vector2Int destPos = GetFrontCellPos();
if (Room.Map.CanGo(destPos))
{
CellPos = destPos;
S_Move movePacket = new S_Move();
movePacket.ObjectId = Id;
movePacket.PosInfo = PosInfo;
Room.Broadcast(movePacket);
Console.WriteLine("Move Arrow");
}
else
{
GameObject target = Room.Map.Find(destPos);
if(target != null)
{
// 피격한정
}
Room.LeaveGame(Id);
}
}
}
}
패킷에 맞게 클라이언트만 수정하면 된다.
'Unity > 온라인 RPG' 카테고리의 다른 글
[Unity 2D] 서버 연동 - Hp bar와 DieEffect 그리고 몬스터 Ai (0) | 2024.03.13 |
---|---|
[Unity 2D] 서버 연동 - Data & Config 과 피격 (0) | 2024.03.12 |
[Unity 2D] 서버 연동 - MyPlayer 분리 및 이동 동기화 (0) | 2024.03.08 |
[Unity 2D] 서버 연동 - 멀티플레이 환경 및 게임 입장 (1) | 2024.03.07 |
[Unity 2D] 컨텐츠 준비 - Monster AI(Patrol AI, Search AI, Skill AI) (0) | 2024.03.05 |