![[Unreal Engine C++] 액터를 원 안의 랜덤 위치에 스폰시키기](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyxmuO%2FbtsKVRYiLBo%2F5NxjqoqKcPqpVrIKiGH4E1%2Fimg.png)
레벨에 적 몬스터들을 스폰시킬 때, 그냥 스폰시키는 것이 아닌 랜덤 장소에 스폰시키는 방식을 사용하고 싶었다.
지금부터 그 방법을 알아보자
시작하기에 앞서..
현재 레벨의 상태는 다음과 같다.
여러가지 구역들이 나누어져있고, 구역에 진입할 시 몹들과의 전투가 이루어지길 원했다.
다만, 맨 처음에 미리 스폰시켜놓으면 최적화에 좋지 않을 것이기 때문에 진입하는 순간에 스폰하도록 할 것이다.
스폰할 때는 내가 원하는 숫자만큼 생성, 스폰 위치는 랜덤으로 할 것이다.
AI 몬스터가 움직일 수 있게 맵에 내비메시 바운드 볼륨을 깔아주도록 한다.
이 볼륨에 대한 설명은 구글에 검색해도 바로 나오기 때문에 간단하게 설명하면 AI들이 움직일 수 있는 곳을 알려주는 것이라고 볼 수 있다.
볼륨의 크기는 이동할 수 있는 모든 지역을 덮을 수 있게 한다.
KeyBoard P를 눌러 어디에 적용되고 있는 지 확인할 수 있다.
스포너의 생성
이를 위해 EnemySpawner라는 이름을 가진 Actor를 C++클래스로 생성하였다.
캐릭터가 이 액터에 Overlap될 때 몹들을 스폰시킬 것이므로, TriggerBox를 만들어 Overlap 되는 지 확인하고 이벤트를 실행시킬 것이다.
// EnemySpawner.h
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "EnemySpawner.generated.h"
UCLASS()
class SEADENRING_API AEnemySpawner : public AActor
{
GENERATED_BODY()
...
private:
UFUNCTION()
void OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
UPROPERTY(VisibleAnywhere)
class UBoxComponent* TriggerBox;
};
// EnemySpawner.cpp
#include "Character/Enemy/EnemySpawner.h"
#include "Components/BoxComponent.h"
// Sets default values
AEnemySpawner::AEnemySpawner()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = false;
TriggerBox = CreateDefaultSubobject<UBoxComponent>(FName("TriggerBox"));
if (TriggerBox != nullptr)
return;
RootComponent = TriggerBox;
}
// Called when the game starts or when spawned
void AEnemySpawner::BeginPlay()
{
Super::BeginPlay();
TriggerBox->OnComponentBeginOverlap.AddDynamic(this, &AEnemySpawner::OnOverlapBegin);
}
...
void AEnemySpawner::OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
...
}
TriggerBox를 Root로 지정해주고 BeginPlay에서 바인딩해주면 오버랩되는 것을 확인할 수 있다.
Overlap시 몹들의 랜덤 생성
이 Spawner의 목표는 오버랩이 될 시 몹들을 '랜덤'하게 스폰시키는 것이다.
이를 위해서 우리는 Navigation System의 GetRandomReachablePointInRadius를 이용할 것이다.
https://dev.epicgames.com/documentation/ko-kr/unreal-engine/basic-navigation-in-unreal-engine
이 주소에는 기본 내비게이션 메시를 사용하는 간단한 방법들이 적혀있는데, Ctrl + F로 검색해보면 다음과 같은 구문이 나온다.
이 구문을 읽어보면 이 함수는 Origin위치에서 Radius만큼의 거리에 있는 좌표 중 움직일 수 있는 포인트를 랜덤으로 가져오는 것을 알 수 있다.
이 함수를 응용해서 랜덤하게 스폰할 것이다.
지금 하고 있는 프로젝트는 멀티를 가정하여 만들었기 때문에, 클라이언트에서는 이 함수가 실행되면 안된다.
서버상에서만 실행할 수 있게 다음과 같은 코드를 작성하였다.
// EnemySpawer.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "EnemySpawner.generated.h"
UCLASS()
class SEADENRING_API AEnemySpawner : public AActor
{
GENERATED_BODY()
...
private:
...
UPROPERTY(EditAnywhere, Category = Spawn, Meta = (AllowPrivateAccess = "true"))
TSubclassOf<class ACharacter> EnemyBpRef;
UPROPERTY(EditAnywhere, Category = Spawn, Meta = (AllowPrivateAccess = true))
FVector SpawnLocation;
UPROPERTY(EditAnywhere, Category = Spawn, Meta = (AllowPrivateAccess = true))
int32 NumberOfSpawn;
UPROPERTY(EditAnywhere, Category = Spawn, Meta = (AllowPrivateAccess = true))
float SpawnRadius;
};
// EnemySpawner.cpp
...
void AEnemySpawner::OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (HasAuthority())
{
for (int i = 0; i < NumberOfSpawn; i++)
{
FNavLocation EnemySpawnLocation;
UNavigationSystemV1::GetNavigationSystem(GetWorld())->GetRandomReachablePointInRadius(SpawnLocation, SpawnRadius, EnemySpawnLocation);
auto SpawnedEnemy = GetWorld()->SpawnActor<ACharacter>(EnemyBpRef, UKismetMathLibrary::MakeTransform(EnemySpawnLocation, UKismetMathLibrary::FindLookAtRotation(EnemySpawnLocation, GetActorLocation())));
if (SpawnedEnemy)
{
FRotator NewRotation = SpawnedEnemy->GetActorRotation();
NewRotation.Pitch = 0;
SpawnedEnemy->SetActorRotation(NewRotation);
SpawnedEnemy->SpawnDefaultController();
}
}
}
Destroy();
}
// Project.Build.cs
using UnrealBuildTool;
public class SeadenRing : ModuleRules
{
public SeadenRing(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "AIModule", ... , "NavigationSystem" });
PrivateIncludePaths.Add("SeadenRing");
}
}
GetRandomReachablePointInRadius를 사용하기 위해서 Build.cs에 "NavigationSystem"를 추가해주었다.
HasAuthority로 서버 권한을 가지고 있는지 확인 후, UPROPERTY로 에디터 상에서 받아온 값을 통해 몹들을 스폰하게 하였다.
EnemyBpRef를 통해 ACharacter를 상속받아 만들어진 AI 블루프린트 레퍼런스를 받아오고 원하는 장소와 몹 수, 범위를 설정하였다.
이를 GetRandomReachablePointInRadius를 통해 EnemySpawnLocation에 저장하고, SpawnActor로 몹들을 소환하였다.
이 때 그 생성된 몹들이 플레이어 캐릭터의 방향을 바라보게 설정하고 싶었기 때문에 FindLookAtRotation를 사용하여 방향을 정해주었다.
하지만, FindLookAtRotation만 적용한다면 경사진 곳이나, 조금의 높이 차이로도 다음과 같은 문제가 발생할 수 있다.
이렇게 발이 뜨게 된다면 이 몹은 공격같은 애니메이션을 수행할 때 공중으로 날라가는 것처럼 보이는 문제가 발생할 수 있다.
그러므로 NewRotation.Pitch = 0 을 통해 다시 정면을 바라볼 수 있게 해주었다.
Actor 스폰의 문제 해결
이 때 AI 또는 다른 액터들을 SpawActor로 스폰시켜 본 사람들이면 다음과 같은 궁금점을 가질 수 있다.
" 만약 랜덤으로 정한 위치가 이전에 나온 위치랑 같거나 겹치게 되면 어떻게 되지? "
답은 간단하다. 그 이후에 생성되는 Actor가 스폰되지 않게 된다.
이는 랜덤 생성 시 원하는 몹 수조차 랜덤이 될 뿐만 아니라 게임 플레이에 큰 문제점으로 남을 수 있다.
이를 수정하기 위해 다음과 같은 방법을 사용하였다.
GetRandomReachablePointInRadius를 다시 확인해보면 도달할 수 있는 점을 저장한다.
액터가 이미 스폰된 장소를 도달할 수 없는 공간으로 만들게 된다면 이는 문제점을 해결할 수 있을 것이다.
필자는 이 문서를 통해 그 방법을 적용하였다.
이를 설명하면 다음과 같다.
먼저 에디터 상의 편집 > 프로젝트 세팅으로 들어간다.
Runtime을 검색 후 런타임 생성이 기본 Static으로 되어있을텐데 이를 Dynamic Modifiers Only로 바꿔준다.
이렇게 되면 실행되고 있을 때 내비게이션 메시가 동적으로 변화할 수 있게 되는데, Enemy 몹들에 적용될 수 있도록 설정하는 작업이 필요하다.
C++의 EnemyBpRef에 넣어준 Enemy 블루프린트 액터로 가서 컴포넌트에 NavModifier를 추가하여 준다.
이제 몹 액터가 스폰되면서 내비게이션 메시를 변화시키게 되고, 이를 통해 겹치거나 같은 위치의 점이 생성되지 않게 된다.
전체 코드와 결과는 다음과 같다.
// EnemySpawner.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "EnemySpawner.generated.h"
UCLASS()
class SEADENRING_API AEnemySpawner : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AEnemySpawner();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
private:
UFUNCTION()
void OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
UPROPERTY(VisibleAnywhere)
class UBoxComponent* TriggerBox;
UPROPERTY(EditAnywhere, Category = Spawn, Meta = (AllowPrivateAccess = "true"))
TSubclassOf<class ACharacter> EnemyBpRef;
UPROPERTY(EditAnywhere, Category = Spawn, Meta = (AllowPrivateAccess = true))
FVector SpawnLocation;
UPROPERTY(EditAnywhere, Category = Spawn, Meta = (AllowPrivateAccess = true))
int32 NumberOfSpawn;
UPROPERTY(EditAnywhere, Category = Spawn, Meta = (AllowPrivateAccess = true))
float SpawnRadius;
};
// EnemySpawner.cpp
#include "Character/Enemy/EnemySpawner.h"
#include "Components/BoxComponent.h"
#include "NavigationSystem.h"
#include "Kismet/KismetMathLibrary.h"
// Sets default values
AEnemySpawner::AEnemySpawner()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = false;
TriggerBox = CreateDefaultSubobject<UBoxComponent>(FName("TriggerBox"));
if (TriggerBox != nullptr)
return;
RootComponent = TriggerBox;
}
// Called when the game starts or when spawned
void AEnemySpawner::BeginPlay()
{
Super::BeginPlay();
TriggerBox->OnComponentBeginOverlap.AddDynamic(this, &AEnemySpawner::OnOverlapBegin);
DrawDebugSphere(GetWorld(), SpawnLocation, SpawnRadius, 12, FColor::White, true);
}
// Called every frame
void AEnemySpawner::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void AEnemySpawner::OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (HasAuthority())
{
for (int i = 0; i < NumberOfSpawn; i++)
{
FNavLocation EnemySpawnLocation;
UNavigationSystemV1::GetNavigationSystem(GetWorld())->GetRandomReachablePointInRadius(SpawnLocation, SpawnRadius, EnemySpawnLocation);
auto SpawnedEnemy = GetWorld()->SpawnActor<ACharacter>(EnemyBpRef, UKismetMathLibrary::MakeTransform(EnemySpawnLocation, UKismetMathLibrary::FindLookAtRotation(EnemySpawnLocation, GetActorLocation())));
if (SpawnedEnemy)
{
FRotator NewRotation = SpawnedEnemy->GetActorRotation();
NewRotation.Pitch = 0;
SpawnedEnemy->SetActorRotation(NewRotation);
SpawnedEnemy->SpawnDefaultController();
}
}
}
Destroy();
}
// Project.Build.cs
using UnrealBuildTool;
...
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "AIModule", ... , "NavigationSystem" });
...
'UnrealEngine > 공부' 카테고리의 다른 글
[Unreal Engine BP] 캐릭터 카메라를 액터에 Close Up 하기 (0) | 2024.09.28 |
---|---|
[Unreal Engine C++] Dash의 기능에서 개선된 Dodge의 구현 (0) | 2024.08.21 |
[Unreal Engine C++] Widget Blueprint를 이용해 세션 생성 및 접속 기능 만들기 (0) | 2024.08.15 |
[GitLab] SourceTree를 활용해 팀 프로젝트 진행해보기 (0) | 2024.08.04 |
[Unreal Engine C++] C++에서 Niagara System 스폰 (1) | 2024.05.29 |
CSE & GAME 개발 블로그
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 부탁드립니다!