Add initial Behavior simulation

This commit is contained in:
Wojtek Figat
2023-08-17 15:26:31 +02:00
parent d1e2d6699e
commit c18625e017
10 changed files with 444 additions and 10 deletions

View File

@@ -0,0 +1,127 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#include "Behavior.h"
#include "BehaviorKnowledge.h"
#include "BehaviorTreeNodes.h"
#include "Engine/Engine/Time.h"
BehaviorKnowledge::~BehaviorKnowledge()
{
FreeMemory();
}
void BehaviorKnowledge::InitMemory(BehaviorTree* tree)
{
ASSERT_LOW_LAYER(!Tree && tree);
Tree = tree;
Blackboard = Variant::NewValue(tree->Graph.Root->BlackboardType);
if (!Memory && tree->Graph.NodesStatesSize)
{
Memory = Allocator::Allocate(tree->Graph.NodesStatesSize);
for (const auto& node : tree->Graph.Nodes)
{
if (node.Instance)
node.Instance->InitState(Behavior, Memory);
}
}
}
void BehaviorKnowledge::FreeMemory()
{
if (Memory)
{
ASSERT_LOW_LAYER(Tree);
for (const auto& node : Tree->Graph.Nodes)
{
if (node.Instance)
node.Instance->ReleaseState(Behavior, Memory);
}
Allocator::Free(Memory);
Memory = nullptr;
}
Blackboard.DeleteValue();
Tree = nullptr;
}
Behavior::Behavior(const SpawnParams& params)
: Script(params)
{
_tickLateUpdate = 1; // TODO: run Behavior via Job System (use Engine::UpdateGraph)
_knowledge.Behavior = this;
Tree.Changed.Bind<Behavior, &Behavior::ResetLogic>(this);
}
void Behavior::StartLogic()
{
// Ensure to have tree loaded on begin play
CHECK(Tree && !Tree->WaitForLoaded());
BehaviorTree* tree = Tree.Get();
CHECK(tree->Graph.Root);
_result = BehaviorUpdateResult::Running;
// Init knowledge
_knowledge.InitMemory(tree);
}
void Behavior::StopLogic()
{
if (_result != BehaviorUpdateResult::Running)
return;
_accumulatedTime = 0.0f;
_result = BehaviorUpdateResult::Success;
}
void Behavior::ResetLogic()
{
const bool isActive = _result == BehaviorUpdateResult::Running;
if (isActive)
StopLogic();
// Reset state
_knowledge.FreeMemory();
_accumulatedTime = 0.0f;
_result = BehaviorUpdateResult::Success;
if (isActive)
StartLogic();
}
void Behavior::OnEnable()
{
if (AutoStart)
StartLogic();
}
void Behavior::OnLateUpdate()
{
if (_result != BehaviorUpdateResult::Running)
return;
const BehaviorTree* tree = Tree.Get();
if (!tree || !tree->Graph.Root)
{
_result = BehaviorUpdateResult::Failed;
Finished();
return;
}
// Update timer
_accumulatedTime += Time::Update.DeltaTime.GetTotalSeconds();
const float updateDeltaTime = 1.0f / Math::Max(tree->Graph.Root->UpdateFPS * UpdateRateScale, ZeroTolerance);
if (_accumulatedTime < updateDeltaTime)
return;
_accumulatedTime -= updateDeltaTime;
// Update tree
BehaviorUpdateContext context;
context.Behavior = this;
context.Knowledge = &_knowledge;
context.Memory = _knowledge.Memory;
context.DeltaTime = updateDeltaTime;
const BehaviorUpdateResult result = tree->Graph.Root->Update(context);
if (result != BehaviorUpdateResult::Running)
{
_result = result;
Finished();
}
}

View File

