Implementing Quest System and Random Events in UE5 C++ for Stardust Mutation
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;
}