이번 시간에는 스킬을 쓰면 서버와 동기화를 시켜서 다른 플레이어에게도 보이게 작업을 해보자.
스킬 같은 경우는 이동과는 달리 가끔 사용하는 것이기 때문에 클라이언트에서 먼저 선처리를 해줄 이유가 없다. 스페이스바를 누르면 서버 쪽에 스킬을 썼다는 패킷을 보내게 되는데 서버쪽에서 검증을 한 후 주변 플레이어들에게 브로드캐스팅을 해주는 방식으로 구현하고 그 후에 스킬 애니메이션을 재생하는 방식으로 구현해보자
피격 판정(맞으면 데미지 저하)을 현재 클라이언트에서 하고 있는데 이는 서버쪽으로 옮겨야 할 것이다.
enum MsgId {
S_ENTER_GAME = 0;
S_LEAVE_GAME = 1;
S_SPAWN = 2;
S_DESPAWN = 3;
C_MOVE = 4;
S_MOVE = 5;
C_SKILL = 6;
S_SKILL = 7;
}
message C_Skill{
SkillInfo info = 1;
}
message S_Skill {
int32 playerId = 1;
SkillInfo info = 2;
}
message SkillInfo {
int32 skillId = 1;
}
먼저 스킬 패킷을 정의해주도록 한다. 클라이언트에서 서버로 보내줄 때 C_Skill 패킷은 SkillInfo (현재는 스킬 아이디)를 보내주고, 브로드캐스트 목적으로 쓰일 서버에서 클라쪽으로 보낼 S_Skill는 어느 플레이어가 스킬을 사용했는지 정보도 추가해서 정의를 해두었다.
public static void C_MoveHandler(PacketSession session, IMessage packet)
{
C_Move movePacket = packet as C_Move;
ClientSession clientSession = session as ClientSession;
Console.WriteLine($"C_Move ({movePacket.PosInfo.PosX}, {movePacket.PosInfo.PosY})");
/*
* if(clientSession.MyPlayer == null)
* return;
* if(clientSession.MyPlayer.Room == null)
* return;
*/
Player player = clientSession.MyPlayer;
// 이렇게 player에 꺼내서 사용하면, player를 null 체크한 후 다른 쓰레드에서 MyPlayer를 null로 만들어도 아래 코드에서 크래시가 일어나지 않는다.
if (player == null)
return;
// 위 코드처럼 null 체크 후 아래 코드를 실행하는 동안 플레이어가 LeaveRoom() 호출로 인해 Room = null로 바꿔버리면 크래시가 날 수 있다. 따라서 room을 한번 빼올 필요가 있다.
GameRoom room = player.Room;
if (room == null)
return;
room.HandleMove(player, movePacket);
}
public void HandleMove(Player player, C_Move movePacket)
{
if (player == null)
return;
lock(_lock)
{ // 경합 상태 해결. 한 쓰레드만 수정 가능
// TODO : 검증
// 일단 서버에서 좌표 이동
PlayerInfo info = player.Info;
info.PosInfo = movePacket.PosInfo;
// 다른 플레이어한테도 알려준다
S_Move resMovePacket = new S_Move();
resMovePacket.PlayerId = player.Info.PlayerId;
resMovePacket.PosInfo = movePacket.PosInfo;
Broadcast(resMovePacket);
}
}
원래 플레이어의 위치를 갱신하는 코드는 GameRoom 내부에서 함수를 새로 정의하여 수정하도록 변경했다. 함수 내부에서 lock을 걸어서 여러 쓰레드에서 동시에 접근하여 수정되는 것을 막을 수 있고, 만약 이전 코드처럼 동작을 하게 된다면 예를들어 플레이어 정보를 꺼내서 수정하는 동안 다른 곳에서 스킬공격을 받아 넉백되어 플레이어가 밀려나는 등 수정이 일어나는 등의 문제가 발생 할 수 있다.
public static void C_SkillHandler(PacketSession session, IMessage packet)
{
C_Move movePacket = packet as C_Move;
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 skillPacket)
{
if(player == null)
return;
// 플레이어가 해당 룸에 소속되어 있는지 이중 체크할 수도 있다.
lock(_lock)
{
PlayerInfo info = player.Info;
if(info.PosInfo.State != CreatureState.Idle) // Idle인 상태에서만 스킬 사용 가능
return;
// TODO : 스킬 사용 가능 여부 체크
info.PosInfo.State = CreatureState.Skill;
S_Skill skill = new S_Skill() { Info = new SkillInfo() };
skill.PlayerId = player.Info.PlayerId;
skill.Info.SkillId = 1; // 스킬마다 고유 id를 가지는데, 여기서는 스킬이 하나뿐이라 1
Broadcast(skill);
// 데미지 판정
}
}