Unreal - 몬스터 웨이브 만들기

2025. 5. 17. 19:13·Unreal 프로젝트 다이어리/첫번째 프로젝트

던전에 입장하면 몬스터들이 차례대로 나오고 던전맵에서 AI 몬스터들이 차례대로 스폰되는

웨이브스폰을 구현해보도록 하겠습니다

 

게임모드에서 구현하겠습니다

던전에 입장하면 해당 던전의 클리어 퍼센트 UI가 좌측상단에 표시되며

처음에 스폰시킨 AI의 일정마리수 이상을 잡으면 다음 몬스터가 스폰되도록하겠습니다

 

먼저 ai가 죽는 카운트를 알아야합니다

게임모드에 카운트하는 변수를 선언

 

스폰지점을 월드좌표의 스폰되는 중앙지점을 구합니다

월드좌표 1108, -1200, 107 로 설정하였습니다

 

스폰위치를 구해 랜덤위치 랜덤좌표로 스폰하는데

플레이어 주변 300만큼에서는 스폰을 하지않고 그 외의 오프셋을 적용한 랜덤위치, 랜덤방향에서

스폰하겠습니다

Property에서 skeleton1 ~ 5 에사용할 AI도 할당시켜주었습니다

 

 

.h입니다

다음 웨이브로 넘어가기위한 need변수 그리고 스폰시킬 count변수등을 선언

//WAVE
int32 skeletonDieCount;

UPROPERTY(EditAnywhere, Category = "MySettings")
int32 wave2NeedCount = 7;

UPROPERTY(EditAnywhere, Category = "MySettings")
int32 wave3NeedCount = 15;

UPROPERTY(EditAnywhere, Category = "MySettings")
int32 wave4NeedCount = 20;

UPROPERTY(EditAnywhere, Category = "MySettings")
int32 wave5NeedCount = 28;

UPROPERTY()
FOnSkeletonDieCountChanged onSkeletonDieCountChanged;

UFUNCTION()
void waveGeneration(int32 newCount);

UFUNCTION()
void increaseSkeletonDieCount();

UPROPERTY(EditAnywhere, Category = "MySettings")
bool UseMonsterWave = false;

UPROPERTY(EditAnywhere, Category = "MySettings")
TSubclassOf<ABasicSkeletonEnemy> skeleton1;

UPROPERTY(EditAnywhere, Category = "MySettings")
TSubclassOf<ABasicSkeletonEnemy> skeleton2;

UPROPERTY(EditAnywhere, Category = "MySettings")
TSubclassOf<ABasicSkeletonEnemy> skeleton3;

UPROPERTY(EditAnywhere, Category = "MySettings")
TSubclassOf<ABasicSkeletonEnemy> skeleton4;

UPROPERTY(EditAnywhere, Category = "MySettings")
TSubclassOf<ABasicSkeletonEnemy> skeleton5;

UFUNCTION()
void SpawnWave(TArray<TSubclassOf<ABasicSkeletonEnemy>>& availableTemplates);

UPROPERTY()
TArray<TSubclassOf<class ABasicSkeletonEnemy>> currentTemplates;

int32 spawnCount;

UPROPERTY(EditAnywhere, Category = "MySettings")
int32 wave1SpawnCount = 10;

UPROPERTY(EditAnywhere, Category = "MySettings")
int32 wave2SpawnCount = 7;

UPROPERTY(EditAnywhere, Category = "MySettings")
int32 wave3SpawnCount = 5;

UPROPERTY(EditAnywhere, Category = "MySettings")
int32 wave4SpawnCount = 11;

UPROPERTY(EditAnywhere, Category = "MySettings")
int32 wave5SpawnCount = 13;

 

스폰함수입니다

 

