728x90

이 글은 인프런 [따라하면서 배우는 고박사의 유니티 기초] 강의를 수강하고 정리한 글입니다. 

 

[지금 무료] 따라하면서 배우는 고박사의 유니티 기초 | 고박사 - 인프런

고박사 | 유니티로 게임을 개발하고 싶은 초보자를 대상으로 하며, 유니티 설치부터 2D/3D 게임 개발에 필요한 기초 지식까지 자세하게 설명합니다. (강의에 사용되는 모든 리소스는 영상 상단의

www.inflearn.com


Off Mesh Link 개요

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

 

Off Mesh Link는 자동 설정과 수동 설정이 있다.

자동 Off Mesh Link (절벽, 낭떠러지)

장점:

  • 게임월드에 배치된 많은 오브젝트의 Off Mesh Link를 한꺼번에 설정할 수 있다.

단점:

  • 낙하 높이와 점프 거리를 하나만 설정할 수 있기 때문에 다양한 지형을 세세하게 설정하는 것이 불가능하다.
  • 또한 위로 올라가는 off mesh link 설정이 불가능하다.

수동 Off Mesh Link (사다리)

장점:

  • 지형에 따라 세세한 설정이 가능하다.
  • 사다기/암벽과 같이 위로 올라가는 off mesh link 설정이 가능하다.

단점:

  • off mesh link로 연결이 필요한 모든 부분을 직접 설정해야 한다.
 

자동 Off Mesh Link 생성

  1. 자동 Off Mesh Link를 설정할 오브젝트 선택
  2. Navigation View > Object tab > Generate OffMeshLinks 체크
  3. 낙하 높이, 점프 거리 설정
  4. Bake 버튼을 눌러 Navigation Mesh 데이터 생성
1번, 2번
3번, 4번
 

Navigation Mesh 데이터 생성 후에 높이가 4 이하인 곳에 절벽을 뛰어내릴 수 있도록 Off Mesh Link가 체크되어 있는 오브젝트들만 절벽 뛰어 넘거나 떨어질 수 있다.

 

수동 Off Mesh Link 생성

  1. Off MEsh Link로 연결되는 두 지점으로 사용할 게임 오브젝트 생성 (※ 게임 오브젝트 위치가 Navigation Mesh의 이동 경로 내에 포함되어야 정상적인 이동 가능)
  2. 두 지점 오브젝트를 가지고 있는 게임 오브젝트에 Off Mesh Link 컴포넌트 추가
  3. Off Mesh Link 컴포넌트의 start와 en 변수에 1에서 생성한 게임 오브젝트 등록

 

생성한 두 오브젝트를 LadderObject의 자식으로 설정하고, transform을 초기화한 다음, scale를 0.4로 설정한다.

 

두 오브젝트는 transform외에 다른 컴포넌트들은 끈다.

 

사다리를 구성하고 있는 LadderObject의 컴포넌트에 Off Mesh Link 컴포넌트를 추가하고, 생성한 Start와 End 의 게임오브젝트를 등록한다. 실행하면 사다리를 통해 이동이 가능한 것을 확인할 수 있다. 


구역 설정

 구역을 설정하는 것은 게임 월드에 다양한 지형 존재하는 경우에 어떤 지형을 가로질러 가는 것이 빠를지, 느릴지 명시하기 위하여 구역을 설정하게 된다. 

A에서 B로 이동한다면 이동 경로는 위의 예제와 같을 것이다. 하지만 동일한 지역에서 02구역을 water라고 지정하면 이동 비용을 water의 10배인 10으로 설정한다면 경로는 바뀌게 된다. 

구역은 Nevigation View의 Area 탭에서 설정할 수 있다.

 

user3에 climb라는 새로운 구역을 생성하고 비용을 5로 설정한다.

LadderObject의 Off Mesh Link 컴포넌트의 Navigation Area 변수를 climb로 설정한다.

 

 
 

Off Mesh Link 컴포넌트를 가지고 있지 않은 일반 오브젝트의 경우에는 게임오브젝트 선택 후 Navigation View > Object tab > Navigation Area 변수 설정으로 할 수 있다.

 

 

새로운 구역이 생기면 이동하는 오브젝트의 Area Mask의 구역이 추가되어 있는지 확인해야 하며 체크되지 않은 구역은 이동이 불가능하다.

 


Off Mesh Link 액션 설정

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

 

Off Mesh Link에 따라 이동을 다르게 수행하도록 스크립트를 작성한다.

using System.Collections;
using UnityEngine;
using UnityEngine.AI;

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의 데이터
			OffMeshLinkData linkData = navMeshAgent.currentOffMeshLinkData;
			
			// 설명 : navMeshAgent.currentOffMeshLinkData.offMeshLink가
			// true이면 수동으로 생성한 OffMeshLink
			// false이면 자동으로 생성한 OffMeshLink

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

		return false;
	}

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

		// 현재 위치에 있는 OffMeshLink의 시작/종료 위치
		OffMeshLinkData linkData = navMeshAgent.currentOffMeshLinkData;
		Vector3 start = linkData.startPos;
		Vector3 end   = linkData.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;
	}
}

