Image that you are working with people who focusing on different fields, and you guys are discussing about the project around a blackboard, you can write your own ideas on it, and understand the ideas written by others.

The blackboard pattern is just like that, in game developping, AI modules seen have connections between every modules. Your AI character may need to know how many players in the game now, and how many HP left now, where can get ammo, where can get weapon, where is a wall to cover himself etc. If declaring variables in your AI Controller, it seems that it will become a diaster in coding.

As is known to us, AI uses state machine, behaviour tree and many other tools, they may need to share variables, including reading from other components and telling other components what it just dealed.

To archieve this, a good idea is using blackboard, for example, an AI perception component has seen a player, and write it in a blackboard which belongs the AI controller, and every task node in the state machine, every node in the behaviour tree can use it.

It looks likeĀ Mediator Pattern. Which allows multiple classes to gain or update data rapidly. But components using blackboard will not call functions belong to each other.

1. Blackboard in Unreal Engine

Implementing a simple blackboard, you just need a dictionary:

struct FSimpleBlackboardData
{
 FString Key;
 std::any Value;
};

UCLASS(BlueprintType)
class ADVANCEDALS_API USimpleBlackboard : public UObject
{
 GENERATED_BODY()
private:

 TMap<FString, std::any> BlackboardData;

public:

void SetValue(FString ValueKey, bool NewValue);
 void SetValue(FString ValueKey, int32 NewValue);
 void SetValue(FString ValueKey, float NewValue);

 bool GetBoolValue(FString ValueKey);
 int32 GetIntValue(FString ValueKey);
 float GetFloatValue(FString ValueKey);
};

This maybe a simplest blackboard, which uses std::any in C++17 to represent variables of any types. However, unreal engine uses a much more complex implementation, in this post, we are going to introduce to 4 most important classes : UBlackboardData, UBalckboardComponent, FBlackboardEntry and UBlackboardkeyType .

Let’s take a look at UBlackboardType, it is what the Blackboard editors deal.

The data of UBlackboardType is :

GENERATED_UCLASS_BODY()
 DECLARE_MULTICAST_DELEGATE_OneParam(FKeyUpdate, UBlackboardData* /*asset*/);

 /** parent blackboard (keys can be overridden) */
 UPROPERTY(EditAnywhere, Category=Parent)
 TObjectPtr<UBlackboardData> Parent;

#if WITH_EDITORONLY_DATA
 /** all keys inherited from parent chain */
 UPROPERTY(VisibleDefaultsOnly, Transient, Category=Parent)
 TArray<FBlackboardEntry> ParentKeys;
#endif

 /** blackboard keys */
 UPROPERTY(EditAnywhere, Category=Blackboard)
 TArray<FBlackboardEntry> Keys;

private:
UPROPERTY()
 uint32 bHasSynchronizedKeys : 1;

As is seen by us, there is a parent blackboard and a FBlackboardEntry array.

In Unreal Engine, a blackboard can have a parent blackboard, it will inherit every FBlackboardEntry of the parent blackboard, and allows to overwrite each entry.

But the question is: what is a FBlackboardEntry?

Well, A BlackboardEntry is just like this:

In c++, it is:

USTRUCT()struct FBlackboardEntry
{
 GENERATED_USTRUCT_BODY()

 UPROPERTY(EditAnywhere, Category=Blackboard)
 FName EntryName;

#if WITH_EDITORONLY_DATA
 UPROPERTY(EditAnywhere, Category=Blackboard, Meta=(ToolTip="Optional description to explain what this blackboard entry does."))
 FString EntryDescription;

 UPROPERTY(EditAnywhere, Category=Blackboard)
 FName EntryCategory;
#endif // WITH_EDITORONLY_DATA

 /** key type and additional properties */
 UPROPERTY(EditAnywhere, Instanced, Category=Blackboard)
 TObjectPtr<UBlackboardKeyType> KeyType;

 /** if set to true then this field will be synchronized across all instances of this blackboard */
 UPROPERTY(EditAnywhere, Category=Blackboard)
 uint32 bInstanceSynced : 1;

 FBlackboardEntry()
 : KeyType(nullptr), bInstanceSynced(0)
 {}

 bool operator==(const FBlackboardEntry& Other) const;
};

What is interesting is it has a ObjectPtr of UBlackboardkeyType called keyTypes, actually, it is where exactly the blackboard stores the value. Besides, there is nothing special.

