Merge remote-tracking branch 'origin/master' into 1.8

This commit is contained in:
Wojtek Figat
2024-02-07 09:40:45 +01:00
64 changed files with 1445 additions and 350 deletions

View File

@@ -0,0 +1,96 @@
using FlaxEditor.GUI;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Base class for custom button editors.
/// See <seealso cref="MouseButtonEditor"/>, <seealso cref="KeyboardKeysEditor"/> and <seealso cref="GamepadButtonEditor"/>.
/// </summary>
public class BindableButtonEditor : EnumEditor
{
private bool _isListeningForInput;
private Button _button;
/// <summary>
/// Where or not we are currently listening for any input.
/// </summary>
protected bool IsListeningForInput
{
get => _isListeningForInput;
set
{
_isListeningForInput = value;
if (_isListeningForInput)
SetupButton();
else
ResetButton();
}
}
/// <summary>
/// The window this editor is attached to.
/// Useful to hook into for key pressed, mouse buttons etc.
/// </summary>
protected Window Window { get; private set; }
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
Window = layout.Control.RootWindow.Window;
var panel = layout.CustomContainer<UniformGridPanel>();
panel.CustomControl.SlotsHorizontally = 2;
panel.CustomControl.SlotsVertically = 1;
var button = panel.Button("Listen", "Press to listen for input events");
_button = button.Button;
_button.Clicked += OnButtonClicked;
ResetButton();
var padding = panel.CustomControl.SlotPadding;
panel.CustomControl.Height = ComboBox.DefaultHeight + padding.Height;
base.Initialize(panel);
}
/// <inheritdoc />
protected override void Deinitialize()
{
_button.Clicked -= OnButtonClicked;
base.Deinitialize();
}
private void ResetButton()
{
_button.Text = "Listen";
_button.BorderThickness = 1;
var style = FlaxEngine.GUI.Style.Current;
_button.BorderColor = style.BorderNormal;
_button.BorderColorHighlighted = style.BorderHighlighted;
_button.BorderColorSelected = style.BorderSelected;
}
private void SetupButton()
{
_button.Text = "Listening...";
_button.BorderThickness = 2;
var color = FlaxEngine.GUI.Style.Current.ProgressNormal;
_button.BorderColor = color;
_button.BorderColorHighlighted = color;
_button.BorderColorSelected = color;
}
private void OnButtonClicked()
{
_isListeningForInput = !_isListeningForInput;
if (_isListeningForInput)
SetupButton();
else
ResetButton();
}
}
}

View File

@@ -3,9 +3,9 @@
using System;
using System.Collections;
using System.Linq;
using FlaxEditor.Content;
using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.CustomEditors.GUI;
using FlaxEditor.GUI.Input;
using FlaxEditor.Content;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.GUI.Drag;
using FlaxEditor.SceneGraph;
@@ -54,8 +54,13 @@ namespace FlaxEditor.CustomEditors.Editors
private void OnSetupContextMenu(PropertyNameLabel label, ContextMenu menu, CustomEditor linkedEditor)
{
menu.AddSeparator();
menu.ItemsContainer.RemoveChildren();
menu.AddButton("Copy", linkedEditor.Copy);
var paste = menu.AddButton("Paste", linkedEditor.Paste);
paste.Enabled = linkedEditor.CanPaste;
menu.AddSeparator();
var moveUpButton = menu.AddButton("Move up", OnMoveUpClicked);
moveUpButton.Enabled = Index > 0;
@@ -65,17 +70,100 @@ namespace FlaxEditor.CustomEditors.Editors
menu.AddButton("Remove", OnRemoveClicked);
}
private void OnMoveUpClicked(ContextMenuButton button)
private void OnMoveUpClicked()
{
Editor.Move(Index, Index - 1);
}
private void OnMoveDownClicked(ContextMenuButton button)
private void OnMoveDownClicked()
{
Editor.Move(Index, Index + 1);
}
private void OnRemoveClicked(ContextMenuButton button)
private void OnRemoveClicked()
{
Editor.Remove(Index);
}
}
private class CollectionDropPanel : DropPanel
{
/// <summary>
/// The collection editor.
/// </summary>
public CollectionEditor Editor;
/// <summary>
/// The index of the item (zero-based).
/// </summary>
public int Index { get; private set; }
/// <summary>
/// The linked editor.
/// </summary>
public CustomEditor LinkedEditor;
private bool _canReorder = true;
public void Setup(CollectionEditor editor, int index, bool canReorder = true)
{
HeaderHeight = 18;
_canReorder = canReorder;
EnableDropDownIcon = true;
var icons = FlaxEditor.Editor.Instance.Icons;
ArrowImageClosed = new SpriteBrush(icons.ArrowRight12);
ArrowImageOpened = new SpriteBrush(icons.ArrowDown12);
HeaderText = $"Element {index}";
IsClosed = false;
Editor = editor;
Index = index;
Offsets = new Margin(7, 7, 0, 0);
MouseButtonRightClicked += OnMouseButtonRightClicked;
if (_canReorder)
{
// TODO: Drag drop
}
}
private void OnMouseButtonRightClicked(DropPanel panel, Float2 location)
{
if (LinkedEditor == null)
return;
var linkedEditor = LinkedEditor;
var menu = new ContextMenu();
menu.AddButton("Copy", linkedEditor.Copy);
var paste = menu.AddButton("Paste", linkedEditor.Paste);
paste.Enabled = linkedEditor.CanPaste;
if (_canReorder)
{
menu.AddSeparator();
var moveUpButton = menu.AddButton("Move up", OnMoveUpClicked);
moveUpButton.Enabled = Index > 0;
var moveDownButton = menu.AddButton("Move down", OnMoveDownClicked);
moveDownButton.Enabled = Index + 1 < Editor.Count;
}
menu.AddButton("Remove", OnRemoveClicked);
menu.Show(panel, location);
}
private void OnMoveUpClicked()
{
Editor.Move(Index, Index - 1);
}
private void OnMoveDownClicked()
{
Editor.Move(Index, Index + 1);
}
private void OnRemoveClicked()
{
Editor.Remove(Index);
}
@@ -86,12 +174,12 @@ namespace FlaxEditor.CustomEditors.Editors
/// </summary>
protected bool NotNullItems;
private IntegerValueElement _size;
private PropertyNameLabel _sizeLabel;
private IntValueBox _sizeBox;
private Color _background;
private int _elementsCount;
private bool _readOnly;
private bool _canReorderItems;
private CollectionAttribute.DisplayType _displayType;
/// <summary>
/// Gets the length of the collection.
@@ -124,12 +212,13 @@ namespace FlaxEditor.CustomEditors.Editors
_readOnly = false;
_canReorderItems = true;
_background = FlaxEngine.GUI.Style.Current.CollectionBackgroundColor;
_displayType = CollectionAttribute.DisplayType.Header;
NotNullItems = false;
// Try get CollectionAttribute for collection editor meta
var attributes = Values.GetAttributes();
Type overrideEditorType = null;
float spacing = 10.0f;
float spacing = 1.0f;
var collection = (CollectionAttribute)attributes?.FirstOrDefault(x => x is CollectionAttribute);
if (collection != null)
{
@@ -140,6 +229,7 @@ namespace FlaxEditor.CustomEditors.Editors
_background = collection.BackgroundColor.Value;
overrideEditorType = TypeUtils.GetType(collection.OverrideEditorTypeName).Type;
spacing = collection.Spacing;
_displayType = collection.Display;
}
var dragArea = layout.CustomContainer<DragAreaControl>();
@@ -172,76 +262,77 @@ namespace FlaxEditor.CustomEditors.Editors
}
// Size
if (_readOnly || (NotNullItems && size == 0))
if (layout.ContainerControl is DropPanel dropPanel)
{
dragArea.Label("Size", size.ToString());
}
else
{
var sizeProperty = dragArea.AddPropertyItem("Size");
_sizeLabel = sizeProperty.Labels.Last();
_size = sizeProperty.IntegerValue();
_size.IntValue.MinValue = 0;
_size.IntValue.MaxValue = ushort.MaxValue;
_size.IntValue.Value = size;
_size.IntValue.EditEnd += OnSizeChanged;
var height = dropPanel.HeaderHeight - dropPanel.HeaderTextMargin.Height;
var y = -dropPanel.HeaderHeight + dropPanel.HeaderTextMargin.Top;
_sizeBox = new IntValueBox(size)
{
MinValue = 0,
MaxValue = ushort.MaxValue,
AnchorPreset = AnchorPresets.TopRight,
Bounds = new Rectangle(-40 - dropPanel.ItemsMargin.Right, y, 40, height),
Parent = dropPanel,
};
var label = new Label
{
Text = "Size",
AnchorPreset = AnchorPresets.TopRight,
Bounds = new Rectangle(-_sizeBox.Width - 40 - dropPanel.ItemsMargin.Right - 2, y, 40, height),
Parent = dropPanel
};
if (_readOnly || (NotNullItems && size == 0))
{
_sizeBox.IsReadOnly = true;
_sizeBox.Enabled = false;
}
else
{
_sizeBox.EditEnd += OnSizeChanged;
}
}
// Elements
if (size > 0)
{
var panel = dragArea.VerticalPanel();
panel.Panel.Offsets = new Margin(7, 7, 0, 0);
panel.Panel.BackgroundColor = _background;
var elementType = ElementType;
bool single = elementType.IsPrimitive ||
elementType.Equals(new ScriptType(typeof(string))) ||
elementType.IsEnum ||
(elementType.GetFields().Length == 1 && elementType.GetProperties().Length == 0) ||
(elementType.GetProperties().Length == 1 && elementType.GetFields().Length == 0) ||
elementType.Equals(new ScriptType(typeof(JsonAsset))) ||
elementType.Equals(new ScriptType(typeof(SettingsBase)));
// Use separate layout cells for each collection items to improve layout updates for them in separation
var useSharedLayout = elementType.IsPrimitive || elementType.IsEnum;
if (_canReorderItems)
for (int i = 0; i < size; i++)
{
for (int i = 0; i < size; i++)
{
if (i != 0 && spacing > 0f)
{
if (panel.Children.Count > 0 && panel.Children[panel.Children.Count - 1] is PropertiesListElement propertiesListElement)
{
if (propertiesListElement.Labels.Count > 0)
{
var label = propertiesListElement.Labels[propertiesListElement.Labels.Count - 1];
var margin = label.Margin;
margin.Bottom += spacing;
label.Margin = margin;
}
propertiesListElement.Space(spacing);
}
else
{
panel.Space(spacing);
}
}
// Apply spacing
if (i > 0 && i < size && spacing > 0 && !single)
panel.Space(spacing);
var overrideEditor = overrideEditorType != null ? (CustomEditor)Activator.CreateInstance(overrideEditorType) : null;
var property = panel.AddPropertyItem(new CollectionItemLabel(this, i));
var itemLayout = useSharedLayout ? (LayoutElementsContainer)property : property.VerticalPanel();
itemLayout.Object(new ListValueContainer(elementType, i, Values, attributes), overrideEditor);
var overrideEditor = overrideEditorType != null ? (CustomEditor)Activator.CreateInstance(overrideEditorType) : null;
if (_displayType == CollectionAttribute.DisplayType.Inline || (collection == null && single) || (_displayType == CollectionAttribute.DisplayType.Default && single))
{
PropertyNameLabel itemLabel;
if (_canReorderItems)
itemLabel = new CollectionItemLabel(this, i);
else
itemLabel = new PropertyNameLabel("Element " + i);
var property = panel.AddPropertyItem(itemLabel);
var itemLayout = (LayoutElementsContainer)property;
itemLabel.LinkedEditor = itemLayout.Object(new ListValueContainer(elementType, i, Values, attributes), overrideEditor);
}
}
else
{
for (int i = 0; i < size; i++)
else if (_displayType == CollectionAttribute.DisplayType.Header || (_displayType == CollectionAttribute.DisplayType.Default && !single))
{
if (i != 0 && spacing > 0f)
{
if (panel.Children.Count > 0 && panel.Children[panel.Children.Count - 1] is PropertiesListElement propertiesListElement)
propertiesListElement.Space(spacing);
else
panel.Space(spacing);
}
var overrideEditor = overrideEditorType != null ? (CustomEditor)Activator.CreateInstance(overrideEditorType) : null;
var property = panel.AddPropertyItem("Element " + i);
var itemLayout = useSharedLayout ? (LayoutElementsContainer)property : property.VerticalPanel();
itemLayout.Object(new ListValueContainer(elementType, i, Values, attributes), overrideEditor);
var cdp = panel.CustomContainer<CollectionDropPanel>();
cdp.CustomControl.Setup(this, i, _canReorderItems);
var itemLayout = cdp.VerticalPanel();
cdp.CustomControl.LinkedEditor = itemLayout.Object(new ListValueContainer(elementType, i, Values, attributes), overrideEditor);
}
}
}
@@ -283,8 +374,7 @@ namespace FlaxEditor.CustomEditors.Editors
/// <inheritdoc />
protected override void Deinitialize()
{
_size = null;
_sizeLabel = null;
_sizeBox = null;
base.Deinitialize();
}
@@ -311,7 +401,8 @@ namespace FlaxEditor.CustomEditors.Editors
{
if (IsSetBlocked)
return;
Resize(_size.IntValue.Value);
Resize(_sizeBox.Value);
}
/// <summary>
@@ -384,14 +475,14 @@ namespace FlaxEditor.CustomEditors.Editors
return;
// Update reference/default value indicator
if (_sizeLabel != null)
if (_sizeBox != null)
{
var color = Color.Transparent;
if (Values.HasReferenceValue && Values.ReferenceValue is IList referenceValue && referenceValue.Count != Count)
color = FlaxEngine.GUI.Style.Current.BackgroundSelected;
else if (Values.HasDefaultValue && Values.DefaultValue is IList defaultValue && defaultValue.Count != Count)
color = Color.Yellow * 0.8f;
_sizeLabel.HighlightStripColor = color;
_sizeBox.BorderColor = color;
}
// Check if collection has been resized (by UI or from external source)

View File

@@ -7,6 +7,7 @@ using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.CustomEditors.GUI;
using FlaxEditor.GUI;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.GUI.Input;
using FlaxEditor.Scripting;
using FlaxEngine;
using FlaxEngine.GUI;
@@ -149,13 +150,14 @@ namespace FlaxEditor.CustomEditors.Editors
}
}
private IntegerValueElement _size;
private IntValueBox _sizeBox;
private Color _background;
private int _elementsCount;
private bool _readOnly;
private bool _notNullItems;
private bool _canEditKeys;
private bool _keyEdited;
private CollectionAttribute.DisplayType _displayType;
/// <summary>
/// Gets the length of the collection.
@@ -178,6 +180,7 @@ namespace FlaxEditor.CustomEditors.Editors
_background = FlaxEngine.GUI.Style.Current.CollectionBackgroundColor;
_readOnly = false;
_notNullItems = false;
_displayType = CollectionAttribute.DisplayType.Header;
// Try get CollectionAttribute for collection editor meta
var attributes = Values.GetAttributes();
@@ -192,20 +195,40 @@ namespace FlaxEditor.CustomEditors.Editors
_background = collection.BackgroundColor.Value;
overrideEditorType = TypeUtils.GetType(collection.OverrideEditorTypeName).Type;
spacing = collection.Spacing;
_displayType = collection.Display;
}
// Size
if (_readOnly || !_canEditKeys)
if (layout.ContainerControl is DropPanel dropPanel)
{
layout.Label("Size", size.ToString());
}
else
{
_size = layout.IntegerValue("Size");
_size.IntValue.MinValue = 0;
_size.IntValue.MaxValue = _notNullItems ? size : ushort.MaxValue;
_size.IntValue.Value = size;
_size.IntValue.EditEnd += OnSizeChanged;
var height = dropPanel.HeaderHeight - dropPanel.HeaderTextMargin.Height;
var y = -dropPanel.HeaderHeight + dropPanel.HeaderTextMargin.Top;
_sizeBox = new IntValueBox(size)
{
MinValue = 0,
MaxValue = _notNullItems ? size : ushort.MaxValue,
AnchorPreset = AnchorPresets.TopRight,
Bounds = new Rectangle(-40 - dropPanel.ItemsMargin.Right, y, 40, height),
Parent = dropPanel,
};
var label = new Label
{
Text = "Size",
AnchorPreset = AnchorPresets.TopRight,
Bounds = new Rectangle(-_sizeBox.Width - 40 - dropPanel.ItemsMargin.Right - 2, y, 40, height),
Parent = dropPanel
};
if (_readOnly || !_canEditKeys)
{
_sizeBox.IsReadOnly = true;
_sizeBox.Enabled = false;
}
else
{
_sizeBox.EditEnd += OnSizeChanged;
}
}
// Elements
@@ -216,29 +239,23 @@ namespace FlaxEditor.CustomEditors.Editors
var keysEnumerable = ((IDictionary)Values[0]).Keys.OfType<object>();
var keys = keysEnumerable as object[] ?? keysEnumerable.ToArray();
var valuesType = new ScriptType(valueType);
bool single = valuesType.IsPrimitive ||
valuesType.Equals(new ScriptType(typeof(string))) ||
valuesType.IsEnum ||
(valuesType.GetFields().Length == 1 && valuesType.GetProperties().Length == 0) ||
(valuesType.GetProperties().Length == 1 && valuesType.GetFields().Length == 0) ||
valuesType.Equals(new ScriptType(typeof(JsonAsset))) ||
valuesType.Equals(new ScriptType(typeof(SettingsBase)));
// Use separate layout cells for each collection items to improve layout updates for them in separation
var useSharedLayout = valueType.IsPrimitive || valueType.IsEnum;
for (int i = 0; i < size; i++)
{
if (i != 0 && spacing > 0f)
if (i > 0 && i < size && spacing > 0)
{
if (panel.Children.Count > 0 && panel.Children[panel.Children.Count - 1] is PropertiesListElement propertiesListElement)
{
if (propertiesListElement.Labels.Count > 0)
{
var label = propertiesListElement.Labels[propertiesListElement.Labels.Count - 1];
var margin = label.Margin;
margin.Bottom += spacing;
label.Margin = margin;
}
propertiesListElement.Space(spacing);
}
else
{
panel.Space(spacing);
}
panel.Space(spacing);
}
var key = keys.ElementAt(i);
@@ -310,7 +327,7 @@ namespace FlaxEditor.CustomEditors.Editors
if (IsSetBlocked)
return;
Resize(_size.IntValue.Value);
Resize(_sizeBox.Value);
}
/// <summary>

