Add **Behavior Tree** asset type and editing

This commit is contained in:
Wojtek Figat
2023-08-16 13:26:33 +02:00
parent f8dc59d670
commit 18b47257fd
23 changed files with 1417 additions and 3 deletions

View File

@@ -0,0 +1,18 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Scripting/ScriptingObject.h"
/// <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);
// TODO: blackboard
// TODO: sensors data
// TODO: goals
// TODO: GetGoal/HasGoal
};

View File

@@ -0,0 +1,161 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#include "BehaviorTree.h"
#include "BehaviorTreeNode.h"
#include "BehaviorTreeNodes.h"
#include "Engine/Content/Factories/BinaryAssetFactory.h"
#include "Engine/Scripting/Scripting.h"
#include "Engine/Serialization/JsonSerializer.h"
#include "Engine/Serialization/MemoryReadStream.h"
#include "Engine/Threading/Threading.h"
#include "FlaxEngine.Gen.h"
REGISTER_BINARY_ASSET(BehaviorTree, "FlaxEngine.BehaviorTree", false);
BehaviorTreeGraphNode::~BehaviorTreeGraphNode()
{
SAFE_DELETE(Instance);
}
bool BehaviorTreeGraph::Load(ReadStream* stream, bool loadMeta)
{
if (VisjectGraph<BehaviorTreeGraphNode>::Load(stream, loadMeta))
return true;
// Build node instances hierarchy
for (Node& node : Nodes)
{
if (auto* nodeCompound = ScriptingObject::Cast<BehaviorTreeCompoundNode>(node.Instance))
{
for (const GraphBox* childBox : node.Boxes[1].Connections)
{
const Node* child = childBox ? (Node*)childBox->Parent : nullptr;
if (child && child->Instance)
{
nodeCompound->Children.Add(child->Instance);
}
}
}
}
return false;
}
void BehaviorTreeGraph::Clear()
{
VisjectGraph<BehaviorTreeGraphNode>::Clear();
Root = nullptr;
}
bool BehaviorTreeGraph::onNodeLoaded(Node* n)
{
if (n->GroupID == 19 && (n->TypeID == 1 || n->TypeID == 2))
{
// Create node instance object
ScriptingTypeHandle type = Scripting::FindScriptingType((StringAnsiView)n->Values[0]);
if (!type)
type = Scripting::FindScriptingType(StringAnsi((StringView)n->Values[0]));
if (type)
{
n->Instance = (BehaviorTreeNode*)Scripting::NewObject(type);
const Variant& data = n->Values[1];
if (data.Type == VariantType::Blob)
JsonSerializer::LoadFromBytes(n->Instance, Span<byte>((byte*)data.AsBlob.Data, data.AsBlob.Length), FLAXENGINE_VERSION_BUILD);
// Find root node
if (!Root && n->Instance && BehaviorTreeRootNode::TypeInitializer == type)
Root = (BehaviorTreeRootNode*)n->Instance;
}
else
{
const String name = n->Values[0].ToString();
if (name.HasChars())
LOG(Error, "Missing type '{0}'", name);
}
}
return VisjectGraph<BehaviorTreeGraphNode>::onNodeLoaded(n);
}
BehaviorTree::BehaviorTree(const SpawnParams& params, const AssetInfo* info)
: BinaryAsset(params, info)
{
}
BytesContainer BehaviorTree::LoadSurface()
{
if (WaitForLoaded())
return BytesContainer();
ScopeLock lock(Locker);
if (!LoadChunks(GET_CHUNK_FLAG(0)))
{
const auto data = GetChunk(0);
BytesContainer result;
result.Copy(data->Data);
return result;
}
LOG(Warning, "\'{0}\' surface data is missing.", ToString());
return BytesContainer();
}
#if USE_EDITOR
bool BehaviorTree::SaveSurface(const BytesContainer& data)
{
// Wait for asset to be loaded or don't if last load failed
if (LastLoadFailed())
{
LOG(Warning, "Saving asset that failed to load.");
}
else if (WaitForLoaded())
{
LOG(Error, "Asset loading failed. Cannot save it.");
return true;
}
ScopeLock lock(Locker);
// Set Visject Surface data
GetOrCreateChunk(0)->Data.Copy(data);
// Save
AssetInitData assetData;
assetData.SerializedVersion = 1;
if (SaveAsset(assetData))
{
LOG(Error, "Cannot save \'{0}\'", ToString());
return true;
}
return false;
}
#endif
Asset::LoadResult BehaviorTree::load()
{
// Load graph
const auto surfaceChunk = GetChunk(0);
if (surfaceChunk == nullptr)
return LoadResult::MissingDataChunk;
MemoryReadStream surfaceStream(surfaceChunk->Get(), surfaceChunk->Size());
if (Graph.Load(&surfaceStream, true))
{
LOG(Warning, "Failed to load graph \'{0}\'", ToString());
return LoadResult::Failed;
}
return LoadResult::Ok;
}
void BehaviorTree::unload(bool isReloading)
{
// Clear resources
Graph.Clear();
}
AssetChunksFlag BehaviorTree::getChunksToPreload() const
{
return GET_CHUNK_FLAG(0);
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#if FLAX_EDITOR
using System;
using FlaxEngine.Utilities;
using FlaxEditor.Scripting;
using FlaxEngine.GUI;
#endif
namespace FlaxEngine
{
partial class BehaviorTreeRootNode
{
#if FLAX_EDITOR
private static bool IsValidBlackboardType(ScriptType type)
{
if (ScriptType.FlaxObject.IsAssignableFrom(type))
return false;
if (type.Type != null)
{
if (type.Type.IsDelegate())
return false;
if (typeof(Control).IsAssignableFrom(type.Type))
return false;
if (typeof(Attribute).IsAssignableFrom(type.Type))
return false;
if (type.Type.FullName.StartsWith("FlaxEditor.", StringComparison.Ordinal))
return false;
}
return !type.IsGenericType &&
!type.IsInterface &&
!type.IsStatic &&
!type.IsAbstract &&
!type.IsArray &&
!type.IsVoid &&
(type.IsClass || type.IsStructure) &&
type.IsPublic &&
type.CanCreateInstance;
}
#endif
}
}

View File

@@ -0,0 +1,91 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Content/BinaryAsset.h"
#include "Engine/Visject/VisjectGraph.h"
class BehaviorTreeNode;
class BehaviorTreeRootNode;
/// <summary>
/// Behavior Tree graph node.
/// </summary>
class BehaviorTreeGraphNode : public VisjectGraphNode<>
{
public:
// Instance of the graph node.
BehaviorTreeNode* Instance = nullptr;
~BehaviorTreeGraphNode();
};
/// <summary>
/// Behavior Tree graph.
/// </summary>
class BehaviorTreeGraph : public VisjectGraph<BehaviorTreeGraphNode>
{
public:
// Instance of the graph root node.
BehaviorTreeRootNode* Root = nullptr;
// [VisjectGraph]
bool Load(ReadStream* stream, bool loadMeta) override;
void Clear() override;
bool onNodeLoaded(Node* n) override;
};
/// <summary>
/// Behavior Tree graph executor runtime.
/// </summary>
class BehaviorTreeExecutor : public VisjectExecutor
{
};
/// <summary>
/// Behavior Tree asset with AI logic graph.
/// </summary>
/// <seealso cref="BinaryAsset" />
API_CLASS(NoSpawn, Sealed) class FLAXENGINE_API BehaviorTree : public BinaryAsset
{
DECLARE_BINARY_ASSET_HEADER(BehaviorTree, 1);
public:
/// <summary>
/// The Behavior Tree graph.
/// </summary>
BehaviorTreeGraph Graph;
/// <summary>
/// Tries to load surface graph from the asset.
/// </summary>
/// <returns>The surface data or empty if failed to load it.</returns>
API_FUNCTION() BytesContainer LoadSurface();
#if USE_EDITOR
/// <summary>
/// Updates the graph surface (save new one, discard cached data, reload asset).
/// </summary>
/// <param name="data">Stream with graph data.</param>
/// <returns>True if cannot save it, otherwise false.</returns>
API_FUNCTION() bool SaveSurface(const BytesContainer& data);
#endif
public:
// [BinaryAsset]
#if USE_EDITOR
void GetReferences(Array<Guid>& output) const override
{
// Base
BinaryAsset::GetReferences(output);
Graph.GetReferences(output);
}
#endif
protected:
// [BinaryAsset]
LoadResult load() override;
void unload(bool isReloading) override;
AssetChunksFlag getChunksToPreload() const override;
};

View File

@@ -0,0 +1,29 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Scripting/SerializableScriptingObject.h"
#include "BehaviorTypes.h"
/// <summary>
/// Base class for Behavior Tree nodes.
/// </summary>
API_CLASS(Abstract) class FLAXENGINE_API BehaviorTreeNode : public SerializableScriptingObject
{
DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(BehaviorTreeNode, SerializableScriptingObject);
public:
/// <summary>
/// Node user name (eg. Follow Enemy, or Pick up Weapon).
/// </summary>
API_FIELD() String Name;
// TODO: decorators/conditionals
// TODO: init methods
// TODO: instance data ctor/dtor
// TODO: start/stop methods
// TODO: update method
void Serialize(SerializeStream& stream, const void* otherObj) override;
void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override;
};

View File

@@ -0,0 +1,20 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#include "BehaviorTreeNodes.h"
#include "Engine/Serialization/Serialization.h"
void BehaviorTreeNode::Serialize(SerializeStream& stream, const void* otherObj)
{
SerializableScriptingObject::Serialize(stream, otherObj);
SERIALIZE_GET_OTHER_OBJ(BehaviorTreeNode);
SERIALIZE(Name);
}
void BehaviorTreeNode::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
{
SerializableScriptingObject::Deserialize(stream, modifier);
Name.Clear(); // Missing Name is assumes as unnamed node
DESERIALIZE(Name);
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#pragma once
#include "BehaviorTreeNode.h"
#include "Engine/Core/Collections/Array.h"
/// <summary>
/// Base class for compound Behavior Tree nodes that composite child nodes.
/// </summary>
API_CLASS(Abstract) class FLAXENGINE_API BehaviorTreeCompoundNode : public BehaviorTreeNode
{
DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(BehaviorTreeCompoundNode, BehaviorTreeNode);
public:
/// <summary>
/// List with all child nodes.
/// </summary>
API_FIELD(Readonly) Array<BehaviorTreeNode*, InlinedAllocation<8>> Children;
};
/// <summary>
/// Sequence node updates all its children 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);
};
/// <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
{
DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(BehaviorTreeRootNode, BehaviorTreeCompoundNode);
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;
};

View File

@@ -0,0 +1,35 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Scripting/ScriptingType.h"
class BehaviorTree;
class BehaviorTreeNode;
class BehaviorKnowledge;
/// <summary>
/// Behavior update context state.
/// </summary>
API_STRUCT() struct FLAXENGINE_API BehaviorUpdateContext
{
DECLARE_SCRIPTING_TYPE_MINIMAL(BehaviorUpdateContext);
/// <summary>
/// Simulation time delta (in seconds) since the last update.
/// </summary>
float DeltaTime;
};
/// <summary>
/// Behavior update result.
/// </summary>
API_ENUM() enum class BehaviorUpdateResult
{
// Action completed successfully.
Success,
// Action is still running and active.
Running,
// Action failed.
Failed,
};