The methods provided by UBlackboardData only contain how to get a FBlackboardEntry, what really used to set value and get value indirectly is provided by UBlackboardComponent, which finally call SetValue and GetValue of UBlackboardkeyType.

Let’s take a look at UBlackboardkeyType_float for example:

UCLASS(EditInlineNew, meta=(DisplayName="Float"))
class AIMODULE_API UBlackboardKeyType_Float : public UBlackboardKeyType
{
 GENERATED_UCLASS_BODY()

 typedef float FDataType;
 static const FDataType InvalidValue;

 static float GetValue(const UBlackboardKeyType_Float* KeyOb, const uint8* RawData);
 static bool SetValue(UBlackboardKeyType_Float* KeyOb, uint8* RawData, float Value);

 virtual EBlackboardCompare::Type CompareValues(const UBlackboardComponent& OwnerComp, const uint8* MemoryBlock,
 const UBlackboardKeyType* OtherKeyOb, const uint8* OtherMemoryBlock) const override;

 virtual FString DescribeArithmeticParam(int32 IntValue, float FloatValue) const override;

protected:
virtual FString DescribeValue(const UBlackboardComponent& OwnerComp, const uint8* RawData) const override;
 virtual bool TestArithmeticOperation(const UBlackboardComponent& OwnerComp, const uint8* MemoryBlock, EArithmeticKeyOperation::Type Op, int32 OtherIntValue, float OtherFloatValue) const override;
};

what is interesting is GetValue() and SetValue() method, it seems weird that they are static methods instead of a member methods. To explain that, we need to take a look at how to get or update a value in UBlackboardComponent:

template<class TDataClass>
typename TDataClass::FDataType UBlackboardComponent::GetValue(FBlackboard::FKey KeyID) const
{
 const FBlackboardEntry* EntryInfo = BlackboardAsset ? BlackboardAsset->GetKey(KeyID) : nullptr;
 if ((EntryInfo == nullptr) || (EntryInfo->KeyType == nullptr) || (EntryInfo->KeyType->GetClass() != TDataClass::StaticClass()))
 {
 return TDataClass::InvalidValue;
 }

 UBlackboardKeyType* KeyOb = EntryInfo->KeyType->HasInstance() ? KeyInstances[KeyID] : EntryInfo->KeyType;
 const uint16 DataOffset = EntryInfo->KeyType->HasInstance() ? sizeof(FBlackboardInstancedKeyMemory) : 0;

 const uint8* RawData = GetKeyRawData(KeyID) + DataOffset;
 return RawData ? TDataClass::GetValue((TDataClass*)KeyOb, RawData) : TDataClass::InvalidValue;
};

It uses TDataClass::GetValue, TDataClass::SetValue and TDataClass::FDataType, The compiler connot know what the exactly type is when you call this function, for UBlackboardKeyType_Float, we will it is a float type value, and for UBlackboardKeyType_bool is a boolean type value.

At first, I treat it as a This is a advanced used of templates of C++, you can use T::MethodName or T::VariableName, the compiler knows the exactly type during compilation time.

class StaticTest
{
public:
static int FuckNumber;
 static void PrintHello()
 {
 std::cout << "Hello";
 }

 void PrintHelloNonStatic()
 {
 std::cout << "HelloNonStatic" << '\n';
 }
};

template<class TDataClass>
void PrintHello(TDataClass* TDC)
{
 TDC->PrintHelloNonStatic();
}

int main(int argc, char* argv[])
{
 StaticTest* ST = new StaticTest;
 PrintHello<StaticTest>(ST);
}

It is a pity that I did’t know it before, it is so useful.

Back to the UBlackboardKeyType_Float, it actually stores the 8bit-address and the size of the value stored by it, by these two values, you can easily get and set the value by operating the memory directly.

2. Customizing your UBlackboardKeyType

The ai plugin – smart object, developed by epic extends UBlackboardKeyType to support a new type SOHandle, we will learn how to costomizing our type.

First, declare a class inherits from UBlackboardKeyType:

UCLASS(EditInlineNew, DisplayName=Double)
class ADVANCEDALS_API UBlackboardKeyType_Double : public UBlackboardKeyType
{
 GENERATED_UCLASS_BODY()

 typedef double FDataType;
 static const FDataType InvalidValue;

 static double GetValue(const UBlackboardKeyType_Double* KeyOb, const uint8* RawData);
 static bool SetValue(UBlackboardKeyType_Double* KeyOb, uint8* RawData, float Value);

