Unity 최종 프로젝트 - 17 (BT Refactoring Plan)

이준호·2024년 2월 6일
0

📌 Unity 최종 프로젝트



📌 Behavior Tree 정리

➔ 정리 이유

현재 BT는 생성자와 Func를 이용해 간단하게 짜기에는 매우 간편하게 되어있다. 하지만 점점 크기가 커질수록 가독성이 떨어지고 복잡해지는 경향이 있어서 새로 리팩토링에 들어가기 전에 지금 해둔 내용들을 정리해두고 리팩토링에 진행하려고 한다.




➔ 리팩토링 순서

1. Behavior Tree Architecture

  • Behavior Tree - Scriptable Object

    • SO를 통해 관리하여 유지보수성 증가 기대
  • Behavior Tree Architecture Change

    • SO를 통해 관리하기 위해 전체적인 BT구조 변경



2. Action Node Class Partial

  • 현재 액션 노드들이 EnemyBasicBT 스크립트에 모여서 사용중이다. (Func)

  • 코드가 너무 길어지고 가독성이 좋지 않아져서 우선 액션 노드를 각 클래스별로 나눠두려고 한다.




3. Behavior Tree UI_Builder Node Graph View

  • BT Node Visualization
    • BT의 종류가 많아지면 (몬스터 종류가 늘어나서) 관리가 어려워질 것을 예상하여 (이미 많이 가독성이 떨어졌다.) 커스텀 에디터를 이용하여 시각화 작업을 할 예정이다.



4. Enemy Damageable, Comative

  • Enemy 공격 및 피격 처리 작업



5. Spanwer

  • ObjectSpawner (플레이어, 좀비 등)

  • EntitySpawner (루팅 가능 오브젝트들)




6. Enemy NavMeshAgent SetDestination UniTask

  • Cysharp namespace

  • UniTask라는 쓰레드

    • 기존 C# Task는 무겁고 Unity(단일 스레드)와 일치하지 않기 때문에 유니티에게 최적화해서 사용 가능하게 만들어짐
    • Coroutine의 단점은 return 값이 따로 없어 따로 Callback 처리를 해줘야 하고 try-catch 예외처리도 불가능, StartCoroutine과 YieldInstruction에서 가비지가 주로 생성되는 단점이 있는 Coroutine을 대체 가능
    • https://wlsdn629.tistory.com/entry/유니티-코루틴-대신-unitask
  • Path Request Manager (← 모든 Enemy가 이 친구한테 경로를 요청 )

    • 순찰 상태
    • 추적 상태











📌 Behavior Tree - Code Organize

➔ BT Core

BehaviorTreeRunner

public sealed class BehaviorTreeRunner
{
    private readonly INode _rootNode;

    public BehaviorTreeRunner(INode rootNode)
    {
        _rootNode = rootNode;
    }

    public void Operate()
    {
        _rootNode.Evaluate();
    }
}



INode

public interface INode
{
    public enum E_NodeState
    {
        ENS_Running,
        ENS_Success,
        ENS_Failure
    }

    public E_NodeState Evaluate();
}



DataContext

public class DataContext
{
    public GameObject GameObject;  // this.GameObject
    public Transform Transform;  // target.Transform
    public Animator Animator;     // this.Animator
    public Rigidbody Rigidbody;   // this.RigidBody
    public RaycastHit RaycastHit;  // Target & Obstacle Check 
    public Collider OverlapColliders; // Target Information Bring
    
    // 추후 필요한 데이터 추가

    public static DataContext CreatDataContext(GameObject gameObject)
    {
        DataContext dataContext = new DataContext
        {
            GameObject = gameObject,
            Transform = gameObject.transform,
            Animator = gameObject.GetComponent<Animator>(),
            Rigidbody = gameObject.GetComponent<Rigidbody>(),
            RaycastHit = gameObject.GetComponent<RaycastHit>(),
            OverlapColliders = gameObject.GetComponent<Collider>()
        };

        // 추후 필요한 데이터 추가
        
        return dataContext;
    }
}





