본문 바로가기
개발/Unity

Monster Pattern 0508 작업

by 남생이야 2024. 5. 8.

작업 내역 

 플레이어 카메라 

 몬스터 추격 반경 내에 플레이어 진입시
 - 1)  플레이어 추격을 개시
 - 2) 추격 반경을 벗어날 경우 일정 시간 대기
 - 다시 순찰 상태로


체크 사항 

플레이어 카메라 조종 

  3인칭 시점을 가지는 게임을 개발하기 위해 카메라를 조정하는 작업을 진행하였다. 생각보다 카메라 무빙에 대해 자유롭지 못했다. 

 이유는 카메라 무빙 워크를 위한 벡터 관련한 수학적 공식이었다. 처음에 수학적 공식으로만 이용해서 오브젝트들의 상속 관계라던지에 대한 개념없이 순수하게 접근하려했다. 

 

https://youtu.be/UeMNGPYeqxg

 

결과는 생각보다 처참했다. 카메라도 회전 캐릭터를 회전시키기 때문에 서로 맞물려 동작해버리는 문제가 있었다. 

 

해결방안 

 카메라 오브젝트를 빈 오브젝트에 하위 객체로 심어두고 부모 오브젝트로 부터 상대 좌표로 일정 거리를 두게 하고 부모 오브젝트를 플레이어 캐릭터 특정 지점 오브젝트와 Position 값을 일치시키는 것으로 해결했다. 

 

몬스터 순찰

  몬스터에게 적을 탐지하고 추적하는 기능을 부여하고 일정 범위를 벗어나면 다시 대기 후, 정찰하게 하는 AI 기능을 추가 했다. 기존에 행동트리(BT)에 일점 범위 탐색, 추적하는 노드를 정의하고 추가했다. 

 

https://youtu.be/C3omOcp99Xk

 

 

 

 

 

더보기

실패한 카메라 무빙 코드 .. 

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

public class Test_Camera : MonoBehaviour
{
    public float clampAngle = 70f;
    [SerializeField]
    private GameObject target;

    [SerializeField]
    private float speed = 5.0f;
    //[SerializeField]
    //private float followSpeed = 10.0f;
    [SerializeField]
    private float desireDistance = 10.0f;
    
    [SerializeField]
    private float smoothness = 1.0f;

    private Vector3 offset;

    private Quaternion initialRootRotation;

    private void Start()
    {
        initialRootRotation = target.transform.root.rotation;
    }

    private void Update()
    {
        if (target == null)
            return;

        float rotY = Input.GetAxis("Mouse X") * speed* Time.deltaTime;
        float rotX = Input.GetAxis("Mouse Y")* speed * Time.deltaTime;

        //transform.RotateAround(target.transform.position, 
        //    Vector3.up, rotY );

        rotX = Mathf.Clamp(rotX, -clampAngle, clampAngle);
        //transform.RotateAround(target.transform.position,
        //    Vector3.right, rotX * speed * Time.deltaTime);

        Quaternion rot = Quaternion.Euler(rotX, rotY, 0);
        transform.rotation = rot;
        Quaternion q = transform.rotation;
        Vector3 e = q.eulerAngles;
        e.y = 0.0f;
        transform.eulerAngles = e;

        Vector3 targetTransform = target.transform.position - transform.position;
        transform.rotation = Quaternion.LookRotation(targetTransform.normalized);

     
    }

    private void LateUpdate()
    {
        Vector3 targetTransform = target.transform.position - transform.position;

        float distance = Vector3.Distance(transform.position, target.transform.position);
        float offset = distance - desireDistance;
        if(offset > 0 || offset < 0)
        {
            Vector3 finalVec = targetTransform.normalized * offset * smoothness * 
                Time.deltaTime;
            transform.Translate(new Vector3(finalVec.x, 0, finalVec.z));
        }

        RotateTarget();
    }


    private void RotateTarget()
    {
        if (target == null)
            return;

        //target.transform.root.Rotate(new Vector3(0, transform.rotation.y, 0));
        //target.transform.root.rotation = initRootRotation * Quaternion.Euler(0,
        //    transform.rotation.eulerAngles.y, 0);

        // 카메라가 회전할 때 루트 오브젝트도 회전합니다.
        float cameraRotationY = transform.rotation.eulerAngles.y;
        float rootRotationY = initialRootRotation.eulerAngles.y;

        // 카메라의 회전값이 180도를 초과하면 루트 오브젝트의 회전값을 보정합니다.
        if (cameraRotationY - rootRotationY > 180f)
        {
            rootRotationY += 360f;
        }
        else if (cameraRotationY - rootRotationY < -180f)
        {
            rootRotationY -= 360f;
        }

        target.transform.root.rotation = initialRootRotation * Quaternion.Euler(0, cameraRotationY - rootRotationY, 0);
    }