스폰위치중앙을 기점으로 (1108, -1200, 107) ->[UPROPERTY로 지정한 center spawnArea위치에서 스폰을 하게됩니다

단 플레이어와 너무 가깝게 스폰되면 안되는걸 위해서

플레이어기준 거리 300보다 작으면 스폰이 되지않게 하였습니다

스폰이되는 대상은 매개변수로 받는 availableTemplates입니다

void ASwordPlayerGameBase::SpawnWave(TArray<TSubclassOf<ABasicSkeletonEnemy>>& availableTemplates)
{
	//FVector centerLoc(1108.f, -1200.f, 107.87f);
	float spawnArea = 600.f;
	UWorld* dungeonWorld = GetWorld();
	int32 spawnedCount = 0;

	while (spawnedCount < spawnCount)
	{
		FVector spawnLoc;
		float distanceToPlayer = 0.f;
		while (true)
		{
			float randAngle = FMath::FRandRange(0.0f, 2 * PI);
			float randDistance = FMath::FRandRange(0.0f, spawnArea);
			FVector Offset = FVector(FMath::Cos(randAngle), FMath::Sin(randAngle), 0.f) * randDistance;
			spawnLoc = centerLoc + Offset;

			distanceToPlayer = FVector::Dist(spawnLoc, player->GetActorLocation());
			if (distanceToPlayer >= 300.f)
			{
				break;
			}
		}

		int32 randomChoiceIndex = FMath::RandRange(0, availableTemplates.Num() - 1);
		TSubclassOf<ABasicSkeletonEnemy> chosenTemplate = availableTemplates[randomChoiceIndex];

		FRotator spawnRot(0.0f, FMath::FRandRange(0.0f, 360.0f), 0.f);
		dungeonWorld->SpawnActor<ABasicSkeletonEnemy>(chosenTemplate, spawnLoc, spawnRot);

		++spawnedCount;
	}
}

 

여기서 만약 그냥 스폰하게 된다면 AI의 컨트롤러가 없는 상태에서 스폰이되는 일이 발생합니다

 

AI의 생성자 에서 반드시 이런식으로 컨트롤러를 오토포세스 를 해야지만 

맵에 스폰되었을때 AIController가 반드시 할당됩니다

 

AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
AIControllerClass = ASkeletonAIController::StaticClass();

 

 

 

델리게이트 코드로 웨이브를 호출하는 방식으로 변경해보도록하겠습니다

 

다음웨이브에 필요한 wave(N)needCount변수 만들어두고

델리게이트에 사용할 onSkeletonDieCountChange와 나머지 함수를 만들어주겠습니다

//WAVE
int32 skeletonDieCount;

UPROPERTY(EditAnywhere, Category = "MySettings")
int32 wave2NeedCount = 7;

UPROPERTY(EditAnywhere, Category = "MySettings")
int32 wave3NeedCount = 15;

UPROPERTY(EditAnywhere, Category = "MySettings")
int32 wave4NeedCount = 20;

UPROPERTY(EditAnywhere, Category = "MySettings")
int32 wave5NeedCount = 28;

UPROPERTY()
FOnSkeletonDieCountChanged onSkeletonDieCountChanged;

UFUNCTION()
void waveGeneration(int32 newCount);

UFUNCTION()
void increaseSkeletonDieCount();

 

AI가 죽을때 죽음을 담당하는 태스크에서 함수 호출하는걸로 변경하겠습니다

gameMode = Cast<ASwordPlayerGameBase>(UGameplayStatics::GetGameMode(this));
gameMode->increaseSkeletonDieCount();

 

게임모드의 increaseSkeletonDieCount함수가 호출이됩니다

게임모드의 .h에서 델리게이트함수를 작성합니다 델리게이트작성은 UCLASS 위에 작성합니다

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSkeletonDieCountChanged, int32, newCount);

 

해당 델리게이트의 사용을 위해 beginplay에서 바인드 해주겠습니다

waveGeneration함수와 델리게이트 바인딩을 해주겠습니다

onSkeletonDieCountChanged.AddDynamic(this, &ASwordPlayerGameBase::waveGeneration);

 

 

TASK에서 호출되는 increaseSkeletonDieCount함수를 만들어줍니다

해당 함수에서는 skeletonDieCount를 증가시키며

이 델리게이트가 Broadcast 하여 알리고 SecondWave함수를 실행해라 라는 의미입니다

void ASwordPlayerGameBase::increaseSkeletonDieCount()
{
	skeletonDieCount++;
	onSkeletonDieCountChanged.Broadcast(skeletonDieCount);
}

 

현재 로직의 순서는 이렇습니다 

1. AI의 죽음을 담당하는 Task에서 게임모드의 increaseSkeletonDieCount()함수를 호출

2.increaseSkeletonDieCount()함수에서 skeletonDieCount를 증가시키며

델리게이트로 연결된 waveGeneration함수 호출

 

waveGeneration에서 웨이브를 담당하게됩니다

 

최종수정된수치

//WAVE
int32 skeletonDieCount;

UPROPERTY(EditAnywhere, Category = "MySettings")
int32 wave2NeedCount = 7;

UPROPERTY(EditAnywhere, Category = "MySettings")
int32 wave3NeedCount = 15;

