icon안동민 개발노트

EQS(Environment Query System) 소개


 환경 쿼리 시스템(EQS)은 언리얼 엔진에서 AI가 주변 환경을 분석하고 최적의 결정을 내리는 데 사용되는 강력한 도구입니다.

 이 절에서는 EQS의 기본 개념과 C++에서의 활용 방법을 살펴보겠습니다.

EQS의 기본 구성 요소

  1. 쿼리(Query) : 환경에 대한 질문
  2. 테스트(Test) : 각 후보 위치에 대한 평가 기준
  3. 제너레이터(Generator) : 후보 위치 생성 방식

C++에서 커스텀 EQS 쿼리 구현

UCLASS()
class MYPROJECT_API UEnvQueryTest_CustomTest : public UEnvQueryTest
{
    GENERATED_BODY()
 
public:
    UEnvQueryTest_CustomTest();
 
    virtual void RunTest(FEnvQueryInstance& QueryInstance) const override;
 
private:
    UPROPERTY(EditAnywhere, Category = "Test Parameters")
    FAIDataProviderFloatValue Radius;
};
 
UEnvQueryTest_CustomTest::UEnvQueryTest_CustomTest()
{
    Cost = EEnvTestCost::Low;
    ValidItemType = UEnvQueryItemType_Point::StaticClass();
}
 
void UEnvQueryTest_CustomTest::RunTest(FEnvQueryInstance& QueryInstance) const
{
    UObject* QueryOwner = QueryInstance.Owner.Get();
    if (QueryOwner == nullptr)
    {
        return;
    }
 
    float RadiusValue = Radius.GetValue();
    for (FEnvQueryInstance::ItemIterator It(this, QueryInstance); It; ++It)
    {
        const FVector ItemLocation = GetItemLocation(QueryInstance, It.GetIndex());
        const float Distance = FVector::Dist(ItemLocation, QueryOwner->GetActorLocation());
        
        It.SetScore(TestPurpose, FilterType, Distance < RadiusValue, Distance);
    }
}

EQS를 활용한 동적 환경 분석

UCLASS()
class MYPROJECT_API UEnvQueryContext_DynamicObjects : public UEnvQueryContext
{
    GENERATED_BODY()
 
public:
    virtual void ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const override;
};
 
void UEnvQueryContext_DynamicObjects::ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const
{
    TArray<AActor*> DynamicObjects;
    UGameplayStatics::GetAllActorsOfClass(QueryInstance.World, ADynamicObject::StaticClass(), DynamicObjects);
 
    UEnvQueryItemType_ActorBase::SetContextHelper(ContextData, DynamicObjects);
}

AI 의사결정 최적화

 EQS를 사용하여 AI의 의사결정을 최적화할 수 있습니다.

UCLASS()
class MYPROJECT_API AMyAIController : public AAIController
{
    GENERATED_BODY()
 
public:
    void FindBestCoverLocation();
 
private:
    UPROPERTY()
    UEnvQuery* FindCoverQuery;
 
    UFUNCTION()
    void OnFindCoverQueryFinished(TSharedPtr<FEnvQueryResult> Result);
};
 
void AMyAIController::FindBestCoverLocation()
{
    if (FindCoverQuery)
    {
        FEnvQueryRequest QueryRequest(FindCoverQuery, GetPawn());
        QueryRequest.Execute(EEnvQueryRunMode::SingleResult, this, &AMyAIController::OnFindCoverQueryFinished);
    }
}
 
void AMyAIController::OnFindCoverQueryFinished(TSharedPtr<FEnvQueryResult> Result)
{
    if (Result.IsValid() && Result->IsSuccessful())
    {
        FVector BestLocation = Result->GetItemAsLocation(0);
        // 최적의 위치로 이동
    }
}

복잡한 게임플레이 상황에서의 EQS 활용

 전략적 위치 선정을 위한 EQS 활용 예

UCLASS()
class MYPROJECT_API UEnvQueryTest_StrategicPosition : public UEnvQueryTest
{
    GENERATED_BODY()
 
public:
    UEnvQueryTest_StrategicPosition();
 
    virtual void RunTest(FEnvQueryInstance& QueryInstance) const override;
 
private:
    UPROPERTY(EditAnywhere, Category = "Test Parameters")
    FAIDataProviderFloatValue CoverValue;
    
    UPROPERTY(EditAnywhere, Category = "Test Parameters")
    FAIDataProviderFloatValue VisibilityValue;
};
 
void UEnvQueryTest_StrategicPosition::RunTest(FEnvQueryInstance& QueryInstance) const
{
    for (FEnvQueryInstance::ItemIterator It(this, QueryInstance); It; ++It)
    {
        const FVector ItemLocation = GetItemLocation(QueryInstance, It.GetIndex());
        float CoverScore = CalculateCoverScore(ItemLocation);
        float VisibilityScore = CalculateVisibilityScore(ItemLocation);
        
        float FinalScore = (CoverValue.GetValue() * CoverScore + VisibilityValue.GetValue() * VisibilityScore) / 
                           (CoverValue.GetValue() + VisibilityValue.GetValue());
        
        It.SetScore(TestPurpose, FilterType, FinalScore, FinalScore);
    }
}

EQS의 성능 최적화 전략

  1. 쿼리 실행 빈도 조절
