[Unity] 3D Navigation Mesh(2)(구역 및 3인칭 카메라 제어)

Southbig·2023년 3월 27일
0

Off Mesh Link

  • 사다리, 암벽과 같이 수직으로 올라가거나 내려오는 길
  • 절벽 사이를 뛰어서 넘어가거나 낭떠러지 아래로 떨어지는 길과 같이 메시가 끊어져 있는 곳을 이동할 수 있게 설정하는 것

자동 설정

장점

  • 게임월드에 배치된 많은 오브젝트의 Off Mesh Link를 한꺼번에 설정 가능

단점

  • 낙하 높이(Drop Height)와 점프 거리(Jump Distance)를 하나만 설정할 수 있기 때문에 다양한 지형을 세세하게 설정하는 것이 불가능 (Off Mesh Link 데이터 소실의 위험)
  • 위로 올라가는 Off Mesh Link 설정 불가능

수동 설정

장점

  • 지현에 따라 세세한 설정이 가능
  • 사다리/암벽과 같이 위로 올라가는 Off Mesh Link 설정 가능

단점

  • Off Mesh Link로 연결이 필요한 모든 부분을 직접 설정해야 함

구역(Areas) 설정

NavMeshAgent 컴포넌트를 사용하는 이동 에이전트의 Area Mask에서 이동 가능/불가능 구역 선택

액션 설정

NavMeshAgent 컴포넌트의 Auto Traverse Off Mesh Link가 체크 해제되어 있으면 Off Mesh Link를 만나면 오브젝트가 멈추게 된다

OffMeshLinkClimb

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class OffMeshLinkClimb : MonoBehaviour
{
    [SerializeField]
    private int offMeshArea = 3; // 오프메시의 구역 (Climb)
    [SerializeField]
    private float climbSpeed = 1.5f;
    private NavMeshAgent navMeshAgent;

    private void Awake()
    {
        navMeshAgent = GetComponent<NavMeshAgent>();
    }

    IEnumerator Start()
    {
        while(true)
        {
            // IsOnClimb() 함수의 변환 값이 true일 때 까지 반복 호출
            yield return new WaitUntil(() => IsOnClimb());

            // 올라가거나 내려오는 행동
            yield return StartCoroutine(ClimbOrDescend());
        }
    }

    public bool IsOnClimb()
    { 
        // 현재 오브젝트의 위치가 OffMeshLink에 있는지 (true / false)
        if (navMeshAgent.isOnOffMeshLink)
        {
            // 현재 위치에 있는 OffMeshLink의 데이터
            OffMeshLinkDate linkDate = navMeshAgent.currentOffMeshLinkData;

            // 설명 : navMeshAgent.currentOffMeshLinkData.offMeshLink가
            // true이면 수동으로 생성한 OffMeshLink
            // false이면 자동으로 생성한 OffMeshLink

            // 현재 위치에 있는 OffMeshLink가 수동으로 생성한 OffMeshLink이고, 장소 정보가 "Climb"이면
            if (linkDate.offMeshLink != null && linkDate.offMeshLink.area == offMeshArea)
            {
                return true;
            }
        }
        return false;
    }

    private IEnumerator ClimbOrDescend()
    {
        // 네비게이션을 이용한 이동을 잠시 중지한다
        navMeshAgent.isStopped = true;

        // 현재 위치에 있는 offMeshLink의 시작 / 종료 위치
        OffMeshLinkData linkDate = navMeshAgent.currentOffMeshLinkData;
        Vector3 start = linkData.startPos;
        Vector3 end = linckData.endPos;

        // 오르내리는 시간 설정
        float climbTime = Mathf.Abs(end.y - start.y) / climbSpeed;
        float currentTime = 0.0f;
        float percent = 0.0f;

        while (percent < 1)
        {
            // 단순히 deltaTime만 더하면 무조건 1초 후에 percent가 1이 되기 때문에 climbTime 변수를 연산해서 시간을 조정한다
            currentTime += Time.deltaTime;
            percent = currentTime / climbTime;
            // 시간 경과(최대1)에 따라 오브젝트의 위치를 바꿔준다
            transform.position = Vector3.Lerp(start, end, percent);

            yield return null;
        }

        // OffMeshLink를 이용한 이동 완료
        navMeshAgent.CompleteOffMeshLink();
        // OffMeshLink 이동이 완료되었으니 네비게이션을 이용한 이동을 다시 시작한다
        navMeshAgent.isStopped = false;
    }
}

