지난 포스팅에 이어서 10일 정도의 시간이 흘렀는데 몬스터 로직을 구성하는데 생각보다 많은 시간을 써서 이제야 포스팅하게 됐다.
전투 시스템(플레이어)
플레이어의 공격은 대체적으로 이전 포스팅 중 다뤘었다. 그 중 실제 타격만 제외하고 구성했었는데 실제로 타격하는 코드를 작성해보자.
이전과 달라진 점이 있다면 공격 모션과 실제 타격을 분리했다는 점이다.
공격마다 특정한 모션이 있을 거고 실제 타격되는 시점이 존재할 것이다. 그렇기 때문에 실제 칼을 휘두르는 모션 패킷 따로 데미지를 입히는 패킷 따로 보내는 형식으로 수정했다.
public void OnClickMouseInputEvent()
{
_moveTime += Time.deltaTime;
if (Input.GetMouseButton(1) && _moveTime >= 0.3f)
{
if (State == CreatureState.Skill || State == CreatureState.Dead || State == CreatureState.Wait) return;
Ray ray = cm.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit))
{
// TODO : 이동 패킷
//MoveTarget(hit.point);
C_Move movePacket = new C_Move() { PosInfo = new PositionInfo() { Pos = new Positions() } };
movePacket.PosInfo.State = CreatureState.Moving;
Positions pos = new Positions() { PosX = hit.point.x, PosY = hit.point.y, PosZ = hit.point.z };
movePacket.PosInfo.Pos = pos;
Managers.Network.Send(movePacket);
_moveTime = 0;
}
}
else if (Input.GetMouseButtonDown(0))
{
if (State == CreatureState.Skill || State == CreatureState.Dead || State == CreatureState.Wait) return;
// idle 상태인지 검증 -> 아니라면 멈춤 패킷 보냈다가 공격 패킷 보냄
if (State != CreatureState.Idle && State == CreatureState.Moving)
{
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 };
Managers.Network.Send(moveStopPacket);
return;
}
int attackRand = Random.Range(1, 3);
if (attackRand < 0 || attackRand > 2)
{
attackNum = -1;
return;
}
else
{
// 스킬 모션 발생
Skill skill = null;
if (Managers.Data.SkillDict.TryGetValue(attackRand, out skill) == false) return;
C_SkillMotion skillMotion = new C_SkillMotion() { Info = new SkillInfo() };
skillMotion.ObjectId = Id;
skillMotion.Info.SkillId = attackRand;
skillMotion.IsMonster = false;
Managers.Network.Send(skillMotion);
State = CreatureState.Wait;
StartCoroutine(CoAttackTimeWait(skill));
}
}
}
public IEnumerator CoAttackTimeWait(Skill skill, bool isContinual = false)
{
for (int i = 0; i < skill.skillDatas.Count; i++)
{
yield return new WaitForSeconds(skill.skillDatas[i].attackTime);
if(State == CreatureState.Skill)
{
C_MeleeAttack meleeAttack = new C_MeleeAttack() { Info = new SkillInfo(), Forward = new Positions() };
meleeAttack.Info.SkillId = skill.id;
meleeAttack.Forward = Util.Vector3ToPositions(transform.forward);
meleeAttack.IsMonster = false;
meleeAttack.ObjectId = Id;
if (isContinual) { meleeAttack.Time = i; }
else { meleeAttack.Time = 0; }
Managers.Network.Send(meleeAttack);
}
}
}
이게 이번에 바뀐 클라에서 플레이어의 공격을 다루는 내용이다. 각 공격(skill)마다 타격하는 시간이 있고 그 시간만큼 기다렸다가 패킷을 보내는 형식이다.
public void HandleSkillMotion(Player player, C_SkillMotion skillMotion)
{
if (player == null)
return;
if(skillMotion.IsMonster == false)
{
S_SkillMotion skillMotionServer = new S_SkillMotion() { Info = new SkillInfo() };
skillMotionServer.ObjectId = player.Id;
skillMotionServer.Info.SkillId = skillMotion.Info.SkillId;
Broadcast(skillMotionServer);
}
else
{
Monster monster = null;
_monsters.TryGetValue(skillMotion.ObjectId, out monster);
if (monster == null) return;
if (monster.isMotion) return;
monster.isMotion = true;
Console.WriteLine("공격 모션");
S_SkillMotion skillMotionServer = new S_SkillMotion() { Info = new SkillInfo() };
skillMotionServer.ObjectId = monster.Id;
skillMotionServer.Info.SkillId = skillMotion.Info.SkillId;
Broadcast(skillMotionServer);
}
}
public void HandleMeleeAttack(Player player, C_MeleeAttack meleeAttack)
{
if (player == null)
return;
if (meleeAttack.IsMonster == false)
{
ObjectInfo info = player.Info;
if (info.PosInfo.State != CreatureState.Idle)
return;
info.PosInfo.State = CreatureState.Skill;
Skill skill = null;
if (DataManager.SkillDict.TryGetValue(meleeAttack.Info.SkillId, out skill) == false) return;
foreach (Monster monster in _monsters.Values)
{
Vector3 a = Utils.PositionsToVector3(player.Pos);
Vector3 b = Utils.PositionsToVector3(monster.Pos);
Vector3 forward = Utils.PositionsToVector3(meleeAttack.Forward);
if (IsObjectInRange(a, b, forward, skill.skillDatas[meleeAttack.Time].range) == true)
{
monster.OnDamaged(player, skill.skillDatas[meleeAttack.Time].damage + player.Attack);
}
}
info.PosInfo.State = CreatureState.Idle;
}
else
{
Monster monster = null;
_monsters.TryGetValue(meleeAttack.ObjectId, out monster);
if (monster == null) return;
Console.WriteLine("공격 중 브로드캐스팅 +" + meleeAttack.Info.SkillId);
monster.forwardMonster = Utils.PositionsToVector3(meleeAttack.Forward);
monster.isCanAttack = true;
}
}
그렇다면 서버에선 이렇게 동작한다. 공격 모션같은 경우 모든 플레이어의 동작을 맞춰주기 위해 단순히 에코서버처럼 돌려주기만하고 중요한 부분은 if (IsObjectInRange(a, b, forward, skill.skillDatas[meleeAttack.Time].range) == true) 이 부분에 있다.
이 부분 함수를 확인해보자.
public bool IsObjectInRange(Vector3 attacker, Vector3 target, Vector3 forward, SKillRange skill)
{
Vector3 up = new Vector3(0, 1, 0); // Up vector assuming Y is up
Vector3 right = forward.Cross(up).normalized; // Right vector
Vector3 relativePos = target - attacker;
float forwardDistance = Vector3.Dot(relativePos, forward);
float rightDistance = Vector3.Dot(relativePos, right);
float upwardDistance = Vector3.Dot(relativePos, up);
if (forwardDistance >= skill.nonDepth && forwardDistance <= skill.depth &&
Math.Abs(rightDistance) <= skill.width / 2 &&
Math.Abs(upwardDistance) <= skill.height / 2)
{
return true;
}
return false;
}
이렇게 공격 대상의 forward를 받아 skill의 공격범위 안에 실제 오브젝트가 있는지(클라와 상관없이 동기화된 서버에서의 좌표) 확인하고 boolean 값을 내뱉는다. 그래서 실제로 타격지점에 있는지 확인 후 공격을 하게 된다.
전투 시스템(몬스터)
플레이어의 입장은 알아봤다. 몬스터의 경우 다만 플레이어처럼 누군가의 입력을 받는것이 아닌 서버쪽에 의존해서 행동 되어야만한다.(그래야 치트가 없다.) 지금 현재는 몬스터는 매 좌표를 기준으로 서성거리고 있는 상태이다.
흐름을 생각해보자.
1. 배회하고 있는 몬스터를 누군가(플레이어 or 몬스터)가 타격했을 때, 그 오브젝트를 추격 대상으러 선정한다.
2. 추격대상으로 선정한 후 타겟을 향해 이동한다.
3. 거리거 타격 범위 안에 들었을 경우 타격한다.
4. 추격대상이 멀어지거나 죽을 때 까지 2번부터 반복한다.
이렇게 생각해 볼 수 있다.
따라서 다음과 같은 코드를 작성한다.
public override void OnDamaged(GameObject attacker, int damage)
{
base.OnDamaged(attacker, damage);
if(Stat.Hp > 0)
{
Target = attacker;
// 추격으로 변환
S_StopMove resStopMovePacket = new S_StopMove();
resStopMovePacket.ObjectId = Info.ObjectId;
resStopMovePacket.PosOk = true;
resStopMovePacket.Rotate = PosInfo.Rotate;
resStopMovePacket.Pos = Pos;
Room.Broadcast(resStopMovePacket);
_MoveTick = 0;
State = CreatureState.Wait;
Console.WriteLine("맞음");
isCanAttack = false;
isMotion = false;
Room.PushAfter(1200, ChangeStateAfterTime, CreatureState.Skill);
}
}
public void ChangeStateAfterTime(CreatureState state)
{
State = state;
}
이것이 바로 1번에 해당된다. 피격을 당했을 경우 타겟을 설정하고 움직이고 있었다면 멈춘다. 그리고 잠시 wait 상태로 변환하는데 이는 잠시 피격했을 때 바로 행동하는 것(실제로 움직이지 않아야함. 피격 딜레이)을 방지하기 위함이다.
그리고 나서 1.2초 뒤에 상태를 변환하게 된다.(추격 상태 본문에서는 Skill 이라고 칭한다.)
스킬 상태를 봐보자.
protected override void UpdateSkill()
{
if (_coolTick == 0)
{
if (isCanAttack)
{
Console.WriteLine("공격 중에 있음");
Skill skillData = null;
DataManager.SkillDict.TryGetValue(4, out skillData);
Vector3 attacker = Utils.PositionsToVector3(Pos);
Vector3 target = Utils.PositionsToVector3(Target.Pos);
if(Room.IsObjectInRange(attacker, target, forwardMonster, skillData.skillDatas[0].range))
{
if (State == CreatureState.Skill)
Target.OnDamaged(this, skillData.skillDatas[0].damage + TotalAttack);
}
int coolTick = (int)(1000 * skillData.cooldown);
_coolTick = Environment.TickCount64 + coolTick;
isCanAttack = false;
isMotion = false;
}
else
{
if (isMotion) return;
if (Target == null || Target.Room != Room)
{
Target = null;
State = CreatureState.Idle;
return;
}
nextPos = Target.Pos;
BroadcastMove();
}
}
if (_coolTick > Environment.TickCount64)
return;
_coolTick = 0;
}
일단 각 몬스터 마다 공격 기술이 존재하는데 (이는 Json 파일로 관리) 거기에 맞춰 쿨타임을 조절한다.
여기서 isCanAttack은 실제 공격모션이 타격지점까지 올바르게 도달했을 경우를 의미한다. 올바르지 않은 경우라 함은 공격 도중 누군가에게 피격을 당했을 때를 의미한다. isCanAttack이 true라서 진행된다면 플레이어와 동일하게 타격 범위에 있는지 확인하고 데미지를 준다. 그리고 쿨타임을 리셋시킨다.
만약 isCanAttack이 false라면 두가지의 경우가 있을 수 있다. 아직 스킬타격모션에 도달하지 못했을 경우나 플레이어와의 거리가 충분하지 않은 경우다. 그렇다면 이는 어떻게 판별할 수 있을까?
이전 포스팅에서 데디서버를 이용해 몬스터의 위치를 동기화 했다.
그런것과 비슷하게 데디서버를 통해서 몬스터의 공격을 서버에 전달하도록 하자.
public override void OnAttack(SkillInfo info)
{
Skill skill = null;
if (Managers.Data.SkillDict.TryGetValue(info.SkillId, out skill) == false) return;
transform.LookAt(TargetObj.transform);
_agent.ResetPath();
_agent.velocity = Vector3.zero;
State = CreatureState.Skill;
_anim.SetTrigger("Attack");
StartCoroutine(CoAttackPacket(skill));
}
public IEnumerator CoAttackPacket(Skill skill)
{
yield return new WaitForSeconds(skill.skillDatas[0].attackTime);
#if UNITY_SERVER
if(State == CreatureState.Skill)
{
C_MeleeAttack meleeAttack = new C_MeleeAttack() { Info = new SkillInfo(), Forward = new Positions() };
meleeAttack.Info.SkillId = skill.id;
meleeAttack.Forward = Util.Vector3ToPositions(transform.forward);
meleeAttack.IsMonster = true;
meleeAttack.Time = 0;
meleeAttack.ObjectId = Id;
Managers.Network.Send(meleeAttack);
}
#endif
yield return new WaitForSeconds(skill.cooldown - (int)skill.skillDatas[0].attackTime);
isAttackMotion = false;
State = CreatureState.Idle;
}
public override IEnumerator OnMove(Vector3 target)
{
if (isAttackMotion) yield break;
_agent.ResetPath();
if (TargetObj != null && Vector3.Distance(target, transform.position) >= 1.2f)
_agent.SetDestination(target);
State = CreatureState.Moving;
while (true)
{
if (TargetObj == null)
{
if (Vector3.Distance(_agent.destination, transform.position) < 0.3f)
{
#if UNITY_SERVER
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;
#else
break;
#endif
}
}
else
{
if (Vector3.Distance(_agent.destination, transform.position) < 1.2f)
{
#if UNITY_SERVER
transform.LookAt(TargetObj.transform);
C_SkillMotion skillMotion = new C_SkillMotion() { Info = new SkillInfo() };
skillMotion.ObjectId = Id;
skillMotion.Info.SkillId = 4;
skillMotion.IsMonster = true;
Managers.Network.Send(skillMotion);
isAttackMotion = true;
break;
#else
isAttackMotion = true;
break;
#endif
}
}
yield return null;
}
yield return null;
}
#if UNITY_SERVER를 통해 다른 클라에서 뜯을 수 없게 코드를 분리한 다음 데디 서버에서만 동작하게 만들었다.
실제 구현영상
'Unity > 온라인 RPG' 카테고리의 다른 글
[Unity 3D] 아이템 드랍 및 획득 (0) | 2024.05.29 |
---|---|
[Unity 3D] 각종 UI 제작! Stat창, Inven창, Equip창 (0) | 2024.05.28 |
[Unity 3D] 몬스터 움직임 동기화 with Dedicated Server (0) | 2024.05.02 |
[Unity 3D] 캐릭터 공격 애니메이션 동기화 (0) | 2024.04.24 |
[Unity 3D] 캐릭터 이동 동기화 (0) | 2024.04.23 |