In this section, we are going to create a new behaviour module: a the follow target module.
Set up Atoms
Add the AtomsUnreal plugin to your unreal projects.
Open the Plugins dialog from the Edit menu. Go inside the “Installed” section and enable the AtomsUnreal plugin.
Before you can start to create creating the behaviour module, you need to add the AtomsUnreal and AtomsRealtime dependency to your build config file.
Open your Build.cs file in your project, then add the “AtomsUnreal“ and “AtomsRealtime” modules.
Code Block | ||
---|---|---|
| ||
using UnrealBuildTool; public class AtomsDemo : ModuleRules { public AtomsDemo(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; PublicDependencyModuleNames.AddRange( new string[] { "Core", "CoreUObject", "Engine", "InputCore", "AtomsRealtime", "AtomsUnreal" }); PrivateDependencyModuleNames.AddRange(new string[] { }); } } |
Create the Atoms module
In your project, create a new header file called MyFollowTargetBehaviourModule.h.
In this file, create a new class and inherit from the base Atoms::BehaviourModule class. Define a constructor, a destructor and a static creator function (needed by Atoms to create an instance of this module).
Code Block | ||
---|---|---|
| ||
#pragma once #include <Atoms/BehaviourModule.h> class MyFollowTargetModule: public Atoms::BehaviourModule { public: MyFollowTargetModule(); virtual ~MyFollowTargetModule(); static Atoms::BehaviourModule* creator(const std::string& parameter); }; |
Now add a MyFollowTargetBehaviourModule.cpp file to your project.
In this file, implement the constructor, destructor and creator functions.
Code Block | ||
---|---|---|
| ||
#include "MyFollowTargetBehaviourModule.h" MyFollowTargetModule::MyFollowTargetModule() : Atoms::BehaviourModule() { } MyFollowTargetModule::~MyFollowTargetModule() { } Atoms::BehaviourModule* MyFollowTargetModule::creator(const std::string& parameter) { return new MyFollowTargetModule(); } |
Register the behaviour module
Now you need to register your new module. Go inside the StartupModule() function of your game module and register your new behaviour module.
Code Block | ||
---|---|---|
| ||
#include "MyFollowTargetBehaviourModule.h" #include <Atoms/BehaviourModules.h> void FAtomsDemoGameModule::StartupModule() { Atoms::BehaviourModules& moduleManager = Atoms::BehaviourModules::instance(); moduleManager.registerBehaviourModule( "MyFollowTarget", &MyFollowTargetModule::creator, Atoms::BehaviourModules::kNative ); } |
Now de-register the module inside the ShutdownModule() function of your game module.
Code Block | ||
---|---|---|
| ||
void FAtomsDemoGameModule::ShutdownModule() { Atoms::BehaviourModules& moduleManager = Atoms::BehaviourModules::instance(); moduleManager.deregisterBehaviourModule("MyFollowTarget"); } |
Add input Attributes
You need to add now some input attributes.
For this module, you need only a target position attribute. The module's attributes of the module are stored inside a MapMetadata object, you can add new attributes using the "addAttribute" function. Attributes should always be added inside the constructor.
The third attribute of the addAttribute method defines if the attribute is overridable per agent or not. In this example, we'll make it overridable.
Code Block |
---|
#include <AtomsCore/Metadata/Vector3Metadata.h> MyFollowTargetModule(): Atoms::BehaviourModule() { AtomsCore::Vector3Metadata targetPosition(AtomsCore::Vector3(0.0, 0.0, 0.0)); // The last argumet specify if this attribute can be override per agent addAttribute("targetPosition", &targetPosition, true); } |
Implement the behaviour logic
Now you need to compute the new agent direction at each frame before the agent pose is computed. This will be done inside the initFrame method.
For this example, we won't need to override any other method.
Please have a look to at the Behaviour Module documentation if you want to implement other methods.
Code Block | ||
---|---|---|
| ||
#pragma once #include <Atoms/BehaviourModule.h> class MyFollowTargetModule : public Atoms::BehaviourModule { public: MyFollowTargetModule(); virtual ~MyFollowTargetModule(); void initFrame(const std::vector<Atoms::Agent*>& agents, Atoms::AgentGroup* agentGroup = nullptr) override; static Atoms::BehaviourModule* creator(const std::string& parameter); }; |
In the initFrame function, get the targetPosition value, iterate over each agent to read the position and set their new direction.
Code Block | ||
---|---|---|
| ||
#include <Atoms/Agent.h> #include <Atoms/GlobalNames.h> void MyFollowTargetModule::initFrame(const std::vector<Atoms::Agent*>& agents, Atoms::AgentGroup* agentGroup) { // Get the target position from the moduel attribute AtomsCore::MapMetadata& attributeMap = attributes(); AtomsCore::Vector3& targetPosition = attributeMap.getTypedEntry<AtomsCore::Vector3Metadata>("targetPosition")->get(); // iterate over each agent for (Atoms::Agent* agent : agents) { if (!agent) continue; //get the agent position AtomsPtr<AtomsCore::Vector3Metadata> positionPtr = agent->metadata().getTypedEntry<AtomsCore::Vector3Metadata>( Atoms::GlobalNameKeys::ATOMS_AGENT_POSITION_KEY ); AtomsPtr<AtomsCore::Vector3Metadata> directionPtr = agent->metadata().getTypedEntry<AtomsCore::Vector3Metadata>( Atoms::GlobalNameKeys::ATOMS_AGENT_DIRECTION_KEY ); if (!positionPtr || !directionPtr) continue; // compute the new direction directionPtr->set((targetPosition - positionPtr->get()).normalized()); } } |
Use per agent attributes
The target position attribute is overridable per agent, but the code to handle it isn't there yet. When you override an attribute, a new map metadata named "AttributeName_override" is created. The keys of this map metadata are the agent group ids, but they will be present only when the user specifies an override from inside his 3D application.
Use the function getAttributePerAgent
to get the value per agent. This function checks if there is an override and returns the value, otherwise it returns the default value.
Code Block | ||
---|---|---|
| ||
#include <AtomsCore/Metadata/IntMetadata.h> void MyFollowTargetModule::initFrame(const std::vector<Atoms::Agent*>& agents, Atoms::AgentGroup* agentGroup) { // Get the target position from the moduel attribute AtomsCore::MapMetadata& attributeMap = attributes(); AtomsCore::Vector3& targetPosition = attributeMap.getTypedEntry<AtomsCore::Vector3Metadata>("targetPosition")->get(); //get the override map metadata AtomsPtr<AtomsCore::MapMetadata> targetPositionOverride = attributeMap.getTypedEntry<AtomsCore::MapMetadata>("targetPosition_override"); // iterate over each agent for (Atoms::Agent* agent : agents) { if (!agent) continue; //get the agent position AtomsPtr<AtomsCore::Vector3Metadata> positionPtr = agent->metadata().getTypedEntry<AtomsCore::Vector3Metadata>( Atoms::GlobalNameKeys::ATOMS_AGENT_POSITION_KEY ); AtomsPtr<AtomsCore::Vector3Metadata> directionPtr = agent->metadata().getTypedEntry<AtomsCore::Vector3Metadata>( Atoms::GlobalNameKeys::ATOMS_AGENT_DIRECTION_KEY ); if (!positionPtr || !directionPtr) continue; // get the agent group id AtomsPtr<AtomsCore::IntMetadata> groupIdPtr = agent->metadata().getTypedEntry<AtomsCore::IntMetadata>( Atoms::GlobalNameKeys::ATOMS_AGENT_GROUPID_KEY); if (!groupIdPtr) continue; //convert the groupId to a string since the map metadata use only strings as key std::string groupIdStr = std::to_string(groupIdPtr->value()); auto overrideTargetPos = getAttributePerAgent(targetPosition, targetPositionOverride.get(), groupIdStr); directionPtr->set((overrideTargetPos - positionPtr->get()).normalized()); } } |
Improve performance
If you want to improve the performance of the module, you can parallelize the main for loop.
Code Block |
---|
#include <AtomsCore/Metadata/IntMetadata.h> #include <AtomsUtils/TaskScheduler.h> void MyFollowTargetModule::initFrame(const std::vector<Atoms::Agent*>& agents, Atoms::AgentGroup* agentGroup) { // Get the target position from the moduel attribute AtomsCore::MapMetadata& attributeMap = attributes(); AtomsCore::Vector3& targetPosition = attributeMap.getTypedEntry<AtomsCore::Vector3Metadata>("targetPosition")->get(); //get the override map metadata AtomsPtr<AtomsCore::MapMetadata> targetPositionOverride = attributeMap.getTypedEntry<AtomsCore::MapMetadata>("targetPosition_override"); // iterate over each agent AtomsUtils::parallel_for(AtomsUtils::ParallelForRange(0, agents.size()), [&](const AtomsUtils::ParallelForRange& r TASK_PARTITION_EXTRA_ARGS) { std::string groupIdStr; for (unsigned int i = r.begin(); i < r.end(); i++) { Atoms::Agent* agent = agents[i]; if (!agent) continue; //get the agent position AtomsPtr<AtomsCore::Vector3Metadata> positionPtr = agent->metadata().getTypedEntry<AtomsCore::Vector3Metadata>( Atoms::GlobalNameKeys::ATOMS_AGENT_POSITION_KEY ); AtomsPtr<AtomsCore::Vector3Metadata> directionPtr = agent->metadata().getTypedEntry<AtomsCore::Vector3Metadata>( Atoms::GlobalNameKeys::ATOMS_AGENT_DIRECTION_KEY ); if (!positionPtr || !directionPtr) continue; // get the agent group id AtomsPtr<AtomsCore::IntMetadata> groupIdPtr = agent->metadata().getTypedEntry<AtomsCore::IntMetadata>( Atoms::GlobalNameKeys::ATOMS_AGENT_GROUPID_KEY); if (!groupIdPtr) continue; //convert the groupId to a string since the map metadata use only strings as key groupIdStr = std::to_string(groupIdPtr->value()); auto overrideTargetPos = getAttributePerAgent(targetPosition, targetPositionOverride.get(), groupIdStr); directionPtr->set((overrideTargetPos - positionPtr->get()).normalized()); } }); } |
Create an UAtomsBehaviourComponent
The atoms behaviour module is now ready. Now you need to create an UAtomsBehaviourComponent in order to use your behaviour module in unreal.
In your project, create a new class MyFollowtargetBehaviourComponent and inherit from the UAtomsBehaviourComponent.
You need to add these arguments to the UCLASS.
Code Block |
---|
UCLASS(ClassGroup = (AtomsBehaviours), meta = (BlueprintSpawnableComponent)) class ATOMSDEMO_API UMyFollowtargetBehaviourComponent : public UAtomsBehaviourComponent { ... } |
You need to add the constructor and destructor.
Code Block |
---|
UCLASS(ClassGroup = (AtomsBehaviours), meta = (BlueprintSpawnableComponent)) class ATOMSDEMO_API UMyFollowtargetBehaviourComponent : public UAtomsBehaviourComponent { GENERATED_BODY() public: UMyFollowtargetBehaviourComponent(); ~UMyFollowtargetBehaviourComponent(); }; |
Implement the constructor setting the AtomsBehaviourModule
member. This variable contains the name of the Atoms behaviour module that this component creates.
Code Block |
---|
UMyFollowtargetBehaviourComponent::UMyFollowtargetBehaviourComponent(): UAtomsBehaviourComponent() { AtomsBehaviourModule = "MyFollowTarget"; } UMyFollowtargetBehaviourComponent::~UMyFollowtargetBehaviourComponent() { } |
Add attributes uproperty
You need now to expose the target position property and pass it’s its value to the atoms behaviour module.
Add an FVector property to your class and call it “targetPosition”. This is the same name of the attribute inside the atoms behaviour module. Set the property as BlueprintReadOnly and EditAnywhere. Add the “Behaviour“ category. Then add as meta, AtomsType=”5”. The value 5 is the atoms metadata type id for the vector3metadata (you can find all the type id below).
This value is important because the component tries to translate automatically the uproperty on the component to the attributes of the atoms module. For each atoms attribute it searches for a property with the same name, then it checks the AtomsType to be sure that it can be converted.
To expose the per-agent override attributute you need to add a uproperty for type TMap<int32, FVector>. The arguments of the uproperty are similar, except that The AtomsType=”1” since it is a MapMetadata and add AtomsOverride = "targetPosition" to the meta. This tell atoms which is the attribute that is overriding.
Then inside the constructor you need to fill the AtomsTypes and AtomsOverrides map passing the data type for each property and the override information like in the example on the right.
Code Block | ||
---|---|---|
| ||
UCLASS(ClassGroup = (AtomsBehaviours), meta = (BlueprintSpawnableComponent))
class ATOMSDEMO_API UMyFollowtargetBehaviourComponent : public UAtomsBehaviourComponent
{
GENERATED_BODY()
public:
UMyFollowtargetBehaviourComponent();
~UMyFollowtargetBehaviourComponent();
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Behaviour", meta = (AtomsType = "5"))
FVector targetPosition;
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Behaviour", meta = (AtomsType = "1", AtomsOverride = "targetPosition"))
TMap<int32, FVector> targetPositionOverride;
};
UMyFollowtargetBehaviourComponent::UMyFollowtargetBehaviourComponent(): UAtomsBehaviourComponent()
{
if (!AtomsTypes.Contains("targetPosition")) AtomsTypes.Add("targetPosition", 5);
if (!AtomsTypes.Contains("targetPositionOverride")) AtomsTypes.Add("targetPositionOverride", 1);
if (!AtomsOverrides.Contains("targetPositionOverride")) AtomsOverrides.Add("targetPositionOverride", "targetPosition");
}
|
Add custom property
The automatic property translation system works only with these types:
bool: BoolMetadata, AtomsType: 13
int: IntMetadata, AtomsType: 3
float: DoubleMetadata, AtomsType: 4
FString: StringMetadata, AtomsType: 10
FVector: Vector3Metadata, AtomsType: 5
FVector2D: Vector2Metadata, AtomsType: 27
FVector4: Vector4Metadata, AtomsType: 6
FMatrix: MatrixMetadata, AtomsType: 7
FRotator: EulerMetadata, AtomsType: 9
FBox: Box3Metadata, AtomsType: 21
FBox2D: Box2Metadata, AtomsType: 29
TArray<bool>: BoolMetadata, AtomsType: 14
TArray<int>: IntArrayMetadata, AtomsType: 11
TArray<float>: DoubleArrayMetadata, AtomsType: 12
TArray<FString>: StringArrayMetadata, AtomsType: 17
TArray<FVector>: Vector3ArrayMetadata, AtomsType: 15
TArray<FVector2D>: Vector2ArrayMetadata, AtomsType: 28
TArray<FVector4>: Vector4ArrayMetadata, AtomsType: 16
TArray<FMatrix>: MatrixArrayMetadata, AtomsType: 19
TArray<FRotator>: EulerArrayMetadata, AtomsType: 18
TArray<FBox>: Box3ArrayMetadata, AtomsType: 69
TArray<FBox2D>: Box2ArrayMetadata, AtomsType: 69
AAtomsSplineActor*: CurveMetadata, AtomsType: 103
TArray<AAtomsSplineActor*>: CurveArrayMetadata, AtomsType: 104
AStaticMeshActorP* MeshMetadata, AtomsType: 101
TArray<AStaticMeshActor*>: MeshArrayMetadata, AtomsType: 105
AAtomsAgentGroup*: StringMetada, AtomsType: 9900
TArray<AAtomsAgentGroup*>: StringArrayMetadata, AtomsType: 9901
UAtomsAgentTypeAsset*: StringMetadata, AtomsType: 9902
TArray<UAtomsAgentTypeAsset*>: StringArrayMetadata, AtomsType: 9903
UAtomsStateMachine*: StringMetadata, AtomsType: 9904
UAtomsCache*: StringMetadata, AtomsType: 9905
UAnimSequence*: StringMetadata, AtomsType: 9906
TArray<UAnimSequence*>: StringArrayMetadata, AtomsType: 9907
If you need to translate more complex data you need to override the method:
Code Block |
---|
virtual void UAtomsBehaviourComponent::OnBehaviourModuleCreated(AtomsPtr<Atoms::BehaviourModule>& module); |
This is called after the automatic translation.
To expose the per-agent override attribute you need to add a uproperty for type TMap<int32, FVector>. The int32 key contains the agent group Id, while the vector value contains the override for that agent.
Now you need to implement the GetAtomsAttributes member to translate the unreal properties to the atoms module attributes. Luckily there are some defines that you can use to translate the properties.
Code Block | ||
---|---|---|
| ||
UCLASS(ClassGroup = (AtomsBehaviours), meta = (BlueprintSpawnableComponent))
class ATOMSDEMO_API UMyFollowtargetBehaviourComponent : public UAtomsBehaviourComponent
{
GENERATED_BODY()
public:
UMyFollowtargetBehaviourComponent();
~UMyFollowtargetBehaviourComponent();
virtual void GetAtomsAttributes(AtomsPtr<AtomsCore::MapMetadata>& attributes) override;
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Behaviour")
FVector targetPosition;
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Behaviour")
TMap<int32, FVector> targetPositionOverride;
};
|
Code Block | ||
---|---|---|
| ||
#include "AtomsModuleGeneratorUtils.h"
#include "AtomsAttributeUtils.h"
#include "Actors/AtomsAgentGroup.h"
UMyFollowtargetBehaviourComponent::UMyFollowtargetBehaviourComponent(): UAtomsBehaviourComponent()
{
}
void UMyFollowtargetBehaviourComponent::GetAtomsAttributes(AtomsPtr<AtomsCore::MapMetadata>& attributes)
{
ATOMS_BEHAVIOUR_COMPONENT_CONVERT_META(targetPosition, AtomsCore::Vector3Metadata, AtomsConverter::ToVector3);
ATOMS_BEHAVIOUR_COMPONENT_CONVERT_META_OVERRIDE(targetPosition_override, targetPositionOverride, AtomsCore::Vector3Metadata, AtomsConverter::ToVector3);
}
|
Add Setter functions
The last step is to expose functions to set the component properties and update the atoms module attributes simultaneously.
We need to add a SetTargetPosition/SetTargetPositionOverride and RemoveSetTargetPositionOverride. The sequencer will also use the SetTargetPosition. So you need to implement this function if you want to drive this property by the sequencer.
Code Block |
---|
virtual void UAtomsBehaviourComponent::OnBehaviourModuleCreated(AtomsPtr<Atoms::BehaviourModule>& module); |
This is called after the automatic translation.
Code Block | ||
---|---|---|
| ||
UCLASS(ClassGroup = (AtomsBehaviours), meta = (BlueprintSpawnableComponent))
class ATOMSUNREAL26CPPTEST_API UMyFollowtargetBehaviourComponent : public UAtomsBehaviourComponent
{
GENERATED_BODY()
public:
UMyFollowtargetBehaviourComponent();
~UMyFollowtargetBehaviourComponent();
virtual void GetAtomsAttributes(AtomsPtr<AtomsCore::MapMetadata>& attributes) override;
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Behaviour")
FVector targetPosition;
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Behaviour")
TMap<int32, FVector> targetPositionOverride;
/** Set target position */
UFUNCTION(BlueprintCallable, Category = "FollowTargetBehaviourComponent", meta = (CallInEditor = "true"))
void SetTargetPosition(const FVector& Value);
/** Set target position override */
UFUNCTION(BlueprintCallable, Category = "FollowTargetBehaviourComponent", meta = (CallInEditor = "true"))
void SetTargetPositionOverride(const FVector& Value, const int32 AgentId);
/** Remove target position override */
UFUNCTION(BlueprintCallable, Category = "FollowTargetBehaviourComponent", meta = (CallInEditor = "true"))
void RemoveSetTargetPositionOverride(const int32 AgentId);
}; |
Code Block | ||
---|---|---|
| ||
#include "AtomsModuleGeneratorUtils.h"
#include "AtomsAttributeUtils.h"
#include "Actors/AtomsAgentGroup.h"
void UMyFollowtargetBehaviourComponent::SetTargetPosition(const FVector& Value)
{
targetPosition = Value;
ATOMS_BEHAVIOUR_COMPONENT_GET_ATTRIBUTES()
ATOMS_BEHAVIOUR_COMPONENT_CONVERT_META(targetPosition, AtomsCore::Vector3Metadata, AtomsConverter::ToVector3);
}
void UMyFollowtargetBehaviourComponent::SetTargetPositionOverride(const FVector& Value, const int32 AgentId)
{
ATOMS_BEHAVIOUR_COMPONENT_SET_META_OVERRIDE(targetPosition_override, targetPositionOverride, AtomsCore::Vector3Metadata, AtomsConverter::ToVector3, AgentId);
}
void UMyFollowtargetBehaviourComponent::RemoveSetTargetPositionOverride(const int32 AgentId)
{
ATOMS_BEHAVIOUR_COMPONENT_REMOVE_OVERRIDE(targetPosition_override, targetPositionOverride, AgentId);
}
|
The GetAtomsAttributes
and the OnBehaviourModuleCreated
method are called only once at the beginning of the simulation. If you want to update the attributes during the simulation, you need to turn on the UpdateOnTick property on the behaviour component and implement the method OnBehaviourModuleUpdated for custom translations. If you want to update the attributes manually outside the behaviour component, use the SetModuleProperty_AsSet* blueprint functionfunction.
Final source files:
View file | ||
---|---|---|
|
View file | ||
---|---|---|
|
View file | ||
---|---|---|
|
View file | ||
---|---|---|
|
.
The module is now ready to be used. Create an agent group with a grid layout and a clipReader or state machine module. Add the new MyFollowTarget component and set the target position property. Press play and you are done!