올라갈때와 내려올때를 구분해서 작성하고, 애니메이션까지 더해지면 좀 더 그럴싸한 사다리 오르기가 완성될 것이다

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class OffMeshLinkJump : MonoBehaviour
{
    [SerializeField]
    private float jumpSpeed = 10.0f; // 점프 속도
    [SerializeField]
    private float gravity = -9.81f; // 중력 계수
    private UnityEngine.AI.NavMeshAgent navMeshAgent;

    private void Awake()
    {
        navMeshAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();
    }

    IEnumerator Start()
    {
        while(true)
        {
            // IsOnJump() 함수의 반환 값이 true일 때 까지 반복 호출
            yield return new WaitUntil(() => IsOnJump()) ;

            // 점프 행동
            yield return StartCoroutine(JumpTo());
        }
    }

    public bool IsOnJump()
    {
        if(navMeshAgent.isOnOffMeshLink)
        {
            // 현재 위치에 있는 OffMeshLink의 데이터
            UnityEngine.AI.OffMeshLinkData linkData = navMeshAgent.currentOffMeshLinkData;

            // 설명 OffMeshLinkType은 Manual=0, DropDown=1, JumpAcross=2로
            // 자동으로 생성한 OffMeshLink의 속성 구분을 위해 사용(1, 2)

            // 현재 위치에 있는 OffMeshLink의 OffMeshLinkType이 JumpAcross이면
            if(linkData.linkType == UnityEngine.AI.OffMeshLinkType.LinkTypeJumpAcross || linkData.linkType == UnityEngine.AI.OffMeshLinkType.LinkTypeDropDown)
            {
                return true;
            }
        }
        return false;
    }

    IEnumerator JumpTo()
    {
        // 네비게이션을 이용한 이동을 잠시 중지한다
        navMeshAgent.isStopped = true;
        // 현재 위치에 있는 OffmeshLink의 시작/종료 위치
        UnityEngine.AI.OffMeshLinkData linkData = navMeshAgent.currentOffMeshLinkData;
        Vector3 start = transform.position;
        Vector3 end = linkData.endPos;

        // 뛰어서 이동하는 시간 설정

        float jumpTime = Mathf.Max(0.3f, Vector3.Distance(start, end) / jumpSpeed);
        float currentTime = 0.0f;
        float percent = 0.0f;
        // y 방향의 초기 속도
        float v0 = (end - start).y - gravity;

        while (percent < 1)
        {
            // 단순히 deltaTime만 더하면 무조건 1초 후에 percent가 1이 되기 때문에 jumpTime 변수를 연산해서 시간을 조절한다
            currentTime += Time.deltaTime;
            percent = currentTime/jumpTime;
            // 시간 경과(최대1)에 따라 오브젝트의 우치(x, z)를 바꿔준다
            Vector3 position = Vector3.Lerp(start, end, percent);
            // 시간 경과에 따라 오브젝트의 위치(Y)를 바꿔준다
            // 포물선 운동 : 시작위치 + 초기속도 * 시간 + 중력 * 시간제곱
            position.y = start.y + (v0 * percent) + (gravity * percent * percent);
            //위에서 계산한 x, y, z 위치 값을 실제 오브젝트에 대입
            transform.position = position;

            yield return null;
        }

        // OffMeshLink를 이용한 이동 완료
        navMeshAgent.CompleteOffMeshLink();
        // OffMeshLink 이동이 완료되었으니 네비게이션을 이용한 이동을 다시 시작한다
        navMeshAgent.isStopped = false;
    }
}

이동 가능한 장애물 설정

Nav Mesh Obstacle은 장애물의 모양, 중심 점과 크기, 공관데이터를 갱신하는 조건들을 설정할 수 있다

