
길찾기 알고리즘인 A*알고리즘을 활용한, 유니티에서 제공하는 길찾기 패키지이다. 어렵게 알고리즘 구조를 짤 필요없이 간편하게 에디터 상에서 적들을 플레이어에게 따라오게 만들 수 있다
자동 전투와 자동 길찾기등에 사용할 수 있는 기능이다. 생성된 맵에서 여러 변화요인들을 감지, 목적지 까지 이동시킨다A* 알고리즘이 적용되어 있다. 먼저 이 알고리즘을 이론적으로 파악한 후, 해당 알고리즘을 통해 만들어진 길찾기 기능인 NavMesh를 다뤄보자노드(타일)에서 목적지와의 거리, 현재까지 진행된 거리, 이전 노드와의 거리를 연산하고, 다음 노드로 넘어가 다시 연산하는 로직으로 구성되어 있다휴리스틱 함수 에 따라 성능이 극명하게 갈리며, 일 때는 다익스트라 알고리즘과 동일하다
- 스타크래프트의 길찾기 또한 해당 알고리즘이 사용되었다
A*를 사용하는 이유는 다익스트라를 직접 현실 문제에 적용하기가 매우 부담되기 때문이다. 현실 세계의 사람이 다닐 수 있는 "거리"를 생각 해보자. 길 하나만 전부 노드화 시키기에도 그 수가 엄청나게 많아질 수 있다. 목적지 까지를 생각하면 탐색해야 하는 공간도 그만큼 커지게 되고(공간 복잡도 증가), 시간 복잡도 또한 마찬가지로 기하급수적으로 커질 것이다. 노드화 시킨다 가정하고 다익스트라를 사용하여 경로를 발견 했다고 해보자. 그렇게 탐색한 경로가 자동차 정체 구간, 출근길 등 다양한 변수로 인해 오히려 더 느려질 수 있는 경우도 발생할 수 있다. 이러한 변수 때문에 A* 알고리즘을 사용하는 것오름차순 우선순위 큐에 노드로 삽입한다(Enqueue)PQ.push(start_node, g(start_node) + h(start_node)) //우선순위 큐에 시작 노드를 삽입한다.
while PQ is not empty //우선순위 큐가 비어있지 않은 동안
node = PQ.pop //우선순위 큐를 pop한다.
if node == goal_node //만일 해당 노드가 목표 노드이면 반복문을 빠져나온다.
break
for next_node in (next_node_begin...next_node_end) //해당 노드에서 이동할 수 있는 다음 노드들을 보는 동안
PQ.push(next_node, g(node) + cost + h(next_node)) //우선순위 큐에 다음 노드를 삽입한다.
print goal_node_dist //시작 노드에서 목표 노드까지의 거리를 출력한다.
Manhattan Distance. 보통 휴리스틱 함수로 많이 쓰인다NavMesh는 3D 공간에서도 작동하므로, 3D 좌표 상의 거리 계산이 필요Unity의 NavMesh는 다양한 방향(심지어 곡선 형태도 가능)으로 이동이 가능한 비격자 기반 경로 탐색을 수행하므로, 맨해튼 디스턴스는 실제 거리와 괴리가 크다. 그래서 유클리디안 거리를 사용
팁
UGUI특성상, 자주 바뀔 만한 요소들은 캔버스를 따로 둬야 한다- 하나의 요소만 바꿔도 싹다 바꿔야 하기 때문이다
그래서, 자주 바뀌지 않는 고정적인 UI들과 변동적인 UI들을 서로 다른 Canvas에 배치하는 것을 권장한다
드래그&드롭으로 추가한다
스프라이트로 하자
하이라키 창에서 UI -> Image를 추가한다. 여기서 Source Image에 해당 에임 이미지를 넣으면 된다하이라키 창에서 Canvas를 선택, 씬 창에서 2D 화면으로 변경 후(단축키: 2) Rect Tool로 변경해준다 
애니메이터를 설정하자. Image UI에 Animator 컴포넌트를 추가하고, 위 사진과 같이 상태 설정을 해준다. 각 상태에 맞는 애니메이션을 생성 후 추가해준다
프로젝트 창에서 애니메이션을 추가하는 방법이다
애니메이터의 상태 간 Transition을 생성 해주고, 위와 같이 설정해준다. ActiveAim은 Conditions를 반대로 해준다
Image Type 및 설정을 위 사진과 같이 변경해준다
ActiveAim 애니메이션과 그 반대의 애니메이션의 프로퍼티를 위 사진과 같이 설정해준다주의
- 이걸 바꿔줘야 다른 애니메이션 편집이 가능함
// 이미지 컴포넌트를 불러오려고 한다
private Image aimImage;
// 애니메이터를 드래그&드롭으로 참조시킴
[SerializeField] private Animator aimAnimator;
// 이미지기 애니메이터와 같은 게임 오브젝트에 묶여 있으니, 다음과 같이 찾을 수 있음
private void Init()
{
aimImage = aimAnimator.GetComponent<Image>();
}
private void SetAimAnimation(bool value)
{
// 에임 이미지를 비활성화 상태로 시작할 거다. 비활성화 상태이면 활성화
// 이걸 해줘야 게임 맨처음 시작 시 에임 이미지가 사라지는 애니메이션이 재생 안됨
if (!aimImage.enabled) aimImage.enabled = true;
// 에임 애니메이터의 파라매터를 true 또는 false로 바꿈
aimAnimator.SetBool("IsAim", value);
}
// 애니메이션 구독
// IsAiming 값에 따라 애니메이션들이 재생된다
public void SubscribeEvents()
{
status.IsAiming.Subscribe(SetAimAnimation);
}
public void UnsubscribeEvents()
{
status.IsAiming.UnSubscribe(SetAimAnimation);
}

