적은 정찰, 쫓기, 공격 3가지 상태를 가진다.
코루틴을 이용해 0.25초마다 상태를 업데이트 한다.
using UnityEngine.AI;
using UnityEngine;
using System.Collections;
public class EnemyAI : LivingEntity
{
public NavMeshAgent agent;
public Transform playerPos;
public Player player;
public float damage = 1; //좀비가 주는 데미지
public LayerMask whatIsGround, whatIsPlayer, whatIsObstacle;
GameObject gameManager;
// 모델링 넣기 전 테스트 용으로 넣은 코드
Material skinMaterial; //적의 메테리얼
Color OriginColor;
float refreshRate = 0.25f; //path 업데이트 갱신 간격
//Patroling
public Vector3 walkPoint; //위치
bool walkPointSet = false; //해당 위치가 이미 세팅 되어있는지 확인
public float walkPointRange; //범위
//Chasing
bool setChase = false;
//Attacking
//bool setAttack;
public float timeBetweenAttacks;
bool alreadyAttacked;
//States
public float sightRange, attackRange;
public bool playerInSightRange, playerInAttackRange;
//충돌범위
float myCollisionRadius; //적의 충돌범위
float targetCollisionRadius;// 타겟 충돌범위
private void Awake()
{
}
protected override void Start()
{
base.Start();
gameManager = GameObject.Find("GameManager");
dead = false;
//플레이어 위치 할당
playerPos = GameObject.Find("Player(Clone)").transform;
player.dead = false;
//nav mesh agent 할당
agent = GetComponent<NavMeshAgent>();
//적과 플레이어 겹침 방지 위해
myCollisionRadius = GetComponent<CapsuleCollider>().radius;
targetCollisionRadius = playerPos.GetComponent<CapsuleCollider>().radius;
//상태 변화 시 색상 변화 위해
skinMaterial = GetComponent<Renderer>().material;
OriginColor = skinMaterial.color;
//Player의 초기 체력 값이 자꾸 스크립트로 리셋이 안되어서 Enemy 쪽에서 강제로 세팅 -> 추후 수정 필요
player.setHealth(player.startingHealth);
StartCoroutine(UpdatePath());
}
private void Update()
{
if (gameManager.GetComponent<GameManager>().isClear)
{
this.gameObject.SetActive(false);
}
}
IEnumerator UpdatePath()
{
Debug.Log("enemy updatePath");
Debug.Log(player.getHealth());
while (playerPos!=null && !player.GetComponent<Player>().dead)
{
//Debug.Log(!player.GetComponent<Player>().dead);
//Debug.Log(!dead);
if (!dead)
{
//Debug.Log("Player exist");
//시야와 공격범위 확인
playerInSightRange = Physics.CheckSphere(transform.position, sightRange, whatIsPlayer);
//Debug.Log(playerInSightRange);
playerInAttackRange = Physics.CheckSphere(transform.position, attackRange, whatIsPlayer);
//Debug.Log(playerInAttackRange);
if (!playerInSightRange && !playerInAttackRange && !setChase)
Patroling();
if ((playerInSightRange && !playerInAttackRange) || setChase)
ChasePlayer();
if ((playerInAttackRange && playerInSightRange))
{
setChase = false;
AttackPlayer();
}
if (gameManager.GetComponent<GameManager>().isClear)
{
this.gameObject.SetActive(false);
}
}
yield return new WaitForSeconds(refreshRate);
}
}
private void Patroling()
{
skinMaterial.color = OriginColor;
//Debug.Log("Patroling");
if (!walkPointSet)
SearchWalkPoint();
if (walkPointSet)
{
agent.SetDestination(walkPoint);
Vector3 distanceToWalkPoint = transform.position - walkPoint;
//Walkpoint에 도달 (거리 1 미만일 때)
if (distanceToWalkPoint.magnitude < 0.1f)
walkPointSet = false;
}
}
private void ChasePlayer()
{
setChase = true;
if (setChase)
{
//chasing할 때 색 바뀌도록 -> 모델링 입히기 전 테스트 위해
skinMaterial.color = Color.yellow;
//원래는 Collider의 Is Trigger를 체크하고 콜라이더 범위만큼 빼고 그 앞까지 오는 걸로 했었다
//그런데 계속 오류나서 Is Trigger를 해제하고 플레이어 위치로 찾아오도록 변경
//Vector3 dirToTarget = (playerPos.position - transform.position).normalized;
//Vector3 targetPosition = playerPos.position - dirToTarget * (myCollisionRadius + targetCollisionRadius);
walkPoint = new Vector3(playerPos.position.x, transform.position.y, playerPos.position.z);
if (agent.SetDestination(walkPoint))
{
Debug.Log("Follow Player");
}
}
}
private void AttackPlayer()
{
skinMaterial.color = Color.red;
Debug.Log("Attack");
setChase = false;
//플레이어 계속 추적
//agent.SetDestination(playerPos.position);
Vector3 dirToTarget = (playerPos.position - transform.position).normalized;
Vector3 targetPosition = playerPos.position - dirToTarget * (myCollisionRadius + targetCollisionRadius);
agent.SetDestination(targetPosition);
transform.LookAt(playerPos);
//Attack
if (!alreadyAttacked) {
Debug.Log("Attacking....");
//Attack Code 여기에
//Attack();
//IDamageble damagebleObject = player.GetComponent<IDamageble>();
if (player.dead != true)
{
//Attack();
//Debug.Log("Attacking....");
Debug.Log(player.getHealth());
player.TakeHit(damage);
Debug.Log(player.getHealth());
}
alreadyAttacked = true;
Invoke(nameof(ResetAttack), timeBetweenAttacks);
}
}
//랜덤으로 순찰할 위치 선정
private void SearchWalkPoint()
{
float randomZ = Random.Range(-walkPointRange, walkPointRange);
float randomX = Random.Range(-walkPointRange, walkPointRange);
//랜덤하게 패트롤할 위지 범위 안에서 선청
walkPoint = new Vector3(transform.position.x + randomX, transform.position.y, transform.position.z + randomZ);
//맵밖으로 안나갔는지 확인
if (Physics.Raycast(walkPoint, -transform.up, 2f, whatIsGround))
{
Vector3 tmpPoint = new Vector3(walkPoint.x, 20, walkPoint.z);
if (!Physics.Raycast(tmpPoint, -transform.up, Mathf.Infinity, whatIsObstacle))
{
walkPointSet = true;
// return;
}
}
else
{
Debug.Log("Can't find");
//SearchWalkPoint();
}
}
protected override void Die()
{
base.Die();
gameManager.GetComponent<GameManager>().IncreaseKillCount();
Destroy(this.gameObject);
}
private void ResetAttack()
{
alreadyAttacked = false;
}
//Sight Range, AttackRange를 EDITOR에서 확인하기 위해
#if UNITY_EDITOR
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, attackRange);
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, sightRange);
}
#endif
}
이미 만들어진 Asset을 어떻게 부착해야할 지 몰라서 조금 헤멨다.
처음에는 미리 만들어둔 Enemy Prefab에 Asset에 있는 zombie prefab 요소를 복사해 붙여넣었는데, 애니메이션 컨트롤러랑 매핑이 안된건지(추측) 애니메이션이 동작하지 않았다.
그래서 Asset에 있는 zombie prefab을 그대로 가져와 거기에 Enemy 스크립트와 Nav Mesh Agent를 붙였다.
그리고 그냥 Mesh render로는 Material이 입혀지지 않아서 Skinned Mesh Renderer를 이용했다.
Unity는 메시의 모양이 미리 정의된 애니메이션 시퀀스에 따라 변형되는 Bone 애니메이션의 렌더링을 위해 Skinned Mesh Renderer 컴포넌트를 사용합니다. 캐릭터 뿐만 아니라 (조인트가 힌지처럼 기능하는 기계와 달리) 조인트가 구부러지는 다른 오브젝트에도 유용한 기법입니다.
= 적이 정찰 및 체이싱을 하지 않는 문제
이건 콜라이더 및 nav mesh agent의 문제였다.
적의 콜라이더와 바닥 콜라이더가 충돌해 적이 바닥에 붙어있지 않아 nav mesh agent가 해당 루트로 나아가지 못했다.
맵에 붙어있는 콜라이더가 초기화 될 때 0.5의 높이를 가지게 스크립트를 짠 것이 원인으로 예상된다.
또한 콜라이더의 Is Trigger를 체크한 상태에서 플레이어와 적의 지름을 뺀 만큼 적을 플레이어 위치로 이동시켜줬는데, 그것도 계속 오류가 나서 Is Trigger를 해제하고 플레이어 위치로 적이 이동하도록 수정했다.
사실 이건 해결하고 보니 Enemy쪽 문제가 아니라 Player 스크립트 문제였지만, Enemy에서 임시변통으로 수정했기 때문에 여기에 쓴다.
Attack 함수를 호출 했을 때 플레이어가 맞고 바로 죽어버려서, 로그를 찍어봤더니 플레이어가 죽고 다시 생성됐을 때 초기 체력값으로 초기화가 안된 것이 원인이었다.
하지만 플레이어와 적을 prefab으로 만들어서 그런지 계속 동기화가 안되어서 임시방편으로 Enemy가 생성될 때 강제로 플레이어 체력을 초기화해주었다.