Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Quest System and Random Events in UE5 C++ for Stardust Mutation

Tech 1

Quest Data Structure

The core quest structure defines mission parameters including objectives, rewards, and completion conditions:

USTRUCT(BlueprintType)
struct FQuest
{
    GENERATED_BODY()
    
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task")
    FString QuestName{ "Empty" };
    
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task")
    ETaskTypes QuestType{ ETaskTypes::Empty };
    
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task")
    int NovaValueReward{ 10 };
    
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task")
    FText QuestDescription;
    
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task/Stardust")
    TArray<FStardustBasic> RequiredStardust;
    
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task/Stardust")
    TArray<FStardustBasic> LackedStardust;
    
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task")
    bool QuestCanBeCompleted{ false };
    
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task")
    TArray<FName> ChildTask;
    
    FQuest() = default;
};

Quest Management System

Quest Container Implementation

quest board maintains active player missions with standard CRUD operations:

UFUNCTION(BlueprintCallable, Category = "QuestBoard")
TArray<int> GetQuestIndexFromStardustId(const FName StardustId)

UFUNCTION(BlueprintCallable, Category = "QuestBoard")
bool AddQuest(const FQuest ExpectedQuest);

UFUNCTION(BlueprintCallable, Category = "QuestBoard")
bool RemoveQuest(const int Index);

UFUNCTION(BlueprintCallable, Category = "QuestBoard")
FQuest QueryQuestByName(const FString Name) const;

Progress Tracking

Mission progress updates automtaically when inventory changes occur:

bool UTaskComponent::CalculateLackStardust(UStarInventoryComponent* Inventory, const int Index)
{
    if (Index < 0 || Index >= QuestArray.Num())
    {
        UE_LOG(LogTemp, Error, TEXT("CalculateLackStardust at %d failed, invalid index"), Index);
        return false;
    }
    
    if (!IsValid(Inventory))
    {
        UE_LOG(LogTemp, Error, TEXT("CalculateLackStardust failed, invalid pointer:Inventory"));
        return false;
    }
    
    bool completionStatus = true;
    
    for (int i = 0; i < QuestArray[Index]->TaskInformation.RequiredStardust.Num(); i++)
    {
        int deficit = std::max(0, QuestArray[Index]->TaskInformation.RequiredStardust[i].Quantity - 
                              Inventory->CheckStardust(QuestArray[Index]->TaskInformation.RequiredStardust[i].StardustId));
        
        QuestArray[Index]->TaskInformation.LackedStardust[i].Quantity = deficit;
        
        if (deficit > 0)
        {
            completionStatus = false;
        }
    }
    
    QuestArray[Index]->TaskInformation.QuestCanBeCompleted = completionStatus;
    return completionStatus;
}

Random Event Framework

Event Data Structure

Random events inherit from data table rows for editor configuration:

USTRUCT(BlueprintType)
struct FNovaBurstEventInfo : public FTableRowBase
{
    GENERATED_BODY()
    
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NovaBurstEventInfo")
    FString EventName{ "Empty" };
    
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NovaBurstEventInfo")
    ENovaBurstEventType EventType{ ENovaBurstEventType::Empty };
    
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NovaBurstEventInfo")
    ENovaBurstEventEffect Effect{ ENovaBurstEventEffect::Other };
    
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NovaBurstEventInfo")
    int EventLevel{ 0 };
    
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NovaBurstEventInfo")
    double DefaultValue{ 1.0 };
    
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NovaBurstEventInfo")
    bool Randomable{ true };
    
    FNovaBurstEventInfo() = default;
};

Event Pool Initialization

Loading random events from data tables during game initialization:

void UAstromutateGameInstance::LoadPrimeEventDataTable()
{
    UDataTable* EventTablePointer = LoadObject<UDataTable>(nullptr, 
        UTF8_TO_TCHAR("DataTable'/Game/Data/NovaBurstEventInformation.NovaBurstEventInformation'"));
    
    PrimeEventPool.Empty();
    
    if (EventTablePointer == nullptr)
    {
        UE_LOG(LogTemp, Error, TEXT("Can't find PrimeEventInfo"));
        return;
    }
    
    TArray<FName> RowNames { EventTablePointer->GetRowNames() };
    
    for (const auto& rowName : RowNames)
    {
        FString ContextString;
        FNovaBurstEventInfo* Row = EventTablePointer->FindRow<FNovaBurstEventInfo>(rowName, ContextString);
        
        if (!Row->Randomable)
            continue;
            
        PrimeEventPool.Add(*Row);
    }
    
    UE_LOG(LogTemp, Warning, TEXT("PrimeEventMap Loaded %d event"), PrimeEventPool.Num());
}