override해서 프리팹 저장을 잊지말자

HP 게이지가 화면 상에서, 캐릭터 위에 떠다니게 구현 해보자

HPUI는 Transform만 있는 빈 게임 오브젝트다. Background는 체력바 뒤에 있는 것, Gauge가 체력 값에 따라 늘고 줄어드는 것이다. 둘 다 이미지UI 다
캔버스 랜더모드 월드 스페이스 설정
LookAt() 으로 체력바 이미지를 회전시킨다private void LateUpdate()
{
transform.LookAt(Camera.main.transform);
}

private void LateUpdate() => SetUIRotate2();
private void SetUIRotate2()
{
transform.forward = Camera.main.transform.forward;
}

private void LateUpdate() => SetUIRotate3();
private void SetUIRotate3()
{
transform.forward = -Camera.main.transform.forward;
}





Fill Amount 값에 따라서 이미지가 늘어나고 줄어든다float 값은 0 ~ 1사이이다//UI 게이지의 FillAmount를 설정.표시 대상의 HP로 설정
public void SetImageFillAmount(float amount)
{
// 현재수치 / 최대수치만큼
HPGauge.fillAmount = amount;
}


HP바가 캐릭터를 가린다HP바 또한 카메라를 기준으로 회전해서 대상을 안 가리게 해보자HPUI와 Canvas의 포지션들 조정만으로도 구현이 가능하다



Gauge는 즉시 감소, DelayGauge는 애니메이션 효과로 천천히 감소 시키는 식으로 연출을 할 수 있다FillAmount와 현재 FillAmount를 따로 두고, 업데이트마다 deltaTime을 이용해 깎아나가는 방식도 가능. 코루틴도 가능하다public class JYL_HpGaugeUI : MonoBehaviour
{
[SerializeField] private Image HPGauge;
private Transform cameraTransform;
private void Awake()
{
Init();
}
private void LateUpdate()
{
SetUIForwardVector(cameraTransform.forward);
}
private void Init()
{
cameraTransform = Camera.main.transform;
// image = GetComponentInChildren<Image>();
}
//UI 게이지의 FillAmount를 설정.표시 대상의 HP로 설정
public void SetImageFillAmount(float amount)
{
// 현재수치 / 최대수치만큼
HPGauge.fillAmount = amount;
}
private void SetUIForwardVector(Vector3 target)
{
transform.forward = target;
}
}
[field:SerializeField]
[field:Range(0,100)]
public int MaxHP { get; set; }
// 현재 체력이 변할 때 마다 이벤트 발생
public JYL_ObservableProperty<int> CurrentHP { get; private set; } = new();
private void Init()
{
hpUI.SetImageFillAmount(1);
}
private void SetHpUIGauge(int currentHP)
{
// 둘 다 int이므로 하나는 형변환을 해줘야 결과값이 `float`가 된다
float hp = (float)currentHP / status.MaxHP;
hpUI.SetImageFillAmount(hp);
}
public void SubscribeEvents()
{
status.CurrentHP.Subscribe(SetHpUIGauge);
}
public void UnsubscribeEvents()
{
status.CurrentHP.UnSubscribe(SetHpUIGauge);
}
public void TakeDamage(int damage)
{
// 체력을 떨어뜨리지만, 0 이하면 플레이어가 죽어야함
status.CurrentHP.Value -= damage;
if (status.CurrentHP.Value <= 0) PlayerDie();
}
public void RecoverHP(int recoverAmount)
{
// 체력을 회복시킨다. MaxHP 초과는 하지 못하게 한다
int hp = status.CurrentHP.Value + recoverAmount;
// Clamp로 최대값, 최소값을 설정할 수 있다
status.CurrentHP.Value = Mathf.Clamp(hp, 0, status.MaxHP);
}
public void PlayerDie()
{
// 직접 구현해보자
Debug.Log("플레이어 죽음");
}