 virtual EBlackboardCompare::Type CompareValues(const UBlackboardComponent& OwnerComp, const uint8* MemoryBlock,
 const UBlackboardKeyType* OtherKeyOb, const uint8* OtherMemoryBlock) const override;
 virtual FString DescribeArithmeticParam(int32 IntValue, float DoubleValue) const override;

protected:

virtual FString DescribeValue(const UBlackboardComponent& OwnerComp, const uint8* MemoryBlock) const override;
 virtual bool TestArithmeticOperation(const UBlackboardComponent& OwnerComp, const uint8* MemoryBlock,
 EArithmeticKeyOperation::Type Op, int32 OtherIntValue, float OtherDoubleValue) const override;
};

Implements construct function and GetValue() and SetValue() first, it is modified by GENERATED_UCLASS_BODY(), doesn’t need a contruction function declaration.

UBlackboardKeyType_Double::UBlackboardKeyType_Double(const FObjectInitializer& ObjectInitializer) : UBlackboardKeyType(ObjectInitializer)
{
 ValueSize = sizeof(double);
 SupportedOp = EBlackboardKeyOperation::Arithmetic;
}

double UBlackboardKeyType_Double::GetValue(const UBlackboardKeyType_Double* KeyOb, const uint8* RawData)
{
 return GetValueFromMemory<double>(RawData);
}

bool UBlackboardKeyType_Double::SetValue(UBlackboardKeyType_Double* KeyOb, uint8* RawData, float Value)
{
 return SetValueInMemory(RawData, Value);
}

Implements DecribeValue and DescribeArithmeticParam, these are for UI displaying, reqest for a return value of FString type:

FString UBlackboardKeyType_Double::DescribeArithmeticParam(int32 IntValue, float DoubleValue) const{
 return FString::Printf(TEXT("%lf"), DoubleValue);
}

FString UBlackboardKeyType_Double::DescribeValue(const UBlackboardComponent& OwnerComp, const uint8* MemoryBlock) const
{
 return FString::Printf(TEXT("%lf"), GetValue(this, MemoryBlock));
}

We set SupportedOp to Arithmetic before, means that it joins comparion as a arithmetic number, all the op tpyes are as followed:

namespace EBlackboardKeyOperation{
 enum Type
 {
 Basic,
 Arithmetic,
 Text,
 };
}

Finally, implements TestArithmeticOperation and CompareValues. You have to implement TestArithmeticOperation if SupportedOp is EBlackboardKeyOperation::Arithmetic.

EBlackboardCompare::Type UBlackboardKeyType_Double::CompareValues(const UBlackboardComponent& OwnerComp, const uint8* MemoryBlock, const UBlackboardKeyType* OtherKeyOb, const uint8* OtherMemoryBlock) const
{
 const float MyValue = GetValue(this, MemoryBlock);
 const float OtherValue = GetValue((UBlackboardKeyType_Double*)OtherKeyOb, OtherMemoryBlock);

 return (FMath::Abs(MyValue - OtherValue) < KINDA_SMALL_NUMBER) ? EBlackboardCompare::Equal :
(MyValue > OtherValue) ? EBlackboardCompare::Greater :
EBlackboardCompare::Less;
}

bool UBlackboardKeyType_Double::TestArithmeticOperation(const UBlackboardComponent& OwnerComp, const uint8* MemoryBlock,
 EArithmeticKeyOperation::Type Op, int32 OtherIntValue, float OtherFloatValue) const
{
 const float Value = GetValue(this, MemoryBlock);
 switch (Op)
 {
 case EArithmeticKeyOperation::Equal: return (Value == OtherFloatValue);
 case EArithmeticKeyOperation::NotEqual: return (Value != OtherFloatValue);
 case EArithmeticKeyOperation::Less: return (Value < OtherFloatValue);
 case EArithmeticKeyOperation::LessOrEqual: return (Value <= OtherFloatValue);
 case EArithmeticKeyOperation::Greater: return (Value > OtherFloatValue);
 case EArithmeticKeyOperation::GreaterOrEqual: return (Value >= OtherFloatValue);
 default: break;
 }

 return false;
}

Restart the editor, and now you can see:

 

By JiahaoLi

Hypergryph - Game Programmer 2023 - Now Shandong University - Bachelor 2019-2023

Leave a Reply

Your email address will not be published. Required fields are marked *