UPROPERTY(EditAnywhere, Category = "MySettings")
int32 wave4NeedCount = 20;

UPROPERTY(EditAnywhere, Category = "MySettings")
int32 wave5NeedCount = 28;

UPROPERTY()
FOnSkeletonDieCountChanged onSkeletonDieCountChanged;

UFUNCTION()
void waveGeneration(int32 newCount);

UFUNCTION()
void increaseSkeletonDieCount();

UPROPERTY(EditAnywhere, Category = "MySettings")
bool UseMonsterWave = false;

UPROPERTY(EditAnywhere, Category = "MySettings")
TSubclassOf<ABasicSkeletonEnemy> skeleton1;

UPROPERTY(EditAnywhere, Category = "MySettings")
TSubclassOf<ABasicSkeletonEnemy> skeleton2;

UPROPERTY(EditAnywhere, Category = "MySettings")
TSubclassOf<ABasicSkeletonEnemy> skeleton3;

UPROPERTY(EditAnywhere, Category = "MySettings")
TSubclassOf<ABasicSkeletonEnemy> skeleton4;

UPROPERTY(EditAnywhere, Category = "MySettings")
TSubclassOf<ABasicSkeletonEnemy> skeleton5;

UFUNCTION()
void SpawnWave(TArray<TSubclassOf<ABasicSkeletonEnemy>>& availableTemplates);

UPROPERTY()
TArray<TSubclassOf<class ABasicSkeletonEnemy>> currentTemplates;

int32 spawnCount;

UPROPERTY(EditAnywhere, Category = "MySettings")
int32 wave1SpawnCount = 10;

UPROPERTY(EditAnywhere, Category = "MySettings")
int32 wave2SpawnCount = 7;

UPROPERTY(EditAnywhere, Category = "MySettings")
int32 wave3SpawnCount = 5;

UPROPERTY(EditAnywhere, Category = "MySettings")
int32 wave4SpawnCount = 11;

UPROPERTY(EditAnywhere, Category = "MySettings")
int32 wave5SpawnCount = 13;

 

 

 

 

웨이브의 로직은 이와 같습니다

 

처음 9마리 스폰
스폰되는템플릿은 skeleton1

7마리 잡으면 나머지는 두마리 wave2로 스폰
스폰되는템플릿은 skeleton1, 2
wave2에서 스폰되는 몬스터수는 7마리
(필드에 남아있는 몬스터수 9마리)

8마리 추가로 잡으면 나머지는 한마리 wave3로 스폰
스폰되는 템플릿은 skeleton3
스폰되는 카운트는 5마리
(필드에 남아있는 몬스터수 6마리)

5마리 추가로 잡으면 나머지는 0마리 wave4로 스폰
스폰되는 템플릿은 skeleton2와 3
스폰되는 카운트는 11마리
(필드에 남아있는 몬스터수 12마리)

8마리 추가로 잡으면 나머지는 4마리 wave5로 스폰
스폰되는 템플릿은 skeleton1, 2
스폰되는 카운트는 13마리
(필드에 남아있는 17마리)

 

반영된 코드입니다

void ASwordPlayerGameBase::waveGeneration(int32 newCount)
{
	if (newCount == wave2NeedCount)
	{
		spawnCount = wave2SpawnCount;
		currentTemplates = { skeleton1, skeleton2 };
		SpawnWave(currentTemplates);
	}
	else if (newCount == wave3NeedCount)
	{
		spawnCount = wave3SpawnCount;
		currentTemplates = { skeleton3 };
		SpawnWave(currentTemplates);
	}
	else if (newCount == wave4NeedCount)
	{
		spawnCount = wave4SpawnCount;
		currentTemplates = { skeleton2, skeleton3 };
		SpawnWave(currentTemplates);
	}
	else if (newCount == wave5NeedCount)
	{
		spawnCount = wave5SpawnCount;
		currentTemplates = { skeleton1, skeleton2 };
		SpawnWave(currentTemplates);
	}
}

 

이런느낌입니다

 

 

---

2025-05-18 스폰할려고하는위치에 콜리전이 있거나 무언가가 콜리전에 가로막혀서

스폰을 하지않아 정확한 갯수를 스폰하지않는 현상을 발견하였습니다

while문이나 for문으로 스폰되지않으면 다시 돌아가게 수정하였습니다

