Unreal Robots

Alright, now we need a hovering and tracking AI boss character! It's time for us to dive into the UE AI layer. The UE4 AI implementation is robust and covers everything you will need to create intelligent, well-functioning AI. Much like everything else in UE4, the AI interface can be expanded and modified using C++ objects that inherit from the UE4-provided base classes. The next section of this chapter will run you through the basics of the AI system.

AI breakdown

The AI toolset present in UE4 is made up of many different asset types and objects. Each object is responsible for a certain subset of the AI's functionality. The following list provides a brief description of the major elements that go into creating an AI in UE4:

  • AI Characters: AI character objects and normal character objects do not differ at all. In fact, they can derive from the same UCharacter base class. An AI character is simply controlled by an AIController instead of a PlayerController. In this sense any character can be an AI character or a player character.
  • Behavior Trees: Behavior trees are the visual representation of the decision-making structure of your AI. It is within these trees that you will plot all of the logic flow for the AI and the resulting decisions of this logic. Behavior trees are made up of seven unique node types—Decorators, Services, Tasks, Selectors, Sequences, Simple Parallel, and a root node. These seven elements will be described in detail later in the chapter.
  • AI Controllers: AI controllers are responsible for the movement of the AI character after all navigation calculations have been made. In other words, it is through this controller that the character is controlled by the governing AI, via the execution of various methods (Move To, for example). You may create your own AI controllers within which you can create your own movement functions to better articulate your AI during runtime. AI controllers will still use the movement properties of a character's CharacterMovementComponent.
  • Blackboard: Blackboard is a very key feature of the AI system. It allows you to create variables that can be retrieved and assigned by ALL AI elements within a given system. This means we are able to set a flag in a blackboard from an AI controller that will dictate a logic-making decision in a behavior tree as long as both have reference to the same blackboard asset.
  • Navigational Meshes: NavMesh for short, are a way we can identify an area for an AI to move within, which accommodates for obstacle avoidance, pathing, and patrolling. NavMesh will handle the pathing generation for any specified AI movement agents within the area covered by a NavMesh volume. This asset is not directly referenced by the others mentioned previously but is a core component of the AI system within UE4.

During the rest of this chapter we are going to be making our own abstractions of some of these objects. Thanks to the dependence of these assets within themselves, we are able to create an efficient bi-directional network of objects. Through this network we are able to easily create any AI behavior we can conceive.

Preparing your project for AI intergration

Over the course of the next two chapters we are going to be referencing and including a new series of objects and functions that are part of UE4's AI module. To ensure that we have no linking or #include problems going forward, it is a good idea to explicitly add this module as a public dependency to our project's build.cs. Navigate to the BossMode.Build.cs and ensure that the public dependency modules name list matches the following:

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "AIModule" });

As you can see, we have included AIModule to the list, this will ensure that all of the objects contained within this module are linked publically to our project. With this in place we begin to work the backend for our boss character. We will start by creating a basic AI character that is based in C++.

Creating the AI character base class

Much like our standard player characters, we require an object that will represent our boss character. AI characters are created in much the same manner as standard player characters, however they are not possessed by player controllers but AI controllers instead. Start by creating a new C++ class using the wizard. The base class for this new object is to be Character. Call this new object BMBossCharacter. This abstraction of the UCharacter base class is going to be very simple. We are going to be including a handle to a Behavior Tree asset that can be assigned from the editor.

Navigate to BMBossCharacter.h now and simply modify the contents of the definition to match the following:

UCLASS()
class BOSSMODE_API ABMBossCharacter : public ACharacter
{
    GENERATED_BODY()

public:
    // Sets default values for this character's properties
    ABMBossCharacter();

    virtual float TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;

    UFUNCTION(BlueprintCallable, BlueprintPure, Category = AI)
    const float GetHealth();

    UPROPERTY(EditAnywhere, Category = Pawn)
    class UBehaviorTree* BehaviorTree;

private:

    UPROPERTY()
    float Health;
};

As you can see, we have removed the Tick(), Begin Play(), and SetupPlayerInputComponent() methods as they will not be used by our AI-controlled character. Ensure you delete the corresponding methods from the BMBossCharacter.cpp otherwise the project will not compile. We have also added some methods. As you can see, we have overridden the TakeDamage() method. This method takes in the various parameters that make up a damage event in UE4. We will describe this in more detail later. Following this we have added a standard accessor method for the Health of this character (a private member declared below).