View File

@@ -0,0 +1,51 @@
using System;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Custom editor for <see cref="GamepadButton"/>.
/// Allows capturing gamepad buttons and assigning them
/// to the edited value.
/// </summary>
[CustomEditor(typeof(GamepadButton))]
public class GamepadButtonEditor : BindableButtonEditor
{
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
base.Initialize(layout);
FlaxEngine.Scripting.Update += OnUpdate;
}
/// <inheritdoc />
protected override void Deinitialize()
{
FlaxEngine.Scripting.Update -= OnUpdate;
base.Deinitialize();
}
private void OnUpdate()
{
if (!IsListeningForInput)
return;
// Since there is no way to get an event about
// which gamepad pressed what button, we have
// to poll all gamepads and buttons manually.
for (var i = 0; i < Input.GamepadsCount; i++)
{
var pad = Input.Gamepads[i];
foreach (var btn in Enum.GetValues<GamepadButton>())
{
if (pad.GetButtonUp(btn))
{
IsListeningForInput = false;
SetValue(btn);
return;
}
}
}
}
}
}

View File

@@ -0,0 +1,36 @@
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Custom editor for <see cref="KeyboardKeys"/>.
/// Allows capturing key presses and assigning them
/// to the edited value.
/// </summary>
[CustomEditor(typeof(KeyboardKeys))]
public class KeyboardKeysEditor : BindableButtonEditor
{
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
base.Initialize(layout);
Window.KeyUp += OnKeyUp;
}
/// <inheritdoc />
protected override void Deinitialize()
{
Window.KeyUp -= OnKeyUp;
base.Deinitialize();
}
private void OnKeyUp(KeyboardKeys key)
{
if (!IsListeningForInput)
return;
IsListeningForInput = false;
SetValue(key);
}
}
}

View File

@@ -4,6 +4,7 @@ using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.CustomEditors.GUI;
using FlaxEditor.Scripting;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Editors
{
@@ -13,7 +14,7 @@ namespace FlaxEditor.CustomEditors.Editors
[CustomEditor(typeof(ModelInstanceEntry)), DefaultEditor]
public sealed class ModelInstanceEntryEditor : GenericEditor
{
private GroupElement _group;
private DropPanel _mainPanel;
private bool _updateName;
private int _entryIndex;
private bool _isRefreshing;
@@ -25,8 +26,11 @@ namespace FlaxEditor.CustomEditors.Editors
public override void Initialize(LayoutElementsContainer layout)
{
_updateName = true;
var group = layout.Group("Entry");
_group = group;
if (layout.ContainerControl.Parent is DropPanel panel)
{
_mainPanel = panel;
_mainPanel.HeaderText = "Entry";
}
if (ParentEditor == null || HasDifferentTypes)
return;
@@ -60,19 +64,19 @@ namespace FlaxEditor.CustomEditors.Editors
var materialValue = new CustomValueContainer(new ScriptType(typeof(MaterialBase)), _material, (instance, index) => _material, (instance, index, value) => _material = value as MaterialBase);
for (var i = 1; i < parentEditorValues.Count; i++)
materialValue.Add(_material);
var materialEditor = (AssetRefEditor)_group.Property(materialLabel, materialValue);
var materialEditor = (AssetRefEditor)layout.Property(materialLabel, materialValue);
materialEditor.Values.SetDefaultValue(defaultValue);
materialEditor.RefreshDefaultValue();
materialEditor.Picker.SelectedItemChanged += OnSelectedMaterialChanged;
_materialEditor = materialEditor;
}
base.Initialize(group);
base.Initialize(layout);
}
private void OnSelectedMaterialChanged()
{
if (_isRefreshing)
if (_isRefreshing || _modelInstance == null)
return;
_isRefreshing = true;
var slots = _modelInstance.MaterialSlots;
@@ -120,7 +124,7 @@ namespace FlaxEditor.CustomEditors.Editors
{
// Update panel title to match material slot name
if (_updateName &&
_group != null &&
_mainPanel != null &&
ParentEditor?.ParentEditor != null &&
ParentEditor.ParentEditor.Values.Count > 0)
{
@@ -131,7 +135,7 @@ namespace FlaxEditor.CustomEditors.Editors
if (slots != null && slots.Length > entryIndex)
{
_updateName = false;
_group.Panel.HeaderText = "Entry " + slots[entryIndex].Name;
_mainPanel.HeaderText = "Entry " + slots[entryIndex].Name;
}
}
}

View File

@@ -0,0 +1,37 @@
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Custom editor for <see cref="MouseButton"/>.
/// Allows capturing mouse button presses and assigning them
/// to the edited value.
/// </summary>
[CustomEditor(typeof(MouseButton))]
public class MouseButtonEditor : BindableButtonEditor
{
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
base.Initialize(layout);
Window.MouseUp += OnMouseUp;
}
/// <inheritdoc />
protected override void Deinitialize()
{
Window.MouseUp -= OnMouseUp;
base.Deinitialize();
}
private void OnMouseUp(ref Float2 mouse, MouseButton button, ref bool handled)
{
if (!IsListeningForInput)
return;
IsListeningForInput = false;
SetValue(button);
handled = true;
}
}
}

View File

@@ -75,6 +75,9 @@ namespace FlaxEditor.GUI
/// </summary>
protected virtual void OnSelectedItemChanged()
{
if (IsDisposing)
return;
// Update tooltip
string tooltip;
if (Validator.SelectedItem is AssetItem assetItem)

View File

@@ -428,6 +428,13 @@ namespace FlaxEditor.GUI
// Show dropdown list
_popupMenu.MinimumWidth = Width;
_popupMenu.Show(this, new Float2(1, Height));
// Adjust menu position if it is not the down direction
if (_popupMenu.Direction == ContextMenuDirection.RightUp)
{
var position = _popupMenu.RootWindow.Window.Position;
_popupMenu.RootWindow.Window.Position = new Float2(position.X, position.Y - Height);
}
}
}

View File

@@ -408,9 +408,9 @@ namespace FlaxEditor.GUI.ContextMenu
{
foreach (var child in _panel.Children)
{
if (child is ContextMenuChildMenu item && item.Visible)
if (child is ContextMenuButton item && item.Visible)
{
item.AdjustArrowAmount = -_panel.VScrollBar.Width;
item.ExtraAdjustmentAmount = -_panel.VScrollBar.Width;
}
}
}

View File

@@ -14,6 +14,11 @@ namespace FlaxEditor.GUI.ContextMenu
public class ContextMenuButton : ContextMenuItem
{
private bool _isMouseDown;
/// <summary>
/// The amount to adjust the short keys and arrow image by in x coordinates.
/// </summary>
public float ExtraAdjustmentAmount = 0;
/// <summary>
/// Event fired when user clicks on the button.
@@ -133,7 +138,7 @@ namespace FlaxEditor.GUI.ContextMenu
if (!string.IsNullOrEmpty(ShortKeys))
{
// Draw short keys
Render2D.DrawText(style.FontMedium, ShortKeys, textRect, textColor, TextAlignment.Far, TextAlignment.Center);
Render2D.DrawText(style.FontMedium, ShortKeys, new Rectangle(textRect.X + ExtraAdjustmentAmount, textRect.Y, textRect.Width, textRect.Height), textColor, TextAlignment.Far, TextAlignment.Center);
}
// Draw icon

View File

@@ -17,11 +17,6 @@ namespace FlaxEditor.GUI.ContextMenu
/// </summary>
public readonly ContextMenu ContextMenu = new ContextMenu();
/// <summary>
/// The amount to adjust the arrow image by in x coordinates.
/// </summary>
public float AdjustArrowAmount = 0;
/// <summary>
/// Initializes a new instance of the <see cref="ContextMenuChildMenu"/> class.
/// </summary>
@@ -49,7 +44,7 @@ namespace FlaxEditor.GUI.ContextMenu
// Draw arrow
if (ContextMenu.HasChildren)
Render2D.DrawSprite(style.ArrowRight, new Rectangle(Width - 15 + AdjustArrowAmount, (Height - 12) / 2, 12, 12), Enabled ? isCMopened ? style.BackgroundSelected : style.Foreground : style.ForegroundDisabled);
Render2D.DrawSprite(style.ArrowRight, new Rectangle(Width - 15 + ExtraAdjustmentAmount, (Height - 12) / 2, 12, 12), Enabled ? isCMopened ? style.BackgroundSelected : style.Foreground : style.ForegroundDisabled);
}
/// <inheritdoc />

View File

@@ -19,7 +19,7 @@ namespace FlaxEditor.GUI.Tree
/// <summary>
/// The key updates timeout in seconds.
/// </summary>
public static float KeyUpdateTimeout = 0.12f;
public static float KeyUpdateTimeout = 0.25f;
/// <summary>
/// Delegate for selected tree nodes collection change.
@@ -113,7 +113,7 @@ namespace FlaxEditor.GUI.Tree
AutoFocus = false;
_supportMultiSelect = supportMultiSelect;
_keyUpdateTime = KeyUpdateTimeout * 10;
_keyUpdateTime = KeyUpdateTimeout;
}
internal void OnRightClickInternal(TreeNode node, ref Float2 location)
@@ -347,10 +347,12 @@ namespace FlaxEditor.GUI.Tree
if (ContainsFocus && node != null && node.AutoFocus)
{
var window = Root;
if (window.GetKeyDown(KeyboardKeys.ArrowUp) || window.GetKeyDown(KeyboardKeys.ArrowDown))
_keyUpdateTime = KeyUpdateTimeout;
if (_keyUpdateTime >= KeyUpdateTimeout && window is WindowRootControl windowRoot && windowRoot.Window.IsFocused)
{
bool keyUpArrow = window.GetKeyDown(KeyboardKeys.ArrowUp);
bool keyDownArrow = window.GetKeyDown(KeyboardKeys.ArrowDown);
bool keyUpArrow = window.GetKey(KeyboardKeys.ArrowUp);
bool keyDownArrow = window.GetKey(KeyboardKeys.ArrowDown);
// Check if arrow flags are different
if (keyDownArrow != keyUpArrow)

View File

@@ -45,6 +45,21 @@ namespace FlaxEditor.Modules.SourceCodeEditing
_checkAssembly = checkAssembly;
}
/// <summary>
/// Gets the type matching the certain Script.
/// </summary>
/// <param name="script">The content item.</param>
/// <returns>The type matching that item, or null if not found.</returns>
public ScriptType Get(Content.ScriptItem script)
{
foreach (var type in Get())
{
if (type.ContentItem == script)
return type;
}
return ScriptType.Null;
}
/// <summary>
/// Gets all the types from the all loaded assemblies (including project scripts and scripts from the plugins).
/// </summary>

View File

@@ -29,6 +29,7 @@ namespace FlaxEditor.SceneGraph.GUI
private DragScripts _dragScripts;
private DragAssets _dragAssets;
private DragActorType _dragActorType;
private DragScriptItems _dragScriptItems;
private DragHandlers _dragHandlers;
private List<Rectangle> _highlights;
private bool _hasSearchFilter;
@@ -395,6 +396,13 @@ namespace FlaxEditor.SceneGraph.GUI
}
if (_dragActorType.OnDragEnter(data))
return _dragActorType.Effect;
if (_dragScriptItems == null)
{
_dragScriptItems = new DragScriptItems(ValidateDragScriptItem);
_dragHandlers.Add(_dragScriptItems);
}
if (_dragScriptItems.OnDragEnter(data))
return _dragScriptItems.Effect;
return DragDropEffect.None;
}
@@ -673,7 +681,34 @@ namespace FlaxEditor.SceneGraph.GUI
actor.Transform = Actor.Transform;
ActorNode.Root.Spawn(actor, Actor);
}
result = DragDropEffect.Move;
}
// Drag script item
else if (_dragScriptItems != null && _dragScriptItems.HasValidDrag)
{
var spawnParent = myActor;
if (DragOverMode == DragItemPositioning.Above || DragOverMode == DragItemPositioning.Below)
spawnParent = newParent;
for (int i = 0; i < _dragScriptItems.Objects.Count; i++)
{
var item = _dragScriptItems.Objects[i];
var actorType = Editor.Instance.CodeEditing.Actors.Get(item);
if (actorType != ScriptType.Null)
{
var actor = actorType.CreateInstance() as Actor;
if (actor == null)
{
Editor.LogWarning("Failed to spawn actor of type " + actorType.TypeName);
continue;
}
actor.StaticFlags = spawnParent.StaticFlags;
actor.Name = actorType.Name;
actor.Transform = spawnParent.Transform;
ActorNode.Root.Spawn(actor, spawnParent);
actor.OrderInParent = newOrder;
}
}
result = DragDropEffect.Move;
}
@@ -728,6 +763,11 @@ namespace FlaxEditor.SceneGraph.GUI
return true;
}
private static bool ValidateDragScriptItem(ScriptItem script)
{
return Editor.Instance.CodeEditing.Actors.Get(script) != ScriptType.Null;
}
/// <inheritdoc />
protected override void DoDragDrop()
{
@@ -768,6 +808,7 @@ namespace FlaxEditor.SceneGraph.GUI
_dragScripts = null;
_dragAssets = null;
_dragActorType = null;
_dragScriptItems = null;
_dragHandlers?.Clear();
_dragHandlers = null;
_highlights = null;

View File

@@ -113,6 +113,25 @@ namespace FlaxEditor.Surface
ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin;
}
internal AnimGraphTraceEvent[] LastTraceEvents;
internal bool TryGetTraceEvent(SurfaceNode node, out AnimGraphTraceEvent traceEvent)
{
if (LastTraceEvents != null)
{
foreach (var e in LastTraceEvents)
{
if (e.NodeId == node.ID)
{
traceEvent = e;
return true;
}
}
}
traceEvent = default;
return false;
}
private static SurfaceStyle CreateStyle()
{
var editor = Editor.Instance;
@@ -383,6 +402,7 @@ namespace FlaxEditor.Surface
}
ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin;
_nodesCache.Wait();
LastTraceEvents = null;
base.OnDestroy();
}