void ASwordPlayerGameBase::SpawnWave(TArray<TSubclassOf<ABasicSkeletonEnemy>>& availableTemplates, int32 countToSpawn)
{
	float spawnArea = 600.f;
	UWorld* dungeonWorld = GetWorld();
	int32 spawnedCount = 0;

	while (spawnedCount < countToSpawn)
	{
		FVector spawnLoc;
		float distanceToPlayer = 0.f;
		int32 maxTry = 100;
		int32 tryCount = 0;

		while (tryCount++ < maxTry)
		{
			float randAngle = FMath::FRandRange(0.0f, 2 * PI);
			float randDistance = FMath::FRandRange(0.0f, spawnArea);
			FVector Offset = FVector(FMath::Cos(randAngle), FMath::Sin(randAngle), 0.f) * randDistance;
			spawnLoc = centerLoc + Offset;
			spawnLoc.Z += 50.f;

			distanceToPlayer = FVector::Dist(spawnLoc, player->GetActorLocation());
			if (distanceToPlayer >= 300.f)
			{
				break;
			}
		}

		if (tryCount >= maxTry)
		{
			continue;
		}

		int32 randomChoiceIndex = FMath::RandRange(0, availableTemplates.Num() - 1);
		TSubclassOf<ABasicSkeletonEnemy> chosenTemplate = availableTemplates[randomChoiceIndex];

		FRotator spawnRot(0.0f, FMath::FRandRange(0.0f, 360.0f), 0.f);

		FActorSpawnParameters spawnParams;
		spawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;

		ABasicSkeletonEnemy* SpawnedEnemy = dungeonWorld->SpawnActor<ABasicSkeletonEnemy>(
			chosenTemplate, spawnLoc, spawnRot, spawnParams
		);

		if (SpawnedEnemy)
		{
			++spawnedCount;
		}
	}

	//UE_LOG(LogTemp, Log, TEXT("Finished spawning wave with count: %d"), spawnedCount);
}

'Unreal 프로젝트 다이어리 > 첫번째 프로젝트' 카테고리의 다른 글

Unrela - 시퀀스 만들기 (캐릭터편)  (0) 2025.05.21
Unreal - 레벨이동시 게임저장, 로드하기 (Save Game)  (0) 2025.05.20
Unreal - 버튼 위젯 사용하기  (0) 2025.05.16
Unreal - 로딩 창  (0) 2025.05.16
Unreal - 페이드 인 / 아웃  (0) 2025.05.15
'Unreal 프로젝트 다이어리/첫번째 프로젝트' 카테고리의 다른 글
  • Unrela - 시퀀스 만들기 (캐릭터편)
  • Unreal - 레벨이동시 게임저장, 로드하기 (Save Game)
  • Unreal - 버튼 위젯 사용하기
  • Unreal - 로딩 창
lucodev
lucodev
커피와 노트북 그리고 개발
  • lucodev
    루코 개발테이블
    lucodev
  • 전체
    오늘
    어제
    • 분류 전체보기 (211) N
      • Unreal 프로젝트 다이어리 (108) N
        • 첫번째 프로젝트 (73)
        • 두번째 프로젝트 (35) N
      • Unreal 팁 (8)
      • Unreal 디버깅 (8)
      • C++ 프로그래머스 (52)
        • Stack,Queue (7)
        • Hash (4)
        • Heap (2)
        • Sort (5)
        • Exhaustive search (5)
        • Greedy (2)
        • BFS , DFS (7)
        • Graph (2)
        • Dynamic Programming (1)
        • C++ Math (2)
        • 기타 문제 (14)
      • C++ 백준 (4)
      • C++ 팁 (1)
      • 개인 코테 & 스타디 <비공개> (29)
        • 코드 개인보관함 (9)
        • 코딩테스트+@ (11)
        • 알고리즘 스타디 (6)
        • 알고리즘 스타디 과제 (3)
        • 비공개 (0)
  • 인기 글

  • 최근 글

  • 최근 댓글

  • 링크

  • 공지사항

  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 태그

    unreal inventory
    언리얼 컷씬
    unreal 모션매칭
    언리얼 motionmatching
    언리얼 behavior tree
    언리얼 프로그래스바
    unreal 시퀀스
    언리얼 파쿠르
    unreal 인벤토리
    Unreal Parkour
    언리얼 parkour
    언리얼
    언리얼 ui
    언리얼 모션매칭
    언리얼 behaviortree
    언리얼 인벤토리
    언리얼 비헤이비어트리
    언리얼 시퀀스
    unreal 파쿠르
    언리얼 상호작용
  • hELLO· Designed By정상우.v4.10.3
lucodev
Unreal - 몬스터 웨이브 만들기
상단으로

티스토리툴바