The last thing to note is that we have added a handle to a UBehaviorTree. As this is not a UBehaviorTreeComponent we do not need to initialize the handle with anything, we will simply be assigning the correct UBehaviorTree asset from the Details panel in editor. Now we will define these functions, so navigate to BMBossCharacter.cpp.

Define the constructor for this object as follows:
// Sets default values
ABMBossCharacter::ABMBossCharacter()
{
    // Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = true;

    Health = 100.0f;

    GetMovementComponent()->NavAgentProps.AgentHeight = 320.0f;
    GetMovementComponent()->NavAgentProps.AgentRadius = 160.0f;
}

Here we are simply flagging that this character is to tick, setting the health of this character to 100.0f, and establishing some of the parameters for this AI-controlled character's navigation agent properties. These navigation agent properties are used by the navigation system to accurately plot pathing points when generating path-following behavior for AI characters—More on this later. Next, define the TakeDamage() method as follows:

float ABMBossCharacter::TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
    Super::TakeDamage(Damage, DamageEvent, EventInstigator, DamageCauser);

    Health -= Damage;

    return Damage;
}

This method simply subtracts the damage value parsed from the Health property. We will be using this value later to influence the decision making of this AI character. Finally, we need to define the GetHealth() accessor method as follows:

const float ABMBossCharacter::GetHealth()
{
    return Health;
}

Now we can create the AI controller that will drive the movement of our boss!

Creating your first AI controller

Much like that of the ABMBossCharacter, this controller will be a very basic abstraction and provide us with a common place to store handles to the various AI assets we will eventually create and use for the boss AI character. Don't worry, these assets will receive their own detailed explanation shortly. Start by creating a new class using the C++ wizard. This class is to inherit from the AIController class provided by UE4 and call it BMAIController. Navigate to BMAIController.h now. Modify the class definition to match the following:

UCLASS()
class BOSSMODE_API ABMAIController : public AAIController
{
    GENERATED_BODY()

public:
    ABMAIController();
    virtual void Possess(APawn* InPawn) override;
    
    UBlackboardComponent* GetBlackboard();

protected:
    UPROPERTY(BlueprintReadWrite, Category = Behavior)
    class UBlackboardComponent* BlackboardComp;

    UPROPERTY(BlueprintReadWrite, Category = Behavior)
    class UBehaviorTreeComponent* BehaviorTreeComp;	
};

Here we are simply providing the object with a default constructor, overriding the Possess() method (which is part of the AAIController interface), and declaring handles to the blackboard and behavior tree components that will be used by our AI. We have also included a public accessor method for the UBlackboardComponent called GetBlackboard(). It is with the creation of this object that we begin to see the network of our AI assets form. Now navigate to the BMAIController.cpp.

Firstly, we need to update the include list of this object to accommodate the UBlackboardComponent and UBehaviorTreeComponent types as well as the ABMBossCharacter type we just created. Do this by adding the following #includes:

#include "BMBossCharacter.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "BehaviorTree/BehaviorTree.h"

Next we are to define the constructor for this object. Within the constructor we are simply going to initialize the two components via CreateDefaultSubobject:

ABMAIController::ABMAIController()
{
    BlackboardComp = CreateDefaultSubobject<UBlackboardComponent> (TEXT("BlackboardComp"));
    check(BlackboardComp);
    BehaviorTreeComp = CreateDefaultSubobject <UBehaviorTreeComponent>(TEXT("BehaviorTreeComp"));
    check(BehaviorTreeComp);
}

Following this we must define the override for the Possess() method we declared earlier. The Possess() method is responsible for providing control of a character to a given controller. We can also utilize this function to retrieve the blackboard and behavior tree assets from the possessed pawn. As not every pawn has a behavior tree and corresponding blackboard asset, we need to ensure that the pawn to be possessed is of type ABMBossCharacter. We can do this with the following code:

Void ABMAIController::Possess(APawn* InPawn)
{
    Super::Possess(InPawn);

ABMBossCharacter* possessedPawn = Cast<ABMBossCharacter>(InPawn);
    if (InPawn != nullptr)
    {
        BlackboardComp->InitializeBlackboard(*(possessedPawn->
        BehaviorTree->BlackboardAsset));

        BehaviorTreeComp->StartTree(*(possessedPawn->
        BehaviorTree));

    }<-- closing if(InPawn != nullptr)
}<-- closing ABMAIController::Possess()

