Portals in Unreal

In Unreal 4.20 by Sascha de Waal

Introduction

Unreal Engine offers great tools for AI. The NavMesh and the build-in behavior tree are very powerful and easy to work with. However, adding portals for teleporting can be a bit problematic. We needed that originally in our game, but online we couldn't find a lot of information about it. In this tutorial I want to explain how we made portals for the AI in the Unreal game Engine.

You could use NavLink Proxy but the distance between the portals is very limited and in my experience they are a bit buggy. In this tutorial we are going to add a layer to the engine so that we keep using the NavMesh and behavior manager.

In this tutorial the AI can teleport from every portal, to every portal. But adding connections to it later, is not too difficult.

You need to have some knowledge about NavMesh, Behavior tree, and c++ in Unreal to understand this tutorial. If this is not the case, you should start exploring the following subjects.

And keep using Google!

The global Idea

The idea is that we going to create a class that inherence of AIController with a function called MoveToWithPortals(FVector Target). When called, it will calculate the direct path cost (without the use of the portals) and calculate the path cost to the closest portal, and the path cost from the target location to it's closest portal.

When the direct path cost is lower than the cost to the closest portal + target to it's closest portal cost, then we don't need to use portals. But if this is not the case, or there is no path possible then the AI will use portals.

When using portals we first should send the AI to the closest portal. When reach, teleport the AI to the next portal and then send it to the target.

For simplicity we assume every portal is connected to every other portal.

Example steps:

  1. Calculate the path cost without portals (Green line). The total cost is 10
  2. Search for the closest portal to the AI and the closest to the target.
  3. Now we calculates the total distance when we use the portals (Blue path). So that is 2 + 3 = 5
  4. If the direct path cost is less than the total cost of using portals, then we just move to the target. But in this case its better to use portals. Direct cost is 10, while the use of portals is 5.
  5. MoveTo of the ai controlled is called to the first teleport location
  6. When portal is reached, we teleport the AI to the next portal.
  7. We call MoveTo to the target location
  8. Call a Callback so the sender knows that the AI has reach its destination

So this is the idea. Of course you can add things like teleport animation or delays but for simplicity I didn't add those to the tutorial. Before we can start building this we first need a world for the AI to life in.

The world

First we need a world where the AI can walk around. Maybe you already have a world, if not you should build one. For the purpose of this tutorial I build a simple block out but I hope yours looks way better than mine.

This world also needs a Nav Mesh Bounds Volume. Drag the volume from the Modes panel to the world, hit P and move and scale it so the volumes matches the walking area of the AI.

Read more about the Nav Mesh Volume here: https://api.unrealengine.com/INT/Engine/AI/BehaviorTrees/QuickStart/2/index.html

When you have a world with obstacles and a navmesh, we can go to the next step.

Build Modules

We are going to use some features of the NavigationSystem and GameplayTasks modules later on in this tutorial. To be able to use these features we first need to add the modules to the Dependency Module.

Open [YourProjectName].Build.cs in Visual Studio. It should already exist. Find the line with PublicDependencyModuleNames.AddRange Here you see a array with all modules that are included. Add NavigationSystem and GameplayTasks to this array.

using UnrealBuildTool;

