// Copyright (c) Wojciech Figat. All rights reserved. using FlaxEditor.Content; using FlaxEditor.CustomEditors; using FlaxEditor.CustomEditors.Editors; using FlaxEditor.GUI; using FlaxEditor.SceneGraph; using FlaxEditor.Scripting; using FlaxEditor.Surface; using FlaxEditor.Viewport; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Utilities; using System; using System.Collections.Generic; using System.Linq; using System.Xml; namespace FlaxEditor.Windows.Assets { [CustomEditor(typeof(BehaviorKnowledge)), DefaultEditor] sealed class BehaviorKnowledgeEditor : CustomEditor { private HashSet _goals; private LayoutElementsContainer _layout; public override void Initialize(LayoutElementsContainer layout) { var knowledge = (BehaviorKnowledge)Values[0]; _layout = layout; // Blackboard var blackboard = knowledge.Blackboard; var blackboardValue = new CustomValueContainer(TypeUtils.GetObjectType(blackboard), blackboard, (instance, _) => ((BehaviorKnowledge)instance).Blackboard, (instance, _, value) => ((BehaviorKnowledge)instance).Blackboard = value); var blackboardEditor = CustomEditorsUtil.CreateEditor(blackboardValue.Type, false); layout.Object(blackboardValue, blackboardEditor); // Goals _goals = new(); var goals = knowledge.Goals; foreach (var goal in goals) { if (goal == null) continue; var goalType = goal.GetType(); var goalValue = new CustomValueContainer(new ScriptType(goalType), goal, (instance, _) => ((BehaviorKnowledge)instance).GetGoal(goalType), (instance, _, value) => ((BehaviorKnowledge)instance).AddGoal(value)); var goalEditor = CustomEditorsUtil.CreateEditor(goalValue.Type, false); var goalPanel = _layout.Group("Goal " + goalType.Name); goalPanel.Object(goalValue, goalEditor); _goals.Add(goalType); } } /// public override void Refresh() { var knowledge = (BehaviorKnowledge)Values[0]; if (!knowledge) return; // Rebuild layout when goals list gets changed var goals = knowledge.Goals; foreach (var goal in goals) { if (goal == null) continue; var goalType = goal.GetType(); if (!_goals.Contains(goalType)) { RebuildLayout(); return; } } foreach (var goalType in _goals.ToArray()) { if (!knowledge.HasGoal(goalType)) { RebuildLayout(); return; } } base.Refresh(); } } /// /// Behavior Tree window allows to view and edit asset. /// /// /// public sealed class BehaviorTreeWindow : AssetEditorWindowBase, IVisjectSurfaceWindow, IPresenterOwner { private readonly SplitPanel _split1; private readonly SplitPanel _split2; private CustomEditorPresenter _nodePropertiesEditor; private CustomEditorPresenter _knowledgePropertiesEditor; private BehaviorTreeSurface _surface; private Undo _undo; private readonly ToolStripButton _saveButton; private readonly ToolStripButton _undoButton; private readonly ToolStripButton _redoButton; private FlaxObjectRefPickerControl _behaviorPicker; private Guid _cachedBehaviorId; private bool _showWholeGraphOnLoad = true; private bool _isWaitingForSurfaceLoad; private bool _canEdit = true, _canDebug = false; /// /// Gets the Visject Surface. /// public BehaviorTreeSurface Surface => _surface; /// /// Gets the undo history context for this window. /// public Undo Undo => _undo; /// /// Current instance of the Behavior Knowledge's blackboard type or null. /// 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) { var inputOptions = Editor.Options.Options.Input; // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; _undo.RedoDone += OnUndoRedo; _undo.ActionDone += OnUndoAction; // Split Panels _split1 = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.None) { AnchorPreset = AnchorPresets.StretchAll, Offsets = new Margin(0, 0, _toolstrip.Bottom, 0), SplitterValue = 0.7f, Parent = this }; _split2 = new SplitPanel(Orientation.Vertical, ScrollBars.Vertical, ScrollBars.Vertical) { AnchorPreset = AnchorPresets.StretchAll, Offsets = Margin.Zero, SplitterValue = 0.5f, Parent = _split1.Panel2 }; // Surface _surface = new BehaviorTreeSurface(this, Save, _undo) { Parent = _split1.Panel1, Enabled = false }; _surface.SelectionChanged += OnNodeSelectionChanged; // Properties editors _nodePropertiesEditor = new CustomEditorPresenter(null, null, this); // Surface handles undo for nodes editing _nodePropertiesEditor.Features = FeatureFlags.UseDefault; _nodePropertiesEditor.Panel.Parent = _split2.Panel1; _nodePropertiesEditor.Modified += OnNodePropertyEdited; _knowledgePropertiesEditor = new CustomEditorPresenter(null, "No blackboard type assigned", this); // No undo for knowledge editing _knowledgePropertiesEditor.Features = FeatureFlags.None; _knowledgePropertiesEditor.Panel.Parent = _split2.Panel2; // Toolstrip SurfaceUtils.PerformCommonSetup(this, _toolstrip, _surface, out _saveButton, out _undoButton, out _redoButton); _toolstrip.AddSeparator(); _toolstrip.AddButton(editor.Icons.Docs64, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/scripting/ai/behavior-trees/index.html")).LinkTooltip("See documentation to learn more"); // Debug behavior picker var behaviorPickerContainer = new ContainerControl(); var behaviorPickerLabel = new Label { AnchorPreset = AnchorPresets.VerticalStretchLeft, VerticalAlignment = TextAlignment.Center, HorizontalAlignment = TextAlignment.Far, Parent = behaviorPickerContainer, Size = new Float2(60.0f, _toolstrip.Height), Text = "Behavior:", TooltipText = "The behavior instance to preview. Pick the behavior to debug it's logic and data.", }; _behaviorPicker = new FlaxObjectRefPickerControl { Location = new Float2(behaviorPickerLabel.Right + 4.0f, 8.0f), Width = 140.0f, Type = new ScriptType(typeof(Behavior)), Parent = behaviorPickerContainer, }; behaviorPickerContainer.Width = _behaviorPicker.Right + 2.0f; behaviorPickerContainer.Size = new Float2(_behaviorPicker.Right + 2.0f, _toolstrip.Height); behaviorPickerContainer.Parent = _toolstrip; _behaviorPicker.CheckValid = OnBehaviorPickerCheckValid; _behaviorPicker.ValueChanged += OnBehaviorPickerValueChanged; SetCanEdit(!Editor.IsPlayMode); ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin; } private void OnUndoRedo(IUndoAction action) { MarkAsEdited(); UpdateToolstrip(); _nodePropertiesEditor.BuildLayoutOnUpdate(); } private void OnUndoAction(IUndoAction action) { MarkAsEdited(); UpdateToolstrip(); } private void OnNodeSelectionChanged() { // Select node instances to view/edit var selection = new List(); var nodes = _surface.Nodes; if (nodes != null) { for (var i = 0; i < nodes.Count; i++) { if (nodes[i] is Surface.Archetypes.BehaviorTree.NodeBase node && node.IsSelected && node.Instance) selection.Add(node.Instance); } } _nodePropertiesEditor.Select(selection); } private void OnNodePropertyEdited() { _surface.MarkAsEdited(); var nodes = _surface.Nodes; for (var i = 0; i < _nodePropertiesEditor.Selection.Count; i++) { if (_nodePropertiesEditor.Selection[i] is BehaviorTreeNode instance) { // Sync instance data with surface node value storage for (var j = 0; j < nodes.Count; j++) { if (nodes[j] is Surface.Archetypes.BehaviorTree.NodeBase node && node.Instance == instance) { node._isValueEditing = true; node.SetValue(1, FlaxEngine.Json.JsonSerializer.SaveToBytes(instance)); node._isValueEditing = false; break; } } } } } private bool OnBehaviorPickerCheckValid(FlaxEngine.Object obj, ScriptType type) { return obj is Behavior behavior && behavior.Tree == Asset; } private void OnBehaviorPickerValueChanged() { _cachedBehaviorId = _behaviorPicker.ValueID; UpdateKnowledge(); } /// protected override void OnScriptsReloadBegin() { base.OnScriptsReloadBegin(); // TODO: impl hot-reload for BT to nicely refresh state (save asset, clear undo/properties, reload surface) Close(); } private void UpdateKnowledge() { // Pick knowledge from the behavior var behavior = (Behavior)_behaviorPicker.Value; if (_canDebug && behavior) { _knowledgePropertiesEditor.ReadOnly = false; _knowledgePropertiesEditor.Select(behavior.Knowledge); return; } // Use blackboard from the root node var rootNode = _surface.FindNode(19, 2) as Surface.Archetypes.BehaviorTree.Node; if (rootNode != null) { rootNode.ValuesChanged -= UpdateKnowledge; rootNode.ValuesChanged += UpdateKnowledge; } var rootInstance = rootNode?.Instance as BehaviorTreeRootNode; var blackboardType = TypeUtils.GetType(rootInstance?.BlackboardType); if (blackboardType) { var blackboardInstance = blackboardType.CreateInstance(); Utilities.Utils.InitDefaultValues(blackboardInstance); _knowledgePropertiesEditor.ReadOnly = true; _knowledgePropertiesEditor.Select(blackboardInstance); } else { _knowledgePropertiesEditor.Deselect(); } } /// public override void Save() { if (!IsEdited || _asset == null || _isWaitingForSurfaceLoad) return; // Check if surface has been edited if (_surface.IsEdited) { if (SaveSurface()) return; } ClearEditedFlag(); OnSurfaceEditedChanged(); _item.RefreshThumbnail(); } /// protected override void UpdateToolstrip() { _saveButton.Enabled = _canEdit && IsEdited; _undoButton.Enabled = _canEdit && _undo.CanUndo; _redoButton.Enabled = _canEdit && _undo.CanRedo; base.UpdateToolstrip(); } /// protected override void OnAssetLinked() { _isWaitingForSurfaceLoad = true; base.OnAssetLinked(); } /// /// Focuses the node. /// /// The node. public void ShowNode(SurfaceNode node) { SelectTab(); RootWindow.Focus(); Surface.Focus(); Surface.FocusNode(node); } /// public Asset SurfaceAsset => Asset; /// public string SurfaceName => "Behavior Tree"; /// public byte[] SurfaceData { get => _asset.LoadSurface(); set { // Save data to the asset if (_asset.SaveSurface(value)) { _surface.MarkAsEdited(); Editor.LogError("Failed to save surface data"); } _asset.Reload(); } } /// public VisjectSurfaceContext ParentContext => null; /// public void OnContextCreated(VisjectSurfaceContext context) { } /// public void OnSurfaceEditedChanged() { if (_surface.IsEdited) MarkAsEdited(); } /// public void OnSurfaceGraphEdited() { } /// public void OnSurfaceClose() { Close(); } /// protected override void UnlinkItem() { _isWaitingForSurfaceLoad = false; base.UnlinkItem(); } private bool LoadSurface() { if (_surface.Load()) { Editor.LogError("Failed to load Behavior Tree surface."); return true; } return false; } private bool SaveSurface() { return _surface.Save(); } private void SetCanEdit(bool canEdit) { if (_canEdit == canEdit) return; _canEdit = canEdit; _undo.Enabled = canEdit; _surface.CanEdit = canEdit; _nodePropertiesEditor.ReadOnly = !canEdit; UpdateToolstrip(); } private void SetCanDebug(bool canDebug) { if (_canDebug == canDebug) return; _canDebug = canDebug; UpdateKnowledge(); } private void UpdateDebugInfos(bool playMode) { var behavior = playMode ? (Behavior)_behaviorPicker.Value : null; if (!behavior) behavior = null; foreach (var e in _surface.Nodes) { if (e is Surface.Archetypes.BehaviorTree.NodeBase node) node.UpdateDebug(behavior); } } /// public override void OnPlayBegin() { base.OnPlayBegin(); SetCanEdit(false); } /// public override void OnPlayEnd() { SetCanEdit(true); UpdateDebugInfos(false); base.OnPlayEnd(); } /// public override void Update(float deltaTime) { // Wait for asset loaded if (_isWaitingForSurfaceLoad && _asset.IsLoaded) { _isWaitingForSurfaceLoad = false; if (LoadSurface()) { Close(); return; } // Setup _undo.Clear(); _surface.Enabled = true; _nodePropertiesEditor.BuildLayout(); _knowledgePropertiesEditor.BuildLayout(); ClearEditedFlag(); if (_showWholeGraphOnLoad) { _showWholeGraphOnLoad = false; _surface.ShowWholeGraph(); } SurfaceLoaded?.Invoke(); UpdateKnowledge(); } // Check if don't have valid behavior picked if (!_behaviorPicker.Value) { // Try to reassign the debug behavior var id = _cachedBehaviorId; if (id != Guid.Empty) { var obj = FlaxEngine.Object.TryFind(ref id); if (obj && obj.Tree == Asset) _behaviorPicker.Value = obj; } else { // Deselect (eg. if native C++ object has been deleted) _behaviorPicker.Value = null; // Remove any links to the behavior instance (eg. it could be deleted) UpdateKnowledge(); } // Preserve cache value _cachedBehaviorId = id; } // Update behavior debugging SetCanDebug(Editor.IsPlayMode && _behaviorPicker.Value); // Update debug info texts on all nodes if (Editor.IsPlayMode) { UpdateDebugInfos(true); } base.Update(deltaTime); } /// public override bool UseLayoutData => true; /// public override void OnLayoutSerialize(XmlWriter writer) { LayoutSerializeSplitter(writer, "Split1", _split1); LayoutSerializeSplitter(writer, "Split2", _split1); writer.WriteAttributeString("SelectedBehavior", (_behaviorPicker.Value?.ID ?? _cachedBehaviorId).ToString()); } /// public override void OnLayoutDeserialize(XmlElement node) { LayoutDeserializeSplitter(node, "Split1", _split1); LayoutDeserializeSplitter(node, "Split2", _split2); if (Guid.TryParse(node.GetAttribute("SelectedBehavior"), out Guid value1)) _cachedBehaviorId = value1; } /// public override void OnLayoutDeserialize() { _split1.SplitterValue = 0.7f; _split2.SplitterValue = 0.5f; } /// public override void OnDestroy() { if (IsDisposing) return; base.OnDestroy(); ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin; _undo.Enabled = false; _nodePropertiesEditor.Deselect(); _knowledgePropertiesEditor.Deselect(); _undo.Clear(); _behaviorPicker = null; } /// public IEnumerable NewParameterTypes => Editor.CodeEditing.VisualScriptPropertyTypes.Get(); /// public event Action SurfaceLoaded; /// public void OnParamRenameUndo() { } /// public void OnParamEditAttributesUndo() { } /// public void OnParamAddUndo() { } /// public void OnParamRemoveUndo() { } /// public object GetParameter(int index) { throw new NotSupportedException(); } /// public void SetParameter(int index, object value) { throw new NotSupportedException(); } /// public Asset VisjectAsset => Asset; /// public VisjectSurface VisjectSurface => _surface; /// public EditorViewport PresenterViewport => null; /// public void Select(List nodes) { } /// public List Selection => new List(); /// public bool LockSelection { get; set; } } }