기록 보관소

[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: 목표를 추적하는 AI 만들기[B48] 본문

유니티 프로젝트/3D 쿼터뷰 액션게임

[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: 목표를 추적하는 AI 만들기[B48]

JongHoon 2022. 4. 2. 23:45

개요

유니티 입문과 독학을 위해서 아래 링크의 골드메탈님의 영상들을 보며 진행 상황 사진 또는 캡처를 올리고 배웠던 점을 요약해서 적는다.

현재는 영상들을 보고 따라하고 배우는 것에 집중할 것이며, 영상을 모두 보고 따라한 후에는 개인 프로젝트를 설계하고 직접 만드는 것이 목표다.

https://youtube.com/playlist?list=PLO-mt5Iu5TeYI4dbYwWP8JqZMC9iuUIW2 

 

유니티 강좌 기초 채널 Basic

유니티 개발을 처음 시작하시는 입문자 분들을 위한 기초 채널. [ 프로젝트 ] B00 ~ B12 (BE1) : 유니티 필수 기초 B13 ~ B19 (BE2) : 2D 플랫포머 B20 ~ B26 (BE3) : 2D 탑다운 대화형 RPG B27 ~ B37 (BE4) : 2D 종스크롤

www.youtube.com


3D 쿼터뷰 액션 게임: 목표를 추적하는 AI 만들기[B48]

1. 오브젝트 생성

에셋의 프리펩 폴더에서 Enemy A를 가져와서 생성
Enemy A 설정

 

//Enemy 스크립트 파일

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

public class Enemy : MonoBehaviour {
	public int maxHealth;
	public int curHealth;

	Rigidbody rigid;
	BoxCollider boxCollider;
	Material mat;

	void Awake() {
		rigid = GetComponent<Rigidbody>();
		boxCollider = GetComponent<BoxCollider>();
		mat = GetComponentInChildren<MeshRenderer>().material;
	}

	void OnTriggerEnter(Collider other) {
		if (other.tag == "Melee") {
			Weapon weapon = other.GetComponent<Weapon>();
			curHealth -= weapon.damage;
			Vector3 reactVec = transform.position - other.transform.position;

			StartCoroutine(OnDamage(reactVec, false));
		}
		else if (other.tag == "Bullet") {
			Bullet bullet = other.GetComponent<Bullet>();
			curHealth -= bullet.damage;
			Vector3 reactVec = transform.position - other.transform.position;
			Destroy(other.gameObject);

			StartCoroutine(OnDamage(reactVec, false));
		}
	}

	public void HitByGrenade(Vector3 explosionPos) {
		curHealth -= 100;
		Vector3 reactVec = transform.position - explosionPos;

		StartCoroutine(OnDamage(reactVec, true));
	}

	IEnumerator OnDamage(Vector3 reactVec, bool isGrenade) {
		mat.color = Color.red;
		yield return new WaitForSeconds(0.1f);

		if (curHealth > 0) {
			mat.color = Color.white;
		}
		else {
			mat.color = Color.gray;
			gameObject.layer = 12;  //Enemy Dead 레이어로 변경

			if (isGrenade) {
				reactVec = reactVec.normalized;
				reactVec += Vector3.up * 5;
				rigid.freezeRotation = false;
				rigid.AddForce(reactVec * 5, ForceMode.Impulse);
				rigid.AddTorque(reactVec * 15, ForceMode.Impulse);
			}
			else {
				reactVec = reactVec.normalized;
				reactVec += Vector3.up;
				rigid.AddForce(reactVec * 5, ForceMode.Impulse);
			}

			Destroy(gameObject, 4);
		}
	}
}
  • Material을 가져올때, Enemy A의 경우 자식 오브젝트 Mesh Object 아래에 있어서 GetComponentInChildren으로 변경해준다.

2. 네비게이션

Navigation 선택
Scene에 몇가지 창이 생기며 Navigation 창이 추가되었다
Bake 창. 아래의 Bake를 눌러준다
NavMesh가 생성되었다

  • NavMesh : NavAgent가 경로를 그리기 위한 바탕. Static 오브젝트만 Bake 가능하다.

자세히 보면 일부는 Bake되지 않았는데, 이는 오른쪽의 Agent 설정 값에 따라서 결정된다.
만약 위 캡처처럼 지형을 추가하거나 변형해도 NavMesh는 변하지 않는다.
새로 지형이 추가되거나 변형되면 Navigation에서 Bake를 한번 더 해주면 그에 맞게 처리해준다.
Enemy A에 Nav Mesh Agent 추가

//Enemy 스크립트 파일

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

public class Enemy : MonoBehaviour {
	public int maxHealth;
	public int curHealth;
	public Transform target;

	Rigidbody rigid;
	BoxCollider boxCollider;
	Material mat;
	NavMeshAgent nav;

	void Awake() {
		rigid = GetComponent<Rigidbody>();
		boxCollider = GetComponent<BoxCollider>();
		mat = GetComponentInChildren<MeshRenderer>().material;
		nav = GetComponent<NavMeshAgent>();
	}

	void Update() {
		nav.SetDestination(target.position);
	}

	void FreezeVelocity() {
		rigid.velocity = Vector3.zero;
		rigid.angularVelocity = Vector3.zero;
	}

	void FixedUpdate() {
		FreezeVelocity();
	}

	void OnTriggerEnter(Collider other) {
		if (other.tag == "Melee") {
			Weapon weapon = other.GetComponent<Weapon>();
			curHealth -= weapon.damage;
			Vector3 reactVec = transform.position - other.transform.position;

			StartCoroutine(OnDamage(reactVec, false));
		}
		else if (other.tag == "Bullet") {
			Bullet bullet = other.GetComponent<Bullet>();
			curHealth -= bullet.damage;
			Vector3 reactVec = transform.position - other.transform.position;
			Destroy(other.gameObject);

			StartCoroutine(OnDamage(reactVec, false));
		}
	}

	public void HitByGrenade(Vector3 explosionPos) {
		curHealth -= 100;
		Vector3 reactVec = transform.position - explosionPos;

		StartCoroutine(OnDamage(reactVec, true));
	}

	IEnumerator OnDamage(Vector3 reactVec, bool isGrenade) {
		mat.color = Color.red;
		yield return new WaitForSeconds(0.1f);

		if (curHealth > 0) {
			mat.color = Color.white;
		}
		else {
			mat.color = Color.gray;
			gameObject.layer = 12;  //Enemy Dead 레이어로 변경

			if (isGrenade) {
				reactVec = reactVec.normalized;
				reactVec += Vector3.up * 5;
				rigid.freezeRotation = false;
				rigid.AddForce(reactVec * 5, ForceMode.Impulse);
				rigid.AddTorque(reactVec * 15, ForceMode.Impulse);
			}
			else {
				reactVec = reactVec.normalized;
				reactVec += Vector3.up;
				rigid.AddForce(reactVec * 5, ForceMode.Impulse);
			}

			Destroy(gameObject, 4);
		}
	}
}
  • NavMeshAgent : Navigation을 사용하는 인공지능 컴포넌트. NavMesh가 없으면 이동할 수 없다.
    • Nav 관련 클래스는 UnityEngine.AI 네임 스페이스를 사용해야한다.
    • .SetDestination(Vector3 target) : 도착할 목표 위치 지정 함수

Enemy A 설정
실행 후 적이 쫓아올 때 Navigation을 통해 보니 Enemy A가 플레이어를 향해 경로를 그리고 쫓아온다
기둥 뒤로 가니 그에 맞게 경로가 휘어진 모습
플레이어와 부딪혀도 속도에 영향을 주지 않는다.


3. 애니메이션

Animator Enemy A 생성 후 Enemy A의 Mesh Object에 추가
에셋의 Enemy A 애니메이션 4개 추가
매개변수 3개 추가 및 Transition 설정

  • bool형 매개변수 isWalk와 isAttack, Trigger형 매개변수 doDie를 추가하고 위 캡처와 같이 Transition을 설정하면 된다. 자세한 Transition 설정은 다음과 같다.
    • 모든 Transition은 Has Exit Time 체크를 해제하고, Transition Duration을 0으로 설정
    • AnyState -> Die : doDie
    • Idle -> Walk : isWalk true
    • Walk -> Idle : isWalk false
    • Walk -> Attack : isAttack true
    • Attack -> Walk : isAttack false
//Enemy 스크립트 파일

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

public class Enemy : MonoBehaviour {
	public int maxHealth;
	public int curHealth;
	public Transform target;
	public bool isChase;

	Rigidbody rigid;
	BoxCollider boxCollider;
	Material mat;
	NavMeshAgent nav;
	Animator anim;

	void Awake() {
		rigid = GetComponent<Rigidbody>();
		boxCollider = GetComponent<BoxCollider>();
		mat = GetComponentInChildren<MeshRenderer>().material;
		nav = GetComponent<NavMeshAgent>();
		anim = GetComponentInChildren<Animator>();

		Invoke("ChaseStart", 2);
	}

	void ChaseStart() {
		isChase = true;
		anim.SetBool("isWalk", true);
	}

	void Update() {
		if (isChase)
			nav.SetDestination(target.position);
	}

	void FreezeVelocity() {
		if (isChase) {
			rigid.velocity = Vector3.zero;
			rigid.angularVelocity = Vector3.zero;
		}
	}

	void FixedUpdate() {
		FreezeVelocity();
	}

	void OnTriggerEnter(Collider other) {
		if (other.tag == "Melee") {
			Weapon weapon = other.GetComponent<Weapon>();
			curHealth -= weapon.damage;
			Vector3 reactVec = transform.position - other.transform.position;

			StartCoroutine(OnDamage(reactVec, false));
		}
		else if (other.tag == "Bullet") {
			Bullet bullet = other.GetComponent<Bullet>();
			curHealth -= bullet.damage;
			Vector3 reactVec = transform.position - other.transform.position;
			Destroy(other.gameObject);

			StartCoroutine(OnDamage(reactVec, false));
		}
	}

	public void HitByGrenade(Vector3 explosionPos) {
		curHealth -= 100;
		Vector3 reactVec = transform.position - explosionPos;

		StartCoroutine(OnDamage(reactVec, true));
	}

	IEnumerator OnDamage(Vector3 reactVec, bool isGrenade) {
		mat.color = Color.red;
		yield return new WaitForSeconds(0.1f);

		if (curHealth > 0) {
			mat.color = Color.white;
		}
		else {
			mat.color = Color.gray;
			gameObject.layer = 12;  //Enemy Dead 레이어로 변경
			isChase = false;
			nav.enabled = false;
			anim.SetTrigger("doDie");

			if (isGrenade) {
				reactVec = reactVec.normalized;
				reactVec += Vector3.up * 5;
				rigid.freezeRotation = false;
				rigid.AddForce(reactVec * 5, ForceMode.Impulse);
				rigid.AddTorque(reactVec * 15, ForceMode.Impulse);
			}
			else {
				reactVec = reactVec.normalized;
				reactVec += Vector3.up;
				rigid.AddForce(reactVec * 5, ForceMode.Impulse);
			}

			Destroy(gameObject, 4);
		}
	}
}

테스트를 위해서 Enemy와 아이템을 추가 배치
실행하니 적들의 발이 움직이면서 쫓아온다
총을 맞추니 빨간색이 되고
총으로 죽이니 색이 조금 어두워지면서 사망 모션이 나온다
수류탄으로 처치. 적들이 날아간다