내일배움캠프 Unity 83일차 TIL - 팀 9와 4분의 3 - 개발일지

Wooooo·2024년 2월 23일
0

내일배움캠프Unity

목록 보기
85/94

[오늘의 키워드]

InteractSystem 클래스 구현


[InteractSystem 클래스 구현]

PlayerBaseState의 일부 기능 분리의 필요성

    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이라는 클래스를 작성해서 기능을 이관하기로 했다.

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를 상호작용 시도마다 호출해서 근처의 상호작용할 오브젝트를 탐색했는데, 상호작용이 가능한 모든 오브젝트를 탐색한 다음, 거리별로 정렬해줬다.

또, InteractSystemPlayerStateMachine의 존재를 모르고 있다.
대신, 각 상호작용 별 이벤트를 호출하는 구조로 작성해놨는데, 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);
    }
profile
game developer

0개의 댓글