    private void OnGUI()
    {
        GUI.color = Color.yellow;
        GUILayout.BeginHorizontal();
        GUILayout.EndHorizontal();
        GUILayout.Label(Input.GetAxis("Mouse Y").ToString());
    }
}

 

EnemyBT.cs

더보기
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.XR;

public class EnemyBT : MonoBehaviour, IDamagable
{
    private Animator animator;
    private StatusComponent status;

    private BTRunner btRunner;

    public bool bWalk = false;
    public bool bAttack = false;
    public bool bChase = false;

    private void Awake()
    {
        animator = GetComponent<Animator>();
        status = GetComponent<StatusComponent>();
        btRunner = new BTRunner(SettingBT());
    }


    private void Update()
    {
        btRunner?.Operate();
    }

    #region BT Seting 

    BTNode SettingBT() => new SelectorNode
            (
                new List<BTNode>
                {
                    new ActionNode(CheckDead),
                    new SequenceNode // 탐지 및 추적 
                    (
                        new List<BTNode>
                        {
                            new ActionNode(CheckDetectEnemy),
                            new ActionNode (ChaseToTarget),
                        }

                    ),
                    new SelectorNode // 대기 및 순찰
                    (
                        new List<BTNode>
                        {
                            new ActionNode(DoWaiting),
                            new ActionNode(DoPatrol),
                        }
                    ),

                 
                }
            );

    #endregion

    #region Idle
    [SerializeField]
    private float waitTime = 3.0f;
    public float currentWaitTime;

    private BTNode.NodeState DoWaiting()
    {
        if (bWalk == true)
            return BTNode.NodeState.FAILURE;

        if (currentWaitTime > 0)
        {
            currentWaitTime -= Time.deltaTime;
            bWalk = false;
            animator.SetBool("IsWalking", bWalk);

            return BTNode.NodeState.RUNNING;
        }

        currentWaitTime = waitTime;

        return BTNode.NodeState.FAILURE;
    }

    #endregion

    #region Patrol
    [SerializeField]
    private float patrolDistance; // 탐지 거리 
    private float minDistance = 3.0f; // 최소 탐지 거리 
    private Vector3 destination;    // 도착지점
    private bool bArrive = true;
    public bool Arrive
    {
        get
        {
            return bArrive;
        }
    }

    // 일정 범위 랜덤 위치 구하기
    private Vector3 GetRandomPosByAround()
    {
        float x = Random.Range(-patrolDistance, patrolDistance) * minDistance;
        float z = Random.Range(-patrolDistance, patrolDistance) * minDistance;

        // 지정한 곳이 이동할 수 있는 구역인지 검사
        Ray ray = new Ray();
        ray.origin = transform.position + new Vector3(x, 1.5f, z);
        ray.direction = Vector3.down;

        RaycastHit hit;
        if (Physics.Raycast(ray, out hit,
            Mathf.Infinity, LayerMask.GetMask("Ground")))
        {
            return new Vector3(x, 0, z);
        }

        return Vector3.zero;
    }

    private void MoveToDest(Vector3 destination)
    {
        Vector3 direction = destination - transform.position;


        direction = direction.normalized * status.Speed;

        // 움직이게함 
        transform.Translate(direction * Time.deltaTime, Space.World);

        // 지정한 방향으로 돌린다.
        Quaternion targetRatation = Quaternion.LookRotation(direction, Vector3.up);
        transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRatation, 3.0f);