View File

@@ -1583,14 +1583,24 @@ namespace FlaxEditor.Surface.Archetypes
None = 0,
/// <summary>
/// Transition rule will be rechecked during active transition with option to interrupt transition.
/// Transition rule will be rechecked during active transition with option to interrupt transition (to go back to the source state).
/// </summary>
RuleRechecking = 1,
/// <summary>
/// Interrupted transition is immediately stopped without blending out.
/// Interrupted transition is immediately stopped without blending out (back to the source/destination state).
/// </summary>
Instant = 2,
/// <summary>
/// Enables checking other transitions in the source state that might interrupt this one.
/// </summary>
SourceState = 4,
/// <summary>
/// Enables checking transitions in the destination state that might interrupt this one.
/// </summary>
DestinationState = 8,
}
/// <summary>
@@ -1613,6 +1623,8 @@ namespace FlaxEditor.Surface.Archetypes
UseDefaultRule = 4,
InterruptionRuleRechecking = 8,
InterruptionInstant = 16,
InterruptionSourceState = 32,
InterruptionDestinationState = 64,
}
/// <summary>
@@ -1773,7 +1785,7 @@ namespace FlaxEditor.Surface.Archetypes
}
/// <summary>
/// Transition interruption options.
/// Transition interruption options (flags, can select multiple values).
/// </summary>
[EditorOrder(70), DefaultValue(InterruptionFlags.None)]
public InterruptionFlags Interruption
@@ -1785,12 +1797,18 @@ namespace FlaxEditor.Surface.Archetypes
flags |= InterruptionFlags.RuleRechecking;
if (_data.HasFlag(Data.FlagTypes.InterruptionInstant))
flags |= InterruptionFlags.Instant;
if (_data.HasFlag(Data.FlagTypes.InterruptionSourceState))
flags |= InterruptionFlags.SourceState;
if (_data.HasFlag(Data.FlagTypes.InterruptionDestinationState))
flags |= InterruptionFlags.DestinationState;
return flags;
}
set
{
_data.SetFlag(Data.FlagTypes.InterruptionRuleRechecking, value.HasFlag(InterruptionFlags.RuleRechecking));
_data.SetFlag(Data.FlagTypes.InterruptionInstant, value.HasFlag(InterruptionFlags.Instant));
_data.SetFlag(Data.FlagTypes.InterruptionSourceState, value.HasFlag(InterruptionFlags.SourceState));
_data.SetFlag(Data.FlagTypes.InterruptionDestinationState, value.HasFlag(InterruptionFlags.DestinationState));
SourceState.SaveTransitions(true);
}
}

View File

@@ -28,6 +28,51 @@ namespace FlaxEditor.Surface.Archetypes
}
}
/// <summary>
/// Customized <see cref="SurfaceNode"/> for Blend with Mask node.
/// </summary>
public class SkeletonMaskSample : SurfaceNode
{
private AssetSelect _assetSelect;
private Box _assetBox;
/// <inheritdoc />
public SkeletonMaskSample(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
: base(id, context, nodeArch, groupArch)
{
}
/// <inheritdoc />
public override void OnSurfaceLoaded(SurfaceNodeActions action)
{
base.OnSurfaceLoaded(action);
if (Surface != null)
{
_assetSelect = GetChild<AssetSelect>();
// 4 is the id of skeleton mask parameter node.
if (TryGetBox(4, out var box))
{
_assetBox = box;
_assetSelect.Visible = !_assetBox.HasAnyConnection;
}
}
}
/// <inheritdoc />
public override void ConnectionTick(Box box)
{
base.ConnectionTick(box);
if (_assetBox == null)
return;
if (box.ID != _assetBox.ID)
return;
_assetSelect.Visible = !box.HasAnyConnection;
}
}
/// <summary>
/// Customized <see cref="SurfaceNode"/> for the animation sampling nodes
/// </summary>
@@ -36,6 +81,7 @@ namespace FlaxEditor.Surface.Archetypes
{
private AssetSelect _assetSelect;
private Box _assetBox;
private ProgressBar _playbackPos;
/// <inheritdoc />
public Sample(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
@@ -75,7 +121,7 @@ namespace FlaxEditor.Surface.Archetypes
Title = _assetBox.HasAnyConnection || asset == null ? "Animation" : asset.ShortName;
else
Title = asset?.ShortName ?? "Animation";
var style = Style.Current;
Resize(Mathf.Max(230, style.FontLarge.MeasureText(Title).X + 30), 160);
}
@@ -93,6 +139,36 @@ namespace FlaxEditor.Surface.Archetypes
_assetSelect.Visible = !box.HasAnyConnection;
UpdateTitle();
}
/// <inheritdoc />
public override void Update(float deltaTime)
{
// Debug current playback position
if (((AnimGraphSurface)Surface).TryGetTraceEvent(this, out var traceEvent) && traceEvent.Asset is FlaxEngine.Animation anim)
{
if (_playbackPos == null)
{
_playbackPos = new ProgressBar
{
SmoothingScale = 0.0f,
Offsets = Margin.Zero,
AnchorPreset = AnchorPresets.HorizontalStretchBottom,
Parent = this,
Height = 12.0f,
};
_playbackPos.Y -= 16.0f;
}
_playbackPos.Visible = true;
_playbackPos.Maximum = anim.Duration;
_playbackPos.Value = traceEvent.Value; // AnimGraph reports position in animation frames, not time
}
else if (_playbackPos != null)
{
_playbackPos.Visible = false;
}
base.Update(deltaTime);
}
}
/// <summary>
@@ -493,7 +569,7 @@ namespace FlaxEditor.Surface.Archetypes
{
TypeID = 10,
Title = "Blend Additive",
Description =
Description =
"Blend animation poses (with additive mode)" +
"\n" +
"\nNote: " +
@@ -521,6 +597,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 11,
Title = "Blend with Mask",
Description = "Blend animation poses using skeleton mask",
Create = (id, context, arch, groupArch) => new SkeletonMaskSample(id, context, arch, groupArch),
Flags = NodeFlags.AnimGraph,
Size = new Float2(180, 140),
DefaultValues = new object[]
@@ -534,7 +611,8 @@ namespace FlaxEditor.Surface.Archetypes
NodeElementArchetype.Factory.Input(0, "Pose A", true, typeof(void), 1),
NodeElementArchetype.Factory.Input(1, "Pose B", true, typeof(void), 2),
NodeElementArchetype.Factory.Input(2, "Alpha", true, typeof(float), 3, 0),
NodeElementArchetype.Factory.Asset(0, 70, 1, typeof(SkeletonMask)),
NodeElementArchetype.Factory.Input(3, "Skeleton Mask Asset", true, typeof(SkeletonMask), 4),
NodeElementArchetype.Factory.Asset(0, Surface.Constants.LayoutOffsetY * 4, 1, typeof(SkeletonMask)),
}
},
new NodeArchetype

View File

@@ -763,6 +763,17 @@ namespace FlaxEditor.Surface.Archetypes
public string Name;
public ScriptType Type;
public bool IsOut;
public override string ToString()
{
var sb = new StringBuilder();
if (IsOut)
sb.Append("out ");
sb.Append(Type.ToString());
sb.Append(" ");
sb.Append(Name);
return sb.ToString();
}
}
private struct SignatureInfo
@@ -892,7 +903,7 @@ namespace FlaxEditor.Surface.Archetypes
{
ref var param = ref signature.Params[i];
ref var paramMember = ref memberParameters[i];
if (param.Type != paramMember.Type || param.IsOut != paramMember.IsOut)
if (!SurfaceUtils.AreScriptTypesEqual(param.Type, paramMember.Type) || param.IsOut != paramMember.IsOut)
{
// Special case: param.Type is serialized as just a type while paramMember.Type might be a reference for output parameters (eg. `out Int32` vs `out Int32&`)
var paramMemberTypeName = paramMember.Type.TypeName;
@@ -1660,7 +1671,7 @@ namespace FlaxEditor.Surface.Archetypes
SaveSignature();
// Check if return type has been changed
if (_signature.ReturnType != prevReturnType)
if (!SurfaceUtils.AreScriptTypesEqual(_signature.ReturnType, prevReturnType))
{
// Update all return nodes used by this function to match the new type
var usedNodes = DepthFirstTraversal(false);
@@ -2158,7 +2169,7 @@ namespace FlaxEditor.Surface.Archetypes
return false;
for (int i = 0; i < _signature.Length; i++)
{
if (_signature[i].Type != sig.Parameters[i].Type)
if (!SurfaceUtils.AreScriptTypesEqual(_signature[i].Type, sig.Parameters[i].Type))
return false;
}
return true;

View File

@@ -105,7 +105,7 @@ namespace FlaxEditor.Surface
if (node != null)
{
args.SurfaceLocation.X += node.Width + 10;
args.SurfaceLocation.Y += node.Height + 10;
}
}

View File

@@ -532,5 +532,24 @@ namespace FlaxEditor.Surface
value = new Double4(i);
return value;
}
private static bool AreScriptTypesEqualInner(ScriptType left, ScriptType right)
{
// Special case for Vector types that use typedefs and might overlap
if (left.Type == typeof(Vector2) && (right.Type == typeof(Float2) || right.Type == typeof(Double2)))
return true;
if (left.Type == typeof(Vector3) && (right.Type == typeof(Float3) || right.Type == typeof(Double3)))
return true;
if (left.Type == typeof(Vector4) && (right.Type == typeof(Float4) || right.Type == typeof(Double4)))
return true;
return false;
}
internal static bool AreScriptTypesEqual(ScriptType left, ScriptType right)
{
if (left == right)
return true;
return AreScriptTypesEqualInner(left, right) || AreScriptTypesEqualInner(right, left);
}
}
}

View File

@@ -24,6 +24,8 @@ namespace FlaxEditor.Surface.Undo
public ConnectBoxesAction(InputBox iB, OutputBox oB, bool connect)
{
if (iB == null || oB == null || iB.ParentNode == null || oB.ParentNode == null)
throw new System.ArgumentNullException();
_surface = iB.Surface;
_context = new ContextHandle(iB.ParentNode.Context);
_connect = connect;

View File

@@ -18,7 +18,7 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
public float TargetHeight = 0.0f;
/// <inheritdoc />
public override unsafe void Apply(ref ApplyParams p)
public override unsafe void ApplyBrushToPatch(ref ApplyParams p)
{
// If used with invert mode pick the target height level
if (p.Options.Invert)

View File

@@ -26,7 +26,7 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
}
/// <inheritdoc />
public override unsafe void Apply(ref ApplyParams p)
public override unsafe void ApplyBrushToPatch(ref ApplyParams p)
{
var strength = p.Strength * -10.0f;
var brushPosition = p.Gizmo.CursorPosition;

View File

@@ -1,6 +1,7 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using FlaxEditor.Tools.Terrain.Brushes;
using FlaxEngine;
@@ -50,18 +51,20 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
public virtual bool EditHoles => false;
/// <summary>
/// Applies the modification to the terrain.
/// Gets all patches that will be affected by the brush
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="options">The options.</param>
/// <param name="gizmo">The gizmo.</param>
/// <param name="terrain">The terrain.</param>
public unsafe void Apply(Brush brush, ref Options options, SculptTerrainGizmoMode gizmo, FlaxEngine.Terrain terrain)
public virtual unsafe List<ApplyParams> GetAffectedPatches(Brush brush, ref Options options, SculptTerrainGizmoMode gizmo, FlaxEngine.Terrain terrain)
{
List<ApplyParams> affectedPatches = new();
// Combine final apply strength
float strength = Strength * options.Strength * options.DeltaTime;
if (strength <= 0.0f)
return;
return affectedPatches;
if (options.Invert && SupportsNegativeApply)
strength *= -1;
@@ -72,20 +75,10 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
var patchSize = chunkSize * FlaxEngine.Terrain.UnitsPerVertex * FlaxEngine.Terrain.PatchEdgeChunksCount;
var tempBuffer = (float*)gizmo.GetHeightmapTempBuffer(heightmapLength * sizeof(float)).ToPointer();
var unitsPerVertexInv = 1.0f / FlaxEngine.Terrain.UnitsPerVertex;
ApplyParams p = new ApplyParams
{
Terrain = terrain,
Brush = brush,
Gizmo = gizmo,
Options = options,
Strength = strength,
HeightmapSize = heightmapSize,
TempBuffer = tempBuffer,
};
// Get brush bounds in terrain local space
var brushBounds = gizmo.CursorBrushBounds;
terrain.GetLocalToWorldMatrix(out p.TerrainWorld);
terrain.GetLocalToWorldMatrix(out var terrainWorld);
terrain.GetWorldToLocalMatrix(out var terrainInvWorld);
BoundingBox.Transform(ref brushBounds, ref terrainInvWorld, out var brushBoundsLocal);
@@ -131,26 +124,78 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
if (sourceHeights == null && sourceHoles == null)
throw new Exception("Cannot modify terrain. Loading heightmap failed. See log for more info.");
// Record patch data before editing it
if (!gizmo.CurrentEditUndoAction.HashPatch(ref patch.PatchCoord))
ApplyParams p = new ApplyParams
{
gizmo.CurrentEditUndoAction.AddPatch(ref patch.PatchCoord);
}
Terrain = terrain,
TerrainWorld = terrainWorld,
Brush = brush,
Gizmo = gizmo,
Options = options,
Strength = strength,
HeightmapSize = heightmapSize,
TempBuffer = tempBuffer,
ModifiedOffset = modifiedOffset,
ModifiedSize = modifiedSize,
PatchCoord = patch.PatchCoord,
PatchPositionLocal = patchPositionLocal,
SourceHeightMap = sourceHeights,
SourceHolesMask = sourceHoles,
};
// Apply modification
p.ModifiedOffset = modifiedOffset;
p.ModifiedSize = modifiedSize;
p.PatchCoord = patch.PatchCoord;
p.PatchPositionLocal = patchPositionLocal;
p.SourceHeightMap = sourceHeights;
p.SourceHolesMask = sourceHoles;
Apply(ref p);
affectedPatches.Add(p);
}
return affectedPatches;
}
/// <summary>
/// Applies the modification to the terrain.
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="options">The options.</param>
/// <param name="gizmo">The gizmo.</param>
/// <param name="terrain">The terrain.</param>
public void Apply(Brush brush, ref Options options, SculptTerrainGizmoMode gizmo, FlaxEngine.Terrain terrain)
{
var affectedPatches = GetAffectedPatches(brush, ref options, gizmo, terrain);
if (affectedPatches.Count == 0)
{
return;
}
ApplyBrush(gizmo, affectedPatches);
// Auto NavMesh rebuild
var brushBounds = gizmo.CursorBrushBounds;
gizmo.CurrentEditUndoAction.AddDirtyBounds(ref brushBounds);
}
/// <summary>
/// Applies the brush to all affected patches
/// </summary>
/// <param name="gizmo"></param>
/// <param name="affectedPatches"></param>
public virtual void ApplyBrush(SculptTerrainGizmoMode gizmo, List<ApplyParams> affectedPatches)
{
for (int i = 0; i < affectedPatches.Count; i++)
{
ApplyParams patchApplyParams = affectedPatches[i];
// Record patch data before editing it
if (!gizmo.CurrentEditUndoAction.HashPatch(ref patchApplyParams.PatchCoord))
{
gizmo.CurrentEditUndoAction.AddPatch(ref patchApplyParams.PatchCoord);
}
ApplyBrushToPatch(ref patchApplyParams);
// Auto NavMesh rebuild
var brushBounds = gizmo.CursorBrushBounds;
gizmo.CurrentEditUndoAction.AddDirtyBounds(ref brushBounds);
}
}
/// <summary>
/// The mode apply parameters.
/// </summary>
@@ -231,6 +276,6 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
/// Applies the modification to the terrain.
/// </summary>
/// <param name="p">The parameters to use.</param>
public abstract void Apply(ref ApplyParams p);
public abstract void ApplyBrushToPatch(ref ApplyParams p);
}
}