SimplePatrol

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SimplePatrol : MonoBehaviour
{
    [SerializeField]
    private Transform[] paths; // 순찰 경로
    private int currentPath = 0; // 헌재 목표지점 인덱스
    private float moveSpeed = 3.0f; // 이동 속도

    private void Update()
    {
        // 이동방향 설정 : (목표위치-내위치).정규화
        Vector3 direction = (paths[currentPath].position - transform.position).normalized;
        // 오브젝트 이동
        transform.position += direction * moveSpeed *Time.deltaTime;

        // 목표위치에 거의 도달했을 때
        if ((paths[currentPath].position - transform.position).sqrMagnitude < 0.1f)
        {
            // 목표 위치 변경 (순찰 경로 순환)
            if(currentPath < paths.Length - 1) currentPath ++;
            else currentPath = 0;
        }
    }
}

Move Threshold의 값을 낮게 설정할수록 정밀한 위치 계산이 되지만 게임이 무거워질 수 있기 때문에 적당한 거리를 설정해야 한다

Nav Mesh Obstacle -> Carve (선택) -> Carve Only Stationary (체크 해제)
오브젝트가 충돌할 수 있는 범위를 보여준다

3인칭 카메라 제어

Cameracontroller

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Cameracontroller : MonoBehaviour
{
    [SerializeField]
    private Transform target; // 카메라가 추적하는 대상
    [SerializeField]
    private float minDistance = 3; // 카메라와 target의 최소 거리
    [SerializeField]
    private float maxDistance = 30; // 카메라와 target의 최대 거리
    [SerializeField]
    private float wheelSpeed = 500; // 마우스 휠 스크롤 속도
    [SerializeField]
    private float xMoveSpeed = 500; // 카메라의 y축 회전 속도
    [SerializeField]
    private float yMoveSpeed = 250; // 카메라의 x축 회전 속도
    private float yMinLimit = 5; // 카메라의 x축 회전 제한 최소 값
    private float yMaxLimit = 80; // 카메라의 x축 회전 제한 최대 값
    private float x, y; // 마우스 이동 방향 값
    private float distance; // 카메라와 target의 거리

    private void Awake()
    {
        // 최초 설정된 target과 카메라의 위치를 바탕으로 distance 값 초기화
        distance = Vector3.Distance(transform.position, target.position);
        // 최초 카메라의 회전 값을 x, y 변수에 저장
        Vector3 angles = transform.eulerAngles;
        x = angles.y;
        y = angles.x;
    }

    private void Update()
    {
        if (target == null) return; // target이 존재하지 않으면 실행 하지 않는다

        // 오른쪽 마우스를 누르고 있을 때
        if (Input.GetMouseButton(1))
        {
            // 마우스를 x, y축 움직임 방향 정보
            x += Input.GetAxis("Mouse X") * xMoveSpeed * Time.deltaTime;
            y -= Input.GetAxis("Mouse Y") * yMoveSpeed * Time.deltaTime;
            // 오브젝트의 위/아래(x축) 한계 범위 설정
            y = ClampAngle(y, yMinLimit, yMaxLimit);
            // 카메라의 회전(Rotation) 정보 갱신
            transform.rotation = Quaternion.Euler(y, x, 0);
        }

        // 마우스 휠 스크롤을 이용해 target과 카메라의 거리 값(distance) 조절
        distance =+ Input.GetAxis("Mouse ScrollWheel") * wheelSpeed * Time.deltaTime;
        // 거리는 최소, 최대 거리를 설ㅈ어해서 그 값을 벗어나지 않도록 한다
        distance = Mathf.Clamp(distance, minDistance, maxDistance);
    }

    private void LateUpdate()
    {
        if (target == null) return; // target이 존재하지 않으면 실행 하지 않는다
        // 카메라의 위치(Position) 정보 갱싱
        // target의 위치를 기준으로 distance만큼 떨어져서 쫒아간다
        transform.position = transform.rotation * new Vector3(0, 0, -distance) + target.position;
    }

    private float ClampAngle(float angle, float min, float max)
    {
        if (angle < -360) angle += 360;
        if (angle > 360) angle -= 360;

        return Mathf.Clamp(angle, min, max);
    }
}

profile
즐겁게 살자

0개의 댓글