지상을 다니는 AI는 NavMesh를 이용했지만 공중 AI는 보통 NavMesh를 쓰지 않습니다.
이 게시글처럼 Transform을 직접 조작하는 것으로 AI를 구현하게 됩니다.
//Enemy.cs
public AIType aiType;
public enum AIType
{
None,
Ground,
Flying,
Swimming
}
// Enemy.cs
public void PerformPatrol()
{
switch(aiType)
{
case AIType.Ground:
patrolCoroutine = StartCoroutine(WaitForPatrolCoroutine());
break;
case AIType.Flying:
patrolCoroutine = StartCoroutine(WaitForPatrolCoroutineFlying());
break;
case AIType.Swimming:
break;
default:
break;
}
}
IEnumerator WaitForPatrolCoroutineFlying()
{
while (true)
{
// 현재 목표 위치
Vector3 targetPosition = SetNewRandomPosition();
while (Vector3.Distance(transform.position, targetPosition) > 0.5f)
{
// 목표 위치와 현재 위치를 연결하는 방향
Vector3 direction = (targetPosition - transform.position).normalized;
if (direction != Vector3.zero)
{
// 해당 방향을 바라보게됨
Quaternion targetRotation = Quaternion.LookRotation(direction);
// Slerp로 부드럽게 바라봄
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, enemyData.rotationSpeed * Time.deltaTime);
}
// MoveTowards는 (a,b,c)일때, a부터 b까지 c의 속도로 이동해주는 함수
transform.position = Vector3.MoveTowards(transform.position, targetPosition, enemyData.moveSpeed * Time.deltaTime);
yield return null;
}
// 도착할 경우 2초 대기
yield return new WaitForSeconds(2f);
}
}
private Vector3 SetNewRandomPosition()
{
Vector3 randomDirection = UnityRandom.insideUnitSphere * patrolRadius;
randomDirection += centerPoint;
return randomDirection;
}
// ChaseState.cs
private void Moving()
{
if (enemy.IsInCenter(enemy.transform))
{
switch(enemy.aiType)
{
case AIType.Ground:
enemy.agent.destination = enemy.target.transform.position;
break;
case AIType.Flying:
FlyingTypeMoving();
break;
case AIType.Swimming:
break;
default:
break;
}
}
}
private void FlyingTypeMoving()
{
Vector3 direction = (enemy.target.transform.position - enemy.transform.position).normalized;
if (direction != Vector3.zero)
{
Quaternion targetRotation = Quaternion.LookRotation(direction);
enemy.transform.rotation = Quaternion.Slerp(enemy.transform.rotation, targetRotation, enemy.enemyData.rotationSpeed * Time.deltaTime);
enemy.transform.position = Vector3.MoveTowards(enemy.transform.position, enemy.target.transform.position, enemy.enemyData.moveSpeed * Time.deltaTime);
}
}
Attackstate에선 agent.isStopped를 GroundType만 적용하게 바꿔주세요. 그리고 EnemyAttackSystem도 수정합시다.
// EnemyAttackSystem.cs
public void PerformAttackByName(string name)
{
switch(name)
{
case "Spitter":
ThrowProjectile(enemy.attackTransform);
break;
case "Bomber":
DropBomb(enemy.attackTransform);
break;
default:
break;
}
}
private void DropBomb(Transform attackTransform)
{
Debug.Log("폭탄 투하!!!");
}
public void LookAtPlayer()
{
Vector3 direction = (enemy.target.transform.position - transform.position).normalized;
direction.y = 0;
Quaternion lookRotation = Quaternion.LookRotation(direction);
switch(enemy.aiType)
{
case AIType.Ground:
transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, enemy.agent.angularSpeed * Time.deltaTime);
break;
case AIType.Flying:
transform.rotation = Quaternion.Slerp(enemy.transform.rotation, lookRotation, enemy.enemyData.rotationSpeed * Time.deltaTime);
break;
case AIType.Swimming:
break;
default:
break;
}
}
순찰 중일때 공중에서 자유롭게 다닙니다.
공격 범위내에 있으면 폭탄을 투하하고, 범위에서 벗어날땐 ChaseState인 것을 볼 수 있습니다.
게임에 자주 나오는 유도 기능입니다. 탄환이 플레이어를 쫓아가고, 일정 시간이 지나면 공중에서 터지거나 플레이어에 닿으면 데미지를 입고 터지죠. 구현해볼까요?
다른 오브젝트에도 쓰일 수도 있으니, 함수명을 ThrowGuidedMissile로 바꿉시다.
// EnemyAttackSystem.cs
public void PerformAttackByName(string name)
{
switch(name)
{
case "Spitter":
ThrowProjectile(enemy.attackTransform);
break;
case "Bomber":
// 이미 발사한 미사일이 있다면 사라질때까지 스폰하지 않음
if (!bomberMissile)
{
ThrowGuidedMissile(enemy.attackTransform);
}
break;
default:
break;
}
}
private void ThrowGuidedMissile(Transform attackTransform)
{
Debug.Log("폭탄 투하!!!");
bomberMissile = Instantiate(enemy.projectilePrefab, attackTransform.position, attackTransform.rotation);
ProjectileManager projectileManager = bomberMissile.GetComponent<ProjectileManager>();
if (projectileManager)
{
projectileManager.damage = enemy.enemyData.damage;
projectileManager.startFollow = true;
projectileManager.enemy = enemy;
}
}
// ProjectileManager.cs
// 총알 유지 시간
public float lifeTime;
[HideInInspector] public float damage = 10f;
[HideInInspector] public bool startFollow = false;
[HideInInspector] public Enemy enemy;
private float currentTime = 0f;
private void Update()
{
if (startFollow)
{
currentTime += Time.deltaTime;
if (currentTime >= lifeTime)
{
Destroy(gameObject);
return;
}
FollowTarget();
}
}
public void FollowTarget()
{
Vector3 direction = (enemy.target.transform.position - transform.position).normalized;
if (direction != Vector3.zero && enemy)
{
Quaternion targetRotation = Quaternion.LookRotation(direction);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, enemy.projectileSpeed * Time.deltaTime);
// 유도탄이 발밑으로 가지 않기 위해 + Vector3.up * 1.5f 를 해줌
transform.position = Vector3.MoveTowards(transform.position, enemy.target.transform.position + Vector3.up * 1.5f, enemy.projectileSpeed * Time.deltaTime);
}
}
공격 범위내에 들어오면 비교적 느린 속도로 유도탄이 날아오고, 일정 시간이 지나면 제자리에서 터지게끔 사라집니다.
나중에 에셋을 적용시키고 더 느리게 한 후, 비틀거리는 애니메이션을 추가한다면 더 보기좋을것 같네요!
다음 글에서는: