몬스터 움직임
몬스터 움직임에대해 처리하기 위한 방법을 꽤나 많이 고민했다.
우리는 플레이어의 움직임을 처리할 때, 서버에서 길 찾기를 하지 않고 Unity NavMesh의 도움을 받아 서버에서 좌표에 대한 검증만 하고 실질적인 길 찾기는 본인에게 맡기고 있었다. 그 이유는 아무래도 개인 노트북이나 pc로 하기에 서버에 대한 연산량을 줄이고, 길찾기를 위한 맵에 정보를 추출하지 않아도 된다는 이점이 있기 때문이었다.
여기까지는 문제가 되지 않을 수 있다.
하지만, 몬스터는 다르다. 우리가 위의 방법을 할 수 있었던 이유는 플레이어 당 한개의 클라이언트라는 보장이 있었기 때문에 길 찾기를 떠넘길 수 있었다.
그렇다면 몬스터의 길 찾기는 누가해줘야할까?
당연히 현실에 존재하는 모든 rpg 게임은 서버쪽에서 연산을 할 것이다. 하지만 본인이 만들고 있는 게임은 서버에서 길 찾기 모듈이 없기에 사용할 수 없다.
그래서 여러 방식을 고민하다 선택한 방법은 길 찾기 전용 서버를 따로 두자는 결론이다.
Dedicated Server
유니티에서는 2022.3 버전 이상 등 에서 Dedicated Server를 제공한다. Dedicated Server란 쉽게 말해 그래픽이 없는 물리연산이 가능한 서버를 의미한다.
그렇기 때문에 데디 서버를 하나 띄워서 몬스터의 움직임을 실제 게임서버로 보낸 후 그 값들을 다시 클라에게 뿌려주기만 하면 된다.
그럼 장점만 있을까??
그건 아니다. 일단 데디 서버는 한 씬당 한개의 서버를 띄어야한다. 물론 우리 게임같은 경우 심리스기 때문에 하나의 큰 씬 밖에 존재하지 않기 때문에 문제될 것은 없다.
또한 몬스터의 모든 연산을 담담해야하기 때문에 조금 느려질 수 있다. 허나 이 문제도 나와는 조금 별개인 것은 어느정도 최적화만 했다면 pc 사양에따라 달라지기 때문에 연습용으로 만드는 나에게는 치명적인 문제가 아니라고 할 수 있다.
아이디어
데디 서버를 사용한다는 것은 이로써 자명해졌다. 그럼 어떤 방식으로 통신을 해야할까 고민해보자.
쉽게 생각하면 위와 같다. 물론 위는 몬스터의 움직임만 다음과 같이 통신한다는 의미이다.
몬스터의 제어권은 데디 서버가 가지고 있고 게임 서버로 결과를 통보하는 식이다.
즉, 서순은 다음과 같다.
- 게임 서버에서 몬스터에게 특정 좌표로 이동 명령 패킷을 데디 서버를 포함한 모든 클라에게 전송
- 각 클라이언트는 몬스터를 특정 좌표로 이동(데디 서버 포함)
- 각 클라이언트는 몬스터가 특정 좌표로 이동을 완료했다면 대기
- 데디 서버에서는 특정 좌표에 도달했다면 서버에게 통보(몬스터가 잘 도착 함 or 실패함 or 클라에서 변조함)
- 통보 받은 서버는 몬스터의 좌표를 동기화하고 모든 클라에게 몬스터의 좌표를 통보함.(클라에서 변조 됐다면 확인)
이렇게 구성할 수 있다.
말은 쉽지만 아무래도 데디 서버를 처음 사용하다보니 아주 혼쭐을 났다.
구현
실제로 데디 서버를 구현해보자.
일단 Unity를 빌드 할 때 데디 서버로 빌드를 할 것이다. 이 때 데디 서버에 Symbol을 추가해주어 일반 클라이언트에서는 컴파일 하지 않는 코드를 만들어준다. Symbol은 UNITY_SERVER로 결정했다.
using DG.Tweening;
using Google.Protobuf.Protocol;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class MonsterController : CreatureController
{
protected NavMeshAgent _agent;
protected Animator _anim;
public GameObject TargetObj;
protected override void Init()
{
base.Init();
_agent = GetComponent<NavMeshAgent>();
_anim = GetComponent<Animator>();
#if UNITY_SERVER
PrevPos = transform.position;
StopCoroutine(CheckPosInfo());
StartCoroutine(CheckPosInfo());
#endif
}
protected virtual void Update()
{
switch (State)
{
case CreatureState.Idle:
UpdateIdle();
break;
case CreatureState.Moving:
UpdateMoving();
break;
case CreatureState.Skill:
break;
case CreatureState.Wait:
break;
case CreatureState.Damaged:
break;
}
}
protected override void UpdateIdle()
{
if (_anim == null) return;
_anim.SetFloat("speed", 0f);
}
protected override void UpdateMoving()
{
if (_anim == null) return;
_anim.SetFloat("speed", _agent.speed);
}
public override void MoveTarget(Vector3 target, GameObject targetObj = null)
{
TargetObj = targetObj;
if (_agent == null)
{
return;
}
StopCoroutine(OnMove(target));
StartCoroutine(OnMove(target));
}
public override void StopMove(Vector3 receivedEuler, Vector3 receivePos)
{
if (_agent == null) return;
_agent.ResetPath();
_agent.velocity = Vector3.zero;
State = CreatureState.Idle;
if (transform.position != receivePos)
transform.DOMove(receivePos, 0.2f);
Quaternion targetRotation = Quaternion.Euler(receivedEuler);
if (transform.rotation != targetRotation)
transform.DORotate(receivedEuler, 0.1f);
}
IEnumerator OnMove(Vector3 target)
{
_agent.ResetPath();
_agent.SetDestination(target);
State = CreatureState.Moving;
#if UNITY_SERVER
while (true)
{
if (Vector3.Distance(_agent.destination, transform.position) < 0.3f)
{
C_StopMove moveStopPacket = new C_StopMove() { PosInfo = new PositionInfo() };
moveStopPacket.PosInfo.Pos = new Positions() { PosX = transform.position.x, PosY = transform.position.y, PosZ = transform.position.z };
Vector3 rotationEuler = transform.rotation.eulerAngles;
moveStopPacket.PosInfo.Rotate = new RotateInfo() { RotateX = rotationEuler.x, RotateY = rotationEuler.y, RotateZ = rotationEuler.z };
moveStopPacket.IsMonster = true;
moveStopPacket.ObjectId = Id;
Managers.Network.Send(moveStopPacket);
break;
}
yield return null;
}
#endif
yield return null;
}
#if UNITY_SERVER
public override IEnumerator CheckPosInfo()
{
while (true)
{
var offset = transform.position - PrevPos;
if (offset.sqrMagnitude > 0.01f)
{
C_CheckPos checkPosPacket = new C_CheckPos() { CurPosInfo = new PositionInfo() };
checkPosPacket.CurPosInfo.Pos = new Positions() { PosX = transform.position.x, PosY = transform.position.y, PosZ = transform.position.z };
Vector3 rotationEuler = transform.rotation.eulerAngles;
checkPosPacket.CurPosInfo.Rotate = new RotateInfo() { RotateX = rotationEuler.x, RotateY = rotationEuler.y, RotateZ = rotationEuler.z };
Managers.Network.Send(checkPosPacket);
PrevPos = transform.position;
}
yield return new WaitForSecondsRealtime(1f);
}
}
#endif
public override void ChangeHp(int hp, bool isHeal)
{
if (isHeal)
{
}
else
{
_anim.SetTrigger("Damage");
// 체력바 등 작업
}
}
}
이게 몬스터 컨트롤러의 전문이다. #if 를 통해 데디 서버에서만 움직임 패킷을 보낼 수 있게 했고, 또한 위치 동기화도 데디서버를 통해서만 가능하다.
서버에서는 7초마다 한번씩 몬스터 어슬렁 거리게 만들어준다.
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);
}
Player _target;
int _chaseCellDist = 20;
Positions nextPos = new Positions();
public long _nextSearchTick = 0;
protected virtual void UpdateIdle()
{
if (Room.PlayerCount < 1) return;
if (_nextSearchTick == 0)
_nextSearchTick = Environment.TickCount64 + 7000;
if (_nextSearchTick > Environment.TickCount64)
return;
_nextSearchTick = Environment.TickCount64 + 7000;
Random random = new Random();
nextPos.PosX = Pos.PosX + random.Next(-5,5);
nextPos.PosY = PosInfo.Pos.PosY + 1;
nextPos.PosZ = Pos.PosZ + random.Next(-5,5);
State = CreatureState.Moving;
}
int _skillRange = 1;
long _nextMoveTick = 0;
protected virtual void UpdateMoving()
{
if (isMoving == true) return;
if (Room.PlayerCount < 1) return;
// 추가적인 추적 및 스킬은 나중에
BroadcastMove();
}
void BroadcastMove()
{
// 다른 플레이어한테도 알려준다
isMoving = true;
S_Move movePacket = new S_Move();
movePacket.ObjectId = Id;
PositionInfo nextPosinfo = new PositionInfo();
nextPosinfo.MergeFrom(PosInfo);
nextPosinfo.Pos = nextPos;
movePacket.DestPosInfo = nextPosinfo;
movePacket.TargetId = -1;
Room.Broadcast(movePacket);
}
0.2초마다 Update를 돌며 현재 상태에 맞는 행동을 하게 된다.
결과
실제 결과물 데디서버는 밑과 같고 플레이 영상은 밑에 있다.
'Unity > 온라인 RPG' 카테고리의 다른 글
[Unity 3D] 각종 UI 제작! Stat창, Inven창, Equip창 (0) | 2024.05.28 |
---|---|
[Unity 3D] 오브젝트 전투 및 몬스터의 플레이어 추격 (0) | 2024.05.13 |
[Unity 3D] 캐릭터 공격 애니메이션 동기화 (0) | 2024.04.24 |
[Unity 3D] 캐릭터 이동 동기화 (0) | 2024.04.23 |
[Unity 3D] Navigation을 이용한 캐릭터 이동 (0) | 2024.04.17 |