View File

@@ -29,7 +29,7 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
public override bool SupportsNegativeApply => true;
/// <inheritdoc />
public override unsafe void Apply(ref ApplyParams p)
public override unsafe void ApplyBrushToPatch(ref ApplyParams p)
{
// Prepare
var brushPosition = p.Gizmo.CursorPosition;

View File

@@ -15,7 +15,7 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
public override bool SupportsNegativeApply => true;
/// <inheritdoc />
public override unsafe void Apply(ref ApplyParams p)
public override unsafe void ApplyBrushToPatch(ref ApplyParams p)
{
var strength = p.Strength * 1000.0f;
var brushPosition = p.Gizmo.CursorPosition;

View File

@@ -1,7 +1,8 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using FlaxEngine;
using System;
namespace FlaxEditor.Tools.Terrain.Sculpt
{
@@ -19,43 +20,109 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
public float FilterRadius = 0.4f;
/// <inheritdoc />
public override unsafe void Apply(ref ApplyParams p)
public override unsafe void ApplyBrush(SculptTerrainGizmoMode gizmo, List<ApplyParams> affectedPatches)
{
// Prepare
var brushPosition = p.Gizmo.CursorPosition;
var radius = Mathf.Max(Mathf.CeilToInt(FilterRadius * 0.01f * p.Brush.Size), 2);
var max = p.HeightmapSize - 1;
var strength = Mathf.Saturate(p.Strength);
// Apply brush modification
Profiler.BeginEvent("Apply Brush");
for (int z = 0; z < p.ModifiedSize.Y; z++)
// TODO: don't need these on each patch; just need them once
var heightmapSize = affectedPatches[0].HeightmapSize;
var radius = Mathf.Max(Mathf.CeilToInt(FilterRadius * 0.01f * affectedPatches[0].Brush.Size), 2);
// Calculate bounding coordinates of the total affected area
Int2 modifieedAreaMinCoord = Int2.Maximum;
Int2 modifiedAreaMaxCoord = Int2.Minimum;
for (int i = 0; i < affectedPatches.Count; i++)
{
var zz = z + p.ModifiedOffset.Y;
for (int x = 0; x < p.ModifiedSize.X; x++)
var patch = affectedPatches[i];
var tl = (patch.PatchCoord * (heightmapSize - 1)) + patch.ModifiedOffset;
var br = tl + patch.ModifiedSize;
if (tl.X <= modifieedAreaMinCoord.X && tl.Y <= modifieedAreaMinCoord.Y)
modifieedAreaMinCoord = tl;
if (br.X >= modifiedAreaMaxCoord.X && br.Y >= modifiedAreaMaxCoord.Y)
modifiedAreaMaxCoord = br;
}
var totalModifiedSize = modifiedAreaMaxCoord - modifieedAreaMinCoord;
// Build map of heights in affected area
var modifiedHeights = new float[totalModifiedSize.X * totalModifiedSize.Y];
for (int i = 0; i < affectedPatches.Count; i++)
{
var patch = affectedPatches[i];
for (int z = 0; z < patch.ModifiedSize.Y; z++)
{
var xx = x + p.ModifiedOffset.X;
var sourceHeight = p.SourceHeightMap[zz * p.HeightmapSize + xx];
var samplePositionLocal = p.PatchPositionLocal + new Vector3(xx * FlaxEngine.Terrain.UnitsPerVertex, sourceHeight, zz * FlaxEngine.Terrain.UnitsPerVertex);
Vector3.Transform(ref samplePositionLocal, ref p.TerrainWorld, out Vector3 samplePositionWorld);
var paintAmount = p.Brush.Sample(ref brushPosition, ref samplePositionWorld) * strength;
if (paintAmount > 0)
for (int x = 0; x < patch.ModifiedSize.X; x++)
{
// Read height from current patch
var localCoordX = (x + patch.ModifiedOffset.X);
var localCoordY = (z + patch.ModifiedOffset.Y);
var coordHeight = patch.SourceHeightMap[(localCoordY * heightmapSize) + localCoordX];
// Calculate the absolute coordinate of the terrain point
var absoluteCurrentPointCoord = patch.PatchCoord * (heightmapSize - 1) + patch.ModifiedOffset + new Int2(x, z);
var currentPointCoordRelativeToModifiedArea = absoluteCurrentPointCoord - modifieedAreaMinCoord;
// Store height
var index = (currentPointCoordRelativeToModifiedArea.Y * totalModifiedSize.X) + currentPointCoordRelativeToModifiedArea.X;
modifiedHeights[index] = coordHeight;
}
}
}
// Iterate through modified points and smooth now that we have height information for all necessary points
for (int i = 0; i < affectedPatches.Count; i++)
{
var patch = affectedPatches[i];
var brushPosition = patch.Gizmo.CursorPosition;
var strength = Mathf.Saturate(patch.Strength);
for (int z = 0; z < patch.ModifiedSize.Y; z++)
{
for (int x = 0; x < patch.ModifiedSize.X; x++)
{
// Read height from current patch
var localCoordX = (x + patch.ModifiedOffset.X);
var localCoordY = (z + patch.ModifiedOffset.Y);
var coordHeight = patch.SourceHeightMap[(localCoordY * heightmapSize) + localCoordX];
// Calculate the absolute coordinate of the terrain point
var absoluteCurrentPointCoord = patch.PatchCoord * (heightmapSize - 1) + patch.ModifiedOffset + new Int2(x, z);
var currentPointCoordRelativeToModifiedArea = absoluteCurrentPointCoord - modifieedAreaMinCoord;
// Calculate brush influence at the current position
var samplePositionLocal = patch.PatchPositionLocal + new Vector3(localCoordX * FlaxEngine.Terrain.UnitsPerVertex, coordHeight, localCoordY * FlaxEngine.Terrain.UnitsPerVertex);
Vector3.Transform(ref samplePositionLocal, ref patch.TerrainWorld, out Vector3 samplePositionWorld);
var paintAmount = patch.Brush.Sample(ref brushPosition, ref samplePositionWorld) * strength;
if (paintAmount == 0)
{
patch.TempBuffer[z * patch.ModifiedSize.X + x] = coordHeight;
continue;
}
// Record patch data before editing it
if (!gizmo.CurrentEditUndoAction.HashPatch(ref patch.PatchCoord))
{
gizmo.CurrentEditUndoAction.AddPatch(ref patch.PatchCoord);
}
// Sum the nearby values
float smoothValue = 0;
int smoothValueSamples = 0;
int minX = Math.Max(x - radius + p.ModifiedOffset.X, 0);
int minZ = Math.Max(z - radius + p.ModifiedOffset.Y, 0);
int maxX = Math.Min(x + radius + p.ModifiedOffset.X, max);
int maxZ = Math.Min(z + radius + p.ModifiedOffset.Y, max);
var minX = Math.Max(0, currentPointCoordRelativeToModifiedArea.X - radius);
var maxX = Math.Min(totalModifiedSize.X - 1, currentPointCoordRelativeToModifiedArea.X + radius);
var minZ = Math.Max(0, currentPointCoordRelativeToModifiedArea.Y - radius);
var maxZ = Math.Min(totalModifiedSize.Y - 1, currentPointCoordRelativeToModifiedArea.Y + radius);
for (int dz = minZ; dz <= maxZ; dz++)
{
for (int dx = minX; dx <= maxX; dx++)
{
var height = p.SourceHeightMap[dz * p.HeightmapSize + dx];
var coordIndex = (dz * totalModifiedSize.X) + dx;
var height = modifiedHeights[coordIndex];
smoothValue += height;
smoothValueSamples++;
}
@@ -65,18 +132,26 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
smoothValue /= smoothValueSamples;
// Blend between the height and smooth value
p.TempBuffer[z * p.ModifiedSize.X + x] = Mathf.Lerp(sourceHeight, smoothValue, paintAmount);
}
else
{
p.TempBuffer[z * p.ModifiedSize.X + x] = sourceHeight;
var newHeight = Mathf.Lerp(coordHeight, smoothValue, paintAmount);
patch.TempBuffer[z * patch.ModifiedSize.X + x] = newHeight;
}
}
}
Profiler.EndEvent();
// Update terrain patch
TerrainTools.ModifyHeightMap(p.Terrain, ref p.PatchCoord, p.TempBuffer, ref p.ModifiedOffset, ref p.ModifiedSize);
// Update terrain patch
TerrainTools.ModifyHeightMap(patch.Terrain, ref patch.PatchCoord, patch.TempBuffer, ref patch.ModifiedOffset, ref patch.ModifiedSize);
}
// Auto NavMesh rebuild
var brushBounds = gizmo.CursorBrushBounds;
gizmo.CurrentEditUndoAction.AddDirtyBounds(ref brushBounds);
Profiler.EndEvent();
}
/// <inheritdoc />
public override unsafe void ApplyBrushToPatch(ref ApplyParams p)
{
// noop; unused
}
}
}

View File

@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FlaxEditor.Content;
using FlaxEditor.Gizmo;
using FlaxEditor.GUI.ContextMenu;
@@ -194,7 +195,7 @@ namespace FlaxEditor.Viewport
: base(Object.New<SceneRenderTask>(), editor.Undo, editor.Scene.Root)
{
_editor = editor;
DragHandlers = new ViewportDragHandlers(this, this, ValidateDragItem, ValidateDragActorType);
DragHandlers = new ViewportDragHandlers(this, this, ValidateDragItem, ValidateDragActorType, ValidateDragScriptItem);
var inputOptions = editor.Options.Options.Input;
// Prepare rendering task
@@ -940,6 +941,11 @@ namespace FlaxEditor.Viewport
return Level.IsAnySceneLoaded;
}
private static bool ValidateDragScriptItem(ScriptItem script)
{
return Editor.Instance.CodeEditing.Actors.Get(script) != ScriptType.Null;
}
/// <inheritdoc />
public override DragDropEffect OnDragMove(ref Float2 location, DragData data)
{

View File

@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FlaxEditor.Content;
using FlaxEditor.Gizmo;
using FlaxEditor.GUI.ContextMenu;
@@ -81,7 +82,7 @@ namespace FlaxEditor.Viewport
_window.SelectionChanged += OnSelectionChanged;
Undo = window.Undo;
ViewportCamera = new FPSCamera();
DragHandlers = new ViewportDragHandlers(this, this, ValidateDragItem, ValidateDragActorType);
DragHandlers = new ViewportDragHandlers(this, this, ValidateDragItem, ValidateDragActorType, ValidateDragScriptItem);
ShowDebugDraw = true;
ShowEditorPrimitives = true;
Gizmos = new GizmosCollection(this);
@@ -702,6 +703,11 @@ namespace FlaxEditor.Viewport
return true;
}
private static bool ValidateDragScriptItem(ScriptItem script)
{
return Editor.Instance.CodeEditing.Actors.Get(script) != ScriptType.Null;
}
/// <inheritdoc />
public override DragDropEffect OnDragMove(ref Float2 location, DragData data)
{

View File

@@ -39,17 +39,19 @@ namespace FlaxEditor.Viewport
private readonly EditorViewport _viewport;
private readonly DragAssets<DragDropEventArgs> _dragAssets;
private readonly DragActorType<DragDropEventArgs> _dragActorType;
private readonly DragScriptItems<DragDropEventArgs> _dragScriptItem;
private StaticModel _previewStaticModel;
private int _previewModelEntryIndex;
private BrushSurface _previewBrushSurface;
internal ViewportDragHandlers(IGizmoOwner owner, EditorViewport viewport, Func<AssetItem, bool> validateAsset, Func<ScriptType, bool> validateDragActorType)
internal ViewportDragHandlers(IGizmoOwner owner, EditorViewport viewport, Func<AssetItem, bool> validateAsset, Func<ScriptType, bool> validateDragActorType, Func<ScriptItem, bool> validateDragScriptItem)
{
_owner = owner;
_viewport = viewport;
Add(_dragAssets = new DragAssets<DragDropEventArgs>(validateAsset));
Add(_dragActorType = new DragActorType<DragDropEventArgs>(validateDragActorType));
Add(_dragScriptItem = new DragScriptItems<DragDropEventArgs>(validateDragScriptItem));
}
internal void ClearDragEffects()
@@ -102,7 +104,12 @@ namespace FlaxEditor.Viewport
foreach (var actorType in _dragActorType.Objects)
Spawn(actorType, hit, ref location, ref hitLocation, ref hitNormal);
}
else if (_dragScriptItem.HasValidDrag)
{
result = _dragScriptItem.Effect;
foreach (var scripItem in _dragScriptItem.Objects)
Spawn(scripItem, hit, ref location, ref hitLocation, ref hitNormal);
}
OnDragDrop(new DragDropEventArgs { Hit = hit, HitLocation = hitLocation });
return result;
@@ -193,6 +200,15 @@ namespace FlaxEditor.Viewport
_viewport.Focus();
}
private void Spawn(ScriptItem item, SceneGraphNode hit, ref Float2 location, ref Vector3 hitLocation, ref Vector3 hitNormal)
{
var actorType = Editor.Instance.CodeEditing.Actors.Get(item);
if (actorType != ScriptType.Null)
{
Spawn(actorType, hit, ref location, ref hitLocation, ref hitNormal);
}
}
private void Spawn(ScriptType item, SceneGraphNode hit, ref Float2 location, ref Vector3 hitLocation, ref Vector3 hitNormal)
{
var actor = item.CreateInstance() as Actor;

View File

@@ -396,6 +396,16 @@ namespace FlaxEditor.Windows.Assets
/// <inheritdoc />
public override void OnUpdate()
{
// Extract animations playback state from the events tracing
var debugActor = _debugPicker.Value as AnimatedModel;
if (debugActor == null)
debugActor = _preview.PreviewActor;
if (debugActor != null)
{
debugActor.EnableTracing = true;
Surface.LastTraceEvents = debugActor.TraceEvents;
}
base.OnUpdate();
// Update graph execution flow debugging visualization
@@ -416,6 +426,8 @@ namespace FlaxEditor.Windows.Assets
/// <inheritdoc />
public override void OnDestroy()
{
if (IsDisposing)
return;
Animations.DebugFlow -= OnDebugFlow;
_properties = null;

View File

@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FlaxEditor.Content;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.GUI.Drag;
@@ -64,6 +65,7 @@ namespace FlaxEditor.Windows.Assets
private PrefabWindow _window;
private DragAssets _dragAssets;
private DragActorType _dragActorType;
private DragScriptItems _dragScriptItems;
private DragHandlers _dragHandlers;
public SceneTreePanel(PrefabWindow window)
@@ -84,6 +86,11 @@ namespace FlaxEditor.Windows.Assets
return true;
}
private static bool ValidateDragScriptItem(ScriptItem script)
{
return Editor.Instance.CodeEditing.Actors.Get(script);
}
/// <inheritdoc />
public override DragDropEffect OnDragEnter(ref Float2 location, DragData data)
{
@@ -106,6 +113,13 @@ namespace FlaxEditor.Windows.Assets
}
if (_dragActorType.OnDragEnter(data))
return _dragActorType.Effect;
if (_dragScriptItems == null)
{
_dragScriptItems = new DragScriptItems(ValidateDragScriptItem);
_dragHandlers.Add(_dragScriptItems);
}
if (_dragScriptItems.OnDragEnter(data))
return _dragScriptItems.Effect;
}
return result;
}
@@ -162,7 +176,27 @@ namespace FlaxEditor.Windows.Assets
}
result = DragDropEffect.Move;
}
// Drag script item
else if (_dragScriptItems != null && _dragScriptItems.HasValidDrag)
{
for (int i = 0; i < _dragScriptItems.Objects.Count; i++)
{
var item = _dragScriptItems.Objects[i];
var actorType = Editor.Instance.CodeEditing.Actors.Get(item);
if (actorType != ScriptType.Null)
{
var actor = actorType.CreateInstance() as Actor;
if (actor == null)
{
Editor.LogWarning("Failed to spawn actor of type " + actorType.TypeName);
continue;
}
actor.Name = actorType.Name;
_window.Spawn(actor);
}
}
result = DragDropEffect.Move;
}
_dragHandlers.OnDragDrop(null);
}
return result;
@@ -173,6 +207,7 @@ namespace FlaxEditor.Windows.Assets
_window = null;
_dragAssets = null;
_dragActorType = null;
_dragScriptItems = null;
_dragHandlers?.Clear();
_dragHandlers = null;