@@ -0,0 +1,84 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#pragma once
#include "BehaviorTree.h"
#include "BehaviorKnowledge.h"
#include "BehaviorTypes.h"
#include "Engine/Scripting/Script.h"
#include "Engine/Content/AssetReference.h"
/// <summary>
/// Behavior instance script that runs Behavior Tree execution.
/// </summary>
API_CLASS() class FLAXENGINE_API Behavior : public Script
{
API_AUTO_SERIALIZATION();
DECLARE_SCRIPTING_TYPE(Behavior);
private:
BehaviorKnowledge _knowledge;
float _accumulatedTime = 0.0f;
BehaviorUpdateResult _result = BehaviorUpdateResult::Success;
void* _memory = nullptr;
public:
/// <summary>
/// Behavior Tree asset to use for logic execution.
/// </summary>
API_FIELD(Attributes="EditorOrder(0)")
AssetReference<BehaviorTree> Tree;
/// <summary>
/// If checked, auto starts the logic on begin play.
/// </summary>
API_FIELD(Attributes="EditorOrder(10)")
bool AutoStart = true;
/// <summary>
/// The behavior logic update rate scale (multiplies the UpdateFPS defined in Behavior Tree root node). Can be used to improve performance via LOD to reduce updates frequency (eg. by 0.5) for behaviors far from player.
/// </summary>
API_FIELD(Attributes="EditorOrder(20), Limit(0, 10, 0.01f)")
float UpdateRateScale = 1.0f;
public:
/// <summary>
/// Gets the current behavior knowledge instance. Empty if not started.
/// </summary>
API_PROPERTY() BehaviorKnowledge* GetKnowledge()
{
return &_knowledge;
}
/// <summary>
/// Gets the last behavior tree execution result.
/// </summary>
API_PROPERTY() BehaviorUpdateResult GetResult() const
{
return _result;
}
/// <summary>
/// Event called when behavior tree execution ends with a result.
/// </summary>
API_EVENT() Action Finished;
/// <summary>
/// Starts the logic.
/// </summary>
API_FUNCTION() void StartLogic();
/// <summary>
/// Stops the logic.
/// </summary>
API_FUNCTION() void StopLogic();
/// <summary>
/// Resets the behavior logic by clearing knowledge (clears blackboard and removes goals) and resetting execution state (goes back to root).
/// </summary>
API_FUNCTION() void ResetLogic();
// [Script]
void OnEnable() override;
void OnLateUpdate() override;
};

View File

@@ -4,15 +4,48 @@
#include "Engine/Scripting/ScriptingObject.h"
class Behavior;
class BehaviorTree;
/// <summary>
/// Behavior logic component knowledge data container. Contains blackboard values, sensors data and goals storage for Behavior Tree execution.
/// </summary>
API_CLASS() class FLAXENGINE_API BehaviorKnowledge : public ScriptingObject
{
DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(BehaviorKnowledge, ScriptingObject);
~BehaviorKnowledge();
/// <summary>
/// Owning Behavior instance (constant).
/// </summary>
API_FIELD(ReadOnly) Behavior* Behavior = nullptr;
/// <summary>
/// Used Behavior Tree asset (defines blackboard and memory constraints).
/// </summary>
API_FIELD(ReadOnly) BehaviorTree* Tree = nullptr;
/// <summary>
/// Raw memory chunk with all Behavior Tree nodes state.
/// </summary>
API_FIELD(ReadOnly) void* Memory = nullptr;
/// <summary>
/// Instance of the behaviour blackboard (structure or class).
/// </summary>
API_FIELD() Variant Blackboard;
// TODO: blackboard
// TODO: sensors data
// TODO: goals
// TODO: GetGoal/HasGoal
/// <summary>
/// Initializes the knowledge for a certain tree.
/// </summary>
void InitMemory(BehaviorTree* tree);
/// <summary>
/// Releases the memory of the knowledge.
/// </summary>
void FreeMemory();
};

View File

@@ -36,6 +36,12 @@ bool BehaviorTreeGraph::Load(ReadStream* stream, bool loadMeta)
}
}
}
if (node.Instance)
{
// Count total states memory size
node.Instance->_memoryOffset = NodesStatesSize;
NodesStatesSize += node.Instance->GetStateSize();
}
}
return false;
@@ -46,6 +52,7 @@ void BehaviorTreeGraph::Clear()
VisjectGraph<BehaviorTreeGraphNode>::Clear();
Root = nullptr;
NodesStatesSize = 0;
}
bool BehaviorTreeGraph::onNodeLoaded(Node* n)

View File