➔ Composite

RandomSelector

public sealed class RandomSelector : MonoBehaviour
{
    private readonly List<INode> _children;
    private int _currentIndex = -1;

    public RandomSelector(List<INode> children)
    {
        _children = children;
    }
    
    // 나중에 실행여부 확인하고 리팩토링 진행예정.
    public INode.E_NodeState Evaluate()
    {
        if (_children == null || _children.Count == 0)
            return INode.E_NodeState.ENS_Failure;
        
        // ChildNode Running Check
        if (_currentIndex != -1 && _children[_currentIndex].Evaluate() == INode.E_NodeState.ENS_Running)
            return INode.E_NodeState.ENS_Running;
        
        // Random ChildNode Apply
        int randomIndex = Random.Range(0, _children.Count);
        _currentIndex = randomIndex;
        INode.E_NodeState randomChild = _children[_currentIndex].Evaluate();
        
        // ChildNode Running Reset
        if (randomChild != INode.E_NodeState.ENS_Running)
            _currentIndex = -1;
        
        return randomChild;
    }
}



Selector

public sealed class SelectorNode : INode
{
    private readonly List<INode> _children;

    public SelectorNode(List<INode> children)
    {
        _children = children;
    }

    public INode.E_NodeState Evaluate()
    {
        if (_children == null || _children.Count == 0)
            return INode.E_NodeState.ENS_Failure;

        foreach (var child in _children)
        {
            switch (child.Evaluate())
            {
                case INode.E_NodeState.ENS_Running:
                    return INode.E_NodeState.ENS_Running;
                case INode.E_NodeState.ENS_Success:
                    return INode.E_NodeState.ENS_Success;
                case INode.E_NodeState.ENS_Failure:
                    continue;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        return INode.E_NodeState.ENS_Failure;
    }
}



Sequence

public sealed class SequenceNode : INode
{
    private readonly List<INode> _children;

    public SequenceNode(List<INode> children)
    {
        _children = children;
    }

    public INode.E_NodeState Evaluate()
    {
        if (_children == null || _children.Count == 0)
            return INode.E_NodeState.ENS_Failure;

        foreach (var child in _children)
        {
            switch (child.Evaluate())
            {
                case INode.E_NodeState.ENS_Running:
                    return INode.E_NodeState.ENS_Running;
                case INode.E_NodeState.ENS_Success:
                    continue;
                case INode.E_NodeState.ENS_Failure:
                    return INode.E_NodeState.ENS_Failure;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }
        
        return INode.E_NodeState.ENS_Success;
    }
}





➔ Decorator

Inverter

public sealed class Inverter : INode
{
    private readonly INode _children;

    public Inverter(INode children)
    {
        _children = children;
    }

    public INode.E_NodeState Evaluate()
    {
        if (_children == null)
            return INode.E_NodeState.ENS_Failure;

        switch (_children.Evaluate())
        {
            case INode.E_NodeState.ENS_Running:
                return INode.E_NodeState.ENS_Running;
            case INode.E_NodeState.ENS_Success:
                return INode.E_NodeState.ENS_Failure;
            case INode.E_NodeState.ENS_Failure:
                return INode.E_NodeState.ENS_Success;
            default:
                throw new ArgumentOutOfRangeException();
        }
    }
}



Repeat

public sealed class Repeat : INode
{
    private readonly INode _children;

    private int _repeatCount;
    private int _currentCount = 0;

    public Repeat(INode children)
    {
        _children = children;
    }

    public INode.E_NodeState Evaluate()
    {
        if (_children == null)
            return INode.E_NodeState.ENS_Failure;
        
        // 지정된 횟수만큼 반복
        if (_currentCount < _repeatCount)
        {
            _children.Evaluate();
            _currentCount++;

            return INode.E_NodeState.ENS_Running;
        }
        else
        {
            _currentCount = 0;
            return INode.E_NodeState.ENS_Success;
        }
    }
}



Succeed

public sealed class Succeed : INode
{
    private readonly INode _children;

    public Succeed(INode children)
    {
        _children = children;
    }

    public INode.E_NodeState Evaluate()
    {
        if (_children == null)
            return INode.E_NodeState.ENS_Failure;

        switch (_children.Evaluate())
        {
            case INode.E_NodeState.ENS_Running:
                return INode.E_NodeState.ENS_Success;
            case INode.E_NodeState.ENS_Success:
                return INode.E_NodeState.ENS_Success;
            case INode.E_NodeState.ENS_Failure:
                return INode.E_NodeState.ENS_Success;
            default:
                throw new ArgumentOutOfRangeException();
        }
    }
}



UntilFail

public sealed class UntilFail : INode
{
    private readonly INode _children;

    public UntilFail(INode children)
    {
        _children = children;
    }

    public INode.E_NodeState Evaluate()
    {
        if (_children == null)
            return INode.E_NodeState.ENS_Failure;

        INode.E_NodeState children = _children.Evaluate();

        if (children == INode.E_NodeState.ENS_Failure)
        {
            return INode.E_NodeState.ENS_Success;
        }
        
        return INode.E_NodeState.ENS_Running;
    }
}





➔ Action

Action

public sealed class ActionNode : INode
{
    private readonly Func<INode.E_NodeState> _onUpdate;

    public ActionNode(Func<INode.E_NodeState> onUpdate)
    {
        _onUpdate = onUpdate;
    }

    public INode.E_NodeState Evaluate() => _onUpdate?.Invoke() ?? INode.E_NodeState.ENS_Failure;
}





➔ EnemyBasicBT

public class EnemyBasicBT : MonoBehaviour
{
    #region Global Variable

    private Animator _animator;
    public Transform _detectedPlayer;
    private NavMeshAgent _agent;
    
    private DataContext _enemyData;
    private BehaviorTreeRunner _btRunner;
    
    /// <summary>
    /// 테스트를 위해 SerializeField 사용. 추후 각 데이터 컨택스트로 옮기고 클래스별로 분류 예정.
    /// </summary>
    [Header("Distance")] 
    [SerializeField] 
    public float _detectDistance = 10f;
    [SerializeField] 
    private float _detectViewAngle = 45;
    [SerializeField]
    private float _attackDistance = 1f;
    
    [Header("PatrolPosition")]
    [SerializeField]
    private Vector2 _patrolMinPos = Vector2.one * -20;
    [SerializeField]
    private Vector2 _patrolMaxPos = Vector2.one * 20;
    [SerializeField] 
    private Vector3 _correctPos;
    [SerializeField] 
    private bool _patrolRandomPosCheck = true;

    [Header("IdleTime")]
    [SerializeField]
    private float _idleDurationTime;
    [SerializeField]
    private float _idleStartTime;
    [SerializeField]
    private bool _idleWaitCheck;

    [Header("Animations")]
    private const string _WALK_ANIM_BOOL_NAME = "IsWalk";
    private const string _RUN_ANIM_BOOL_NAME = "IsRun";
    private const string _ATTACK_ANIM_BOOL_NAME = "IsAttack";
    private const string _ATTACK_ANIM_STATE_NAME = "Attack";

    [Header("LayerMask")] 
    private readonly LayerMask _PLAYER_LAYER_MASK = 1 << 6;
    private readonly LayerMask _ENEMY_LAYER_MASK = 1 << 7;

    [Header("NavMeshAgent")]
    [SerializeField]
    private float _agentSpeed = 0.1f;
    [SerializeField] 
    private float _agentTrackingSpeed = 3f;
    [SerializeField]
    private float _agentStoppingDistance = 0.5f; // 이동 중지 거리 (Mathf.Epsilon은 너무 거리가 짧아 애니메이션이 고장남)
    [SerializeField]
    private bool _agentUpdateRotation = true; // 자동 방향전환 여부
    [SerializeField]
    private float _agentacceleartion = 50f; // 가속도
    [SerializeField]
    private float _agentCorrectionDistance = 1f; // 보정 거리 (버그 방지) 
    [SerializeField] 
    private float _agentAngularSpeed = 400f;  // Angular Speed : Agent 회전 속도 (프로퍼티)(회전 속도 : degree/sec)
    
    #endregion
    
    

    #region Unity Event Method

    private void Awake()
    {
        InitializeData();
        InitializeAgent();

    }

    private void Update()
    {
        _btRunner.Operate(); // BT 순회
    }

    #endregion
    
    

    # region Setting BT
    
    private INode SettingBT()
    {
        return new SelectorNode
        (
            new List<INode>()
            {
                new Inverter
                (
                    new SelectorNode    // ## 적 감지 & 순찰 분기 노드
                    (
                        new List<INode>()
                        {
                            new ActionNode(CheckDetectPlayer), // 범위 안에 적이 있는가?
                            new SelectorNode    // ## 순찰 판정 분기 노드
                            (
                                new List<INode>()
                                {
                                    new ActionNode(RandomPositionAssignment), // 랜덤 목적지 부여 여부
                                    new ActionNode(CorrectPathCheck), // 목적지 까지의 경로가 유효한가?
                                    new SequenceNode
                                    (
                                        new List<INode>()
                                        {
                                            new ActionNode(CheckArrivalAtDestination), // 목적지에 도착헸는가?
                                            new ActionNode(IdleWaitTimeCheck) // 목적지에 도착했는가?
                                        }
                                    )
                                }
                            )
                        }
                    )
                ),
                new SequenceNode    // ## 공격실행 판정 분기 노드
                (
                    new List<INode>()
                    {
                        new ActionNode(CheckAttacking), // 공격중인가?
                        new ActionNode(CheckPlayerWithineAttackDistance), // 공격범위 안에 플레이어가 있는가?
                        new ActionNode(DoAttack) // 공격
                    }
                ),
                new ActionNode(DoTracking) // 추적
                
            }
        );
    }
    
    #endregion
    
    

    # region Action(Leaf) Nodes

    #region Detect Player Node
    
    /// <summary>
    /// 범위 거리 체크할 Distance,범위 안에 있다면 플레이어의 위치 정보를 가져올 Transform
    /// 이 쪽에서 플레이어가 존재시에 Transform에 플레이어 정보를 저장한다.
    /// 만약 Transform != null 이라면 할당하지 않고, null 일때에만 할당한다.
    /// 플레이어가 감지 범위 밖으로 나간다면 Transform을 null으로 만들어준다.
    /// 이런식으로 최대한 Physics2D.OverlapSphere 를 적게 사용해야 불필요한 연산을 줄일 수 있다.
    /// </summary>

    // 범위안에 적이 있는가? 
    private INode.E_NodeState CheckDetectPlayer()
    {
        var overlapColliders =
            Physics.OverlapSphere(transform.position, _detectDistance, _PLAYER_LAYER_MASK);
        
        // 추후 범위 안에 있으면 한번만 할당하게 변경
        if (overlapColliders != null & overlapColliders.Length> 0)
        {
            // FOV (Field Of View)
            // 범위 안에 플레이어가 있다면, 그 플레이어가 FOV 범위 안에 있는지 확인한다.
            // FOV 범위 안에 있다면, 플레이어한테 Ray를 쏜다.
            // 레이 사이에 장애물이 감지되지 않고 플레이어가 맞는다면, 추적을 한다.
            // FOV 범위 밖에 있다면, 감지가 되지 않는다.

            Transform undefinedPlayer = overlapColliders[0].transform;
            Vector3 directionToPlayer = (undefinedPlayer.position - transform.position).normalized;
            
            /*===========================================================================================*/
            
            if (Vector3.Dot(transform.forward, directionToPlayer) > 0.9f)
            {
                float distanceToTarget = Vector3.Distance(undefinedPlayer.position ,transform.position);

                if (!Physics.Raycast(transform.position, directionToPlayer, distanceToTarget, _ENEMY_LAYER_MASK))
                {
                    // 순찰 노드 최초확인 bool값 초기화.
                    _patrolRandomPosCheck = true;
                    _idleWaitCheck = true;
                    
                    _detectedPlayer = undefinedPlayer;
                }
            }
            
            /*===========================================================================================*/

            return INode.E_NodeState.ENS_Success;
        }

        // 추후 범위 밖으로 나가면 한번만 비우게 변경
        _detectedPlayer = null;
        
        return INode.E_NodeState.ENS_Failure;
    }

    #endregion

    #region Correct Destination Check & Patrol/Idle Node
    
    /*================================================================================================*/

    // 목적지가 있는가? (_correctPos의 좌표에 랜덤값 할당 & 목적지 셋팅)
    private INode.E_NodeState RandomPositionAssignment()
    {
        // 랜덤 목적지 배정
        // ##이전 랜덤 포지션에서 너무 가까운 거리는 다시 찍지 못하도록 리팩토링 필요.##
        if (_detectedPlayer == null & _patrolRandomPosCheck == true)
        {
            _correctPos.x = Random.Range(_patrolMinPos.x, _patrolMaxPos.x);
            _correctPos.z = Random.Range(_patrolMinPos.y, _patrolMaxPos.y);

            NavMeshAgentPatrolSetting();
        }
   
        return INode.E_NodeState.ENS_Failure;
    }
    
    /*================================================================================================*/
    
    // 목적지 까지의 경로가 유효한가?
    private INode.E_NodeState CorrectPathCheck()
    {
        // 경로가 유요하지 않거나 초기화되지 않은지 체크
        if (_agent.pathStatus == NavMeshPathStatus.PathInvalid)
        {
            DebugLogger.LogError("Agent Path is Invalid");
            
            _patrolRandomPosCheck = true;
            return INode.E_NodeState.ENS_Running;
        }
        
        return INode.E_NodeState.ENS_Failure;
    }
    
    /*================================================================================================*/
    
    // 목적지 도착했는가? ( 남은거리 체크 -> agent.remainingDistance < _agentCorrectionDistance)
    private INode.E_NodeState CheckArrivalAtDestination()
    {
        if (_agent.pathPending)
        {
            return INode.E_NodeState.ENS_Running;
        }
        
        if (_agent.remainingDistance < _agent.stoppingDistance)
        {
            return INode.E_NodeState.ENS_Success;
        }
        
        return INode.E_NodeState.ENS_Failure;
    }
    
    /*================================================================================================*/
    
    // 대기시간이 남아있는가? ( 대기시간 체크 -> Coroutine or Time.time ) Running , End -> _patrolRandomPosCheck = true;
    private INode.E_NodeState IdleWaitTimeCheck()
    {
        // 최초 한번 시간 할당.
        if (_idleWaitCheck == true)
        {
            IsAnimationIdleCheck();
            _idleDurationTime = Random.Range(3f, 5f);
            _idleStartTime = Time.time;
            _idleWaitCheck = false;
        }

        if (Time.time - _idleStartTime > _idleDurationTime)
        {
            _patrolRandomPosCheck = true;
            _idleWaitCheck = true;

            return INode.E_NodeState.ENS_Failure;
        }

        return INode.E_NodeState.ENS_Running;
    }
    
    /*================================================================================================*/

    #endregion

    #region Attack Check/Excute Node
    
    /*================================================================================================*/
    
    // 공격중인가?
    private INode.E_NodeState CheckAttacking()
    {
        if (IsAnimationRunning(_ATTACK_ANIM_STATE_NAME))
        {
            _agent.speed = 0;
            return INode.E_NodeState.ENS_Running;
        }

        return INode.E_NodeState.ENS_Success;
    }
    
    /*================================================================================================*/
    
    // 공격범위 안에 적이 있는가?
    private INode.E_NodeState CheckPlayerWithineAttackDistance()
    {
        if (_detectedPlayer != null)
        {
            if (Vector3.SqrMagnitude(_detectedPlayer.position - transform.position) <
                (_attackDistance * _attackDistance))
            {
                NavMeshAgentAttackSetting();
                return INode.E_NodeState.ENS_Success;
            }
        }
        
        return INode.E_NodeState.ENS_Failure;
    }
    
    /*================================================================================================*/
    
    // 공격 실행
    private INode.E_NodeState DoAttack()
    {
        if (_detectedPlayer != null)
        {
            IsAnimationAttackCheck();
            return INode.E_NodeState.ENS_Success;
        }

        return INode.E_NodeState.ENS_Failure;
    }
    
    /*================================================================================================*/

    #endregion

    #region Tracking Node
    
    /*================================================================================================*/
    
    // 적 발견시 Agent의 목적지를 Player로 할당.    
    // 추적 로직
    private INode.E_NodeState DoTracking()
    {
        if (_detectedPlayer != null)
        {
            NavMeshAgentTrackingSetting();
            _agent.SetDestination(_detectedPlayer.position);
        }

        return INode.E_NodeState.ENS_Failure;
    }
    
    /*================================================================================================*/

    #endregion


    # endregion



    #region Action Inside Logics

    # region Animations Logic

    private bool IsAnimationRunning(string animationName)
    {
        if (_animator != null)
        {
            if (_animator.GetCurrentAnimatorStateInfo(0).IsName((animationName)))
            {
                var normalizedTime = _animator.GetCurrentAnimatorStateInfo(0).normalizedTime;

                return normalizedTime != 0 && normalizedTime < 1f;
            }
        }
        
        return false;
    }
    
    private void IsAnimationWalkCheck()
    {
        if (!_animator.GetBool(_WALK_ANIM_BOOL_NAME) || _animator.GetBool(_RUN_ANIM_BOOL_NAME))
        {
            _animator.SetBool(_ATTACK_ANIM_BOOL_NAME, false);
            _animator.SetBool(_RUN_ANIM_BOOL_NAME, false);
            _animator.SetBool(_WALK_ANIM_BOOL_NAME, true);
        }
    }

    private void IsAnimationIdleCheck()
    {
        if (_animator.GetBool(_WALK_ANIM_BOOL_NAME) || _animator.GetBool(_RUN_ANIM_BOOL_NAME))
        {
            _animator.SetBool(_RUN_ANIM_BOOL_NAME, false);
            _animator.SetBool(_WALK_ANIM_BOOL_NAME, false);
        }
    }

    private void IsAnimationRunCheck()
    {
        if (!_animator.GetBool(_RUN_ANIM_BOOL_NAME))
        {
            _animator.SetBool(_RUN_ANIM_BOOL_NAME, true);
        }

        if (_animator.GetBool(_ATTACK_ANIM_BOOL_NAME))
        {
            _animator.SetBool(_ATTACK_ANIM_BOOL_NAME, false);
        }
    }

    private void IsAnimationAttackCheck()
    {
        if (!_animator.GetBool(_ATTACK_ANIM_BOOL_NAME))
        {
            _animator.SetBool(_ATTACK_ANIM_BOOL_NAME, true);
        }
    }

    # endregion

    # region NavMeshAgent Setting

    private void NavMeshAgentAttackSetting()
    {
        IsAnimationAttackCheck();
        
        _animator.applyRootMotion = false;
        _agent.isStopped = true;
        _agent.updatePosition = false;
        _agent.updateRotation = false;
        _agent.velocity = Vector3.zero;
    }

    private void NavMeshAgentTrackingSetting()
    {
        IsAnimationRunCheck();

        _animator.applyRootMotion = false;
        _agent.speed = _agentTrackingSpeed;
        _agent.isStopped = false;
        _agent.updatePosition = true;
        _agent.updateRotation = true;
    }

    private void NavMeshAgentPatrolSetting()
    {
        IsAnimationWalkCheck();
        
        _animator.applyRootMotion = true;
        _agent.speed = _agentSpeed;
        _agent.SetDestination(_correctPos);
        _patrolRandomPosCheck = false;
        
        _agent.isStopped = false;
        _agent.updatePosition = true;
        _agent.updateRotation = true;
    }

    # endregion



    #endregion
    
    
    
    #region Initializer

    private void InitializeData()
    {
        //_enemyData = DataContext.CreatDataContext(this.gameObject);
        _btRunner = new BehaviorTreeRunner(SettingBT());
        _agent = GetComponent<NavMeshAgent>();
        _animator = GetComponent<Animator>();
        _idleDurationTime = Random.Range(1f, 3f);
        _idleWaitCheck = true;
    }

    private void InitializeAgent()
    {
        _agent.stoppingDistance = _agentStoppingDistance;   // 정지 거리
        _agent.speed = _agentSpeed; // 이동 속도
        _agent.destination = _correctPos; // 목적지
        _agent.updateRotation = _agentUpdateRotation; // 회전 유무
        _agent.acceleration = _agentacceleartion; // 가속도
        _agent.angularSpeed = _agentAngularSpeed; // 회전 속도
    }

    #endregion
    
/*
    private void OnDrawGizmos()
    {
        private Vector3 leftViewAngle;
        private Vector3 rightViewAngle;

        private float myVecMag;
        private float youVecMag;
    
        Gizmos.color = Color.green;
        Gizmos.DrawWireSphere(this.transform.position, _detectDistance);
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(this.transform.position, _attackDistance);

        Gizmos.color = Color.blue;
        leftViewAngle = new Vector3
            (Mathf.Sin(-_detectViewAngle * 0.5f * Mathf.Deg2Rad), 0, Mathf.Cos(-_detectViewAngle * 0.5f * Mathf.Deg2Rad));
        rightViewAngle = new Vector3
            (Mathf.Sin(_detectViewAngle * 0.5f * Mathf.Deg2Rad), 0, Mathf.Cos(_detectViewAngle * 0.5f * Mathf.Deg2Rad));
        Gizmos.DrawLine(transform.position, transform.position + leftViewAngle * 10f);
        Gizmos.DrawLine(transform.position, transform.position + rightViewAngle * 10f);
        
        Gizmos.color = Color.black;
        float vie = Mathf.Acos(0.9f);
        Vector3 leftView = new Vector3
            (Mathf.Cos(-vie * 0.5f), 0, Mathf.Sin(-vie * 0.5f));
        Vector3 rightView = new Vector3
            (Mathf.Cos(vie * 0.5f), 0, Mathf.Sin(vie * 0.5f));
        Gizmos.DrawLine(transform.position, transform.position + leftView * 10f);
        Gizmos.DrawLine(transform.position, transform.position + rightView * 10f);

    }
    */
}





➔ Editor

FieldOfViewEditor

[CustomEditor (typeof(EnemyBasicBT))]
public class FieldOfViewEditor : Editor
{
    private void OnSceneGUI()
    {
        #if DEBUG_MODE
        EnemyBasicBT enemyFOV = (EnemyBasicBT)target;
        Vector3 enemyForward = enemyFOV.transform.forward;
        
        Handles.color = Color.black;
        Handles.DrawWireArc // Radius = _detectDistance 인 원을 그린다.
            (enemyFOV.transform.position, Vector3.up, Vector3.forward, 360, enemyFOV._detectDistance);

        float vie = Mathf.Acos(0.9f) * Mathf.Rad2Deg;
        Vector3 leftViewDirection = Quaternion.Euler(0f, -vie, 0) * enemyForward;
        Vector3 rightViewDirection = Quaternion.Euler(0f, vie, 0f) * enemyForward;
        Handles.DrawLine    // 좀비 정면으로부터 내적 0.9에 해당하는 선
            (enemyFOV.transform.position, enemyFOV.transform.position + leftViewDirection * enemyFOV._detectDistance);
        Handles.DrawLine
            (enemyFOV.transform.position, enemyFOV.transform.position + rightViewDirection * enemyFOV._detectDistance);
        
        if (enemyFOV._detectedPlayer != null)
        {
            Handles.color = Color.red;
            Handles.DrawLine(enemyFOV.transform.position, enemyFOV._detectedPlayer.position);
        }
        #endif
    }
    
}
profile
No Easy Day

0개의 댓글