UCLASS()
class MYPROJECT_API AMyAIController : public AAIController
{
    GENERATED_BODY()
 
private:
    FTimerHandle QueryTimerHandle;
    
    void PerformPeriodicQuery();
};
 
void AMyAIController::PerformPeriodicQuery()
{
    // EQS 쿼리 실행
    GetWorldTimerManager().SetTimer(QueryTimerHandle, this, &AMyAIController::PerformPeriodicQuery, 1.0f, false);
}
  1. 결과 캐싱
UCLASS()
class MYPROJECT_API UMyEQSManager : public UObject
{
    GENERATED_BODY()
 
public:
    FVector GetCachedResult(FName QueryName);
    void UpdateCache(FName QueryName, FVector Result);
 
private:
    TMap<FName, FVector> QueryResultCache;
};

대규모 환경에서의 효율적인 EQS 사용법

  1. 공간 분할
UCLASS()
class MYPROJECT_API UEnvQueryGenerator_SpatialPartition : public UEnvQueryGenerator
{
    GENERATED_BODY()
 
public:
    virtual void GenerateItems(FEnvQueryInstance& QueryInstance) const override;
 
private:
    UPROPERTY(EditAnywhere, Category = "Generator Parameters")
    float CellSize;
};
 
void UEnvQueryGenerator_SpatialPartition::GenerateItems(FEnvQueryInstance& QueryInstance) const
{
    // 공간을 CellSize 크기의 격자로 분할하고 각 셀의 중심점을 생성
}
  1. LOD (Level of Detail) 시스템 구현
UCLASS()
class MYPROJECT_API UEnvQueryTest_LODFilter : public UEnvQueryTest
{
    GENERATED_BODY()
 
public:
    virtual void RunTest(FEnvQueryInstance& QueryInstance) const override;
 
private:
    UPROPERTY(EditAnywhere, Category = "LOD")
    float HighDetailDistance;
    
    UPROPERTY(EditAnywhere, Category = "LOD")
    float MediumDetailDistance;
};

비헤이비어 트리와 EQS의 연동

 비헤이비어 트리에서 EQS를 사용하는 태스크 구현

UCLASS()
class MYPROJECT_API UBTTask_RunEQSQuery : public UBTTaskNode
{
    GENERATED_BODY()
 
public:
    virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
 
private:
    UPROPERTY(EditAnywhere, Category = "EQS")
    UEnvQuery* EQSQuery;
    
    UPROPERTY(EditAnywhere, Category = "Blackboard")
    FBlackboardKeySelector ResultKey;
};
 
EBTNodeResult::Type UBTTask_RunEQSQuery::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
    AAIController* AIController = OwnerComp.GetAIOwner();
    if (AIController && EQSQuery)
    {
        FEnvQueryRequest QueryRequest(EQSQuery, AIController->GetPawn());
        QueryRequest.Execute(EEnvQueryRunMode::SingleResult, this, &UBTTask_RunEQSQuery::OnQueryFinished);
        return EBTNodeResult::InProgress;
    }
    return EBTNodeResult::Failed;
}

고급 AI 기능 구현

  1. 은신처 찾기
UCLASS()
class MYPROJECT_API UEnvQueryTest_CoverQuality : public UEnvQueryTest
{
    GENERATED_BODY()
 
public:
    virtual void RunTest(FEnvQueryInstance& QueryInstance) const override;
 
private:
    UPROPERTY(EditAnywhere, Category = "Cover")
    FAIDataProviderFloatValue MinCoverHeight;
    
    float EvaluateCoverQuality(const FVector& Location, const AActor* Threat) const;
};
  1. 전략적 위치 선정
UCLASS()
class MYPROJECT_API UEnvQueryTest_StrategicValue : public UEnvQueryTest
{
    GENERATED_BODY()
 
public:
    virtual void RunTest(FEnvQueryInstance& QueryInstance) const override;
 
private:
    UPROPERTY(EditAnywhere, Category = "Strategy")
    FAIDataProviderFloatValue ControlPointWeight;
    
    UPROPERTY(EditAnywhere, Category = "Strategy")
    FAIDataProviderFloatValue ResourcePointWeight;
    
    float CalculateStrategicValue(const FVector& Location) const;
};
  1. 동적 난이도 조절
UCLASS()
class MYPROJECT_API UEnvQueryTest_DynamicDifficulty : public UEnvQueryTest
{
    GENERATED_BODY()
 
public:
    virtual void RunTest(FEnvQueryInstance& QueryInstance) const override;
 
private:
    UPROPERTY(EditAnywhere, Category = "Difficulty")
    FAIDataProviderFloatValue PlayerSkillFactor;
    
    float AdjustScoreBasedOnDifficulty(float BaseScore, float PlayerSkill) const;
};

 EQS는 언리얼 엔진에서 AI의 환경 인식과 의사결정을 크게 향상시킬 수 있는 강력한 도구입니다.

 C++를 사용하여 커스텀 쿼리와 테스트를 구현함으로써 게임의 특정 요구사항에 맞는 복잡한 AI 행동을 구현할 수 있습니다.

 동적 환경 분석을 통해 AI는 실시간으로 변화하는 게임 환경에 적응할 수 있으며, 이는 더 현실적이고 도전적인 AI 행동으로 이어집니다.

 또한 EQS를 비헤이비어 트리와 연동하여 사용하면 더욱 유연하고 강력한 AI 시스템을 구축할 수 있습니다.