@@ -1,5 +1,7 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
#if FLAX_EDITOR
using System;
using FlaxEngine.Utilities;
@@ -39,4 +41,55 @@ namespace FlaxEngine
}
#endif
}
unsafe partial class BehaviorTreeNode
{
/// <summary>
/// Gets the size in bytes of the given typed node state structure.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetStateSize<T>()
{
// C# nodes state is stored via pinned GCHandle to support holding managed references (eg. string or Vector3[])
return sizeof(IntPtr);
}
/// <summary>
/// Sets the node state at the given memory address.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AllocState(IntPtr memory, object state)
{
var handle = GCHandle.Alloc(state);
var ptr = IntPtr.Add(memory, _memoryOffset).ToPointer();
Unsafe.Write<IntPtr>(ptr, (IntPtr)handle);
}
/// <summary>
/// Returns the typed node state at the given memory address.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref T GetState<T>(IntPtr memory) where T : struct
{
var ptr = IntPtr.Add(memory, _memoryOffset).ToPointer();
var handle = GCHandle.FromIntPtr(Unsafe.Read<IntPtr>(ptr));
var state = handle.Target;
#if !BUILD_RELEASE
if (state == null)
throw new NullReferenceException();
#endif
return ref Unsafe.Unbox<T>(state);
}
/// <summary>
/// Frees the allocated node state at the given memory address.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void FreeState(IntPtr memory)
{
var ptr = IntPtr.Add(memory, _memoryOffset).ToPointer();
var handle = GCHandle.FromIntPtr(Unsafe.Read<IntPtr>(ptr));
handle.Free();
}
}
}

View File

@@ -5,6 +5,7 @@
#include "Engine/Content/BinaryAsset.h"
#include "Engine/Visject/VisjectGraph.h"
class BehaviorKnowledge;
class BehaviorTreeNode;
class BehaviorTreeRootNode;
@@ -28,6 +29,8 @@ class BehaviorTreeGraph : public VisjectGraph<BehaviorTreeGraphNode>
public:
// Instance of the graph root node.
BehaviorTreeRootNode* Root = nullptr;
// Total size of the nodes states memory.
int32 NodesStatesSize = 0;
// [VisjectGraph]
bool Load(ReadStream* stream, bool loadMeta) override;

View File

@@ -11,6 +11,11 @@
API_CLASS(Abstract) class FLAXENGINE_API BehaviorTreeNode : public SerializableScriptingObject
{
DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(BehaviorTreeNode, SerializableScriptingObject);
friend class BehaviorTreeGraph;
protected:
// Raw memory byte offset from the start of the behavior memory block.
API_FIELD(ReadOnly) int32 _memoryOffset = 0;
public:
/// <summary>
@@ -18,11 +23,6 @@ public:
/// </summary>
API_FIELD() String Name;
// TODO: decorators/conditionals
// TODO: instance data ctor/dtor
// TODO: start/stop methods
// TODO: update method
/// <summary>
/// Initializes node state. Called after whole tree is loaded and nodes hierarchy is setup.
/// </summary>
@@ -31,6 +31,52 @@ public:
{
}
/// <summary>
/// Gets the node instance state size. A chunk of the valid memory is passed via InitState to setup that memory chunk (one per-behavior).
/// </summary>
API_FUNCTION() virtual int32 GetStateSize() const
{
return 0;
}
/// <summary>
/// Initializes node instance state. Called when starting logic simulation for a given behavior.
/// </summary>
/// <param name="behavior">Behavior to simulate.</param>
/// <param name="memory">Pointer to pre-allocated memory for this node to use (call constructor of the state container).</param>
API_FUNCTION() virtual void InitState(Behavior* behavior, void* memory)
{
}
/// <summary>
/// Cleanups node instance state. Called when stopping logic simulation for a given behavior.
/// </summary>
/// <param name="behavior">Behavior to simulate.</param>
/// <param name="memory">Pointer to pre-allocated memory for this node to use (call destructor of the state container).</param>
API_FUNCTION() virtual void ReleaseState(Behavior* behavior, void* memory)
{
}
/// <summary>
/// Updates node logic.
/// </summary>
/// <param name="context">Behavior update context data.</param>
/// <returns>Operation result enum.</returns>
API_FUNCTION() virtual BehaviorUpdateResult Update(BehaviorUpdateContext context)
{
return BehaviorUpdateResult::Success;
}
// [SerializableScriptingObject]
void Serialize(SerializeStream& stream, const void* otherObj) override;
void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override;
public:
// Returns the typed node state at the given memory address.
template<typename T>
T* GetState(void* memory) const
{
ASSERT((int32)sizeof(T) <= GetStateSize());
return reinterpret_cast<T*>((byte*)memory + _memoryOffset);
}
};

View File

