InteractSystem
클래스 구현
protected virtual void OnInteractStarted(InputAction.CallbackContext context)
{
if (_stateMachine.IsFalling) return;
// ================================= TEST =====================================
Collider[] targets;
var hand = _stateMachine.Player.EquippedItem.itemSlot.itemData;
if (hand is WeaponItemData)
{
_stateMachine.ChangeState(_stateMachine.ComboAttackState);
return;
}
if (hand is ToolItemData tool)
{
targets = Physics.OverlapSphere(_stateMachine.Player.transform.position, tool.range, tool.targetLayers, QueryTriggerInteraction.Collide);
foreach (var target in targets)
{
if (target.TryGetComponent<IDestructible>(out var destructible))
{
_stateMachine.InteractState.SetTarget(destructible, tool.targetTagName, target.transform.position);
_stateMachine.ChangeState(_stateMachine.InteractState);
return;
}
}
foreach (var target in targets)
{
if (!target.CompareTag(tool.targetTagName))
continue;
if (target.TryGetComponent<IInteractable>(out var interactable))
{
_stateMachine.InteractState.SetTarget(interactable, tool.targetTagName, target.transform.position);
_stateMachine.ChangeState(_stateMachine.InteractState);
return;
}
}
}
targets = Physics.OverlapSphere(_stateMachine.Player.transform.position, _emptyHandData.range, LayerMask.GetMask("Architecture"), QueryTriggerInteraction.Collide);
foreach (var target in targets)
{
_stateMachine.InteractState.SetTarget(target.GetComponent<IInteractable>(), target.tag, target.transform.position);
_stateMachine.ChangeState(_stateMachine.InteractState);
return;
}
targets = Physics.OverlapSphere(_stateMachine.Player.transform.position, _emptyHandData.range, _emptyHandData.targetLayers, QueryTriggerInteraction.Collide);
foreach (var target in targets)
if (target.CompareTag(_emptyHandData.targetTagName))
{
_stateMachine.InteractState.SetTarget(target.GetComponent<IInteractable>(), target.tag, target.transform.position);
_stateMachine.ChangeState(_stateMachine.InteractState);
return;
}
// ============================================================================
}
현재 PlayerBaseState
에서는 플레이어의 각 상호작용을 순서대로 진행하는 코드가 모두 몰려있다.
이 기능이 워낙 크고, 중요한 작업이고, 코드도 긴데, 나중에 이 기능을 수정할 일이 생기면 어디에 구현되있는지도 찾아야하고, 플레이어의 상호작용을 수정하는 일인데 PlayerBaseState
를 수정해야하는 이상한 상황이 일어나게 된다.
우선, 단일책임원칙을 준수할 수 있도록 이 기능을 담당하는 클래스인 InteractSystem
이라는 클래스를 작성해서 기능을 이관하기로 했다.
public class InteractSystem
{
private ToolSystem _toolSystem;
private Transform _transform;
private ToolItemData _emptyHand;
private LayerMask _architectureLayerMask;
private LayerMask _monsterLayerMask;
private LayerMask _resourcesLayerMask;
private LayerMask _interactableAllLayerMask;
private Collider[] targets;
public event Action<IHit, Vector3> OnWeaponInteract;
public event Action<IInteractable, string, Vector3> OnToolInteract;
public event Action<IDestructible, string, Vector3> OnToolDestruct;
public event Action<IInteractable, string, Vector3> OnArchitectureInteract;
public InteractSystem()
{
_toolSystem = GameManager.Instance.Player.ToolSystem;
_transform = _toolSystem.transform;
_emptyHand = Managers.Resource.GetCache<ToolItemData>("EmptyHandItemData.data");
_architectureLayerMask = LayerMask.GetMask("Architecture");
_monsterLayerMask = LayerMask.GetMask("Monster");
_resourcesLayerMask = LayerMask.GetMask("Resources");
_interactableAllLayerMask = _architectureLayerMask | _monsterLayerMask | _resourcesLayerMask;
}
public void TryInteractSequence()
{
SearchAllObject(_transform.position);
var tool = GetCurrentTool();
var isWeapon = tool is WeaponItemData;
// # 1. 타게팅 공격
if (isWeapon)
{
if (TryWeaponInteract(tool))
return;
}
// # 2. 도구 상호작용
if (tool != _emptyHand)
{
// # 2-1. 파괴
if (TryToolDestruct(tool))
return;
// # 2-2. 벌목, 채광
if (TryToolInteract(tool))
return;
}
// # 3. 구조물 상호작용
if (TryArchitectureInteract(_emptyHand))
return;
// # 4. 채집 상호작용
if (TryToolInteract(_emptyHand))
return;
// # 5. 허공에 공격
if (isWeapon)
{
if (TryWeaponInteract(tool, true))
return;
}
}
public bool TryWeaponInteract(ToolItemData tool, bool force = false)
{
if (force)
{
OnWeaponInteract?.Invoke(null, _transform.forward);
return true;
}
foreach (var target in targets)
{
if (!target.CompareTag("Monster") || (1 << target.gameObject.layer & tool.targetLayers) == 0)
continue;
if (Vector3.SqrMagnitude(target.transform.position - _transform.position) > tool.range * tool.range)
break;
if (target.TryGetComponent<IHit>(out var hit))
{
OnWeaponInteract?.Invoke(hit, target.transform.position);
return true;
}
}
return false;
}
public 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;
}
public bool TryToolDestruct(ToolItemData tool)
{
foreach (var target in targets)
{
if ((1 << target.gameObject.layer & tool.targetLayers) == 0)
continue;
if (Vector3.SqrMagnitude(target.transform.position - _transform.position) > tool.range * tool.range)
break;
if (target.TryGetComponent<IDestructible>(out var IDestructible))
{
OnToolDestruct?.Invoke(IDestructible, tool.targetTagName, target.transform.position);
return true;
}
}
return false;
}
public bool TryArchitectureInteract(ToolItemData tool)
{
foreach (var target in targets)
{
if ((1 << target.gameObject.layer & _architectureLayerMask) == 0)
continue;
if (Vector3.SqrMagnitude(target.transform.position - _transform.position) > tool.range * tool.range)
break;
if (target.TryGetComponent<IInteractable>(out var interactable))
{
OnArchitectureInteract?.Invoke(interactable, "Make", target.transform.position);
return true;
}
}
return false;
}
// 한 번에 모든 오브젝트를 서칭하고, 거리 순으로 정렬
public void SearchAllObject(Vector3 position) => SearchObject(position, GetMaxRange(), _interactableAllLayerMask);
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();
}
private float GetMaxRange()
{
return Mathf.Max(GetCurrentTool().range, _emptyHand.range);
}
private ToolItemData GetCurrentTool()
{
return _toolSystem.EquippedTool.itemSlot.itemData as ToolItemData;
}
}
일단 코드를 그대로 가져온 다음, 가독성을 위해 작은 단위 기능들을 먼저 메서드로 빼놓고, 가장 중요한 우선순위에 따른 상호작용
을 진행하는 TryInteractSequence()
에서 단위 기능 메서드들을 순서대로 붙여줬다.
그 다음에는 추가 기능들을 보탰다. 기능을 이관하기 전에는 OverlapSphere를 상호작용 시도마다 호출해서 근처의 상호작용할 오브젝트를 탐색했는데, 상호작용이 가능한 모든 오브젝트를 탐색한 다음, 거리별로 정렬해줬다.
또, InteractSystem
은 PlayerStateMachine
의 존재를 모르고 있다.
대신, 각 상호작용 별 이벤트를 호출하는 구조로 작성해놨는데, PlayerStateMachine
은 이 이벤트를 통해서 상태를 전환하도록 했다.
public class PlayerStateMachine : StateMachine
{
...
public InteractSystem InteractSystem { get; private set; }
public PlayerStateMachine(Player player)
{
...
InteractSystem = new();
InteractSystem.OnWeaponInteract += TransitionComboAttack;
InteractSystem.OnToolInteract += TansitionInteract;
InteractSystem.OnToolDestruct += TansitionInteract;
InteractSystem.OnArchitectureInteract += TansitionInteract;
}
private void TransitionComboAttack(IHit target, Vector3 targetPos)
{
ChangeState(ComboAttackState);
}
private void TansitionInteract(IInteractable target, string targetTag, Vector3 targetPos)
{
InteractState.SetTarget(target, targetTag, targetPos);
ChangeState(InteractState);
}
private void TansitionInteract(IDestructible target, string targetTag, Vector3 targetPos)
{
InteractState.SetTarget(target, targetTag, targetPos);
ChangeState(InteractState);
}