From 0defecaeb94b898e73e7518c38a303972db8db66 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 29 Aug 2023 11:41:07 +0200 Subject: [PATCH] Add concept of Goals to Behavior Knowledge --- .../BehaviorKnowledgeSelectorEditor.cs | 117 +++++++++++------- .../Windows/Assets/BehaviorTreeWindow.cs | 5 + Source/Engine/AI/BehaviorKnowledge.cpp | 55 +++++++- Source/Engine/AI/BehaviorKnowledge.h | 47 +++++++ Source/Engine/AI/BehaviorKnowledgeSelector.cs | 7 +- Source/Engine/AI/BehaviorTree.cs | 24 ++++ Source/Engine/AI/BehaviorTreeNodes.h | 6 +- 7 files changed, 213 insertions(+), 48 deletions(-) diff --git a/Source/Editor/CustomEditors/Editors/BehaviorKnowledgeSelectorEditor.cs b/Source/Editor/CustomEditors/Editors/BehaviorKnowledgeSelectorEditor.cs index 0da87c7ec..c8695e09a 100644 --- a/Source/Editor/CustomEditors/Editors/BehaviorKnowledgeSelectorEditor.cs +++ b/Source/Editor/CustomEditors/Editors/BehaviorKnowledgeSelectorEditor.cs @@ -43,7 +43,7 @@ namespace FlaxEditor.CustomEditors.Editors base.Refresh(); // Update label - _label.Text = Path; + _label.Text = _label.TooltipText = Path; } private string Path @@ -78,8 +78,8 @@ namespace FlaxEditor.CustomEditors.Editors { // Get Behavior Knowledge to select from var behaviorTreeWindow = Presenter.Owner as Windows.Assets.BehaviorTreeWindow; - var blackboard = behaviorTreeWindow?.Blackboard; - if (blackboard == null) + var rootNode = behaviorTreeWindow?.RootNode; + if (rootNode == null) return; var typed = ScriptType.Null; var valueType = Values[0].GetType(); @@ -88,49 +88,10 @@ namespace FlaxEditor.CustomEditors.Editors // Get typed selector type to show only assignable items typed = new ScriptType(valueType.GenericTypeArguments[0]); } - // TODO: add support for selecting goals and sensors // Create menu with tree-like structure and search box var menu = Utilities.Utils.CreateSearchPopup(out var searchBox, out var tree, 0, true); - var blackboardType = TypeUtils.GetObjectType(blackboard); - var items = GenericEditor.GetItemsForType(blackboardType, blackboardType.IsClass, true); var selected = Path; - var noneNode = new TreeNode - { - Text = "", - TooltipText = "Deselect value", - Parent = tree, - }; - if (string.IsNullOrEmpty(selected)) - tree.Select(noneNode); - var blackboardNode = new TreeNode - { - Text = "Blackboard", - TooltipText = blackboardType.TypeName, - Tag = "Blackboard/", // Ability to select whole blackboard data - Parent = tree, - }; - if (typed && !typed.IsAssignableFrom(blackboardType)) - blackboardNode.Tag = null; - if (string.Equals(selected, (string)blackboardNode.Tag, StringComparison.Ordinal)) - tree.Select(blackboardNode); - foreach (var item in items) - { - if (typed && !typed.IsAssignableFrom(item.Info.ValueType)) - continue; - var path = "Blackboard/" + item.Info.Name; - var node = new TreeNode - { - Text = item.DisplayName, - TooltipText = item.TooltipText, - Tag = path, - Parent = blackboardNode, - }; - if (string.Equals(selected, path, StringComparison.Ordinal)) - tree.Select(node); - // TODO: add support for nested items (eg. field from blackboard structure field) - } - blackboardNode.Expand(true); tree.SelectedChanged += delegate(List before, List after) { if (after.Count == 1) @@ -139,7 +100,79 @@ namespace FlaxEditor.CustomEditors.Editors Path = after[0].Tag as string; } }; + + // Empty + var noneNode = new TreeNode + { + Text = "", + TooltipText = "Deselect value", + Parent = tree, + }; + if (string.IsNullOrEmpty(selected)) + tree.Select(noneNode); + + // Blackboard + SetupPickerTypeItems(tree, typed, selected, "Blackboard", "Blackboard/", rootNode.BlackboardType); + + // Goals + var goalTypes = rootNode.GoalTypes; + if (goalTypes?.Length != 0) + { + var goalsNode = new TreeNode + { + Text = "Goal", + TooltipText = "List of goal types defined in Blackboard Tree", + Parent = tree, + }; + foreach (var goalTypeName in goalTypes) + { + var goalType = TypeUtils.GetType(goalTypeName); + if (goalType == null) + continue; + var goalTypeNode = SetupPickerTypeItems(tree, typed, selected, goalType.Name, "Goal/" + goalTypeName + "/", goalTypeName); + goalTypeNode.Parent = goalsNode; + } + goalsNode.ExpandAll(true); + } + menu.Show(_label, new Float2(0, _label.Height)); } + + private TreeNode SetupPickerTypeItems(Tree tree, ScriptType typed, string selected, string text, string typePath, string typeName) + { + var type = TypeUtils.GetType(typeName); + if (type == null) + return null; + var items = GenericEditor.GetItemsForType(type, type.IsClass, true); + var typeNode = new TreeNode + { + Text = text, + TooltipText = type.TypeName, + Tag = typePath, // Ability to select whole item type data (eg. whole blackboard value) + Parent = tree, + }; + if (typed && !typed.IsAssignableFrom(type)) + typeNode.Tag = null; + if (string.Equals(selected, (string)typeNode.Tag, StringComparison.Ordinal)) + tree.Select(typeNode); + foreach (var item in items) + { + if (typed && !typed.IsAssignableFrom(item.Info.ValueType)) + continue; + var itemPath = typePath + item.Info.Name; + var node = new TreeNode + { + Text = item.DisplayName, + TooltipText = item.TooltipText, + Tag = itemPath, + Parent = typeNode, + }; + if (string.Equals(selected, itemPath, StringComparison.Ordinal)) + tree.Select(node); + // TODO: add support for nested items (eg. field from blackboard structure field) + } + typeNode.Expand(true); + return typeNode; + } } } diff --git a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs index 07c024adf..5e2869c22 100644 --- a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs +++ b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs @@ -51,6 +51,11 @@ namespace FlaxEditor.Windows.Assets /// public object Blackboard => _knowledgePropertiesEditor.Selection.Count != 0 ? _knowledgePropertiesEditor.Selection[0] : null; + /// + /// Gets instance of the root node of the graph. Returns null if not added (or graph not yet loaded). + /// + public BehaviorTreeRootNode RootNode => (_surface.FindNode(19, 2) as Surface.Archetypes.BehaviorTree.Node)?.Instance as BehaviorTreeRootNode; + /// public BehaviorTreeWindow(Editor editor, BinaryAssetItem item) : base(editor, item) diff --git a/Source/Engine/AI/BehaviorKnowledge.cpp b/Source/Engine/AI/BehaviorKnowledge.cpp index 1ad5319c4..3b611cf11 100644 --- a/Source/Engine/AI/BehaviorKnowledge.cpp +++ b/Source/Engine/AI/BehaviorKnowledge.cpp @@ -100,7 +100,20 @@ bool AccessBehaviorKnowledge(BehaviorKnowledge* knowledge, const StringAnsiView& const StringAnsiView member(path.Get() + typeEnd + 1, path.Length() - typeEnd - 1); return AccessVariant(knowledge->Blackboard, member, value, set); } - // TODO: goals and sensors data access from BehaviorKnowledge via Selector + if (type == "Goal") + { + const StringAnsiView subPath(path.Get() + typeEnd + 1, path.Length() - typeEnd - 1); + const int32 goalTypeEnd = subPath.Find('/'); + const StringAnsiView goalType(subPath.Get(), goalTypeEnd); + const StringAnsiView member(subPath.Get() + goalTypeEnd + 1, subPath.Length() - goalTypeEnd - 1); + for (Variant& goal : knowledge->Goals) + { + if (goalType == goal.Type.GetTypeName()) + { + return AccessVariant(goal, member, value, set); + } + } + } return false; } @@ -161,6 +174,9 @@ void BehaviorKnowledge::FreeMemory() } RelevantNodes.Clear(); Blackboard.DeleteValue(); + for (Variant& goal : Goals) + goal.DeleteValue(); + Goals.Resize(0); Tree = nullptr; } @@ -174,6 +190,43 @@ bool BehaviorKnowledge::Set(const StringAnsiView& path, const Variant& value) return AccessBehaviorKnowledge(this, path, const_cast(value), true); } +bool BehaviorKnowledge::HasGoal(ScriptingTypeHandle type) const +{ + for (int32 i = 0; i < Goals.Count(); i++) + { + const ScriptingTypeHandle goalType = Scripting::FindScriptingType(Goals[i].Type.GetTypeName()); + if (goalType == type) + return true; + } + return false; +} + +void BehaviorKnowledge::AddGoal(Variant&& goal) +{ + int32 i = 0; + for (; i < Goals.Count(); i++) + { + if (Goals[i].Type == goal.Type) + break; + } + if (i == Goals.Count()) + Goals.AddDefault(); + Goals.Get()[i] = MoveTemp(goal); +} + +void BehaviorKnowledge::RemoveGoal(ScriptingTypeHandle type) +{ + for (int32 i = 0; i < Goals.Count(); i++) + { + const ScriptingTypeHandle goalType = Scripting::FindScriptingType(Goals[i].Type.GetTypeName()); + if (goalType == type) + { + Goals.RemoveAt(i); + break; + } + } +} + bool BehaviorKnowledge::CompareValues(float a, float b, BehaviorValueComparison comparison) { switch (comparison) diff --git a/Source/Engine/AI/BehaviorKnowledge.h b/Source/Engine/AI/BehaviorKnowledge.h index c4b515d43..036c5c22b 100644 --- a/Source/Engine/AI/BehaviorKnowledge.h +++ b/Source/Engine/AI/BehaviorKnowledge.h @@ -3,6 +3,7 @@ #pragma once #include "Engine/Core/Types/Variant.h" +#include "Engine/Core/Collections/Array.h" #include "Engine/Core/Collections/BitArray.h" #include "Engine/Scripting/ScriptingObject.h" @@ -43,6 +44,12 @@ API_CLASS() class FLAXENGINE_API BehaviorKnowledge : public ScriptingObject /// API_FIELD() Variant Blackboard; + /// + /// List of all active goals of the behaviour (structure or class). + /// + Array Goals; + +public: /// /// Initializes the knowledge for a certain tree. /// @@ -71,6 +78,46 @@ API_CLASS() class FLAXENGINE_API BehaviorKnowledge : public ScriptingObject /// True if set value, otherwise false. API_FUNCTION() bool Set(const StringAnsiView& path, const Variant& value); +public: + /// + /// Checks if knowledge has a given goal (exact type match without base class check). + /// + /// The goal type. + /// True if has a given goal, otherwise false. + API_FUNCTION() bool HasGoal(ScriptingTypeHandle type) const; + + /// + /// Checks if knowledge has a given goal (exact type match without base class check). + /// + /// True if has a given goal, otherwise false. + template + FORCE_INLINE bool HasGoal() + { + return HasGoal(T::TypeInitializer); + } + + /// + /// Adds the goal to the knowledge. If goal of that type already exists then it's value is updated. + /// + /// The goal value to add/set. + API_FUNCTION() void AddGoal(Variant&& goal); + + /// + /// Removes the goal from the knowledge. Does nothing if goal of the given type doesn't exist in the knowledge. + /// + /// The goal type. + API_FUNCTION() void RemoveGoal(ScriptingTypeHandle type); + + /// + /// Removes the goal from the knowledge. Does nothing if goal of the given type doesn't exist in the knowledge. + /// + template + FORCE_INLINE void RemoveGoal() + { + RemoveGoal(T::TypeInitializer); + } + +public: /// /// Compares two values and returns the comparision result. /// diff --git a/Source/Engine/AI/BehaviorKnowledgeSelector.cs b/Source/Engine/AI/BehaviorKnowledgeSelector.cs index 7d5698b6e..b77bf371f 100644 --- a/Source/Engine/AI/BehaviorKnowledgeSelector.cs +++ b/Source/Engine/AI/BehaviorKnowledgeSelector.cs @@ -179,10 +179,9 @@ namespace FlaxEngine /// The output value or null (if cannot read it - eg. missing goal or no blackboard entry of that name). public T Get(BehaviorKnowledge knowledge) { - object value = null; - if (knowledge != null) - knowledge.Get(Path, out value); - return (T)value; + if (knowledge != null && knowledge.Get(Path, out var value)) + return (T)value; + return default; } /// diff --git a/Source/Engine/AI/BehaviorTree.cs b/Source/Engine/AI/BehaviorTree.cs index d35cf2802..4b05491e3 100644 --- a/Source/Engine/AI/BehaviorTree.cs +++ b/Source/Engine/AI/BehaviorTree.cs @@ -11,6 +11,30 @@ using FlaxEngine.GUI; namespace FlaxEngine { + partial class BehaviorKnowledge + { + /// + /// Checks if knowledge has a given goal (exact type match without base class check). + /// + /// goal type. + /// True if ahs a given goal, otherwise false. + [Unmanaged] + public bool HasGoal() + { + return HasGoal(typeof(T)); + } + + /// + /// Removes the goal from the knowledge. Does nothing if goal of the given type doesn't exist in the knowledge. + /// + /// goal type. + [Unmanaged] + public void RemoveGoal() + { + RemoveGoal(typeof(T)); + } + } + partial class BehaviorTreeRootNode { #if FLAX_EDITOR diff --git a/Source/Engine/AI/BehaviorTreeNodes.h b/Source/Engine/AI/BehaviorTreeNodes.h index c8fd25e63..8ccc6a3db 100644 --- a/Source/Engine/AI/BehaviorTreeNodes.h +++ b/Source/Engine/AI/BehaviorTreeNodes.h @@ -86,8 +86,12 @@ API_CLASS(Sealed) class FLAXENGINE_API BehaviorTreeRootNode : public BehaviorTre API_FIELD(Attributes="EditorOrder(0), TypeReference(\"\", \"IsValidBlackboardType\"), CustomEditorAlias(\"FlaxEditor.CustomEditors.Editors.TypeNameEditor\")") StringAnsi BlackboardType; + // List of full typenames of the behavior goals (structure or class). + API_FIELD(Attributes="EditorOrder(10), Collection(OverrideEditorTypeName=\"FlaxEditor.CustomEditors.Editors.TypeNameEditor\")") + Array GoalTypes; + // The target amount of the behavior logic updates per second. - API_FIELD(Attributes="EditorOrder(10)") + API_FIELD(Attributes="EditorOrder(100)") float UpdateFPS = 10.0f; };