언리얼 엔진 에디터에서 Actor를 배치하거나 위치를 조작하는 경우는 흔하다. 예를 들어, 맵에 몬스터 100마리를 배치한다고 가정했을때 몬스터 한마리 한마리 배치하기 시작하면 시간이 너무 많이 소요된다. 이 때, 몬스터 100마리를 각각 X, Y좌표로 1000 간격으로 배치하고 바라보는 방향으로 랜덤으로 돌리면 빠르게 맵에 배치할 수 있고 테스트하기도 좋다. 이런 기능을 담당하는 툴을 만들었다. 

 

목차

     

     


     

     

    Actor SRT Tool 만들기


     

    UEditorSubsystem 기능을 사용할 것이다

     

    UEditorActorSubsystem

    • TArray<class > GetSelectedLevelActors()
    • TArray<class > GetAllLevelActors()

     

     

    QuickActorActionWidget 생성

     

    새 C++ 클래스 - EditorUtilityWidget - QuickActorActionWidget 생성

     

     

    QuickActorActionWidget.h

    더보기
    #pragma once
    
    #include "CoreMinimal.h"
    #include "EditorUtilityWidget.h"
    #include "QuickActorActionsWidget.generated.h"
    
    /**	아래 3가지의 기능들을 수행하는 Tool를 만드는 클래스
     *	1. 비슷한 이름의 Actor들을 선택하는 기능
     *	2. 선택한 Actor를 설정한 Offset간격으로 지정한 수만큼 복제하는 기능
     *	3. Actor의 SRT(Scale, Rotate, Transform)을 조작하는 기능
     *  UEditorUtilityWidget 상속을 받음
     */
    UENUM(BlueprintType)
    enum class E_DuplicationAxis : uint8
    {
    	EDA_XAxis UMETA(DisplayName = "X Axis"),
    	EDA_YAxis UMETA(DisplayName = "Y Axis"),
    	EDA_ZAxis UMETA(DisplayName = "Z Axis"),
    	EDA_MAX UMETA(DisplayName = "Default Max")
    };
    
    USTRUCT(BlueprintType)
    struct FRandomActorRotation
    {
    	GENERATED_BODY()
    	
    	UPROPERTY(EditAnywhere, BlueprintReadWrite)
    	bool bRandomizeRotYaw = false;
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (EditCondition = "bRandomizeRotYaw")) // bRandomizeRotYaw == true일 때만 적용
    	float RotYawMin = -45.f;
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (EditCondition = "bRandomizeRotYaw"))
    	float RotYawMax = 45.f;
    
    	UPROPERTY(EditAnywhere, BlueprintReadWrite)
    	bool bRandomizeRotPitch = false;
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (EditCondition = "bRandomizeRotPitch"))
    	float RotPitchMin = -45.f;
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (EditCondition = "bRandomizeRotPitch"))
    	float RotPitchMax = 45.f;
    
    	UPROPERTY(EditAnywhere, BlueprintReadWrite)
    	bool bRandomizeRotRoll = false;
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (EditCondition = "bRandomizeRotRoll"))
    	float RotRollMin = -45.f;
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (EditCondition = "bRandomizeRotRoll"))
    	float RotRollMax = 45.f;
    };
    
    UCLASS()
    class SWMANAGER_API UQuickActorActionsWidget : public UEditorUtilityWidget
    {
    	GENERATED_BODY()
    
    public:
    #pragma region ActorBatchSelection
    
    	UFUNCTION(BlueprintCallable) // WBP의 Graph에 노출시켜 버튼 클릭 시 실행되도록함
    	void SelectAllActorsWithSimilarName(); // 비슷한 이름의 모든 Actor를 선택
    
    	// "ActorBatchSelection"를 WBP의 ActorBatchSelectionDetailsView에 연결
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ActorBatchSelection")
    	TEnumAsByte<ESearchCase::Type> SearchCase = ESearchCase::IgnoreCase;
    
    #pragma endregion
    
    //** Actor 복제. WBP의 ActorBatchDuplicationDetailsView/Button에 아래변수 연결
    #pragma region ActorBatchDuplication 
    
    	UFUNCTION(BlueprintCallable)
    	void DuplicateActors(); // Actor 복제
    
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ActorBatchDuplication")
    	E_DuplicationAxis AxisForDuplication = E_DuplicationAxis::EDA_XAxis;
    
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ActorBatchDuplication")
    	int32 NumberOfDuplicates = 5; // 복제 수
    
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ActorBatchDuplication")
    	float OffsetDist = 300.f; // 복제 시 Actor 간 간격
    
    #pragma endregion
    
    //** Actor를 랜덤으로 Transform
    #pragma region RandomizeActorTransform
    
    	UFUNCTION(BlueprintCallable)
    	void RandomizeActorTransform(); // Actor를 랜덤으로 Transform
    
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RandomizeActorTransform")
    	FRandomActorRotation RandomActorRotation;
    
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RandomizeActorTransform")
    	bool bRandomizeScale = false;
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RandomizeActorTransform", meta = (EditCondition = "bRandomizeScale"))
    	float ScaleMin = 0.8f;
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RandomizeActorTransform", meta = (EditCondition = "bRandomizeScale"))
    	float ScaleMax = 1.2f;
    
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RandomizeActorTransform")
    	bool bRandomizeOffset = false;
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RandomizeActorTransform", meta = (EditCondition = "bRandomizeOffset"))
    	float OffsetMin = -50.f;
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RandomizeActorTransform", meta = (EditCondition = "bRandomizeOffset"))
    	float OffsetMax = 50.f;
    
    #pragma endregion
    
    private:
    	UPROPERTY()
    	TObjectPtr<class UEditorActorSubsystem> EditorActorSubsystem;
    
    	bool GetEditorActorSubsystem();
    };

     

     

     

    QuickActorActionWidget.cpp

    더보기
    #include "ActorActions/QuickActorActionsWidget.h"
    #include "Subsystems/EditorActorSubsystem.h"
    #include "DebugHeader.h"
    
    void UQuickActorActionsWidget::SelectAllActorsWithSimilarName()
    {
    	if (false == GetEditorActorSubsystem()) return;
    
    	TArray<AActor*> SelectedActors = EditorActorSubsystem->GetSelectedLevelActors();
    	uint32 SelectionCounter = 0; // 비슷한 이름으로 선택된 Actor의 수
    
    	//** 예외처리
    	if (SelectedActors.Num() == 0) // Actor를 선택하지 않은 경우
    	{
    		DebugHeader::ShowNotifyInfo(TEXT("선택된 Actor가 없습니다."));
    		return;
    	}
    	if (SelectedActors.Num() > 1)  // Actor를 2개 이상 선택한 경우
    	{
    		DebugHeader::ShowNotifyInfo(TEXT("한가지 Actor만 선택할 수 있습니다. 다시 선택하십시오."));
    		return;
    	}
    
    	//** 제대로 선택한 경우. Actor를 1개 선택한 경우 
    	FString SelectedActorName = SelectedActors[0]->GetActorLabel(); // 선택한 Actor가 WorldOutlinear에 노출되는 이름
    	const FString NameToSearch = SelectedActorName.LeftChop(4); // SelectedActorName에서 끝의 4글자를 제거하여 NameToSearch변수에 담음
    
    	TArray<AActor*> AllLeveActors = EditorActorSubsystem->GetAllLevelActors(); // Level에 있는 모든 Actor
    
    	for (AActor* ActorInLevel : AllLeveActors)
    	{
    		if (false == IsValid(ActorInLevel)) continue;
    
    		// Actor의 이름에 NameToSearch가 포함되어 있다면
    		if (ActorInLevel->GetActorLabel().Contains(NameToSearch, SearchCase))
    		{
    			// 해당 Actor를 선택하고 SelectionCounter를 증가시킨다
    			EditorActorSubsystem->SetActorSelectionState(ActorInLevel, true);
    			SelectionCounter++; // 비슷한 이름으로 선택된 Actor의 수++
    		}
    	}
    
    	if (SelectionCounter > 0)
    	{
    		DebugHeader::ShowNotifyInfo(TEXT("총 ") +
    			FString::FromInt(SelectionCounter) + TEXT(" 개의 Actor들이 성공적으로 선택되었습니다."));
    	}
    	else
    	{
    		DebugHeader::ShowNotifyInfo(TEXT("비슷한 이름의 Actor를 찾지 못했습니다."));
    	}
    }
    
    void UQuickActorActionsWidget::DuplicateActors() // Actor 복제
    {
    	if (false == GetEditorActorSubsystem()) return;
    
    	TArray<AActor*> SelectedActors = EditorActorSubsystem->GetSelectedLevelActors(); // 선택된 Actor들을 담는 변수
    	uint32 Counter = 0; // 복제된 Actor 수 기록할 변수
    
    	if (SelectedActors.Num() == 0)
    	{
    		DebugHeader::ShowNotifyInfo(TEXT("선택된 Actor가 없습니다."));
    		return;
    	}
    
    	if (NumberOfDuplicates <= 0 || OffsetDist == 0)
    	{
    		DebugHeader::ShowNotifyInfo(TEXT("'복제를 희망하는 수'와 '복제 간격'을 올바르게 기입해주세요."));
    		return;
    	}
    
    	for (AActor* SelectedActor : SelectedActors) // 선택된 Actor들을 for문 돌아 복제
    	{
    		if (false == IsValid(SelectedActor)) continue; // 선택된 Actor가 없으면 continue
    
    		for (int32 i = 0; i < NumberOfDuplicates; i++) // NumberOfDuplicates 수 만큼 복제
    		{
    			//** Actor 복제
    			AActor* DuplicatedActor = EditorActorSubsystem->DuplicateActor(SelectedActor, SelectedActor->GetWorld()); 
    
    			if (false == IsValid(DuplicatedActor)) continue;
    
    			//** 복제 후 Offset 이동
    			const float DuplicationOffsetDist = (i + 1) * OffsetDist;
    
    			switch (AxisForDuplication)
    			{
    			case E_DuplicationAxis::EDA_XAxis:
    
    				DuplicatedActor->AddActorWorldOffset(FVector(DuplicationOffsetDist, 0.f, 0.f));
    				break;
    
    			case E_DuplicationAxis::EDA_YAxis:
    
    				DuplicatedActor->AddActorWorldOffset(FVector(0.f, DuplicationOffsetDist, 0.f));
    				break;
    
    			case E_DuplicationAxis::EDA_ZAxis:
    
    				DuplicatedActor->AddActorWorldOffset(FVector(0.f, 0.f, DuplicationOffsetDist));
    				break;
    
    			case E_DuplicationAxis::EDA_MAX:
    				break;
    
    			default:
    				break;
    			}
    
    			//** 복제된 Actor들 선택
    			EditorActorSubsystem->SetActorSelectionState(DuplicatedActor, true);
    			Counter++; // 복제된 Actor 수 기록
    		}
    	}
    
    	if (Counter > 0)
    	{
    		DebugHeader::ShowNotifyInfo(TEXT("총 ") +
    			FString::FromInt(Counter) + TEXT(" 개의 Actor가 성공적으로 복제되었습니다."));
    	}
    }
    
    void UQuickActorActionsWidget::RandomizeActorTransform() // Actor를 랜덤으로 Transform
    {
    	const bool bConditionNotSet =
    		false == RandomActorRotation.bRandomizeRotYaw &&
    		false == RandomActorRotation.bRandomizeRotPitch &&
    		false == RandomActorRotation.bRandomizeRotRoll &&
    		false == bRandomizeScale &&
    		false == bRandomizeOffset;
    
    	if (bConditionNotSet)
    	{
    		DebugHeader::ShowNotifyInfo(TEXT("변경 조건이 설정되지 않았습니다."));
    		return;
    	}
    
    	if (false == GetEditorActorSubsystem()) return;
    
    	TArray<AActor*> SelectedActors = EditorActorSubsystem->GetSelectedLevelActors(); // 선택된 Actor들을 담음
    	uint32 Counter = 0; // Transform된 Actor 수를 기록할 변수
    
    	if (SelectedActors.Num() == 0)
    	{
    		DebugHeader::ShowNotifyInfo(TEXT("선택된 Actor가 없습니다."));
    		return;
    	}
    
    	for (AActor* SelectedActor : SelectedActors)
    	{
    		if (false == IsValid(SelectedActor)) continue; 
    
    		if (RandomActorRotation.bRandomizeRotYaw) // Yaw
    		{
    			const float RandomRotYawValue = FMath::RandRange(RandomActorRotation.RotYawMin, RandomActorRotation.RotYawMax);
    
    			SelectedActor->AddActorWorldRotation(FRotator(0.f, RandomRotYawValue, 0.f));
    		}
    		if (RandomActorRotation.bRandomizeRotPitch) // Pitch
    		{
    			const float RandomRotPitchValue = FMath::RandRange(RandomActorRotation.RotPitchMin, RandomActorRotation.RotPitchMax);
    
    			SelectedActor->AddActorWorldRotation(FRotator(RandomRotPitchValue, 0.f, 0.f));
    		}
    		if (RandomActorRotation.bRandomizeRotRoll) // Roll
    		{
    			const float RandomRotRollValue = FMath::RandRange(RandomActorRotation.RotRollMin, RandomActorRotation.RotRollMax);
    
    			SelectedActor->AddActorWorldRotation(FRotator(0.f, 0.f, RandomRotRollValue));
    		}
    		if (bRandomizeScale) // Scale
    		{
    			SelectedActor->SetActorScale3D(FVector(FMath::RandRange(ScaleMin, ScaleMax)));
    		}
    
    		if (bRandomizeOffset) // Offset
    		{
    			const float RandomOffsetValue = FMath::RandRange(OffsetMin, OffsetMax);
    
    			SelectedActor->AddActorWorldOffset(FVector(RandomOffsetValue, RandomOffsetValue, 0.f)); 
    		}
    
    		Counter++; // Transform된 Actor 수++
    	}
    
    	if (Counter > 0)
    	{
    		DebugHeader::ShowNotifyInfo(TEXT("총 ") +
    			FString::FromInt(Counter) + TEXT(" 개의 Actor들이 Tranform 되었습니다."));
    	}
    
    }
    
    bool UQuickActorActionsWidget::GetEditorActorSubsystem()
    {
    	if (false == IsValid(EditorActorSubsystem))
    	{
    		EditorActorSubsystem = GEditor->GetEditorSubsystem<UEditorActorSubsystem>(); // 할당
    	}
    
    	return IsValid(EditorActorSubsystem); // EditorActorSubsystem 존재한다면 true리턴
    }

     

     

     

    ※ 참고: FString LeftChop(int32 Count) 

    -  FString의 값에서 끝의 Count 숫자만큼 글자를 제거한다.

     

     

    WBP_ QuickActorActionWidget 생성


     

     

    실행화면