        animator.SetBool("IsWalking", bWalk);
    }

    private BTNode.NodeState DoPatrol()
    {
        if (Vector3.Distance(transform.position, destination) <= 0.001f)
            bArrive = true;

        if (bArrive == true)
        {
            destination = transform.position + GetRandomPosByAround();

            Debug.DrawLine(transform.position, this.destination,
              Color.green, waitTime + 2f);

            bArrive = false;
            bWalk = false;
            animator.SetBool("IsWalking", bWalk);
            return BTNode.NodeState.SUCCESS;
        }

        bWalk = true;

        MoveToDest(destination);
        return BTNode.NodeState.RUNNING;
    }


    #endregion

    #region Detect
    [SerializeField]
    private float detectAngle;

    [SerializeField]
    private float detectDistance;

    [SerializeField]
    private float enemyToMinDistance; // 적과 최소거리 

    [SerializeField]
    private GameObject target;

    Vector3 leftBoundary;
    Vector3 rightBoundary;

    // 시야각을 구하는 함수 
    private Vector3 BoundaryAngle(float angle)
    {
        float rad = (angle + transform.eulerAngles.y) * Mathf.Deg2Rad;
        return new Vector3(Mathf.Sin(rad), 0, Mathf.Cos(rad));
    }

    // 적이 시야각 내부에 들어 왔는지 검사 
    private bool CheckInTheBoundaryWithEenmy(Transform tr)
    {
        if (tr == null)
            return false;

        // 타겟의 방향
        Vector3 targetDir = (tr.position - transform.position).normalized;
        float dot = Vector3.Dot(transform.forward, targetDir);

        // 내적 각 계산 
        float theta = Mathf.Acos(dot) * Mathf.Rad2Deg; // 계산결과는 rad이므로 deg로 변환

        if (theta <= detectAngle)
            return true;

        return false;
    }

    // 적을 발견했는지 검사 
    BTNode.NodeState CheckDetectEnemy()
    {
        var overlapColliders = Physics.OverlapSphere(transform.position,
            detectDistance, LayerMask.GetMask("Player"));

        leftBoundary = BoundaryAngle(detectAngle * -1 * 0.5f);
        rightBoundary = BoundaryAngle(detectAngle * 0.5f);
        
        Debug.DrawRay(transform.position, leftBoundary * detectDistance, Color.red);
        Debug.DrawRay(transform.position, rightBoundary * detectDistance, Color.red);

        foreach (Collider collider in overlapColliders)
        {
            if (CheckInTheBoundaryWithEenmy(collider.transform) == true)
            {
                Debug.Log($"Find Enemy!");
                target = collider.gameObject;
                return BTNode.NodeState.SUCCESS;
            }
        }

        target = null;
        return BTNode.NodeState.FAILURE;
    }

    // 추적하기 
    BTNode.NodeState ChaseToTarget()
    {
        if (target == null)
        {
            return BTNode.NodeState.FAILURE;
        }

        // 연산을 빠르게 처리하기 위해 제곱된 값을 구한다.
        float distance = Vector3.SqrMagnitude(
            target.transform.position - transform.position);

        // 적과의 거리가 멀 경우 
        if (detectDistance * detectDistance < distance)
        {
            // 추격 중지 
            destination = transform.position;
            // TODO: 아래 코드는 몬가 많이 써서 모듈화가 필요할 지도 모르겠다.. 
            bWalk = false;
            animator.SetBool("IsWalking", bWalk);
            return BTNode.NodeState.FAILURE;
        }
        // 추격 거리 내에 있지만 최소 거리보다 큰 경우 계속 추격 
        else if (detectDistance * detectDistance >= distance &&
            enemyToMinDistance * enemyToMinDistance < distance)
        {
            // 움직임
            destination = target.transform.position;
            bWalk = true;
            animator.SetBool("IsWalking", bWalk);

            Debug.DrawLine(transform.position, destination, Color.blue);
            MoveToDest(destination);

            return BTNode.NodeState.RUNNING;
        }

        return BTNode.NodeState.SUCCESS;
    }

    #endregion

    #region Attack
    #endregion

    #region
    private BTNode.NodeState CheckDead()
    {
        if (status.Dead == true)
            return BTNode.NodeState.SUCCESS;

        return BTNode.NodeState.FAILURE;
    }

    #endregion


    public void Damage(GameObject attacker, Sword causer, float power)
    {
        status.Damage(power);

        if (status.Dead == false)
        {
            animator.SetTrigger("Damaged");

            return;
        }

        animator.SetTrigger("Dead");

        Collider collider = GetComponent<Collider>();
        collider.enabled = false;

        Destroy(this, 5.0f);
    }

    private void OnValidate()
    {
        //leftBoundary = BoundaryAngle(detectAngle * -1 * 0.5f);
        //rightBoundary = BoundaryAngle(detectAngle * 0.5f);
        //Debug.Log($"left : {leftBoundary} right : {rightBoundary}");
        //Debug.DrawRay(transform.position, leftBoundary * detectAngle, Color.red);
        //Debug.DrawRay(transform.position, rightBoundary * detectAngle, Color.red);
    }

    private void OnDrawGizmos()
    {
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.position, detectDistance);

    }
}

'개발 > Unity' 카테고리의 다른 글

0510 Mosnter Pattern - 적 ->플레이어 공격 및 게임 종료  (0) 2024.05.10
0509 Monster Pattern  (0) 2024.05.09
몬스터 패턴 작업  (0) 2024.05.07
[Unity] 벡터의 내적과 외적  (0) 2024.04.30
프리팹(Prefab)  (0) 2024.04.27