Add Decorators support to BT graph
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FlaxEditor.GUI.ContextMenu;
|
||||
using FlaxEditor.Scripting;
|
||||
using FlaxEditor.Surface.Elements;
|
||||
using FlaxEngine;
|
||||
@@ -17,26 +19,16 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
public static class BehaviorTree
|
||||
{
|
||||
/// <summary>
|
||||
/// Customized <see cref="SurfaceNode" /> for the Behavior Tree node.
|
||||
/// Base class for Behavior Tree nodes wrapped inside <see cref="SurfaceNode" />.
|
||||
/// </summary>
|
||||
internal class Node : SurfaceNode
|
||||
internal class NodeBase : SurfaceNode
|
||||
{
|
||||
private const float ConnectionAreaMargin = 12.0f;
|
||||
private const float ConnectionAreaHeight = 12.0f;
|
||||
|
||||
private ScriptType _type;
|
||||
private InputBox _input;
|
||||
private OutputBox _output;
|
||||
protected ScriptType _type;
|
||||
internal bool _isValueEditing;
|
||||
|
||||
public BehaviorTreeNode Instance;
|
||||
|
||||
internal static SurfaceNode Create(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
|
||||
{
|
||||
return new Node(id, context, nodeArch, groupArch);
|
||||
}
|
||||
|
||||
internal Node(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
|
||||
protected NodeBase(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
|
||||
: base(id, context, nodeArch, groupArch)
|
||||
{
|
||||
}
|
||||
@@ -48,11 +40,13 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
title = title.Substring(12);
|
||||
if (title.EndsWith("Node"))
|
||||
title = title.Substring(0, title.Length - 4);
|
||||
if (title.EndsWith("Decorator"))
|
||||
title = title.Substring(0, title.Length - 9);
|
||||
title = Utilities.Utils.GetPropertyNameUI(title);
|
||||
return title;
|
||||
}
|
||||
|
||||
private void UpdateTitle()
|
||||
protected virtual void UpdateTitle()
|
||||
{
|
||||
string title = null;
|
||||
if (Instance != null)
|
||||
@@ -66,34 +60,18 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
var typeName = (string)Values[0];
|
||||
title = "Missing Type " + typeName;
|
||||
}
|
||||
if (title != Title)
|
||||
{
|
||||
Title = title;
|
||||
ResizeAuto();
|
||||
}
|
||||
Title = title;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLoaded()
|
||||
public override void OnLoaded(SurfaceNodeActions action)
|
||||
{
|
||||
base.OnLoaded();
|
||||
|
||||
// Setup boxes
|
||||
_input = (InputBox)GetBox(0);
|
||||
_output = (OutputBox)GetBox(1);
|
||||
base.OnLoaded(action);
|
||||
|
||||
// Setup node type and data
|
||||
var flagsRoot = NodeFlags.NoRemove | NodeFlags.NoCloseButton | NodeFlags.NoSpawnViaPaste;
|
||||
var flags = Archetype.Flags & ~flagsRoot;
|
||||
var typeName = (string)Values[0];
|
||||
_type = TypeUtils.GetType(typeName);
|
||||
if (_type != null)
|
||||
{
|
||||
bool isRoot = _type.Type == typeof(BehaviorTreeRootNode);
|
||||
_input.Enabled = _input.Visible = !isRoot;
|
||||
_output.Enabled = _output.Visible = new ScriptType(typeof(BehaviorTreeCompoundNode)).IsAssignableFrom(_type);
|
||||
if (isRoot)
|
||||
flags |= flagsRoot;
|
||||
TooltipText = Editor.Instance.CodeDocs.GetTooltip(_type);
|
||||
try
|
||||
{
|
||||
@@ -112,6 +90,259 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
{
|
||||
Instance = null;
|
||||
}
|
||||
|
||||
UpdateTitle();
|
||||
}
|
||||
|
||||
public override void OnValuesChanged()
|
||||
{
|
||||
base.OnValuesChanged();
|
||||
|
||||
// Skip updating instance when it's being edited by user via UI
|
||||
if (!_isValueEditing)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Instance != null)
|
||||
{
|
||||
// Reload node instance from data
|
||||
var instanceData = (byte[])Values[1];
|
||||
if (instanceData == null || instanceData.Length == 0)
|
||||
{
|
||||
// Recreate instance data to default state if previous state was empty
|
||||
var defaultInstance = (BehaviorTreeNode)_type.CreateInstance(); // TODO: use default instance from native ScriptingType
|
||||
instanceData = FlaxEngine.Json.JsonSerializer.SaveToBytes(defaultInstance);
|
||||
}
|
||||
FlaxEngine.Json.JsonSerializer.LoadFromBytes(Instance, instanceData, Globals.EngineBuildNumber);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Editor.LogError("Failed to load Behavior Tree node of type " + _type);
|
||||
Editor.LogWarning(ex);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateTitle();
|
||||
}
|
||||
|
||||
public override void OnSpawned(SurfaceNodeActions action)
|
||||
{
|
||||
base.OnSpawned(action);
|
||||
|
||||
ResizeAuto();
|
||||
}
|
||||
|
||||
public override void OnDestroy()
|
||||
{
|
||||
if (IsDisposing)
|
||||
return;
|
||||
_type = ScriptType.Null;
|
||||
Object.Destroy(ref Instance);
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Customized <see cref="SurfaceNode" /> for the Behavior Tree node.
|
||||
/// </summary>
|
||||
internal class Node : NodeBase
|
||||
{
|
||||
private const float ConnectionAreaMargin = 12.0f;
|
||||
private const float ConnectionAreaHeight = 12.0f;
|
||||
private const float DecoratorsMarginX = 5.0f;
|
||||
private const float DecoratorsMarginY = 2.0f;
|
||||
|
||||
private InputBox _input;
|
||||
private OutputBox _output;
|
||||
internal List<Decorator> _decorators;
|
||||
|
||||
internal static SurfaceNode Create(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
|
||||
{
|
||||
return new Node(id, context, nodeArch, groupArch);
|
||||
}
|
||||
|
||||
internal Node(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
|
||||
: base(id, context, nodeArch, groupArch)
|
||||
{
|
||||
}
|
||||
|
||||
public unsafe List<uint> DecoratorIds
|
||||
{
|
||||
get
|
||||
{
|
||||
var result = new List<uint>();
|
||||
var ids = Values.Length >= 3 ? Values[2] as byte[] : null;
|
||||
if (ids != null)
|
||||
{
|
||||
fixed (byte* data = ids)
|
||||
{
|
||||
uint* ptr = (uint*)data;
|
||||
int count = ids.Length / sizeof(uint);
|
||||
for (int i = 0; i < count; i++)
|
||||
result.Add(ptr[i]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
set
|
||||
{
|
||||
var ids = new byte[sizeof(uint) * value.Count];
|
||||
if (value != null)
|
||||
{
|
||||
fixed (byte* data = ids)
|
||||
{
|
||||
uint* ptr = (uint*)data;
|
||||
for (var i = 0; i < value.Count; i++)
|
||||
ptr[i] = value[i];
|
||||
}
|
||||
}
|
||||
SetValue(2, ids);
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe List<Decorator> Decorators
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_decorators == null)
|
||||
{
|
||||
_decorators = new List<Decorator>();
|
||||
var ids = Values.Length >= 3 ? Values[2] as byte[] : null;
|
||||
if (ids != null)
|
||||
{
|
||||
fixed (byte* data = ids)
|
||||
{
|
||||
uint* ptr = (uint*)data;
|
||||
int count = ids.Length / sizeof(uint);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var decorator = Surface.FindNode(ptr[i]) as Decorator;
|
||||
if (decorator != null)
|
||||
_decorators.Add(decorator);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return _decorators;
|
||||
}
|
||||
set
|
||||
{
|
||||
_decorators = null;
|
||||
var ids = new byte[sizeof(uint) * value.Count];
|
||||
if (value != null)
|
||||
{
|
||||
fixed (byte* data = ids)
|
||||
{
|
||||
uint* ptr = (uint*)data;
|
||||
for (var i = 0; i < value.Count; i++)
|
||||
ptr[i] = value[i].ID;
|
||||
}
|
||||
}
|
||||
SetValue(2, ids);
|
||||
}
|
||||
}
|
||||
|
||||
public override unsafe SurfaceNode[] SealedNodes
|
||||
{
|
||||
get
|
||||
{
|
||||
SurfaceNode[] result = null;
|
||||
var ids = Values.Length >= 3 ? Values[2] as byte[] : null;
|
||||
if (ids != null)
|
||||
{
|
||||
fixed (byte* data = ids)
|
||||
{
|
||||
uint* ptr = (uint*)data;
|
||||
int count = ids.Length / sizeof(uint);
|
||||
result = new SurfaceNode[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var decorator = Surface.FindNode(ptr[i]) as Decorator;
|
||||
if (decorator != null)
|
||||
result[i] = decorator;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnShowSecondaryContextMenu(FlaxEditor.GUI.ContextMenu.ContextMenu menu, Float2 location)
|
||||
{
|
||||
base.OnShowSecondaryContextMenu(menu, location);
|
||||
|
||||
if (!Surface.CanEdit)
|
||||
return;
|
||||
|
||||
menu.AddSeparator();
|
||||
|
||||
var nodeTypes = Editor.Instance.CodeEditing.BehaviorTreeNodes.Get();
|
||||
|
||||
if (_input.Enabled) // Root node cannot have decorators
|
||||
{
|
||||
var decorators = menu.AddChildMenu("Add Decorator");
|
||||
var decoratorType = new ScriptType(typeof(BehaviorTreeDecorator));
|
||||
foreach (var nodeType in nodeTypes)
|
||||
{
|
||||
if (nodeType != decoratorType && decoratorType.IsAssignableFrom(nodeType))
|
||||
{
|
||||
var button = decorators.ContextMenu.AddButton(GetTitle(nodeType));
|
||||
button.Tag = nodeType;
|
||||
button.TooltipText = Editor.Instance.CodeDocs.GetTooltip(nodeType);
|
||||
button.ButtonClicked += OnAddDecoratorButtonClicked;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddDecoratorButtonClicked(ContextMenuButton button)
|
||||
{
|
||||
var nodeType = (ScriptType)button.Tag;
|
||||
|
||||
// Spawn decorator
|
||||
var decorator = Context.SpawnNode(19, 3, Location, new object[]
|
||||
{
|
||||
nodeType.TypeName,
|
||||
Utils.GetEmptyArray<byte>(),
|
||||
});
|
||||
|
||||
// Add decorator to the node
|
||||
var decorators = Decorators;
|
||||
decorators.Add((Decorator)decorator);
|
||||
Decorators = decorators;
|
||||
}
|
||||
|
||||
public override void OnValuesChanged()
|
||||
{
|
||||
// Reject cached value
|
||||
_decorators = null;
|
||||
|
||||
base.OnValuesChanged();
|
||||
|
||||
ResizeAuto();
|
||||
}
|
||||
|
||||
public override void OnLoaded(SurfaceNodeActions action)
|
||||
{
|
||||
base.OnLoaded(action);
|
||||
|
||||
// Setup boxes
|
||||
_input = (InputBox)GetBox(0);
|
||||
_output = (OutputBox)GetBox(1);
|
||||
|
||||
// Setup node type and data
|
||||
var flagsRoot = NodeFlags.NoRemove | NodeFlags.NoCloseButton | NodeFlags.NoSpawnViaPaste;
|
||||
var flags = Archetype.Flags & ~flagsRoot;
|
||||
if (_type != null)
|
||||
{
|
||||
bool isRoot = _type.Type == typeof(BehaviorTreeRootNode);
|
||||
_input.Enabled = _input.Visible = !isRoot;
|
||||
_output.Enabled = _output.Visible = new ScriptType(typeof(BehaviorTreeCompoundNode)).IsAssignableFrom(_type);
|
||||
if (isRoot)
|
||||
flags |= flagsRoot;
|
||||
}
|
||||
if (Archetype.Flags != flags)
|
||||
{
|
||||
// Apply custom flags
|
||||
@@ -119,18 +350,60 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
Archetype.Flags = flags;
|
||||
}
|
||||
|
||||
UpdateTitle();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnSpawned()
|
||||
{
|
||||
base.OnSpawned();
|
||||
|
||||
ResizeAuto();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override unsafe void OnPasted(Dictionary<uint, uint> idsMapping)
|
||||
{
|
||||
base.OnPasted(idsMapping);
|
||||
|
||||
// Update decorators
|
||||
var ids = Values.Length >= 3 ? Values[2] as byte[] : null;
|
||||
if (ids != null)
|
||||
{
|
||||
_decorators = null;
|
||||
fixed (byte* data = ids)
|
||||
{
|
||||
uint* ptr = (uint*)data;
|
||||
int count = ids.Length / sizeof(uint);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (idsMapping.TryGetValue(ptr[i], out var id))
|
||||
{
|
||||
// Fix previous parent node to re-apply layout (in case it was forced by spawned decorator)
|
||||
var decorator = Surface.FindNode(ptr[i]) as Decorator;
|
||||
var decoratorNode = decorator?.Node;
|
||||
if (decoratorNode != null)
|
||||
decoratorNode.ResizeAuto();
|
||||
|
||||
// Update mapping to the new node
|
||||
ptr[i] = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
Values[2] = ids;
|
||||
ResizeAuto();
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnSurfaceLoaded(SurfaceNodeActions action)
|
||||
{
|
||||
base.OnSurfaceLoaded(action);
|
||||
|
||||
ResizeAuto();
|
||||
Surface.NodeDeleted += OnNodeDeleted;
|
||||
}
|
||||
|
||||
private void OnNodeDeleted(SurfaceNode node)
|
||||
{
|
||||
if (node is Decorator decorator && Decorators.Contains(decorator))
|
||||
{
|
||||
// Decorator was spawned (eg. via undo)
|
||||
_decorators = null;
|
||||
ResizeAuto();
|
||||
}
|
||||
}
|
||||
|
||||
public override void ResizeAuto()
|
||||
{
|
||||
if (Surface == null)
|
||||
@@ -144,74 +417,134 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
height += ConnectionAreaHeight;
|
||||
if (_output != null && _output.Visible)
|
||||
height += ConnectionAreaHeight;
|
||||
var decorators = Decorators;
|
||||
foreach (var decorator in decorators)
|
||||
{
|
||||
decorator.ResizeAuto();
|
||||
height += decorator.Height + DecoratorsMarginY;
|
||||
width = Mathf.Max(width, decorator.Width + 2 * DecoratorsMarginX);
|
||||
}
|
||||
Size = new Float2(width + FlaxEditor.Surface.Constants.NodeMarginX * 2, height + FlaxEditor.Surface.Constants.NodeHeaderSize + FlaxEditor.Surface.Constants.NodeFooterSize);
|
||||
if (_input != null && _input.Visible)
|
||||
_input.Bounds = new Rectangle(ConnectionAreaMargin, 0, Width - ConnectionAreaMargin * 2, ConnectionAreaHeight);
|
||||
if (_output != null && _output.Visible)
|
||||
_output.Bounds = new Rectangle(ConnectionAreaMargin, Height - ConnectionAreaHeight, Width - ConnectionAreaMargin * 2, ConnectionAreaHeight);
|
||||
UpdateRectangles();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UpdateRectangles()
|
||||
{
|
||||
base.UpdateRectangles();
|
||||
|
||||
// Update boxes placement
|
||||
Rectangle bounds = Bounds;
|
||||
if (_input != null && _input.Visible)
|
||||
{
|
||||
_input.Bounds = new Rectangle(ConnectionAreaMargin, 0, Width - ConnectionAreaMargin * 2, ConnectionAreaHeight);
|
||||
bounds.Location.Y += _input.Height;
|
||||
}
|
||||
var decorators = Decorators;
|
||||
var indexInParent = IndexInParent;
|
||||
foreach (var decorator in decorators)
|
||||
{
|
||||
decorator.Bounds = new Rectangle(bounds.Location.X + DecoratorsMarginX, bounds.Location.Y, bounds.Width - 2 * DecoratorsMarginX, decorator.Height);
|
||||
bounds.Location.Y += decorator.Height + DecoratorsMarginY;
|
||||
if (decorator.IndexInParent < indexInParent)
|
||||
decorator.IndexInParent = indexInParent + 1; // Push elements above the node
|
||||
}
|
||||
const float footerSize = FlaxEditor.Surface.Constants.NodeFooterSize;
|
||||
const float headerSize = FlaxEditor.Surface.Constants.NodeHeaderSize;
|
||||
const float closeButtonMargin = FlaxEditor.Surface.Constants.NodeCloseButtonMargin;
|
||||
const float closeButtonSize = FlaxEditor.Surface.Constants.NodeCloseButtonSize;
|
||||
_headerRect = new Rectangle(0, 0, Width, headerSize);
|
||||
if (_input != null && _input.Visible)
|
||||
_headerRect.Y += ConnectionAreaHeight;
|
||||
_footerRect = new Rectangle(0, _headerRect.Bottom, Width, footerSize);
|
||||
_closeButtonRect = new Rectangle(Width - closeButtonSize - closeButtonMargin, _headerRect.Y + closeButtonMargin, closeButtonSize, closeButtonSize);
|
||||
_headerRect = new Rectangle(0, bounds.Y - Y, bounds.Width, headerSize);
|
||||
_closeButtonRect = new Rectangle(bounds.Width - closeButtonSize - closeButtonMargin, _headerRect.Y + closeButtonMargin, closeButtonSize, closeButtonSize);
|
||||
_footerRect = new Rectangle(0, _headerRect.Bottom, bounds.Width, footerSize);
|
||||
if (_output != null && _output.Visible)
|
||||
_output.Bounds = new Rectangle(ConnectionAreaMargin, bounds.Height - ConnectionAreaHeight, bounds.Width - ConnectionAreaMargin * 2, ConnectionAreaHeight);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnValuesChanged()
|
||||
protected override void OnLocationChanged()
|
||||
{
|
||||
base.OnValuesChanged();
|
||||
base.OnLocationChanged();
|
||||
|
||||
if (_isValueEditing)
|
||||
{
|
||||
// Skip updating instance when it's being edited by user via UI
|
||||
UpdateTitle();
|
||||
return;
|
||||
}
|
||||
// Sync attached elements placement
|
||||
UpdateRectangles();
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
/// <summary>
|
||||
/// Customized <see cref="SurfaceNode" /> for the Behavior Tree decorator.
|
||||
/// </summary>
|
||||
internal class Decorator : NodeBase
|
||||
{
|
||||
internal static SurfaceNode Create(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
|
||||
{
|
||||
return new Decorator(id, context, nodeArch, groupArch);
|
||||
}
|
||||
|
||||
internal Decorator(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
|
||||
: base(id, context, nodeArch, groupArch)
|
||||
{
|
||||
}
|
||||
|
||||
public Node Node
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Instance != null)
|
||||
foreach (var node in Surface.Nodes)
|
||||
{
|
||||
// Reload node instance from data
|
||||
var instanceData = (byte[])Values[1];
|
||||
if (instanceData == null || instanceData.Length == 0)
|
||||
{
|
||||
// Recreate instance data to default state if previous state was empty
|
||||
var defaultInstance = (BehaviorTreeNode)_type.CreateInstance(); // TODO: use default instance from native ScriptingType
|
||||
instanceData = FlaxEngine.Json.JsonSerializer.SaveToBytes(defaultInstance);
|
||||
}
|
||||
FlaxEngine.Json.JsonSerializer.LoadFromBytes(Instance, instanceData, Globals.EngineBuildNumber);
|
||||
UpdateTitle();
|
||||
if (node is Node n && n.DecoratorIds.Contains(ID))
|
||||
return n;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override Color FooterColor => Color.Transparent;
|
||||
|
||||
protected override Float2 CalculateNodeSize(float width, float height)
|
||||
{
|
||||
return new Float2(width, height + FlaxEditor.Surface.Constants.NodeHeaderSize);
|
||||
}
|
||||
|
||||
protected override void UpdateRectangles()
|
||||
{
|
||||
base.UpdateRectangles();
|
||||
|
||||
_footerRect = Rectangle.Empty;
|
||||
}
|
||||
|
||||
protected override void UpdateTitle()
|
||||
{
|
||||
var title = Title;
|
||||
|
||||
base.UpdateTitle();
|
||||
|
||||
// Update parent node on title change
|
||||
if (title != Title)
|
||||
Node?.ResizeAuto();
|
||||
}
|
||||
|
||||
public override void Draw()
|
||||
{
|
||||
base.Draw();
|
||||
|
||||
// Outline
|
||||
if (!_isSelected)
|
||||
{
|
||||
var style = Style.Current;
|
||||
var rect = new Rectangle(Float2.Zero, Size);
|
||||
Render2D.DrawRectangle(rect, style.BorderHighlighted);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnSurfaceLoaded(SurfaceNodeActions action)
|
||||
{
|
||||
base.OnSurfaceLoaded(action);
|
||||
|
||||
if (action == SurfaceNodeActions.Undo)
|
||||
{
|
||||
// Update parent node layout when restoring decorator from undo
|
||||
var node = Node;
|
||||
if (node != null)
|
||||
{
|
||||
node._decorators = null;
|
||||
node.ResizeAuto();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Editor.LogError("Failed to load Behavior Tree node of type " + _type);
|
||||
Editor.LogWarning(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnDestroy()
|
||||
{
|
||||
if (IsDisposing)
|
||||
return;
|
||||
_type = ScriptType.Null;
|
||||
Object.Destroy(ref Instance);
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,13 +555,14 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
{
|
||||
new NodeArchetype
|
||||
{
|
||||
TypeID = 1,
|
||||
TypeID = 1, // Task Node
|
||||
Create = Node.Create,
|
||||
Flags = NodeFlags.BehaviorTreeGraph | NodeFlags.NoSpawnViaGUI,
|
||||
DefaultValues = new object[]
|
||||
{
|
||||
string.Empty, // Type Name
|
||||
Utils.GetEmptyArray<byte>(), // Instance Data
|
||||
null, // List of Decorator Nodes IDs
|
||||
},
|
||||
Size = new Float2(100, 0),
|
||||
Elements = new[]
|
||||
@@ -239,7 +573,7 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
},
|
||||
new NodeArchetype
|
||||
{
|
||||
TypeID = 2,
|
||||
TypeID = 2, // Root Node
|
||||
Create = Node.Create,
|
||||
Flags = NodeFlags.BehaviorTreeGraph | NodeFlags.NoSpawnViaGUI,
|
||||
DefaultValues = new object[]
|
||||
@@ -254,6 +588,18 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
NodeElementArchetype.Factory.Output(0, string.Empty, ScriptType.Void, 1),
|
||||
}
|
||||
},
|
||||
new NodeArchetype
|
||||
{
|
||||
TypeID = 3, // Decorator Node
|
||||
Create = Decorator.Create,
|
||||
Flags = NodeFlags.BehaviorTreeGraph | NodeFlags.NoSpawnViaGUI | NodeFlags.NoMove,
|
||||
DefaultValues = new object[]
|
||||
{
|
||||
string.Empty, // Type Name
|
||||
Utils.GetEmptyArray<byte>(), // Instance Data
|
||||
},
|
||||
Size = new Float2(100, 0),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,10 @@ namespace FlaxEditor.Surface
|
||||
scriptType == typeof(BehaviorTreeRootNode))
|
||||
return;
|
||||
|
||||
// Nodes-only
|
||||
if (new ScriptType(typeof(BehaviorTreeDecorator)).IsAssignableFrom(scriptType))
|
||||
return;
|
||||
|
||||
// Create group archetype
|
||||
var groupKey = new KeyValuePair<string, ushort>("Behavior Tree", 19);
|
||||
if (!cache.TryGetValue(groupKey, out var group))
|
||||
@@ -171,6 +175,7 @@ namespace FlaxEditor.Surface
|
||||
{
|
||||
typeof(BehaviorTreeSubTreeNode).FullName,
|
||||
FlaxEngine.Json.JsonSerializer.SaveToBytes(instance),
|
||||
null,
|
||||
});
|
||||
FlaxEngine.Object.Destroy(instance);
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
for (var i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
if (nodes[i] is Surface.Archetypes.BehaviorTree.Node node && node.IsSelected && node.Instance)
|
||||
if (nodes[i] is Surface.Archetypes.BehaviorTree.NodeBase node && node.IsSelected && node.Instance)
|
||||
selection.Add(node.Instance);
|
||||
}
|
||||
}
|
||||
@@ -153,7 +153,7 @@ namespace FlaxEditor.Windows.Assets
|
||||
// Sync instance data with surface node value storage
|
||||
for (var j = 0; j < nodes.Count; j++)
|
||||
{
|
||||
if (nodes[j] is Surface.Archetypes.BehaviorTree.Node node && node.Instance == instance)
|
||||
if (nodes[j] is Surface.Archetypes.BehaviorTree.NodeBase node && node.Instance == instance)
|
||||
{
|
||||
node._isValueEditing = true;
|
||||
node.SetValue(1, FlaxEngine.Json.JsonSerializer.SaveToBytes(instance));
|
||||
|
||||
@@ -65,7 +65,7 @@ void BehaviorTreeGraph::Clear()
|
||||
|
||||
bool BehaviorTreeGraph::onNodeLoaded(Node* n)
|
||||
{
|
||||
if (n->GroupID == 19 && (n->TypeID == 1 || n->TypeID == 2))
|
||||
if (n->GroupID == 19 && (n->TypeID == 1 || n->TypeID == 2 || n->TypeID == 3))
|
||||
{
|
||||
// Create node instance object
|
||||
ScriptingTypeHandle type = Scripting::FindScriptingType((StringAnsiView)n->Values[0]);
|
||||
@@ -102,6 +102,26 @@ void BehaviorTreeGraph::LoadRecursive(Node& node)
|
||||
NodesStatesSize += node.Instance->GetStateSize();
|
||||
NodesCount++;
|
||||
|
||||
if (node.TypeID == 1 && node.Values.Count() >= 3)
|
||||
{
|
||||
// Load node decorators
|
||||
const auto& decoratorIds = node.Values[2];
|
||||
if (decoratorIds.Type.Type == VariantType::Blob && decoratorIds.AsBlob.Length)
|
||||
{
|
||||
const Span<uint32> ids((uint32*)decoratorIds.AsBlob.Data, decoratorIds.AsBlob.Length / sizeof(uint32));
|
||||
for (int32 i = 0; i < ids.Length(); i++)
|
||||
{
|
||||
Node* decorator = GetNode(ids[i]);
|
||||
if (decorator && decorator->Instance)
|
||||
{
|
||||
ASSERT_LOW_LAYER(decorator->Instance->Is<BehaviorTreeDecorator>());
|
||||
node.Instance->_decorators.Add((BehaviorTreeDecorator*)decorator->Instance);
|
||||
decorator->Instance->_parent = node.Instance;
|
||||
LoadRecursive(*decorator);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (auto* nodeCompound = ScriptingObject::Cast<BehaviorTreeCompoundNode>(node.Instance))
|
||||
{
|
||||
auto& children = node.Boxes[1].Connections;
|
||||
@@ -116,6 +136,7 @@ void BehaviorTreeGraph::LoadRecursive(Node& node)
|
||||
if (child && child->Instance)
|
||||
{
|
||||
nodeCompound->Children.Add(child->Instance);
|
||||
child->Instance->_parent = nodeCompound;
|
||||
LoadRecursive(*child);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Engine/Core/Collections/Array.h"
|
||||
#include "Engine/Scripting/SerializableScriptingObject.h"
|
||||
#include "BehaviorTypes.h"
|
||||
|
||||
@@ -21,6 +22,11 @@ protected:
|
||||
API_FIELD(ReadOnly) int32 _memoryOffset = 0;
|
||||
// Execution index of the node within tree.
|
||||
API_FIELD(ReadOnly) int32 _executionIndex = -1;
|
||||
// Parent node that owns this node (parent composite or decorator attachment node).
|
||||
API_FIELD(ReadOnly) BehaviorTreeNode* _parent = nullptr;
|
||||
|
||||
private:
|
||||
Array<class BehaviorTreeDecorator*, InlinedAllocation<8>> _decorators;
|
||||
|
||||
public:
|
||||
/// <summary>
|
||||
@@ -74,6 +80,10 @@ public:
|
||||
|
||||
// Helper utility to update node with state creation/cleanup depending on node relevancy.
|
||||
BehaviorUpdateResult InvokeUpdate(const BehaviorUpdateContext& context);
|
||||
// Helper utility to make node relevant and init it state.
|
||||
void BecomeRelevant(const BehaviorUpdateContext& context);
|
||||
// Helper utility to make node irrelevant and release its state (including any nested nodes).
|
||||
virtual void BecomeIrrelevant(const BehaviorUpdateContext& context, bool nodeOnly = false);
|
||||
|
||||
// [SerializableScriptingObject]
|
||||
void Serialize(SerializeStream& stream, const void* otherObj) override;
|
||||
@@ -87,8 +97,31 @@ public:
|
||||
ASSERT((int32)sizeof(T) <= GetStateSize());
|
||||
return reinterpret_cast<T*>((byte*)memory + _memoryOffset);
|
||||
}
|
||||
};
|
||||
|
||||
protected:
|
||||
virtual void InvokeReleaseState(const BehaviorUpdateContext& context);
|
||||
};
|
||||
/// <summary>
|
||||
/// Base class for Behavior Tree node decorators. Decorators can implement conditional filtering or override node logic and execution flow.
|
||||
/// </summary>
|
||||
API_CLASS(Abstract) class FLAXENGINE_API BehaviorTreeDecorator : public BehaviorTreeNode
|
||||
{
|
||||
DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(BehaviorTreeDecorator, BehaviorTreeNode);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the node can be updated (eg. decorator can block it depending on the gameplay conditions or its state).
|
||||
/// </summary>
|
||||
/// <param name="context">Behavior update context data.</param>
|
||||
/// <returns>True if can update, otherwise false to block it.</returns>
|
||||
API_FUNCTION() virtual bool CanUpdate(BehaviorUpdateContext context)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after node update to post-process result or perform additional action.
|
||||
/// </summary>
|
||||
/// <param name="context">Behavior update context data.</param>
|
||||
/// <param name="result">The node update result. Can be modified by the decorator (eg. to force success).</param>
|
||||
API_FUNCTION() virtual void PostUpdate(BehaviorUpdateContext context, API_PARAM(ref) BehaviorUpdateResult& result)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
@@ -42,26 +42,72 @@ bool IsAssignableFrom(const StringAnsiView& to, const StringAnsiView& from)
|
||||
BehaviorUpdateResult BehaviorTreeNode::InvokeUpdate(const BehaviorUpdateContext& context)
|
||||
{
|
||||
ASSERT_LOW_LAYER(_executionIndex != -1);
|
||||
BitArray<>& relevantNodes = *(BitArray<>*)context.RelevantNodes;
|
||||
if (relevantNodes.Get(_executionIndex) == false)
|
||||
const BitArray<>& relevantNodes = *(const BitArray<>*)context.RelevantNodes;
|
||||
|
||||
// Check decorators if node can be executed
|
||||
for (BehaviorTreeDecorator* decorator : _decorators)
|
||||
{
|
||||
// Node becomes relevant so initialize it's state
|
||||
relevantNodes.Set(_executionIndex, true);
|
||||
InitState(context.Behavior, context.Memory);
|
||||
ASSERT_LOW_LAYER(decorator->_executionIndex != -1);
|
||||
if (relevantNodes.Get(decorator->_executionIndex) == false)
|
||||
decorator->BecomeRelevant(context);
|
||||
if (!decorator->CanUpdate(context))
|
||||
{
|
||||
return BehaviorUpdateResult::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
// Make node relevant before update
|
||||
if (relevantNodes.Get(_executionIndex) == false)
|
||||
BecomeRelevant(context);
|
||||
|
||||
// Node-specific update
|
||||
const BehaviorUpdateResult result = Update(context);
|
||||
for (BehaviorTreeDecorator* decorator : _decorators)
|
||||
{
|
||||
decorator->Update(context);
|
||||
}
|
||||
BehaviorUpdateResult result = Update(context);
|
||||
for (BehaviorTreeDecorator* decorator : _decorators)
|
||||
{
|
||||
decorator->PostUpdate(context, result);
|
||||
}
|
||||
|
||||
// Check if node is not relevant anymore
|
||||
if (result != BehaviorUpdateResult::Running)
|
||||
{
|
||||
InvokeReleaseState(context);
|
||||
}
|
||||
BecomeIrrelevant(context);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void BehaviorTreeNode::BecomeRelevant(const BehaviorUpdateContext& context)
|
||||
{
|
||||
// Initialize state
|
||||
BitArray<>& relevantNodes = *(BitArray<>*)context.RelevantNodes;
|
||||
ASSERT_LOW_LAYER(relevantNodes.Get(_executionIndex) == false);
|
||||
relevantNodes.Set(_executionIndex, true);
|
||||
InitState(context.Behavior, context.Memory);
|
||||
}
|
||||
|
||||
void BehaviorTreeNode::BecomeIrrelevant(const BehaviorUpdateContext& context, bool nodeOnly)
|
||||
{
|
||||
// Release state
|
||||
BitArray<>& relevantNodes = *(BitArray<>*)context.RelevantNodes;
|
||||
ASSERT_LOW_LAYER(relevantNodes.Get(_executionIndex) == true);
|
||||
relevantNodes.Set(_executionIndex, false);
|
||||
ReleaseState(context.Behavior, context.Memory);
|
||||
|
||||
if (nodeOnly)
|
||||
return;
|
||||
|
||||
// Release decorators
|
||||
for (BehaviorTreeDecorator* decorator : _decorators)
|
||||
{
|
||||
if (relevantNodes.Get(decorator->_executionIndex) == true)
|
||||
{
|
||||
decorator->BecomeIrrelevant(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BehaviorTreeNode::Serialize(SerializeStream& stream, const void* otherObj)
|
||||
{
|
||||
SerializableScriptingObject::Serialize(stream, otherObj);
|
||||
@@ -78,13 +124,6 @@ void BehaviorTreeNode::Deserialize(DeserializeStream& stream, ISerializeModifier
|
||||
DESERIALIZE(Name);
|
||||
}
|
||||
|
||||
void BehaviorTreeNode::InvokeReleaseState(const BehaviorUpdateContext& context)
|
||||
{
|
||||
BitArray<>& relevantNodes = *(BitArray<>*)context.RelevantNodes;
|
||||
relevantNodes.Set(_executionIndex, false);
|
||||
ReleaseState(context.Behavior, context.Memory);
|
||||
}
|
||||
|
||||
void BehaviorTreeCompoundNode::Init(BehaviorTree* tree)
|
||||
{
|
||||
for (BehaviorTreeNode* child : Children)
|
||||
@@ -102,18 +141,19 @@ BehaviorUpdateResult BehaviorTreeCompoundNode::Update(BehaviorUpdateContext cont
|
||||
return result;
|
||||
}
|
||||
|
||||
void BehaviorTreeCompoundNode::InvokeReleaseState(const BehaviorUpdateContext& context)
|
||||
void BehaviorTreeCompoundNode::BecomeIrrelevant(const BehaviorUpdateContext& context, bool nodeOnly)
|
||||
{
|
||||
// Make any nested nodes irrelevant as well
|
||||
const BitArray<>& relevantNodes = *(const BitArray<>*)context.RelevantNodes;
|
||||
for (BehaviorTreeNode* child : Children)
|
||||
{
|
||||
if (relevantNodes.Get(child->_executionIndex) == true)
|
||||
{
|
||||
child->InvokeReleaseState(context);
|
||||
child->BecomeIrrelevant(context);
|
||||
}
|
||||
}
|
||||
|
||||
BehaviorTreeNode::InvokeReleaseState(context);
|
||||
BehaviorTreeNode::BecomeIrrelevant(context, nodeOnly);
|
||||
}
|
||||
|
||||
int32 BehaviorTreeSequenceNode::GetStateSize() const
|
||||
|
||||
@@ -28,7 +28,7 @@ public:
|
||||
|
||||
protected:
|
||||
// [BehaviorTreeNode]
|
||||
void InvokeReleaseState(const BehaviorUpdateContext& context) override;
|
||||
void BecomeIrrelevant(const BehaviorUpdateContext& context, bool nodeOnly) override;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user