@@ -24,3 +24,51 @@ void BehaviorTreeCompoundNode::Init(BehaviorTree* tree)
for (BehaviorTreeNode* child : Children)
child->Init(tree);
}
BehaviorUpdateResult BehaviorTreeCompoundNode::Update(BehaviorUpdateContext context)
{
auto result = BehaviorUpdateResult::Success;
for (int32 i = 0; i < Children.Count() && result == BehaviorUpdateResult::Success; i++)
{
BehaviorTreeNode* child = Children[i];
result = child->Update(context);
}
return result;
}
int32 BehaviorTreeSequenceNode::GetStateSize() const
{
return sizeof(State);
}
void BehaviorTreeSequenceNode::InitState(Behavior* behavior, void* memory)
{
auto state = GetState<State>(memory);
new(state)State();
}
BehaviorUpdateResult BehaviorTreeSequenceNode::Update(BehaviorUpdateContext context)
{
auto state = GetState<State>(context.Memory);
if (state->CurrentChildIndex >= Children.Count())
return BehaviorUpdateResult::Success;
if (state->CurrentChildIndex == -1)
return BehaviorUpdateResult::Failed;
auto result = Children[state->CurrentChildIndex]->Update(context);
switch (result)
{
case BehaviorUpdateResult::Success:
state->CurrentChildIndex++; // Move to the next node
if (state->CurrentChildIndex < Children.Count())
result = BehaviorUpdateResult::Running; // Keep on running to the next child on the next update
break;
case BehaviorUpdateResult::Failed:
state->CurrentChildIndex = -1; // Mark whole sequence as failed
break;
}
return result;
}

View File

@@ -20,25 +20,42 @@ API_CLASS(Abstract) class FLAXENGINE_API BehaviorTreeCompoundNode : public Behav
public:
// [BehaviorTreeNode]
void Init(BehaviorTree* tree) override;
BehaviorUpdateResult Update(BehaviorUpdateContext context) override;
};
/// <summary>
/// Sequence node updates all its children as long as they return success. If any child fails, the sequence is failed.
/// Sequence node updates all its children (from left to right) as long as they return success. If any child fails, the sequence is failed.
/// </summary>
API_CLASS() class FLAXENGINE_API BehaviorTreeSequenceNode : public BehaviorTreeCompoundNode
{
DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(BehaviorTreeSequenceNode, BehaviorTreeCompoundNode);
public:
// [BehaviorTreeNode]
int32 GetStateSize() const override;
void InitState(Behavior* behavior, void* memory) override;
BehaviorUpdateResult Update(BehaviorUpdateContext context) override;
private:
struct State
{
int32 CurrentChildIndex = 0;
};
};
/// <summary>
/// Root node of the behavior tree. Contains logic properties and definitions for the runtime.
/// </summary>
API_CLASS(Sealed) class FLAXENGINE_API BehaviorTreeRootNode : public BehaviorTreeCompoundNode
API_CLASS(Sealed) class FLAXENGINE_API BehaviorTreeRootNode : public BehaviorTreeSequenceNode
{
DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(BehaviorTreeRootNode, BehaviorTreeCompoundNode);
DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(BehaviorTreeRootNode, BehaviorTreeSequenceNode);
API_AUTO_SERIALIZATION();
// Full typename of the blackboard data type (structure or class). Spawned for each instance of the behavior.
API_FIELD(Attributes="EditorOrder(0), TypeReference(\"\", \"IsValidBlackboardType\"), CustomEditorAlias(\"FlaxEditor.CustomEditors.Editors.TypeNameEditor\")")
StringAnsi BlackboardType;
// The target amount of the behavior logic updates per second.
API_FIELD(Attributes="EditorOrder(10)")
float UpdateFPS = 10.0f;
};

View File

@@ -4,6 +4,7 @@
#include "Engine/Scripting/ScriptingType.h"
class Behavior;
class BehaviorTree;
class BehaviorTreeNode;
class BehaviorKnowledge;
@@ -15,10 +16,25 @@ API_STRUCT() struct FLAXENGINE_API BehaviorUpdateContext
{
DECLARE_SCRIPTING_TYPE_MINIMAL(BehaviorUpdateContext);
/// <summary>
/// Behavior to simulate.
/// </summary>
API_FIELD() Behavior* Behavior;
/// <summary>
/// Behavior's logic knowledge container (data, goals and sensors).
/// </summary>
API_FIELD() BehaviorKnowledge* Knowledge;
/// <summary>
/// Current instance memory buffer location (updated while moving down the tree).
/// </summary>
API_FIELD() void* Memory;
/// <summary>
/// Simulation time delta (in seconds) since the last update.
/// </summary>
float DeltaTime;
API_FIELD() float DeltaTime;
};
/// <summary>