View File

@@ -83,14 +83,14 @@ namespace FlaxEditor.Windows.Assets
set => Sprite.Name = value;
}
[EditorOrder(1), Limit(-4096, 4096)]
[EditorOrder(1)]
public Float2 Location
{
get => Sprite.Location;
set => Sprite.Location = value;
}
[EditorOrder(3), Limit(0, 4096)]
[EditorOrder(3), Limit(0)]
public Float2 Size
{
get => Sprite.Size;

View File

@@ -799,7 +799,10 @@ namespace FlaxEditor.Windows
if (proxy == null)
throw new ArgumentNullException(nameof(proxy));
// Setup name
string name = initialName ?? proxy.NewItemName;
if (!proxy.IsFileNameValid(name) || Utilities.Utils.HasInvalidPathChar(name))
name = proxy.NewItemName;
// If the proxy can not be created in the current folder, then navigate to the content folder
if (!proxy.CanCreate(CurrentViewFolder))

View File

@@ -95,6 +95,7 @@ namespace FlaxEditor.Windows
Bounds = new Rectangle(nameLabel.X, tmp1, nameLabel.Width, Height - tmp1 - margin),
};
var xOffset = nameLabel.Width;
string versionString = string.Empty;
if (desc.IsAlpha)
versionString = "ALPHA ";
@@ -109,7 +110,7 @@ namespace FlaxEditor.Windows
AnchorPreset = AnchorPresets.TopRight,
Text = versionString,
Parent = this,
Bounds = new Rectangle(Width - 140 - margin, margin, 140, 14),
Bounds = new Rectangle(Width - 140 - margin - xOffset, margin, 140, 14),
};
string url = null;
@@ -129,7 +130,7 @@ namespace FlaxEditor.Windows
AnchorPreset = AnchorPresets.TopRight,
Text = desc.Author,
Parent = this,
Bounds = new Rectangle(Width - authorWidth - margin, versionLabel.Bottom + margin, authorWidth, 14),
Bounds = new Rectangle(Width - authorWidth - margin - xOffset, versionLabel.Bottom + margin, authorWidth, 14),
};
if (url != null)
{
@@ -671,11 +672,11 @@ namespace FlaxEditor.Windows
Editor.Log($"Using plugin code type name: {pluginCodeName}");
var oldPluginPath = Path.Combine(extractPath, "ExamplePlugin-master");
var newPluginPath = Path.Combine(extractPath, pluginName);
var newPluginPath = Path.Combine(extractPath, pluginCodeName);
Directory.Move(oldPluginPath, newPluginPath);
var oldFlaxProjFile = Path.Combine(newPluginPath, "ExamplePlugin.flaxproj");
var newFlaxProjFile = Path.Combine(newPluginPath, $"{pluginName}.flaxproj");
var newFlaxProjFile = Path.Combine(newPluginPath, $"{pluginCodeName}.flaxproj");
File.Move(oldFlaxProjFile, newFlaxProjFile);
var readme = Path.Combine(newPluginPath, "README.md");
@@ -687,7 +688,7 @@ namespace FlaxEditor.Windows
// Flax plugin project file
var flaxPluginProjContents = JsonSerializer.Deserialize<ProjectInfo>(await File.ReadAllTextAsync(newFlaxProjFile));
flaxPluginProjContents.Name = pluginName;
flaxPluginProjContents.Name = pluginCodeName;
if (!string.IsNullOrEmpty(pluginVersion))
flaxPluginProjContents.Version = new Version(pluginVersion);
if (!string.IsNullOrEmpty(companyName))
@@ -751,7 +752,7 @@ namespace FlaxEditor.Windows
}
Editor.Log($"Plugin project {pluginName} has successfully been created.");
await AddReferenceToProject(pluginName, pluginName);
await AddReferenceToProject(pluginCodeName, pluginCodeName);
MessageBox.Show($"{pluginName} has been successfully created. Restart editor for changes to take effect.", "Plugin Project Created", MessageBoxButtons.OK);
}
@@ -775,8 +776,12 @@ namespace FlaxEditor.Windows
var pluginModuleScriptPath = Path.Combine(subDir, pluginModuleName + ".Build.cs");
if (File.Exists(pluginModuleScriptPath))
{
gameScriptContents = gameScriptContents.Insert(insertLocation, $"\n options.PublicDependencies.Add(\"{pluginModuleName}\");");
modifiedAny = true;
var text = await File.ReadAllTextAsync(pluginModuleScriptPath);
if (!text.Contains("GameEditorModule", StringComparison.OrdinalIgnoreCase))
{
gameScriptContents = gameScriptContents.Insert(insertLocation, $"\n options.PublicDependencies.Add(\"{pluginModuleName}\");");
modifiedAny = true;
}
}
}

View File

@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FlaxEditor.Gizmo;
using FlaxEditor.Content;
using FlaxEditor.GUI.Tree;
@@ -31,6 +32,7 @@ namespace FlaxEditor.Windows
private DragAssets _dragAssets;
private DragActorType _dragActorType;
private DragScriptItems _dragScriptItems;
private DragHandlers _dragHandlers;
/// <summary>
@@ -273,6 +275,11 @@ namespace FlaxEditor.Windows
return true;
}
private static bool ValidateDragScriptItem(ScriptItem script)
{
return Editor.Instance.CodeEditing.Actors.Get(script) != ScriptType.Null;
}
/// <inheritdoc />
public override void Draw()
{
@@ -380,6 +387,13 @@ namespace FlaxEditor.Windows
}
if (_dragActorType.OnDragEnter(data) && result == DragDropEffect.None)
return _dragActorType.Effect;
if (_dragScriptItems == null)
{
_dragScriptItems = new DragScriptItems(ValidateDragScriptItem);
_dragHandlers.Add(_dragScriptItems);
}
if (_dragScriptItems.OnDragEnter(data) && result == DragDropEffect.None)
return _dragScriptItems.Effect;
}
return result;
}
@@ -445,6 +459,28 @@ namespace FlaxEditor.Windows
}
result = DragDropEffect.Move;
}
// Drag script item
else if (_dragScriptItems != null && _dragScriptItems.HasValidDrag)
{
for (int i = 0; i < _dragScriptItems.Objects.Count; i++)
{
var item = _dragScriptItems.Objects[i];
var actorType = Editor.Instance.CodeEditing.Actors.Get(item);
if (actorType != ScriptType.Null)
{
var actor = actorType.CreateInstance() as Actor;
if (actor == null)
{
Editor.LogWarning("Failed to spawn actor of type " + actorType.TypeName);
continue;
}
actor.Name = actorType.Name;
Level.SpawnActor(actor);
Editor.Scene.MarkSceneEdited(actor.Scene);
}
}
result = DragDropEffect.Move;
}
_dragHandlers.OnDragDrop(null);
}
@@ -456,6 +492,7 @@ namespace FlaxEditor.Windows
{
_dragAssets = null;
_dragActorType = null;
_dragScriptItems = null;
_dragHandlers?.Clear();
_dragHandlers = null;
_tree = null;

View File

@@ -99,10 +99,7 @@ void BlendPoseBucketInit(AnimGraphInstanceData::Bucket& bucket)
void StateMachineBucketInit(AnimGraphInstanceData::Bucket& bucket)
{
bucket.StateMachine.LastUpdateFrame = 0;
bucket.StateMachine.CurrentState = nullptr;
bucket.StateMachine.ActiveTransition = nullptr;
bucket.StateMachine.TransitionPosition = 0.0f;
Platform::MemoryClear(&bucket.StateMachine, sizeof(bucket.StateMachine));
}
void SlotBucketInit(AnimGraphInstanceData::Bucket& bucket)

View File

@@ -39,6 +39,7 @@ void AnimGraphImpulse::SetNodeModelTransformation(SkeletonData& skeleton, int32
void AnimGraphInstanceData::Clear()
{
ClearState();
Slots.Clear();
Parameters.Resize(0);
}
@@ -55,7 +56,7 @@ void AnimGraphInstanceData::ClearState()
RootMotion = Transform::Identity;
State.Resize(0);
NodesPose.Resize(0);
Slots.Clear();
TraceEvents.Clear();
}
void AnimGraphInstanceData::Invalidate()
@@ -238,6 +239,7 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt)
}
for (auto& e : data.ActiveEvents)
e.Hit = false;
data.TraceEvents.Clear();
// Init empty nodes data
context.EmptyNodes.RootMotion = Transform::Identity;

View File

@@ -129,6 +129,8 @@ public:
UseDefaultRule = 4,
InterruptionRuleRechecking = 8,
InterruptionInstant = 16,
InterruptionSourceState = 32,
InterruptionDestinationState = 64,
};
public:
@@ -200,6 +202,21 @@ struct FLAXENGINE_API AnimGraphSlot
bool Reset = false;
};
/// <summary>
/// The animation graph state container for a single node playback trace (eg. animation sample info or state transition). Can be used by Anim Graph debugging or custom scripting.
/// </summary>
API_STRUCT() struct FLAXENGINE_API AnimGraphTraceEvent
{
DECLARE_SCRIPTING_TYPE_MINIMAL(AnimGraphTraceEvent);
// Contextual asset used. For example, sampled animation.
API_FIELD() Asset* Asset = nullptr;
// Generic value contextual to playback type (eg. animation sample position).
API_FIELD() float Value = 0;
// Identifier of the node in the graph.
API_FIELD() uint32 NodeId = 0;
};
/// <summary>
/// The animation graph instance data storage. Required to update the animation graph.
/// </summary>
@@ -241,7 +258,10 @@ public:
uint64 LastUpdateFrame;
AnimGraphNode* CurrentState;
AnimGraphStateTransition* ActiveTransition;
AnimGraphStateTransition* BaseTransition;
AnimGraphNode* BaseTransitionState;
float TransitionPosition;
float BaseTransitionPosition;
};
struct SlotBucket
@@ -358,6 +378,12 @@ public:
/// </summary>
void InvokeAnimEvents();
public:
// Anim Graph logic tracing feature that allows to collect insights of animations sampling and skeleton poses operations.
bool EnableTracing = false;
// Trace events collected when using EnableTracing option.
Array<AnimGraphTraceEvent> TraceEvents;
private:
struct OutgoingEvent
{
@@ -837,7 +863,7 @@ public:
}
/// <summary>
/// Resets all the state bucket used by the given graph including sub-graphs (total). Can eb used to reset the animation state of the nested graph (including children).
/// Resets all the state bucket used by the given graph including sub-graphs (total). Can be used to reset the animation state of the nested graph (including children).
/// </summary>
void ResetBuckets(AnimGraphContext& context, AnimGraphBase* graph);
@@ -866,5 +892,7 @@ private:
Variant SampleAnimationsWithBlend(AnimGraphNode* node, bool loop, float length, float startTimePos, float prevTimePos, float& newTimePos, Animation* animA, Animation* animB, Animation* animC, float speedA, float speedB, float speedC, float alphaA, float alphaB, float alphaC);
Variant Blend(AnimGraphNode* node, const Value& poseA, const Value& poseB, float alpha, AlphaBlendMode alphaMode);
Variant SampleState(AnimGraphNode* state);
void InitStateTransition(AnimGraphContext& context, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, AnimGraphStateTransition* transition = nullptr);
AnimGraphStateTransition* UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphNode* state, AnimGraphNode* ignoreState = nullptr);
void UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, const AnimGraphNode::StateBaseData& stateData);
};

View File

