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

주예성·2025년 7월 10일
post-thumbnail

📋 목차

  1. 몹 데이터 설정
  2. 적 생성
  3. Enemy 설정
  4. 중간 결과
  5. 여러 적 스폰
  6. 최종 결과
  7. 오늘의 배운 점
  8. 다음 계획

👾 몹 데이터 설정

EnemyData를 담아두는 Resources -> Enemys에 특성에 따른 폴더를 제작할겁니다. 전 Aerial(비행), Ground(대지), Special(특이) 로 구분할겁니다. 일단 예시로 몇개만 생성해볼까요?

Aerial

이름유형설명
Bomber폭격형상공에서 폭탄 투하
Drone정찰형플레이어 추적 및 다른 적 소환
Swarm떼공격형작지만 수십 마리가 몰려옴

Ground

이름유형설명
Brute거대형느리지만 강력한 공격력
Crawler기어다니는 벌레형빠르고 약하며 떼로 몰려옴
Spitter원거리형산성 탄환 발사
Stalker은밀형투명화 능력, 뒤에서 기습

공격력 및 체력 등 자세한 것은 임의로 설정했으므로 생략합니다.
Special은 나중에 다루겠습니다!


👾 적 생성

이전에 계속 시험했던 Enemy는 Brute로 설정하고 진행하겠습니다. 이 몹은 Forest(숲지형) 타입으로 숲에서만 스폰하게 할 겁니다. 그렇다면 만들었던 ZoneManager에서 숲지형의 Transform을 넣고 그곳에서만 생성되게 해야겠죠?

// ZoneManager.cs

[Header("Forest Zone")]
public Transform[] forestTransforms;

숲 지형이며 몹이 스폰되는 곳은 여러곳일테니 Transform형식의 배열로 선언합니다. 그리고 Hierarchy에서 Map의 자식에 빈 오브젝트의 Forest를 넣고 스폰을 원하는 위치로 옮겨주세요. 그리고 ZoneManager의 Inspector에 집어넣습니다.

이제 영역은 지정했으니 스폰 로직이 필요합니다. LivingSpawner 스크립트를 생성해주세요.

// LivingSpawner.cs

using UnityEngine;
using UnityEngine.AI;
using UnityRandom = UnityEngine.Random;

public class LivingSpawner : MonoBehaviour
{
    [Header("Spawner Zone")]
    public ZoneManager zoneManager;

    [Header("Spawn Settings")]
    private float spawnRadius = 10f;
           
    public void SpawnEnemy(Enemy enemy)
    {
        if (!zoneManager || !enemy) return;
        GameObject enemyObject = Instantiate(enemy.enemyData.enemyPrefab);
        Vector3 randomDirection = UnityRandom.insideUnitSphere * spawnRadius;
        randomDirection += enemy.centerPoint;
        NavMeshHit hit;
        NavMesh.SamplePosition(randomDirection, out hit, spawnRadius, NavMesh.AllAreas
    }
}

// GameManager.cs

[Header("Scripts")]
public LivingSpawner livingSpawner;

// 테스트용으로 Inspector에서 Enemy를 하나만 추가합시다
public EnemyData[] allEnemies;

private void Start()
{
	if (allEnemies.Length > 0 && livingSpawner)
	{
    	Enemy enemy = Enemy.Create(allEnemies[0]);
    	livingSpawner.SpawnEnemy(enemy);
	}
}

// Enemy.cs

// 스폰된 후 Inspector에서 넣은 AttackPoint Collider가 None일 경우를 대비함
private void Awake()
{
    if (!attackPoint)
    {
        attackPoint = GetComponentInChildren<Collider>();
        if (attackPoint)
        {
            attackPoint.enabled = false; // 공격 콜라이더 비활성화
        }
        else
        {
            Debug.LogError("Attack Point Collider가 설정되지 않았습니다!");
        }
    }
}
	

⚙️ Enemy 설정

전에 맵을 설치하면서 Enemy AI가 약간씩 고장나는 것을 확인할 수 있죠. 그 이유는 플레이어의 벽 오르기 버그를 해결하기 위해 임시 벽 Collider를 설치했는데, Enemy가 그곳을 지나려고 할때 발생하는 문제입니다.

1. Layer 설정

Enemy는 NavMesh의 영역에서만 이동하는데, 그것을 Collider가 막아버리면 Enemy는 지나가지 못합니다. 그러니 Enemy Prefab의 Inspector -> Layer에 들어가 Add Layer를 합니다. 그 안에 EnemyWall을 추가합니다.

Enemy는 Enemy를, 구조물들의 Collider에는 Wall로 Layer를 설정합니다.

2. Layer Collision Matrix 설정

Edit -> Project Settings -> Physics -> Layer Collision Matrix에 들어갑니다. 그렇다면 Layer들이 있는 체크박스를 볼 수가 있습니다. 거기서 Wall / Enemy 체크박스를 해제해주세요.

3. 충돌 제거

Enemy는 AI이기 때문에 물리 판정이 대체로 필요가 없습니다. 그러니 해당 프리팹의 Inspector에서 Rigidbody의 Is Kinematictrue로 변경합시다.


🎮 중간 결과

확인을 위해 코드를 약간 변경하여 적들이 여러개 스폰되게 했습니다. 특정 지역을 중심으로 랜덤한 곳에서 스폰되고, 각자 이동하는 것을 확인할 수 있습니다.


👾 여러 적 스폰

게임에는 한가지의 적만 있지는 않죠. 여러 적들이 있을 겁니다.
이전에 아이템 정보를 배열에 담았듯이 적도 그럴 필요가 있습니다. 그래서 GameManager에서 enemyData 배열을 만들었죠!! EnemyData에서도 environmentType이라는 해당 적이 사는 지역에 대한 정보도 담았습니다. 그것을 기반으로 해볼까요?

1. GameManager에 Enemy 종류 저장

// EnvironmentType.cs

public enum EnvironmentType
{
    None,	
    Forest,	// 숲
    Desert, // 사막
    Cave,	// 동굴
    Beach,	// 해변
}

// GameManager.cs

public ZoneManager zoneManager;

public EnemyData[] allEnemies;

private void Start()
{
	GetAllEnemies();
	zoneManager.SpawnEnemyByZone(allEnemies);
}

private void GetAllEnemies()
{
    allEnemies = Resources.LoadAll<EnemyData>("Enemies");
    Array.Sort(allEnemies, (a, b) => a.enemyID - b.enemyID);
}

2. Zone별 Enemy 스폰

// zoneManager.cs

using UnityEngine;

public class ZoneManager : MonoBehaviour
{
    [Header("Scripts")]
    public LivingSpawner livingSpawner;

