InteractSystem
플레이어와 타게팅 오브젝트의 거리 계산 로직 개선PlayerStateMachine
캐릭터가 공격 상태일 때 판단public void SearchObject(Vector3 position, float maxRange, LayerMask targetLayers)
{
targets = Physics.OverlapSphere(position, maxRange, targetLayers, QueryTriggerInteraction.Collide);
targets = targets.OrderBy(x => Vector3.SqrMagnitude(position - x.transform.position)).ToArray();
}
플레이어와 타겟 오브젝트의 단순 Position 거리 비교를 진행하고 있었다.
이렇게 계산하니 문제가 발생했다. OverlapSphere로 검출된 콜라이더지만, 계산된 거리는 상호작용할 도구의 사정거리보다 멀어서 상호작용을 진행하지 않았다.
private bool TryToolInteract(ToolItemData tool)
{
foreach (var target in targets)
{
if (!target.CompareTag(tool.targetTagName) || (1 << target.gameObject.layer & tool.targetLayers) == 0)
continue;
if (Vector3.SqrMagnitude(target.transform.position - _transform.position) > tool.range * tool.range)
break;
if (target.TryGetComponent<IInteractable>(out var interactable))
{
OnToolInteract?.Invoke(interactable, target.tag, target.transform.position);
return true;
}
}
return false;
}
targets
배열은 캐릭터와 거리 순으로 정렬해뒀기 때문에, 도구의 사정거리보다 현재 검사 중인 인덱스의 거리가 멀어지면 검사를 종료하도록 해뒀다. (어차피 다음 인덱스는 현제 인덱스의 오브젝트보다 거리가 더 멀어서 검사할 필요가 없다.)
하지만 거리를 단순비교로 계산했기 때문에 거리 계산 로직을 개선해야한다.
빨간 원이 OverlapSphere의 범위라고 치자.
현재 거리 계산 방식은 파란선의 거리를 계산한다.
OverlapSphere가 돌에 아슬아슬하게 걸치면, 파란선의 길이는 사정거리 (빨간 원의 반지름)을 넘어간다.
따라서, 초록선의 거리를 계산해야한다. 이는 Collider.ClosestPoint(Vector3)
를 이용해 찾기로 했다.
ClosestPoint(Vector3)
는 매개변수로 넘겨준 Vector3값과 가장 가까운 콜라이더의 표면의 위치를 반환해준다.
즉, 이 이미지에서 예시로 초록색 원의 위치를 반환해준다.
이 위치와 플레이어의 position을 기준으로 거리를 다시 계산한다.
private InteractableTarget[] _targets;
public readonly struct InteractableTarget
{
public readonly Collider targetCollider;
public readonly float closestPoint;
public readonly GameObject gameObject => targetCollider.gameObject;
public readonly Transform transform => targetCollider.transform;
public readonly string tag => targetCollider.tag;
public InteractableTarget(Collider targetCollider, Vector3 searchOrigin)
{
this.targetCollider = targetCollider;
closestPoint = Vector3.SqrMagnitude(targetCollider.transform.position - targetCollider.ClosestPoint(searchOrigin));
}
public readonly bool CompareTag(string tag) => targetCollider.CompareTag(tag);
public readonly bool TryGetComponent<T>(out T component) => targetCollider.TryGetComponent(out component);
}
적용하기 앞서, ClosestPoint
를 매번 계산하는 것을 피하고(최초 검사 시 1번만 계산), 정렬을 쉽게 하기 위해 InteractableTarget
구조체를 새로 작성했다.
public void SearchObject(Vector3 position, float maxRange, LayerMask targetLayers)
{
var colliders = Physics.OverlapSphere(position, maxRange, targetLayers, QueryTriggerInteraction.Collide);
_targets = colliders.Select(x => new InteractableTarget(x, position)).OrderBy(x => x.closestPoint).ToArray();
}
OverlapSphere로 검출한 콜라이더들을 구조체로 변환한 후, 정렬한다.
private bool TryToolInteract(ToolItemData tool)
{
foreach (var target in _targets)
{
if (!target.CompareTag(tool.targetTagName) || (1 << target.gameObject.layer & tool.targetLayers) == 0)
continue;
if (target.closestPoint > tool.range * tool.range)
break;
if (target.TryGetComponent<IInteractable>(out var interactable))
{
OnToolInteract?.Invoke(interactable, target.tag, target.transform.position);
return true;
}
}
return false;
}
상호작용 검사 시에는 미리 계산된 ClosestPoint를 이용해 검사를 중단할지 판단한다.
현재, PlayerComboAttackState
는 유저가 퀵슬롯을 터치 했을 때 호출되는 메서드를 아무것도 하지 않는
메서드로 오버라이딩 중이다.
하지만, 전투 중 체력을 회복하기 위해선 퀵슬롯에 등록된 음식을 먹어야하는데, 공격 모션이 나가는 동안, 즉, PlayerComboAttackState
인 동안은 퀵슬롯으로 음식을 먹을 수가 없다.
이를 해결하기 위해 OnQuickUseStarted
메서드를 오버라이딩하지 않도록 하고, QuickSlotSystem
에서 캐릭터의 상태에 따라 로직을 제어할 필요가 있다고 판단했다.
사실 오버라이딩만으로 해결된다면 결합도도 낮고 좋겠지만, 캐릭터가 아이템을 사용하는 로직이 생각보다 복잡해서(?) 각 클래스에서 직접 로직을 제어하도록 했다.
PlayerStateMachine
에는 IsAttacking
이란 프로퍼티가 있긴한데, 이 프로퍼티는 유저가 공격 버튼을 누르고 있는지 판단하는 프로퍼티이다. 따라서, 캐릭터가 공격 상태인지 판단하는 프로퍼티를 새로 작성할 필요가 있었다.
아직 실제로 인스턴스를 갖고 공격을 하는 상태는 PlayerComboAttackState
뿐이지만, 이 친구도 PlayerAttackState
를 상속받고 있다.
public bool IsAttackState => currentState is PlayerAttackState;
IsAttackState
프로퍼티를 PlayerStateMachine
에 추가하여, 현재 상태가 PlayerAttackState
를 상속받고 있는지를 반환해주도록 했다.
public void UnRegist(int index)
{
for (int i = 0; i < _slots.Length; ++i)
{
if (_slots[i].itemSlot.itemData != null && _slots[i].targetIndex == index)
{
if (_player.StateMachine.IsAttackState && _indexInUse == i)
return;
_slots[i].itemSlot.SetRegist(false);
OnUnRegisted?.Invoke(_slots[i]);
_slots[i].Clear();
OnUpdated?.Invoke(i, _slots[i].itemSlot);
return;
}
}
}
공격 모션 중에는 현재 착용 중인 무기를 장착해제 할 수 없도록 했다.
private void EatFood(int index, ItemSlot itemSlot)
{
var foodItem = itemSlot.itemData as FoodItemData;
if (foodItem == null) return;
var conditionHandler = _player.ConditionHandler;
foreach (var playerCondition in foodItem.conditionModifier)
{
switch (playerCondition.Condition)
{
case Conditions.HP:
conditionHandler.HP.Add(playerCondition.value);
break;
case Conditions.Hunger:
conditionHandler.Hunger.Add(playerCondition.value);
break;
}
}
_player.Inventory.TryConsumeQuantity(index, 1);
}
private void EquipTool(int index, ItemSlot itemSlot)
{
if (_player.StateMachine.IsAttackState)
return;
_player.ToolSystem.Equip(index, itemSlot);
}
private void EquipWeapon(int index, ItemSlot itemSlot)
{
if (_player.StateMachine.IsAttackState)
return;
_player.ToolSystem.Equip(index, itemSlot);
}
private void EquipArmor(int index, ItemSlot itemSlot)
{
if (_player.StateMachine.IsAttackState)
return;
_player.ArmorSystem.Equip(index, itemSlot);
}
private void Build(int index, ItemSlot itemSlot)
{
if (_player.StateMachine.IsAttackState)
return;
_player.Building.CreateArchitecture(index, itemSlot);
}
음식을 먹는 메서드를 제외하고는 공격 모션일 때 요청이 무시되도록 수정했다.
private void OnItemUnregisted()
{
if (_player.StateMachine.IsAttackState)
return;
UnEquip();
}
아무것도 등록되지 않은 퀵슬롯을 터치했을 때, 장착을 해제하고 빈 손으로 만들어 주는 메서드다.
마찬가지로 공격 상태일 때는 장비가 해제되지 않도록 했다.