[Unity / C#] 적 만들기 #5

주예성·2025년 7월 11일

📋 목차

  1. 날아다니는 AI
  2. 중간 결과
  3. 유도 미사일
  4. 최종 결과
  5. 오늘의 배운 점
  6. 다음 계획

🪽 날아다니는 AI

지상을 다니는 AI는 NavMesh를 이용했지만 공중 AI는 보통 NavMesh를 쓰지 않습니다.
게시글처럼 Transform을 직접 조작하는 것으로 AI를 구현하게 됩니다.

1. 타입 구분

//Enemy.cs

public AIType aiType;

public enum AIType
{
	None,
    Ground,
    Flying,
    Swimming
}

2. 이동 구현

// 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;
}

3. ChaseState 수정

// 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);
    }
}

4. AttackState 수정

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);
    }
}

🎮 최종 결과

공격 범위내에 들어오면 비교적 느린 속도로 유도탄이 날아오고, 일정 시간이 지나면 제자리에서 터지게끔 사라집니다.
나중에 에셋을 적용시키고 더 느리게 한 후, 비틀거리는 애니메이션을 추가한다면 더 보기좋을것 같네요!


📚 오늘의 배운 점

  • MoveTowards를 이용한 공중 이동
  • 유도탄 구현

🎯 다음 계획

다음 글에서는:

  1. 손전등 구현
  2. 아이템을 든 모션 구현
    • 손전등
    • 나이프
profile
Unreal Engine & Unity 게임 개발자

0개의 댓글