    [Header("Forest Zone")]
    public GameObject[] forestZones;

    [Header("Desert Zone")]
    public GameObject[] desertZones;

    [Header("Cave Zone")]
    public GameObject[] caveZones;

    [Header("Beach Zone")]
    public GameObject[] beachZones;


    public void SpawnEnemyByZone(EnemyData[] datas)
    {
        if (datas.Length <= 0 || !livingSpawner) return;

        int count = 0;
        Vector3 point = Vector3.zero;

        foreach (EnemyData data in datas)
        {
            if (data.environmentType == EnvironmentType.Forest)
            {
                foreach (GameObject zone in forestZones)
                {
                    count = zone.GetComponent<ZoneController>().SpawnCount(EnvironmentType.Forest);
                    for (int i = 0; i < count; i++)
                    {
                        Debug.Log("Forest 에서 생성!");
                        Enemy enemy = Enemy.Create(data);
                        point = zone.GetComponent<Transform>().position;
                        livingSpawner.SpawnEnemyRandomPosition(enemy, point);
                    }
                }
            }
            else if (data.environmentType == EnvironmentType.Desert)
            {
                foreach (GameObject zone in desertZones)
                {
                    count = zone.GetComponent<ZoneController>().SpawnCount(EnvironmentType.Desert);
                    for (int i = 0; i < count; i++)
                    {
                        Debug.Log("Desert 에서 생성!");
                        Enemy enemy = Enemy.Create(data);
                        point = zone.GetComponent<Transform>().position;
                        livingSpawner.SpawnEnemyRandomPosition(enemy, point);
                    }
                }
            }
            else if (data.environmentType == EnvironmentType.Cave)
            {
                foreach (GameObject zone in caveZones)
                {
                    count = zone.GetComponent<ZoneController>().SpawnCount(EnvironmentType.Cave);
                    for (int i = 0; i < count; i++)
                    {
                        Enemy enemy = Enemy.Create(data);
                        point = zone.GetComponent<Transform>().position;
                        livingSpawner.SpawnEnemyRandomPosition(enemy, point);
                    }
                }
            }
            else if (data.environmentType == EnvironmentType.Beach)
            {
                foreach (GameObject zone in beachZones)
                {
                    count = zone.GetComponent<ZoneController>().SpawnCount(EnvironmentType.Beach);
                    for (int i = 0; i < count; i++)
                    {
                        Enemy enemy = Enemy.Create(data);
                        point = zone.GetComponent<Transform>().position;
                        livingSpawner.SpawnEnemyRandomPosition(enemy, point);
                    }
                }
            }
        }
    }
}

4. Enemy 데이터 및 오브젝트 생성

// Enemy.cs

public static Enemy Create(EnemyData data)
{
    GameObject obj = Instantiate(data.enemyPrefab);
    Enemy enemy = obj.GetComponent<Enemy>();
    enemy.enemyData = data;
    return enemy;
}

// LivingSpawner.cs

[Header("Spawner Zone")]
public ZoneManager zoneManager;

[Header("Spawn Settings")]
private float spawnRadius = 10f;
       
public void SpawnEnemyRandomPosition(Enemy enemy, Vector3 centerPoint)
{
    if (!zoneManager || !enemy) return;
    
    Vector3 randomDirection = UnityRandom.insideUnitSphere * spawnRadius;
    randomDirection += centerPoint;
	enemy.centerPoint = centerPoint;
    NavMeshHit hit;
    
    if(NavMesh.SamplePosition(randomDirection, out hit, spawnRadius, NavMesh.AllAreas))
	{
    	enemy.transform.position = hit.position;
	}
}

🎮 최종 결과

맵에 각 지형의 위치를 빈 오브젝트로 설정하고, EnemyData에 사는 지형을 지정했습니다. 그럼 해당 Enemy가 Min~Max까지의 수로, 해당 지역 주위에 랜덤으로 스폰되는 모습입니다.


📚 오늘의 배운 점

  • Layer 활용
  • 조건에 따른 랜덤 스폰

🎯 다음 계획

다음 글에서는:

  1. 투사체 공격 구현
profile
Unreal Engine & Unity 게임 개발자

0개의 댓글