@@ -219,6 +219,16 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
const float animPos = GetAnimSamplePos(length, anim, pos, speed);
const float animPrevPos = GetAnimSamplePos(length, anim, prevPos, speed);
// Add to trace
auto& context = Context.Get();
if (context.Data->EnableTracing)
{
auto& trace = context.Data->TraceEvents.AddOne();
trace.Asset = anim;
trace.Value = animPos;
trace.NodeId = node->ID;
}
// Evaluate nested animations
bool hasNested = false;
if (anim->NestedAnims.Count() != 0)
@@ -460,10 +470,12 @@ Variant AnimGraphExecutor::Blend(AnimGraphNode* node, const Value& poseA, const
{
ANIM_GRAPH_PROFILE_EVENT("Blend Pose");
if (isnan(alpha) || isinf(alpha))
alpha = 0;
alpha = Math::Saturate(alpha);
alpha = AlphaBlend::Process(alpha, alphaMode);
const auto nodes = node->GetNodes(this);
auto nodesA = static_cast<AnimGraphImpulse*>(poseA.AsPointer);
auto nodesB = static_cast<AnimGraphImpulse*>(poseB.AsPointer);
if (!ANIM_GRAPH_IS_VALID_PTR(poseA))
@@ -484,32 +496,40 @@ Variant AnimGraphExecutor::Blend(AnimGraphNode* node, const Value& poseA, const
Variant AnimGraphExecutor::SampleState(AnimGraphNode* state)
{
// Prepare
auto& data = state->Data.State;
if (data.Graph == nullptr || data.Graph->GetRootNode() == nullptr)
{
// Invalid state graph
return Value::Null;
}
ANIM_GRAPH_PROFILE_EVENT("Evaluate State");
// Evaluate state
auto rootNode = data.Graph->GetRootNode();
auto result = eatBox((Node*)rootNode, &rootNode->Boxes[0]);
return result;
return eatBox((Node*)rootNode, &rootNode->Boxes[0]);
}
void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, const AnimGraphNode::StateBaseData& stateData)
void AnimGraphExecutor::InitStateTransition(AnimGraphContext& context, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, AnimGraphStateTransition* transition)
{
// Reset transiton
stateMachineBucket.ActiveTransition = transition;
stateMachineBucket.TransitionPosition = 0.0f;
// End base transition
if (stateMachineBucket.BaseTransition)
{
ResetBuckets(context, stateMachineBucket.BaseTransitionState->Data.State.Graph);
stateMachineBucket.BaseTransition = nullptr;
stateMachineBucket.BaseTransitionState = nullptr;
stateMachineBucket.BaseTransitionPosition = 0.0f;
}
}
AnimGraphStateTransition* AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphNode* state, AnimGraphNode* ignoreState)
{
int32 transitionIndex = 0;
const AnimGraphNode::StateBaseData& stateData = state->Data.State;
while (transitionIndex < ANIM_GRAPH_MAX_STATE_TRANSITIONS && stateData.Transitions[transitionIndex] != AnimGraphNode::StateData::InvalidTransitionIndex)
{
const uint16 idx = stateData.Transitions[transitionIndex];
ASSERT(idx < stateMachineData.Graph->StateTransitions.Count());
auto& transition = stateMachineData.Graph->StateTransitions[idx];
if (transition.Destination == stateMachineBucket.CurrentState)
if (transition.Destination == state || transition.Destination == ignoreState)
{
// Ignore transition to the current state
transitionIndex++;
@@ -517,7 +537,7 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const
}
// Evaluate source state transition data (position, length, etc.)
const Value sourceStatePtr = SampleState(stateMachineBucket.CurrentState);
const Value sourceStatePtr = SampleState(state);
auto& transitionData = context.TransitionData; // Note: this could support nested transitions but who uses state machine inside transition rule?
if (ANIM_GRAPH_IS_VALID_PTR(sourceStatePtr))
{
@@ -538,6 +558,7 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const
if (transition.RuleGraph && !useDefaultRule)
{
// Execute transition rule
ANIM_GRAPH_PROFILE_EVENT("Rule");
auto rootNode = transition.RuleGraph->GetRootNode();
ASSERT(rootNode);
if (!(bool)eatBox((Node*)rootNode, &rootNode->Boxes[0]))
@@ -560,10 +581,7 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const
canEnter = true;
if (canEnter)
{
// Start transition
stateMachineBucket.ActiveTransition = &transition;
stateMachineBucket.TransitionPosition = 0.0f;
break;
return &transition;
}
// Skip after Solo transition
@@ -573,6 +591,18 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const
transitionIndex++;
}
// No transition
return nullptr;
}
void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, const AnimGraphNode::StateBaseData& stateData)
{
AnimGraphStateTransition* transition = UpdateStateTransitions(context, stateMachineData, stateMachineBucket.CurrentState);
if (transition)
{
InitStateTransition(context, stateMachineBucket, transition);
}
}
void ComputeMultiBlendLength(float& length, AnimGraphNode* node)
@@ -1101,6 +1131,17 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
{
const float alpha = Math::Saturate((float)tryGetValue(node->GetBox(3), node->Values[0]));
auto mask = node->Assets[0].As<SkeletonMask>();
auto maskAssetBox = node->GetBox(4); // 4 is the id of skeleton mask parameter node.
// Check if have some mask asset connected with the mask node
if (maskAssetBox->HasConnection())
{
const Value assetBoxValue = tryGetValue(maskAssetBox, Value::Null);
// Use the mask connected with this node instead of default mask asset
if (assetBoxValue != Value::Null)
mask = (SkeletonMask*)assetBoxValue.AsAsset;
}
// Only A or missing/invalid mask
if (Math::NearEqual(alpha, 0.0f, ANIM_GRAPH_BLEND_THRESHOLD) || mask == nullptr || mask->WaitForLoaded())
@@ -1501,10 +1542,9 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
// Blend two animations
{
const float alpha = Math::Saturate(bucket.TransitionPosition / blendDuration);
const float alpha = bucket.TransitionPosition / blendDuration;
const auto valueA = tryGetValue(node->GetBox(FirstBlendPoseBoxIndex + bucket.PreviousBlendPoseIndex), Value::Null);
const auto valueB = tryGetValue(node->GetBox(FirstBlendPoseBoxIndex + poseIndex), Value::Null);
value = Blend(node, valueA, valueB, alpha, mode);
}
@@ -1610,22 +1650,21 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
// Enter to the first state pointed by the Entry node (without transitions)
bucket.CurrentState = data.Graph->GetRootNode();
bucket.ActiveTransition = nullptr;
bucket.TransitionPosition = 0.0f;
InitStateTransition(context, bucket);
// Reset all state buckets pof the graphs and nodes included inside the state machine
// Reset all state buckets of the graphs and nodes included inside the state machine
ResetBuckets(context, data.Graph);
}
#define END_TRANSITION() \
ResetBuckets(context, bucket.CurrentState->Data.State.Graph); \
bucket.CurrentState = bucket.ActiveTransition->Destination; \
bucket.ActiveTransition = nullptr; \
bucket.TransitionPosition = 0.0f
InitStateTransition(context, bucket)
// Update the active transition
if (bucket.ActiveTransition)
{
bucket.TransitionPosition += context.DeltaTime;
ASSERT(bucket.CurrentState);
// Check for transition end
if (bucket.TransitionPosition >= bucket.ActiveTransition->BlendDuration)
@@ -1633,38 +1672,70 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
END_TRANSITION();
}
// Check for transition interruption
else if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionRuleRechecking))
else if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionRuleRechecking) &&
EnumHasNoneFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::UseDefaultRule) &&
bucket.ActiveTransition->RuleGraph)
{
const bool useDefaultRule = EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::UseDefaultRule);
if (bucket.ActiveTransition->RuleGraph && !useDefaultRule)
// Execute transition rule
auto rootNode = bucket.ActiveTransition->RuleGraph->GetRootNode();
if (!(bool)eatBox((Node*)rootNode, &rootNode->Boxes[0]))
{
// Execute transition rule
auto rootNode = bucket.ActiveTransition->RuleGraph->GetRootNode();
if (!(bool)eatBox((Node*)rootNode, &rootNode->Boxes[0]))
bool cancelTransition = false;
if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant))
{
bool cancelTransition = false;
if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant))
cancelTransition = true;
}
else
{
// Blend back to the source state (remove currently applied delta and rewind transition)
bucket.TransitionPosition -= context.DeltaTime;
bucket.TransitionPosition -= context.DeltaTime;
if (bucket.TransitionPosition <= ZeroTolerance)
{
cancelTransition = true;
}
else
{
// Blend back to the source state (remove currently applied delta and rewind transition)
bucket.TransitionPosition -= context.DeltaTime;
bucket.TransitionPosition -= context.DeltaTime;
if (bucket.TransitionPosition <= ZeroTolerance)
{
cancelTransition = true;
}
}
if (cancelTransition)
{
// Go back to the source state
ResetBuckets(context, bucket.CurrentState->Data.State.Graph);
bucket.ActiveTransition = nullptr;
bucket.TransitionPosition = 0.0f;
}
}
if (cancelTransition)
{
// Go back to the source state
ResetBuckets(context, bucket.CurrentState->Data.State.Graph);
InitStateTransition(context, bucket);
}
}
}
if (bucket.ActiveTransition && !bucket.BaseTransition && EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionSourceState))
{
// Try to interrupt with any other transition in the source state (except the current transition)
if (AnimGraphStateTransition* transition = UpdateStateTransitions(context, data, bucket.CurrentState, bucket.ActiveTransition->Destination))
{
// Change active transition to the interrupted one
if (EnumHasNoneFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant))
{
// Cache the current blending state to be used as a base when blending towards new destination state (seamless blending after interruption)
bucket.BaseTransition = bucket.ActiveTransition;
bucket.BaseTransitionState = bucket.CurrentState;
bucket.BaseTransitionPosition = bucket.TransitionPosition;
}
bucket.ActiveTransition = transition;
bucket.TransitionPosition = 0.0f;
}
}
if (bucket.ActiveTransition && !bucket.BaseTransition && EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionDestinationState))
{
// Try to interrupt with any other transition in the destination state (except the transition back to the current state if exists)
if (AnimGraphStateTransition* transition = UpdateStateTransitions(context, data, bucket.ActiveTransition->Destination, bucket.CurrentState))
{
// Change active transition to the interrupted one
if (EnumHasNoneFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant))
{
// Cache the current blending state to be used as a base when blending towards new destination state (seamless blending after interruption)
bucket.BaseTransition = bucket.ActiveTransition;
bucket.BaseTransitionState = bucket.CurrentState;
bucket.BaseTransitionPosition = bucket.TransitionPosition;
}
bucket.CurrentState = bucket.ActiveTransition->Destination;
bucket.ActiveTransition = transition;
bucket.TransitionPosition = 0.0f;
}
}
}
@@ -1693,9 +1764,23 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
}
}
// Sample the current state
const auto currentState = SampleState(bucket.CurrentState);
value = currentState;
if (bucket.BaseTransitionState)
{
// Sample the other state (eg. when blending from interrupted state to the another state from the old destination)
value = SampleState(bucket.BaseTransitionState);
if (bucket.BaseTransition)
{
// Evaluate the base pose from the time when transition was interrupted
const auto destinationState = SampleState(bucket.BaseTransition->Destination);
const float alpha = bucket.BaseTransitionPosition / bucket.BaseTransition->BlendDuration;
value = Blend(node, value, destinationState, alpha, bucket.BaseTransition->BlendMode);
}
}
else
{
// Sample the current state
value = SampleState(bucket.CurrentState);
}
// Handle active transition blending
if (bucket.ActiveTransition)
@@ -1704,14 +1789,12 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
const auto destinationState = SampleState(bucket.ActiveTransition->Destination);
// Perform blending
const float alpha = Math::Saturate(bucket.TransitionPosition / bucket.ActiveTransition->BlendDuration);
value = Blend(node, currentState, destinationState, alpha, bucket.ActiveTransition->BlendMode);
const float alpha = bucket.TransitionPosition / bucket.ActiveTransition->BlendDuration;
value = Blend(node, value, destinationState, alpha, bucket.ActiveTransition->BlendMode);
}
// Update bucket
bucket.LastUpdateFrame = context.CurrentFrameIndex;
#undef END_TRANSITION
break;
}
// Entry
@@ -2132,7 +2215,7 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
// Blend out
auto input = tryGetValue(node->GetBox(1), Value::Null);
bucket.BlendOutPosition += deltaTime;
const float alpha = Math::Saturate(bucket.BlendOutPosition / slot.BlendOutTime);
const float alpha = bucket.BlendOutPosition / slot.BlendOutTime;
value = Blend(node, value, input, alpha, AlphaBlendMode::HermiteCubic);
}
else if (bucket.LoopsDone == 0 && slot.BlendInTime > 0.0f && bucket.BlendInPosition < slot.BlendInTime)
@@ -2140,7 +2223,7 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
// Blend in
auto input = tryGetValue(node->GetBox(1), Value::Null);
bucket.BlendInPosition += deltaTime;
const float alpha = Math::Saturate(bucket.BlendInPosition / slot.BlendInTime);
const float alpha = bucket.BlendInPosition / slot.BlendInTime;
value = Blend(node, input, value, alpha, AlphaBlendMode::HermiteCubic);
}
break;

View File

@@ -19,6 +19,7 @@ AudioSource::AudioSource(const SpawnParams& params)
, _minDistance(1000.0f)
, _loop(false)
, _playOnStart(false)
, _startTime(0.0f)
, _allowSpatialization(true)
{
Clip.Changed.Bind<AudioSource, &AudioSource::OnClipChanged>(this);
@@ -71,6 +72,11 @@ void AudioSource::SetPlayOnStart(bool value)
_playOnStart = value;
}
void AudioSource::SetStartTime(float value)
{
_startTime = value;
}
void AudioSource::SetMinDistance(float value)
{
value = Math::Max(0.0f, value);
@@ -361,6 +367,7 @@ void AudioSource::Serialize(SerializeStream& stream, const void* otherObj)
SERIALIZE_MEMBER(DopplerFactor, _dopplerFactor);
SERIALIZE_MEMBER(Loop, _loop);
SERIALIZE_MEMBER(PlayOnStart, _playOnStart);
SERIALIZE_MEMBER(StartTime, _startTime);
SERIALIZE_MEMBER(AllowSpatialization, _allowSpatialization);
}
@@ -377,6 +384,7 @@ void AudioSource::Deserialize(DeserializeStream& stream, ISerializeModifier* mod
DESERIALIZE_MEMBER(DopplerFactor, _dopplerFactor);
DESERIALIZE_MEMBER(Loop, _loop);
DESERIALIZE_MEMBER(PlayOnStart, _playOnStart);
DESERIALIZE_MEMBER(StartTime, _startTime);
DESERIALIZE_MEMBER(AllowSpatialization, _allowSpatialization);
DESERIALIZE(Clip);
}
@@ -540,5 +548,7 @@ void AudioSource::BeginPlay(SceneBeginData* data)
return;
#endif
Play();
if (GetStartTime() > 0)
SetTime(GetStartTime());
}
}

View File

@@ -52,6 +52,7 @@ private:
float _dopplerFactor = 1.0f;
bool _loop;
bool _playOnStart;
float _startTime;
bool _allowSpatialization;
bool _clipChanged = false;
@@ -148,11 +149,25 @@ public:
return _playOnStart;
}
/// <summary>
/// Determines the time (in seconds) at which the audio clip starts playing if Play On Start is enabled.
/// </summary>
API_PROPERTY(Attributes = "EditorOrder(51), DefaultValue(0.0f), Limit(0, float.MaxValue, 0.01f), EditorDisplay(\"Audio Source\", \"Start Time\"), VisibleIf(nameof(PlayOnStart))")
FORCE_INLINE float GetStartTime() const
{
return _startTime;
}
/// <summary>
/// Determines whether the audio clip should auto play on game start.
/// </summary>
API_PROPERTY() void SetPlayOnStart(bool value);
/// <summary>
/// Determines the time (in seconds) at which the audio clip starts playing if Play On Start is enabled.
/// </summary>
API_PROPERTY() void SetStartTime(float value);
/// <summary>
/// Gets the minimum distance at which audio attenuation starts. When the listener is closer to the source than this value, audio is heard at full volume. Once farther away the audio starts attenuating.
/// </summary>

View File

