• 03/25/16 02:14 PM
    Sign in to follow this  

    Unreal Engine 4 C++ Quest Framework

    General and Gameplay Programming

    Martin H Hollstein

    Lately, I have been working on a simple horror game in UE4 that has a very simple Objective system that drives the gameplay. After looking at the code, I realized it could serve as the basis of a framework for a generic questing system. Today I will share all of that code and explain each class as it pertains to the framework. The following classes to get started on a simple quest framework are [tt]AQuest[/tt] and [tt]AObjective[/tt], using the UE4 naming conventions for classes. [tt]AObjective[/tt] is metadata about the quest as well the actual worker when it comes to completing parts of a quest. [tt]AQuest[/tt] is a container of objectives and does group management of objectives. Both classes are derived from [tt]AInfo[/tt] as they are purely classes of information and do not need to have a transform or collision within the world. Objectives: Since it is the foundation for a quest, I will first layout and explain [tt]AObjective[/tt]. The header of [tt]AObjective[/tt] goes as follows: [code] // Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "GameFramework/Info.h" #include "Objective.generated.h" UCLASS() class QUESTGAME_API AObjective : public AInfo { GENERATED_BODY() public: // Sets default values for this actor's properties AObjective(); // Called when the game starts or when spawned virtual void BeginPlay() override; // Called every frame virtual void Tick( float DeltaSeconds ) override; UPROPERTY( EditDefaultsOnly, BlueprintReadOnly, Category = "O" ) FText Description; UPROPERTY( EditDefaultsOnly, BlueprintReadOnly, Category = "O" ) FName ObjectiveName; UPROPERTY( EditDefaultsOnly, BlueprintReadOnly, Category = "O" ) bool MustBeCompletedToAdvance; UPROPERTY( EditDefaultsOnly, BlueprintReadOnly, Category = "O" ) int32 TotalProgressNeeded; UPROPERTY( EditDefaultsOnly, BlueprintReadOnly, Category = "O" ) int32 CurrentProgress; UFUNCTION( BlueprintCallable, Category = "O" ) void Update( int32 Progress ); UFUNCTION( BlueprintCallable, Category = "O" ) virtual bool IsComplete( ) const; UFUNCTION( BlueprintCallable, Category = "O" ) virtual float GetProgress( ) const; }; [/code] Not that bad of a deal. The only responsibilities of an [tt]AObjective[/tt] is to track the completion of the sub-portion of an [tt]AQuest[/tt] and offer some idea of what the player must do. The objective is tracked by the [tt]CurrentProgress[/tt] and [tt]TotalProgressNeeded[/tt] properties. Added by the supplied helper functions, [tt]Update[/tt], [tt]IsComplete[/tt], and [tt]GetProgress[/tt], we can get a reasonable amount of data about this tiny portion of a quest. These functions give you all the functionality needed to start a questing framework for your UE4 game. There is one boolean property that has not been mentioned: [tt]MustBeCompletedToAdvance[/tt]. Depending on the use case, this could be used to ensure a sequential order in objectives or having required and optional objectives. I will implement it as the first in this tutorial. Only minor changes later on are needed to use it as an indicator of optional or required quests. Or, you could just add a new property to support both. There are two more properties that help us out with [tt]AObjective[/tt] management: [tt]ObjectiveName[/tt] and [tt]Description[/tt]. [tt]ObjectiveName[/tt] can be thought of as a unique identifier for the implemented [tt]AObjective[/tt]. The [tt]ObjectiveName[/tt]'s purpose is for player feedback. For instance, the [tt]FText[/tt] value could be (in simple string terms) "Get a rock". It is nothing specific to the game, it is only something to be used as a hint in either a UI or other visual element to let the player know that they need to do something in order to complete the objective. Next, we can look at the small amount of code that is used to define [tt]AObjective[/tt]. [code] // Fill out your copyright notice in the Description page of Project Settings. #include "QuestGame.h" #include "Objective.h" // Sets default values AObjective::AObjective( ) : Description( ), ObjectiveName( NAME_None ), TotalProgressNeeded( 1 ), CurrentProgress( 0 ), MustBeCompletedToAdvance( true ) { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; } // Called when the game starts or when spawned void AObjective::BeginPlay() { Super::BeginPlay(); } // Called every frame void AObjective::Tick( float DeltaTime ) { Super::Tick( DeltaTime ); } void AObjective::Update( int32 Progress ) { CurrentProgress += Progress; } bool AObjective::IsComplete( ) const { return CurrentProgress >= TotalProgressNeeded; } float AObjective::GetProgress( ) const { check( TotalProgressNeeded != 0 ) return (float)CurrentProgress / (float)TotalProgressNeeded; } [/code] Again, you will be hard pressed to say "that is a lot of code". Indeed, the most complex code is the division in the [tt]GetProgress[/tt] function. Wait, why do we call/override [tt]BeginPlay[/tt] or [tt]Tick[/tt]? Well, that is an extreme implementation detail. For instance, what if, while an [tt]AObjective[/tt] is active, you want to tick a countdown for a time trialed [tt]AObjective[/tt]. For [tt]BeginPlay[/tt] we could implement various other details such as activating certain items in the world, spawning enemies, and so on and so forth. You are only limited by your code skills and imagination. Right, so how do we manage all of these objectives and make sure only relevant [tt]AObjectives[/tt] are available? Well, we implement an [tt]AQuest[/tt] class in which it acts as an [tt]AObjective[/tt] manager. Quests: Here is the declaration of an [tt]AQuest[/tt] to get you started: [code] // Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "GameFramework/Info.h" #include "Quest.generated.h" UCLASS() class QUESTGAME_API AQuest : public AInfo { GENERATED_BODY() public: // Sets default values for this actor's properties AQuest(); // Called when the game starts or when spawned virtual void BeginPlay() override; // Called every frame virtual void Tick( float DeltaSeconds ) override; public: UPROPERTY( EditDefaultsOnly, BlueprintReadWrite, Category = "Q" ) TArray CurrentObjectives; UPROPERTY( EditDefaultsOnly, BlueprintReadWrite, Category = "Q" ) TArray> Objectives; UPROPERTY( EditDefaultsOnly, BlueprintReadOnly, Category = "Q" ) USoundCue* QuestStartSoundCue; UPROPERTY( EditDefaultsOnly, BlueprintReadOnly, Category = "Q" ) FName QuestName; UPROPERTY( EditDefaultsOnly, BlueprintReadOnly, Category = "Q" ) FText QuestStartDescription; UPROPERTY( EditDefaultsOnly, BlueprintReadOnly, Category = "Q" ) FText QuestEndDescription; UFUNCTION( BlueprintCallable, Category = "Q" ) bool IsQuestComplete( ) const; UFUNCTION( BlueprintCallable, Category = "Q" ) bool CanUpdate( FName Objective ); UFUNCTION( BlueprintCallable, Category = "Q" ) void Update( FName Objective, int32 Progress ); UFUNCTION( BlueprintCallable, Category = "Q" ) bool TryUpdate( FName Objective, int32 Progress ); UFUNCTION( BlueprintCallable, Category = "Q" ) float QuestCompletion( ) const; }; [/code] Not much bigger than the [tt]AObjective[/tt] class is it? This is because all [tt]AQuest[/tt] does is wrap around a collection of [tt]AObjective[/tt]'s and provides some utility functions to help manage them. The [tt]Objectives[/tt] property is a simple way to configure an [tt]AQuest[/tt]'s objectives via the Blueprints Editor, and the [tt]CurrentObjectives[/tt] is a collection of all live [tt]AObjective[/tt]'s that are configured for the given [tt]AQuest[/tt]. There are several user-friendly properties such as a [tt]USoundCue[/tt], [tt]FName[/tt], and [tt]FText[/tt] types that help give audio visual feedback to the player. For instance, when a player starts a quest, a nice sound plays - like a chime - and the [tt]QuestStartDescription[/tt] text is written to the player's HUD and a journal implementation. Then, when a player completes a quest, a [tt]get[/tt] is called for the [tt]QuestEndDescription[/tt] property and writes it to a journal implementation. But those are all specific implementation details related to your game and is limited only by coding skills and imagination. All of the functions for [tt]AQuest[/tt] are wrappers to operate on collections of [tt]AObjectives[/tt] to update and query for completion. All [tt]AObjectives[/tt] in the [tt]AQuest[/tt] are referenced and found by [tt]FName[/tt] property types. This allows for updating different instances of [tt]AObjectives[/tt] that are essentially the same, but differ at the data level. It also allows the removal of managing pointers. As another argument, it decouples knowledge of what an [tt]AObjective[/tt] object is from other classes, so completing quests via other class implementations only requires the knowledge of an [tt]AQuest[/tt] - or the container for the [tt]AQuest[/tt] - object and default types supplied by the engine such as [tt]int32[/tt] and [tt]FName[/tt]. How does this all work? Well, just like before, here is the definition of [tt]AQuest[/tt]: [code] // Fill out your copyright notice in the Description page of Project Settings. #include "QuestGame.h" #include "Objective.h" #include "Quest.h" AQuest::AQuest() : QuestName( NAME_None ), CurrentObjectives( ), QuestStartDescription( ), QuestEndDescription( ) { } void AQuest::BeginPlay() { Super::BeginPlay(); UWorld* World = GetWorld(); if ( World ) { for ( auto Objective : Objectives ) { CurrentObjectives.Add(World->SpawnActor(Objective)); } } } // Called every frame void AQuest::Tick( float DeltaTime ) { Super::Tick( DeltaTime ); } bool AQuest::IsQuestComplete() const { bool result = true; for ( auto Objective : CurrentObjectives ) { result &= Objective->IsComplete(); } return result; } bool AQuest::CanUpdate( FName Objective ) { bool PreviousIsComplete = true; for ( auto Obj : CurrentObjectives ) { if ( PreviousIsComplete ) { if ( Objective == Obj->ObjectiveName ) return true; else PreviousIsComplete = Obj->IsComplete() | !Obj->MustBeCompletedToAdvance; } else { return false; } } return true; } void AQuest::Update( FName Objective, int32 Progress ) { for ( auto Obj : CurrentObjectives ) { if ( Obj->ObjectiveName == Objective ) { Obj->Update( Progress ); return; } } } bool AQuest::TryUpdate( FName Objective, int32 Progress ) { bool result = CanUpdate( Objective ); if ( result ) { Update( Objective, Progress ); } return result; } float AQuest::QuestCompletion( ) const { int32 NumObjectives = CurrentObjectives.Num( ); if( NumObjectives == 0 ) return 1.0f; float AggregateCompletion = 0.0f; for( auto Objective : CurrentObjectives ) { AggregateCompletion += Objective->GetProgress( ); } return AggregateCompletion / (float)NumObjectives; } [/code] Probably the most complex code out of all of this is the [tt]CanUpdate[/tt] method. It checks to see, sequentially (so order of [tt]AObjective[/tt] configuration matters), if an [tt]AObjective[/tt] is completed and if it is required to complete any other [tt]AObjectives[/tt] after it. This is where the bitwise OR comes in. So basicly, we cannot advance to the requested [tt]AObjective[/tt] if any of the previous [tt]AObjectives[/tt] are not complete and are set to [tt]MustBeCompletedToAdvance[/tt] (or as the listeral code says you CAN advance if the previous [tt]AObjective[/tt] IS complete OR it does not required to be completed in order to advance). The [tt]IsComplete[/tt] function is just and aggregate check to see if all [tt]AObjectives[/tt] are complete - defining a completed [tt]AQuest[/tt]. The [tt]QuestCompletion[/tt] method is a simple averaging of all [tt]AObjective[/tt] completion percentages. Also, the [tt]AQuest[/tt] class has a simple function to wrap up the [tt]CanUpdate[/tt] and [tt]Update[/tt] calls into one neat little function called [tt]TryUpdate[/tt]. This allows a check for the ability to update before applying the requested progress update and returns an indicator of success or failure. This is useful when code outside of [tt]AQuest[/tt] wants to attempt [tt]AObjective[/tt] updates without caring about much else. Finally, for the same reason of [tt]AObjective[/tt]'s [tt]BeginPlay[/tt] and [tt]Tick[/tt] functions, [tt]AQuest[/tt] also overrides these to allow your coding skills and imagination to fly. Hopefully, this was a good introduction into the groundwork of designing a questing framework for your Unreal Engine 4 game. If you did enjoy it, comment or like it. If there is enough interest I will continue onwards with Part II: Nesting Quests and Objectives. That part will be a tutorial just like this, with full code samples, explaining how to structure the framework to nest multiple AObjectives into an [tt]AObjective[/tt] to create a structure of sub-objectives as well as the same pattern applied to [tt]AQuest[/tt] to supply sub-quests. Originally posted on Blogspot

    Update! A link to adding Random Objective generation using this framework.


      Report Article
    Sign in to follow this  


    User Feedback

    Create an account or sign in to leave a review

    You need to be a member in order to leave a review

    Create an account

    Sign up for a new account in our community. It's easy!

    Register a new account

    Sign in

    Already have an account? Sign in here.

    Sign In Now


    EMascheG

    Report ·

      

    Share this review


    Link to review
    Gian-Reto

    Report ·

      

    Share this review


    Link to review
    Envy123

    Report ·

      

    Share this review


    Link to review
    Navyman

    Report ·

      

    Share this review


    Link to review
    Brain

    Report ·

      

    Share this review


    Link to review
    RaoulJWZ

    Report ·

      

    Share this review


    Link to review