Random Event Generation

Generating celestial body enhancement events with specified levels:

TArray<FEventWithRandomAster> APrime::GetRandomEvent(TArray<int> ExpectedLevels)
{
    std::random_device rd;
    std::default_random_engine rng { rd() };
    
    std::unordered_map<int, TArray<FNovaBurstEventInfo>> CurrentEventPool;
    
    UAstromutateGameInstance::RandomShuffle(ExpectedLevels);
    
    for (const auto& eventEntry : Instance->PrimeEventPool)
    {
        CurrentEventPool.emplace(eventEntry.first, TArray<FNovaBurstEventInfo>());
        for (const auto& eventData : *eventEntry.second)
        {
            CurrentEventPool[eventEntry.first].Add(eventData);
        }
    }
    
    auto result { TArray<FEventWithRandomAster>() };
    
    if (!Instance->IsValidLowLevel())
    {
        UE_LOG(LogTemp, Error, TEXT("GetRandomEvent failed, invalid pointer:Instance"));
        return result;
    }
    
    if (ExpectedLevels.IsEmpty())
    {
        UE_LOG(LogTemp, Error, TEXT("get random event failed, no expected levels, or game ended"));
        return result;
    }
    
    for (const auto& level : ExpectedLevels)
    {
        if (CurrentEventPool[level].IsEmpty())
        {
            UE_LOG(LogTemp, Error, TEXT("No valid event for level: %d"), level);
            continue;
        }
        
        std::uniform_int_distribution<int> dist { 0, CurrentEventPool[level].Num() - 1 };
        int randomIndex { dist(rng) };
        FName randomAster { "Empty" };
        
        if (CurrentEventPool[level][randomIndex].EventType == ENovaBurstEventType::UpgradeAster)
            randomAster = GetRandomUnlockedAster();
            
        result.Add(FEventWithRandomAster(randomAster, CurrentEventPool[level][randomIndex]));
        CurrentEventPool[level].RemoveAt(randomIndex);
    }
    
    return result;
}

Generic array shuffling algorithm ensuring uniform distribution:

template<typename T>
static void RandomShuffle(TArray<T>& Array)
{
    if (Array.IsEmpty())
        return;
        
    std::random_device rd;
    std::default_random_engine rng { rd() };
    std::uniform_int_distribution<int> dist { 0, Array.Num() - 1 };
    
    for (int i = 0; i < Array.Num(); i++)
    {
        int j { dist(rng) };
        std::swap(Array[i], Array[j]);
    }
}

Probability Distribution Calculation

Dynamic probability growth algroithm for rare event sampling:

double GetProbabilityGrowth(const double& ExpectedProbability, const int& MaxDrawTime)
{
    double minSearchValue { 0.000001 };
    double maxSearchValue { 1.0 };
    
    while (maxSearchValue - minSearchValue >= 1e-6)
    {
        double searchValue { (minSearchValue + maxSearchValue) / 2.0 };
        double currentProbability { searchValue };
        int drawCount { 0 };
        double expectation { 0.0 };
        double previousFailureProb { 1.0 };
        
        while (true)
        {
            drawCount++;
            expectation += drawCount * currentProbability * previousFailureProb;
            previousFailureProb *= (1.0 - currentProbability);
            currentProbability += searchValue * (drawCount + 1);
            
            if (currentProbability >= 1.0 || drawCount + 1 == MaxDrawTime)
            {
                expectation += (drawCount + 1) * previousFailureProb;
                break;
            }
        }
        
        double calculatedExpectedProb { 1.0 / expectation };
        
        if (calculatedExpectedProb >= ExpectedProbability)
        {
            maxSearchValue = searchValue;
        }
        else
        {
            minSearchValue = searchValue;
        }
    }
    
    return maxSearchValue;
}

Actor Placement Algorithm

Generating positions within valid construction zones avoiding overlaps:

FTransform ADebugActor::GetRandomTransformInRing(const int& MinimumRadius, const int& MaximumRadius) const
{
    std::random_device rd;
    std::default_random_engine rng { rd() };
    
    std::uniform_int_distribution dist { MinimumRadius, MaximumRadius + 5 * NebulasSpawnTriedTimes }, 
                                   dist2 { 0, 628 };
    
    FVector result { FVector(0, 0, 0) };
    int randomRadius { dist(rng) };
    int randomDegree { dist2(rng) };
    
    randomDegree /= 100;
    result.X = randomRadius * cos(randomDegree);
    result.Y = randomRadius * sin(randomDegree);
    
    auto resultTransform { FTransform(result) };
    resultTransform.SetScale3D(FVector(0.5, 0.5, 0.5));
    
    return resultTransform;
}

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.