@@ -116,7 +116,7 @@ namespace ALC
{
AudioBackend::Listener::TransformChanged(listener);
const Vector3 velocity = listener->GetVelocity();
const Float3 velocity = listener->GetVelocity();
alListener3f(AL_VELOCITY, FLAX_VEL_TO_OAL(velocity));
alListenerf(AL_GAIN, Audio::GetVolume());
}
@@ -319,8 +319,6 @@ void AudioBackendOAL::Listener_OnAdd(AudioListener* listener)
ALC::RebuildContexts(false);
#else
AudioBackend::Listener::TransformChanged(listener);
const Vector3 velocity = listener->GetVelocity();
alListener3f(AL_VELOCITY, FLAX_VEL_TO_OAL(velocity));
alListenerf(AL_GAIN, Audio::GetVolume());
#endif
}
@@ -336,7 +334,7 @@ void AudioBackendOAL::Listener_VelocityChanged(AudioListener* listener)
{
ALC_GET_LISTENER_CONTEXT(listener)
const Vector3 velocity = listener->GetVelocity();
const Float3 velocity = listener->GetVelocity();
alListener3f(AL_VELOCITY, FLAX_VEL_TO_OAL(velocity));
}
@@ -344,15 +342,15 @@ void AudioBackendOAL::Listener_TransformChanged(AudioListener* listener)
{
ALC_GET_LISTENER_CONTEXT(listener)
const Vector3 position = listener->GetPosition();
const Float3 position = listener->GetPosition();
const Quaternion orientation = listener->GetOrientation();
const Vector3 flipX(-1, 1, 1);
const Vector3 alOrientation[2] =
const Float3 flipX(-1, 1, 1);
const Float3 alOrientation[2] =
{
// Forward
orientation * Vector3::Forward * flipX,
orientation * Float3::Forward * flipX,
// Up
orientation * Vector3::Up * flipX
orientation * Float3::Up * flipX
};
alListenerfv(AL_ORIENTATION, (float*)alOrientation);

View File

@@ -18,7 +18,7 @@ namespace FlaxEditor.Content.Settings
/// <summary>
/// The layers names.
/// </summary>
[EditorOrder(10), EditorDisplay("Layers", EditorDisplayAttribute.InlineStyle), Collection(ReadOnly = true)]
[EditorOrder(10), EditorDisplay("Layers", EditorDisplayAttribute.InlineStyle), Collection(ReadOnly = true, Display = CollectionAttribute.DisplayType.Inline)]
public string[] Layers = new string[32];
/// <summary>

View File

@@ -56,13 +56,7 @@ using System.Runtime.InteropServices;
namespace FlaxEngine
{
/// <summary>
/// Represents a 3x3 Matrix ( contains only Scale and Rotation ).
/// </summary>
[Serializable]
[StructLayout(LayoutKind.Sequential, Pack = 4)]
// ReSharper disable once InconsistentNaming
public struct Matrix3x3 : IEquatable<Matrix3x3>, IFormattable
partial struct Matrix3x3 : IEquatable<Matrix3x3>, IFormattable
{
/// <summary>
/// The size of the <see cref="Matrix3x3"/> type, in bytes.
@@ -135,9 +129,7 @@ namespace FlaxEngine
/// <param name="value">The value that will be assigned to all components.</param>
public Matrix3x3(float value)
{
M11 = M12 = M13 =
M21 = M22 = M23 =
M31 = M32 = M33 = value;
M11 = M12 = M13 = M21 = M22 = M23 = M31 = M32 = M33 = value;
}
/// <summary>

View File

@@ -9,8 +9,9 @@
/// <summary>
/// Represents a 3x3 mathematical matrix.
/// </summary>
API_STRUCT(InBuild) struct FLAXENGINE_API Matrix3x3
API_STRUCT() struct FLAXENGINE_API Matrix3x3
{
DECLARE_SCRIPTING_TYPE_MINIMAL(Matrix3x3);
public:
union
{

View File

@@ -139,6 +139,24 @@ VariantType::VariantType(const StringAnsiView& typeName)
return;
}
}
{
// Aliases
if (typeName == "FlaxEngine.Vector2")
{
new(this) VariantType(Vector2);
return;
}
if (typeName == "FlaxEngine.Vector3")
{
new(this) VariantType(Vector3);
return;
}
if (typeName == "FlaxEngine.Vector4")
{
new(this) VariantType(Vector4);
return;
}
}
// Check case for array
if (typeName.EndsWith(StringAnsiView("[]"), StringSearchCase::CaseSensitive))
@@ -3985,15 +4003,32 @@ void Variant::CopyStructure(void* src)
{
if (AsBlob.Data && src)
{
const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(StringAnsiView(Type.TypeName));
const StringAnsiView typeName(Type.TypeName);
const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(typeName);
if (typeHandle)
{
auto& type = typeHandle.GetType();
type.Struct.Copy(AsBlob.Data, src);
}
#if USE_CSHARP
else if (const auto mclass = Scripting::FindClass(typeName))
{
// Fallback to C#-only types
MCore::Thread::Attach();
if (MANAGED_GC_HANDLE && mclass->IsValueType())
{
MObject* instance = MCore::GCHandle::GetTarget(MANAGED_GC_HANDLE);
void* data = MCore::Object::Unbox(instance);
Platform::MemoryCopy(data, src, mclass->GetInstanceSize());
}
}
#endif
else
{
Platform::MemoryCopy(AsBlob.Data, src, AsBlob.Length);
if (typeName.Length() != 0)
{
LOG(Warning, "Missing scripting type \'{0}\'", String(typeName));
}
}
}
}

View File

@@ -1359,7 +1359,7 @@ bool Actor::IsPrefabRoot() const
Actor* Actor::FindActor(const StringView& name) const
{
Actor* result = nullptr;
if (StringUtils::Compare(*_name, *name) == 0)
if (_name == name)
{
result = const_cast<Actor*>(this);
}
@@ -1393,7 +1393,7 @@ Actor* Actor::FindActor(const MClass* type) const
Actor* Actor::FindActor(const MClass* type, const StringView& name) const
{
CHECK_RETURN(type, nullptr);
if (GetClass()->IsSubClassOf(type) && StringUtils::Compare(*_name, *name) == 0)
if (GetClass()->IsSubClassOf(type) && _name == name)
return const_cast<Actor*>(this);
for (auto child : Children)
{

View File

@@ -225,6 +225,17 @@ void AnimatedModel::SetMasterPoseModel(AnimatedModel* masterPose)
_masterPose->AnimationUpdated.Bind<AnimatedModel, &AnimatedModel::OnAnimationUpdated>(this);
}
const Array<AnimGraphTraceEvent>& AnimatedModel::GetTraceEvents() const
{
#if !BUILD_RELEASE
if (!GetEnableTracing())
{
LOG(Warning, "Accessing AnimatedModel.TraceEvents with tracing disabled.");
}
#endif
return GraphInstance.TraceEvents;
}
#define CHECK_ANIM_GRAPH_PARAM_ACCESS() \
if (!AnimationGraph) \
{ \

View File

@@ -259,6 +259,27 @@ public:
/// <param name="masterPose">The master pose actor to use.</param>
API_FUNCTION() void SetMasterPoseModel(AnimatedModel* masterPose);
/// <summary>
/// Enables extracting animation playback insights for debugging or custom scripting.
/// </summary>
API_PROPERTY(Attributes="HideInEditor, NoSerialize") bool GetEnableTracing() const
{
return GraphInstance.EnableTracing;
}
/// <summary>
/// Enables extracting animation playback insights for debugging or custom scripting.
/// </summary>
API_PROPERTY() void SetEnableTracing(bool value)
{
GraphInstance.EnableTracing = value;
}
/// <summary>
/// Gets the trace events from the last animation update. Valid only when EnableTracing is active.
/// </summary>
API_PROPERTY(Attributes="HideInEditor, NoSerialize") const Array<AnimGraphTraceEvent>& GetTraceEvents() const;
public:
/// <summary>
/// Gets the anim graph instance parameters collection.

View File

@@ -16,7 +16,7 @@ public:
/// <summary>
/// The list of the string localization tables used by the game.
/// </summary>
API_FIELD()
API_FIELD(Attributes="Collection(Display = CollectionAttribute.DisplayType.Inline)")
Array<AssetReference<LocalizedStringTable>> LocalizedStringTables;
/// <summary>

View File

@@ -300,6 +300,44 @@ void RigidBody::ClosestPoint(const Vector3& position, Vector3& result) const
}
}
void RigidBody::AddMovement(const Vector3& translation, const Quaternion& rotation)
{
// filter rotation according to constraints
Quaternion allowedRotation;
if (EnumHasAllFlags(GetConstraints(), RigidbodyConstraints::LockRotation))
allowedRotation = Quaternion::Identity;
else
{
Float3 euler = rotation.GetEuler();
if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockRotationX))
euler.X = 0;
if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockRotationY))
euler.Y = 0;
if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockRotationZ))
euler.Z = 0;
allowedRotation = Quaternion::Euler(euler);
}
// filter translation according to the constraints
auto allowedTranslation = translation;
if (EnumHasAllFlags(GetConstraints(), RigidbodyConstraints::LockPosition))
allowedTranslation = Vector3::Zero;
else
{
if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockPositionX))
allowedTranslation.X = 0;
if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockPositionY))
allowedTranslation.Y = 0;
if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockPositionZ))
allowedTranslation.Z = 0;
}
Transform t;
t.Translation = _transform.Translation + allowedTranslation;
t.Orientation = _transform.Orientation * allowedRotation;
t.Scale = _transform.Scale;
SetTransform(t);
}
void RigidBody::OnCollisionEnter(const Collision& c)
{
CollisionEnter(c);

View File

@@ -486,6 +486,7 @@ public:
// [Actor]
void Serialize(SerializeStream& stream, const void* otherObj) override;
void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override;
void AddMovement(const Vector3& translation, const Quaternion& rotation) override;
// [IPhysicsActor]
void* GetPhysicsActor() const override;

View File

@@ -3312,7 +3312,6 @@ void* PhysicsBackend::CreateVehicle(WheeledVehicle* actor)
// Create vehicle drive
auto drive4W = PxVehicleDrive4W::allocate(wheels.Count());
drive4W->setup(PhysX, actorPhysX, *wheelsSimData, driveSimData, Math::Max(wheels.Count() - 4, 0));
drive4W->setToRestState();
drive4W->mDriveDynData.forceGearChange(PxVehicleGearsData::eFIRST);
drive4W->mDriveDynData.setUseAutoGears(gearbox.AutoGear);
vehicle = drive4W;
@@ -3355,7 +3354,6 @@ void* PhysicsBackend::CreateVehicle(WheeledVehicle* actor)
// Create vehicle drive
auto driveNW = PxVehicleDriveNW::allocate(wheels.Count());
driveNW->setup(PhysX, actorPhysX, *wheelsSimData, driveSimData, wheels.Count());
driveNW->setToRestState();
driveNW->mDriveDynData.forceGearChange(PxVehicleGearsData::eFIRST);
driveNW->mDriveDynData.setUseAutoGears(gearbox.AutoGear);
vehicle = driveNW;
@@ -3366,7 +3364,6 @@ void* PhysicsBackend::CreateVehicle(WheeledVehicle* actor)
// Create vehicle drive
auto driveNo = PxVehicleNoDrive::allocate(wheels.Count());
driveNo->setup(PhysX, actorPhysX, *wheelsSimData);
driveNo->setToRestState();
vehicle = driveNo;
break;
}

View File

@@ -203,8 +203,11 @@ void Physics::Simulate(float dt)
void Physics::CollectResults()
{
if (DefaultScene)
DefaultScene->CollectResults();
for (PhysicsScene* scene : Scenes)
{
if (scene->GetAutoSimulation())
scene->CollectResults();
}
}
bool Physics::IsDuringSimulation()

View File

@@ -324,7 +324,19 @@ void WindowsWindow::SetBorderless(bool isBorderless, bool maximized)
lStyle |= WS_OVERLAPPED | WS_SYSMENU | WS_BORDER | WS_CAPTION;
SetWindowLong(_handle, GWL_STYLE, lStyle);
SetWindowPos(_handle, nullptr, 0, 0, (int)_settings.Size.X, (int)_settings.Size.Y, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
const Float2 clientSize = GetClientSize();
const Float2 desktopSize = Platform::GetDesktopSize();
// Move window and half size if it is larger than desktop size
if (clientSize.X >= desktopSize.X && clientSize.Y >= desktopSize.Y)
{
const Float2 halfSize = desktopSize * 0.5f;
const Float2 middlePos = halfSize * 0.5f;
SetWindowPos(_handle, nullptr, (int)middlePos.X, (int)middlePos.Y, (int)halfSize.X, (int)halfSize.Y, SWP_FRAMECHANGED | SWP_NOZORDER | SWP_NOACTIVATE);
}
else
{
SetWindowPos(_handle, nullptr, 0, 0, (int)clientSize.X, (int)clientSize.Y, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
}
if (maximized)
{

View File

@@ -10,6 +10,32 @@ namespace FlaxEngine
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Class)]
public sealed class CollectionAttribute : Attribute
{
/// <summary>
/// The display type for collections.
/// </summary>
public enum DisplayType
{
/// <summary>
/// Displays the default display type.
/// </summary>
Default,
/// <summary>
/// Displays a header.
/// </summary>
Header,
/// <summary>
/// Displays inline.
/// </summary>
Inline,
}
/// <summary>
/// Gets or sets the display type.
/// </summary>
public DisplayType Display;
/// <summary>
/// Gets or sets whether this collection is read-only. If <c>true</c>, applications using this collection should not allow to add or remove items.
/// </summary>

View File

@@ -606,14 +606,15 @@ namespace FlaxEngine.GUI
_popup.LostFocus += DestroyPopup;
// Show dropdown popup
var locationRootSpace = Location + new Float2(0, Height);
var locationRootSpace = Location + new Float2(0, Height - Height * (1 - Scale.Y) / 2);
var parent = Parent;
while (parent != null && parent != root)
{
locationRootSpace = parent.PointToParent(ref locationRootSpace);
parent = parent.Parent;
}
_popup.Location = locationRootSpace;
_popup.Scale = Scale;
_popup.Location = locationRootSpace - new Float2(0, _popup.Height * (1 - _popup.Scale.Y) / 2);
_popup.Parent = root;
_popup.Focus();
_popup.StartMouseCapture();

View File

@@ -132,6 +132,22 @@ namespace FlaxEngine.GUI
if (_alwaysShowScrollbars != value)
{
_alwaysShowScrollbars = value;
switch (_scrollBars)
{
case ScrollBars.None:
break;
case ScrollBars.Horizontal:
HScrollBar.Visible = value;
break;
case ScrollBars.Vertical:
VScrollBar.Visible = value;
break;
case ScrollBars.Both:
HScrollBar.Visible = value;
VScrollBar.Visible = value;
break;
default: break;
}
PerformLayout();
}
}

View File

@@ -685,9 +685,9 @@ void VisjectExecutor::ProcessGroupPacking(Box* box, Node* node, Value& value)
case 36:
{
// Get value with structure data
Variant structureValue = eatBox(node, node->GetBox(0)->FirstConnection());
if (!node->GetBox(0)->HasConnection())
return;
Variant structureValue = eatBox(node, node->GetBox(0)->FirstConnection());
// Find type
const StringView typeName(node->Values[0]);
@@ -741,6 +741,12 @@ void VisjectExecutor::ProcessGroupPacking(Box* box, Node* node, Value& value)
return;
}
const ScriptingType& type = typeHandle.GetType();
if (structureValue.Type.Type != VariantType::Structure) // If structureValue is eg. Float we can try to cast it to a required structure type
{
VariantType typeVariantType(typeNameAnsiView);
if (Variant::CanCast(structureValue, typeVariantType))
structureValue = Variant::Cast(structureValue, typeVariantType);
}
structureValue.InvertInline(); // Extract any Float3/Int32 into Structure type from inlined format
const ScriptingTypeHandle structureValueTypeHandle = Scripting::FindScriptingType(structureValue.Type.GetTypeName());
if (structureValue.Type.Type != VariantType::Structure || typeHandle != structureValueTypeHandle)

View File

@@ -1970,7 +1970,7 @@ namespace Flax.Build.Bindings
{
if (i != 0)
contents.Append(", ");
contents.Append(eventInfo.Type.GenericArgs[i]).Append(" arg" + i);
contents.Append(eventInfo.Type.GenericArgs[i].GetFullNameNative(buildData, classInfo)).Append(" arg" + i);
}
contents.Append(')').AppendLine();
contents.Append(" {").AppendLine();
@@ -2058,7 +2058,7 @@ namespace Flax.Build.Bindings
{
if (i != 0)
contents.Append(", ");
contents.Append(eventInfo.Type.GenericArgs[i]);
contents.Append(eventInfo.Type.GenericArgs[i].GetFullNameNative(buildData, classInfo));
}
contents.Append(")> f;").AppendLine();
if (eventInfo.IsStatic)
@@ -2084,7 +2084,7 @@ namespace Flax.Build.Bindings
{
if (i != 0)
contents.Append(", ");
contents.Append(eventInfo.Type.GenericArgs[i]).Append(" arg" + i);
contents.Append(eventInfo.Type.GenericArgs[i].GetFullNameNative(buildData, classInfo)).Append(" arg" + i);
}
contents.Append(')').AppendLine();
contents.Append(" {").AppendLine();
@@ -2111,7 +2111,7 @@ namespace Flax.Build.Bindings
{
if (i != 0)
contents.Append(", ");
contents.Append(eventInfo.Type.GenericArgs[i]);
contents.Append(eventInfo.Type.GenericArgs[i].GetFullNameNative(buildData, classInfo));
}
contents.Append(")> f;").AppendLine();
if (eventInfo.IsStatic)
@@ -2423,28 +2423,29 @@ namespace Flax.Build.Bindings
// Getter for structure field
contents.AppendLine(" static void GetField(void* ptr, const String& name, Variant& value)");
contents.AppendLine(" {");
for (var i = 0; i < structureInfo.Fields.Count; i++)
for (int i = 0, count = 0; i < structureInfo.Fields.Count; i++)
{
var fieldInfo = structureInfo.Fields[i];
if (fieldInfo.IsReadOnly || fieldInfo.IsStatic || fieldInfo.IsConstexpr || fieldInfo.Access == AccessLevel.Private)
continue;
if (i == 0)
if (count == 0)
contents.AppendLine($" if (name == TEXT(\"{fieldInfo.Name}\"))");
else
contents.AppendLine($" else if (name == TEXT(\"{fieldInfo.Name}\"))");
contents.AppendLine($" value = {GenerateCppWrapperNativeToVariant(buildData, fieldInfo.Type, structureInfo, $"(({structureTypeNameNative}*)ptr)->{fieldInfo.Name}")};");
count++;
}
contents.AppendLine(" }").AppendLine();
// Setter for structure field
contents.AppendLine(" static void SetField(void* ptr, const String& name, const Variant& value)");
contents.AppendLine(" {");
for (var i = 0; i < structureInfo.Fields.Count; i++)
for (int i = 0, count = 0; i < structureInfo.Fields.Count; i++)
{
var fieldInfo = structureInfo.Fields[i];
if (fieldInfo.IsReadOnly || fieldInfo.IsStatic || fieldInfo.IsConstexpr || fieldInfo.Access == AccessLevel.Private)
continue;
if (i == 0)
if (count == 0)
contents.AppendLine($" if (name == TEXT(\"{fieldInfo.Name}\"))");
else
contents.AppendLine($" else if (name == TEXT(\"{fieldInfo.Name}\"))");
@@ -2460,6 +2461,7 @@ namespace Flax.Build.Bindings
}
else
contents.AppendLine($" (({structureTypeNameNative}*)ptr)->{fieldInfo.Name} = {GenerateCppWrapperVariantToNative(buildData, fieldInfo.Type, structureInfo, "value")};");
count++;
}
contents.AppendLine(" }");