This code will attempt to cast the parsed APawn to an ABMBossCharacter object. If the cast succeeds, the possessedPawn pointer will be populated and the following if statement will pass. Within the scope of this if, check we get access to the UBehaviorTree handle we declared in the ABMBossCharacter earlier. With this handle we can initialize the UBlackboardComponent BlackboardComp via passing a const reference to the blackboard asset associated with the behavior tree to the InitailzeBlackboard method. This will ensure that any queries made to BlackboardComp about variable states will retrieve information from the blackboard asset assigned to the behavior tree we set in the ABMBossCharacter.

Next, we start the behavior tree by parsing a const reference to the behavior tree to the StartTree() method of the BehaviorTreeComponent. This will ensure that the tree that has been assigned to this pawn starts upon possession and the implemented AI behavior will take place.

The last thing we need to add is a definition for the GetBlackboard() accessor method we declared earlier:

UBlackboardComponent* ABMAIController::GetBlackboard()
{
    return BlackboardComp;
}

Behavior tree breakdown and logic flow

Much like a Blueprint, a behavior tree is a grouping of connected nodes that, when fired, will perform some kind of functionality. Unlike Blueprints, we have advanced control over the flow of our behavior tree and the order in which the nodes execute. Node execution in behavior trees is dictated by the way the nodes are connected, and if the previous nodes have either succeeded or failed. Success or failure of a node is gauged by the node's ability to perform its given action. When a node fails its parent node, the previous node in the logic flow will be informed of the failure. This may result in the entire tree failing or simply a single branch of the tree failing, resulting in the logic flow of the tree to shift.

Before we go ahead and begin to make behavior trees of our own, it is important to understand the basic components that make up a behavior tree. As stated previously, there are several important pieces to behavior trees and they are described as follows:

  • Root Node: Simply the entry point for the behavior tree, all BT's (behavior trees) will have a root node and provide us with a clear entry point for the tree. If a tree ever fails (a command is hit that cannot be carried out or is aborted), the tree will reset from the root node.
  • Task Nodes: Tasks are operations that the AI can perform. Tasks are things such as Move To, Rotate, and Wait. Tasks can also be created from scratch by either creating a blueprint that inherits from the task base class, or a C++ object that does the same.
  • Service Nodes: Service nodes are things that can perform at a certain frequency, for example every 0.5 seconds. It is with services that we can get our AI to perform ticking actions or ticking checks. For example, a good use of a service is to check every 0.3 seconds whether an enemy is close enough to the AI for the AI to engage with that enemy. Much like tasks, these can be created from scratch via C++ or blueprint.
  • Selector Nodes: Selector nodes are utilized in the flow control of the behavior tree. You can have multiple child nodes connected to a selector node. The selector node will parse the logic flow to its child nodes from left to right until one succeeds (meaning child nodes of that child also succeed, and so forth). This means we are able to create behavior trees that execute a given branch based on the success or failure of other branches. It is important to note that if all children of a selector node fail, the selector node itself fails.
  • Sequence Nodes: Sequence nodes are also utilized in the flow control of the behavior tree, but differ from selector nodes. Sequence nodes will execute all children from right to left until one child fails. This means we can create a series of steps that must execute in order. If one of the steps fails, the order is stopped and the tree is reset. If all of the child nodes succeed, the sequence succeeds!
  • Simple Parallel Nodes: Simple parallel nodes can boast two children: One a task node and the other can be a complete subtree if desired. This allows us to perform one task while having other behaviors running in the background. The task child must be of a single task with optional decorators.
  • Decorator Nodes: Decorators are attached to the aforementioned node types (apart from Root) and act as conditional checks for the nodes to execute. For example, you can have a decorator that checks whether the AI is close enough to a desired location. If this decorator passes, the attached node can execute. If not, the attached node fails and the following nodes will not be executed. This is a great way to control logic flow within your behavior tree. For example, you may have a selector node that has three children, each with their own decorator. If child 1's decorator fails, then child 2 will be checked, if this fails then child 3, and so forth.

What are blackboards?

As described previously, blackboards provide us with a way to create a set of values that we can write to and read from within any of the associated AI assets or objects. This is done through blackboard keys. Blackboard keys are identifiers that allow us to obtain any given value (or variable) within a Blackboard asset. For example, we can have a key saved in our blackboard called HomeLocation that is of type vector. We can now retrieve this vector from our blackboard by specifying we wish to retrieve the value HomeLocation from the blackboard asset as a vector type. The use of keys provides us with the ability to retrieve and set data types held in our blackboard from anywhere within our AI network.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.54.242