[Go-Unity 3D] 15. Navigation Mesh - Off Mesh Link โญโญโญ
์นดํ ๊ณ ๋ฆฌ: Go Unity
Navigation Mesh , Off mesh Link(์๋,์๋), Nav Mesh Obstacle, Patrol
๊ณ ๋ฐ์ฌ์ ์ ๋ํฐ ๊ธฐ์ด๋ฅผ ์ ๋ฆฌ.
์ฌ๋ค๋ฆฌ ์ ๋ฒฝ์ฌ์ด ์ด๋, ๋ํ
1. Off Mesh Link
- ์ฌ๋ค๋ฆฌ, ์๋ฒฝ๊ณผ ๊ฐ์ด ์์ง์ผ๋ก ์ฌ๋ผ๊ฐ๊ฑฐ๋ ๋ด๋ ค์ค๋ ๊ธธ
- ์ ๋ฒฝ ์ฌ์ด๋ฅผ ๋ฐ์ด์ ๋์ด๊ฐ๊ฑฐ๋ ๋ญ๋ ๋ฌ์ง ์๋๋ก ๋จ์ด์ง๋ ๊ธธ
- ๋ฉ์๊ฐ ๋์ด์ ธ ์๋ ๊ณณ์ ์ด๋ํ ์ ์๊ฒ ์ค์ ํ๋ ๊ฒ
- ์ค์ : ์๋ , ์๋
- auto traverse off mesh : ์ด๋์ค๋ธ์ ํธ์ nav mesh agent ์ปดํฌ๋ํธ์ ์์ผ๋ฉฐ,
์ฒดํฌํด์ ์ off mesh link ๊ตฌ๊ฐ์์ ๋ฉ์ถ๋ค, ํด์ ์ ๋ฐ๋ก ๋์์ ๋ง๋ค์ด ์ฌ์ฉํ๋ค ex) Off Mesh Link Climb.cs
์๋(Auto)_ OffMeshLink
- Navigation view - Objectํญ -> Generate OffMeshLinks ์ฒดํฌ
- Navigation view - Bakeํญ์ ๋ํ ๋์ด, ์ ํ ๊ฑฐ๋ฆฌ ์ค์ ํ Bake๋ก ๋ฐ์ดํฐ ์ ์ฅ
์ฅ์
- ๊ฒ์์๋์ ๋ฐฐ์น๋ ๋ง์ ์ค๋ธ์ ํธ์ Off Mesh Link๋ฅผ ํ๊บผ๋ฒ์ ์ค์ ๊ฐ๋ฅ
๋จ์
- ๋ํ ๋์ด์ ์ ํ ๊ฑฐ๋ฆฌ๋ฅผ ํ๋๋ง ์ค์ ํ ์ ์๊ธฐ ๋๋ฌธ์ ๋ค์ํ ์งํ์ ์ธ์ธํ๊ฒ X
- ์๋ก ์ฌ๋ผ๊ฐ๋ Off Mesh Link ์ค์ X
์๋(Manual)_ OffMeshLink
- ์ฐ๊ฒฐ๋๋ ๋ ์ง์ ์ผ๋ก ์ฌ์ฉํ ์ค๋ธ์ ํธ ์์ฑ ๋ฐ ๋ฐฐ์น (Start, End)
- off mesh link ์ปดํฌ๋ํธ๋ฅผ ์์ฑํ๊ณ , Start, End ๋ณ์์ ์ฐ๊ฒฐ๋๋ ๋ ์ง์ ์ ์ค์
์ฅ์
- ์งํ์ ๋ฐ๋ผ ์ธ์ธํ ์ค์ ์ด ๊ฐ๋ฅ
- ์ฌ๋ค๋ฆฌ/์๋ฒฝ๊ณผ ๊ฐ์ด ์๋ก ์ฌ๋ผ๊ฐ๋ ์ค์ ๊ฐ๋ฅ
๋จ์
- Off Mesh Link๋ก ์ฐ๊ฒฐ์ด ํ์ํ ๋ชจ๋ ๋ถ๋ถ์ ์ง์ ์ค์ ํด์ผํจ
2. ๊ตฌ์ญ(Area)
์ด๋๊ธธ์ด ๋ ๋น ๋ฅด๊ณ ๋๋ฆฐ์ง ํ์ธํ๊ธฐ ์ํด์
-
์ผ๋ฐ๊ตฌ์ญ์ ObJectํญ์์ ์ค์ ๊ฐ๋ฅ
-
Navigation view - Areaํญ์์ ๊ตฌ์ญ ์ถ๊ฐ
-
์ฌ๋ค๋ฆฌ์ Climb area๋ฅผ ์ถ๊ฐ ํ๊ธฐ์ํด Area์ Climb ์ถ๊ฐ
-
์๋ก์ด ๊ตฌ์ญ์ด ์๊ธฐ๊ฒ ๋๋ฉด ์ด๋ํ๋ ์ค๋ธ์ ํธ์ nav mesh agent์ปดํฌ๋ํธ์ Area Mask์ ํ์ธํด๋ณด๊ธฐ
3. OffMeshLinkClimb.cs
์ฌ๋ค๋ฆฌ ์ฌ์ฉ์ ๋์
OffMeshLinkClimb.cs
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;
}
}
- offmeshlink ์ฌ๋ค๋ฆฌ ๋์์ ๋ง๋๋ฉด ํ๋์ ๋ฉ์ถํ ์ฌ๋ค๋ฆฌ๋์์ ์คํํ๊ณ ๋ค์ ์ด๋
4. OffMeshLinkJump.cs
๋จ์ด์ง๊ฑฐ๋ ์ ๋ฒฝ์ฌ์ด๋ฅผ ์ ํํ ๋
OffMeshLinkJump.cs
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;
}
}
- offmeshlink ์ ๋ฒฝ์ ๋ง๋๋ฉด ํ๋์ ๋ฉ์ถํ ์ฌ๋ค๋ฆฌ๋์์ ์คํํ๊ณ ๋ค์ ์ด๋
5. Nav Mesh Obstacle ์ปดํฌ๋ํธ
์ด๋ ์ค๋ธ์ ํธ์ ๋ค๋น๊ฒ์ด์ ๋ฉ์ ์ค์ ์ ์ฌ์ฉ๋๋ ์ปดํฌ๋ํธ
- Shape : ์ฅ์ ๋ฌผ์ ๋ชจ์
- Center : shape ์ ์ค์ฌ
- Carve : navigation mesh์ ๊ณต๊ฐ์ ๋น์ธ์ง (true:๋น์ด๋ค)
move Threshold
์ค์ ๋ ๊ฑฐ๋ฆฌ๋ฅผ ์ด๋ํ๋ฉด ์ค๋ธ์ ํธ์ Navigation mesh ๋ฐ์ดํฐ ๊ฐฑ์
time to stationary
: ์ค์ ๋ ์๊ฐ๋งํผ ์์ง์์ด ์์ผ๋ฉด โ๋ฉ์ถคโ์ผ๋ก ์ธ์
carve Only stationary
: ๋ฉ์ถค ์ผ๋ ๊ณต๊ฐ์ ๋น์ธ์ง
(true : ๋ฉ์ถ๋๋ง ๋น์, flase : ์ค์๊ฐ์ผ๋ก ๋น์)
Patrol ์์ฐฐ
SimplePatrol.cs
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;
// ๋ชฉํ ์์น์ ๊ฑฐ์ ๋๋ฌํ์ ๋ (a-b)sqrMagnitude : a-b ๊ฑฐ๋ฆฌ ๊ฐ์ ์ ๊ณฑํ ๊ฒฐ๊ณผ ์ฐ์ฐ์๋ ๋น ๋ฆ
if ( (paths[currentPath].position - transform.position).sqrMagnitude < 0.1f )
{
// ๋ชฉํ ์์น ๋ณ๊ฒฝ (์์ฐฐ ๊ฒฝ๋ก ์ํ)
if ( currentPath < paths.Length - 1) currentPath ++;
else currentPath = 0;
}
}
}
- patrol ์ฝ๋ ์ ์ฉ ํ cube ์ด๋ ์๋ฆฌ๋ ์ด๋ํ ์ ์๋ ์๋ฆฌ๋ก ๋ณด์ง ์๋๋ค.
- nav mesh obstacle ์ปดํฌ๋ํธ๋ฅผ ์ถ๊ฐํ์ฌ ํ๋ธ๋ก ์ด๋ ์์น๋ฅผ ์ด๋ํ ์ ์๋ ๊ฒฝ๋ก๋ก๋ณธ๋ค.
nav mesh obstacle ์ถ๊ฐ X
nav mesh obstacle ์ถ๊ฐ ํ
nav mesh obstacle ์ปดํฌ๋ํธ๋ฅผ ์ถ๊ฐํ์ฌ ํ๋ธ๋ก ์ด๋ ์์น๋ฅผ ์ด๋ํ ์ ์๋ ๊ฒฝ๋ก๋ก๋ณธ๋ค.
6. ์ ๋ฆฌ
- Off Mesh Link ์ ๋ํด์ (์ฌ๋ค๋ฆฌ, ์ ๋ฒฝ์ฌ์ด ์ ํ, ์ ๋ฒฝ ๋ํ)
- Area ์ถ๊ฐ , ์ฝ์คํธ, ๋ ๋น ๋ฅธ๊ธธ ์ฐพ๊ฒ ๋์์ค.
- nav Mesh Obstacle์ ์ด์ฉํด ์ค์๊ฐ์ผ๋ก navํ์ธ
๋๊ธ๋จ๊ธฐ๊ธฐ