View File

@@ -47,6 +47,20 @@ namespace Flax.Build.Bindings
}
}
private class ParseException : Exception
{
public ParseException(ref ParsingContext context, string msg)
: base(GetParseErrorLocation(ref context, msg))
{
}
}
private static string GetParseErrorLocation(ref ParsingContext context, string msg)
{
// Make it a link clickable in Visual Studio build output
return $"{context.File.Name}({context.Tokenizer.CurrentLine}): {msg}";
}
private static string[] ParseComment(ref ParsingContext context)
{
if (context.StringCache == null)
@@ -180,7 +194,7 @@ namespace Flax.Build.Bindings
case TokenType.RightParent:
parameters.Add(tag);
return parameters;
default: throw new Exception($"Expected right parent or next argument, but got {token.Type}.");
default: throw new ParseException(ref context, $"Expected right parent or next argument, but got {token.Type}.");
}
}
}
@@ -302,7 +316,7 @@ namespace Flax.Build.Bindings
if (context.PreprocessorDefines.TryGetValue(length, out var define))
length = define;
if (!int.TryParse(length, out type.ArraySize))
throw new Exception($"Failed to parse the field {entry} array size '{length}'");
throw new ParseException(ref context, $"Failed to parse the field {entry} array size '{length}'");
}
private static List<FunctionInfo.ParameterInfo> ParseFunctionParameters(ref ParsingContext context)
@@ -354,7 +368,7 @@ namespace Flax.Build.Bindings
if (valid)
break;
var location = "function parameter";
Log.Warning($"Unknown or not supported tag parameter {tag} used on {location} at line {context.Tokenizer.CurrentLine}");
Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{location}'"));
break;
}
}
@@ -483,8 +497,7 @@ namespace Flax.Build.Bindings
desc.Inheritance = new List<TypeInfo>();
desc.Inheritance.Add(inheritType);
token = context.Tokenizer.NextToken();
while (token.Type == TokenType.CommentSingleLine
|| token.Type == TokenType.CommentMultiLine)
while (token.Type == TokenType.CommentSingleLine || token.Type == TokenType.CommentMultiLine)
{
token = context.Tokenizer.NextToken();
}
@@ -563,7 +576,7 @@ namespace Flax.Build.Bindings
// Read 'class' keyword
var token = context.Tokenizer.NextToken();
if (token.Value != "class")
throw new Exception($"Invalid {ApiTokens.Class} usage (expected 'class' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}').");
throw new ParseException(ref context, $"Invalid {ApiTokens.Class} usage (expected 'class' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}').");
// Read specifiers
while (true)
@@ -644,7 +657,7 @@ namespace Flax.Build.Bindings
ParseTypeTag?.Invoke(ref valid, tag, desc);
if (valid)
break;
Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}");
Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'"));
break;
}
}
@@ -672,7 +685,7 @@ namespace Flax.Build.Bindings
// Read 'class' keyword
var token = context.Tokenizer.NextToken();
if (token.Value != "class")
throw new Exception($"Invalid {ApiTokens.Interface} usage (expected 'class' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}').");
throw new ParseException(ref context, $"Invalid {ApiTokens.Interface} usage (expected 'class' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}').");
// Read specifiers
while (true)
@@ -735,7 +748,7 @@ namespace Flax.Build.Bindings
ParseTypeTag?.Invoke(ref valid, tag, desc);
if (valid)
break;
Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}");
Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'"));
break;
}
}
@@ -815,13 +828,13 @@ namespace Flax.Build.Bindings
{
case "const":
if (desc.IsConst)
throw new Exception($"Invalid double 'const' specifier in function {desc.Name} at line {context.Tokenizer.CurrentLine}.");
throw new ParseException(ref context, $"Invalid double 'const' specifier in function {desc.Name}");
desc.IsConst = true;
break;
case "override":
desc.IsVirtual = true;
break;
default: throw new Exception($"Unknown identifier '{token.Value}' in function {desc.Name} at line {context.Tokenizer.CurrentLine}.");
default: throw new ParseException(ref context, $"Unknown identifier '{token.Value}' in function {desc.Name}");
}
}
else if (token.Type == TokenType.LeftCurlyBrace)
@@ -875,7 +888,7 @@ namespace Flax.Build.Bindings
ParseMemberTag?.Invoke(ref valid, tag, desc);
if (valid)
break;
Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}");
Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'"));
break;
}
}
@@ -892,11 +905,11 @@ namespace Flax.Build.Bindings
var classInfo = context.ScopeInfo as ClassInfo;
if (classInfo == null)
throw new Exception($"Found property {propertyName} outside the class at line {context.Tokenizer.CurrentLine}.");
throw new ParseException(ref context, $"Found property {propertyName} outside the class");
var isGetter = !functionInfo.ReturnType.IsVoid;
if (!isGetter && functionInfo.Parameters.Count == 0)
throw new Exception($"Property {propertyName} setter method in class {classInfo.Name} has to have value parameter to set (line {context.Tokenizer.CurrentLine}).");
throw new ParseException(ref context, $"Property {propertyName} setter method in class {classInfo.Name} has to have value parameter to set (line {context.Tokenizer.CurrentLine}).");
var propertyType = isGetter ? functionInfo.ReturnType : functionInfo.Parameters[0].Type;
var propertyInfo = classInfo.Properties.FirstOrDefault(x => x.Name == propertyName);
@@ -917,7 +930,7 @@ namespace Flax.Build.Bindings
else
{
if (propertyInfo.IsStatic != functionInfo.IsStatic)
throw new Exception($"Property {propertyName} in class {classInfo.Name} has to have both getter and setter methods static or non-static (line {context.Tokenizer.CurrentLine}).");
throw new ParseException(ref context, $"Property {propertyName} in class {classInfo.Name} has to have both getter and setter methods static or non-static (line {context.Tokenizer.CurrentLine}).");
}
if (functionInfo.Tags != null)
{
@@ -934,9 +947,9 @@ namespace Flax.Build.Bindings
}
if (isGetter && propertyInfo.Getter != null)
throw new Exception($"Property {propertyName} in class {classInfo.Name} cannot have multiple getter method (line {context.Tokenizer.CurrentLine}). Getter methods of properties must return value, while setters take this as a parameter.");
throw new ParseException(ref context, $"Property {propertyName} in class {classInfo.Name} cannot have multiple getter method (line {context.Tokenizer.CurrentLine}). Getter methods of properties must return value, while setters take this as a parameter.");
if (!isGetter && propertyInfo.Setter != null)
throw new Exception($"Property {propertyName} in class {classInfo.Name} cannot have multiple setter method (line {context.Tokenizer.CurrentLine}). Getter methods of properties must return value, while setters take this as a parameter.");
throw new ParseException(ref context, $"Property {propertyName} in class {classInfo.Name} cannot have multiple setter method (line {context.Tokenizer.CurrentLine}). Getter methods of properties must return value, while setters take this as a parameter.");
if (isGetter)
propertyInfo.Getter = functionInfo;
@@ -963,7 +976,7 @@ namespace Flax.Build.Bindings
return propertyInfo;
if (getterType.Type == "Array" && setterType.Type == "Span" && getterType.GenericArgs?.Count == 1 && setterType.GenericArgs?.Count == 1 && getterType.GenericArgs[0].Equals(setterType.GenericArgs[0]))
return propertyInfo;
throw new Exception($"Property {propertyName} in class {classInfo.Name} (line {context.Tokenizer.CurrentLine}) has mismatching getter return type ({getterType}) and setter parameter type ({setterType}). Both getter and setter methods must use the same value type used for property.");
throw new ParseException(ref context, $"Property {propertyName} in class {classInfo.Name} (line {context.Tokenizer.CurrentLine}) has mismatching getter return type ({getterType}) and setter parameter type ({setterType}). Both getter and setter methods must use the same value type used for property.");
}
if (propertyInfo.Comment != null)
@@ -996,7 +1009,7 @@ namespace Flax.Build.Bindings
// Read 'enum' or `enum class` keywords
var token = context.Tokenizer.NextToken();
if (token.Value != "enum")
throw new Exception($"Invalid {ApiTokens.Enum} usage at line {context.Tokenizer.CurrentLine} (expected 'enum' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}').");
throw new ParseException(ref context, $"Invalid {ApiTokens.Enum} usage (expected 'enum' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}').");
token = context.Tokenizer.NextToken();
if (token.Value != "class")
context.Tokenizer.PreviousToken();
@@ -1079,7 +1092,7 @@ namespace Flax.Build.Bindings
entry.Attributes = tag.Value;
break;
default:
Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name + " enum entry"} at line {context.Tokenizer.CurrentLine}");
Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'"));
break;
}
}
@@ -1153,7 +1166,7 @@ namespace Flax.Build.Bindings
ParseTypeTag?.Invoke(ref valid, tag, desc);
if (valid)
break;
Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}");
Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'"));
break;
}
}
@@ -1176,7 +1189,7 @@ namespace Flax.Build.Bindings
// Read 'struct' keyword
var token = context.Tokenizer.NextToken();
if (token.Value != "struct")
throw new Exception($"Invalid {ApiTokens.Struct} usage (expected 'struct' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}').");
throw new ParseException(ref context, $"Invalid {ApiTokens.Struct} usage (expected 'struct' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}').");
// Read name
desc.Name = desc.NativeName = ParseName(ref context);
@@ -1233,7 +1246,7 @@ namespace Flax.Build.Bindings
ParseTypeTag?.Invoke(ref valid, tag, desc);
if (valid)
break;
Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}");
Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'"));
break;
}
}
@@ -1366,7 +1379,7 @@ namespace Flax.Build.Bindings
ParseMemberTag?.Invoke(ref valid, tag, desc);
if (valid)
break;
Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}");
Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'"));
break;
}
}
@@ -1396,7 +1409,7 @@ namespace Flax.Build.Bindings
if (desc.Type.Type == "Action")
desc.Type = new TypeInfo { Type = "Delegate", GenericArgs = new List<TypeInfo>() };
else if (desc.Type.Type != "Delegate")
throw new Exception($"Invalid {ApiTokens.Event} type. Only Action and Delegate<> types are supported. '{desc.Type}' used on event at line {context.Tokenizer.CurrentLine}.");
throw new ParseException(ref context, $"Invalid {ApiTokens.Event} type. Only Action and Delegate<> types are supported. '{desc.Type}' used on event.");
// Read name
desc.Name = ParseName(ref context);
@@ -1438,7 +1451,7 @@ namespace Flax.Build.Bindings
ParseMemberTag?.Invoke(ref valid, tag, desc);
if (valid)
break;
Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}");
Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'"));
break;
}
}
@@ -1474,7 +1487,7 @@ namespace Flax.Build.Bindings
// Read 'typedef' keyword
var token = context.Tokenizer.NextToken();
if (token.Value != "typedef")
throw new Exception($"Invalid {ApiTokens.Typedef} usage (expected 'typedef' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}').");
throw new ParseException(ref context, $"Invalid {ApiTokens.Typedef} usage (expected 'typedef' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}').");
// Read type definition
desc.Type = ParseType(ref context);
@@ -1513,7 +1526,7 @@ namespace Flax.Build.Bindings
ParseTypeTag?.Invoke(ref valid, tag, desc);
if (valid)
break;
Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}");
Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'"));
break;
}
}

View File

@@ -296,7 +296,7 @@ namespace Flax.Build.Projects.VisualStudio
var folderIdMatches = new Regex("Project\\(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\"\\) = \"(.*?)\", \"(.*?)\", \"{(.*?)}\"").Matches(contents);
foreach (Match match in folderIdMatches)
{
var folder = match.Groups[1].Value;
var folder = match.Groups[2].Value;
var folderId = Guid.ParseExact(match.Groups[3].Value, "D");
folderIds[folder] = folderId;
}
@@ -385,8 +385,7 @@ namespace Flax.Build.Projects.VisualStudio
{
if (!folderIds.TryGetValue(folderPath, out project.FolderGuid))
{
if (!folderIds.TryGetValue(folderParents[i], out project.FolderGuid))
project.FolderGuid = Guid.NewGuid();
project.FolderGuid = Guid.NewGuid();
folderIds.Add(folderPath, project.FolderGuid);
}
folderNames.Add(folderPath);
@@ -401,7 +400,7 @@ namespace Flax.Build.Projects.VisualStudio
var lastSplit = folder.LastIndexOf('\\');
var name = lastSplit != -1 ? folder.Substring(lastSplit + 1) : folder;
vcSolutionFileContent.AppendLine(string.Format("Project(\"{0}\") = \"{1}\", \"{2}\", \"{3}\"", typeGuid, name, name, folderGuid));
vcSolutionFileContent.AppendLine(string.Format("Project(\"{0}\") = \"{1}\", \"{2}\", \"{3}\"", typeGuid, name, folder, folderGuid));
vcSolutionFileContent.AppendLine("EndProject");
}
}