사다리와 같이 오르내리는 Off Mesh Link를 처리하기 위한 스크립트를 작성한다. 

Player 오브젝트에 “OffMeshLinkClimb" 적용하고 컴포넌트 변수를 설정한다. 사다리를 올라갈 때 이전보다 느린 속도로 설정한 속도에 의해 올라가는 것을 확인할 수 있다.

절벽과 같이 뛰어넘는 Off Mesh Link 처리를 위해 OffMeshLinkJump 스크립트

using System.Collections;
using UnityEngine;
using UnityEngine.AI;

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

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

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

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

	public bool IsOnJump()
	{
		if ( navMeshAgent.isOnOffMeshLink )
		{
			// 현재 위치에 있는 OffMeshLink의 데이터
			OffMeshLinkData linkData = navMeshAgent.currentOffMeshLinkData;
			
			// 설명 OffMeshLinkType은 Manual=0, DropDown=1, JumpAcross=2로
			// 자동으로 생성한 OffMeshLink의 속성 구분을 위해 사용(1, 2)

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

		return false;
	}

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

		// 현재 위치에 있는 OffMeshLink의 시작/종료 위치
		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;
	}
}

Player 오브젝트에 OffMeshLinkJump 스크립트 적용하고, 컴포넌트 변수를 설정한다.

 

실행하면 절벽 지나갈 떄 포물선 운동으로 점프하는 것을 확인할 수 있다. 

 


이동 가능한 장애물 설정

Navigation Mesh Data는 Bake를 이용하여 미리 구워두기 때문에 게임 내에서 오브젝트 이동 오브젝트의 좌표를 기준으로 Mesh 데이터가 새로 생성되지 않는다.

이동 가능한 장애물의 경우에 NavMeshObstacle 컴포넌트를 이용하여 실시간으로 Navigation Mesh Data 관리하게 된다.

이동하는 장애물 설정을 위해 cube 오브젝트를 생성하고 다음과 같이 설정한다.

이동 경로로 사용할 빈 오브젝트 2개를 만들고 다음과 같이 설정한다.

지정된 경로를 자동으로 이동하는 “SimplePatrol” 스크립트 

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;
		}
	}
}

이동하는 장애물 오브젝트에 스크립트를 컴포넌트로 등록하고 path01, pah02를 등록한다.

Patrolcube가 머무는 자리를 이동할 수 없는 구역으로 경로에 해당되지 않는 것을 확인할 수 있다.

Nav Mesh Obstacle 컴포넌트를 추가하고 설정을 체크한다. PatroCube 오브젝트가 머무는 위치가 이동할 수 없는 경로 표현되는 것이 확인된다.

 


3인칭 카메라 제어

카메라의 이동 회전을 제어하는 스크립트 CameraController 작성한다.

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의 위치를 기준으로 distacne만큼 떨어져서 쫓아간다
		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);
	}
}
 

작성한 스크립트를 컴포넌트로 등록하고 player 오브젝트를 target으로 등록한다. 그러면 player가 움직일 때 카메라가 쫓아다니는 것을 확인할 수 있다.

마우스 오른쪽 버튼을 눌러 마우스를 움직이면 카메라가 회전하는 것도 확인할 수 있다. 휠 스크롤로 줌인 줌아웃을 할 수 있다.

+ Recent posts