Saturday, September 17, 2016

Unreal Engine Quest Framework Part 1.5

After spending some time on vacation and on an epic honeymoon, I have finally had time to get back to continue my original Quest Framework series. But, first, let me interrupt and thank everyone out there for their support. This is by far the most popular content I have ever written and it brings me great joy that you support it and find it useful.

With all the time off, and the time back at my day job, I have not been able to fully complete the second part of my Quest Framework series. However, I do have an addition I would like to share that may be useful. This addition is a new class that generates random Objectives from a predefined list of objectives inside of a Quest.


Quest Framework Part 1.5: Random Objectives


Want your players to look this excited for your quests? Don't use this framework for evil, no randomly generated fetch quests allowed!


Following the format of the last post, I will layout the header file for you, then explain some of the variables. From there I will layout the definitions and explain all of the logic.

The Head


class QUESTGAME_API AQuestRandom : public AQuest
{
    GENERATED_BODY()

public:
    AQuestRandom();

    virtual void BeginPlay() override;
 
protected:
    UPROPERTY(EditDefaultsOnly, Category = "Quest")
         TArray<TSubclassOf<AObjective>> PossibleObjectives;

    UPROPERTY(EditDefaultsOnly, Category = "Quest", meta=(UIMin=1))
         int32 MinObjectivesGenerated;

    UPROPERTY(EditDefaultsOnly, Category = "Quest", meta=(UIMin=1))
        int32 MaxObjectivesGenerated;

private:
    /** Helper methods for validation and generation.
    * ::GenerateRandomObjectives() could be made public 
    * and wrapped with UFUNCTION(BlueprintCallable, Category="Quest") to make it exposed to the rest of your game.
    */
    void GenerateRandomObjectives();
    bool IsValidConfiguration() const; 
 
};

Pretty nifty huh? We just inherit from AQuest (defined in the original Part 1 post) and add a few configuration parameters that can be set in either an inherited class or Blueprint.

PossibleObjectives is the meat of this class. Just add AObjective subclasses to it in the derived Blueprint or subclass and whenever the AQuestRandom is spawned in game, the class will do all the work of adding some random Objectives.

MinObjectivesGenerated and MaxObjectivesGenerated are there to help add some constraints to the amount of randomly generated quests. In proper fashion, these numbers are just constraints on the random determination of the amount of random Objectives created. I heard people like randomness on top of their randomness so I threw it in just for fun.

Next, I will show you the source for this nifty class and explain the true meat of the randomization algorithm (which isn't too difficult at all).



The Source


Free stock photo of technology, computer, desktop, programming

#include "QuestRandom.h"

AQuestRandom::AQuestRandom() :
    AQuest(),
    MinObjectivesGenerated(AbsoluteMin),
    MaxObjectivesGenerated(AbsoluteMin)
{

}

void AQuestRandom::BeginPlay()
{
    if (IsValidConfiguration())
    {
        GenerateRandomObjectives();
    }
    //Must be called last as we need to fill the Objective subclass array first
    Super::BeginPlay(); 
}

void AQuestRandom::GenerateRandomObjectives()
{
    const bool bMakeOnlyOne = MinObjectivesGenerated == MaxObjectivesGenerated;
    if (bMakeOnlyOne)
    {
        int32 RandomIndex = FMath::RandRange(0, PossibleObjectives.Num() - 1);
        Objectives.Add(PossibleObjectives[RandomIndex]);
    }
    else
    {
        int32 RandomCount = FMath::RandRange(MinObjectivesGenerated, MaxObjectivesGenerated);
        for (int32 i = (AbsoluteMin - 1); i < RandomCount; ++i) 
        {
            int32 RandomIndex = FMath::RandRange(0, PossibleObjectives.Num() - 1);
            /* Note: if you want only one type of quest to be active at a time,
            * i.e. treat Objective types array as a set, use AddUnique
            */
            Objectives.Add(PossibleObjectives[RandomIndex]);
        }
    }
}

bool AQuestRandom::IsValidConfiguration() const
{
    return MinObjectivesGenerated <= MaxObjectivesGenerated && PossibleObjectives.Num() > 0;
}

Awesome right? We really only need one function to add randomly generated Objectives to our Quests! Huzzah! So, let us look immediately into the GenerateRandomObjectives function as it is the crux of our class.

First, if we have the same MinObjectivesGenerated and MaxObjectivesGenerated, we are going to short circuit and say the designer only wants to pick one random Objective for this Quest. It is also the default behavior due to the values set in the Constructor of the class.

If the designer does not go with this default behavior we will actually do some work.

So, if we really look at it, the algorithm is not that hard to implement. All we need is to grab a random number clamped to the values provided to us from MinObjectivesGenerated and MaxObjectivesGenerated. From there we just iterate as many times as the RandomCount has defined to create that many Objectives.

In both configurations we just grab a random number starting at 0 (the starting index of an array) and PossibleObjectives.Num() - 1 (we subtract one to ensure we are within the bounds of the array as index access is 0 based but the Num() starts at 1 - this is a general rule).

From there, the base class takes care of everything else for us.

The End


And there you have it. Random objective generation. So, hopefully you can add some more content and unique playthroughs of your games via random Objectives in your Quests.

Until next time!

Note - I will have an official part 2 for this series that will have subsections. I am currently prototyping with the idea of implementing quests as state machines and message publishers to decouple a lot of the quest implementation from your actual game-play code. On top of that I am going to implement it as a plugin with components so that you can port it over multiple games. Why? Because 'Quests', 'Objectives', and 'Missions' are really all the same thing and can be written generic enough that the core foundation of them work across all game types.