Navigation Mesh 데이터 생성 후에 높이가 4 이하인 곳에 절벽을 뛰어내릴 수 있도록 Off Mesh Link가 체크되어 있는 오브젝트들만 절벽 뛰어 넘거나 떨어질 수 있다.
수동 Off Mesh Link 생성
Off MEsh Link로 연결되는 두 지점으로 사용할 게임 오브젝트 생성 (※ 게임 오브젝트 위치가 Navigation Mesh의 이동 경로 내에 포함되어야 정상적인 이동 가능)
두 지점 오브젝트를 가지고 있는 게임 오브젝트에 Off Mesh Link 컴포넌트 추가
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가 움직일 때 카메라가 쫓아다니는 것을 확인할 수 있다.
마우스 오른쪽 버튼을 눌러 마우스를 움직이면 카메라가 회전하는 것도 확인할 수 있다. 휠 스크롤로 줌인 줌아웃을 할 수 있다.
현재 2022.3..22f1 버전에서 Navigation View는 보이지 않기 때문에 강의와는 다르게 다운을 받아야 한다. Window > Package Manager > Unity Registry > AI Navigation 들어가서 AI Navigation 을 설치한다.
Navigation이 잘 보인다.
Agents
네비게이션 메시 정보를 바탕으로 움직이는 이전트에 대한 설정 (NavMeshAgent 컴포넌트)
Agent Type
에이전트 속성으로 “+”로 새로운 에이전트 속성을 추가할 수 있다.
Agnet 정보
Name : Agent Type에 보여지는 이름
Radius : 에이전트의 반지름
Height : 에이전트의 높이(키)
Step Height : 오르내릴 수 있는 계단의 높이
Max Slope : 올라갈 수 있는 경사 각도
Areas
네비게이션 메시로 사용되는 오브젝트들의 구역 설정
Name
구역 이름으로 기본으로 Walkable(이동 가능), Not Walkable(이동 불가능), Jump(뛰기)가 제공되고, User 3부터 원하는 구역을 추가로 설정할 수 있다.
Cost
구역과 함께 등록. 이동하는데 소요되는 비용 (1 이상) 경로를 탐색할 때 Cost 정보를 기준으로 최단거리를 찾게 된다.
Cost가 2인 Jump는 Cost가 1인 Walkable을 지나갈 때보다 2배의 시간이 걸린다는 뜻으로 사용된다. (실제 오브젝트의 물리적인 이동 속도가 느려 지는 것이 아닌 경로를 계산할 때 활용된다)
Bake
네비게이션 메시 데이터를 생성
Baked Agent Size
Agent Radius : 에이전트가 지나갈 수 있는 반지름
Agent Height : 에이전트가 아래로 지나갈 수 있는 높이
Max Slope : 에이전트가 올라갈 수 있는 경사 각도
Step Height : 에이전트가 오르내릴 수 있는 계단의 높이
Generated Off Mesh Links
오프 메시 링크는 올라가기 힘든 언덕, 사다리, 절벽 등을 연결해서 이동 가능하게 만드는 옵션
Drop Height : 이동할 수 있는 절벽 아래의 높이
Jump Distance : 뛰어서 넘을 수 있는 절벽 거리
Bake 버튼
Navigation에 설정된 옵션들을 바탕으로 네비게이션 정보를 데이터로 굽는다
Object
현재 씬(Scene)에 있는 오브젝트 설정 (하나 or 다수)
Scene Filter
현재 씬에서 원하는 오브젝트만 선택해서 볼 수 있다. (Mesh Renderer 컴포넌트, Terrain 컴포넌트 선택 가능)
선택된 오브젝트
Navigation Static : 네비게이션 메시로 사용할지 설정
Generate OffMeshLinks : 자동으로 Off Mesh를 생성할지 설정
Navigation Area : 해당 오브젝트의 구역 설정 (설정되는 구역에 따라 해당 오브젝트의 Cost가 설정됨)
Navigation Mesh 데이터 생성
게임 오브젝트의 Navigation static 설정하는 방법으로 원하는 게임 오브젝트 선택 후 Inspector View의 static > Navigation static 선택한 후 Object tab에 있는 Navigation Static 변수를 선택한다.
Navigation View > Bake 에서 Bake 버튼을 누르면 현재 설정된 정보를 바탕으로 Navigation Mesh 데이터가 생성된다.
Scene view를 보면 하늘색으로 표시되는 부분이 이동할 수 있는 이동 경로이고, 하늘색 없으면 이동 불가능한 구역이다. Navigation View가 활성화 되어 있어야 Navigation Mesh가 출력된다.
또한 맵에 새로운 바닥 혹은 장애물을 배치했다면 Navigation Static 설정과 Bake를 다시 진행해야한다.
경사각(Max Slope)과 계단 높이(Step Height) 설정
bake tab에서 Max slope가 45로 설정되어 있어서 경사각 45도까지는 이동할 수 있다. 그렇기 때문에 slope02 경로가 이어지지 않았다.
Step height는 오르내릴 수 있는 계단의 최대 높이이다. 현재 Step Height는 0.4로 인접한 두 계단 사이의 높이가 0.4 이하 차이가 나는 것 까지만 오를 수 있어 오른쪽의 경우 이동 경로가 생성되지 않았다.
플레이어 오브젝트 생성
플레이어 오브젝트를 생성하고자 capsule 오브젝트를 추가한 뒤에 색상 설정을 위해 meterial 생성한다.
Player 오브젝트의 설정은 아래와 같이 했다.
Navigation Mesh 기반의 캐릭터 이동
player 오브젝트에 NavMeshAgent 컴포넌트를 등록한다.
NavMeshAgent component
네비게이션 메시 정보를 기반으로 이동하는 에이전트
에이전트 이동 (Steering)
Speed : 이동 속도
Angular Speed : 방향을 바꿀 때의 회전 속도
Acceleration : 가속도 (정지 상태에서 이동속도가 될 때까지 적용)
Stopping Distance : 목적지가 이 값까지 가까워지면 이동을 멈추게 된다
Auto Braking : 목적지에 가까워지면 멈추는 기능
(목적지에 도착해도 에이전트를 멈추지 않을 때 사용)
(여러 목적지를 계속 탐색하는 Patrol에 주로 사용)
장애물 회피 (Obstacle Avoidance)
Radius : 장애물을 회피할 때 에이전트의 반지름
Height : 에이전트의 높이
Quality : 장애물과 충돌 수준 (None이면 뚫고 지나간다)
Priority : 장애물과 충돌했을 때의 우선순위 (낮을 수록 높다)
(이동 중인 두 에이전트의 모든 조건이 동일할 때 Priority가
낮은 에이전트가 더 우선권을 가지고 경로를 탐색하게 된다)
경로 탐색 (Path Finding)
Auto Traverse Off Mesh Link : 오프 메시 링크가 있을 경우
자동으로 탐색해서 찾아갈지 설정
Auto Repath : 이동 중에 경로 탐색을 다시 할지 설정
(true : 이동 중에 장애물 등으로 막혔을 때 자동으로 재 계산)
Area Mask : 해당 에이전트의 이동 가능한 구역 지정
이동을 제어하는 스크립트 Movement3D
using System.Collections;
using UnityEngine;
using UnityEngine.AI;
public class Movement3D : MonoBehaviour
{
[SerializeField]
private float moveSpeed = 5.0f;
private NavMeshAgent navMeshAgent;
private void Awake()
{
navMeshAgent = GetComponent<NavMeshAgent>();
}
public void MoveTo(Vector3 goalPosition)
{
// 기존에 이동 행동을 하고 있었다면 코루틴 중지
StopCoroutine("OnMove");
// 이동 속도 설정
navMeshAgent.speed = moveSpeed;
// 목표지점 설정 (목표까지의 경로 계산 후 알아서 움직인다)
navMeshAgent.SetDestination(goalPosition);
// 이동 행동에 대한 코루틴 시작
StartCoroutine("OnMove");
}
IEnumerator OnMove()
{
while ( true )
{
// 목표 위치(navMeshAgent.destination)와 내 위치(transform.position)의 거리가 0.1미만일 때
// 즉, 목표 위치에 거의 도착했을 때
if ( Vector3.Distance(navMeshAgent.destination, transform.position) < 0.1f )
{
// 내 위치를 목표 위치로 설정
transform.position = navMeshAgent.destination;
// SetDestination()에 의해 설정된 경로를 초기화. 이동을 멈춘다
navMeshAgent.ResetPath();
break;
}
yield return null;
}
}
}
NavMeshAgent와 같이 유니티에서 만든 인공지능을 사용하기 위해서는 UnityEngine.AI가 필요하다.
NavMeshAgnet.SetDestination(Vector3 position) : position을 목표지점으로 설정 (목표지점만 설정되면 경로를 탐색해서 자동 이동함)
float distance = Vector3.Distance(Vector3, Vector3) ;a와 b 두 벡터 사이의 거리 값을 구한다.
NavMeshAgnet.destination : SetDestination()으로 설정한 목표지점 Vector3 정보가 저장되어 있다.
NavMeshAgent.ResetPath() : 현재 설정되어 있는 이동 경로를 초기화하여 이동을 멈추게 한다 .
Physics.Raycast()를 이용한 오브젝트 선
우리가 바라보는 화면은 2차원 모니터이고, 게임 세상은 3차원으로 이루어져 있다. 즉, 2차원(2D)을 통해 3차원(3D)의 오브젝트를 제어해야 한다.
구현 원리
카메라로부터 플레이어가 클릭한 마우스 위치를 관통하는 광선을 생성하여 발사한다.
현재 카메라에 보이는 화면을 관통해 뻗어나가는 광선은 우리가 지정한 길이에 도달하거나 오브젝트에 부딪히면 멈춘다.
오브젝트에 부딪혀 멈추게 되면 부딪힌 오브젝트의 정보를 반환한다.(광선에 부딪혔다 = 마우스로 선택한 오브젝트이다.)
플레이어를 제어하는 스크립트 PlayerController
using UnityEngine;
public class PlayerController : MonoBehaviour
{
private Movement3D movement3D;
private void Awake()
{
movement3D = GetComponent<Movement3D>();
}
private void Update()
{
// 마우스 왼쪽 버튼을 눌렀을 때
if ( Input.GetMouseButtonDown(0) )
{
RaycastHit hit;
// Camera.main : 태그가 "Camera"인 오브젝트 = "Main Camera"
// 카메라로부터 마우스 좌표(Input.mousePosition) 위치를 관통하는 광선 생성
// ray.origin : 광선의 시작 위치
// ray.direction : 광선의 진행 방향
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
// Physics.Raycast() : 광선을 발사해서 부딪히는 오브젝트를 검출
// (광선에 부딪히는 오브젝트가 있으면 true 반환)
// ray.origin 위치로부터 ray.direction 방향으로 무한한 길이(Mathf.Infinity)의 광선 발사
// 광선에 부딪히는 오브젝트의 정보를 hit에 저장
if ( Physics.Raycast(ray, out hit, Mathf.Infinity) )
{
// hit.transform.position : 부딪힌 오브젝트의 위치
// hit.point : 광선과 오브젝트가 부딪힌 세부 위치
// hit.point를 목표위치로 이동!
movement3D.MoveTo(hit.point);
}
}
}
}
카메라가 게임월드를 전체적으로 볼 수 있도록 카메라의 위치와 회전값을 수정한 뒤에 실행한다. 선택한 위치로 player 오브젝트가 이동하지만 이동경로가 없는 사다리나 높은 경사면, 절벽 등을 선택하는 경우 근처까지만 이동하고 더 이상 이동하지 못한다.
CharacterController 컴포넌트는 유니티에서 제공하는 컴포넌트로 3차원 게임에서 사람 형태의 캐릭터 움직임과 관련된 제어를 위해 사용하는 컴포넌트이다.
캡슐(Capsule) 형태의 collider이 포함되어 있기 때문에 따로 Collider 컴포넌트를 추가하지 않아도 충돌 처리 및 물리처리가 가능하다.
캐릭터의 움직임을 위해 제작된 컴포넌트이기 때문에 오브젝트가 이동할 수 있는 경사각도, 올라갈 수 있는 계단 높이와 같은 정보를 설정할 수 있으며, center, radius, height 정보를 설정해 collider을 설정할 수 있다.
Slope Limit : 올라갈 수 있는 경사 한계 각
Step Offset : 설정 값보다 낮은 높이의 계단(그 외 오브젝트)을 오를 수 있다
Center : Capsule 충돌 범위의 중심점
Radius : Capsule 충돌 범위의 반지름 (x, z)
Height : Capsule 충돌 범위의 높이 (y)
게임 월드 구성, 플레이어 캐릭터 제작
영상 하단에 있는 게임월드 패키지를 프로젝트 내부로 가져온다. prefab 폴더에 GameWorld prefab을 Hirearchy view로 드래그한다.
한 눈에 볼 수 있도록 main camera를 다음과 같이 바꾸어 준다.
player로 사용할 3D 오브젝트를 생성한다.(GameObject > 3D Object > Capsule)
player의 색상을 변경할 때 3D에서는 Project View > “+” > Material로 meterial Asset을 생성하고 색상과 이미지를 정하고 오브젝트에 등록해야한다.
설정이 완료되었다면 플레이어 오브젝트의 Mesh renderer 컴포넌트에서 등록을 해준다.
플레이어 오브젝트에 character controller 컴포넌트를 추가하고 capsule collider를 지워준다. scene view에서 확인을 해보면 캡슐 범위의 충돌 범위가 capsule collider에 의하여 설정된 것을 확인할 수 있다.
플레이어 캐릭터 이동
캐릭터 컨트롤러를 기반으로 이동을 제어하는 Movement3D 스크립트를 작성한다.
using UnityEngine;
public class Movement3D : MonoBehaviour
{
[SerializeField]
private float moveSpeed = 5.0f; // 이동 속도
private Vector3 moveDirection; // 이동 방향
private CharacterController characterController;
private void Awake()
{
characterController = GetComponent<CharacterController>();
}
private void Update()
{
characterController.Move(moveDirection * moveSpeed * Time.deltaTime);
}
public void MoveTo(Vector3 direction)
{
moveDirection = direction;
}
}
플레이어를 제어하는 스크립트, PlayerController 를 생성한다.
using UnityEngine;
public class PlayerController : MonoBehaviour
{
private Movement3D movement3D;
private void Awake()
{
movement3D = GetComponent<Movement3D>();
}
private void Update()
{
// x, z축 방향으로 이동
float x = Input.GetAxisRaw("Horizontal"); // 방향키 좌/우 움직임
float z = Input.GetAxisRaw("Vertical"); // 방향키 위/아래 움직임
movement3D.MoveTo(new Vector3(x, 0, z));
}
}
생성한 두 스크립트를 플레이어 오브젝트에 컴포넌트에 추가한다. 방향키의 방향에 따라 오브젝트가 움직이는 것을 확인할 수 있다.
중력 적용
위에 플레이어 오브젝트의 y 축을 올렸을 때, 중력에 의해 떨어지지 않고 공중에서 이동을 하는데 캐릭터 컨트롤러 컴포넌트는 충돌 처리가 가능할 뿐 중력은 적용되지 않기 때문이다. 중력 적용을 위해 직접 스크립트를 통해 입력을 해야한다.
우선 Movement3D 스크립트 코드로 가서 중력 계수를 추가하였고, 업데이트 함수를 확인하면 characterController.isGrounded == false 즉 캐릭터의 발 위치의 충돌을 체크하여 충돌이 된다면 true, 아니라면 flase값을 나타내는 변수로 캐릭터가 땅에 닿아있을 때를 확인하는 변수로 사용하기 때문에 false로 설정했다.
공중에 있을 떄 moveDirection.y += gravity * Time.deltaTime; 를 통해 y축 이동 방향에 gravity * Time.deltaTime을 계속 더해준다. gravity가 음수이기에 y축 이동 방향은 계속 감소하게 되고, 계속 아래로 떨어지려고 하는 중력을 받는 것이 적용되게 될 것이다.
[SerializeField] private float jumpForce = 3.0f; // 뛰어 오르는 힘
뛰는 힘을 변수를 생성하고 Jump To 함수를 public으로 선언하여 외부에서 호출하는 용도로 제작하였다.
public void JumpTo()
{
if ( characterController.isGrounded == true )
{
moveDirection.y = jumpForce;
}
}
이 함수가 호출되면 characterController.isGrounded 값이 true일 때, 즉 캐릭터가 바닥에 있으면 moveDirection y값에 jumpforce가 대입한다. 즉 y축 방향이 3이 되어 위로 점프 된다.
update에 작성했던 위 코드에 의해 위로 점프를 하지만 y축 이동 방향 값이 서서히 감소하게 되어 0이 되는 시점이 점프 최고점이 될 것이고 다시 음수에 의해 떨어지게 된다.
// y축 방향으로 뛰어오름
if ( Input.GetKeyDown(jumpKeyCode) )
{
movement3D.JumpTo();
}
스페이스바를 눌렀을 때 movement3D에 있는 JumpTo() 함수를 호출해서 점프하게끔 설정하였다.
경사면 이동과 계단 이동
Slope Limit 변수는 경사 한계 각도를 설정할 수 있으며 현재 45도로 설정되어 있어 45도까지 경사를 오를 수 있다.
Step Offset 변수는 인접한 계단 사이의 높이가 해당 변수 이하일 때 계단을 오를 수 있다.
GameWorld에 경사면이 총 2개가 있고 하나는 39도 경사라 오를 수 있지만 다른 하나는 52도여서 올라갈 수 없다.
인접한 계단의 높이 차가 0.4로 플레이어의 step offset 값을 0.4로 변경하면 계단에 올라갈 수 있다.
1인칭 카메라 제어
먼저 camera 오브젝트가 플레이어를 쫓아다니도록 player의 자식으로 설정한다. 그리고 카메라 transeform 정보를 초기화한 뒤에 y축을 0.6으로 바꾼다.
scene 뷰에서 카메라가 player 오브젝트 안에 있는 것을 확인할 수 있었다.
카메라의 회전을 설정하기 위한 스크립트를 하나 작성한다.
using UnityEngine;
public class CameraController : MonoBehaviour
{
private float rotateSpeedX = 3;
private float rotateSpeedY = 5;
private float limitMinX = -80;
private float limitMaxX = 50;
private float eulerAngleX;
private float eulerAngleY;
public void RotateTo(float mouseX, float mouseY)
{
// 마우스를 좌/우로 움직이는 mouseX 값을 y축에 대입하는 이유는
// 마우스를 좌/우로 움직일 때 카메라도 좌/우를 보려면 카메라 오브젝트의
// y축이 회전되어야 하기 때문
eulerAngleY += mouseX * rotateSpeedX;
// 같은 개념으로 카메라가 위/아래를 보려면 카메라 오브젝트의 x축이 회전!
eulerAngleX -= mouseY * rotateSpeedY;
// x축 회전 값의 경우 아래, 위를 볼 수 있는 제한 각도가 설정되어 있다
eulerAngleX = ClampAngle(eulerAngleX, limitMinX, limitMaxX);
// 실제 오브젝트의 쿼터니온 회전에 적용
transform.rotation = Quaternion.Euler(eulerAngleX, eulerAngleY, 0);
}
private float ClampAngle(float angle, float min, float max)
{
if ( angle < -360 ) angle += 360;
if ( angle > 360 ) angle -= 360;
// Mathf.Clamp()를 이용해 angle이 min <= angle <= max을 유지하도록 함
return Mathf.Clamp(angle, min, max);
}
}
RotateTo() 함수는 외부에서 마우스의 x,y축 움직임을 가져와 카메라의 로테이션에 적용해 카메라를 회전시키는 함수이다.
마우스를 좌/우로 움직일 때 카메라도 좌/우를 보려면 카메라 오브젝트의 y축이 회전해야하기 때문에 마우스를 좌/우로 움직이는 mouseX 값을 y축에 대입한다.
// 같은 개념으로 카메라가 위/아래를 보려면 카메라 오브젝트의 x축이 회전!
eulerAngleX -= mouseY * rotateSpeedY;
위에 함수 코드 중에 위에 부분에서 더하지 않고 빼는 이유는 마우는 아래로 가는 것이 -이기 때문에 빼준다.
playercontroller에서 카메라 제어하는 부분을 구현하도록 하자면, CamerController에 있는 Rotate To 함수를 호출하기 위해서 변수를 생성한다.
[SerializeField]
private CameraController cameraController;
private void Update()
{
// x, z축 방향으로 이동
float x = Input.GetAxisRaw("Horizontal"); // 방향키 좌/우 움직임
float z = Input.GetAxisRaw("Vertical"); // 방향키 위/아래 움직임
movement3D.MoveTo(new Vector3(x, 0, z));
// y축 방향으로 뛰어오름
if ( Input.GetKeyDown(jumpKeyCode) )
{
movement3D.JumpTo();
}
// 카메라 x, y축 회전
float mouseX = Input.GetAxis("Mouse X"); // 마우스 좌/우 움직임
float mouseY = Input.GetAxis("Mouse Y"); // 마우스 위/아래 움직임
cameraController.RotateTo(mouseX, mouseY);
}
위의 두 변수 값이 CameraController의 Rotate To 함수의 매개변수값으로 전달되면 마우스가 움직였을 때 Rotate To() 함수가 호출되어 실제 카메라가 회전하게 된다.
Movement3D 스크립트에서 카메라를 제어하는 부분을 추가하여 방향키를 눌러 이동 시 현재 카메라가 비추고 있는 전방 방향으로 이동할 수 있도록 설정한다.
public void MoveTo(Vector3 direction)
{
// 카메라가 바라보고 있는 방향을 기준으로 방향키에 따라 이동할 수 있도록 함
Vector3 movedis = cameraTransform.rotation * direction;
moveDirection = new Vector3(movedis.x, moveDirection.y, movedis.z);
}
public void JumpTo()
{
if ( characterController.isGrounded == true )
{
moveDirection.y = jumpForce;
}
}
}
이렇게 설정하면 게임 실행시 마우스가 움직이는 곳을 카메라가 비추고 캐릭터가 잘 움직이는 것을 확인할 수 있다.