NavMesh. 길찾기 알고리즘인 A*을 활용한 유니티의 기능이다
패키지 매니저에서 좌측 상단에서 Packages를 Unity Registry로 변경한다. 이후 Nav만 검색해도 AI Navigation이 나온다. 설치하자
Map으로 프리팹화 해두고, 컴포넌트로 위의 사진과 같이 NavMeshSurface를 추가한다
Bake 버튼을 누르면 위의 사진과 같이 파란 영역들이 생겨난다. 이것이 정해진 물체들이 길찾기로 이동할 수 있는 영역들을 나타낸 것이다

Collect Objects에서 All Game Objects는 모든 게임 오브젝트를, Current Object Hierarchy는 이 컴포넌트가 있는 게임 오브젝트 및 그 하위 오브젝트들을 대상으로 Bake 된다
Include Layers : 지정된 레이어만 Bake 한다

Area 설정이 가능하다. Cost는 비용. 높을수록 경로로써 안 지나가려고 하는 저항 수치다
Cost가 충분히 높을 경우, 빨간색 점선이 아니라, 빨간색 실선을 경로로 움직인다


NavMesh 속성을 정해줄 수 있다
Bake 시, 다른 영역은 지정색에 따라 결과가 달라진다
NavMeshComponent에서 위와같이 세팅으로 들어갈 수 있다
Agent Types에서 에이전트의 타입을 추가, 삭제 가능Radius : 에이전트의 단면적. Bake 시 통행 가능 영역에 영향을 준다Height : 에이전트의 높이Step Height : 그냥 넘어갈 수 있는 높이Max Slope : 경사면을 몇 도까지 올라갈 수 있는 지 정할 수 있다Bake 영역에 영항을 줌Generated Links : NavMesh Link와 관련있다
NavMeshAgent navAgent;
// 요기에 타겟(플레이어)을 참조시킨다
[SerializeField] Transform targetTransform;
void Awake()
{
navAgent = GetComponent<NavMeshAgent>();
}
public void Update()
{
// 업데이트마다 타겟의 위치를 목적지로 설정하고, 추적함
navAgent.SetDestination(targetTransfrom.position);
}
NavMesh 기능으로 플레이어를 쫒아가는 몬스터를 구현해본다public interface IDamagable
{
public void TakeDamage(int damage);
}
IDamagable을 상속시킨다TakeDamage()함수가 구현되어 있어서 매개변수만 맞춰준다public abstract class Monster : MonoBehaviour
{
}
Monster 추상클래스와 인터페이스 IDamagable을 상속받는다MVC 모델로 구현했지만, 시간 편의상 몬스터는 한 스크립트 안에 구현한다public class NormalMonster : Monster, IDamagable
{
private bool IsActivateControl = true;
private bool canTracking = true;
[Header("Set HP")]
[SerializeField] private int MaxHP;
private JYL_ObservableProperty<int> CurrentHP;
private JYL_ObservableProperty<bool> IsMoving = new();
private JYL_ObservableProperty<bool> IsAttacking = new();
// 애니메이션 판단용, 움직일 때의 이벤트에 활용가능
private NavMeshAgent navMeshAgent;
[Header("Set Target")]
[SerializeField] private Transform targetTransform;
private void Awake()
{
Init();
// 처음부터 쫒아가지 않게 함
navMeshAgent.isStopped = true;
}
private void Update()
{
HandleControl();
}
private void Init()
{
navMeshAgent = GetComponent<NavMeshAgent>();
}
private void HandleControl()
{
// 컨트롤 권한이 없으면 리턴
if (!IsActivateControl) return;
HandleMove();
}
private void HandleMove()
{
// 타겟이 없을 경우 예외처리
if (targetTransform == null) return;
// 추적이 가능한 상태이면
if (canTracking)
{
// 정지 중인지? true라면 false로 바꾼다
//navMeshAgent.isStopped = false;
navMeshAgent.SetDestination(targetTransform.position);
IsMoving.Value = true;
}
//else
//{
// navMeshAgent.isStopped = true;
// // 정지 중인지 확인. false라면 true로 바꿈
// IsMoving.Value = false;
//}
// 추적할 수 없는 상태면, 멈춘 상태
navMeshAgent.isStopped = !canTracking;
// 추적할 수 없는 상태면, 움직이지 않음
IsMoving.Value = canTracking;
}
public void TakeDamage(int damage)
{
// 데미지 판정 구현
// 1. 체력 깎기
// 2. 체력이 0 이하면 죽음
// 3. 체력 UI처리
Debug.Log($"{gameObject.name}이 데미지 입음 : 2");
}
}
몬스터 2마리 이상
맵
총을 쏴서 적에게 데미지 입히기
적도 HP바 가지기
떨어진 아이템을 주울 경우 체력 회복
플레이어 죽는 애니메이션
그럼 그와 관련한 필드가 있어야 한다. status에 상태 추가
씬 전환
게임 스테이터스 초기화