public class AIWithPortals : ModuleRules {
    public AIWithPortals(ReadOnlyTargetRules Target) : base(Target) {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

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

Portals

Before we can build the teleport logic, we first need portals. Portals won't really have a active role in this system other than being at a location.

First create a c++ class with AActoras parent. We will call it ANavigationPortal

The only thing we want to do in this class is to make sure it has a USceneComponent as root component so it will have a location. The file should looks like this:

Header

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "NavigationPortal.generated.h"

UCLASS()
class AIWITHPORTALS_API ANavigationPortal : public AActor {
    GENERATED_BODY()

public: 
    ANavigationPortal();

protected:
    UPROPERTY(Category = "Teleport", VisibleDefaultsOnly, BlueprintReadOnly)
    USceneComponent* SceneComponent = nullptr;
};

Cpp

#include "NavigationPortal.h"

ANavigationPortal::ANavigationPortal() {
    PrimaryActorTick.bCanEverTick = false;

    SceneComponent = CreateDefaultSubobject<USceneComponent>(FName("Root"));
    RootComponent  = SceneComponent;
}

If you want the option to disable portals you should add it in this class. Add a boolean that is called bIsEnabled. When we are looking for the closest portal, you first should check is this boolean is True before using it.

To add visuals you have a few different options to choose from. You can place the NavigationPortals in an other visual actors in the world (useful if you work with level streaming) or like I am doing now, create a blueprint with NavigationPortals as parent and add a visual within this blueprint.

Open the Blueprint and add a visual with this added we know where the portal is placed. I will use a plate but feel free to do something better.

Make sure the model doesn't have collision enabled. The AI should be able to walk into it. And turn off Can Ever Affect Navigation so the navmesh won't be influenced.

The last step of preparing the portals is placing them in the world. Make sure that the portals are reachable by the AI and placed on the NavMesh.

AI Movement

We finally can start by working on the movement logic descripted in The Global Idea. Lets start with creating a AI Controller that will run the logic.

Create a C++ class with AIController as parent. Call it AIWithPortalsController

Open the created file and add a function called MoveToWithPortalswith a constant FVector as parameter. This function should be blueprint callable.

Header

#pragma once

#include "CoreMinimal.h"
#include "AIController.h"
#include "AIWithPortalsController.generated.h"

UCLASS()
class AIWITHPORTALS_API AAIWithPortalsController : public AAIController {
    GENERATED_BODY()

public:
    UFUNCTION(Category = "Teleporting", BlueprintCallable)
    virtual void MoveToWithPortals(const FVector TargetLocation);

};

CPP

#include "AIWithPortalsController.h"

void AAIWithPortalsController::MoveToWithPortals(const FVector TargetLocation) {

}

This function can be called when you want to move the AI to a location while using the portals if that is faster option.

[^1]: We could override MoveTo and replace the movement logic there. However I think it's better to add a layer to the system instead of hacking the logic inside the engine.

Later, we need to compare the travel cost inbetween different locations. To make this easier, we are going to make a function that can compare the costs. So add GetPointToPointCost and CreateMoveRequest as private functions to the class.

To calculate the path cost, we first need have a move request. This is what the function CreateMoveRequest will do. It will return a FAIMoveRequest with all information that is used to calculate a path. Some of the data that we use for the move request is hardcoded. These default variables should be stored somewhere else but to keep this tutorial easy I keep the variables inside the function.

GetPointToPointCost will create the path with use of the created move request and return the path cost.

Below you see the code that is doing this.

Header

#include "AIController.h"
#include "AIWithPortalsController.generated.h"

UCLASS()
class AIWITHPORTALS_API AAIWithPortalsController : public AAIController {
    GENERATED_BODY()

public:
    UFUNCTION(Category = "Teleporting", BlueprintCallable)
    virtual void MoveToWithPortals(const FVector TargetLocation);

private:
    const float GetPointToPointCost(FVector StartPoint, FVector EndPoint) const;
    const FAIMoveRequest CreateMoveRequest(FVector EndPoint) const;

};

Cpp

#include "AIWithPortalsController.h"
#include "NavigationSystem.h"
#include "NavigationSystemTypes.h"

void AAIWithPortalsController::MoveToWithPortals(const FVector TargetLocation) {

}

/** Get the travel cost betwheen two locations on the navmesh */
const float AAIWithPortalsController::GetPointToPointCost(FVector StartPoint, FVector EndPoint) const {

    FAIMoveRequest MoveRequest = CreateMoveRequest(EndPoint);
    FPathFindingQuery PFQuery;
    FNavPathSharedPtr Path;
    const bool bValidQuery = BuildPathfindingQuery(MoveRequest, PFQuery);

    if (bValidQuery) {

        PFQuery.StartLocation = StartPoint;
        PFQuery.EndLocation = EndPoint;

        FindPathForMoveRequest(MoveRequest, PFQuery, Path);

        if (Path.IsValid()) {
            return Path->GetCost();
        }
    }

    // Path is not found, return -1.
    return -1;
}

/** Create a MoveRequest for pathfinding */
const FAIMoveRequest AAIWithPortalsController::CreateMoveRequest(FVector EndPoint) const {

    // It would be better to store these values somewhere else
    FAIMoveRequest MoveReq(EndPoint);
    MoveReq.SetUsePathfinding(true);
    MoveReq.SetAllowPartialPath(true);
    MoveReq.SetProjectGoalLocation(true);
    MoveReq.SetNavigationFilter(DefaultNavigationFilterClass);
    MoveReq.SetAcceptanceRadius(40);
    MoveReq.SetReachTestIncludesAgentRadius(true);
    MoveReq.SetCanStrafe(true);

    return MoveReq;
}

Now we have a method of getting the travel cost between two location, we can start working on the first step of the logic: Checking the direct distance without using the portals. We do this in the MoveToWithPortals. It's a simple as calling our created function.

void AAIWithPortalsController::MoveToWithPortals(const FVector TargetLocation) {

    FVector AILocation = GetPawn()->GetActorLocation();
    float DirectDistance = GetPointToPointCost(AILocation, TargetLocation);

}

The second step is finding the closest portal to the AI and the closest to the target. To find this we need a list of all portals in the world. We will do that in BeginPlayhowever if you need the list to be dynamic, you need to update the list.

Override BeginPlay in the header and add the private variable called Portals. This should be a TArray<ANavigationPortal*> and it will store all portals in the world.

Add the logic you see below to your code.

Header


protected:
    virtual void BeginPlay() override;

private:
    TArray<ANavigationPortal*> Portals = TArray<ANavigationPortal *>();

Cpp

void AAIWithPortalsController::BeginPlay() {
    Super::BeginPlay();

    // Get all actors if type ANavigationPortal
    TArray<AActor*> FoundActors = TArray<AActor*>();
    UGameplayStatics::GetAllActorsOfClass(GetWorld(), ANavigationPortal::StaticClass(), FoundActors);

    // Cast and Add the portals to the array
    for (AActor* Actor : FoundActors) {
        Portals.Add(Cast<ANavigationPortal>(Actor));
    }
}

Now we are going to create a function that will search for the closest portal from a particular location. We will call this function GetClosestTeleport and it will return the closest portal and the travel cost. We need the travel cost for comparison later.

The idea is that we are going to loop thought all portals in the world. We will have a variable called closestPortal. We compare every portal path cost to this portal. When the cost is lower then closestPortal, we will set closestPortalto that portal. At the end we will return closestPortal.

Header

private:
   const TPair<ANavigationPortal*, float> GetClosestTeleport(const FVector Point, const float MaxDistance = -1.f) const;

Cpp

const TPair<ANavigationPortal*, float> AAIWithPortalsController::GetClosestTeleport(const FVector Point, const float MaxCost) const {
    TPair<ANavigationPortal*, float> closestPortal = TPair<ANavigationPortal*, float>(nullptr, -1.f);

    for (ANavigationPortal* Portal : Portals) {
        if (Portal) {
            float PathCost = GetPointToPointCost(Point, Portal->GetActorLocation());

            // Check if there is a path ( > 0)
            // Check if the path cost is not bigger then the MaxCost
            // check if the cost is lower then the last one, or the last one is null.
            if (PathCost > 0 && PathCost < MaxCost && (closestPortal.Value > PathCost || closestPortal.Value < -1)) {
                closestPortal = TPair<ANavigationPortal*, float>(Portal, PathCost);
            }
        }
    }

    return closestPortal;
}

We need to do one final thing before we can finishes step 2. Add private variable called MinDistanceToTeleport. If the direct distance to the target is smaller than this, we don't have to look if a portal is better the AI just start walking to the target.

private:
    const float MinDistanceToTeleport = 1000;

Let's use GetClosestTeleport function now for step 2: Finding the closest portal to the AI and the target location. and while we are doing this, let's also add the check if the portal usage cost is lower then direct walking.

Add this logic to MoveToWithPortals

void AAIWithPortalsController::MoveToWithPortals(const FVector TargetLocation) {

    FVector AILocation = GetPawn()->GetActorLocation();
    float DirectDistance = GetPointToPointCost(AILocation, TargetLocation);

    if ((DirectDistance < 0.f || DirectDistance > MinDistanceToTeleport)) {
        TPair<ANavigationPortal*, float> AIPortal = GetClosestTeleport(AILocation, DirectDistance);
        TPair<ANavigationPortal*, float> PlayerPortal = GetClosestTeleport(TargetLocation, DirectDistance);

        if (DirectDistance < 0 || AIPortal.Value + PlayerPortal.Value < DirectDistance) {
            // We should use portals
        }

    }
}

Step 2 and step 3 is finished. But let's add the logic for when it is faster to walk:

void AAIWithPortalsController::MoveToWithPortals(const FVector TargetLocation) {

    FVector AILocation = GetPawn()->GetActorLocation();
    float DirectDistance = GetPointToPointCost(AILocation, TargetLocation);

    if ((DirectDistance < 0.f || DirectDistance > MinDistanceToTeleport)) {
        TPair<ANavigationPortal*, float> AIPortal = GetClosestTeleport(AILocation, DirectDistance);
        TPair<ANavigationPortal*, float> TargetPortal = GetClosestTeleport(TargetLocation, DirectDistance);

        if (DirectDistance < 0 || AIPortal.Value + TargetPortal.Value < DirectDistance){
            // Start using the portals

            return;
        }

    }

    // If a check failed above, move directly to target.
    MoveToLocation(TargetLocation);
}

When it's smarter to use the portal, the next steps will be this:

  1. Move to the closest portal
  2. Teleport to second portal
  3. Move from portal to target

So when we send the AI to the fist portal, we need to know when the AI has reach that location. To do this, we are going to override the function OnMoveCompleted from AIControllerThis will be called when a movement is done.

void AAIWithPortalsController::OnMoveCompleted(FAIRequestID RequestID, const FPathFollowingResult& Result) {
    Super::OnMoveCompleted(RequestID, Result);
}

Depending on what MoveToWithPortals decided, we need to do a few different things here. We should store some variables to know what must happened when OnMoveCompleted is called. Add these private variables to the header.

private:
    bool bIsWalkingToPortal = false;
    ANavigationPortal* NextPortal = nullptr;
    FVector AfterTeleportGoal;

And we need to set these variables in the MoveToWithPortals function and we can send the AI to the first portal.

void AAIWithPortalsController::MoveToWithPortals(const FVector TargetLocation) {

    FVector AILocation = GetPawn()->GetActorLocation();
    float DirectDistance = GetPointToPointCost(AILocation, TargetLocation);

    if ((DirectDistance < 0.f || DirectDistance > MinDistanceToTeleport)) {
        TPair<ANavigationPortal*, float> AIPortal = GetClosestTeleport(AILocation, DirectDistance);
        TPair<ANavigationPortal*, float> TargetPortal = GetClosestTeleport(TargetLocation, DirectDistance);

        if (DirectDistance < 0 || AIPortal.Value + TargetPortal.Value < DirectDistance) {
            bIsWalkingToPortal = true;
            NextPortal = TargetPortal.Key;
            AfterTeleportGoal = TargetLocation;

            MoveToLocation(AIPortal.Key->GetActorLocation());
            return;
        }

    }

    // If a check failed above, move directly to target.
    bIsWalkingToPortal = false;
    MoveToLocation(TargetLocation);
}

When the AI has reach the first portal, OnMoveCompleted is called. Here we should teleport the AI to the next location and send it to the target location.

void AAIWithPortalsController::OnMoveCompleted(FAIRequestID RequestID, const FPathFollowingResult& Result) {

    if (bIsWalkingToPortal) {

        // make sure this won't happend on the next move
        bIsWalkingToPortal = false;

        //Teleport ai to the next portal
        GetPawn()->SetActorLocation(NextPortal->GetActorLocation());

        //Send ai to the target location
        MoveToLocation(AfterTeleportGoal);
    }

    Super::OnMoveCompleted(RequestID, Result);
}

From this point the AI movement should be working. You can send the AI to a location and it will use the portals if that is faster. However, we also should know when the AI has reach it's end goal. So we still need to do that.

We start with declaring a delegate in the header file. Do this on the top above the class definition

DECLARE_DYNAMIC_DELEGATE_TwoParams(FAIMoveCompletedWithPortalsSignature, FAIRequestID, RequestID, EPathFollowingResult::Type, Result);

The idea is that we are going to create a Callback function. So add a parameter to MoveToWithPortals called Callback

virtual void MoveToWithPortals(const FVector TargetLocation, const FAIMoveCompletedWithPortalsSignature& Callback);

and we need to store the callback somewhere because we need to call it later. For this, add a private variable called LastCallback

private:
    FAIMoveCompletedWithPortalsSignature LastCallback;

Let's set this variable in the MoveToWithPortals

void AAIWithPortalsController::MoveToWithPortals(const FVector TargetLocation, const FAIMoveCompletedWithPortalsSignature& Callback) {

    FVector AILocation = GetPawn()->GetActorLocation();
    float DirectDistance = GetPointToPointCost(AILocation, TargetLocation);
    LastCallback = Callback;

    if ((DirectDistance < 0.f || DirectDistance > MinDistanceToTeleport)) {
        TPair<ANavigationPortal*, float> AIPortal = GetClosestTeleport(AILocation, DirectDistance);
        TPair<ANavigationPortal*, float> TargetPortal = GetClosestTeleport(TargetLocation, DirectDistance);

        if (DirectDistance < 0 || AIPortal.Value + TargetPortal.Value < DirectDistance) {
            bIsWalkingToPortal = true;
            NextPortal = TargetPortal.Key;
            AfterTeleportGoal = TargetLocation;

            MoveToLocation(AIPortal.Key->GetActorLocation());
            return;
        }

    }

    // If a check failed above, move directly to target.
    bIsWalkingToPortal = false;
    MoveToLocation(TargetLocation);
}

Last thing is that this callback should be called when the movement is done. We do that in the OnMoveCompleted function

void AAIWithPortalsController::OnMoveCompleted(FAIRequestID RequestID, const FPathFollowingResult& Result) {

    if (bIsWalkingToPortal) {
        bIsWalkingToPortal = false;

        GetPawn()->SetActorLocation(NextPortal->GetActorLocation());
        MoveToLocation(AfterTeleportGoal);
        return;
    }

    LastCallback.ExecuteIfBound(RequestID, Result.Code);
    LastCallback.Clear();
    Super::OnMoveCompleted(RequestID, Result);
}

So this part is working now. The code should looks like this:

AIWithPortalsController.h

#pragma once

#include "CoreMinimal.h"
#include "AIController.h"
#include "AIWithPortalsController.generated.h"

class ANavigationPortal;

DECLARE_DYNAMIC_DELEGATE_TwoParams(FAIMoveCompletedWithPortalsSignature, FAIRequestID, RequestID, EPathFollowingResult::Type, Result);

UCLASS()
class AIWITHPORTALS_API AAIWithPortalsController : public AAIController {
    GENERATED_BODY()

public:
    UFUNCTION(Category = "Teleporting", BlueprintCallable)
    virtual void MoveToWithPortals(const FVector TargetLocation, const FAIMoveCompletedWithPortalsSignature& Callback);
    virtual void OnMoveCompleted(FAIRequestID RequestID, const FPathFollowingResult& Result) override;

protected:
    virtual void BeginPlay() override;

private:
    const float GetPointToPointCost(FVector StartPoint, FVector EndPoint) const;
    const FAIMoveRequest CreateMoveRequest(FVector EndPoint) const;
    const TPair<ANavigationPortal*, float> GetClosestTeleport(const FVector Point, const float MaxDistance = -1.f) const;

    TArray<ANavigationPortal*> Portals = TArray<ANavigationPortal *>();
    const float MinDistanceToTeleport = 1000;

    bool bIsWalkingToPortal = false;
    ANavigationPortal* NextPortal = nullptr;
    FVector AfterTeleportGoal;
    FAIMoveCompletedWithPortalsSignature LastCallback;
};

AIWithPortalsController.cpp

#include "AIWithPortalsController.h"
#include "NavigationSystem.h"
#include "NavigationSystemTypes.h"
#include "Kismet/GameplayStatics.h"
#include "NavigationPortal.h"

void AAIWithPortalsController::BeginPlay() {
    Super::BeginPlay();

    TArray<AActor*> FoundActors = TArray<AActor*>();
    UGameplayStatics::GetAllActorsOfClass(GetWorld(), ANavigationPortal::StaticClass(), FoundActors);

    for (AActor* Actor : FoundActors) {
        Portals.Add(Cast<ANavigationPortal>(Actor));
    }
}

void AAIWithPortalsController::MoveToWithPortals(const FVector TargetLocation, const FAIMoveCompletedWithPortalsSignature& Callback) {

    FVector AILocation = GetPawn()->GetActorLocation();
    float DirectDistance = GetPointToPointCost(AILocation, TargetLocation);
    LastCallback = Callback;

    if ((DirectDistance < 0.f || DirectDistance > MinDistanceToTeleport)) {
        TPair<ANavigationPortal*, float> AIPortal = GetClosestTeleport(AILocation, DirectDistance);
        TPair<ANavigationPortal*, float> TargetPortal = GetClosestTeleport(TargetLocation, DirectDistance);

        if (DirectDistance < 0 || AIPortal.Value + TargetPortal.Value < DirectDistance) {
            bIsWalkingToPortal = true;
            NextPortal = TargetPortal.Key;
            AfterTeleportGoal = TargetLocation;

            MoveToLocation(AIPortal.Key->GetActorLocation());
            return;
        }

    }

    // If a check failed above, move directly to target.
    bIsWalkingToPortal = false;
    MoveToLocation(TargetLocation);
}

void AAIWithPortalsController::OnMoveCompleted(FAIRequestID RequestID, const FPathFollowingResult& Result) {

    if (bIsWalkingToPortal) {
        bIsWalkingToPortal = false;

        GetPawn()->SetActorLocation(NextPortal->GetActorLocation());
        MoveToLocation(AfterTeleportGoal);
        return;
    }

    LastCallback.ExecuteIfBound(RequestID, Result.Code);
    LastCallback.Clear();
    Super::OnMoveCompleted(RequestID, Result);
}

const float AAIWithPortalsController::GetPointToPointCost(FVector StartPoint, FVector EndPoint) const {

    FAIMoveRequest MoveRequest = CreateMoveRequest(EndPoint);
    FPathFindingQuery PFQuery;
    FNavPathSharedPtr Path;
    const bool bValidQuery = BuildPathfindingQuery(MoveRequest, PFQuery);

    if (bValidQuery) {

        PFQuery.StartLocation = StartPoint;
        PFQuery.EndLocation = EndPoint;

        FindPathForMoveRequest(MoveRequest, PFQuery, Path);

        if (Path.IsValid()) {
            return Path->GetCost();
        }
    }

    // Path is not found, return -1.
    return -1;
}

const FAIMoveRequest AAIWithPortalsController::CreateMoveRequest(FVector EndPoint) const {

    FAIMoveRequest MoveReq(EndPoint);
    MoveReq.SetUsePathfinding(true);
    MoveReq.SetAllowPartialPath(true);
    MoveReq.SetProjectGoalLocation(true);
    MoveReq.SetNavigationFilter(DefaultNavigationFilterClass);
    MoveReq.SetAcceptanceRadius(40);
    MoveReq.SetReachTestIncludesAgentRadius(true);
    MoveReq.SetCanStrafe(true);

    return MoveReq;
}

const TPair<ANavigationPortal*, float> AAIWithPortalsController::GetClosestTeleport(const FVector Point, const float MaxCost) const {
    TPair<ANavigationPortal*, float> closestPortal = TPair<ANavigationPortal*, float>(nullptr, -1.f);

    for (ANavigationPortal* Portal : Portals) {
        if (Portal) {
            float PathCost = GetPointToPointCost(Point, Portal->GetActorLocation());

            // Check if there is a path ( > 0)
            // Check if the path cost is not bigger then the MaxCost
            // check if the cost is lower then the last one, or the last one is null.
            if (PathCost > 0 && PathCost < MaxCost && (closestPortal.Value > PathCost || closestPortal.Value < -1)) {
                closestPortal = TPair<ANavigationPortal*, float>(Portal, PathCost);
            }
        }
    }

    return closestPortal;
}

Finishing and Testing the system

To test if the system work, we first need a Character for the AI. Create a blueprint with Character as parent. I am going to call it BP_AICharacter

Open the Character and set AIWithPortalsController as the controller of this character.

Also, make sure there is some sort of visuals of the character. I am going to use a cube but any mesh will help. But make sure it doesn't effect the NavMesh.

For testing purpose, we are going to make a ugly blueprint. When its turns out everything is working, you can delete it.

Go to the event graph and create this graph

What we are doing here is on BeginPlay we send the AI to a hard coded location. If we place the AI correctly and set the target location correct, we will see the AI is using portals. DoneMoving is a custom event that is called when the movement is done. The text "Done" should be appearing for a short time on your screen.

Place this character in the world, make sure using the portals is faster than waking directly, set the target location in the blueprint and hit play.

By checking how the AI is behaving when it should walk directly. Make sure the text "Done" is appearing on the screen. Otherwise the callback is not responding correcly.

You now have a AI controller that is using portals! The next step is creating a custom behavior node for if you use the behavior tree. If you are not using the behavior tree, you can stop now.

Behavior node

So if you want to be able to use this movement logic inside the behavior tree, you must create a custom node. We are going to do that here. It should behave the same way as MoveTo: You send the AI to a blackboard value location.

Read more about custom behavior nodes: http://api.unrealengine.com/INT/API/Runtime/AIModule/BehaviorTree/UBTTaskNode/index.html

Create a C++ class with BTTASK_BlackboardBase as parent. Call is BTTask_MoveToWithPortals

What this node is going to do is very simple. On Execute it should call MoveToWithPortals with the location of the blackboard value. When the location has been reach, it should call FinishLatentTask to finishes the task.

So open the created file and type this

Header

#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/Tasks/BTTask_BlackboardBase.h"
#include "Navigation/PathFollowingComponent.h"
#include "BTTask_MoveToWithPortals.generated.h"

UCLASS()
class AIWITHPORTALS_API UBTTask_MoveToWithPortals : public UBTTask_BlackboardBase {
    GENERATED_BODY()

public:
    UBTTask_MoveToWithPortals(const FObjectInitializer& ObjectInitializer);

protected:
    UFUNCTION()
    void OnTagetReach(FAIRequestID RequestID, EPathFollowingResult::Type Result);

private:
    virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
    const FVector GetTarget(UBlackboardComponent* BlackboardComp) const;

    UBehaviorTreeComponent* BehaviorTreeComponent;
};

Cpp

#include "BTTask_MoveToWithPortals.h"
#include "BehaviorTree/BehaviorTreeComponent.h"
#include "AIWithPortalsController.h"
#include "BehaviorTree/Blackboard/BlackboardKeyType_Object.h"
#include "BehaviorTree/Blackboard/BlackboardKeyType_Vector.h"
#include "BehaviorTree/BlackboardComponent.h"

UBTTask_MoveToWithPortals::UBTTask_MoveToWithPortals(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) {
    NodeName = "Move To (With use of portals)";
}

// Called when this task is Execute
EBTNodeResult::Type UBTTask_MoveToWithPortals::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) {
    AAIController* Controller = OwnerComp.GetAIOwner();
    AAIWithPortalsController* AIWithPortalsController = Cast<AAIWithPortalsController>(Controller);
    UBlackboardComponent* Blackboard = OwnerComp.GetBlackboardComponent();
    BehaviorTreeComponent = &OwnerComp;

    if (AIWithPortalsController) {
        FAIMoveCompletedWithPortalsSignature FinishedDelegate;
        FinishedDelegate.BindDynamic(this, &UBTTask_MoveToWithPortals::OnTagetReach);
        AIWithPortalsController->MoveToWithPortals(GetTarget(Blackboard), FinishedDelegate);

        return EBTNodeResult::InProgress;
    } else {
        UE_LOG(LogTemp, Warning, TEXT("You can only use 'MoveToWhileUsingPortals' on a AAIWithPortalsController"));
        return EBTNodeResult::Failed;
    }
}

const FVector UBTTask_MoveToWithPortals::GetTarget(UBlackboardComponent* BlackboardComp) const {
    if (!BlackboardKey.IsSet()) {
        UE_LOG(LogTemp, Warning, TEXT("Target not set"));
        return FVector();
    }

    if (BlackboardKey.SelectedKeyType == UBlackboardKeyType_Object::StaticClass()) {
        UObject* KeyValue = BlackboardComp->GetValue<UBlackboardKeyType_Object>(BlackboardKey.GetSelectedKeyID());
        AActor* TargetActor = Cast<AActor>(KeyValue);

        return TargetActor->GetActorLocation();
    }

    if (BlackboardKey.SelectedKeyType == UBlackboardKeyType_Vector::StaticClass()) {
        const FVector TargetLocation = BlackboardComp->GetValue<UBlackboardKeyType_Vector>(BlackboardKey.GetSelectedKeyID());

        return TargetLocation;
    }

    return FVector();
}

void UBTTask_MoveToWithPortals::OnTagetReach(FAIRequestID RequestID, EPathFollowingResult::Type Result) {
    FinishLatentTask(*BehaviorTreeComponent, EBTNodeResult::Type::Succeeded);
}

Now you should be able to use the node in the behavior tree

The usage of this node is the same as MoveTo. You need to assign a blackboard value to it. When the node is executed it will send the AI to that location. Only this node will make sure the AI is using the portals.

Notes

I designed this system for a game we are working on. However, the system we are using for the game is more complex. The system supports teleport animation, portals can be destroyed and not all portals are connected to every other portals. There is also a lot of performance things that can be improved. But I want to make this tutorial very easy and I hope you can implement the things you need into this system.