MyPlayer 분리
클라이언트에서 플레이어를 실질적으로 만드는 부분을 만들어보자.
우리가 이전에 PlayerController에서 키 입력 받아 플레이어를 조종 했다. 그런데 문제는 모든 플레이어가 PlayerController를 가지고 있으면 아마 한명이 움직이면 나머지 모든 플레이어도 움직일 것이다. 그러기에 자신의 플레이어만 분리를 할 필요가 있다.
이럴 경우에는 내가 조정하는 Player를 알기 위해 새로운 컴포넌트를 만들어서 결정하는 편이다.
MyPlayerController를 만들어서 작업한다.
ObjectManager에서 자신의 플레이어 일 때는 MyplayerController 객체를 추가하고 아닐 경우는 Playercontroller를 추가해서 입력을 막게끔 설계하면 된다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;
public class MyPlayerController : PlayerController
{
protected override void Init()
{
base.Init();
}
protected override void UpdateController()
{
switch (State)
{
case CreatureState.Idle:
GetDirInput();
break;
case CreatureState.Moving:
GetDirInput();
break;
}
base.UpdateController();
}
protected override void UpdateIdle()
{
// 이동 상태로 갈지 확인
if (Dir != MoveDir.None)
{
State = CreatureState.Moving;
return;
}
// 스킬 상태로 갈지 확인
if (Input.GetKey(KeyCode.Space))
{
State = CreatureState.Skill;
//_coSkill = StartCoroutine("CoStartPunch");
_coSkill = StartCoroutine("CoStartShootArrow");
}
}
void LateUpdate()
{
Camera.main.transform.position = new Vector3(transform.position.x, transform.position.y, -10);
}
// 키보드 입력
void GetDirInput()
{
if (Input.GetKey(KeyCode.W))
{
Dir = MoveDir.Up;
}
else if (Input.GetKey(KeyCode.S))
{
Dir = MoveDir.Down;
}
else if (Input.GetKey(KeyCode.A))
{
Dir = MoveDir.Left;
}
else if (Input.GetKey(KeyCode.D))
{
Dir = MoveDir.Right;
}
else
{
Dir = MoveDir.None;
}
}
}
이동 로직을 분리해서 적용한다.
using Google.Protobuf.Protocol;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ObjectManager
{
public MyPlayerController MyPlayer { get; set; }
Dictionary<int, GameObject> _objects = new Dictionary<int, GameObject>();
public void Add(PlayerInfo info, bool myPlayer = false)
{
if (myPlayer)
{
GameObject go = Managers.Resource.Instantiate("Creature/MyPlayer");
go.name = info.Name;
_objects.Add(info.PlayerId, go);
MyPlayer = go.GetComponent<MyPlayerController>();
MyPlayer.Id = info.PlayerId;
MyPlayer.CellPos = new Vector3Int(info.PosX, info.PosY, 0);
}
else
{
GameObject go = Managers.Resource.Instantiate("Creature/Player");
go.name = info.Name;
_objects.Add(info.PlayerId, go);
PlayerController pc = go.GetComponent<PlayerController>();
pc.Id = info.PlayerId;
pc.CellPos = new Vector3Int(info.PosX, info.PosY, 0);
}
}
public void Add(int id, GameObject go)
{
_objects.Add(id, go);
}
public void Remove(int id)
{
_objects.Remove(id);
}
public void RemoveMyPlayer()
{
if (MyPlayer == null)
return;
Remove(MyPlayer.Id);
MyPlayer = null;
}
public GameObject Find(Vector3Int cellPos)
{
foreach (GameObject obj in _objects.Values)
{
CreatureController cc = obj.GetComponent<CreatureController>();
if (cc == null)
continue;
if (cc.CellPos == cellPos)
return obj;
}
return null;
}
public GameObject Find(Func<GameObject, bool> condition)
{
foreach (GameObject obj in _objects.Values)
{
if (condition.Invoke(obj))
return obj;
}
return null;
}
public void Clear()
{
_objects.Clear();
}
}
이동 동기화(서버)
이동 동기화가 진짜 제일 어려운 부분이긴 하다. 이해하기 쉽지 않고 단순하지도 않다.
유니티는 매 60프레임마다 좌표가 계산되고 갱신된다. 그런데 서버는 그정도의 효율을 낼 수 없기 때문에 플레이어가 움직이는 모습을 자연스럽게 보여주기란 여간 쉬운일은 아니다.
2가지 방식이 있다.
1. 클라이언트가 서버에 이동 요청을 한 후에 서버가 플레이어를 이동 시킨 후 클라이언트에게 결과를 알려주는 방식
2. 클라이언트에서 일단 먼저 이동한 후 그 결과를 서버에 보내는 방식
이는 장르에 따라 달라지게 된다.
대부분의 MMORPG 같은 경우는 2번을 이용한다.
진행하기 앞서 move 패킷에는 어떤 정보를 가지고 있어야할까. 그냥 단순히 좌표만 가지고 있으면 되는 것인가? 그건 아니다. 방향과 상태 역시 매우 중요하다. 그렇기에 패킷을 보낼때 두 정보를 같이 추가해서 보내도록한다.
그렇기 때문에 우리가 따로 정의를 해던 enum인 CreatureState와 MoveDir을 패킷으로 만들어준다.
syntax = "proto3";
package Protocol;
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;
}
enum CreatureState{
IDLE = 0;
MOVING = 1;
SKILL = 2;
DEAD = 3;
}
enum MoveDir{
NONE = 0;
UP = 1;
DOWN = 2;
LEFT = 3;
RIGHT =4;
}
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 {
PositionInfo posInfo = 1;
}
message S_Move {
int32 playerId =1;
PositionInfo posInfo = 2;
}
message PlayerInfo {
int32 playerId = 1;
string name = 2;
PositionInfo posInfo = 3;
}
message PositionInfo {
CreatureState state = 1;
MoveDir moveDir = 2;
int32 posX = 3;
int32 posY =4;
}
이제 C_Move 패킷이 왔을 때, 즉 클라이언트가 움직인 후 서버쪽에 패킷을 보냈을 때를 처리하기 위해 PacketHandler를 수정해준다.
using Google.Protobuf;
using Google.Protobuf.Protocol;
using Server;
using ServerCore;
using System;
class PacketHandler
{
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;
// 서버에서 좌표 이동
PlayerInfo info = clientSession.MyPlayer.Info;
info.PosInfo = movePacket.PosInfo;
// 다른 플레이어들한테도 알려준다.
S_Move resMovePacket = new S_Move();
resMovePacket.PlayerId = clientSession.MyPlayer.Info.PlayerId;
resMovePacket.PosInfo = movePacket.PosInfo;
clientSession.MyPlayer.Room.Broadcast(resMovePacket);
}
}
서버는 C_Move 패킷을 받으면 자신과 같은 방에 있는 모든 플레이어한테 브로드캐스트를 하게된다.
이제 클라이언트에서 작업을 해보자.
이동 동기화(클라)
먼저 클라이언트에서 움직였을 때, 좌표가 바뀌거나 방향이 바뀌었을 때 패킷을 보낸다.
protected override void MoveToNextPos()
{
CreatureState prevState = State;
Vector3Int prevCellPos = CellPos;
base.MoveToNextPos();
if(prevState != State || CellPos != prevCellPos)
{
C_Move movePacket = new C_Move();
movePacket.PosInfo = PosInfo;
Managers.Network.Send(movePacket);
}
}
이는 MyPlayerController에서 다루는 것이다.
그리고 이제 S_Move 패킷이 왔을 때 처리를 해주어야 한다.
public static void S_MoveHandler(PacketSession session, IMessage packet)
{
S_Move movePacket = packet as S_Move;
ServerSession serverSession = session as ServerSession;
GameObject go = Managers.Object.FindById(movePacket.PlayerId);
CreatureController cc = go.GetComponent<CreatureController>();
if (cc == null)
return;
cc.PosInfo = movePacket.PosInfo;
}
여기까지 작업을 했다면 약간의 문제를 포함해서 움직임이 동기화가 된다. 일단 문제점은 2번 방식으로 플레이어를 움직이다보니 클라와 서버쪽의 좌표값이 달라지는 현상이 발생한다. 이를 수정해야만 한다.
이동 동기화(버그 수정)
왜 이러한 버그가 발생했을까? 실질적으로 플레이어가 움직였을 때, 방향을 서버에 보내고 그 값을 다른 클라이언트가 받아 그 방향에 맞춰 계산했기 때문이다.
거기다 state나 좌표가 변했을 경우에만 패킷을 보냈기 때문에 문제가 발생한 것이다.
이 문제를 해결하기 위해서는 자신의 움직임만 절대적으로 계산하고 남의 플레이어는 절대로 계산하면 안되게 수정해야한다.
그렇기에 CreatureController에 있던 MoveToNextPos 함수를 MyPlayerController로 움직이면 된다.
protected override void MoveToNextPos()
{
if (Dir == MoveDir.None)
{
State = CreatureState.Idle;
CheckUpdatedFlag();
return;
}
Vector3Int destPos = CellPos;
switch (Dir)
{
case MoveDir.Up:
destPos += Vector3Int.up;
break;
case MoveDir.Down:
destPos += Vector3Int.down;
break;
case MoveDir.Left:
destPos += Vector3Int.left;
break;
case MoveDir.Right:
destPos += Vector3Int.right;
break;
}
if (Managers.Map.CanGo(destPos))
{
if (Managers.Object.Find(destPos) == null)
{
CellPos = destPos;
}
}
CheckUpdatedFlag();
}
void CheckUpdatedFlag()
{
if (_updated)
{
C_Move movePacket = new C_Move();
movePacket.PosInfo = PosInfo;
Managers.Network.Send(movePacket);
_updated = false;
}
}
_updated라는 플래그를 두어 그 플래그가 변할때(움직임, 상태 변환 등)만 패킷을 보내면 된다.
이렇게 이동 동기화를 마치겠다.
'Unity > 온라인 RPG' 카테고리의 다른 글
[Unity 2D] 서버 연동 - Data & Config 과 피격 (0) | 2024.03.12 |
---|---|
[Unity 2D] 서버 연동 - 스킬 동기화와 히트 판정 (0) | 2024.03.11 |
[Unity 2D] 서버 연동 - 멀티플레이 환경 및 게임 입장 (1) | 2024.03.07 |
[Unity 2D] 컨텐츠 준비 - Monster AI(Patrol AI, Search AI, Skill AI) (0) | 2024.03.05 |
[Unity 2D] 컨텐츠 준비 - 스킬(평타, 화살) 사용하기 (1) | 2024.03.05 |