diff --git a/.gitignore b/.gitignore index 1920b3eea..54907892f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ Cache/ Binaries/ Output/ Logs/ +Source/*.Gen.* Source/*.csproj /Package_*/ !Source/Engine/Debug diff --git a/Content/Shaders/Lights.flax b/Content/Shaders/Lights.flax index 34e8c9a8c..ea30cd9f7 100644 --- a/Content/Shaders/Lights.flax +++ b/Content/Shaders/Lights.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b405f5698304e87e6f510c49a0cea3817c9a1a00d7ba63ef4027ac49ba7cc7a4 -size 5299 +oid sha256:83cb261770aba2813f85063f987032f9914387f4011474b0fe347c42522116b4 +size 5122 diff --git a/Content/Shaders/VolumetricFog.flax b/Content/Shaders/VolumetricFog.flax index 18eddae16..eb3ca9f2c 100644 --- a/Content/Shaders/VolumetricFog.flax +++ b/Content/Shaders/VolumetricFog.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72415ba69a685e9a739d1a08d08c6eb9efd3cab31d8cc0c949cb187a9bad19a5 -size 13841 +oid sha256:4ab2c2551ce9cce0355c92f4f6499858e04e4bf764900b305273a7d57b887596 +size 13633 diff --git a/Source/Editor/Content/GUI/ContentView.cs b/Source/Editor/Content/GUI/ContentView.cs index 7eb2e0950..6065ca9f8 100644 --- a/Source/Editor/Content/GUI/ContentView.cs +++ b/Source/Editor/Content/GUI/ContentView.cs @@ -692,10 +692,13 @@ namespace FlaxEditor.Content.GUI c = char.ToLowerInvariant(c); for (int i = 0; i < _items.Count; i++) { - var name = _items[i].ShortName; + var item = _items[i]; + var name = item.ShortName; if (!string.IsNullOrEmpty(name) && char.ToLowerInvariant(name[0]) == c) { - Select(_items[i]); + Select(item); + if (Parent is Panel panel) + panel.ScrollViewTo(item, true); break; } } diff --git a/Source/Editor/Content/Items/ContentItem.cs b/Source/Editor/Content/Items/ContentItem.cs index 4fe75d2c2..66825fb42 100644 --- a/Source/Editor/Content/Items/ContentItem.cs +++ b/Source/Editor/Content/Items/ContentItem.cs @@ -360,7 +360,7 @@ namespace FlaxEditor.Content } /// - /// Updates the tooltip text. + /// Updates the tooltip text text. /// public virtual void UpdateTooltipText() { @@ -384,7 +384,8 @@ namespace FlaxEditor.Content protected virtual void OnBuildTooltipText(StringBuilder sb) { sb.Append("Type: ").Append(TypeDescription).AppendLine(); - sb.Append("Size: ").Append(Utilities.Utils.FormatBytesCount((int)new FileInfo(Path).Length)).AppendLine(); + if (File.Exists(Path)) + sb.Append("Size: ").Append(Utilities.Utils.FormatBytesCount((int)new FileInfo(Path).Length)).AppendLine(); sb.Append("Path: ").Append(Utilities.Utils.GetAssetNamePathWithExt(Path)).AppendLine(); } @@ -718,7 +719,7 @@ namespace FlaxEditor.Content public override bool OnMouseDoubleClick(Float2 location, MouseButton button) { Focus(); - + // Open (Parent as ContentView).OnItemDoubleClick(this); diff --git a/Source/Editor/Content/Items/NewItem.cs b/Source/Editor/Content/Items/NewItem.cs index 94d95f15b..7040adf44 100644 --- a/Source/Editor/Content/Items/NewItem.cs +++ b/Source/Editor/Content/Items/NewItem.cs @@ -1,5 +1,6 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. +using System.Text; using FlaxEngine; namespace FlaxEditor.Content @@ -47,5 +48,11 @@ namespace FlaxEditor.Content /// protected override bool DrawShadow => true; + + /// + public override void UpdateTooltipText() + { + TooltipText = null; + } } } diff --git a/Source/Editor/Content/Proxy/ScriptProxy.cs b/Source/Editor/Content/Proxy/ScriptProxy.cs index 3196ea833..f688f37df 100644 --- a/Source/Editor/Content/Proxy/ScriptProxy.cs +++ b/Source/Editor/Content/Proxy/ScriptProxy.cs @@ -45,7 +45,7 @@ namespace FlaxEditor.Content } /// - public override string NewItemName => "Script"; + public override string NewItemName => "MyScript"; /// public override bool CanCreate(ContentFolder targetLocation) @@ -72,6 +72,8 @@ namespace FlaxEditor.Content // Scripts cannot start with digit. if (Char.IsDigit(filename[0])) return false; + if (filename.Equals("Script")) + return false; return true; } diff --git a/Source/Editor/CustomEditors/CustomEditor.cs b/Source/Editor/CustomEditors/CustomEditor.cs index 1ece31f4d..03b21e12b 100644 --- a/Source/Editor/CustomEditors/CustomEditor.cs +++ b/Source/Editor/CustomEditors/CustomEditor.cs @@ -148,7 +148,7 @@ namespace FlaxEditor.CustomEditors return; // Special case for root objects to run normal layout build - if (_presenter.Selection == Values) + if (_presenter != null && _presenter.Selection == Values) { _presenter.BuildLayout(); return; @@ -159,7 +159,7 @@ namespace FlaxEditor.CustomEditors var layout = _layout; var control = layout.ContainerControl; var parent = _parent; - var parentScrollV = (_presenter.Panel.Parent as Panel)?.VScrollBar?.Value ?? -1; + var parentScrollV = (_presenter?.Panel.Parent as Panel)?.VScrollBar?.Value ?? -1; control.IsLayoutLocked = true; control.DisposeChildren(); @@ -249,6 +249,28 @@ namespace FlaxEditor.CustomEditors internal virtual void RefreshRootChild() { + // Check if need to update value + if (_hasValueDirty) + { + IsSettingValue = true; + try + { + // Cleanup (won't retry update in case of exception) + object val = _valueToSet; + _hasValueDirty = false; + _valueToSet = null; + + // Assign value + for (int i = 0; i < _values.Count; i++) + _values[i] = val; + } + finally + { + OnUnDirty(); + IsSettingValue = false; + } + } + Refresh(); for (int i = 0; i < _children.Count; i++) diff --git a/Source/Editor/CustomEditors/CustomEditorsUtil.cs b/Source/Editor/CustomEditors/CustomEditorsUtil.cs index 4c664031f..5f76b07e2 100644 --- a/Source/Editor/CustomEditors/CustomEditorsUtil.cs +++ b/Source/Editor/CustomEditors/CustomEditorsUtil.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; +using FlaxEditor.CustomEditors.Dedicated; using FlaxEditor.CustomEditors.Editors; using FlaxEditor.Scripting; using FlaxEngine; @@ -110,7 +111,7 @@ namespace FlaxEditor.CustomEditors // Select default editor (based on type) if (targetType.IsEnum) return new EnumEditor(); - if (targetType.IsGenericType) + if (targetType.IsGenericType) { if (targetTypeType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) return new DictionaryEditor(); @@ -121,6 +122,8 @@ namespace FlaxEditor.CustomEditors if (customEditorType != null) return (CustomEditor)Activator.CreateInstance(customEditorType); } + if (typeof(FlaxEngine.Object).IsAssignableFrom(targetTypeType)) + return new ScriptingObjectEditor(); // The most generic editor return new GenericEditor(); diff --git a/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs b/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs index 6a2986d2b..f9325fb26 100644 --- a/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs @@ -21,7 +21,7 @@ namespace FlaxEditor.CustomEditors.Dedicated /// /// [CustomEditor(typeof(Actor)), DefaultEditor] - public class ActorEditor : GenericEditor + public class ActorEditor : ScriptingObjectEditor { private Guid _linkedPrefabId; diff --git a/Source/Editor/CustomEditors/Dedicated/ParticleEffectEditor.cs b/Source/Editor/CustomEditors/Dedicated/ParticleEffectEditor.cs index 75fec4eba..ec71348cf 100644 --- a/Source/Editor/CustomEditors/Dedicated/ParticleEffectEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/ParticleEffectEditor.cs @@ -1,8 +1,10 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. +using System; using System.Linq; using FlaxEditor.Surface; using FlaxEngine; +using FlaxEngine.GUI; namespace FlaxEditor.CustomEditors.Dedicated { @@ -13,6 +15,7 @@ namespace FlaxEditor.CustomEditors.Dedicated [CustomEditor(typeof(ParticleEffect)), DefaultEditor] public class ParticleEffectEditor : ActorEditor { + private Label _infoLabel; private bool _isValid; private bool _isActive; private uint _parametersVersion; @@ -48,6 +51,15 @@ namespace FlaxEditor.CustomEditors.Dedicated return null; } + private void Foreach(Action func) + { + foreach (var value in Values) + { + if (value is ParticleEffect player) + func(player); + } + } + /// public override void Initialize(LayoutElementsContainer layout) { @@ -60,6 +72,26 @@ namespace FlaxEditor.CustomEditors.Dedicated _parametersVersion = effect.ParametersVersion; _isActive = effect.IsActive; + // Show playback options during simulation + if (Editor.IsPlayMode) + { + var playbackGroup = layout.Group("Playback"); + playbackGroup.Panel.Open(); + + _infoLabel = playbackGroup.Label(string.Empty).Label; + _infoLabel.AutoHeight = true; + + var grid = playbackGroup.CustomContainer(); + var gridControl = grid.CustomControl; + gridControl.ClipChildren = false; + gridControl.Height = Button.DefaultHeight; + gridControl.SlotsHorizontally = 3; + gridControl.SlotsVertically = 1; + grid.Button("Play").Button.Clicked += () => Foreach(x => x.Play()); + grid.Button("Pause").Button.Clicked += () => Foreach(x => x.Pause()); + grid.Button("Stop").Button.Clicked += () => Foreach(x => x.Stop()); + } + // Show all effect parameters grouped by the emitter track name var groups = layout.Group("Parameters"); groups.Panel.Open(); @@ -99,7 +131,7 @@ namespace FlaxEditor.CustomEditors.Dedicated base.RefreshRootChild(); return; } - + for (int i = 0; i < ChildrenEditors.Count; i++) { if (_isActive != effect.IsActive || _parametersVersion != effect.ParametersVersion) diff --git a/Source/Editor/CustomEditors/Dedicated/ScriptingObjectEditor.cs b/Source/Editor/CustomEditors/Dedicated/ScriptingObjectEditor.cs new file mode 100644 index 000000000..477deb708 --- /dev/null +++ b/Source/Editor/CustomEditors/Dedicated/ScriptingObjectEditor.cs @@ -0,0 +1,29 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +using FlaxEditor.CustomEditors.Editors; +using FlaxEngine.Networking; + +namespace FlaxEditor.CustomEditors.Dedicated +{ + /// + /// Custom editor for . + /// + public class ScriptingObjectEditor : GenericEditor + { + /// + public override void Initialize(LayoutElementsContainer layout) + { + // Network objects debugging + var obj = Values[0] as FlaxEngine.Object; + if (Editor.IsPlayMode && NetworkManager.IsConnected && NetworkReplicator.HasObject(obj)) + { + var group = layout.Group("Network"); + group.Panel.Open(); + group.Label("Role", Utilities.Utils.GetPropertyNameUI(NetworkReplicator.GetObjectRole(obj).ToString())); + group.Label("Owner Client Id", NetworkReplicator.GetObjectOwnerClientId(obj).ToString()); + } + + base.Initialize(layout); + } + } +} diff --git a/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs b/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs index 41dc1dc08..bf82e19da 100644 --- a/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs @@ -3,7 +3,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; using FlaxEditor.Actions; using FlaxEditor.Content; using FlaxEditor.GUI; diff --git a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs index 2612440a4..076b94b0a 100644 --- a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs @@ -78,7 +78,7 @@ namespace FlaxEditor.CustomEditors.Editors /// public class ScaleEditor : Float3Editor { - private Image _linkImage; + private Button _linkButton; /// public override void Initialize(LayoutElementsContainer layout) @@ -87,19 +87,20 @@ namespace FlaxEditor.CustomEditors.Editors LinkValues = Editor.Instance.Windows.PropertiesWin.ScaleLinked; - _linkImage = new Image + // Add button with the link icon + _linkButton = new Button { + BackgroundBrush = new SpriteBrush(Editor.Instance.Icons.Link32), Parent = LinkedLabel, Width = 18, Height = 18, - Brush = LinkValues ? new SpriteBrush(Editor.Instance.Icons.Link32) : new SpriteBrush(), AnchorPreset = AnchorPresets.TopLeft, - TooltipText = "Scale values are linked together.", }; + _linkButton.Clicked += ToggleLink; + SetLinkStyle(); var x = LinkedLabel.Text.Value.Length * 7 + 5; - _linkImage.LocalX += x; - _linkImage.LocalY += 1; - + _linkButton.LocalX += x; + _linkButton.LocalY += 1; LinkedLabel.SetupContextMenu += (label, menu, editor) => { menu.AddSeparator(); @@ -127,7 +128,16 @@ namespace FlaxEditor.CustomEditors.Editors { LinkValues = !LinkValues; Editor.Instance.Windows.PropertiesWin.ScaleLinked = LinkValues; - _linkImage.Brush = LinkValues ? new SpriteBrush(Editor.Instance.Icons.Link32) : new SpriteBrush(); + SetLinkStyle(); + } + + private void SetLinkStyle() + { + var style = FlaxEngine.GUI.Style.Current; + var backgroundColor = LinkValues ? style.Foreground : style.ForegroundDisabled; + _linkButton.SetColors(backgroundColor); + _linkButton.BorderColor = _linkButton.BorderColorSelected = _linkButton.BorderColorHighlighted = Color.Transparent; + _linkButton.TooltipText = LinkValues ? "Unlinks scale components from uniform scaling" : "Links scale components for uniform scaling"; } } } diff --git a/Source/Editor/CustomEditors/Editors/DictionaryEditor.cs b/Source/Editor/CustomEditors/Editors/DictionaryEditor.cs index f26bf25a4..73089ff04 100644 --- a/Source/Editor/CustomEditors/Editors/DictionaryEditor.cs +++ b/Source/Editor/CustomEditors/Editors/DictionaryEditor.cs @@ -2,7 +2,6 @@ using System; using System.Collections; -using System.Collections.Generic; using System.Linq; using FlaxEditor.CustomEditors.Elements; using FlaxEditor.CustomEditors.GUI; @@ -46,8 +45,8 @@ namespace FlaxEditor.CustomEditors.Editors private void OnSetupContextMenu(PropertyNameLabel label, ContextMenu menu, CustomEditor linkedEditor) { - menu.AddSeparator(); - + if (menu.Items.Any()) + menu.AddSeparator(); menu.AddButton("Remove", OnRemoveClicked).Enabled = !_editor._readOnly; menu.AddButton("Edit", OnEditClicked).Enabled = _editor._canEditKeys; } @@ -62,6 +61,7 @@ namespace FlaxEditor.CustomEditors.Editors var keyType = _editor.Values.Type.GetGenericArguments()[0]; if (keyType == typeof(string) || keyType.IsPrimitive) { + // Edit as text var popup = RenamePopup.Show(Parent, Rectangle.Margin(Bounds, Margin), Text, false); popup.Validate += (renamePopup, value) => { @@ -79,7 +79,6 @@ namespace FlaxEditor.CustomEditors.Editors newKey = JsonSerializer.Deserialize(renamePopup.Text, keyType); else newKey = renamePopup.Text; - _editor.ChangeKey(_key, newKey); _key = newKey; Text = _key.ToString(); @@ -87,6 +86,7 @@ namespace FlaxEditor.CustomEditors.Editors } else if (keyType.IsEnum) { + // Edit via enum picker var popup = RenamePopup.Show(Parent, Rectangle.Margin(Bounds, Margin), Text, false); var picker = new EnumComboBox(keyType) { @@ -109,7 +109,21 @@ namespace FlaxEditor.CustomEditors.Editors } else { - throw new NotImplementedException("Missing editing for dictionary key type " + keyType); + // Generic editor + var popup = ContextMenuBase.ShowEmptyMenu(Parent, Rectangle.Margin(Bounds, Margin)); + var presenter = new CustomEditorPresenter(null); + presenter.Panel.AnchorPreset = AnchorPresets.StretchAll; + presenter.Panel.IsScrollable = false; + presenter.Panel.Parent = popup; + presenter.Select(_key); + presenter.Modified += () => + { + popup.Hide(); + object newKey = presenter.Selection[0]; + _editor.ChangeKey(_key, newKey); + _key = newKey; + Text = _key?.ToString(); + }; } } @@ -160,7 +174,7 @@ namespace FlaxEditor.CustomEditors.Editors var argTypes = type.GetGenericArguments(); var keyType = argTypes[0]; var valueType = argTypes[1]; - _canEditKeys = keyType == typeof(string) || keyType.IsPrimitive || keyType.IsEnum; + _canEditKeys = keyType == typeof(string) || keyType.IsPrimitive || keyType.IsEnum || keyType.IsValueType; _background = FlaxEngine.GUI.Style.Current.CollectionBackgroundColor; _readOnly = false; _notNullItems = false; @@ -383,6 +397,7 @@ namespace FlaxEditor.CustomEditors.Editors int newItemsLeft = newSize - oldSize; while (newItemsLeft-- > 0) { + object newKey = null; if (keyType.IsPrimitive) { long uniqueKey = 0; @@ -401,8 +416,7 @@ namespace FlaxEditor.CustomEditors.Editors } } } while (!isUnique); - - newValues[Convert.ChangeType(uniqueKey, keyType)] = TypeUtils.GetDefaultValue(new ScriptType(valueType)); + newKey = Convert.ChangeType(uniqueKey, keyType); } else if (keyType.IsEnum) { @@ -422,8 +436,7 @@ namespace FlaxEditor.CustomEditors.Editors } } } while (!isUnique && uniqueKeyIndex < enumValues.Length); - - newValues[enumValues.GetValue(uniqueKeyIndex)] = TypeUtils.GetDefaultValue(new ScriptType(valueType)); + newKey = enumValues.GetValue(uniqueKeyIndex); } else if (keyType == typeof(string)) { @@ -442,13 +455,13 @@ namespace FlaxEditor.CustomEditors.Editors } } } while (!isUnique); - - newValues[uniqueKey] = TypeUtils.GetDefaultValue(new ScriptType(valueType)); + newKey = uniqueKey; } else { - throw new InvalidOperationException(); + newKey = TypeUtils.GetDefaultValue(new ScriptType(keyType)); } + newValues[newKey] = TypeUtils.GetDefaultValue(new ScriptType(valueType)); } SetValue(newValues); diff --git a/Source/Editor/CustomEditors/Editors/TypeEditor.cs b/Source/Editor/CustomEditors/Editors/TypeEditor.cs index eb0c839ed..ab17c4ae1 100644 --- a/Source/Editor/CustomEditors/Editors/TypeEditor.cs +++ b/Source/Editor/CustomEditors/Editors/TypeEditor.cs @@ -394,11 +394,8 @@ namespace FlaxEditor.CustomEditors.Editors if (_element != null) { _element.CustomControl.ValueChanged += () => SetValue(_element.CustomControl.Value.Type); - if (_element.CustomControl.Type == ScriptType.Object) - { _element.CustomControl.Type = Values.Type.Type != typeof(object) || Values[0] == null ? ScriptType.Object : TypeUtils.GetObjectType(Values[0]); - } } } @@ -408,9 +405,7 @@ namespace FlaxEditor.CustomEditors.Editors base.Refresh(); if (!HasDifferentValues) - { _element.CustomControl.Value = new ScriptType(Values[0] as Type); - } } } @@ -426,9 +421,7 @@ namespace FlaxEditor.CustomEditors.Editors base.Initialize(layout); if (_element != null) - { _element.CustomControl.ValueChanged += () => SetValue(_element.CustomControl.Value); - } } /// @@ -437,9 +430,32 @@ namespace FlaxEditor.CustomEditors.Editors base.Refresh(); if (!HasDifferentValues) - { _element.CustomControl.Value = (ScriptType)Values[0]; - } + } + } + + /// + /// Default implementation of the inspector used to edit reference to the . Used to pick classes. + /// + [CustomEditor(typeof(SoftTypeReference)), DefaultEditor] + public class SoftTypeReferenceEditor : TypeEditorBase + { + /// + public override void Initialize(LayoutElementsContainer layout) + { + base.Initialize(layout); + + if (_element != null) + _element.CustomControl.ValueChanged += () => SetValue(new SoftTypeReference(_element.CustomControl.ValueTypeName)); + } + + /// + public override void Refresh() + { + base.Refresh(); + + if (!HasDifferentValues) + _element.CustomControl.ValueTypeName = ((SoftTypeReference)Values[0]).TypeName; } } diff --git a/Source/Editor/Editor.cpp b/Source/Editor/Editor.cpp index fb6d53c4c..62ef85f2c 100644 --- a/Source/Editor/Editor.cpp +++ b/Source/Editor/Editor.cpp @@ -259,13 +259,13 @@ bool Editor::CheckProjectUpgrade() LOG(Warning, "Project layout upgraded!"); } - // Check if last version was the same + // Check if last version was the same else if (lastMajor == FLAXENGINE_VERSION_MAJOR && lastMinor == FLAXENGINE_VERSION_MINOR) { // Do nothing IsOldProjectOpened = false; } - // Check if last version was older + // Check if last version was older else if (lastMajor < FLAXENGINE_VERSION_MAJOR || (lastMajor == FLAXENGINE_VERSION_MAJOR && lastMinor < FLAXENGINE_VERSION_MINOR)) { LOG(Warning, "The project was opened with the older editor version last time"); @@ -288,7 +288,7 @@ bool Editor::CheckProjectUpgrade() return true; } } - // Check if last version was newer + // Check if last version was newer else if (lastMajor > FLAXENGINE_VERSION_MAJOR || (lastMajor == FLAXENGINE_VERSION_MAJOR && lastMinor > FLAXENGINE_VERSION_MINOR)) { LOG(Warning, "The project was opened with the newer editor version last time"); @@ -312,6 +312,14 @@ bool Editor::CheckProjectUpgrade() } } + // When changing between major/minor version clear some caches to prevent possible issues + if (lastMajor != FLAXENGINE_VERSION_MAJOR || lastMinor != FLAXENGINE_VERSION_MINOR) + { + LOG(Info, "Cleaning cache files from different engine version"); + FileSystem::DeleteDirectory(Globals::ProjectFolder / TEXT("Cache/Cooker")); + FileSystem::DeleteDirectory(Globals::ProjectFolder / TEXT("Cache/Intermediate")); + } + // Upgrade old 0.7 projects // [Deprecated: 01.11.2020, expires 01.11.2021] if (lastMajor == 0 && lastMinor == 7 && lastBuild <= 6197) @@ -330,12 +338,11 @@ bool Editor::CheckProjectUpgrade() file->WriteInt32(FLAXENGINE_VERSION_MAJOR); file->WriteInt32(FLAXENGINE_VERSION_MINOR); file->WriteInt32(FLAXENGINE_VERSION_BUILD); - Delete(file); } else { - LOG(Warning, "Failed to create version cache file"); + LOG(Error, "Failed to create version cache file"); } } diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs index 27d47345d..7ecd197eb 100644 --- a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs +++ b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs @@ -110,6 +110,25 @@ namespace FlaxEditor.GUI.ContextMenu _isSubMenu = true; } + /// + /// Shows the empty menu popup o na screen. + /// + /// The target control. + /// The target control area to cover. + /// Created popup. + public static ContextMenuBase ShowEmptyMenu(Control control, Rectangle area) + { + // Calculate the control size in the window space to handle scaled controls + var upperLeft = control.PointToWindow(area.UpperLeft); + var bottomRight = control.PointToWindow(area.BottomRight); + var size = bottomRight - upperLeft; + + var popup = new ContextMenuBase(); + popup.Size = size; + popup.Show(control, area.Location + new Float2(0, (size.Y - popup.Height) * 0.5f)); + return popup; + } + /// /// Show context menu over given control. /// diff --git a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs index 8209b63cf..161b3f4ae 100644 --- a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs +++ b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs @@ -25,6 +25,7 @@ namespace FlaxEditor.GUI.Dialogs { private const float ButtonsWidth = 60.0f; private const float PickerMargin = 6.0f; + private const float EyedropperMargin = 8.0f; private const float RGBAMargin = 12.0f; private const float HSVMargin = 0.0f; private const float ChannelsMargin = 4.0f; @@ -34,6 +35,7 @@ namespace FlaxEditor.GUI.Dialogs private Color _value; private bool _disableEvents; private bool _useDynamicEditing; + private bool _activeEyedropper; private ColorValueBox.ColorPickerEvent _onChanged; private ColorValueBox.ColorPickerClosedEvent _onClosed; @@ -48,6 +50,7 @@ namespace FlaxEditor.GUI.Dialogs private TextBox _cHex; private Button _cCancel; private Button _cOK; + private Button _cEyedropper; /// /// Gets the selected color. @@ -192,10 +195,44 @@ namespace FlaxEditor.GUI.Dialogs }; _cOK.Clicked += OnSubmit; + // Eyedropper button + var style = Style.Current; + _cEyedropper = new Button(_cOK.X - EyedropperMargin, _cHex.Bottom + PickerMargin) + { + TooltipText = "Eyedropper tool to pick a color directly from the screen", + BackgroundBrush = new SpriteBrush(Editor.Instance.Icons.Search32), + BackgroundColor = style.Foreground, + BackgroundColorHighlighted = style.Foreground.RGBMultiplied(0.9f), + BorderColor = Color.Transparent, + BorderColorHighlighted = style.BorderSelected, + Parent = this, + }; + _cEyedropper.Clicked += OnEyedropStart; + _cEyedropper.Height = (_cValue.Bottom - _cEyedropper.Y) * 0.5f; + _cEyedropper.Width = _cEyedropper.Height; + _cEyedropper.X -= _cEyedropper.Width; + // Set initial color SelectedColor = initialValue; } + private void OnColorPicked(Color32 colorPicked) + { + if (_activeEyedropper) + { + _activeEyedropper = false; + SelectedColor = colorPicked; + ScreenUtilities.PickColorDone -= OnColorPicked; + } + } + + private void OnEyedropStart() + { + _activeEyedropper = true; + ScreenUtilities.PickColor(); + ScreenUtilities.PickColorDone += OnColorPicked; + } + private void OnRGBAChanged() { if (_disableEvents) @@ -221,6 +258,19 @@ namespace FlaxEditor.GUI.Dialogs SelectedColor = color; } + /// + public override void Update(float deltaTime) + { + base.Update(deltaTime); + + // Update eye dropper tool + if (_activeEyedropper) + { + Float2 mousePosition = Platform.MousePosition; + SelectedColor = ScreenUtilities.GetColorAt(mousePosition); + } + } + /// public override void Draw() { @@ -274,6 +324,20 @@ namespace FlaxEditor.GUI.Dialogs base.OnShow(); } + /// + public override bool OnKeyDown(KeyboardKeys key) + { + if (_activeEyedropper && key == KeyboardKeys.Escape) + { + // Cancel eye dropping + _activeEyedropper = false; + ScreenUtilities.PickColorDone -= OnColorPicked; + return true; + } + + return base.OnKeyDown(key); + } + /// public override void OnSubmit() { diff --git a/Source/Editor/GUI/Input/ColorValueBox.cs b/Source/Editor/GUI/Input/ColorValueBox.cs index bafa27c87..167cc65bb 100644 --- a/Source/Editor/GUI/Input/ColorValueBox.cs +++ b/Source/Editor/GUI/Input/ColorValueBox.cs @@ -57,6 +57,11 @@ namespace FlaxEditor.GUI.Input /// protected Color _value; + /// + /// Enables live preview of the selected value from the picker. Otherwise will update the value only when user confirms it on dialog closing. + /// + public bool UseDynamicEditing = true; + /// /// Occurs when value gets changed. /// @@ -143,7 +148,7 @@ namespace FlaxEditor.GUI.Input base.OnSubmit(); // Show color picker dialog - _currentDialog = ShowPickColorDialog?.Invoke(this, _value, OnColorChanged, OnPickerClosed); + _currentDialog = ShowPickColorDialog?.Invoke(this, _value, OnColorChanged, OnPickerClosed, UseDynamicEditing); } private void OnColorChanged(Color color, bool sliding) diff --git a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs index 27b28596f..b8c920266 100644 --- a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs +++ b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs @@ -726,8 +726,6 @@ namespace FlaxEditor.SceneGraph.GUI { DragData data; var tree = ParentTree; - if (tree.Selection.Count == 1) - Select(); // Check if this node is selected if (tree.Selection.Contains(this)) diff --git a/Source/Editor/Scripting/ScriptType.cs b/Source/Editor/Scripting/ScriptType.cs index aa0ca5655..d174b7dff 100644 --- a/Source/Editor/Scripting/ScriptType.cs +++ b/Source/Editor/Scripting/ScriptType.cs @@ -832,7 +832,7 @@ namespace FlaxEditor.Scripting get { if (_managed != null) - return _managed.GetConstructor(Type.EmptyTypes) != null; + return _managed.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null) != null; return _custom?.CanCreateInstance ?? false; } } @@ -892,7 +892,12 @@ namespace FlaxEditor.Scripting public object CreateInstance() { if (_managed != null) - return Activator.CreateInstance(_managed); + { + var ctor = _managed.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null); + object value = RuntimeHelpers.GetUninitializedObject(_managed); + ctor.Invoke(value, null); + return value; + } return _custom.CreateInstance(); } diff --git a/Source/Editor/Surface/Elements/ColorValue.cs b/Source/Editor/Surface/Elements/ColorValue.cs index 96069cfeb..19934581d 100644 --- a/Source/Editor/Surface/Elements/ColorValue.cs +++ b/Source/Editor/Surface/Elements/ColorValue.cs @@ -30,7 +30,7 @@ namespace FlaxEditor.Surface.Elements { ParentNode = parentNode; Archetype = archetype; - + UseDynamicEditing = false; ParentNode.ValuesChanged += OnNodeValuesChanged; } diff --git a/Source/Editor/Utilities/EditorScene.cpp b/Source/Editor/Utilities/EditorScene.cpp index 764b79184..7060baac5 100644 --- a/Source/Editor/Utilities/EditorScene.cpp +++ b/Source/Editor/Utilities/EditorScene.cpp @@ -20,4 +20,6 @@ void EditorScene::Update() e.Call(); for (auto& e : Ticking.FixedUpdate.Ticks) e.Call(); + for (auto& e : Ticking.LateFixedUpdate.Ticks) + e.Call(); } diff --git a/Source/Editor/Utilities/ScreenUtilities.cpp b/Source/Editor/Utilities/ScreenUtilities.cpp new file mode 100644 index 000000000..44f52350e --- /dev/null +++ b/Source/Editor/Utilities/ScreenUtilities.cpp @@ -0,0 +1,130 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#include "ScreenUtilities.h" +#include "Engine/Core/Math/Vector2.h" +#include "Engine/Core/Delegate.h" +#include "Engine/Core/Log.h" +#include "Engine/Profiler/ProfilerCPU.h" + +Delegate ScreenUtilities::PickColorDone; + +#if PLATFORM_WINDOWS + +#include + +#pragma comment(lib, "Gdi32.lib") + +static HHOOK MouseCallbackHook; + +LRESULT CALLBACK OnScreenUtilsMouseCallback(_In_ int nCode, _In_ WPARAM wParam, _In_ LPARAM lParam) +{ + if (nCode >= 0 && wParam == WM_LBUTTONDOWN) + { + UnhookWindowsHookEx(MouseCallbackHook); + + // Push event with the picked color + const Float2 cursorPos = Platform::GetMousePosition(); + const Color32 colorPicked = ScreenUtilities::GetColorAt(cursorPos); + ScreenUtilities::PickColorDone(colorPicked); + return 1; + } + return CallNextHookEx(NULL, nCode, wParam, lParam); +} + +Color32 ScreenUtilities::GetColorAt(const Float2& pos) +{ + PROFILE_CPU(); + HDC deviceContext = GetDC(NULL); + COLORREF color = GetPixel(deviceContext, (int)pos.X, (int)pos.Y); + ReleaseDC(NULL, deviceContext); + return Color32(GetRValue(color), GetGValue(color), GetBValue(color), 255); +} + +void ScreenUtilities::PickColor() +{ + MouseCallbackHook = SetWindowsHookEx(WH_MOUSE_LL, OnScreenUtilsMouseCallback, NULL, NULL); + if (MouseCallbackHook == NULL) + { + LOG(Warning, "Failed to set mouse hook."); + LOG(Warning, "Error: {0}", GetLastError()); + } +} + +#elif PLATFORM_LINUX + +#include "Engine/Platform/Linux/LinuxPlatform.h" +#include "Engine/Platform/Linux/IncludeX11.h" + +Color32 ScreenUtilities::GetColorAt(const Float2& pos) +{ + X11::XColor color; + + X11::Display* display = (X11::Display*) LinuxPlatform::GetXDisplay(); + int defaultScreen = X11::XDefaultScreen(display); + + X11::XImage* image; + image = X11::XGetImage(display, X11::XRootWindow(display, defaultScreen), (int)pos.X, (int)pos.Y, 1, 1, AllPlanes, XYPixmap); + color.pixel = XGetPixel(image, 0, 0); + X11::XFree(image); + + X11::XQueryColor(display, X11::XDefaultColormap(display, defaultScreen), &color); + + Color32 outputColor; + outputColor.R = color.red / 256; + outputColor.G = color.green / 256; + outputColor.B = color.blue / 256; + return outputColor; +} + +void OnScreenUtilsXEventCallback(void* eventPtr) +{ + X11::XEvent* event = (X11::XEvent*) eventPtr; + X11::Display* display = (X11::Display*)LinuxPlatform::GetXDisplay(); + if (event->type == ButtonPress) + { + const Float2 cursorPos = Platform::GetMousePosition(); + const Color32 colorPicked = ScreenUtilities::GetColorAt(cursorPos); + X11::XUngrabPointer(display, CurrentTime); + ScreenUtilities::PickColorDone(colorPicked); + LinuxPlatform::xEventRecieved.Unbind(OnScreenUtilsXEventCallback); + } +} + +void ScreenUtilities::PickColor() +{ + PROFILE_CPU(); + X11::Display* display = (X11::Display*) LinuxPlatform::GetXDisplay(); + X11::Window rootWindow = X11::XRootWindow(display, X11::XDefaultScreen(display)); + + X11::Cursor cursor = XCreateFontCursor(display, 130); + int grabbedPointer = X11::XGrabPointer(display, rootWindow, 0, ButtonPressMask, GrabModeAsync, GrabModeAsync, rootWindow, cursor, CurrentTime); + if (grabbedPointer != GrabSuccess) + { + LOG(Error, "Failed to grab cursor for events."); + X11::XFreeCursor(display, cursor); + return; + } + + X11::XFreeCursor(display, cursor); + LinuxPlatform::xEventRecieved.Bind(OnScreenUtilsXEventCallback); +} + +#elif PLATFORM_MAC + +#include +#include + +Color32 ScreenUtilities::GetColorAt(const Float2& pos) +{ + // TODO: implement ScreenUtilities for macOS + return { 0, 0, 0, 255 }; +} + +void ScreenUtilities::PickColor() +{ + // This is what C# calls to start the color picking sequence + // This should stop mouse clicks from working for one click, and that click is on the selected color + // There is a class called NSColorSample that might implement that for you, but maybe not. +} + +#endif diff --git a/Source/Editor/Utilities/ScreenUtilities.h b/Source/Editor/Utilities/ScreenUtilities.h new file mode 100644 index 000000000..506dc8634 --- /dev/null +++ b/Source/Editor/Utilities/ScreenUtilities.h @@ -0,0 +1,33 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Core/Types/BaseTypes.h" +#include "Engine/Core/Math/Color32.h" +#include "Engine/Core/Math/Vector2.h" +#include "Engine/Core/Delegate.h" + +/// +/// Platform-dependent screen utilities. +/// +API_CLASS(Static) class FLAXENGINE_API ScreenUtilities +{ + DECLARE_SCRIPTING_TYPE_MINIMAL(ScreenUtilities); + + /// + /// Gets the pixel color at the specified coordinates. + /// + /// Screen-space coordinate to read. + /// Pixel color at the specified coordinates. + API_FUNCTION() static Color32 GetColorAt(const Float2& pos); + + /// + /// Starts async color picking. Color will be returned through PickColorDone event when the actions ends (user selected the final color with a mouse). When action is active, GetColorAt can be used to read the current value. + /// + API_FUNCTION() static void PickColor(); + + /// + /// Called when PickColor action is finished. + /// + API_EVENT() static Delegate PickColorDone; +}; diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs index 7aca2526a..a44c35cb3 100644 --- a/Source/Editor/Windows/ContentWindow.cs +++ b/Source/Editor/Windows/ContentWindow.cs @@ -612,7 +612,16 @@ namespace FlaxEditor.Windows /// The files paths to import. public void Paste(string[] files) { - Editor.ContentImporting.Import(files, CurrentViewFolder); + var importFiles = new List(); + foreach (var sourcePath in files) + { + var item = Editor.ContentDatabase.Find(sourcePath); + if (item != null) + Editor.ContentDatabase.Copy(item, Path.Combine(CurrentViewFolder.Path, item.FileName)); + else + importFiles.Add(sourcePath); + } + Editor.ContentImporting.Import(importFiles, CurrentViewFolder); } /// diff --git a/Source/Editor/Windows/OutputLogWindow.cs b/Source/Editor/Windows/OutputLogWindow.cs index 91da504e8..0b2249a33 100644 --- a/Source/Editor/Windows/OutputLogWindow.cs +++ b/Source/Editor/Windows/OutputLogWindow.cs @@ -542,7 +542,7 @@ namespace FlaxEditor.Windows { ref var line = ref lines[j]; textBlock.Range.StartIndex = startIndex + line.FirstCharIndex; - textBlock.Range.EndIndex = startIndex + line.LastCharIndex; + textBlock.Range.EndIndex = startIndex + line.LastCharIndex + 1; textBlock.Bounds = new Rectangle(new Float2(0.0f, prevBlockBottom), line.Size); if (textBlock.Range.Length > 0) @@ -551,7 +551,7 @@ namespace FlaxEditor.Windows var regexStart = line.FirstCharIndex; if (j == 0) regexStart += prefixLength; - var regexLength = line.LastCharIndex - regexStart; + var regexLength = line.LastCharIndex + 1 - regexStart; if (regexLength > 0) { var match = _compileRegex.Match(entryText, regexStart, regexLength); diff --git a/Source/Editor/Windows/Profiler/Network.cs b/Source/Editor/Windows/Profiler/Network.cs index 662ccf445..f0b93a03c 100644 --- a/Source/Editor/Windows/Profiler/Network.cs +++ b/Source/Editor/Windows/Profiler/Network.cs @@ -52,7 +52,7 @@ namespace FlaxEditor.Windows.Profiler private static string FormatSampleBytes(float v) { - return (uint)v + " bytes"; + return Utilities.Utils.FormatBytesCount((ulong)v); } /// diff --git a/Source/Engine/Content/Content.cpp b/Source/Engine/Content/Content.cpp index e58aa458d..d57529b92 100644 --- a/Source/Engine/Content/Content.cpp +++ b/Source/Engine/Content/Content.cpp @@ -760,9 +760,14 @@ bool Content::CloneAssetFile(const StringView& dstPath, const StringView& srcPat // Change asset ID { auto storage = ContentStorageManager::GetStorage(tmpPath); + if (!storage) + { + LOG(Warning, "Cannot change asset ID."); + return true; + } FlaxStorage::Entry e; storage->GetEntry(0, e); - if (!storage || storage->ChangeAssetID(e, dstId)) + if (storage->ChangeAssetID(e, dstId)) { LOG(Warning, "Cannot change asset ID."); return true; diff --git a/Source/Engine/Content/Storage/FlaxStorage.cpp b/Source/Engine/Content/Storage/FlaxStorage.cpp index 8608a34f6..52d7c34b5 100644 --- a/Source/Engine/Content/Storage/FlaxStorage.cpp +++ b/Source/Engine/Content/Storage/FlaxStorage.cpp @@ -211,6 +211,12 @@ FlaxStorage::~FlaxStorage() CHECK(_chunksLock == 0); CHECK(_refCount == 0); ASSERT(_chunks.IsEmpty()); + +#if USE_EDITOR + // Ensure to close any outstanding file handles to prevent file locking in case it failed to load + _file.DeleteAll(); +#endif + } FlaxStorage::LockData FlaxStorage::LockSafe() diff --git a/Source/Engine/Core/Collections/BitArray.h b/Source/Engine/Core/Collections/BitArray.h index 6058dc8a5..01238d434 100644 --- a/Source/Engine/Core/Collections/BitArray.h +++ b/Source/Engine/Core/Collections/BitArray.h @@ -214,7 +214,7 @@ public: /// /// The index of the item. /// The value to set. - void Set(int32 index, bool value) const + void Set(int32 index, bool value) { ASSERT(index >= 0 && index < _count); const ItemType offset = index / sizeof(ItemType); diff --git a/Source/Engine/Core/Delegate.h b/Source/Engine/Core/Delegate.h index 21a4374ea..4fcf60390 100644 --- a/Source/Engine/Core/Delegate.h +++ b/Source/Engine/Core/Delegate.h @@ -203,23 +203,12 @@ public: return _function != nullptr; } - /// - /// Calls the binded function if any has been assigned. - /// - /// A list of parameters for the function invocation. - /// Function result - void TryCall(Params ... params) const - { - if (_function) - _function(_callee, Forward(params)...); - } - /// /// Calls the binded function (it must be assigned). /// /// A list of parameters for the function invocation. /// Function result - ReturnType operator()(Params ... params) const + FORCE_INLINE ReturnType operator()(Params ... params) const { ASSERT(_function); return _function(_callee, Forward(params)...); diff --git a/Source/Engine/Core/ObjectsRemovalService.cpp b/Source/Engine/Core/ObjectsRemovalService.cpp index 77ce686e0..35458595e 100644 --- a/Source/Engine/Core/ObjectsRemovalService.cpp +++ b/Source/Engine/Core/ObjectsRemovalService.cpp @@ -1,6 +1,7 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. #include "ObjectsRemovalService.h" +#include "Utilities.h" #include "Collections/Dictionary.h" #include "Engine/Engine/Time.h" #include "Engine/Engine/EngineService.h" @@ -8,6 +9,11 @@ #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Scripting/ScriptingObject.h" +const Char* BytesSizesData[] = { TEXT("b"), TEXT("Kb"), TEXT("Mb"), TEXT("Gb"), TEXT("Tb"), TEXT("Pb"), TEXT("Eb"), TEXT("Zb"), TEXT("Yb") }; +const Char* HertzSizesData[] = { TEXT("Hz"), TEXT("KHz"), TEXT("MHz"), TEXT("GHz"), TEXT("THz"), TEXT("PHz"), TEXT("EHz"), TEXT("ZHz"), TEXT("YHz") }; +Span Utilities::Private::BytesSizes(BytesSizesData, ARRAY_COUNT(BytesSizesData)); +Span Utilities::Private::HertzSizes(HertzSizesData, ARRAY_COUNT(HertzSizesData)); + namespace ObjectsRemovalServiceImpl { CriticalSection PoolLocker; diff --git a/Source/Engine/Core/Utilities.h b/Source/Engine/Core/Utilities.h index c65200077..36339baf5 100644 --- a/Source/Engine/Core/Utilities.h +++ b/Source/Engine/Core/Utilities.h @@ -4,12 +4,19 @@ #include "Types/BaseTypes.h" #include "Types/String.h" +#include "Types/Span.h" #if _MSC_VER && PLATFORM_SIMD_SSE4_2 #include #endif namespace Utilities { + struct Private + { + static FLAXENGINE_API Span BytesSizes; + static FLAXENGINE_API Span HertzSizes; + }; + // Round floating point value up to 1 decimal place template FORCE_INLINE T RoundTo1DecimalPlace(T value) @@ -31,20 +38,41 @@ namespace Utilities return (T)round((double)value * 1000.0) / (T)1000; } + // Converts units to the best fitting human-readable denominator + // @param units Units count + // @param divider Amount of units required for the next size + // @param sizes Array with human-readable sizes to convert from + // @return The best fitting string of the units + template + String UnitsToText(T units, int32 divider, const Span sizes) + { + if (sizes.Length() == 0) + return String::Format(TEXT("{0}"), units); + int32 i = 0; + double dblSUnits = static_cast(units); + for (; static_cast(units / static_cast(divider)) > 0; i++, units /= divider) + dblSUnits = units / static_cast(divider); + if (i >= sizes.Length()) + i = 0; + return String::Format(TEXT("{0} {1}"), RoundTo2DecimalPlaces(dblSUnits), sizes[i]); + } + // Converts size of the file (in bytes) to the best fitting string // @param bytes Size of the file in bytes // @return The best fitting string of the file size template String BytesToText(T bytes) { - static const Char* sizes[] = { TEXT("B"), TEXT("KB"), TEXT("MB"), TEXT("GB"), TEXT("TB") }; - uint64 i = 0; - double dblSByte = static_cast(bytes); - for (; static_cast(bytes / 1024.0) > 0; i++, bytes /= 1024) - dblSByte = bytes / 1024.0; - if (i >= ARRAY_COUNT(sizes)) - return String::Empty; - return String::Format(TEXT("{0} {1}"), RoundTo2DecimalPlaces(dblSByte), sizes[i]); + return UnitsToText(bytes, 1024, Private::BytesSizes); + } + + // Converts hertz to the best fitting string + // @param hertz Hertz for convertion + // @return The best fitting string + template + String HertzToText(T hertz) + { + return UnitsToText(hertz, 1000, Private::HertzSizes); } // Returns the amount of set bits in 32-bit integer. diff --git a/Source/Engine/Engine/Engine.cpp b/Source/Engine/Engine/Engine.cpp index 160e07a91..bafcd30b3 100644 --- a/Source/Engine/Engine/Engine.cpp +++ b/Source/Engine/Engine/Engine.cpp @@ -66,6 +66,7 @@ Action Engine::FixedUpdate; Action Engine::Update; TaskGraph* Engine::UpdateGraph = nullptr; Action Engine::LateUpdate; +Action Engine::LateFixedUpdate; Action Engine::Draw; Action Engine::Pause; Action Engine::Unpause; @@ -199,6 +200,7 @@ int32 Engine::Main(const Char* cmdLine) if (Time::OnBeginPhysics()) { OnFixedUpdate(); + OnLateFixedUpdate(); Time::OnEndPhysics(); } @@ -274,6 +276,17 @@ void Engine::OnFixedUpdate() } } +void Engine::OnLateFixedUpdate() +{ + PROFILE_CPU_NAMED("Late Fixed Update"); + + // Call event + LateFixedUpdate(); + + // Update services + EngineService::OnLateFixedUpdate(); +} + void Engine::OnUpdate() { PROFILE_CPU_NAMED("Update"); diff --git a/Source/Engine/Engine/Engine.h b/Source/Engine/Engine/Engine.h index 6f8c5fe6e..31d1f6ea6 100644 --- a/Source/Engine/Engine/Engine.h +++ b/Source/Engine/Engine/Engine.h @@ -54,6 +54,11 @@ public: /// static Action LateUpdate; + /// + /// Event called after engine update. + /// + static Action LateFixedUpdate; + /// /// Event called during frame rendering and can be used to invoke custom rendering with GPUDevice. /// @@ -107,6 +112,11 @@ public: /// static void OnLateUpdate(); + /// + /// Late fixed update callback. + /// + static void OnLateFixedUpdate(); + /// /// Draw callback. /// diff --git a/Source/Engine/Engine/EngineService.cpp b/Source/Engine/Engine/EngineService.cpp index 9a01b08b5..7eea66853 100644 --- a/Source/Engine/Engine/EngineService.cpp +++ b/Source/Engine/Engine/EngineService.cpp @@ -33,6 +33,7 @@ static bool CompareEngineServices(EngineService* const& a, EngineService* const& DEFINE_ENGINE_SERVICE_EVENT(FixedUpdate); DEFINE_ENGINE_SERVICE_EVENT(Update); DEFINE_ENGINE_SERVICE_EVENT(LateUpdate); +DEFINE_ENGINE_SERVICE_EVENT(LateFixedUpdate); DEFINE_ENGINE_SERVICE_EVENT(Draw); DEFINE_ENGINE_SERVICE_EVENT_INVERTED(BeforeExit); diff --git a/Source/Engine/Engine/EngineService.h b/Source/Engine/Engine/EngineService.h index f4ae1b271..a142455d0 100644 --- a/Source/Engine/Engine/EngineService.h +++ b/Source/Engine/Engine/EngineService.h @@ -44,6 +44,7 @@ public: DECLARE_ENGINE_SERVICE_EVENT(void, FixedUpdate); DECLARE_ENGINE_SERVICE_EVENT(void, Update); DECLARE_ENGINE_SERVICE_EVENT(void, LateUpdate); + DECLARE_ENGINE_SERVICE_EVENT(void, LateFixedUpdate); DECLARE_ENGINE_SERVICE_EVENT(void, Draw); DECLARE_ENGINE_SERVICE_EVENT(void, BeforeExit); DECLARE_ENGINE_SERVICE_EVENT(void, Dispose); diff --git a/Source/Engine/Engine/NativeInterop.Managed.cs b/Source/Engine/Engine/NativeInterop.Managed.cs index 41ace593b..538703d4e 100644 --- a/Source/Engine/Engine/NativeInterop.Managed.cs +++ b/Source/Engine/Engine/NativeInterop.Managed.cs @@ -16,6 +16,9 @@ namespace FlaxEngine.Interop /// /// Wrapper for managed arrays which are passed to unmanaged code. /// +#if FLAX_EDITOR + [HideInEditor] +#endif public unsafe class ManagedArray { private ManagedHandle _pinnedArrayHandle; @@ -261,11 +264,14 @@ namespace FlaxEngine.Interop return; } - throw new Exception("Tried to free non-pooled ManagedArray as pooled ManagedArray"); + throw new NativeInteropException("Tried to free non-pooled ManagedArray as pooled ManagedArray"); } } } +#if FLAX_EDITOR + [HideInEditor] +#endif internal static class ManagedString { internal static ManagedHandle EmptyStringHandle = ManagedHandle.Alloc(string.Empty); @@ -315,6 +321,9 @@ namespace FlaxEngine.Interop /// /// Handle to managed objects which can be stored in native code. /// +#if FLAX_EDITOR + [HideInEditor] +#endif public struct ManagedHandle { private IntPtr handle; @@ -499,7 +508,7 @@ namespace FlaxEngine.Interop else if (weakPoolOther.TryGetValue(handle, out value)) return value; - throw new Exception("Invalid ManagedHandle"); + throw new NativeInteropException("Invalid ManagedHandle"); } internal static void SetObject(IntPtr handle, object value) @@ -527,7 +536,7 @@ namespace FlaxEngine.Interop else if (weakPoolOther.ContainsKey(handle)) weakPoolOther[handle] = value; - throw new Exception("Invalid ManagedHandle"); + throw new NativeInteropException("Invalid ManagedHandle"); } internal static void FreeHandle(IntPtr handle) @@ -556,7 +565,7 @@ namespace FlaxEngine.Interop else return; - throw new Exception("Invalid ManagedHandle"); + throw new NativeInteropException("Invalid ManagedHandle"); } } } diff --git a/Source/Engine/Engine/NativeInterop.Marshallers.cs b/Source/Engine/Engine/NativeInterop.Marshallers.cs index 148f02539..a3531d11d 100644 --- a/Source/Engine/Engine/NativeInterop.Marshallers.cs +++ b/Source/Engine/Engine/NativeInterop.Marshallers.cs @@ -12,6 +12,9 @@ using System.Runtime.InteropServices.Marshalling; namespace FlaxEngine.Interop { +#if FLAX_EDITOR + [HideInEditor] +#endif [CustomMarshaller(typeof(object), MarshalMode.ManagedToUnmanagedIn, typeof(ManagedHandleMarshaller.ManagedToNative))] [CustomMarshaller(typeof(object), MarshalMode.UnmanagedToManagedOut, typeof(ManagedHandleMarshaller.ManagedToNative))] [CustomMarshaller(typeof(object), MarshalMode.ElementIn, typeof(ManagedHandleMarshaller.ManagedToNative))] @@ -23,6 +26,9 @@ namespace FlaxEngine.Interop [CustomMarshaller(typeof(object), MarshalMode.ElementRef, typeof(ManagedHandleMarshaller))] public static class ManagedHandleMarshaller { +#if FLAX_EDITOR + [HideInEditor] +#endif public static class NativeToManaged { public static object ConvertToManaged(IntPtr unmanaged) => unmanaged == IntPtr.Zero ? null : ManagedHandle.FromIntPtr(unmanaged).Target; @@ -33,6 +39,9 @@ namespace FlaxEngine.Interop } } +#if FLAX_EDITOR + [HideInEditor] +#endif public static class ManagedToNative { public static IntPtr ConvertToUnmanaged(object managed) => managed != null ? ManagedHandle.ToIntPtr(managed, GCHandleType.Weak) : IntPtr.Zero; @@ -48,6 +57,9 @@ namespace FlaxEngine.Interop } } +#if FLAX_EDITOR + [HideInEditor] +#endif public struct Bidirectional { object managed; @@ -99,6 +111,9 @@ namespace FlaxEngine.Interop } } +#if FLAX_EDITOR + [HideInEditor] +#endif [CustomMarshaller(typeof(Type), MarshalMode.Default, typeof(SystemTypeMarshaller))] public static class SystemTypeMarshaller { @@ -118,6 +133,9 @@ namespace FlaxEngine.Interop } } +#if FLAX_EDITOR + [HideInEditor] +#endif [CustomMarshaller(typeof(Exception), MarshalMode.Default, typeof(ExceptionMarshaller))] public static class ExceptionMarshaller { @@ -126,6 +144,9 @@ namespace FlaxEngine.Interop public static void Free(IntPtr unmanaged) => ManagedHandleMarshaller.Free(unmanaged); } +#if FLAX_EDITOR + [HideInEditor] +#endif [CustomMarshaller(typeof(FlaxEngine.Object), MarshalMode.ManagedToUnmanagedIn, typeof(ObjectMarshaller.ManagedToNative))] [CustomMarshaller(typeof(FlaxEngine.Object), MarshalMode.UnmanagedToManagedOut, typeof(ObjectMarshaller.ManagedToNative))] [CustomMarshaller(typeof(FlaxEngine.Object), MarshalMode.ElementIn, typeof(ObjectMarshaller.ManagedToNative))] @@ -134,17 +155,26 @@ namespace FlaxEngine.Interop [CustomMarshaller(typeof(FlaxEngine.Object), MarshalMode.ElementOut, typeof(ObjectMarshaller.NativeToManaged))] public static class ObjectMarshaller { +#if FLAX_EDITOR + [HideInEditor] +#endif public static class NativeToManaged { public static FlaxEngine.Object ConvertToManaged(IntPtr unmanaged) => unmanaged != IntPtr.Zero ? Unsafe.As(ManagedHandle.FromIntPtr(unmanaged).Target) : null; } +#if FLAX_EDITOR + [HideInEditor] +#endif public static class ManagedToNative { public static IntPtr ConvertToUnmanaged(FlaxEngine.Object managed) => Unsafe.As(managed) != null ? ManagedHandle.ToIntPtr(managed) : IntPtr.Zero; } } +#if FLAX_EDITOR + [HideInEditor] +#endif [CustomMarshaller(typeof(CultureInfo), MarshalMode.Default, typeof(CultureInfoMarshaller))] public static class CultureInfoMarshaller { @@ -159,6 +189,9 @@ namespace FlaxEngine.Interop [CustomMarshaller(typeof(Array), MarshalMode.UnmanagedToManagedIn, typeof(SystemArrayMarshaller.NativeToManaged))] public static unsafe class SystemArrayMarshaller { +#if FLAX_EDITOR + [HideInEditor] +#endif public struct ManagedToNative { ManagedArray managedArray; @@ -187,6 +220,9 @@ namespace FlaxEngine.Interop } } +#if FLAX_EDITOR + [HideInEditor] +#endif public struct NativeToManaged { ManagedHandle handle; @@ -217,6 +253,9 @@ namespace FlaxEngine.Interop } } +#if FLAX_EDITOR + [HideInEditor] +#endif [CustomMarshaller(typeof(Dictionary<,>), MarshalMode.ManagedToUnmanagedIn, typeof(DictionaryMarshaller<,>.ManagedToNative))] [CustomMarshaller(typeof(Dictionary<,>), MarshalMode.UnmanagedToManagedOut, typeof(DictionaryMarshaller<,>.ManagedToNative))] [CustomMarshaller(typeof(Dictionary<,>), MarshalMode.ElementIn, typeof(DictionaryMarshaller<,>.ManagedToNative))] @@ -228,21 +267,31 @@ namespace FlaxEngine.Interop [CustomMarshaller(typeof(Dictionary<,>), MarshalMode.ElementRef, typeof(DictionaryMarshaller<,>))] public static unsafe class DictionaryMarshaller { +#if FLAX_EDITOR + [HideInEditor] +#endif public static class NativeToManaged { public static Dictionary ConvertToManaged(IntPtr unmanaged) => DictionaryMarshaller.ToManaged(unmanaged); public static void Free(IntPtr unmanaged) => DictionaryMarshaller.Free(unmanaged); } +#if FLAX_EDITOR + [HideInEditor] +#endif public static class ManagedToNative { public static IntPtr ConvertToUnmanaged(Dictionary managed) => DictionaryMarshaller.ToNative(managed, GCHandleType.Weak); + public static void Free(IntPtr unmanaged) { //DictionaryMarshaller.Free(unmanaged); // No need to free weak handles } } +#if FLAX_EDITOR + [HideInEditor] +#endif public struct Bidirectional { Dictionary managed; @@ -281,6 +330,9 @@ namespace FlaxEngine.Interop } } +#if FLAX_EDITOR + [HideInEditor] +#endif [CustomMarshaller(typeof(CustomMarshallerAttribute.GenericPlaceholder[]), MarshalMode.ManagedToUnmanagedIn, typeof(ArrayMarshaller<,>.ManagedToNative))] [CustomMarshaller(typeof(CustomMarshallerAttribute.GenericPlaceholder[]), MarshalMode.UnmanagedToManagedOut, typeof(ArrayMarshaller<,>.ManagedToNative))] [CustomMarshaller(typeof(CustomMarshallerAttribute.GenericPlaceholder[]), MarshalMode.ElementIn, typeof(ArrayMarshaller<,>.ManagedToNative))] @@ -293,6 +345,9 @@ namespace FlaxEngine.Interop [ContiguousCollectionMarshaller] public static unsafe class ArrayMarshaller where TUnmanagedElement : unmanaged { +#if FLAX_EDITOR + [HideInEditor] +#endif public static class NativeToManaged { public static T[]? AllocateContainerForManagedElements(TUnmanagedElement* unmanaged, int numElements) @@ -330,6 +385,9 @@ namespace FlaxEngine.Interop } } +#if FLAX_EDITOR + [HideInEditor] +#endif public static class ManagedToNative { public static TUnmanagedElement* AllocateContainerForUnmanagedElements(T[]? managed, out int numElements) @@ -364,6 +422,9 @@ namespace FlaxEngine.Interop } } +#if FLAX_EDITOR + [HideInEditor] +#endif public struct Bidirectional { T[] managedArray; @@ -460,6 +521,9 @@ namespace FlaxEngine.Interop } } +#if FLAX_EDITOR + [HideInEditor] +#endif [CustomMarshaller(typeof(string), MarshalMode.ManagedToUnmanagedIn, typeof(StringMarshaller.ManagedToNative))] [CustomMarshaller(typeof(string), MarshalMode.UnmanagedToManagedOut, typeof(StringMarshaller.ManagedToNative))] [CustomMarshaller(typeof(string), MarshalMode.ElementIn, typeof(StringMarshaller.ManagedToNative))] @@ -471,12 +535,18 @@ namespace FlaxEngine.Interop [CustomMarshaller(typeof(string), MarshalMode.ElementRef, typeof(StringMarshaller))] public static class StringMarshaller { +#if FLAX_EDITOR + [HideInEditor] +#endif public static class NativeToManaged { public static string ConvertToManaged(IntPtr unmanaged) => ManagedString.ToManaged(unmanaged); public static void Free(IntPtr unmanaged) => ManagedString.Free(unmanaged); } +#if FLAX_EDITOR + [HideInEditor] +#endif public static class ManagedToNative { public static unsafe IntPtr ConvertToUnmanaged(string managed) @@ -490,6 +560,9 @@ namespace FlaxEngine.Interop } } +#if FLAX_EDITOR + [HideInEditor] +#endif public struct Bidirectional { string managed; diff --git a/Source/Engine/Engine/NativeInterop.Unmanaged.cs b/Source/Engine/Engine/NativeInterop.Unmanaged.cs index ae55f1ed7..c079a14e1 100644 --- a/Source/Engine/Engine/NativeInterop.Unmanaged.cs +++ b/Source/Engine/Engine/NativeInterop.Unmanaged.cs @@ -908,12 +908,7 @@ namespace FlaxEngine.Interop while (unloading) System.Threading.Thread.Sleep(1); -#if FLAX_EDITOR - var isCollectible = true; -#else - var isCollectible = false; -#endif - scriptingAssemblyLoadContext = new AssemblyLoadContext("Flax", isCollectible); + InitScriptingAssemblyLoadContext(); DelegateHelpers.InitMethods(); } @@ -1162,7 +1157,7 @@ namespace FlaxEngine.Interop case Type _ when type.IsClass: monoType = MTypes.Object; break; - default: throw new Exception($"Unsupported type '{type.FullName}'"); + default: throw new NativeInteropException($"Unsupported type '{type.FullName}'"); } return (uint)monoType; } diff --git a/Source/Engine/Engine/NativeInterop.cs b/Source/Engine/Engine/NativeInterop.cs index 38af2e8fe..4f9822570 100644 --- a/Source/Engine/Engine/NativeInterop.cs +++ b/Source/Engine/Engine/NativeInterop.cs @@ -78,6 +78,19 @@ namespace FlaxEngine.Interop return nativeLibrary; } + private static void InitScriptingAssemblyLoadContext() + { +#if FLAX_EDITOR + var isCollectible = true; +#else + var isCollectible = false; +#endif + scriptingAssemblyLoadContext = new AssemblyLoadContext("Flax", isCollectible); +#if FLAX_EDITOR + scriptingAssemblyLoadContext.Resolving += OnScriptingAssemblyLoadContextResolving; +#endif + } + [UnmanagedCallersOnly] internal static unsafe void Init() { @@ -89,15 +102,23 @@ namespace FlaxEngine.Interop System.Threading.Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; System.Threading.Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; -#if FLAX_EDITOR - var isCollectible = true; -#else - var isCollectible = false; -#endif - scriptingAssemblyLoadContext = new AssemblyLoadContext("Flax", isCollectible); + InitScriptingAssemblyLoadContext(); DelegateHelpers.InitMethods(); } +#if FLAX_EDITOR + private static Assembly? OnScriptingAssemblyLoadContextResolving(AssemblyLoadContext assemblyLoadContext, AssemblyName assemblyName) + { + // FIXME: There should be a better way to resolve the path to EditorTargetPath where the dependencies are stored + string editorTargetPath = Path.GetDirectoryName(nativeLibraryPaths.Keys.First(x => x != "FlaxEngine")); + + var assemblyPath = Path.Combine(editorTargetPath, assemblyName.Name + ".dll"); + if (File.Exists(assemblyPath)) + return assemblyLoadContext.LoadFromAssemblyPath(assemblyPath); + return null; + } +#endif + [UnmanagedCallersOnly] internal static unsafe void Exit() { @@ -504,7 +525,7 @@ namespace FlaxEngine.Interop } } - throw new Exception($"Invalid field {field.Name} to marshal for type {typeof(T).Name}"); + throw new NativeInteropException($"Invalid field {field.Name} to marshal for type {typeof(T).Name}"); } private static void ToManagedFieldPointer(FieldInfo field, ref T fieldOwner, IntPtr fieldPtr, out int fieldOffset) @@ -685,7 +706,7 @@ namespace FlaxEngine.Interop MarshalHelper.toManagedFieldMarshallers[i](MarshalHelper.marshallableFields[i], ref managedValue, fieldPtr, out int fieldOffset); fieldPtr += fieldOffset; } - Assert.IsTrue((fieldPtr - nativePtr) == Unsafe.SizeOf()); + Assert.IsTrue((fieldPtr - nativePtr) <= Unsafe.SizeOf()); } else managedValue = Unsafe.Read(nativePtr.ToPointer()); @@ -724,7 +745,7 @@ namespace FlaxEngine.Interop MarshalHelper.toNativeFieldMarshallers[i](MarshalHelper.marshallableFields[i], ref managedValue, nativePtr, out int fieldOffset); nativePtr += fieldOffset; } - Assert.IsTrue((nativePtr - fieldPtr) == Unsafe.SizeOf()); + Assert.IsTrue((nativePtr - fieldPtr) <= Unsafe.SizeOf()); } else Unsafe.AsRef(nativePtr.ToPointer()) = managedValue; @@ -1252,6 +1273,17 @@ namespace FlaxEngine.Interop } #endif } + + internal class NativeInteropException : Exception + { + public NativeInteropException(string message) + : base(message) + { +#if !BUILD_RELEASE + Debug.Logger.LogHandler.LogWrite(LogType.Error, "Native interop exception!"); +#endif + } + } } #endif diff --git a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUDeviceDX11.cpp b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUDeviceDX11.cpp index 79bb57d16..1f833933a 100644 --- a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUDeviceDX11.cpp +++ b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUDeviceDX11.cpp @@ -114,7 +114,15 @@ GPUDevice* GPUDeviceDX11::Create() // Create DXGI factory #if PLATFORM_WINDOWS IDXGIFactory1* dxgiFactory; - HRESULT hr = CreateDXGIFactory1(IID_PPV_ARGS(&dxgiFactory)); + IDXGIFactory6* dxgiFactory6; + HRESULT hr = CreateDXGIFactory1(IID_PPV_ARGS(&dxgiFactory6)); + if (hr == S_OK) + dxgiFactory = dxgiFactory6; + else + { + dxgiFactory6 = nullptr; + hr = CreateDXGIFactory1(IID_PPV_ARGS(&dxgiFactory)); + } #else IDXGIFactory2* dxgiFactory; HRESULT hr = CreateDXGIFactory1(IID_PPV_ARGS(&dxgiFactory)); @@ -126,16 +134,17 @@ GPUDevice* GPUDeviceDX11::Create() } // Enumerate the DXGIFactory's adapters + int32 selectedAdapterIndex = -1; Array adapters; - IDXGIAdapter* tmpAdapter; - for (uint32 index = 0; dxgiFactory->EnumAdapters(index, &tmpAdapter) != DXGI_ERROR_NOT_FOUND; index++) + IDXGIAdapter* tempAdapter; + for (uint32 index = 0; dxgiFactory->EnumAdapters(index, &tempAdapter) != DXGI_ERROR_NOT_FOUND; index++) { GPUAdapterDX adapter; - if (tmpAdapter && TryCreateDevice(tmpAdapter, maxAllowedFeatureLevel, &adapter.MaxFeatureLevel)) + if (tempAdapter && TryCreateDevice(tempAdapter, maxAllowedFeatureLevel, &adapter.MaxFeatureLevel)) { adapter.Index = index; - VALIDATE_DIRECTX_RESULT(tmpAdapter->GetDesc(&adapter.Description)); - uint32 outputs = RenderToolsDX::CountAdapterOutputs(tmpAdapter); + VALIDATE_DIRECTX_RESULT(tempAdapter->GetDesc(&adapter.Description)); + uint32 outputs = RenderToolsDX::CountAdapterOutputs(tempAdapter); LOG(Info, "Adapter {1}: '{0}', DirectX {2}", adapter.Description.Description, index, RenderToolsDX::GetFeatureLevelString(adapter.MaxFeatureLevel)); LOG(Info, " Dedicated Video Memory: {0}, Dedicated System Memory: {1}, Shared System Memory: {2}, Output(s): {3}", Utilities::BytesToText(adapter.Description.DedicatedVideoMemory), Utilities::BytesToText(adapter.Description.DedicatedSystemMemory), Utilities::BytesToText(adapter.Description.SharedSystemMemory), outputs); @@ -143,14 +152,41 @@ GPUDevice* GPUDeviceDX11::Create() adapters.Add(adapter); } } +#if PLATFORM_WINDOWS + // Find the best performing adapter and prefer using it instead of the first device + const auto gpuPreference = DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE; + if (dxgiFactory6 != nullptr && selectedAdapterIndex == -1) + { + if (dxgiFactory6->EnumAdapterByGpuPreference(0, gpuPreference, IID_PPV_ARGS(&tempAdapter)) != DXGI_ERROR_NOT_FOUND) + { + GPUAdapterDX adapter; + if (tempAdapter && TryCreateDevice(tempAdapter, maxAllowedFeatureLevel, &adapter.MaxFeatureLevel)) + { + DXGI_ADAPTER_DESC desc; + VALIDATE_DIRECTX_RESULT(tempAdapter->GetDesc(&desc)); + for (int i = 0; i < adapters.Count(); i++) + { + if (adapters[i].Description.AdapterLuid.LowPart == desc.AdapterLuid.LowPart && + adapters[i].Description.AdapterLuid.HighPart == desc.AdapterLuid.HighPart) + { + selectedAdapterIndex = i; + break; + } + } + } + } + } +#endif // Select the adapter to use - if (adapters.Count() == 0) + if (selectedAdapterIndex < 0) + selectedAdapterIndex = 0; + if (adapters.Count() == 0 || selectedAdapterIndex >= adapters.Count()) { LOG(Error, "Failed to find valid DirectX adapter!"); return nullptr; } - GPUAdapterDX selectedAdapter = adapters[0]; + GPUAdapterDX selectedAdapter = adapters[selectedAdapterIndex]; uint32 vendorId = 0; if (CommandLine::Options.NVIDIA) vendorId = GPU_VENDOR_ID_NVIDIA; @@ -185,6 +221,15 @@ GPUDevice* GPUDeviceDX11::Create() Delete(device); return nullptr; } + +#if PLATFORM_WINDOWS + if (dxgiFactory6 != nullptr) + dxgiFactory6->Release(); + else +#endif + { + dxgiFactory->Release(); + } return device; } diff --git a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUDeviceDX12.cpp b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUDeviceDX12.cpp index d54f3f38d..ce6016b33 100644 --- a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUDeviceDX12.cpp +++ b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUDeviceDX12.cpp @@ -89,7 +89,15 @@ GPUDevice* GPUDeviceDX12::Create() // Create DXGI factory (CreateDXGIFactory2 is supported on Windows 8.1 or newer) IDXGIFactory4* dxgiFactory; - HRESULT hr = CreateDXGIFactory1(IID_PPV_ARGS(&dxgiFactory)); + IDXGIFactory6* dxgiFactory6; + HRESULT hr = CreateDXGIFactory1(IID_PPV_ARGS(&dxgiFactory6)); + if (hr == S_OK) + dxgiFactory = dxgiFactory6; + else + { + dxgiFactory6 = nullptr; + hr = CreateDXGIFactory1(IID_PPV_ARGS(&dxgiFactory)); + } if (hr != S_OK) { LOG(Error, "Cannot create DXGI adapter. Error code: {0:x}.", hr); @@ -97,6 +105,7 @@ GPUDevice* GPUDeviceDX12::Create() } // Enumerate the DXGIFactory's adapters + int32 selectedAdapterIndex = -1; Array adapters; IDXGIAdapter* tempAdapter; for (uint32 index = 0; dxgiFactory->EnumAdapters(index, &tempAdapter) != DXGI_ERROR_NOT_FOUND; index++) @@ -118,13 +127,39 @@ GPUDevice* GPUDeviceDX12::Create() } } + // Find the best performing adapter and prefer using it instead of the first device + const auto gpuPreference = DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE; + if (dxgiFactory6 != nullptr && selectedAdapterIndex == -1) + { + if (dxgiFactory6->EnumAdapterByGpuPreference(0, gpuPreference, IID_PPV_ARGS(&tempAdapter)) != DXGI_ERROR_NOT_FOUND) + { + GPUAdapterDX adapter; + if (tempAdapter && CheckDX12Support(tempAdapter)) + { + DXGI_ADAPTER_DESC desc; + VALIDATE_DIRECTX_RESULT(tempAdapter->GetDesc(&desc)); + for (int i = 0; i < adapters.Count(); i++) + { + if (adapters[i].Description.AdapterLuid.LowPart == desc.AdapterLuid.LowPart && + adapters[i].Description.AdapterLuid.HighPart == desc.AdapterLuid.HighPart) + { + selectedAdapterIndex = i; + break; + } + } + } + } + } + // Select the adapter to use - if (adapters.Count() == 0) + if (selectedAdapterIndex < 0) + selectedAdapterIndex = 0; + if (adapters.Count() == 0 || selectedAdapterIndex >= adapters.Count()) { LOG(Error, "Failed to find valid DirectX adapter!"); return nullptr; } - GPUAdapterDX selectedAdapter = adapters[0]; + GPUAdapterDX selectedAdapter = adapters[selectedAdapterIndex]; uint32 vendorId = 0; if (CommandLine::Options.NVIDIA) vendorId = GPU_VENDOR_ID_NVIDIA; @@ -167,6 +202,15 @@ GPUDevice* GPUDeviceDX12::Create() return nullptr; } +#if !(PLATFORM_XBOX_SCARLETT || PLATFORM_XBOX_ONE) + if (dxgiFactory6 != nullptr) + dxgiFactory6->Release(); + else +#endif + { + dxgiFactory->Release(); + } + return device; } diff --git a/Source/Engine/GraphicsDevice/DirectX/IncludeDirectXHeaders.h b/Source/Engine/GraphicsDevice/DirectX/IncludeDirectXHeaders.h index 79e8fb7bd..deb0168fb 100644 --- a/Source/Engine/GraphicsDevice/DirectX/IncludeDirectXHeaders.h +++ b/Source/Engine/GraphicsDevice/DirectX/IncludeDirectXHeaders.h @@ -40,9 +40,11 @@ typedef IGraphicsUnknown IDXGISwapChain3; #include #include #include +#include #endif #if GRAPHICS_API_DIRECTX12 #include +#include #endif #pragma comment(lib, "DXGI.lib") diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp index e1ef6fff3..7d5a6a08a 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp @@ -1155,6 +1155,7 @@ GPUDevice* GPUDeviceVulkan::Create() #endif // Enumerate all GPU devices and pick one + int32 selectedAdapterIndex = -1; uint32 gpuCount = 0; VALIDATE_VULKAN_RESULT(vkEnumeratePhysicalDevices(Instance, &gpuCount, nullptr)); if (gpuCount <= 0) @@ -1187,6 +1188,9 @@ GPUDevice* GPUDeviceVulkan::Create() break; case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: type = TEXT("Discrete GPU"); + // Select the first discrete GPU device + if (selectedAdapterIndex == -1) + selectedAdapterIndex = gpuIndex; break; case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: type = TEXT("Virtual GPU"); @@ -1203,12 +1207,13 @@ GPUDevice* GPUDeviceVulkan::Create() } // Select the adapter to use - if (adapters.Count() == 0) + if (selectedAdapterIndex < 0) + selectedAdapterIndex = 0; + if (adapters.Count() == 0 || selectedAdapterIndex >= adapters.Count()) { LOG(Error, "Failed to find valid Vulkan adapter!"); return nullptr; } - int32 selectedAdapter = 0; uint32 vendorId = 0; if (CommandLine::Options.NVIDIA) vendorId = GPU_VENDOR_ID_NVIDIA; @@ -1222,15 +1227,15 @@ GPUDevice* GPUDeviceVulkan::Create() { if (adapters[i].GetVendorId() == vendorId) { - selectedAdapter = i; + selectedAdapterIndex = i; break; } } } - ASSERT(selectedAdapter != -1 && adapters[selectedAdapter].IsValid()); + ASSERT(adapters[selectedAdapterIndex].IsValid()); // Create device - auto device = New(ShaderProfile::Vulkan_SM5, New(adapters[selectedAdapter])); + auto device = New(ShaderProfile::Vulkan_SM5, New(adapters[selectedAdapterIndex])); if (device->Init()) { LOG(Warning, "Graphics Device init failed"); diff --git a/Source/Engine/Input/Enums.h b/Source/Engine/Input/Enums.h index 80e10786f..82498f500 100644 --- a/Source/Engine/Input/Enums.h +++ b/Source/Engine/Input/Enums.h @@ -263,6 +263,37 @@ API_ENUM() enum class InputActionMode Release = 2, }; +/// +/// The input action event phases. +/// +API_ENUM() enum class InputActionState +{ + /// + /// The key/button is not assigned. + /// + None = 0, + + /// + /// The key/button is waiting for input. + /// + Waiting = 1, + + /// + /// User is pressing the key/button. + /// + Pressing = 2, + + /// + /// User pressed the key/button (but wasn't pressing it in the previous frame). + /// + Press = 3, + + /// + /// User released the key/button (was pressing it in the previous frame). + /// + Release = 4, +}; + /// /// The input gamepad index. /// diff --git a/Source/Engine/Input/Input.cpp b/Source/Engine/Input/Input.cpp index 7a4547c62..1fa7454ea 100644 --- a/Source/Engine/Input/Input.cpp +++ b/Source/Engine/Input/Input.cpp @@ -29,11 +29,13 @@ struct ActionData { bool Active; uint64 FrameIndex; + InputActionState State; ActionData() { Active = false; FrameIndex = 0; + State = InputActionState::Waiting; } }; @@ -597,6 +599,16 @@ bool Input::GetAction(const StringView& name) return e ? e->Active : false; } +InputActionState Input::GetActionState(const StringView& name) +{ + const auto e = Actions.TryGet(name); + if (e != nullptr) + { + return e->State; + } + return InputActionState::None; +} + float Input::GetAxis(const StringView& name) { const auto e = Axes.TryGet(name); @@ -806,6 +818,7 @@ void InputService::Update() ActionData& data = Actions[name]; data.Active = false; + data.State = InputActionState::Waiting; // Mark as updated in this frame data.FrameIndex = frame; @@ -830,6 +843,19 @@ void InputService::Update() isActive = Input::GetKeyUp(config.Key) || Input::GetMouseButtonUp(config.MouseButton) || Input::GetGamepadButtonUp(config.Gamepad, config.GamepadButton); } + if (Input::GetKeyDown(config.Key) || Input::GetMouseButtonDown(config.MouseButton) || Input::GetGamepadButtonDown(config.Gamepad, config.GamepadButton)) + { + data.State = InputActionState::Press; + } + else if (Input::GetKey(config.Key) || Input::GetMouseButton(config.MouseButton) || Input::GetGamepadButton(config.Gamepad, config.GamepadButton)) + { + data.State = InputActionState::Pressing; + } + else if (Input::GetKeyUp(config.Key) || Input::GetMouseButtonUp(config.MouseButton) || Input::GetGamepadButtonUp(config.Gamepad, config.GamepadButton)) + { + data.State = InputActionState::Release; + } + data.Active |= isActive; } diff --git a/Source/Engine/Input/Input.h b/Source/Engine/Input/Input.h index d9f86076b..1fcb352ea 100644 --- a/Source/Engine/Input/Input.h +++ b/Source/Engine/Input/Input.h @@ -309,6 +309,14 @@ public: /// API_FUNCTION() static bool GetAction(const StringView& name); + /// + /// Gets the value of the virtual action identified by name. Use to get the current config. + /// + /// The action name. + /// A InputActionPhase determining the current phase of the Action (e.g If it was just pressed, is being held or just released). + /// + API_FUNCTION() static InputActionState GetActionState(const StringView& name); + /// /// Gets the value of the virtual axis identified by name. Use to get the current config. /// diff --git a/Source/Engine/Input/Keyboard.h b/Source/Engine/Input/Keyboard.h index 45f8e295e..7e238bcf5 100644 --- a/Source/Engine/Input/Keyboard.h +++ b/Source/Engine/Input/Keyboard.h @@ -26,9 +26,8 @@ protected: public: /// - /// Gets the text entered during the current frame. + /// Gets the text entered during the current frame (Unicode format). /// - /// The input text (Unicode). API_PROPERTY() StringView GetInputText() const { return StringView(_state.InputText, _state.InputTextLength); diff --git a/Source/Engine/Input/Mouse.h b/Source/Engine/Input/Mouse.h index 7fc2be75a..0acd31238 100644 --- a/Source/Engine/Input/Mouse.h +++ b/Source/Engine/Input/Mouse.h @@ -58,7 +58,6 @@ public: /// /// Gets the position of the mouse in the screen-space coordinates. /// - /// The mouse position API_PROPERTY() FORCE_INLINE Float2 GetPosition() const { return _state.MousePosition; @@ -72,7 +71,6 @@ public: /// /// Gets the delta position of the mouse in the screen-space coordinates. /// - /// The mouse position delta API_PROPERTY() FORCE_INLINE Float2 GetPositionDelta() const { return _state.MousePosition - _prevState.MousePosition; @@ -81,7 +79,6 @@ public: /// /// Gets the mouse wheel change during the last frame. /// - /// Mouse wheel value delta API_PROPERTY() FORCE_INLINE float GetScrollDelta() const { return _state.MouseWheelDelta; diff --git a/Source/Engine/Level/Actors/PointLight.h b/Source/Engine/Level/Actors/PointLight.h index 43f0e7d06..d3e170a03 100644 --- a/Source/Engine/Level/Actors/PointLight.h +++ b/Source/Engine/Level/Actors/PointLight.h @@ -33,7 +33,7 @@ public: /// /// Controls the radial falloff of light when UseInverseSquaredFalloff is disabled. /// - API_FIELD(Attributes="EditorOrder(13), DefaultValue(8.0f), EditorDisplay(\"Light\"), Limit(2, 16, 0.01f)") + API_FIELD(Attributes="EditorOrder(13), DefaultValue(8.0f), EditorDisplay(\"Light\"), Limit(2, 16, 0.01f), VisibleIf(nameof(UseInverseSquaredFalloff), true)") float FallOffExponent = 8.0f; /// @@ -62,9 +62,8 @@ public: public: /// - /// Computes light brightness value + /// Computes light brightness value. /// - /// Brightness float ComputeBrightness() const; /// diff --git a/Source/Engine/Level/Actors/SpotLight.h b/Source/Engine/Level/Actors/SpotLight.h index 547a54e3c..6bb0daf8d 100644 --- a/Source/Engine/Level/Actors/SpotLight.h +++ b/Source/Engine/Level/Actors/SpotLight.h @@ -32,7 +32,7 @@ public: /// /// Controls the radial falloff of light when UseInverseSquaredFalloff is disabled. /// - API_FIELD(Attributes="EditorOrder(13), DefaultValue(8.0f), EditorDisplay(\"Light\"), Limit(2, 16, 0.01f)") + API_FIELD(Attributes="EditorOrder(13), DefaultValue(8.0f), EditorDisplay(\"Light\"), Limit(2, 16, 0.01f), VisibleIf(nameof(UseInverseSquaredFalloff), true)") float FallOffExponent = 8.0f; /// diff --git a/Source/Engine/Level/Level.cpp b/Source/Engine/Level/Level.cpp index 9e3bc1915..124a415cd 100644 --- a/Source/Engine/Level/Level.cpp +++ b/Source/Engine/Level/Level.cpp @@ -138,6 +138,7 @@ public: void Update() override; void LateUpdate() override; void FixedUpdate() override; + void LateFixedUpdate() override; void Dispose() override; }; @@ -242,96 +243,60 @@ void LayersAndTagsSettings::Apply() } } -void LevelService::Update() -{ - PROFILE_CPU_NAMED("Level::Update"); - - ScopeLock lock(Level::ScenesLock); - auto& scenes = Level::Scenes; - - // Update all actors - if (!Time::GetGamePaused() && Level::TickEnabled) - { - for (int32 i = 0; i < scenes.Count(); i++) - { - if (scenes[i]->GetIsActive()) - scenes[i]->Ticking.Update.Tick(); - } +#define TICK_LEVEL(tickingStage, name) \ + PROFILE_CPU_NAMED(name); \ + ScopeLock lock(Level::ScenesLock); \ + auto& scenes = Level::Scenes; \ + if (!Time::GetGamePaused() && Level::TickEnabled) \ + { \ + for (int32 i = 0; i < scenes.Count(); i++) \ + { \ + if (scenes[i]->GetIsActive()) \ + scenes[i]->Ticking.tickingStage.Tick(); \ + } \ } #if USE_EDITOR - else if (!Editor::IsPlayMode) - { - // Run event for script executed in editor - for (int32 i = 0; i < scenes.Count(); i++) - { - if (scenes[i]->GetIsActive()) - scenes[i]->Ticking.Update.TickExecuteInEditor(); - } +#define TICK_LEVEL_EDITOR(tickingStage) \ + else if (!Editor::IsPlayMode) \ + { \ + for (int32 i = 0; i < scenes.Count(); i++) \ + { \ + if (scenes[i]->GetIsActive()) \ + scenes[i]->Ticking.tickingStage.TickExecuteInEditor(); \ + } \ } +#else +#define TICK_LEVEL_EDITOR(tickingStage) #endif + +void LevelService::Update() +{ + TICK_LEVEL(Update, "Level::Update") + TICK_LEVEL_EDITOR(Update) } void LevelService::LateUpdate() { - PROFILE_CPU_NAMED("Level::LateUpdate"); - - ScopeLock lock(Level::ScenesLock); - auto& scenes = Level::Scenes; - - // Update all actors - if (!Time::GetGamePaused() && Level::TickEnabled) - { - for (int32 i = 0; i < scenes.Count(); i++) - { - if (scenes[i]->GetIsActive()) - scenes[i]->Ticking.LateUpdate.Tick(); - } - } -#if USE_EDITOR - else if (!Editor::IsPlayMode) - { - // Run event for script executed in editor - for (int32 i = 0; i < scenes.Count(); i++) - { - if (scenes[i]->GetIsActive()) - scenes[i]->Ticking.LateUpdate.TickExecuteInEditor(); - } - } -#endif - - // Flush actions + TICK_LEVEL(LateUpdate, "Level::LateUpdate") + TICK_LEVEL_EDITOR(LateUpdate) flushActions(); } void LevelService::FixedUpdate() { - PROFILE_CPU_NAMED("Level::FixedUpdate"); - - ScopeLock lock(Level::ScenesLock); - auto& scenes = Level::Scenes; - - // Update all actors - if (!Time::GetGamePaused() && Level::TickEnabled) - { - for (int32 i = 0; i < scenes.Count(); i++) - { - if (scenes[i]->GetIsActive()) - scenes[i]->Ticking.FixedUpdate.Tick(); - } - } -#if USE_EDITOR - else if (!Editor::IsPlayMode) - { - // Run event for script executed in editor - for (int32 i = 0; i < scenes.Count(); i++) - { - if (scenes[i]->GetIsActive()) - scenes[i]->Ticking.FixedUpdate.TickExecuteInEditor(); - } - } -#endif + TICK_LEVEL(FixedUpdate, "Level::FixedUpdate") + TICK_LEVEL_EDITOR(FixedUpdate) } +void LevelService::LateFixedUpdate() +{ + TICK_LEVEL(LateFixedUpdate, "Level::LateFixedUpdate") + TICK_LEVEL_EDITOR(LateFixedUpdate) +} + +#undef TICK_LEVEL +#undef TICK_LEVEL_EDITOR + void LevelService::Dispose() { ScopeLock lock(_sceneActionsLocker); diff --git a/Source/Engine/Level/Scene/SceneTicking.cpp b/Source/Engine/Level/Scene/SceneTicking.cpp index 8998a4b37..235d7bbab 100644 --- a/Source/Engine/Level/Scene/SceneTicking.cpp +++ b/Source/Engine/Level/Scene/SceneTicking.cpp @@ -121,6 +121,19 @@ void SceneTicking::LateUpdateTickData::TickScripts(const Array& scripts } } +SceneTicking::LateFixedUpdateTickData::LateFixedUpdateTickData() + : TickData(64) +{ +} + +void SceneTicking::LateFixedUpdateTickData::TickScripts(const Array& scripts) +{ + for (auto* script : scripts) + { + script->OnLateFixedUpdate(); + } +} + void SceneTicking::AddScript(Script* obj) { ASSERT_LOW_LAYER(obj && obj->GetParent() && obj->GetParent()->GetScene()); @@ -130,6 +143,8 @@ void SceneTicking::AddScript(Script* obj) Update.AddScript(obj); if (obj->_tickLateUpdate) LateUpdate.AddScript(obj); + if (obj->_tickLateFixedUpdate) + LateFixedUpdate.AddScript(obj); } void SceneTicking::RemoveScript(Script* obj) @@ -141,6 +156,8 @@ void SceneTicking::RemoveScript(Script* obj) Update.RemoveScript(obj); if (obj->_tickLateUpdate) LateUpdate.RemoveScript(obj); + if (obj->_tickLateFixedUpdate) + LateFixedUpdate.RemoveScript(obj); } void SceneTicking::Clear() @@ -148,4 +165,5 @@ void SceneTicking::Clear() FixedUpdate.Clear(); Update.Clear(); LateUpdate.Clear(); + LateFixedUpdate.Clear(); } diff --git a/Source/Engine/Level/Scene/SceneTicking.h b/Source/Engine/Level/Scene/SceneTicking.h index 230861087..7d590c8e7 100644 --- a/Source/Engine/Level/Scene/SceneTicking.h +++ b/Source/Engine/Level/Scene/SceneTicking.h @@ -109,6 +109,13 @@ public: void TickScripts(const Array& scripts) override; }; + class FLAXENGINE_API LateFixedUpdateTickData : public TickData + { + public: + LateFixedUpdateTickData(); + void TickScripts(const Array& scripts) override; + }; + public: /// /// Adds the script to scene ticking system. @@ -142,4 +149,9 @@ public: /// The late update tick function. /// LateUpdateTickData LateUpdate; + + /// + /// The late fixed update tick function. + /// + LateFixedUpdateTickData LateFixedUpdate; }; diff --git a/Source/Engine/Networking/INetworkObject.h b/Source/Engine/Networking/INetworkObject.h index 457811557..932c3aa0d 100644 --- a/Source/Engine/Networking/INetworkObject.h +++ b/Source/Engine/Networking/INetworkObject.h @@ -13,22 +13,27 @@ API_INTERFACE(Namespace = "FlaxEngine.Networking") class FLAXENGINE_API INetwork DECLARE_SCRIPTING_TYPE_MINIMAL(INetworkObject); public: /// - /// Event called when network objects gets spawned. + /// Event called when network object gets spawned. /// - API_FUNCTION() virtual void OnNetworkSpawn() = 0; + API_FUNCTION() virtual void OnNetworkSpawn() {}; /// - /// Event called when network objects gets despawned. + /// Event called when network object gets despawned. /// - API_FUNCTION() virtual void OnNetworkDespawn() = 0; + API_FUNCTION() virtual void OnNetworkDespawn() {}; /// /// Event called before network object gets replicated (before reading data). /// - API_FUNCTION() virtual void OnNetworkSerialize() = 0; + API_FUNCTION() virtual void OnNetworkSerialize() {}; /// - /// Event called when network objects gets replicated (after reading data). + /// Event called when network object gets replicated (after reading data). /// - API_FUNCTION() virtual void OnNetworkDeserialize() = 0; + API_FUNCTION() virtual void OnNetworkDeserialize() {}; + + /// + /// Event called when network object gets synced (called only once upon initial sync). + /// + API_FUNCTION() virtual void OnNetworkSync() {}; }; diff --git a/Source/Engine/Networking/NetworkManager.cpp b/Source/Engine/Networking/NetworkManager.cpp index 88a104c3e..88c59ad9a 100644 --- a/Source/Engine/Networking/NetworkManager.cpp +++ b/Source/Engine/Networking/NetworkManager.cpp @@ -317,11 +317,12 @@ bool NetworkManager::StartHost() LocalClient = New(LocalClientId, NetworkConnection{ 0 }); // Auto-connect host + LocalClient->State = NetworkConnectionState::Connecting; + State = NetworkConnectionState::Connected; + StateChanged(); LocalClient->State = NetworkConnectionState::Connected; ClientConnected(LocalClient); - State = NetworkConnectionState::Connected; - StateChanged(); return false; } diff --git a/Source/Engine/Networking/NetworkPeer.cpp b/Source/Engine/Networking/NetworkPeer.cpp index 342cba674..d86824156 100644 --- a/Source/Engine/Networking/NetworkPeer.cpp +++ b/Source/Engine/Networking/NetworkPeer.cpp @@ -6,6 +6,7 @@ #include "Engine/Core/Log.h" #include "Engine/Core/Math/Math.h" #include "Engine/Platform/CPUInfo.h" +#include "Engine/Profiler/ProfilerCPU.h" namespace { @@ -131,6 +132,7 @@ void NetworkPeer::Disconnect(const NetworkConnection& connection) bool NetworkPeer::PopEvent(NetworkEvent& eventRef) { + PROFILE_CPU(); return NetworkDriver->PopEvent(&eventRef); } diff --git a/Source/Engine/Networking/NetworkReplicationHierarchy.cpp b/Source/Engine/Networking/NetworkReplicationHierarchy.cpp new file mode 100644 index 000000000..d86acfc7f --- /dev/null +++ b/Source/Engine/Networking/NetworkReplicationHierarchy.cpp @@ -0,0 +1,205 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#include "NetworkReplicationHierarchy.h" +#include "NetworkManager.h" +#include "Engine/Level/Actor.h" +#include "Engine/Level/SceneObject.h" + +uint16 NetworkReplicationNodeObjectCounter = 0; +NetworkClientsMask NetworkClientsMask::All = { MAX_uint64, MAX_uint64 }; + +Actor* NetworkReplicationHierarchyObject::GetActor() const +{ + auto* actor = ScriptingObject::Cast(Object); + if (!actor) + { + if (const auto* sceneObject = ScriptingObject::Cast(Object)) + actor = sceneObject->GetParent(); + } + return actor; +} + +void NetworkReplicationHierarchyUpdateResult::Init() +{ + _clientsHaveLocation = false; + _clients.Resize(NetworkManager::Clients.Count()); + _clientsMask = NetworkClientsMask(); + for (int32 i = 0; i < _clients.Count(); i++) + _clientsMask.SetBit(i); + _entries.Clear(); + ReplicationScale = 1.0f; +} + +void NetworkReplicationHierarchyUpdateResult::SetClientLocation(int32 clientIndex, const Vector3& location) +{ + CHECK(clientIndex >= 0 && clientIndex < _clients.Count()); + _clientsHaveLocation = true; + Client& client = _clients[clientIndex]; + client.HasLocation = true; + client.Location = location; +} + +bool NetworkReplicationHierarchyUpdateResult::GetClientLocation(int32 clientIndex, Vector3& location) const +{ + CHECK_RETURN(clientIndex >= 0 && clientIndex < _clients.Count(), false); + const Client& client = _clients[clientIndex]; + location = client.Location; + return client.HasLocation; +} + +void NetworkReplicationNode::AddObject(NetworkReplicationHierarchyObject obj) +{ + if (obj.ReplicationFPS > 0.0f) + { + // Randomize initial replication update to spread rep rates more evenly for large scenes that register all objects within the same frame + obj.ReplicationUpdatesLeft = NetworkReplicationNodeObjectCounter++ % Math::Clamp(Math::RoundToInt(NetworkManager::NetworkFPS / obj.ReplicationFPS), 1, 60); + } + + Objects.Add(obj); +} + +bool NetworkReplicationNode::RemoveObject(ScriptingObject* obj) +{ + return !Objects.Remove(obj); +} + +bool NetworkReplicationNode::DirtyObject(ScriptingObject* obj) +{ + const int32 index = Objects.Find(obj); + if (index != -1) + { + NetworkReplicationHierarchyObject& e = Objects[index]; + e.ReplicationUpdatesLeft = 0; + } + return index != -1; +} + +void NetworkReplicationNode::Update(NetworkReplicationHierarchyUpdateResult* result) +{ + CHECK(result); + const float networkFPS = NetworkManager::NetworkFPS / result->ReplicationScale; + for (NetworkReplicationHierarchyObject& obj : Objects) + { + if (obj.ReplicationFPS <= 0.0f) + { + // Always relevant + result->AddObject(obj.Object); + } + else if (obj.ReplicationUpdatesLeft > 0) + { + // Move to the next frame + obj.ReplicationUpdatesLeft--; + } + else + { + NetworkClientsMask targetClients = result->GetClientsMask(); + if (result->_clientsHaveLocation && obj.CullDistance > 0.0f) + { + // Cull object against viewers locations + if (const Actor* actor = obj.GetActor()) + { + const Vector3 objPosition = actor->GetPosition(); + const Real cullDistanceSq = Math::Square(obj.CullDistance); + for (int32 clientIndex = 0; clientIndex < result->_clients.Count(); clientIndex++) + { + const auto& client = result->_clients[clientIndex]; + if (client.HasLocation) + { + const Real distanceSq = Vector3::DistanceSquared(objPosition, client.Location); + // TODO: scale down replication FPS when object is far away from all clients (eg. by 10-50%) + if (distanceSq >= cullDistanceSq) + { + // Object is too far from this viewer so don't send data to him + targetClients.UnsetBit(clientIndex); + } + } + } + } + } + if (targetClients && obj.Object) + { + // Replicate this frame + result->AddObject(obj.Object, targetClients); + } + + // Calculate frames until next replication + obj.ReplicationUpdatesLeft = (uint16)Math::Clamp(Math::RoundToInt(networkFPS / obj.ReplicationFPS) - 1, 0, MAX_uint16); + } + } +} + +NetworkReplicationGridNode::~NetworkReplicationGridNode() +{ + for (const auto& e : _children) + Delete(e.Value.Node); +} + +void NetworkReplicationGridNode::AddObject(NetworkReplicationHierarchyObject obj) +{ + // Chunk actors locations into a grid coordinates + Int3 coord = Int3::Zero; + if (const Actor* actor = obj.GetActor()) + { + coord = actor->GetPosition() / CellSize; + } + + Cell* cell = _children.TryGet(coord); + if (!cell) + { + // Allocate new cell + cell = &_children[coord]; + cell->Node = New(); + cell->MinCullDistance = obj.CullDistance; + } + cell->Node->AddObject(obj); + + // Cache minimum culling distance for a whole cell to skip it at once + cell->MinCullDistance = Math::Min(cell->MinCullDistance, obj.CullDistance); +} + +bool NetworkReplicationGridNode::RemoveObject(ScriptingObject* obj) +{ + for (const auto& e : _children) + { + if (e.Value.Node->RemoveObject(obj)) + { + // TODO: remove empty cells? + // TODO: update MinCullDistance for cell? + return true; + } + } + return false; +} + +void NetworkReplicationGridNode::Update(NetworkReplicationHierarchyUpdateResult* result) +{ + CHECK(result); + if (result->_clientsHaveLocation) + { + // Update only cells within a range + const Real cellRadiusSq = Math::Square(CellSize * 1.414f); + for (const auto& e : _children) + { + const Vector3 cellPosition = (e.Key * CellSize) + (CellSize * 0.5f); + Real distanceSq = MAX_Real; + for (auto& client : result->_clients) + { + if (client.HasLocation) + distanceSq = Math::Min(distanceSq, Vector3::DistanceSquared(cellPosition, client.Location)); + } + const Real minCullDistanceSq = Math::Square(e.Value.MinCullDistance); + if (distanceSq < minCullDistanceSq + cellRadiusSq) + { + e.Value.Node->Update(result); + } + } + } + else + { + // Brute-force over all cells + for (const auto& e : _children) + { + e.Value.Node->Update(result); + } + } +} diff --git a/Source/Engine/Networking/NetworkReplicationHierarchy.cs b/Source/Engine/Networking/NetworkReplicationHierarchy.cs new file mode 100644 index 000000000..f03419275 --- /dev/null +++ b/Source/Engine/Networking/NetworkReplicationHierarchy.cs @@ -0,0 +1,25 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +namespace FlaxEngine.Networking +{ + partial struct NetworkReplicationHierarchyObject + { + /// + /// Gets the actors context (object itself or parent actor). + /// + public Actor Actor + { + get + { + var actor = Object as Actor; + if (actor == null) + { + var sceneObject = Object as SceneObject; + if (sceneObject != null) + actor = sceneObject.Parent; + } + return actor; + } + } + } +} diff --git a/Source/Engine/Networking/NetworkReplicationHierarchy.h b/Source/Engine/Networking/NetworkReplicationHierarchy.h new file mode 100644 index 000000000..55cd6b7b1 --- /dev/null +++ b/Source/Engine/Networking/NetworkReplicationHierarchy.h @@ -0,0 +1,260 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Types.h" +#include "Engine/Core/Math/Vector3.h" +#include "Engine/Core/Collections/Array.h" +#include "Engine/Core/Collections/Dictionary.h" +#include "Engine/Scripting/ScriptingObject.h" +#include "Engine/Scripting/ScriptingObjectReference.h" + +class Actor; + +/// +/// Network replication hierarchy object data. +/// +API_STRUCT(NoDefault, Namespace = "FlaxEngine.Networking") struct FLAXENGINE_API NetworkReplicationHierarchyObject +{ + DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkReplicationObjectInfo); + + // The object to replicate. + API_FIELD() ScriptingObjectReference Object; + // The target amount of the replication updates per second (frequency of the replication). Constrained by NetworkManager::NetworkFPS. Use 0 for 'always relevant' object. + API_FIELD() float ReplicationFPS = 60; + // The minimum distance from the player to the object at which it can process replication. For example, players further away won't receive object data. Use 0 if unused. + API_FIELD() float CullDistance = 15000; + // Runtime value for update frames left for the next replication of this object. Matches NetworkManager::NetworkFPS calculated from ReplicationFPS. + API_FIELD(Attributes="HideInEditor") uint16 ReplicationUpdatesLeft = 0; + + FORCE_INLINE NetworkReplicationHierarchyObject(const ScriptingObjectReference& obj) + : Object(obj.Get()) + { + } + + FORCE_INLINE NetworkReplicationHierarchyObject(ScriptingObject* obj = nullptr) + : Object(obj) + { + } + + // Gets the actors context (object itself or parent actor). + Actor* GetActor() const; + + bool operator==(const NetworkReplicationHierarchyObject& other) const + { + return Object == other.Object; + } + + bool operator==(const ScriptingObject* other) const + { + return Object == other; + } +}; + +inline uint32 GetHash(const NetworkReplicationHierarchyObject& key) +{ + return GetHash(key.Object); +} + +/// +/// Bit mask for NetworkClient list (eg. to selectively send object replication). +/// +API_STRUCT(NoDefault, Namespace = "FlaxEngine.Networking") struct FLAXENGINE_API NetworkClientsMask +{ + DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkClientsMask); + + // The first 64 bits (each for one client). + API_FIELD() uint64 Word0 = 0; + // The second 64 bits (each for one client). + API_FIELD() uint64 Word1 = 0; + + // All bits set for all clients. + API_FIELD() static NetworkClientsMask All; + + FORCE_INLINE bool HasBit(int32 bitIndex) const + { + const int32 wordIndex = bitIndex / 64; + const uint64 wordMask = 1ull << (uint64)(bitIndex - wordIndex * 64); + const uint64 word = *(&Word0 + wordIndex); + return (word & wordMask) == wordMask; + } + + FORCE_INLINE void SetBit(int32 bitIndex) + { + const int32 wordIndex = bitIndex / 64; + const uint64 wordMask = 1ull << (uint64)(bitIndex - wordIndex * 64); + uint64& word = *(&Word0 + wordIndex); + word |= wordMask; + } + + FORCE_INLINE void UnsetBit(int32 bitIndex) + { + const int32 wordIndex = bitIndex / 64; + const uint64 wordMask = 1ull << (uint64)(bitIndex - wordIndex * 64); + uint64& word = *(&Word0 + wordIndex); + word &= ~wordMask; + } + + FORCE_INLINE operator bool() const + { + return Word0 + Word1 != 0; + } + + bool operator==(const NetworkClientsMask& other) const + { + return Word0 == other.Word0 && Word1 == other.Word1; + } +}; + +/// +/// Network replication hierarchy output data to send. +/// +API_CLASS(Namespace = "FlaxEngine.Networking") class FLAXENGINE_API NetworkReplicationHierarchyUpdateResult : public ScriptingObject +{ + DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(NetworkReplicationHierarchyUpdateResult, ScriptingObject); + friend class NetworkInternal; + friend class NetworkReplicationNode; + friend class NetworkReplicationGridNode; + +private: + struct Client + { + bool HasLocation; + Vector3 Location; + }; + + struct Entry + { + ScriptingObject* Object; + NetworkClientsMask TargetClients; + }; + + bool _clientsHaveLocation; + NetworkClientsMask _clientsMask; + Array _clients; + Array _entries; + + void Init(); + +public: + // Scales the ReplicationFPS property of objects in hierarchy. Can be used to slow down or speed up replication rate. + API_FIELD() float ReplicationScale = 1.0f; + + // Adds object to the update results. + API_FUNCTION() void AddObject(ScriptingObject* obj) + { + Entry& e = _entries.AddOne(); + e.Object = obj; + e.TargetClients = NetworkClientsMask::All; + } + + // Adds object to the update results. Defines specific clients to receive the update (server-only, unused on client). Mask matches NetworkManager::Clients. + API_FUNCTION() void AddObject(ScriptingObject* obj, NetworkClientsMask targetClients) + { + Entry& e = _entries.AddOne(); + e.Object = obj; + e.TargetClients = targetClients; + } + + // Gets amount of the clients to use. Matches NetworkManager::Clients. + API_PROPERTY() int32 GetClientsCount() const + { + return _clients.Count(); + } + + // Gets mask with all client bits set. Matches NetworkManager::Clients. + API_PROPERTY() NetworkClientsMask GetClientsMask() const + { + return _clientsMask; + } + + // Sets the viewer location for a certain client. Client index must match NetworkManager::Clients. + API_FUNCTION() void SetClientLocation(int32 clientIndex, const Vector3& location); + + // Gets the viewer location for a certain client. Client index must match NetworkManager::Clients. Returns true if got a location set, otherwise false. + API_FUNCTION() bool GetClientLocation(int32 clientIndex, API_PARAM(out) Vector3& location) const; +}; + +/// +/// Base class for the network objects replication hierarchy nodes. Contains a list of objects. +/// +API_CLASS(Abstract, Namespace = "FlaxEngine.Networking") class FLAXENGINE_API NetworkReplicationNode : public ScriptingObject +{ + DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(NetworkReplicationNode, ScriptingObject); + + /// + /// List with objects stored in this node. + /// + API_FIELD() Array Objects; + + /// + /// Adds an object into the hierarchy. + /// + /// The object to add. + API_FUNCTION() virtual void AddObject(NetworkReplicationHierarchyObject obj); + + /// + /// Removes object from the hierarchy. + /// + /// The object to remove. + /// True on successful removal, otherwise false. + API_FUNCTION() virtual bool RemoveObject(ScriptingObject* obj); + + /// + /// Force replicates the object during the next update. Resets any internal tracking state to force the synchronization. + /// + /// The object to update. + /// True on successful update, otherwise false. + API_FUNCTION() virtual bool DirtyObject(ScriptingObject* obj); + + /// + /// Iterates over all objects and adds them to the replication work. + /// + /// The update results container. + API_FUNCTION() virtual void Update(NetworkReplicationHierarchyUpdateResult* result); +}; + +inline uint32 GetHash(const Int3& key) +{ + uint32 hash = GetHash(key.X); + CombineHash(hash, GetHash(key.Y)); + CombineHash(hash, GetHash(key.Z)); + return hash; +} + +/// +/// Network replication hierarchy node with 3D grid spatialization. Organizes static objects into chunks to improve performance in large worlds. +/// +API_CLASS(Namespace = "FlaxEngine.Networking") class FLAXENGINE_API NetworkReplicationGridNode : public NetworkReplicationNode +{ + DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(NetworkReplicationGridNode, NetworkReplicationNode); + ~NetworkReplicationGridNode(); + +private: + struct Cell + { + NetworkReplicationNode* Node; + float MinCullDistance; + }; + + Dictionary _children; + +public: + /// + /// Size of the grid cell (in world units). Used to chunk the space for separate nodes. + /// + API_FIELD() float CellSize = 10000.0f; + + void AddObject(NetworkReplicationHierarchyObject obj) override; + bool RemoveObject(ScriptingObject* obj) override; + void Update(NetworkReplicationHierarchyUpdateResult* result) override; +}; + +/// +/// Defines the network objects replication hierarchy (tree structure) that controls chunking and configuration of the game objects replication. +/// Contains only 'owned' objects. It's used by the networking system only on a main thread. +/// +API_CLASS(Namespace = "FlaxEngine.Networking") class FLAXENGINE_API NetworkReplicationHierarchy : public NetworkReplicationNode +{ + DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(NetworkReplicationHierarchy, NetworkReplicationNode); +}; diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp index a61d7577f..c9a4773f0 100644 --- a/Source/Engine/Networking/NetworkReplicator.cpp +++ b/Source/Engine/Networking/NetworkReplicator.cpp @@ -12,6 +12,7 @@ #include "NetworkRpc.h" #include "INetworkSerializable.h" #include "INetworkObject.h" +#include "NetworkReplicationHierarchy.h" #include "Engine/Core/Collections/HashSet.h" #include "Engine/Core/Collections/Dictionary.h" #include "Engine/Core/Collections/ChunkedArray.h" @@ -108,12 +109,14 @@ struct NetworkReplicatedObject uint32 LastOwnerFrame = 0; NetworkObjectRole Role; uint8 Spawned : 1; + uint8 Synced : 1; DataContainer TargetClientIds; INetworkObject* AsNetworkObject; NetworkReplicatedObject() { Spawned = 0; + Synced = 0; } bool operator==(const NetworkReplicatedObject& other) const @@ -199,6 +202,8 @@ namespace Dictionary IdsRemappingTable; NetworkStream* CachedWriteStream = nullptr; NetworkStream* CachedReadStream = nullptr; + NetworkReplicationHierarchyUpdateResult* CachedReplicationResult = nullptr; + NetworkReplicationHierarchy* Hierarchy = nullptr; Array NewClients; Array CachedTargets; Dictionary SerializersTable; @@ -307,14 +312,15 @@ void BuildCachedTargets(const Array& clients, const NetworkClien } } -void BuildCachedTargets(const Array& clients, const DataContainer& clientIds, const uint32 excludedClientId = NetworkManager::ServerClientId) +void BuildCachedTargets(const Array& clients, const DataContainer& clientIds, const uint32 excludedClientId = NetworkManager::ServerClientId, const NetworkClientsMask clientsMask = NetworkClientsMask::All) { CachedTargets.Clear(); if (clientIds.IsValid()) { - for (const NetworkClient* client : clients) + for (int32 clientIndex = 0; clientIndex < clients.Count(); clientIndex++) { - if (client->State == NetworkConnectionState::Connected && client->ClientId != excludedClientId) + const NetworkClient* client = clients.Get()[clientIndex]; + if (client->State == NetworkConnectionState::Connected && client->ClientId != excludedClientId && clientsMask.HasBit(clientIndex)) { for (int32 i = 0; i < clientIds.Length(); i++) { @@ -329,9 +335,10 @@ void BuildCachedTargets(const Array& clients, const DataContaine } else { - for (const NetworkClient* client : clients) + for (int32 clientIndex = 0; clientIndex < clients.Count(); clientIndex++) { - if (client->State == NetworkConnectionState::Connected && client->ClientId != excludedClientId) + const NetworkClient* client = clients.Get()[clientIndex]; + if (client->State == NetworkConnectionState::Connected && client->ClientId != excludedClientId && clientsMask.HasBit(clientIndex)) CachedTargets.Add(client->Connection); } } @@ -377,10 +384,10 @@ void BuildCachedTargets(const Array& clients, const DataContaine } } -FORCE_INLINE void BuildCachedTargets(const NetworkReplicatedObject& item) +FORCE_INLINE void BuildCachedTargets(const NetworkReplicatedObject& item, const NetworkClientsMask clientsMask = NetworkClientsMask::All) { // By default send object to all connected clients excluding the owner but with optional TargetClientIds list - BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds, item.OwnerClientId); + BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds, item.OwnerClientId, clientsMask); } FORCE_INLINE void GetNetworkName(char buffer[128], const StringAnsiView& name) @@ -561,9 +568,10 @@ void FindObjectsForSpawn(SpawnGroup& group, ChunkedArray& spawnI } } -void DirtyObjectImpl(NetworkReplicatedObject& item, ScriptingObject* obj) +FORCE_INLINE void DirtyObjectImpl(NetworkReplicatedObject& item, ScriptingObject* obj) { - // TODO: implement objects state replication frequency and dirtying + if (Hierarchy) + Hierarchy->DirtyObject(obj); } template @@ -631,7 +639,14 @@ void InvokeObjectReplication(NetworkReplicatedObject& item, uint32 ownerFrame, b } if (item.AsNetworkObject) + { item.AsNetworkObject->OnNetworkDeserialize(); + if (!item.Synced) + { + item.Synced = true; + item.AsNetworkObject->OnNetworkSync(); + } + } // Speed up replication of client-owned objects to other clients from server to reduce lag (data has to go from client to server and then to other clients) if (NetworkManager::IsServer()) @@ -703,6 +718,34 @@ StringAnsiView NetworkReplicator::GetCSharpCachedName(const StringAnsiView& name #endif +NetworkReplicationHierarchy* NetworkReplicator::GetHierarchy() +{ + return Hierarchy; +} + +void NetworkReplicator::SetHierarchy(NetworkReplicationHierarchy* value) +{ + ScopeLock lock(ObjectsLock); + if (Hierarchy == value) + return; + NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Set hierarchy to '{}'", value ? value->ToString() : String::Empty); + if (Hierarchy) + { + // Clear old hierarchy + Delete(Hierarchy); + } + Hierarchy = value; + if (value) + { + // Add all owned objects to the hierarchy + for (auto& e : Objects) + { + if (e.Item.Object && e.Item.Role == NetworkObjectRole::OwnedAuthoritative) + value->AddObject(e.Item.Object); + } + } +} + void NetworkReplicator::AddSerializer(const ScriptingTypeHandle& typeHandle, SerializeFunc serialize, SerializeFunc deserialize, void* serializeTag, void* deserializeTag) { if (!typeHandle) @@ -745,7 +788,7 @@ bool NetworkReplicator::InvokeSerializer(const ScriptingTypeHandle& typeHandle, return false; } -void NetworkReplicator::AddObject(ScriptingObject* obj, ScriptingObject* parent) +void NetworkReplicator::AddObject(ScriptingObject* obj, const ScriptingObject* parent) { if (!obj || NetworkManager::IsOffline()) return; @@ -774,7 +817,22 @@ void NetworkReplicator::AddObject(ScriptingObject* obj, ScriptingObject* parent) item.OwnerClientId = NetworkManager::ServerClientId; // Server owns objects by default item.Role = NetworkManager::IsClient() ? NetworkObjectRole::Replicated : NetworkObjectRole::OwnedAuthoritative; NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Add new object {}:{}, parent {}:{}", item.ToString(), obj->GetType().ToString(), item.ParentId.ToString(), parent ? parent->GetType().ToString() : String::Empty); + for (const SpawnItem& spawnItem : SpawnQueue) + { + if (spawnItem.HasOwnership && spawnItem.HierarchicalOwnership) + { + if (IsParentOf(obj, spawnItem.Object)) + { + // Inherit ownership + item.Role = spawnItem.Role; + item.OwnerClientId = spawnItem.OwnerClientId; + break; + } + } + } Objects.Add(MoveTemp(item)); + if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative) + Hierarchy->AddObject(obj); } void NetworkReplicator::RemoveObject(ScriptingObject* obj) @@ -788,6 +846,8 @@ void NetworkReplicator::RemoveObject(ScriptingObject* obj) // Remove object from the list NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remove object {}, owned by {}", obj->GetID().ToString(), it->Item.ParentId.ToString()); + if (Hierarchy && it->Item.Role == NetworkObjectRole::OwnedAuthoritative) + Hierarchy->RemoveObject(obj); Objects.Remove(it); } @@ -857,14 +917,33 @@ void NetworkReplicator::DespawnObject(ScriptingObject* obj) DespawnedObjects.Add(item.ObjectId); if (item.AsNetworkObject) item.AsNetworkObject->OnNetworkDespawn(); + if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative) + Hierarchy->RemoveObject(obj); Objects.Remove(it); DeleteNetworkObject(obj); } +bool NetworkReplicator::HasObject(const ScriptingObject* obj) +{ + if (obj) + { + ScopeLock lock(ObjectsLock); + const auto it = Objects.Find(obj->GetID()); + if (it != Objects.End()) + return true; + for (const SpawnItem& item : SpawnQueue) + { + if (item.Object == obj) + return true; + } + } + return false; +} + uint32 NetworkReplicator::GetObjectOwnerClientId(const ScriptingObject* obj) { uint32 id = NetworkManager::ServerClientId; - if (obj) + if (obj && NetworkManager::IsConnected()) { ScopeLock lock(ObjectsLock); const auto it = Objects.Find(obj->GetID()); @@ -878,9 +957,16 @@ uint32 NetworkReplicator::GetObjectOwnerClientId(const ScriptingObject* obj) { if (item.HasOwnership) id = item.OwnerClientId; +#if USE_NETWORK_REPLICATOR_LOG + return id; +#else break; +#endif } } +#if USE_NETWORK_REPLICATOR_LOG + NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to get ownership of unregistered network object {} ({})", obj->GetID(), obj->GetType().ToString()); +#endif } } return id; @@ -889,7 +975,7 @@ uint32 NetworkReplicator::GetObjectOwnerClientId(const ScriptingObject* obj) NetworkObjectRole NetworkReplicator::GetObjectRole(const ScriptingObject* obj) { NetworkObjectRole role = NetworkObjectRole::None; - if (obj) + if (obj && NetworkManager::IsConnected()) { ScopeLock lock(ObjectsLock); const auto it = Objects.Find(obj->GetID()); @@ -903,9 +989,16 @@ NetworkObjectRole NetworkReplicator::GetObjectRole(const ScriptingObject* obj) { if (item.HasOwnership) role = item.Role; +#if USE_NETWORK_REPLICATOR_LOG + return role; +#else break; +#endif } } +#if USE_NETWORK_REPLICATOR_LOG + NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to get ownership of unregistered network object {} ({})", obj->GetID(), obj->GetType().ToString()); +#endif } } return role; @@ -913,10 +1006,11 @@ NetworkObjectRole NetworkReplicator::GetObjectRole(const ScriptingObject* obj) void NetworkReplicator::SetObjectOwnership(ScriptingObject* obj, uint32 ownerClientId, NetworkObjectRole localRole, bool hierarchical) { - if (!obj) + if (!obj || NetworkManager::IsOffline()) return; + const Guid objectId = obj->GetID(); ScopeLock lock(ObjectsLock); - const auto it = Objects.Find(obj->GetID()); + const auto it = Objects.Find(objectId); if (it == Objects.End()) { // Special case if we're just spawning this object @@ -944,31 +1038,37 @@ void NetworkReplicator::SetObjectOwnership(ScriptingObject* obj, uint32 ownerCli break; } } - return; - } - auto& item = it->Item; - if (item.Object != obj) - return; - - // Check if this client is object owner - if (item.OwnerClientId == NetworkManager::LocalClientId) - { - // Check if object owner will change - if (item.OwnerClientId != ownerClientId) - { - // Change role locally - CHECK(localRole != NetworkObjectRole::OwnedAuthoritative); - item.OwnerClientId = ownerClientId; - item.LastOwnerFrame = 1; - item.Role = localRole; - SendObjectRoleMessage(item); - } } else { - // Allow to change local role of the object (except ownership) - CHECK(localRole != NetworkObjectRole::OwnedAuthoritative); - item.Role = localRole; + auto& item = it->Item; + if (item.Object != obj) + return; + + // Check if this client is object owner + if (item.OwnerClientId == NetworkManager::LocalClientId) + { + // Check if object owner will change + if (item.OwnerClientId != ownerClientId) + { + // Change role locally + CHECK(localRole != NetworkObjectRole::OwnedAuthoritative); + if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative) + Hierarchy->RemoveObject(obj); + item.OwnerClientId = ownerClientId; + item.LastOwnerFrame = 1; + item.Role = localRole; + SendObjectRoleMessage(item); + } + } + else + { + // Allow to change local role of the object (except ownership) + CHECK(localRole != NetworkObjectRole::OwnedAuthoritative); + if (Hierarchy && it->Item.Role == NetworkObjectRole::OwnedAuthoritative) + Hierarchy->RemoveObject(obj); + item.Role = localRole; + } } // Go down hierarchy @@ -976,9 +1076,15 @@ void NetworkReplicator::SetObjectOwnership(ScriptingObject* obj, uint32 ownerCli { for (auto& e : Objects) { - if (e.Item.ParentId == item.ObjectId) + if (e.Item.ParentId == objectId) SetObjectOwnership(e.Item.Object.Get(), ownerClientId, localRole, hierarchical); } + + for (const SpawnItem& spawnItem : SpawnQueue) + { + if (IsParentOf(spawnItem.Object, obj)) + SetObjectOwnership(spawnItem.Object, ownerClientId, localRole, hierarchical); + } } } @@ -1054,6 +1160,8 @@ void NetworkInternal::NetworkReplicatorClientDisconnected(NetworkClient* client) // Delete object locally NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Despawn object {}", item.ObjectId); + if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative) + Hierarchy->RemoveObject(obj); if (item.AsNetworkObject) item.AsNetworkObject->OnNetworkDespawn(); DeleteNetworkObject(obj); @@ -1068,6 +1176,7 @@ void NetworkInternal::NetworkReplicatorClear() // Cleanup NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Shutdown"); + NetworkReplicator::SetHierarchy(nullptr); for (auto it = Objects.Begin(); it.IsNotEnd(); ++it) { auto& item = it->Item; @@ -1087,6 +1196,7 @@ void NetworkInternal::NetworkReplicatorClear() IdsRemappingTable.Clear(); SAFE_DELETE(CachedWriteStream); SAFE_DELETE(CachedReadStream); + SAFE_DELETE(CachedReplicationResult); NewClients.Clear(); CachedTargets.Clear(); DespawnedObjects.Clear(); @@ -1182,9 +1292,11 @@ void NetworkInternal::NetworkReplicatorUpdate() { if (!q.HasOwnership && IsParentOf(q.Object, e.Object)) { + // Inherit ownership q.HasOwnership = true; q.Role = e.Role; q.OwnerClientId = e.OwnerClientId; + break; } } } @@ -1213,7 +1325,14 @@ void NetworkInternal::NetworkReplicatorUpdate() if (e.HasOwnership) { - item.Role = e.Role; + if (item.Role != e.Role) + { + if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative) + Hierarchy->RemoveObject(obj); + item.Role = e.Role; + if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative) + Hierarchy->AddObject(obj); + } item.OwnerClientId = e.OwnerClientId; if (e.HierarchicalOwnership) NetworkReplicator::SetObjectOwnership(obj, e.OwnerClientId, e.Role, true); @@ -1247,65 +1366,104 @@ void NetworkInternal::NetworkReplicatorUpdate() } // Apply parts replication - for (int32 i = ReplicationParts.Count() - 1; i >= 0; i--) { - auto& e = ReplicationParts[i]; - if (e.PartsLeft > 0) + PROFILE_CPU_NAMED("ReplicationParts"); + for (int32 i = ReplicationParts.Count() - 1; i >= 0; i--) { - // TODO: remove replication items after some TTL to prevent memory leaks - continue; - } - ScriptingObject* obj = e.Object.Get(); - if (obj) - { - auto it = Objects.Find(obj->GetID()); - if (it != Objects.End()) + auto& e = ReplicationParts[i]; + if (e.PartsLeft > 0) { - auto& item = it->Item; - - // Replicate from all collected parts data - InvokeObjectReplication(item, e.OwnerFrame, e.Data.Get(), e.Data.Count(), e.OwnerClientId); + // TODO: remove replication items after some TTL to prevent memory leaks + continue; } - } + ScriptingObject* obj = e.Object.Get(); + if (obj) + { + auto it = Objects.Find(obj->GetID()); + if (it != Objects.End()) + { + auto& item = it->Item; - ReplicationParts.RemoveAt(i); + // Replicate from all collected parts data + InvokeObjectReplication(item, e.OwnerFrame, e.Data.Get(), e.Data.Count(), e.OwnerClientId); + } + } + + ReplicationParts.RemoveAt(i); + } } - // Brute force synchronize all networked objects with clients - if (CachedWriteStream == nullptr) - CachedWriteStream = New(); - NetworkStream* stream = CachedWriteStream; - stream->SenderId = NetworkManager::LocalClientId; - // TODO: introduce NetworkReplicationHierarchy to optimize objects replication in large worlds (eg. batched culling networked scene objects that are too far from certain client to be relevant) - // TODO: per-object sync interval (in frames) - could be scaled by hierarchy (eg. game could slow down sync rate for objects far from player) - for (auto it = Objects.Begin(); it.IsNotEnd(); ++it) + // Replicate all owned networked objects with other clients or server + if (!CachedReplicationResult) + CachedReplicationResult = New(); + CachedReplicationResult->Init(); + if (!isClient && NetworkManager::Clients.IsEmpty()) { - auto& item = it->Item; - ScriptingObject* obj = item.Object.Get(); - if (!obj) + // No need to update replication when nobody's around + } + else if (Hierarchy) + { + // Tick using hierarchy + PROFILE_CPU_NAMED("ReplicationHierarchyUpdate"); + Hierarchy->Update(CachedReplicationResult); + } + else + { + // Tick all owned objects + PROFILE_CPU_NAMED("ReplicationUpdate"); + for (auto it = Objects.Begin(); it.IsNotEnd(); ++it) { - // Object got deleted - NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remove object {}, owned by {}", item.ToString(), item.ParentId.ToString()); - Objects.Remove(it); - continue; + auto& item = it->Item; + ScriptingObject* obj = item.Object.Get(); + if (!obj) + { + // Object got deleted + NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remove object {}, owned by {}", item.ToString(), item.ParentId.ToString()); + Objects.Remove(it); + continue; + } + if (item.Role != NetworkObjectRole::OwnedAuthoritative) + continue; // Send replication messages of only owned objects or from other client objects + CachedReplicationResult->AddObject(obj); } - if (item.Role != NetworkObjectRole::OwnedAuthoritative && (!isClient && item.OwnerClientId != NetworkManager::LocalClientId)) - continue; // Send replication messages of only owned objects or from other client objects - - if (item.AsNetworkObject) - item.AsNetworkObject->OnNetworkSerialize(); - - // Serialize object - stream->Initialize(); - const bool failed = NetworkReplicator::InvokeSerializer(obj->GetTypeHandle(), obj, stream, true); - if (failed) + } + if (CachedReplicationResult->_entries.HasItems()) + { + PROFILE_CPU_NAMED("Replication"); + if (CachedWriteStream == nullptr) + CachedWriteStream = New(); + NetworkStream* stream = CachedWriteStream; + stream->SenderId = NetworkManager::LocalClientId; + // TODO: use Job System when replicated objects count is large + for (auto& e : CachedReplicationResult->_entries) { - //NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Cannot serialize object {} of type {} (missing serialization logic)", item.ToString(), obj->GetType().ToString()); - continue; - } + ScriptingObject* obj = e.Object; + auto it = Objects.Find(obj->GetID()); + if (it.IsEnd()) + continue; + auto& item = it->Item; - // Send object to clients - { + // Skip serialization of objects that none will receive + if (!isClient) + { + BuildCachedTargets(item, e.TargetClients); + if (CachedTargets.Count() == 0) + continue; + } + + if (item.AsNetworkObject) + item.AsNetworkObject->OnNetworkSerialize(); + + // Serialize object + stream->Initialize(); + const bool failed = NetworkReplicator::InvokeSerializer(obj->GetTypeHandle(), obj, stream, true); + if (failed) + { + //NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Cannot serialize object {} of type {} (missing serialization logic)", item.ToString(), obj->GetType().ToString()); + continue; + } + + // Send object to clients const uint32 size = stream->GetPosition(); ASSERT(size <= MAX_uint16) NetworkMessageObjectReplicate msgData; @@ -1344,11 +1502,7 @@ void NetworkInternal::NetworkReplicatorUpdate() if (isClient) peer->EndSendMessage(NetworkChannelType::Unreliable, msg); else - { - // TODO: per-object relevancy for connected clients (eg. skip replicating actor to far players) - BuildCachedTargets(item); peer->EndSendMessage(NetworkChannelType::Unreliable, msg, CachedTargets); - } // Send all other parts for (uint32 partIndex = 1; partIndex < partsCount; partIndex++) @@ -1376,49 +1530,52 @@ void NetworkInternal::NetworkReplicatorUpdate() } // Invoke RPCs - for (auto& e : RpcQueue) { - ScriptingObject* obj = e.Object.Get(); - if (!obj) - continue; - auto it = Objects.Find(obj->GetID()); - if (it == Objects.End()) - continue; - auto& item = it->Item; + PROFILE_CPU_NAMED("Rpc"); + for (auto& e : RpcQueue) + { + ScriptingObject* obj = e.Object.Get(); + if (!obj) + continue; + auto it = Objects.Find(obj->GetID()); + if (it == Objects.End()) + continue; + auto& item = it->Item; - // Send RPC message - //NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Rpc {}::{} object ID={}", e.Name.First.ToString(), String(e.Name.Second), item.ToString()); - NetworkMessageObjectRpc msgData; - msgData.ObjectId = item.ObjectId; - if (isClient) - { - // Remap local client object ids into server ids - IdsRemappingTable.KeyOf(msgData.ObjectId, &msgData.ObjectId); - } - GetNetworkName(msgData.RpcTypeName, e.Name.First.GetType().Fullname); - GetNetworkName(msgData.RpcName, e.Name.Second); - msgData.ArgsSize = (uint16)e.ArgsData.Length(); - NetworkMessage msg = peer->BeginSendMessage(); - msg.WriteStructure(msgData); - msg.WriteBytes(e.ArgsData.Get(), e.ArgsData.Length()); - NetworkChannelType channel = (NetworkChannelType)e.Info.Channel; - if (e.Info.Server && isClient) - { - // Client -> Server + // Send RPC message + //NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Rpc {}::{} object ID={}", e.Name.First.ToString(), String(e.Name.Second), item.ToString()); + NetworkMessageObjectRpc msgData; + msgData.ObjectId = item.ObjectId; + if (isClient) + { + // Remap local client object ids into server ids + IdsRemappingTable.KeyOf(msgData.ObjectId, &msgData.ObjectId); + } + GetNetworkName(msgData.RpcTypeName, e.Name.First.GetType().Fullname); + GetNetworkName(msgData.RpcName, e.Name.Second); + msgData.ArgsSize = (uint16)e.ArgsData.Length(); + NetworkMessage msg = peer->BeginSendMessage(); + msg.WriteStructure(msgData); + msg.WriteBytes(e.ArgsData.Get(), e.ArgsData.Length()); + NetworkChannelType channel = (NetworkChannelType)e.Info.Channel; + if (e.Info.Server && isClient) + { + // Client -> Server #if USE_NETWORK_REPLICATOR_LOG - if (e.Targets.Length() != 0) - NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Server RPC '{}::{}' called with non-empty list of targets is not supported (only server will receive it)", e.Name.First.ToString(), e.Name.Second.ToString()); + if (e.Targets.Length() != 0) + NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Server RPC '{}::{}' called with non-empty list of targets is not supported (only server will receive it)", e.Name.First.ToString(), e.Name.Second.ToString()); #endif - peer->EndSendMessage(channel, msg); - } - else if (e.Info.Client && (isServer || isHost)) - { - // Server -> Client(s) - BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds, e.Targets, NetworkManager::LocalClientId); - peer->EndSendMessage(channel, msg, CachedTargets); + peer->EndSendMessage(channel, msg); + } + else if (e.Info.Client && (isServer || isHost)) + { + // Server -> Client(s) + BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds, e.Targets, NetworkManager::LocalClientId); + peer->EndSendMessage(channel, msg, CachedTargets); + } } + RpcQueue.Clear(); } - RpcQueue.Clear(); // Clear networked objects mapping table Scripting::ObjectsLookupIdMapping.Set(nullptr); @@ -1426,6 +1583,7 @@ void NetworkInternal::NetworkReplicatorUpdate() void NetworkInternal::OnNetworkMessageObjectReplicate(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer) { + PROFILE_CPU(); NetworkMessageObjectReplicate msgData; event.Message.ReadStructure(msgData); ScopeLock lock(ObjectsLock); @@ -1457,6 +1615,7 @@ void NetworkInternal::OnNetworkMessageObjectReplicate(NetworkEvent& event, Netwo void NetworkInternal::OnNetworkMessageObjectReplicatePart(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer) { + PROFILE_CPU(); NetworkMessageObjectReplicatePart msgData; event.Message.ReadStructure(msgData); ScopeLock lock(ObjectsLock); @@ -1469,6 +1628,7 @@ void NetworkInternal::OnNetworkMessageObjectReplicatePart(NetworkEvent& event, N void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer) { + PROFILE_CPU(); NetworkMessageObjectSpawn msgData; event.Message.ReadStructure(msgData); auto* msgDataItems = (NetworkMessageObjectSpawnItem*)event.Message.SkipBytes(msgData.ItemsCount * sizeof(NetworkMessageObjectSpawnItem)); @@ -1493,7 +1653,11 @@ void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkCl // Server always knows the best so update ownership of the existing object item.OwnerClientId = msgData.OwnerClientId; if (item.Role == NetworkObjectRole::OwnedAuthoritative) + { + if (Hierarchy) + Hierarchy->AddObject(item.Object); item.Role = NetworkObjectRole::Replicated; + } } else if (item.OwnerClientId != msgData.OwnerClientId) { @@ -1623,7 +1787,7 @@ void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkCl } } - // Setup all newly spawned objects + // Add all newly spawned objects for (int32 i = 0; i < msgData.ItemsCount; i++) { auto& msgDataItem = msgDataItems[i]; @@ -1648,10 +1812,22 @@ void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkCl item.Spawned = true; NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Add new object {}:{}, parent {}:{}", item.ToString(), obj->GetType().ToString(), item.ParentId.ToString(), parent ? parent->Object->GetType().ToString() : String::Empty); Objects.Add(MoveTemp(item)); + if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative) + Hierarchy->AddObject(obj); // Boost future lookups by using indirection NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remap object ID={} into object {}:{}", msgDataItem.ObjectId, item.ToString(), obj->GetType().ToString()); IdsRemappingTable.Add(msgDataItem.ObjectId, item.ObjectId); + } + + // Spawn all newly spawned objects (ensure to have valid ownership hierarchy set before spawning object) + for (int32 i = 0; i < msgData.ItemsCount; i++) + { + auto& msgDataItem = msgDataItems[i]; + ScriptingObject* obj = objects[i]; + auto it = Objects.Find(obj->GetID()); + auto& item = it->Item; + const NetworkReplicatedObject* parent = ResolveObject(msgDataItem.ParentId); // Automatic parenting for scene objects auto sceneObject = ScriptingObject::Cast(obj); @@ -1666,7 +1842,7 @@ void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkCl #if USE_NETWORK_REPLICATOR_LOG // Ignore case when parent object in a message was a scene (eg. that is already unloaded on a client) AssetInfo assetInfo; - if (!Content::GetAssetInfo(msgDataItem.ParentId, assetInfo) || assetInfo.TypeName == TEXT("FlaxEngine.SceneAsset")) + if (!Content::GetAssetInfo(msgDataItem.ParentId, assetInfo) || assetInfo.TypeName != TEXT("FlaxEngine.SceneAsset")) { NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} as parent to spawned object", msgDataItem.ParentId.ToString()); } @@ -1687,6 +1863,7 @@ void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkCl void NetworkInternal::OnNetworkMessageObjectDespawn(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer) { + PROFILE_CPU(); NetworkMessageObjectDespawn msgData; event.Message.ReadStructure(msgData); ScopeLock lock(ObjectsLock); @@ -1704,6 +1881,8 @@ void NetworkInternal::OnNetworkMessageObjectDespawn(NetworkEvent& event, Network // Remove object NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Despawn object {}", msgData.ObjectId); + if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative) + Hierarchy->RemoveObject(obj); DespawnedObjects.Add(msgData.ObjectId); if (item.AsNetworkObject) item.AsNetworkObject->OnNetworkDespawn(); @@ -1718,6 +1897,7 @@ void NetworkInternal::OnNetworkMessageObjectDespawn(NetworkEvent& event, Network void NetworkInternal::OnNetworkMessageObjectRole(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer) { + PROFILE_CPU(); NetworkMessageObjectRole msgData; event.Message.ReadStructure(msgData); ScopeLock lock(ObjectsLock); @@ -1739,12 +1919,16 @@ void NetworkInternal::OnNetworkMessageObjectRole(NetworkEvent& event, NetworkCli if (item.OwnerClientId == NetworkManager::LocalClientId) { // Upgrade ownership automatically + if (Hierarchy && item.Role != NetworkObjectRole::OwnedAuthoritative) + Hierarchy->AddObject(obj); item.Role = NetworkObjectRole::OwnedAuthoritative; item.LastOwnerFrame = 0; } else if (item.Role == NetworkObjectRole::OwnedAuthoritative) { // Downgrade ownership automatically + if (Hierarchy) + Hierarchy->RemoveObject(obj); item.Role = NetworkObjectRole::Replicated; } if (!NetworkManager::IsClient()) @@ -1761,6 +1945,7 @@ void NetworkInternal::OnNetworkMessageObjectRole(NetworkEvent& event, NetworkCli void NetworkInternal::OnNetworkMessageObjectRpc(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer) { + PROFILE_CPU(); NetworkMessageObjectRpc msgData; event.Message.ReadStructure(msgData); ScopeLock lock(ObjectsLock); diff --git a/Source/Engine/Networking/NetworkReplicator.h b/Source/Engine/Networking/NetworkReplicator.h index 2ab97a51b..156e7eeea 100644 --- a/Source/Engine/Networking/NetworkReplicator.h +++ b/Source/Engine/Networking/NetworkReplicator.h @@ -42,6 +42,17 @@ public: API_FIELD() static bool EnableLog; #endif + /// + /// Gets the network replication hierarchy. + /// + API_PROPERTY() static NetworkReplicationHierarchy* GetHierarchy(); + + /// + /// Sets the network replication hierarchy. + /// + API_PROPERTY() static void SetHierarchy(NetworkReplicationHierarchy* value); + +public: /// /// Adds the network replication serializer for a given type. /// @@ -68,7 +79,7 @@ public: /// Does nothing if network is offline. /// The object to replicate. /// The parent of the object (eg. player that spawned it). - API_FUNCTION() static void AddObject(ScriptingObject* obj, ScriptingObject* parent = nullptr); + API_FUNCTION() static void AddObject(ScriptingObject* obj, const ScriptingObject* parent = nullptr); /// /// Removes the object from the network replication system. @@ -80,14 +91,14 @@ public: /// /// Spawns the object to the other clients. Can be spawned by the owner who locally created it (eg. from prefab). /// - /// Does nothing if network is offline. + /// Does nothing if network is offline. Doesn't spawn actor in a level - but in network replication system. /// The object to spawn on other clients. API_FUNCTION() static void SpawnObject(ScriptingObject* obj); /// /// Spawns the object to the other clients. Can be spawned by the owner who locally created it (eg. from prefab). /// - /// Does nothing if network is offline. + /// Does nothing if network is offline. Doesn't spawn actor in a level - but in network replication system. /// The object to spawn on other clients. /// List with network client IDs that should receive network spawn event. Empty to spawn on all clients. API_FUNCTION() static void SpawnObject(ScriptingObject* obj, const DataContainer& clientIds); @@ -99,6 +110,13 @@ public: /// The object to despawn on other clients. API_FUNCTION() static void DespawnObject(ScriptingObject* obj); + /// + /// Checks if the network object is spawned or added to the network replication system. + /// + /// The network object. + /// True if object exists in networking, otherwise false. + API_FUNCTION() static bool HasObject(const ScriptingObject* obj); + /// /// Gets the Client Id of the network object owner. /// diff --git a/Source/Engine/Networking/Types.h b/Source/Engine/Networking/Types.h index 11675023a..0d754e727 100644 --- a/Source/Engine/Networking/Types.h +++ b/Source/Engine/Networking/Types.h @@ -11,6 +11,7 @@ class INetworkSerializable; class NetworkPeer; class NetworkClient; class NetworkStream; +class NetworkReplicationHierarchy; struct NetworkEvent; struct NetworkConnection; diff --git a/Source/Engine/Particles/ParticleEffect.cpp b/Source/Engine/Particles/ParticleEffect.cpp index 4b7923439..18bcbdd54 100644 --- a/Source/Engine/Particles/ParticleEffect.cpp +++ b/Source/Engine/Particles/ParticleEffect.cpp @@ -253,6 +253,11 @@ int32 ParticleEffect::GetParticlesCount() const return Instance.GetParticlesCount(); } +bool ParticleEffect::GetIsPlaying() const +{ + return _isPlaying; +} + void ParticleEffect::ResetSimulation() { Instance.ClearState(); @@ -275,6 +280,22 @@ void ParticleEffect::UpdateSimulation(bool singleFrame) Particles::UpdateEffect(this); } +void ParticleEffect::Play() +{ + _isPlaying = true; +} + +void ParticleEffect::Pause() +{ + _isPlaying = false; +} + +void ParticleEffect::Stop() +{ + _isPlaying = false; + ResetSimulation(); +} + void ParticleEffect::UpdateBounds() { BoundingBox bounds = BoundingBox::Empty; @@ -395,6 +416,13 @@ void ParticleEffect::SetParametersOverrides(const Array& valu void ParticleEffect::Update() { + if (!_isPlaying) + { + // Move update timer forward while paused for correct delta time after unpause + Instance.LastUpdateTime = (UseTimeScale ? Time::Update.Time : Time::Update.UnscaledTime).GetTotalSeconds(); + return; + } + // Skip if off-screen if (!UpdateWhenOffscreen && _lastMinDstSqr >= MAX_Real) return; @@ -416,8 +444,12 @@ void ParticleEffect::Update() void ParticleEffect::UpdateExecuteInEditor() { + // Auto-play in Editor if (!Editor::IsPlayMode) + { + _isPlaying = true; Update(); + } } #endif @@ -576,6 +608,7 @@ void ParticleEffect::Serialize(SerializeStream& stream, const void* otherObj) SERIALIZE(SimulationSpeed); SERIALIZE(UseTimeScale); SERIALIZE(IsLooping); + SERIALIZE(PlayOnStart); SERIALIZE(UpdateWhenOffscreen); SERIALIZE(DrawModes); SERIALIZE(SortOrder); @@ -589,6 +622,7 @@ void ParticleEffect::Deserialize(DeserializeStream& stream, ISerializeModifier* const auto overridesMember = stream.FindMember("Overrides"); if (overridesMember != stream.MemberEnd()) { + // [Deprecated on 25.11.2018, expires on 25.11.2022] if (modifier->EngineBuild < 6197) { const auto& overrides = overridesMember->value; @@ -675,6 +709,7 @@ void ParticleEffect::Deserialize(DeserializeStream& stream, ISerializeModifier* DESERIALIZE(SimulationSpeed); DESERIALIZE(UseTimeScale); DESERIALIZE(IsLooping); + DESERIALIZE(PlayOnStart); DESERIALIZE(UpdateWhenOffscreen); DESERIALIZE(DrawModes); DESERIALIZE(SortOrder); @@ -706,6 +741,9 @@ void ParticleEffect::OnEnable() GetScene()->Ticking.Update.AddTickExecuteInEditor(this); #endif + if (PlayOnStart) + Play(); + // Base Actor::OnEnable(); } diff --git a/Source/Engine/Particles/ParticleEffect.h b/Source/Engine/Particles/ParticleEffect.h index 3487a35f4..bfee23a3b 100644 --- a/Source/Engine/Particles/ParticleEffect.h +++ b/Source/Engine/Particles/ParticleEffect.h @@ -184,6 +184,7 @@ private: uint32 _parametersVersion = 0; // Version number for _parameters to be in sync with Instance.ParametersVersion Array _parameters; // Cached for scripting API Array _parametersOverrides; // Cached parameter modifications to be applied to the parameters + bool _isPlaying = false; public: /// @@ -235,9 +236,15 @@ public: bool IsLooping = true; /// - /// If true, the particle simulation will be updated even when an actor cannot be seen by any camera. Otherwise, the simulation will stop running when the actor is off-screen. + /// Determines whether the particle effect should play on start. /// API_FIELD(Attributes="EditorDisplay(\"Particle Effect\"), DefaultValue(true), EditorOrder(60)") + bool PlayOnStart = true; + + /// + /// If true, the particle simulation will be updated even when an actor cannot be seen by any camera. Otherwise, the simulation will stop running when the actor is off-screen. + /// + API_FIELD(Attributes="EditorDisplay(\"Particle Effect\"), DefaultValue(true), EditorOrder(70)") bool UpdateWhenOffscreen = true; /// @@ -326,6 +333,11 @@ public: /// API_PROPERTY() int32 GetParticlesCount() const; + /// + /// Gets whether or not the particle effect is playing. + /// + API_PROPERTY(Attributes="NoSerialize, HideInEditor") bool GetIsPlaying() const; + /// /// Resets the particles simulation state (clears the instance state data but preserves the instance parameters values). /// @@ -337,6 +349,21 @@ public: /// True if update animation by a single frame only (time time since last engine update), otherwise will update simulation with delta time since last update. API_FUNCTION() void UpdateSimulation(bool singleFrame = false); + /// + /// Plays the simulation. + /// + API_FUNCTION() void Play(); + + /// + /// Pauses the simulation. + /// + API_FUNCTION() void Pause(); + + /// + /// Stops and resets the simulation. + /// + API_FUNCTION() void Stop(); + /// /// Updates the actor bounds. /// diff --git a/Source/Engine/Platform/Apple/ApplePlatform.cpp b/Source/Engine/Platform/Apple/ApplePlatform.cpp index 4ad126ecb..8ae5a484f 100644 --- a/Source/Engine/Platform/Apple/ApplePlatform.cpp +++ b/Source/Engine/Platform/Apple/ApplePlatform.cpp @@ -329,15 +329,6 @@ void ApplePlatform::CreateGuid(Guid& result) result.D = ptr[3]; } -bool ApplePlatform::CanOpenUrl(const StringView& url) -{ - return false; -} - -void ApplePlatform::OpenUrl(const StringView& url) -{ -} - String ApplePlatform::GetExecutableFilePath() { char buf[PATH_MAX]; diff --git a/Source/Engine/Platform/Apple/ApplePlatform.h b/Source/Engine/Platform/Apple/ApplePlatform.h index fc2da0374..f1148d0b6 100644 --- a/Source/Engine/Platform/Apple/ApplePlatform.h +++ b/Source/Engine/Platform/Apple/ApplePlatform.h @@ -88,8 +88,6 @@ public: static String GetUserLocaleName(); static bool GetHasFocus(); static void CreateGuid(Guid& result); - static bool CanOpenUrl(const StringView& url); - static void OpenUrl(const StringView& url); static Float2 GetDesktopSize(); static String GetMainDirectory(); static String GetExecutableFilePath(); diff --git a/Source/Engine/Platform/Base/PlatformBase.cpp b/Source/Engine/Platform/Base/PlatformBase.cpp index 08e3bd53e..d15bec61c 100644 --- a/Source/Engine/Platform/Base/PlatformBase.cpp +++ b/Source/Engine/Platform/Base/PlatformBase.cpp @@ -5,6 +5,7 @@ #include "Engine/Platform/MemoryStats.h" #include "Engine/Platform/MessageBox.h" #include "Engine/Platform/FileSystem.h" +#include "Engine/Platform/Window.h" #include "Engine/Platform/User.h" #include "Engine/Core/Log.h" #include "Engine/Core/Types/DateTime.h" @@ -157,7 +158,7 @@ void PlatformBase::LogInfo() LOG(Info, "CPU package count: {0}, Core count: {1}, Logical processors: {2}", cpuInfo.ProcessorPackageCount, cpuInfo.ProcessorCoreCount, cpuInfo.LogicalProcessorCount); LOG(Info, "CPU Page size: {0}, cache line size: {1} bytes", Utilities::BytesToText(cpuInfo.PageSize), cpuInfo.CacheLineSize); LOG(Info, "L1 cache: {0}, L2 cache: {1}, L3 cache: {2}", Utilities::BytesToText(cpuInfo.L1CacheSize), Utilities::BytesToText(cpuInfo.L2CacheSize), Utilities::BytesToText(cpuInfo.L3CacheSize)); - LOG(Info, "Clock speed: {0} GHz", Utilities::RoundTo2DecimalPlaces(cpuInfo.ClockSpeed * 1e-9f)); + LOG(Info, "Clock speed: {0}", Utilities::HertzToText(cpuInfo.ClockSpeed)); const MemoryStats memStats = Platform::GetMemoryStats(); LOG(Info, "Physical Memory: {0} total, {1} used ({2}%)", Utilities::BytesToText(memStats.TotalPhysicalMemory), Utilities::BytesToText(memStats.UsedPhysicalMemory), Utilities::RoundTo2DecimalPlaces((float)memStats.UsedPhysicalMemory * 100.0f / (float)memStats.TotalPhysicalMemory)); @@ -520,6 +521,30 @@ void PlatformBase::CreateGuid(Guid& result) result = Guid(dateThingHigh, randomThing | (sequentialThing << 16), cyclesThing, dateThingLow); } +bool PlatformBase::CanOpenUrl(const StringView& url) +{ + return false; +} + +void PlatformBase::OpenUrl(const StringView& url) +{ +} + +Float2 PlatformBase::GetMousePosition() +{ + const Window* win = Engine::MainWindow; + if (win) + return win->ClientToScreen(win->GetMousePosition()); + return Float2::Minimum; +} + +void PlatformBase::SetMousePosition(const Float2& position) +{ + const Window* win = Engine::MainWindow; + if (win) + win->SetMousePosition(win->ScreenToClient(position)); +} + Rectangle PlatformBase::GetMonitorBounds(const Float2& screenPos) { return Rectangle(Float2::Zero, Platform::GetDesktopSize()); diff --git a/Source/Engine/Platform/Base/PlatformBase.h b/Source/Engine/Platform/Base/PlatformBase.h index d3bcb8dfb..2780d3fa4 100644 --- a/Source/Engine/Platform/Base/PlatformBase.h +++ b/Source/Engine/Platform/Base/PlatformBase.h @@ -167,7 +167,6 @@ DECLARE_SCRIPTING_TYPE_MINIMAL(PlatformBase); static void Exit(); public: - /// /// Copy memory region /// @@ -334,7 +333,6 @@ public: static void FreePages(void* ptr); public: - /// /// Returns the current runtime platform type. It's compile-time constant. /// @@ -409,7 +407,6 @@ public: static void Sleep(int32 milliseconds) = delete; public: - /// /// Gets the current time in seconds. /// @@ -455,7 +452,6 @@ public: static void GetUTCTime(int32& year, int32& month, int32& dayOfWeek, int32& day, int32& hour, int32& minute, int32& second, int32& millisecond) = delete; public: - /// /// Shows the fatal error message to the user. /// @@ -482,7 +478,6 @@ public: static void Info(const Char* msg); public: - /// /// Shows the fatal error message to the user. /// @@ -520,7 +515,6 @@ public: static bool IsDebuggerPresent(); public: - /// /// Performs a fatal crash. /// @@ -560,7 +554,6 @@ public: static void CheckFailed(const char* message, const char* file, int line); public: - /// /// Sets the High DPI awareness. /// @@ -628,7 +621,6 @@ public: static void CreateGuid(Guid& result); public: - /// /// The list of users. /// @@ -645,21 +637,31 @@ public: API_EVENT() static Delegate UserRemoved; public: - /// /// Returns a value indicating whether can open a given URL in a web browser. /// /// The URI to assign to web browser. /// True if can open URL, otherwise false. - API_FUNCTION() static bool CanOpenUrl(const StringView& url) = delete; + API_FUNCTION() static bool CanOpenUrl(const StringView& url); /// /// Launches a web browser and opens a given URL. /// /// The URI to assign to web browser. - API_FUNCTION() static void OpenUrl(const StringView& url) = delete; + API_FUNCTION() static void OpenUrl(const StringView& url); public: + /// + /// Gets the mouse cursor position in screen-space coordinates. + /// + /// Mouse cursor coordinates. + API_PROPERTY() static Float2 GetMousePosition(); + + /// + /// Sets the mouse cursor position in screen-space coordinates. + /// + /// Cursor position to set. + API_PROPERTY() static void SetMousePosition(const Float2& position); /// /// Gets the origin position and size of the monitor at the given screen-space location. @@ -686,7 +688,6 @@ public: API_PROPERTY() static Float2 GetVirtualDesktopSize(); public: - /// /// Gets full path of the main engine directory. /// @@ -719,7 +720,6 @@ public: static bool SetWorkingDirectory(const String& path); public: - /// /// Gets the process environment variables (pairs of key and value). /// diff --git a/Source/Engine/Platform/Base/StringUtilsBase.cpp b/Source/Engine/Platform/Base/StringUtilsBase.cpp index c18d309a5..ef4e66b72 100644 --- a/Source/Engine/Platform/Base/StringUtilsBase.cpp +++ b/Source/Engine/Platform/Base/StringUtilsBase.cpp @@ -11,9 +11,9 @@ #include #endif -const char DirectorySeparatorChar = '\\'; -const char AltDirectorySeparatorChar = '/'; -const char VolumeSeparatorChar = ':'; +constexpr char DirectorySeparatorChar = '\\'; +constexpr char AltDirectorySeparatorChar = '/'; +constexpr char VolumeSeparatorChar = ':'; const Char* StringUtils::FindIgnoreCase(const Char* str, const Char* toFind) { @@ -378,20 +378,17 @@ void StringUtils::PathRemoveRelativeParts(String& path) path.Insert(0, TEXT("/")); } -const char DigitPairs[201] = { - "00010203040506070809" - "10111213141516171819" - "20212223242526272829" - "30313233343536373839" - "40414243444546474849" - "50515253545556575859" - "60616263646566676869" - "70717273747576777879" - "80818283848586878889" - "90919293949596979899" -}; - -#define STRING_UTILS_ITOSTR_BUFFER_SIZE 15 +int32 StringUtils::HexDigit(Char c) +{ + int32 result = 0; + if (c >= '0' && c <= '9') + result = c - '0'; + else if (c >= 'a' && c <= 'f') + result = c + 10 - 'a'; + else if (c >= 'A' && c <= 'F') + result = c + 10 - 'A'; + return result; +} bool StringUtils::Parse(const Char* str, float* result) { @@ -419,108 +416,22 @@ bool StringUtils::Parse(const char* str, float* result) String StringUtils::ToString(int32 value) { - char buf[STRING_UTILS_ITOSTR_BUFFER_SIZE]; - char* it = &buf[STRING_UTILS_ITOSTR_BUFFER_SIZE - 2]; - int32 div = value / 100; - if (value >= 0) - { - while (div) - { - Platform::MemoryCopy(it, &DigitPairs[2 * (value - div * 100)], 2); - value = div; - it -= 2; - div = value / 100; - } - Platform::MemoryCopy(it, &DigitPairs[2 * value], 2); - if (value < 10) - it++; - } - else - { - while (div) - { - Platform::MemoryCopy(it, &DigitPairs[-2 * (value - div * 100)], 2); - value = div; - it -= 2; - div = value / 100; - } - Platform::MemoryCopy(it, &DigitPairs[-2 * value], 2); - if (value <= -10) - it--; - *it = '-'; - } - return String(it, (int32)(&buf[STRING_UTILS_ITOSTR_BUFFER_SIZE] - it)); + return String::Format(TEXT("{}"), value); } String StringUtils::ToString(int64 value) { - char buf[STRING_UTILS_ITOSTR_BUFFER_SIZE]; - char* it = &buf[STRING_UTILS_ITOSTR_BUFFER_SIZE - 2]; - int64 div = value / 100; - if (value >= 0) - { - while (div) - { - Platform::MemoryCopy(it, &DigitPairs[2 * (value - div * 100)], 2); - value = div; - it -= 2; - div = value / 100; - } - Platform::MemoryCopy(it, &DigitPairs[2 * value], 2); - if (value < 10) - it++; - } - else - { - while (div) - { - Platform::MemoryCopy(it, &DigitPairs[-2 * (value - div * 100)], 2); - value = div; - it -= 2; - div = value / 100; - } - Platform::MemoryCopy(it, &DigitPairs[-2 * value], 2); - if (value <= -10) - it--; - *it = '-'; - } - return String(it, (int32)(&buf[STRING_UTILS_ITOSTR_BUFFER_SIZE] - it)); + return String::Format(TEXT("{}"), value); } String StringUtils::ToString(uint32 value) { - char buf[STRING_UTILS_ITOSTR_BUFFER_SIZE]; - char* it = &buf[STRING_UTILS_ITOSTR_BUFFER_SIZE - 2]; - int32 div = value / 100; - while (div) - { - Platform::MemoryCopy(it, &DigitPairs[2 * (value - div * 100)], 2); - value = div; - it -= 2; - div = value / 100; - } - Platform::MemoryCopy(it, &DigitPairs[2 * value], 2); - if (value < 10) - it++; - return String((char*)it, (int32)((char*)&buf[STRING_UTILS_ITOSTR_BUFFER_SIZE] - (char*)it)); + return String::Format(TEXT("{}"), value); } String StringUtils::ToString(uint64 value) { - char buf[STRING_UTILS_ITOSTR_BUFFER_SIZE]; - char* it = &buf[STRING_UTILS_ITOSTR_BUFFER_SIZE - 2]; - int64 div = value / 100; - while (div) - { - Platform::MemoryCopy(it, &DigitPairs[2 * (value - div * 100)], 2); - value = div; - it -= 2; - div = value / 100; - } - Platform::MemoryCopy(it, &DigitPairs[2 * value], 2); - if (value < 10) - it++; - return String((char*)it, (int32)((char*)&buf[STRING_UTILS_ITOSTR_BUFFER_SIZE] - (char*)it)); + return String::Format(TEXT("{}"), value); } String StringUtils::ToString(float value) @@ -544,5 +455,3 @@ String StringUtils::GetZZString(const Char* str) } return String(str, (int32)(end - str)); } - -#undef STRING_UTILS_ITOSTR_BUFFER_SIZE diff --git a/Source/Engine/Platform/Linux/LinuxPlatform.cpp b/Source/Engine/Platform/Linux/LinuxPlatform.cpp index 23267b193..0434d8414 100644 --- a/Source/Engine/Platform/Linux/LinuxPlatform.cpp +++ b/Source/Engine/Platform/Linux/LinuxPlatform.cpp @@ -89,6 +89,7 @@ X11::Cursor Cursors[(int32)CursorType::MAX]; X11::XcursorImage* CursorsImg[(int32)CursorType::MAX]; Dictionary KeyNameMap; Array KeyCodeMap; +Delegate LinuxPlatform::xEventRecieved; // Message boxes configuration #define LINUX_DIALOG_MIN_BUTTON_WIDTH 64 @@ -2232,10 +2233,12 @@ void LinuxPlatform::Tick() { X11::XEvent event; X11::XNextEvent(xDisplay, &event); - if (X11::XFilterEvent(&event, 0)) continue; + // External event handling + xEventRecieved(&event); + LinuxWindow* window; switch (event.type) { @@ -2640,10 +2643,8 @@ Float2 LinuxPlatform::GetMousePosition() { if (!xDisplay) return Float2::Zero; - - int32 x, y; + int32 x = 0, y = 0; uint32 screenCount = (uint32)X11::XScreenCount(xDisplay); - for (uint32 i = 0; i < screenCount; i++) { X11::Window outRoot, outChild; @@ -2652,7 +2653,6 @@ Float2 LinuxPlatform::GetMousePosition() if (X11::XQueryPointer(xDisplay, X11::XRootWindow(xDisplay, i), &outRoot, &outChild, &x, &y, &childX, &childY, &mask)) break; } - return Float2((float)x, (float)y); } diff --git a/Source/Engine/Platform/Linux/LinuxPlatform.h b/Source/Engine/Platform/Linux/LinuxPlatform.h index 032ea4c6b..827a6b635 100644 --- a/Source/Engine/Platform/Linux/LinuxPlatform.h +++ b/Source/Engine/Platform/Linux/LinuxPlatform.h @@ -33,6 +33,11 @@ public: /// The user home directory. static const String& GetHomeDirectory(); + /// + /// An event that is fired when an XEvent is received during platform tick. + /// + static Delegate xEventRecieved; + public: // [UnixPlatform] diff --git a/Source/Engine/Platform/StringUtils.h b/Source/Engine/Platform/StringUtils.h index ea982c2ee..04e644d2e 100644 --- a/Source/Engine/Platform/StringUtils.h +++ b/Source/Engine/Platform/StringUtils.h @@ -21,12 +21,11 @@ enum class StringSearchCase }; /// -/// The string operations utilities collection. +/// The string operations utilities. /// class FLAXENGINE_API StringUtils { public: - /// /// Calculates the hash code for input string. /// @@ -65,7 +64,6 @@ public: } public: - // Returns true if character is uppercase static bool IsUpper(char c); @@ -92,7 +90,6 @@ public: static char ToLower(char c); public: - // Returns true if character is uppercase static bool IsUpper(Char c); @@ -119,7 +116,6 @@ public: static Char ToLower(Char c); public: - // Compare two strings with case sensitive. Strings must not be null. static int32 Compare(const Char* str1, const Char* str2); @@ -145,7 +141,6 @@ public: static int32 CompareIgnoreCase(const char* str1, const char* str2, int32 maxCount); public: - // Get string length. Returns 0 if str is null. static int32 Length(const Char* str); @@ -183,7 +178,6 @@ public: static const char* FindIgnoreCase(const char* str, const char* toFind); public: - // Converts characters from ANSI to UTF-16 static void ConvertANSI2UTF16(const char* from, Char* to, int32 len); @@ -203,7 +197,6 @@ public: static char* ConvertUTF162UTF8(const Char* from, int32 fromLength, int32& toLength); public: - // Returns the directory name of the specified path string // @param path The path string from which to obtain the directory name // @returns Directory name @@ -224,95 +217,8 @@ public: static void PathRemoveRelativeParts(String& path); public: - - /// - /// Convert integer value to string - /// - /// Value to convert - /// Base (8,10,16) - /// Input buffer - /// Result string length - template - static void itoa(int32 value, int32 base, CharType* buffer, int32& length) - { - // Allocate buffer - bool isNegative = false; - CharType* pos = buffer; - CharType* pos1 = buffer; - length = 0; - - // Validate input base - if (base < 8 || base > 16) - { - *pos = '\0'; - return; - } - - // Special case for zero - if (value == 0) - { - length++; - *pos++ = '0'; - *pos = '\0'; - return; - } - - // Check if value is negative - if (value < 0) - { - isNegative = true; - value = -value; - } - - // Convert. If base is power of two (2,4,8,16..) - // we could use binary and operation and shift offset instead of division - while (value) - { - length++; - int32 reminder = value % base; - *pos++ = reminder + (reminder > 9 ? 'a' - 10 : '0'); - value /= base; - } - - // Apply negative sign - if (isNegative) - *pos++ = '-'; - - // Add null terminator char - *pos-- = 0; - - // Reverse the buffer - while (pos1 < pos) - { - CharType c = *pos; - *pos-- = *pos1; - *pos1++ = c; - } - } - - static int32 HexDigit(Char c) - { - int32 result = 0; - - if (c >= '0' && c <= '9') - { - result = c - '0'; - } - else if (c >= 'a' && c <= 'f') - { - result = c + 10 - 'a'; - } - else if (c >= 'A' && c <= 'F') - { - result = c + 10 - 'A'; - } - else - { - result = 0; - } - - return result; - } + // Converts hexadecimal character into the value. + static int32 HexDigit(Char c); // Parse text to unsigned integer value // @param str String to parse @@ -431,7 +337,6 @@ public: static bool Parse(const char* str, float* result); public: - static String ToString(int32 value); static String ToString(int64 value); static String ToString(uint32 value); @@ -440,7 +345,6 @@ public: static String ToString(double value); public: - // Returns the String to double null-terminated string // @param str Double null-terminated string // @return Double null-terminated String diff --git a/Source/Engine/Platform/UWP/UWPPlatform.cpp b/Source/Engine/Platform/UWP/UWPPlatform.cpp index 0e9f465b6..58240ca89 100644 --- a/Source/Engine/Platform/UWP/UWPPlatform.cpp +++ b/Source/Engine/Platform/UWP/UWPPlatform.cpp @@ -148,37 +148,6 @@ bool UWPPlatform::GetHasFocus() return true; } -bool UWPPlatform::CanOpenUrl(const StringView& url) -{ - return false; -} - -void UWPPlatform::OpenUrl(const StringView& url) -{ - // TODO: add support for OpenUrl on UWP -} - -Float2 UWPPlatform::GetMousePosition() -{ - // Use the main window - auto win = Engine::MainWindow; - if (win) - { - return win->ClientToScreen(win->GetMousePosition()); - } - return Float2::Minimum; -} - -void UWPPlatform::SetMousePosition(const Float2& pos) -{ - // Use the main window - auto win = Engine::MainWindow; - if (win) - { - win->SetMousePosition(win->ScreenToClient(pos)); - } -} - Float2 UWPPlatform::GetDesktopSize() { Float2 result; diff --git a/Source/Engine/Platform/UWP/UWPPlatform.h b/Source/Engine/Platform/UWP/UWPPlatform.h index 794a8f97a..f2f629d02 100644 --- a/Source/Engine/Platform/UWP/UWPPlatform.h +++ b/Source/Engine/Platform/UWP/UWPPlatform.h @@ -35,10 +35,6 @@ public: static String GetUserLocaleName(); static String GetComputerName(); static bool GetHasFocus(); - static bool CanOpenUrl(const StringView& url); - static void OpenUrl(const StringView& url); - static Float2 GetMousePosition(); - static void SetMousePosition(const Float2& pos); static Float2 GetDesktopSize(); static Window* CreateWindow(const CreateWindowSettings& settings); static void* LoadLibrary(const Char* filename); diff --git a/Source/Engine/Platform/Win32/Win32Platform.h b/Source/Engine/Platform/Win32/Win32Platform.h index b50d31616..d3f9a5cdc 100644 --- a/Source/Engine/Platform/Win32/Win32Platform.h +++ b/Source/Engine/Platform/Win32/Win32Platform.h @@ -109,7 +109,7 @@ public: static void CreateGuid(Guid& result); static String GetMainDirectory(); static String GetExecutableFilePath(); - static struct Guid GetUniqueDeviceId(); + static Guid GetUniqueDeviceId(); static String GetWorkingDirectory(); static bool SetWorkingDirectory(const String& path); static void FreeLibrary(void* handle); diff --git a/Source/Engine/Platform/Windows/WindowsPlatform.cpp b/Source/Engine/Platform/Windows/WindowsPlatform.cpp index 2a4cd2fd4..8d02312af 100644 --- a/Source/Engine/Platform/Windows/WindowsPlatform.cpp +++ b/Source/Engine/Platform/Windows/WindowsPlatform.cpp @@ -830,6 +830,18 @@ void WindowsPlatform::OpenUrl(const StringView& url) ::ShellExecuteW(nullptr, TEXT("open"), *url, nullptr, nullptr, SW_SHOWNORMAL); } +Float2 WindowsPlatform::GetMousePosition() +{ + POINT cursorPos; + GetCursorPos(&cursorPos); + return Float2((float)cursorPos.x, (float)cursorPos.y); +} + +void WindowsPlatform::SetMousePosition(const Float2& pos) +{ + ::SetCursorPos((int)pos.X, (int)pos.Y); +} + struct GetMonitorBoundsData { Float2 Pos; @@ -1221,10 +1233,11 @@ void* WindowsPlatform::LoadLibrary(const Char* filename) return handle; } +#if CRASH_LOG_ENABLE + Array WindowsPlatform::GetStackFrames(int32 skipCount, int32 maxDepth, void* context) { Array result; -#if CRASH_LOG_ENABLE DbgHelpLock(); // Initialize @@ -1350,12 +1363,9 @@ Array WindowsPlatform::GetStackFrames(int32 skipCount, } DbgHelpUnlock(); -#endif return result; } -#if CRASH_LOG_ENABLE - void WindowsPlatform::CollectCrashData(const String& crashDataFolder, void* context) { // Create mini dump file for crash debugging diff --git a/Source/Engine/Platform/Windows/WindowsPlatform.h b/Source/Engine/Platform/Windows/WindowsPlatform.h index cc85a6d1e..b4fd87a35 100644 --- a/Source/Engine/Platform/Windows/WindowsPlatform.h +++ b/Source/Engine/Platform/Windows/WindowsPlatform.h @@ -71,6 +71,8 @@ public: static bool GetHasFocus(); static bool CanOpenUrl(const StringView& url); static void OpenUrl(const StringView& url); + static Float2 GetMousePosition(); + static void SetMousePosition(const Float2& pos); static Rectangle GetMonitorBounds(const Float2& screenPos); static Float2 GetDesktopSize(); static Rectangle GetVirtualDesktopBounds(); @@ -80,8 +82,8 @@ public: static int32 CreateProcess(CreateProcessSettings& settings); static Window* CreateWindow(const CreateWindowSettings& settings); static void* LoadLibrary(const Char* filename); - static Array GetStackFrames(int32 skipCount = 0, int32 maxDepth = 60, void* context = nullptr); #if CRASH_LOG_ENABLE + static Array GetStackFrames(int32 skipCount = 0, int32 maxDepth = 60, void* context = nullptr); static void CollectCrashData(const String& crashDataFolder, void* context = nullptr); #endif }; diff --git a/Source/Engine/Render2D/Font.cpp b/Source/Engine/Render2D/Font.cpp index b026e2d80..f12a9ce1c 100644 --- a/Source/Engine/Render2D/Font.cpp +++ b/Source/Engine/Render2D/Font.cpp @@ -118,16 +118,10 @@ void Font::ProcessText(const StringView& text, Array& outputLines tmpLine.FirstCharIndex = 0; tmpLine.LastCharIndex = -1; - int32 lastWhitespaceIndex = INVALID_INDEX; - float lastWhitespaceX = 0; + int32 lastWrapCharIndex = INVALID_INDEX; + float lastWrapCharX = 0; bool lastMoveLine = false; - int32 lastUpperIndex = INVALID_INDEX; - float lastUpperX = 0; - - int32 lastUnderscoreIndex = INVALID_INDEX; - float lastUnderscoreX = 0; - // Process each character to split text into single lines for (int32 currentIndex = 0; currentIndex < textLength;) { @@ -137,30 +131,14 @@ void Font::ProcessText(const StringView& text, Array& outputLines // Cache current character const Char currentChar = text[currentIndex]; - - // Check if character is a whitespace const bool isWhitespace = StringUtils::IsWhitespace(currentChar); - if (isWhitespace) - { - // Cache line break point - lastWhitespaceIndex = currentIndex; - lastWhitespaceX = cursorX; - } - // Check if character is an upper case letter - const bool isUpper = StringUtils::IsUpper(currentChar); - if (isUpper && currentIndex != 0) + // Check if character can wrap words + const bool isWrapChar = !StringUtils::IsAlnum(currentChar) || isWhitespace || StringUtils::IsUpper(currentChar); + if (isWrapChar && currentIndex != 0) { - lastUpperIndex = currentIndex; - lastUpperX = cursorX; - } - - // Check if character is an underscore - const bool isUnderscore = currentChar == '_'; - if (isUnderscore) - { - lastUnderscoreIndex = currentIndex; - lastUnderscoreX = cursorX; + lastWrapCharIndex = currentIndex; + lastWrapCharX = cursorX; } // Check if it's a newline character @@ -197,41 +175,32 @@ void Font::ProcessText(const StringView& text, Array& outputLines } else if (layout.TextWrapping == TextWrapping::WrapWords) { - // Move line but back to the last after-whitespace character - moveLine = true; - if (lastWhitespaceIndex != INVALID_INDEX) - { - cursorX = lastWhitespaceX; - tmpLine.LastCharIndex = lastWhitespaceIndex - 1; - nextCharIndex = currentIndex = lastWhitespaceIndex + 1; - } - else if (lastUpperIndex != INVALID_INDEX) + if (lastWrapCharIndex != INVALID_INDEX) { // Skip moving twice for the same character - if (outputLines.HasItems() && outputLines.Last().LastCharIndex == lastUpperIndex - 1) + int32 lastLineLasCharIndex = outputLines.HasItems() ? outputLines.Last().LastCharIndex : -10000; + if (lastLineLasCharIndex == lastWrapCharIndex || lastLineLasCharIndex == lastWrapCharIndex - 1 || lastLineLasCharIndex == lastWrapCharIndex - 2) { currentIndex = nextCharIndex; lastMoveLine = moveLine; continue; } - cursorX = lastUpperX; - tmpLine.LastCharIndex = lastUpperIndex - 1; - nextCharIndex = currentIndex = lastUpperIndex; - } - else if (lastUnderscoreIndex != INVALID_INDEX) - { - cursorX = lastUnderscoreX; - tmpLine.LastCharIndex = lastUnderscoreIndex - 2; - nextCharIndex = currentIndex = lastUnderscoreIndex + 1; - } - else - { - nextCharIndex = currentIndex; - - // Skip moving twice for the same character - if (lastMoveLine) - break; + // Move line + const Char wrapChar = text[lastWrapCharIndex]; + moveLine = true; + cursorX = lastWrapCharX; + if (StringUtils::IsWhitespace(wrapChar)) + { + // Skip whitespaces + tmpLine.LastCharIndex = lastWrapCharIndex - 1; + nextCharIndex = currentIndex = lastWrapCharIndex + 1; + } + else + { + tmpLine.LastCharIndex = lastWrapCharIndex - 1; + nextCharIndex = currentIndex = lastWrapCharIndex; + } } } else if (layout.TextWrapping == TextWrapping::WrapChars) @@ -260,16 +229,8 @@ void Font::ProcessText(const StringView& text, Array& outputLines tmpLine.FirstCharIndex = currentIndex; tmpLine.LastCharIndex = currentIndex - 1; cursorX = 0; - - lastWhitespaceIndex = INVALID_INDEX; - lastWhitespaceX = 0; - - lastUpperIndex = INVALID_INDEX; - lastUpperX = 0; - - lastUnderscoreIndex = INVALID_INDEX; - lastUnderscoreX = 0; - + lastWrapCharIndex = INVALID_INDEX; + lastWrapCharX = 0; previous.IsValid = false; } diff --git a/Source/Engine/Render2D/Font.h b/Source/Engine/Render2D/Font.h index d3806f0b4..3f42fd396 100644 --- a/Source/Engine/Render2D/Font.h +++ b/Source/Engine/Render2D/Font.h @@ -17,17 +17,17 @@ class FontAsset; /// /// The text range. /// -API_STRUCT() struct TextRange +API_STRUCT(NoDefault) struct TextRange { DECLARE_SCRIPTING_TYPE_MINIMAL(TextRange); /// - /// The start index. + /// The start index (inclusive). /// API_FIELD() int32 StartIndex; /// - /// The end index. + /// The end index (exclusive). /// API_FIELD() int32 EndIndex; @@ -70,7 +70,7 @@ DECLARE_SCRIPTING_TYPE_MINIMAL(TextRange); /// /// Gets the substring from the source text. /// - /// The text. + /// The text. /// The substring of the original text of the defined range. StringView Substring(const StringView& text) const { @@ -87,7 +87,7 @@ struct TIsPODType /// /// The font line info generated during text processing. /// -API_STRUCT() struct FontLineCache +API_STRUCT(NoDefault) struct FontLineCache { DECLARE_SCRIPTING_TYPE_MINIMAL(FontLineCache); @@ -151,7 +151,7 @@ struct TIsPODType /// /// The cached font character entry (read for rendering and further processing). /// -API_STRUCT() struct FontCharacterEntry +API_STRUCT(NoDefault) struct FontCharacterEntry { DECLARE_SCRIPTING_TYPE_MINIMAL(FontCharacterEntry); diff --git a/Source/Engine/Scripting/ManagedCLR/MCore.h b/Source/Engine/Scripting/ManagedCLR/MCore.h index 88124a223..38b58403d 100644 --- a/Source/Engine/Scripting/ManagedCLR/MCore.h +++ b/Source/Engine/Scripting/ManagedCLR/MCore.h @@ -45,6 +45,11 @@ public: /// static void UnloadEngine(); +#if USE_EDITOR + // Called by Scripting in a middle of hot-reload (after unloading modules but before loading them again). + static void OnMidHotReload(); +#endif + public: /// /// Utilities for C# object management. diff --git a/Source/Engine/Scripting/Runtime/DotNet.cpp b/Source/Engine/Scripting/Runtime/DotNet.cpp index 80f9a801f..f2c5ee376 100644 --- a/Source/Engine/Scripting/Runtime/DotNet.cpp +++ b/Source/Engine/Scripting/Runtime/DotNet.cpp @@ -165,9 +165,8 @@ extern MDomain* MActiveDomain; extern Array> MDomains; Dictionary CachedFunctions; - -Dictionary classHandles; -Dictionary assemblyHandles; +Dictionary CachedClassHandles; +Dictionary CachedAssemblyHandles; /// /// Returns the function pointer to the managed static method in NativeInterop class. @@ -300,6 +299,17 @@ void MCore::UnloadEngine() ShutdownHostfxr(); } +#if USE_EDITOR + +void MCore::OnMidHotReload() +{ + // Clear any cached class attributes (see https://github.com/FlaxEngine/FlaxEngine/issues/1108) + for (auto e : CachedClassHandles) + e.Value->_attributes.Clear(); +} + +#endif + MObject* MCore::Object::Box(void* value, const MClass* klass) { static void* BoxValuePtr = GetStaticMethodPointer(TEXT("BoxValue")); @@ -672,7 +682,7 @@ bool MAssembly::LoadCorlib() return true; } _hasCachedClasses = false; - assemblyHandles.Add(_handle, this); + CachedAssemblyHandles.Add(_handle, this); // End OnLoaded(startTime); @@ -681,6 +691,7 @@ bool MAssembly::LoadCorlib() bool MAssembly::LoadImage(const String& assemblyPath, const StringView& nativePath) { + // TODO: Use new hostfxr delegate load_assembly_bytes? (.NET 8+) // Open .Net assembly const StringAnsi assemblyPathAnsi = assemblyPath.ToStringAnsi(); const char* name; @@ -696,7 +707,7 @@ bool MAssembly::LoadImage(const String& assemblyPath, const StringView& nativePa Log::CLRInnerException(TEXT(".NET assembly image is invalid at ") + assemblyPath); return true; } - assemblyHandles.Add(_handle, this); + CachedAssemblyHandles.Add(_handle, this); // Provide new path of hot-reloaded native library path for managed DllImport if (nativePath.HasChars()) @@ -722,7 +733,7 @@ bool MAssembly::UnloadImage(bool isReloading) CallStaticMethod(CloseAssemblyPtr, _handle); } - assemblyHandles.Remove(_handle); + CachedAssemblyHandles.Remove(_handle); _handle = nullptr; } return false; @@ -780,7 +791,7 @@ MClass::MClass(const MAssembly* parentAssembly, void* handle, const char* name, static void* TypeIsEnumPtr = GetStaticMethodPointer(TEXT("TypeIsEnum")); _isEnum = CallStaticMethod(TypeIsEnumPtr, handle); - classHandles.Add(handle, this); + CachedClassHandles.Add(handle, this); } bool MAssembly::ResolveMissingFile(String& assemblyPath) const @@ -800,7 +811,7 @@ MClass::~MClass() _properties.ClearDelete(); _events.ClearDelete(); - classHandles.Remove(_handle); + CachedClassHandles.Remove(_handle); } StringAnsiView MClass::GetName() const @@ -1018,11 +1029,7 @@ const Array& MClass::GetAttributes() const int numAttributes; static void* GetClassAttributesPtr = GetStaticMethodPointer(TEXT("GetClassAttributes")); CallStaticMethod(GetClassAttributesPtr, _handle, &attributes, &numAttributes); - _attributes.Resize(numAttributes); - for (int i = 0; i < numAttributes; i++) - { - _attributes[i] = attributes[i]; - } + _attributes.Set(attributes, numAttributes); MCore::GC::FreeMemory(attributes); _hasCachedAttributes = true; @@ -1444,7 +1451,7 @@ const Array& MProperty::GetAttributes() const MAssembly* GetAssembly(void* assemblyHandle) { MAssembly* assembly; - if (assemblyHandles.TryGet(assemblyHandle, assembly)) + if (CachedAssemblyHandles.TryGet(assemblyHandle, assembly)) return assembly; return nullptr; } @@ -1452,7 +1459,7 @@ MAssembly* GetAssembly(void* assemblyHandle) MClass* GetClass(MType* typeHandle) { MClass* klass = nullptr; - classHandles.TryGet(typeHandle, klass); + CachedClassHandles.TryGet(typeHandle, klass); return nullptr; } @@ -1461,7 +1468,7 @@ MClass* GetOrCreateClass(MType* typeHandle) if (!typeHandle) return nullptr; MClass* klass; - if (!classHandles.TryGet(typeHandle, klass)) + if (!CachedClassHandles.TryGet(typeHandle, klass)) { NativeClassDefinitions classInfo; void* assemblyHandle; @@ -1567,9 +1574,9 @@ bool InitHostfxr() Platform::OpenUrl(TEXT("https://dotnet.microsoft.com/en-us/download/dotnet/7.0")); #endif #if USE_EDITOR - LOG(Fatal, "Missing .NET 7 SDK installation requried to run Flax Editor."); + LOG(Fatal, "Missing .NET 7 SDK installation required to run Flax Editor."); #else - LOG(Fatal, "Missing .NET 7 Runtime installation requried to run this application."); + LOG(Fatal, "Missing .NET 7 Runtime installation required to run this application."); #endif return true; } @@ -1596,6 +1603,15 @@ bool InitHostfxr() return true; } + // TODO: Implement picking different version of hostfxr, currently prefers highest available version. + // Allow future and preview versions of .NET + String dotnetRollForward; + String dotnetRollForwardPr; + if (Platform::GetEnvironmentVariable(TEXT("DOTNET_ROLL_FORWARD"), dotnetRollForward)) + Platform::SetEnvironmentVariable(TEXT("DOTNET_ROLL_FORWARD"), TEXT("LatestMajor")); + if (Platform::GetEnvironmentVariable(TEXT("DOTNET_ROLL_FORWARD_TO_PRERELEASE"), dotnetRollForwardPr)) + Platform::SetEnvironmentVariable(TEXT("DOTNET_ROLL_FORWARD_TO_PRERELEASE"), TEXT("1")); + // Initialize hosting component const char_t* argv[1] = { libraryPath.Get() }; hostfxr_initialize_parameters init_params; diff --git a/Source/Engine/Scripting/Runtime/Mono.cpp b/Source/Engine/Scripting/Runtime/Mono.cpp index 54892b11b..ca4ef367b 100644 --- a/Source/Engine/Scripting/Runtime/Mono.cpp +++ b/Source/Engine/Scripting/Runtime/Mono.cpp @@ -714,6 +714,14 @@ void MCore::UnloadEngine() #endif } +#if USE_EDITOR + +void MCore::OnMidHotReload() +{ +} + +#endif + MObject* MCore::Object::Box(void* value, const MClass* klass) { return mono_value_box(mono_domain_get(), klass->GetNative(), value); diff --git a/Source/Engine/Scripting/Runtime/None.cpp b/Source/Engine/Scripting/Runtime/None.cpp index 0c88c519d..c8f8926e5 100644 --- a/Source/Engine/Scripting/Runtime/None.cpp +++ b/Source/Engine/Scripting/Runtime/None.cpp @@ -59,6 +59,14 @@ void MCore::UnloadEngine() MRootDomain = nullptr; } +#if USE_EDITOR + +void MCore::OnMidHotReload() +{ +} + +#endif + MObject* MCore::Object::Box(void* value, const MClass* klass) { return nullptr; diff --git a/Source/Engine/Scripting/Script.cpp b/Source/Engine/Scripting/Script.cpp index 3f8df354c..8eaff42de 100644 --- a/Source/Engine/Scripting/Script.cpp +++ b/Source/Engine/Scripting/Script.cpp @@ -26,6 +26,7 @@ Script::Script(const SpawnParams& params) , _tickFixedUpdate(false) , _tickUpdate(false) , _tickLateUpdate(false) + , _tickLateFixedUpdate(false) , _wasStartCalled(false) , _wasEnableCalled(false) { @@ -181,6 +182,7 @@ void Script::SetupType() _tickUpdate |= type.Script.ScriptVTable[8] != nullptr; _tickLateUpdate |= type.Script.ScriptVTable[9] != nullptr; _tickFixedUpdate |= type.Script.ScriptVTable[10] != nullptr; + _tickLateFixedUpdate |= type.Script.ScriptVTable[11] != nullptr; } typeHandle = type.GetBaseType(); } diff --git a/Source/Engine/Scripting/Script.h b/Source/Engine/Scripting/Script.h index 2304ea57c..49cba0ec4 100644 --- a/Source/Engine/Scripting/Script.h +++ b/Source/Engine/Scripting/Script.h @@ -19,6 +19,7 @@ protected: int32 _tickFixedUpdate : 1; int32 _tickUpdate : 1; int32 _tickLateUpdate : 1; + int32 _tickLateFixedUpdate : 1; int32 _wasStartCalled : 1; int32 _wasEnableCalled : 1; #if USE_EDITOR @@ -108,6 +109,13 @@ public: { } + /// + /// Called every fixed framerate frame (after FixedUpdate) if object is enabled. + /// + API_FUNCTION(Attributes = "NoAnimate") virtual void OnLateFixedUpdate() + { + } + /// /// Called during drawing debug shapes in editor. Use to draw debug shapes and other visualization. /// diff --git a/Source/Engine/Scripting/Scripting.cpp b/Source/Engine/Scripting/Scripting.cpp index 1159ae823..046e5c6b0 100644 --- a/Source/Engine/Scripting/Scripting.cpp +++ b/Source/Engine/Scripting/Scripting.cpp @@ -46,6 +46,7 @@ public: void Update() override; void LateUpdate() override; void FixedUpdate() override; + void LateFixedUpdate() override; void Draw() override; void BeforeExit() override; void Dispose() override; @@ -100,6 +101,7 @@ namespace MMethod* _method_Update = nullptr; MMethod* _method_LateUpdate = nullptr; MMethod* _method_FixedUpdate = nullptr; + MMethod* _method_LateFixedUpdate = nullptr; MMethod* _method_Draw = nullptr; MMethod* _method_Exit = nullptr; Array> _nonNativeModules; @@ -210,6 +212,12 @@ void ScriptingService::FixedUpdate() INVOKE_EVENT(FixedUpdate); } +void ScriptingService::LateFixedUpdate() +{ + PROFILE_CPU_NAMED("Scripting::LateFixedUpdate"); + INVOKE_EVENT(LateFixedUpdate); +} + void ScriptingService::Draw() { PROFILE_CPU_NAMED("Scripting::Draw"); @@ -663,6 +671,7 @@ void Scripting::Reload(bool canTriggerSceneReload) modules.Clear(); _nonNativeModules.ClearDelete(); _hasGameModulesLoaded = false; + MCore::OnMidHotReload(); // Give GC a try to cleanup old user objects and the other mess MCore::GC::Collect(); diff --git a/Source/Engine/Scripting/Scripting.cs b/Source/Engine/Scripting/Scripting.cs index 204401867..7a91b2d96 100644 --- a/Source/Engine/Scripting/Scripting.cs +++ b/Source/Engine/Scripting/Scripting.cs @@ -80,17 +80,22 @@ namespace FlaxEngine public static event Action Update; /// - /// Occurs on scripting 'late' update. + /// Occurs on scripting late update. /// public static event Action LateUpdate; /// - /// Occurs on scripting `fixed` update. + /// Occurs on scripting fixed update. /// public static event Action FixedUpdate; /// - /// Occurs on scripting `draw` update. Called during frame rendering and can be used to invoke custom rendering with GPUDevice. + /// Occurs on scripting late fixed update. + /// + public static event Action LateFixedUpdate; + + /// + /// Occurs on scripting draw update. Called during frame rendering and can be used to invoke custom rendering with GPUDevice. /// public static event Action Draw; @@ -302,6 +307,11 @@ namespace FlaxEngine FixedUpdate?.Invoke(); } + internal static void Internal_LateFixedUpdate() + { + LateFixedUpdate?.Invoke(); + } + internal static void Internal_Draw() { Draw?.Invoke(); diff --git a/Source/Engine/Scripting/SoftTypeReference.cs b/Source/Engine/Scripting/SoftTypeReference.cs new file mode 100644 index 000000000..f12f2bdb7 --- /dev/null +++ b/Source/Engine/Scripting/SoftTypeReference.cs @@ -0,0 +1,87 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +using System; + +namespace FlaxEngine +{ + /// + /// The soft reference to the scripting type contained in the scripting assembly. + /// + public struct SoftTypeReference : IComparable, IComparable + { + private string _typeName; + + /// + /// Gets or sets the type full name (eg. FlaxEngine.Actor). + /// + public string TypeName + { + get => _typeName; + set => _typeName = value; + } + + /// + /// Gets or sets the type (resolves soft reference). + /// + public Type Type + { + get => _typeName != null ? Type.GetType(_typeName) : null; + set => _typeName = value?.FullName; + } + + /// + /// Initializes a new instance of the . + /// + /// The type name. + public SoftTypeReference(string typeName) + { + _typeName = typeName; + } + + /// + /// Gets the soft type reference from full name. + /// + /// The type name. + /// The soft type reference. + public static implicit operator SoftTypeReference(string s) + { + return new SoftTypeReference { _typeName = s }; + } + + /// + /// Gets the soft type reference from runtime type. + /// + /// The type. + /// The soft type reference. + public static implicit operator SoftTypeReference(Type s) + { + return new SoftTypeReference { _typeName = s?.FullName }; + } + + /// + public override string ToString() + { + return _typeName; + } + + /// + public override int GetHashCode() + { + return _typeName?.GetHashCode() ?? 0; + } + + /// + public int CompareTo(object obj) + { + if (obj is SoftTypeReference other) + return CompareTo(other); + return 0; + } + + /// + public int CompareTo(SoftTypeReference other) + { + return string.Compare(_typeName, other._typeName, StringComparison.Ordinal); + } + } +} diff --git a/Source/Engine/Scripting/SoftTypeReference.h b/Source/Engine/Scripting/SoftTypeReference.h new file mode 100644 index 000000000..30508f639 --- /dev/null +++ b/Source/Engine/Scripting/SoftTypeReference.h @@ -0,0 +1,151 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Scripting.h" +#include "ScriptingObject.h" +#include "Engine/Core/Log.h" +#include "Engine/Core/Types/String.h" +#include "Engine/Serialization/SerializationFwd.h" + +/// +/// The soft reference to the scripting type contained in the scripting assembly. +/// +template +API_STRUCT(InBuild) struct SoftTypeReference +{ +protected: + StringAnsi _typeName; + +public: + SoftTypeReference() = default; + + SoftTypeReference(const SoftTypeReference& s) + : _typeName(s._typeName) + { + } + + SoftTypeReference(SoftTypeReference&& s) noexcept + : _typeName(MoveTemp(s._typeName)) + { + } + + SoftTypeReference(const StringView& s) + : _typeName(s) + { + } + + SoftTypeReference(const StringAnsiView& s) + : _typeName(s) + { + } + + SoftTypeReference(const char* s) + : _typeName(s) + { + } + +public: + FORCE_INLINE SoftTypeReference& operator=(SoftTypeReference&& s) noexcept + { + _typeName = MoveTemp(s._typeName); + return *this; + } + + FORCE_INLINE SoftTypeReference& operator=(StringAnsi&& s) noexcept + { + _typeName = MoveTemp(s); + return *this; + } + + FORCE_INLINE SoftTypeReference& operator=(const SoftTypeReference& s) + { + _typeName = s._typeName; + return *this; + } + + FORCE_INLINE SoftTypeReference& operator=(const StringAnsiView& s) + { + _typeName = s; + return *this; + } + + FORCE_INLINE bool operator==(const SoftTypeReference& other) const + { + return _typeName == other._typeName; + } + + FORCE_INLINE bool operator!=(const SoftTypeReference& other) const + { + return _typeName != other._typeName; + } + + FORCE_INLINE bool operator==(const StringAnsiView& other) const + { + return _typeName == other; + } + + FORCE_INLINE bool operator!=(const StringAnsiView& other) const + { + return _typeName != other; + } + + FORCE_INLINE operator bool() const + { + return _typeName.HasChars(); + } + +public: + // Gets the type full name (eg. FlaxEngine.Actor). + StringAnsiView GetTypeName() const + { + return StringAnsiView(_typeName); + } + + // Gets the type (resolves soft reference). + ScriptingTypeHandle GetType() const + { + return Scripting::FindScriptingType(_typeName); + } + + // Creates a new objects of that type (or of type T if failed to solve typename). + T* NewObject() const + { + const ScriptingTypeHandle type = Scripting::FindScriptingType(_typeName); + auto obj = ScriptingObject::NewObject(type); + if (!obj) + { + if (_typeName.HasChars()) + LOG(Error, "Unknown or invalid type {0}", String(_typeName)); + obj = ScriptingObject::NewObject(); + } + return obj; + } +}; + +template +uint32 GetHash(const SoftTypeReference& key) +{ + return GetHash(key.GetTypeName()); +} + +// @formatter:off +namespace Serialization +{ + template + bool ShouldSerialize(const SoftTypeReference& v, const void* otherObj) + { + return !otherObj || v != *(SoftTypeReference*)otherObj; + } + template + void Serialize(ISerializable::SerializeStream& stream, const SoftTypeReference& v, const void* otherObj) + { + stream.String(v.GetTypeName()); + } + template + void Deserialize(ISerializable::DeserializeStream& stream, SoftTypeReference& v, ISerializeModifier* modifier) + { + v = stream.GetTextAnsi(); + } +} +// @formatter:on diff --git a/Source/Engine/Serialization/JsonConverters.cs b/Source/Engine/Serialization/JsonConverters.cs index 07388a09b..40f9cb5a2 100644 --- a/Source/Engine/Serialization/JsonConverters.cs +++ b/Source/Engine/Serialization/JsonConverters.cs @@ -7,7 +7,7 @@ using Newtonsoft.Json; namespace FlaxEngine.Json { /// - /// Serialize references to the FlaxEngine.Object as Guid. + /// Serialize references to the as Guid. /// /// internal class FlaxObjectConverter : JsonConverter @@ -46,7 +46,7 @@ namespace FlaxEngine.Json } /// - /// Serialize SceneReference as Guid in internal format. + /// Serialize as Guid in internal format. /// /// internal class SceneReferenceConverter : JsonConverter @@ -79,7 +79,7 @@ namespace FlaxEngine.Json } /// - /// Serialize SoftObjectReference as Guid in internal format. + /// Serialize as Guid in internal format. /// /// internal class SoftObjectReferenceConverter : JsonConverter @@ -111,7 +111,36 @@ namespace FlaxEngine.Json } /// - /// Serialize SoftObjectReference as Guid in internal format. + /// Serialize as typename string in internal format. + /// + /// + internal class SoftTypeReferenceConverter : JsonConverter + { + /// + public override void WriteJson(JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer) + { + writer.WriteValue(((SoftTypeReference)value).TypeName); + } + + /// + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer) + { + var result = new SoftTypeReference(); + if (reader.TokenType == JsonToken.String) + result.TypeName = (string)reader.Value; + + return result; + } + + /// + public override bool CanConvert(Type objectType) + { + return objectType == typeof(SoftTypeReference); + } + } + + /// + /// Serialize as Guid in internal format. /// /// internal class MarginConverter : JsonConverter @@ -237,7 +266,7 @@ namespace FlaxEngine.Json } /// - /// Serialize LocalizedString as inlined text is not using localization (Id member is empty). + /// Serialize as inlined text is not using localization (Id member is empty). /// /// internal class LocalizedStringConverter : JsonConverter diff --git a/Source/Engine/Serialization/JsonSerializer.cs b/Source/Engine/Serialization/JsonSerializer.cs index b9706b724..619862b1f 100644 --- a/Source/Engine/Serialization/JsonSerializer.cs +++ b/Source/Engine/Serialization/JsonSerializer.cs @@ -123,6 +123,7 @@ namespace FlaxEngine.Json settings.Converters.Add(ObjectConverter); settings.Converters.Add(new SceneReferenceConverter()); settings.Converters.Add(new SoftObjectReferenceConverter()); + settings.Converters.Add(new SoftTypeReferenceConverter()); settings.Converters.Add(new MarginConverter()); settings.Converters.Add(new VersionConverter()); settings.Converters.Add(new LocalizedStringConverter()); diff --git a/Source/Engine/UI/GUI/Common/Button.cs b/Source/Engine/UI/GUI/Common/Button.cs index 0e0d29615..8ab6366eb 100644 --- a/Source/Engine/UI/GUI/Common/Button.cs +++ b/Source/Engine/UI/GUI/Common/Button.cs @@ -10,7 +10,7 @@ namespace FlaxEngine.GUI public class Button : ContainerControl { /// - /// The default height fro the buttons. + /// The default height for the buttons. /// public const float DefaultHeight = 24.0f; @@ -42,7 +42,7 @@ namespace FlaxEngine.GUI /// /// Gets or sets the font used to draw button text. /// - [EditorDisplay("Style"), EditorOrder(2000)] + [EditorDisplay("Text Style"), EditorOrder(2022), ExpandGroups] public FontReference Font { get => _font; @@ -52,15 +52,51 @@ namespace FlaxEngine.GUI /// /// Gets or sets the custom material used to render the text. It must has domain set to GUI and have a public texture parameter named Font used to sample font atlas texture with font characters data. /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("Custom material used to render the text. It must has domain set to GUI and have a public texture parameter named Font used to sample font atlas texture with font characters data.")] + [EditorDisplay("Text Style"), EditorOrder(2021), Tooltip("Custom material used to render the text. It must has domain set to GUI and have a public texture parameter named Font used to sample font atlas texture with font characters data.")] public MaterialBase TextMaterial { get; set; } /// /// Gets or sets the color used to draw button text. /// - [EditorDisplay("Style"), EditorOrder(2000)] + [EditorDisplay("Text Style"), EditorOrder(2020)] public Color TextColor; + /// + /// Gets or sets the brush used for background drawing. + /// + [EditorDisplay("Background Style"), EditorOrder(1999), Tooltip("The brush used for background drawing."), ExpandGroups] + public IBrush BackgroundBrush { get; set; } + + /// + /// Gets or sets the background color when button is highlighted. + /// + [EditorDisplay("Background Style"), EditorOrder(2001)] + public Color BackgroundColorHighlighted { get; set; } + + /// + /// Gets or sets the background color when button is selected. + /// + [EditorDisplay("Background Style"), EditorOrder(2002)] + public Color BackgroundColorSelected { get; set; } + + /// + /// Gets or sets the color of the border. + /// + [EditorDisplay("Border Style"), EditorOrder(2010), ExpandGroups] + public Color BorderColor { get; set; } + + /// + /// Gets or sets the border color when button is highlighted. + /// + [EditorDisplay("Border Style"), EditorOrder(2011)] + public Color BorderColorHighlighted { get; set; } + + /// + /// Gets or sets the border color when button is selected. + /// + [EditorDisplay("Border Style"), EditorOrder(2012)] + public Color BorderColorSelected { get; set; } + /// /// Event fired when user clicks on the button. /// @@ -81,42 +117,6 @@ namespace FlaxEngine.GUI /// public event Action HoverEnd; - /// - /// Gets or sets the brush used for background drawing. - /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The brush used for background drawing.")] - public IBrush BackgroundBrush { get; set; } - - /// - /// Gets or sets the color of the border. - /// - [EditorDisplay("Style"), EditorOrder(2000)] - public Color BorderColor { get; set; } - - /// - /// Gets or sets the background color when button is selected. - /// - [EditorDisplay("Style"), EditorOrder(2010)] - public Color BackgroundColorSelected { get; set; } - - /// - /// Gets or sets the border color when button is selected. - /// - [EditorDisplay("Style"), EditorOrder(2020)] - public Color BorderColorSelected { get; set; } - - /// - /// Gets or sets the background color when button is highlighted. - /// - [EditorDisplay("Style"), EditorOrder(2000)] - public Color BackgroundColorHighlighted { get; set; } - - /// - /// Gets or sets the border color when button is highlighted. - /// - [EditorDisplay("Style"), EditorOrder(2000)] - public Color BorderColorHighlighted { get; set; } - /// /// Gets a value indicating whether this button is being pressed (by mouse or touch). /// @@ -195,7 +195,7 @@ namespace FlaxEngine.GUI /// Sets the button colors palette based on a given main color. /// /// The main color. - public void SetColors(Color color) + public virtual void SetColors(Color color) { BackgroundColor = color; BorderColor = color.RGBMultiplied(0.5f); @@ -209,7 +209,7 @@ namespace FlaxEngine.GUI public override void ClearState() { base.ClearState(); - + if (_isPressed) OnPressEnd(); } diff --git a/Source/Engine/UI/GUI/Common/CheckBox.cs b/Source/Engine/UI/GUI/Common/CheckBox.cs index ab689bfc3..939708f36 100644 --- a/Source/Engine/UI/GUI/Common/CheckBox.cs +++ b/Source/Engine/UI/GUI/Common/CheckBox.cs @@ -97,6 +97,7 @@ namespace FlaxEngine.GUI /// /// Gets or sets the size of the box. /// + [EditorOrder(20)] public float BoxSize { get => _boxSize; @@ -107,34 +108,34 @@ namespace FlaxEngine.GUI } } - /// - /// Gets or sets the color of the checkbox icon. - /// - [EditorDisplay("Style"), EditorOrder(2000)] - public Color ImageColor { get; set; } - /// /// Gets or sets the color of the border. /// - [EditorDisplay("Style"), EditorOrder(2000)] + [EditorDisplay("Border Style"), EditorOrder(2010), ExpandGroups] public Color BorderColor { get; set; } /// /// Gets or sets the border color when checkbox is hovered. /// - [EditorDisplay("Style"), EditorOrder(2000)] + [EditorDisplay("Border Style"), EditorOrder(2011)] public Color BorderColorHighlighted { get; set; } + /// + /// Gets or sets the color of the checkbox icon. + /// + [EditorDisplay("Image Style"), EditorOrder(2020), ExpandGroups] + public Color ImageColor { get; set; } + /// /// Gets or sets the image used to render checkbox checked state. /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The image used to render checkbox checked state.")] + [EditorDisplay("Image Style"), EditorOrder(2021), Tooltip("The image used to render checkbox checked state.")] public IBrush CheckedImage { get; set; } /// /// Gets or sets the image used to render checkbox intermediate state. /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The image used to render checkbox intermediate state.")] + [EditorDisplay("Image Style"), EditorOrder(2022), Tooltip("The image used to render checkbox intermediate state.")] public IBrush IntermediateImage { get; set; } /// diff --git a/Source/Engine/UI/GUI/Common/Dropdown.cs b/Source/Engine/UI/GUI/Common/Dropdown.cs index 80d4beb02..6ad962d6d 100644 --- a/Source/Engine/UI/GUI/Common/Dropdown.cs +++ b/Source/Engine/UI/GUI/Common/Dropdown.cs @@ -246,79 +246,79 @@ namespace FlaxEngine.GUI /// /// Gets or sets the font used to draw text. /// - [EditorDisplay("Style"), EditorOrder(2000)] + [EditorDisplay("Text Style"), EditorOrder(2021)] public FontReference Font { get; set; } /// /// Gets or sets the custom material used to render the text. It must has domain set to GUI and have a public texture parameter named Font used to sample font atlas texture with font characters data. /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("Custom material used to render the text. It must has domain set to GUI and have a public texture parameter named Font used to sample font atlas texture with font characters data.")] + [EditorDisplay("Text Style"), EditorOrder(2022), Tooltip("Custom material used to render the text. It must has domain set to GUI and have a public texture parameter named Font used to sample font atlas texture with font characters data.")] public MaterialBase FontMaterial { get; set; } /// /// Gets or sets the color of the text. /// - [EditorDisplay("Style"), EditorOrder(2000)] + [EditorDisplay("Text Style"), EditorOrder(2020), ExpandGroups] public Color TextColor { get; set; } /// /// Gets or sets the color of the border. /// - [EditorDisplay("Style"), EditorOrder(2000)] + [EditorDisplay("Border Style"), EditorOrder(2010), ExpandGroups] public Color BorderColor { get; set; } /// /// Gets or sets the background color when dropdown popup is opened. /// - [EditorDisplay("Style"), EditorOrder(2010)] + [EditorDisplay("Background Style"), EditorOrder(2002)] public Color BackgroundColorSelected { get; set; } /// /// Gets or sets the border color when dropdown popup is opened. /// - [EditorDisplay("Style"), EditorOrder(2020)] + [EditorDisplay("Border Style"), EditorOrder(2012)] public Color BorderColorSelected { get; set; } /// /// Gets or sets the background color when dropdown is highlighted. /// - [EditorDisplay("Style"), EditorOrder(2000)] + [EditorDisplay("Background Style"), EditorOrder(2001), ExpandGroups] public Color BackgroundColorHighlighted { get; set; } /// /// Gets or sets the border color when dropdown is highlighted. /// - [EditorDisplay("Style"), EditorOrder(2000)] + [EditorDisplay("Border Style"), EditorOrder(2011)] public Color BorderColorHighlighted { get; set; } /// /// Gets or sets the image used to render dropdown drop arrow icon. /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The image used to render dropdown drop arrow icon.")] + [EditorDisplay("Icon Style"), EditorOrder(2033), Tooltip("The image used to render dropdown drop arrow icon.")] public IBrush ArrowImage { get; set; } /// /// Gets or sets the color used to render dropdown drop arrow icon. /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The color used to render dropdown drop arrow icon.")] + [EditorDisplay("Icon Style"), EditorOrder(2030), Tooltip("The color used to render dropdown drop arrow icon."), ExpandGroups] public Color ArrowColor { get; set; } /// /// Gets or sets the color used to render dropdown drop arrow icon (menu is opened). /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The color used to render dropdown drop arrow icon (menu is opened).")] + [EditorDisplay("Icon Style"), EditorOrder(2032), Tooltip("The color used to render dropdown drop arrow icon (menu is opened).")] public Color ArrowColorSelected { get; set; } /// /// Gets or sets the color used to render dropdown drop arrow icon (menu is highlighted). /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The color used to render dropdown drop arrow icon (menu is highlighted).")] + [EditorDisplay("Icon Style"), EditorOrder(2031), Tooltip("The color used to render dropdown drop arrow icon (menu is highlighted).")] public Color ArrowColorHighlighted { get; set; } /// /// Gets or sets the image used to render dropdown checked item icon. /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The image used to render dropdown checked item icon.")] + [EditorDisplay("Icon Style"), EditorOrder(2034), Tooltip("The image used to render dropdown checked item icon.")] public IBrush CheckedImage { get; set; } /// diff --git a/Source/Engine/UI/GUI/Common/Image.cs b/Source/Engine/UI/GUI/Common/Image.cs index 98113b00c..f911664f6 100644 --- a/Source/Engine/UI/GUI/Common/Image.cs +++ b/Source/Engine/UI/GUI/Common/Image.cs @@ -25,19 +25,19 @@ namespace FlaxEngine.GUI /// /// Gets or sets the color used to multiply the image pixels. /// - [EditorDisplay("Style"), EditorOrder(2000)] + [EditorDisplay("Image Style"), EditorOrder(2010), ExpandGroups] public Color Color { get; set; } = Color.White; /// /// Gets or sets the color used to multiply the image pixels when mouse is over the image. /// - [EditorDisplay("Style"), EditorOrder(2000)] + [EditorDisplay("Image Style"), EditorOrder(2011)] public Color MouseOverColor { get; set; } = Color.White; /// /// Gets or sets the color used to multiply the image pixels when control is disabled. /// - [EditorDisplay("Style"), EditorOrder(2000)] + [EditorDisplay("Image Style"), EditorOrder(2012)] public Color DisabledTint { get; set; } = Color.Gray; /// diff --git a/Source/Engine/UI/GUI/Common/Label.cs b/Source/Engine/UI/GUI/Common/Label.cs index b64764a40..2779dfdea 100644 --- a/Source/Engine/UI/GUI/Common/Label.cs +++ b/Source/Engine/UI/GUI/Common/Label.cs @@ -47,37 +47,37 @@ namespace FlaxEngine.GUI /// /// Gets or sets the color of the text. /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The color of the text.")] + [EditorDisplay("Text Style"), EditorOrder(2010), Tooltip("The color of the text."), ExpandGroups] public Color TextColor { get; set; } /// /// Gets or sets the color of the text when it is highlighted (mouse is over). /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The color of the text when it is highlighted (mouse is over).")] + [EditorDisplay("Text Style"), EditorOrder(2011), Tooltip("The color of the text when it is highlighted (mouse is over).")] public Color TextColorHighlighted { get; set; } /// /// Gets or sets the horizontal text alignment within the control bounds. /// - [EditorDisplay("Style"), EditorOrder(2010), Tooltip("The horizontal text alignment within the control bounds.")] + [EditorDisplay("Text Style"), EditorOrder(2020), Tooltip("The horizontal text alignment within the control bounds.")] public TextAlignment HorizontalAlignment { get; set; } = TextAlignment.Center; /// /// Gets or sets the vertical text alignment within the control bounds. /// - [EditorDisplay("Style"), EditorOrder(2020), Tooltip("The vertical text alignment within the control bounds.")] + [EditorDisplay("Text Style"), EditorOrder(2021), Tooltip("The vertical text alignment within the control bounds.")] public TextAlignment VerticalAlignment { get; set; } = TextAlignment.Center; /// /// Gets or sets the text wrapping within the control bounds. /// - [EditorDisplay("Style"), EditorOrder(2030), Tooltip("The text wrapping within the control bounds.")] + [EditorDisplay("Text Style"), EditorOrder(2022), Tooltip("The text wrapping within the control bounds.")] public TextWrapping Wrapping { get; set; } = TextWrapping.NoWrap; /// /// Gets or sets the font. /// - [EditorDisplay("Style"), EditorOrder(2000)] + [EditorDisplay("Text Style"), EditorOrder(2023)] public FontReference Font { get => _font; @@ -99,7 +99,7 @@ namespace FlaxEngine.GUI /// /// Gets or sets the custom material used to render the text. It must has domain set to GUI and have a public texture parameter named Font used to sample font atlas texture with font characters data. /// - [EditorDisplay("Style"), EditorOrder(2000)] + [EditorDisplay("Text Style"), EditorOrder(2024)] public MaterialBase Material { get; set; } /// diff --git a/Source/Engine/UI/GUI/Common/ProgressBar.cs b/Source/Engine/UI/GUI/Common/ProgressBar.cs index fa76d9d26..a742d3b43 100644 --- a/Source/Engine/UI/GUI/Common/ProgressBar.cs +++ b/Source/Engine/UI/GUI/Common/ProgressBar.cs @@ -99,19 +99,19 @@ namespace FlaxEngine.GUI /// /// Gets or sets the margin for the progress bar rectangle within the control bounds. /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The margin for the progress bar rectangle within the control bounds.")] + [EditorDisplay("Bar Style"), EditorOrder(2011), Tooltip("The margin for the progress bar rectangle within the control bounds.")] public Margin BarMargin { get; set; } /// /// Gets or sets the color of the progress bar rectangle. /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The color of the progress bar rectangle.")] + [EditorDisplay("Bar Style"), EditorOrder(2010), Tooltip("The color of the progress bar rectangle."), ExpandGroups] public Color BarColor { get; set; } /// /// Gets or sets the brush used for progress bar drawing. /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The brush used for progress bar drawing.")] + [EditorDisplay("Bar Style"), EditorOrder(2012), Tooltip("The brush used for progress bar drawing.")] public IBrush BarBrush { get; set; } /// diff --git a/Source/Engine/UI/GUI/Common/TextBox.cs b/Source/Engine/UI/GUI/Common/TextBox.cs index 00c381e01..5ec86a94e 100644 --- a/Source/Engine/UI/GUI/Common/TextBox.cs +++ b/Source/Engine/UI/GUI/Common/TextBox.cs @@ -27,7 +27,7 @@ namespace FlaxEngine.GUI /// /// Gets or sets the text wrapping within the control bounds. /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The text wrapping within the control bounds.")] + [EditorDisplay("Text Style"), EditorOrder(2023), Tooltip("The text wrapping within the control bounds.")] public TextWrapping Wrapping { get => _layout.TextWrapping; @@ -37,31 +37,31 @@ namespace FlaxEngine.GUI /// /// Gets or sets the font. /// - [EditorDisplay("Style"), EditorOrder(2000)] + [EditorDisplay("Text Style"), EditorOrder(2024)] public FontReference Font { get; set; } /// /// Gets or sets the custom material used to render the text. It must has domain set to GUI and have a public texture parameter named Font used to sample font atlas texture with font characters data. /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("Custom material used to render the text. It must has domain set to GUI and have a public texture parameter named Font used to sample font atlas texture with font characters data.")] + [EditorDisplay("Text Style"), EditorOrder(2025), Tooltip("Custom material used to render the text. It must has domain set to GUI and have a public texture parameter named Font used to sample font atlas texture with font characters data.")] public MaterialBase TextMaterial { get; set; } /// /// Gets or sets the color of the text. /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The color of the text.")] + [EditorDisplay("Text Style"), EditorOrder(2020), Tooltip("The color of the text."), ExpandGroups] public Color TextColor { get; set; } /// /// Gets or sets the color of the text. /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The color of the watermark text.")] + [EditorDisplay("Text Style"), EditorOrder(2021), Tooltip("The color of the watermark text.")] public Color WatermarkTextColor { get; set; } /// /// Gets or sets the color of the selection (Transparent if not used). /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The color of the selection (Transparent if not used).")] + [EditorDisplay("Text Style"), EditorOrder(2022), Tooltip("The color of the selection (Transparent if not used).")] public Color SelectionColor { get; set; } /// diff --git a/Source/Engine/UI/GUI/Common/TextBoxBase.cs b/Source/Engine/UI/GUI/Common/TextBoxBase.cs index fb130bf18..d94ff8854 100644 --- a/Source/Engine/UI/GUI/Common/TextBoxBase.cs +++ b/Source/Engine/UI/GUI/Common/TextBoxBase.cs @@ -243,42 +243,43 @@ namespace FlaxEngine.GUI /// /// Gets or sets a value indicating whether you can scroll the text in the text box (eg. with a mouse wheel). /// + [EditorOrder(41)] public bool IsMultilineScrollable { get; set; } = true; /// /// Gets or sets textbox background color when the control is selected (has focus). /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The textbox background color when the control is selected (has focus).")] + [EditorDisplay("Background Style"), EditorOrder(2001), Tooltip("The textbox background color when the control is selected (has focus)."), ExpandGroups] public Color BackgroundSelectedColor { get; set; } /// /// Gets or sets the color of the caret (Transparent if not used). /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The color of the caret (Transparent if not used).")] + [EditorDisplay("Caret Style"), EditorOrder(2020), Tooltip("The color of the caret (Transparent if not used)."), ExpandGroups] public Color CaretColor { get; set; } /// /// Gets or sets the speed of the caret flashing animation. /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The speed of the caret flashing animation.")] + [EditorDisplay("Caret Style"), EditorOrder(2021), Tooltip("The speed of the caret flashing animation.")] public float CaretFlashSpeed { get; set; } = 6.0f; /// /// Gets or sets the speed of the selection background flashing animation. /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The speed of the selection background flashing animation.")] + [EditorDisplay("Background Style"), EditorOrder(2002), Tooltip("The speed of the selection background flashing animation.")] public float BackgroundSelectedFlashSpeed { get; set; } = 6.0f; /// /// Gets or sets the color of the border (Transparent if not used). /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The color of the border (Transparent if not used).")] + [EditorDisplay("Border Style"), EditorOrder(2010), Tooltip("The color of the border (Transparent if not used)."), ExpandGroups] public Color BorderColor { get; set; } /// /// Gets or sets the color of the border when control is focused (Transparent if not used). /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The color of the border when control is focused (Transparent if not used)")] + [EditorDisplay("Border Style"), EditorOrder(2011), Tooltip("The color of the border when control is focused (Transparent if not used)")] public Color BorderSelectedColor { get; set; } /// @@ -409,6 +410,7 @@ namespace FlaxEngine.GUI /// /// Gets or sets the selection range. /// + [EditorOrder(50)] public TextRange SelectionRange { get => new TextRange(SelectionLeft, SelectionRight); @@ -1195,7 +1197,7 @@ namespace FlaxEngine.GUI { SetSelection(hitPos); } - + if (Cursor == CursorType.Default && _changeCursor) Cursor = CursorType.IBeam; diff --git a/Source/Engine/UI/GUI/Control.cs b/Source/Engine/UI/GUI/Control.cs index 427c1aa8f..49b7f65d3 100644 --- a/Source/Engine/UI/GUI/Control.cs +++ b/Source/Engine/UI/GUI/Control.cs @@ -166,7 +166,7 @@ namespace FlaxEngine.GUI /// /// Gets or sets control background color (transparent color (alpha=0) means no background rendering) /// - [ExpandGroups, EditorDisplay("Style"), EditorOrder(2000), Tooltip("The control background color. Use transparent color (alpha=0) to hide background.")] + [ExpandGroups, EditorDisplay("Background Style"), EditorOrder(2000), Tooltip("The control background color. Use transparent color (alpha=0) to hide background.")] public Color BackgroundColor { get => _backgroundColor; diff --git a/Source/Engine/UI/GUI/Panels/DropPanel.cs b/Source/Engine/UI/GUI/Panels/DropPanel.cs index 409209549..d650d6205 100644 --- a/Source/Engine/UI/GUI/Panels/DropPanel.cs +++ b/Source/Engine/UI/GUI/Panels/DropPanel.cs @@ -95,48 +95,48 @@ namespace FlaxEngine.GUI } } - /// - /// Gets or sets the color used to draw header text. - /// - [EditorDisplay("Style"), EditorOrder(2000)] - public Color HeaderTextColor; - - /// - /// Gets or sets the color of the header. - /// - [EditorDisplay("Style"), EditorOrder(2000)] - public Color HeaderColor { get; set; } - - /// - /// Gets or sets the color of the header when mouse is over. - /// - [EditorDisplay("Style"), EditorOrder(2000)] - public Color HeaderColorMouseOver { get; set; } - - /// - /// Gets or sets the font used to render panel header text. - /// - [EditorDisplay("Style"), EditorOrder(2000)] - public FontReference HeaderTextFont { get; set; } - - /// - /// Gets or sets the custom material used to render the text. It must has domain set to GUI and have a public texture parameter named Font used to sample font atlas texture with font characters data. - /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("Custom material used to render the header text. It must has domain set to GUI and have a public texture parameter named Font used to sample font atlas texture with font characters data.")] - public MaterialBase HeaderTextMaterial { get; set; } - /// /// Gets or sets a value indicating whether enable drop down icon drawing. /// - [EditorDisplay("Style"), EditorOrder(2000)] + [EditorOrder(1)] public bool EnableDropDownIcon { get; set; } /// /// Gets or sets a value indicating whether to enable containment line drawing, /// - [EditorDisplay("Style"), EditorOrder(2000)] + [EditorOrder(2)] public bool EnableContainmentLines { get; set; } = false; + /// + /// Gets or sets the color used to draw header text. + /// + [EditorDisplay("Header Style"), EditorOrder(2010), ExpandGroups] + public Color HeaderTextColor; + + /// + /// Gets or sets the color of the header. + /// + [EditorDisplay("Header Style"), EditorOrder(2011)] + public Color HeaderColor { get; set; } + + /// + /// Gets or sets the color of the header when mouse is over. + /// + [EditorDisplay("Header Style"), EditorOrder(2012)] + public Color HeaderColorMouseOver { get; set; } + + /// + /// Gets or sets the font used to render panel header text. + /// + [EditorDisplay("Header Text Style"), EditorOrder(2020), ExpandGroups] + public FontReference HeaderTextFont { get; set; } + + /// + /// Gets or sets the custom material used to render the text. It must has domain set to GUI and have a public texture parameter named Font used to sample font atlas texture with font characters data. + /// + [EditorDisplay("Header Text Style"), EditorOrder(2021), Tooltip("Custom material used to render the header text. It must has domain set to GUI and have a public texture parameter named Font used to sample font atlas texture with font characters data.")] + public MaterialBase HeaderTextMaterial { get; set; } + /// /// Occurs when mouse right-clicks over the header. /// @@ -192,13 +192,13 @@ namespace FlaxEngine.GUI /// /// Gets or sets the image used to render drop panel drop arrow icon when panel is opened. /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The image used to render drop panel drop arrow icon when panel is opened.")] + [EditorDisplay("Icon Style"), EditorOrder(2030), Tooltip("The image used to render drop panel drop arrow icon when panel is opened."), ExpandGroups] public IBrush ArrowImageOpened { get; set; } /// /// Gets or sets the image used to render drop panel drop arrow icon when panel is closed. /// - [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The image used to render drop panel drop arrow icon when panel is closed.")] + [EditorDisplay("Icon Style"), EditorOrder(2031), Tooltip("The image used to render drop panel drop arrow icon when panel is closed.")] public IBrush ArrowImageClosed { get; set; } /// @@ -376,7 +376,6 @@ namespace FlaxEngine.GUI Render2D.DrawText(HeaderTextFont.GetFont(), HeaderTextMaterial, HeaderText, textRect, textColor, TextAlignment.Near, TextAlignment.Center); - if (!_isClosed && EnableContainmentLines) { Color lineColor = Style.Current.ForegroundGrey - new Color(0, 0, 0, 100); @@ -385,7 +384,7 @@ namespace FlaxEngine.GUI Render2D.DrawLine(new Float2(1, Height), new Float2(Width, Height), lineColor, lineThickness); Render2D.DrawLine(new Float2(Width, HeaderHeight), new Float2(Width, Height), lineColor, lineThickness); } - + // Children DrawChildren(); } diff --git a/Source/Engine/Utilities/Screenshot.cpp b/Source/Engine/Utilities/Screenshot.cpp index d67f8d123..7e1d3d902 100644 --- a/Source/Engine/Utilities/Screenshot.cpp +++ b/Source/Engine/Utilities/Screenshot.cpp @@ -10,7 +10,6 @@ #include "Engine/Graphics/GPUResourceProperty.h" #include "Engine/Graphics/GPUDevice.h" #include "Engine/Graphics/GPUSwapChain.h" -#include "Engine/Engine/Engine.h" #include "Engine/Threading/ThreadPoolTask.h" #include "Engine/Engine/Globals.h" #if COMPILE_WITH_TEXTURE_TOOL diff --git a/Source/FlaxEngine.Gen.cpp b/Source/FlaxEngine.Gen.cpp deleted file mode 100644 index 6e0ce7876..000000000 --- a/Source/FlaxEngine.Gen.cpp +++ /dev/null @@ -1,12 +0,0 @@ -// This code was auto-generated. Do not modify it. - -#include "Engine/Scripting/BinaryModule.h" -#include "FlaxEngine.Gen.h" - -StaticallyLinkedBinaryModuleInitializer StaticallyLinkedBinaryModuleFlaxEngine(GetBinaryModuleFlaxEngine); - -extern "C" BinaryModule* GetBinaryModuleFlaxEngine() -{ - static NativeBinaryModule module("FlaxEngine"); - return &module; -} diff --git a/Source/FlaxEngine.Gen.cs b/Source/FlaxEngine.Gen.cs deleted file mode 100644 index 3afeab9fd..000000000 --- a/Source/FlaxEngine.Gen.cs +++ /dev/null @@ -1,19 +0,0 @@ -// This code was auto-generated. Do not modify it. - -using System.Reflection; -using System.Runtime.InteropServices; -using System.Runtime.CompilerServices; - -[assembly: AssemblyTitle("FlaxEngine")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Flax")] -[assembly: AssemblyProduct("FlaxEngine")] -[assembly: AssemblyCopyright("Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: ComVisible(false)] -[assembly: Guid("b8442186-4a70-7c85-704a-857c68060f38")] -[assembly: AssemblyVersion("1.6.6340")] -[assembly: AssemblyFileVersion("1.6.6340")] -[assembly: DisableRuntimeMarshalling] diff --git a/Source/FlaxEngine.Gen.h b/Source/FlaxEngine.Gen.h deleted file mode 100644 index 15d6f9783..000000000 --- a/Source/FlaxEngine.Gen.h +++ /dev/null @@ -1,15 +0,0 @@ -// This code was auto-generated. Do not modify it. - -#pragma once - -#define FLAXENGINE_NAME "FlaxEngine" -#define FLAXENGINE_VERSION Version(1, 6, 6340) -#define FLAXENGINE_VERSION_TEXT "1.6.6340" -#define FLAXENGINE_VERSION_MAJOR 1 -#define FLAXENGINE_VERSION_MINOR 6 -#define FLAXENGINE_VERSION_BUILD 6340 -#define FLAXENGINE_COMPANY "Flax" -#define FLAXENGINE_COPYRIGHT "Copyright (c) 2012-2023 Wojciech Figat. All rights reserved." - -class BinaryModule; -extern "C" FLAXENGINE_API BinaryModule* GetBinaryModuleFlaxEngine(); diff --git a/Source/Shaders/Lighting.hlsl b/Source/Shaders/Lighting.hlsl index d4b9da107..4b909c024 100644 --- a/Source/Shaders/Lighting.hlsl +++ b/Source/Shaders/Lighting.hlsl @@ -129,9 +129,8 @@ float4 GetLighting(float3 viewPos, LightData lightData, GBufferSample gBuffer, f toLight = lightData.Position - gBuffer.WorldPos; float distanceSqr = dot(toLight, toLight); L = toLight * rsqrt(distanceSqr); - float distanceAttenuation = 1, lightRadiusMask = 1, spotAttenuation = 1; - GetRadialLightAttenuation(lightData, isSpotLight, N, distanceSqr, 1, toLight, L, NoL, distanceAttenuation, lightRadiusMask, spotAttenuation); - float attenuation = distanceAttenuation * lightRadiusMask * spotAttenuation; + float attenuation = 1; + GetRadialLightAttenuation(lightData, isSpotLight, N, distanceSqr, 1, toLight, L, NoL, attenuation); shadow.SurfaceShadow *= attenuation; shadow.TransmissionShadow *= attenuation; } diff --git a/Source/Shaders/LightingCommon.hlsl b/Source/Shaders/LightingCommon.hlsl index bb7a6ce7e..3479157b5 100644 --- a/Source/Shaders/LightingCommon.hlsl +++ b/Source/Shaders/LightingCommon.hlsl @@ -63,10 +63,9 @@ void GetRadialLightAttenuation( float3 toLight, float3 L, inout float NoL, - inout float distanceAttenuation, - inout float lightRadiusMask, - inout float spotAttenuation) + inout float attenuation) { + // Distance attenuation if (lightData.InverseSquared) { BRANCH @@ -77,29 +76,31 @@ void GetRadialLightAttenuation( float3 l1 = toLight + 0.5 * l01; float lengthL0 = length(l0); float lengthL1 = length(l1); - distanceAttenuation = rcp((lengthL0 * lengthL1 + dot(l0, l1)) * 0.5 + distanceBiasSqr); + attenuation = rcp((lengthL0 * lengthL1 + dot(l0, l1)) * 0.5 + distanceBiasSqr); NoL = saturate(0.5 * (dot(N, l0) / lengthL0 + dot(N, l1) / lengthL1)); } else { - distanceAttenuation = rcp(distanceSqr + distanceBiasSqr); + attenuation = rcp(distanceSqr + distanceBiasSqr); NoL = saturate(dot(N, L)); } - lightRadiusMask = Square(saturate(1 - Square(distanceSqr * Square(lightData.RadiusInv)))); + attenuation *= Square(saturate(1 - Square(distanceSqr * Square(lightData.RadiusInv)))); } else { - distanceAttenuation = 1; + attenuation = 1; NoL = saturate(dot(N, L)); float3 worldLightVector = toLight * lightData.RadiusInv; float t = dot(worldLightVector, worldLightVector); - lightRadiusMask = pow(1.0f - saturate(t), lightData.FalloffExponent); + attenuation *= pow(1.0f - saturate(t), lightData.FalloffExponent); } + // Spot mask attenuation if (isSpotLight) { - // SpotAngles.x is CosOuterCone, SpotAngles.y is InvCosConeDifference - spotAttenuation = Square(saturate((dot(normalize(-L), lightData.Direction) - lightData.SpotAngles.x) * lightData.SpotAngles.y)); + float cosOuterCone = lightData.SpotAngles.x; + float invCosConeDifference = lightData.SpotAngles.y; + attenuation *= Square(saturate((dot(normalize(-L), lightData.Direction) - cosOuterCone) * invCosConeDifference)); } } @@ -107,7 +108,6 @@ void GetRadialLightAttenuation( float AreaLightSpecular(LightData lightData, float roughness, inout float3 toLight, inout float3 L, float3 V, half3 N) { float energy = 1; - float m = roughness * roughness; float3 r = reflect(-V, N); float invDistToLight = rsqrt(dot(toLight, toLight)); @@ -137,7 +137,6 @@ float AreaLightSpecular(LightData lightData, float roughness, inout float3 toLig } L = normalize(toLight); - return energy; } diff --git a/Source/Shaders/VolumetricFog.shader b/Source/Shaders/VolumetricFog.shader index 2f5103736..b97fede24 100644 --- a/Source/Shaders/VolumetricFog.shader +++ b/Source/Shaders/VolumetricFog.shader @@ -205,9 +205,6 @@ float4 PS_InjectLight(Quad_GS2PS input) : SV_Target0 uint samplesCount = historyAlpha < 0.001f ? MissedHistorySamplesCount : 1; float NoL = 0; - float distanceAttenuation = 1; - float lightRadiusMask = 1; - float spotAttenuation = 1; bool isSpotLight = LocalLight.SpotAngles.x > -2.0f; float4 scattering = 0; for (uint sampleIndex = 0; sampleIndex < samplesCount; sampleIndex++) @@ -224,19 +221,19 @@ float4 PS_InjectLight(Quad_GS2PS input) : SV_Target0 float3 L = toLight * rsqrt(distanceSqr); // Calculate the light attenuation - GetRadialLightAttenuation(LocalLight, isSpotLight, float3(0, 0, 1), distanceSqr, distanceBias * distanceBias, toLight, L, NoL, distanceAttenuation, lightRadiusMask, spotAttenuation); - float combinedAttenuation = distanceAttenuation * lightRadiusMask * spotAttenuation; + float attenuation = 1; + GetRadialLightAttenuation(LocalLight, isSpotLight, float3(0, 0, 1), distanceSqr, distanceBias * distanceBias, toLight, L, NoL, attenuation); // Peek the shadow float shadowFactor = 1.0f; #if USE_SHADOW - if (combinedAttenuation > 0) + if (attenuation > 0) { shadowFactor = ComputeVolumeShadowing(positionWS, isSpotLight); } #endif - scattering.rgb += LocalLight.Color * (GetPhase(PhaseG, dot(L, -cameraVector)) * combinedAttenuation * shadowFactor * LocalLightScatteringIntensity); + scattering.rgb += LocalLight.Color * (GetPhase(PhaseG, dot(L, -cameraVector)) * attenuation * shadowFactor * LocalLightScatteringIntensity); } scattering.rgb /= (float)samplesCount; diff --git a/Source/ThirdParty/tracy/client/TracyProfiler.cpp b/Source/ThirdParty/tracy/client/TracyProfiler.cpp index cba3593da..dfbb22a83 100644 --- a/Source/ThirdParty/tracy/client/TracyProfiler.cpp +++ b/Source/ThirdParty/tracy/client/TracyProfiler.cpp @@ -369,10 +369,7 @@ static int64_t SetupHwTimer() #endif } #endif -#else -static int64_t SetupHwTimer() -{ -#endif + return Profiler::GetTime(); } #else @@ -4786,4 +4783,4 @@ TRACY_API void ___tracy_shutdown_profiler( void ) #endif #endif - +#endif diff --git a/Source/ThirdParty/tracy/tracy.Build.cs b/Source/ThirdParty/tracy/tracy.Build.cs index d6119a5ca..ca5f485b2 100644 --- a/Source/ThirdParty/tracy/tracy.Build.cs +++ b/Source/ThirdParty/tracy/tracy.Build.cs @@ -57,6 +57,7 @@ public class tracy : ThirdPartyModule files.Add(Path.Combine(FolderPath, "tracy", "Tracy.hpp")); files.Add(Path.Combine(FolderPath, "common", "TracySystem.hpp")); + files.Add(Path.Combine(FolderPath, "common", "TracyQueue.hpp")); files.Add(Path.Combine(FolderPath, "client", "TracyCallstack.h")); } } diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs index 205cefac3..eb35e475b 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs @@ -90,7 +90,7 @@ namespace Flax.Build.Bindings "Int4", }; - private static string GenerateCSharpDefaultValueNativeToManaged(BuildData buildData, string value, ApiTypeInfo caller, TypeInfo valueType = null, bool attribute = false) + private static string GenerateCSharpDefaultValueNativeToManaged(BuildData buildData, string value, ApiTypeInfo caller, TypeInfo valueType = null, bool attribute = false, string managedType = null) { if (string.IsNullOrEmpty(value)) return null; @@ -99,6 +99,13 @@ namespace Flax.Build.Bindings if (value.StartsWith("TEXT(\"") && value.EndsWith("\")")) return value.Substring(5, value.Length - 6); + // Pointer constant value + if (managedType != null && managedType == "IntPtr" && + (value == "nullptr" || value == "NULL" || value == "0")) + { + return "new IntPtr()"; + } + // In-built constants switch (value) { @@ -534,7 +541,7 @@ namespace Flax.Build.Bindings var separator = false; if (!functionInfo.IsStatic) { - contents.Append("IntPtr obj"); + contents.Append("IntPtr __obj"); separator = true; } @@ -1059,12 +1066,12 @@ namespace Flax.Build.Bindings else if (fieldInfo.IsStatic) contents.Append("static "); - var returnValueType = GenerateCSharpNativeToManaged(buildData, fieldInfo.Type, classInfo); - contents.Append(returnValueType).Append(' ').Append(fieldInfo.Name); + var managedType = GenerateCSharpNativeToManaged(buildData, fieldInfo.Type, classInfo); + contents.Append(managedType).Append(' ').Append(fieldInfo.Name); if (!useUnmanaged || fieldInfo.IsConstexpr) { - var defaultValue = GenerateCSharpDefaultValueNativeToManaged(buildData, fieldInfo.DefaultValue, classInfo, fieldInfo.Type); + var defaultValue = GenerateCSharpDefaultValueNativeToManaged(buildData, fieldInfo.DefaultValue, classInfo, fieldInfo.Type, false, managedType); if (!string.IsNullOrEmpty(defaultValue)) contents.Append(" = ").Append(defaultValue); contents.AppendLine(";"); @@ -1113,7 +1120,7 @@ namespace Flax.Build.Bindings { contents.Append(indent); if (propertyInfo.Access != propertyInfo.Getter.Access) - contents.Append(GenerateCSharpAccessLevel(propertyInfo.Access)); + contents.Append(GenerateCSharpAccessLevel(propertyInfo.Getter.Access)); contents.Append("get { "); GenerateCSharpWrapperFunctionCall(buildData, contents, classInfo, propertyInfo.Getter); contents.Append(" }").AppendLine(); @@ -1123,7 +1130,7 @@ namespace Flax.Build.Bindings { contents.Append(indent); if (propertyInfo.Access != propertyInfo.Setter.Access) - contents.Append(GenerateCSharpAccessLevel(propertyInfo.Access)); + contents.Append(GenerateCSharpAccessLevel(propertyInfo.Setter.Access)); contents.Append("set { "); GenerateCSharpWrapperFunctionCall(buildData, contents, classInfo, propertyInfo.Setter, true); contents.Append(" }").AppendLine(); @@ -1183,7 +1190,7 @@ namespace Flax.Build.Bindings contents.Append(' '); contents.Append(parameterInfo.Name); - var defaultValue = GenerateCSharpDefaultValueNativeToManaged(buildData, parameterInfo.DefaultValue, classInfo, parameterInfo.Type); + var defaultValue = GenerateCSharpDefaultValueNativeToManaged(buildData, parameterInfo.DefaultValue, classInfo, parameterInfo.Type, false, managedType); if (!string.IsNullOrEmpty(defaultValue)) contents.Append(" = ").Append(defaultValue); } @@ -1243,7 +1250,7 @@ namespace Flax.Build.Bindings contents.Append(' '); contents.Append(parameterInfo.Name); - var defaultValue = GenerateCSharpDefaultValueNativeToManaged(buildData, parameterInfo.DefaultValue, classInfo, parameterInfo.Type); + var defaultValue = GenerateCSharpDefaultValueNativeToManaged(buildData, parameterInfo.DefaultValue, classInfo, parameterInfo.Type, false, managedType); if (!string.IsNullOrEmpty(defaultValue)) contents.Append(" = ").Append(defaultValue); } @@ -1287,6 +1294,9 @@ namespace Flax.Build.Bindings /// /// Marshaller for type . /// "); + #if FLAX_EDITOR + [HideInEditor] + #endif [CustomMarshaller(typeof({{classInfo.Name}}), MarshalMode.ManagedToUnmanagedIn, typeof({{marshallerName}}.ManagedToNative))] [CustomMarshaller(typeof({{classInfo.Name}}), MarshalMode.UnmanagedToManagedOut, typeof({{marshallerName}}.ManagedToNative))] [CustomMarshaller(typeof({{classInfo.Name}}), MarshalMode.ElementIn, typeof({{marshallerName}}.ManagedToNative))] @@ -1299,17 +1309,25 @@ namespace Flax.Build.Bindings {{GenerateCSharpAccessLevel(classInfo.Access)}}static class {{marshallerName}} { #pragma warning disable 1591 + #if FLAX_EDITOR + [HideInEditor] + #endif public static class NativeToManaged { public static {{classInfo.Name}} ConvertToManaged(IntPtr unmanaged) => Unsafe.As<{{classInfo.Name}}>(ManagedHandleMarshaller.NativeToManaged.ConvertToManaged(unmanaged)); public static void Free(IntPtr unmanaged) => ManagedHandleMarshaller.NativeToManaged.Free(unmanaged); } + #if FLAX_EDITOR + [HideInEditor] + #endif public static class ManagedToNative { public static IntPtr ConvertToUnmanaged({{classInfo.Name}} managed) => ManagedHandleMarshaller.ManagedToNative.ConvertToUnmanaged(managed); public static void Free(IntPtr unmanaged) => ManagedHandleMarshaller.ManagedToNative.Free(unmanaged); } + #if FLAX_EDITOR [HideInEditor] + #endif public struct Bidirectional { ManagedHandleMarshaller.Bidirectional marsh; @@ -1358,6 +1376,8 @@ namespace Flax.Build.Bindings contents.Append(indent).AppendLine($"/// "); contents.Append(indent).AppendLine($"/// Marshaller for type ."); contents.Append(indent).AppendLine($"/// "); + if (buildData.Target != null & buildData.Target.IsEditor) + contents.Append(indent).AppendLine("[HideInEditor]"); contents.Append(indent).AppendLine($"[CustomMarshaller(typeof({structureInfo.Name}), MarshalMode.ManagedToUnmanagedIn, typeof({marshallerName}.ManagedToNative))]"); contents.Append(indent).AppendLine($"[CustomMarshaller(typeof({structureInfo.Name}), MarshalMode.UnmanagedToManagedOut, typeof({marshallerName}.ManagedToNative))]"); contents.Append(indent).AppendLine($"[CustomMarshaller(typeof({structureInfo.Name}), MarshalMode.ElementIn, typeof({marshallerName}.ManagedToNative))]"); @@ -1382,7 +1402,8 @@ namespace Flax.Build.Bindings // Native struct begin // TODO: skip using this utility structure if the auto-generated C# struct is already the same as XXXInternal here below GenerateCSharpAttributes(buildData, contents, indent, structureInfo, true); - contents.Append(indent).AppendLine("[HideInEditor]"); + if (buildData.Target != null & buildData.Target.IsEditor) + contents.Append(indent).AppendLine("[HideInEditor]"); contents.Append(indent).AppendLine("[StructLayout(LayoutKind.Sequential)]"); contents.Append(indent).Append("public struct ").Append(structureInfo.Name).Append("Internal"); if (structureInfo.BaseType != null && structureInfo.IsPod) @@ -1504,7 +1525,8 @@ namespace Flax.Build.Bindings if (fieldInfo.Type.IsObjectRef) { - toManagedContent.Append($"managed.{fieldInfo.Name} != IntPtr.Zero ? Unsafe.As<{fieldInfo.Type.GenericArgs[0].Type}>(ManagedHandle.FromIntPtr(managed.{fieldInfo.Name}).Target) : null"); + var managedType = GenerateCSharpNativeToManaged(buildData, fieldInfo.Type.GenericArgs[0], structureInfo); + toManagedContent.Append($"managed.{fieldInfo.Name} != IntPtr.Zero ? Unsafe.As<{managedType}>(ManagedHandle.FromIntPtr(managed.{fieldInfo.Name}).Target) : null"); toNativeContent.Append($"managed.{fieldInfo.Name} != null ? ManagedHandle.ToIntPtr(managed.{fieldInfo.Name}, GCHandleType.Weak) : IntPtr.Zero"); freeContents.AppendLine($"if (unmanaged.{fieldInfo.Name} != IntPtr.Zero) {{ ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}).Free(); }}"); @@ -1617,13 +1639,17 @@ namespace Flax.Build.Bindings // NativeToManaged stateless shape // NOTE: GCHandles of FlaxEngine.Object must not be released in this case - contents.Append(indent).AppendLine($"public static class NativeToManaged").Append(indent).AppendLine("{"); + if (buildData.Target != null && buildData.Target.IsEditor) + contents.Append(indent).AppendLine("[HideInEditor]"); + contents.Append(indent).AppendLine("public static class NativeToManaged").Append(indent).AppendLine("{"); contents.Append(indent2).AppendLine($"public static {structureInfo.Name} ConvertToManaged({structureInfo.Name}Internal unmanaged) => {marshallerName}.ToManaged(unmanaged);"); contents.Append(indent2).AppendLine($"public static void Free({structureInfo.Name}Internal unmanaged)"); contents.Append(indent2).AppendLine("{").Append(indent3).AppendLine(freeContents2.Replace("\n", "\n" + indent3).ToString().TrimEnd()).Append(indent2).AppendLine("}"); contents.Append(indent).AppendLine("}"); // ManagedToNative stateless shape + if (buildData.Target != null && buildData.Target.IsEditor) + contents.Append(indent).AppendLine("[HideInEditor]"); contents.Append(indent).AppendLine($"public static class ManagedToNative").Append(indent).AppendLine("{"); contents.Append(indent2).AppendLine($"public static {structureInfo.Name}Internal ConvertToUnmanaged({structureInfo.Name} managed) => {marshallerName}.ToNative(managed);"); contents.Append(indent2).AppendLine($"public static void Free({structureInfo.Name}Internal unmanaged) => {marshallerName}.Free(unmanaged);"); @@ -1631,7 +1657,8 @@ namespace Flax.Build.Bindings // Bidirectional stateful shape // NOTE: GCHandles of FlaxEngine.Object must not be released unless they were allocated by this marshaller - contents.Append(indent).AppendLine("[HideInEditor]"); + if (buildData.Target != null && buildData.Target.IsEditor) + contents.Append(indent).AppendLine("[HideInEditor]"); contents.Append(indent).AppendLine($"public struct Bidirectional").Append(indent).AppendLine("{"); contents.Append(indent2).AppendLine($"{structureInfo.Name} managed;"); contents.Append(indent2).AppendLine($"{structureInfo.Name}Internal unmanaged;"); @@ -1697,25 +1724,25 @@ namespace Flax.Build.Bindings else if (fieldInfo.IsStatic) contents.Append("static "); hasDefaultMember |= string.Equals(fieldInfo.Name, "Default", StringComparison.Ordinal); - string type; + string managedType; if (fieldInfo.Type.IsArray && (fieldInfo.NoArray || structureInfo.IsPod)) { fieldInfo.Type.IsArray = false; - type = GenerateCSharpNativeToManaged(buildData, fieldInfo.Type, structureInfo); + managedType = GenerateCSharpNativeToManaged(buildData, fieldInfo.Type, structureInfo); fieldInfo.Type.IsArray = true; #if USE_NETCORE // Use fixed statement with primitive types of buffers - if (type == "char") + if (managedType == "char") { // char's are not blittable, store as short instead - contents.Append($"fixed short {fieldInfo.Name}0[{fieldInfo.Type.ArraySize}]; // {type}*").AppendLine(); + contents.Append($"fixed short {fieldInfo.Name}0[{fieldInfo.Type.ArraySize}]; // {managedType}*").AppendLine(); } else #endif { // Padding in structs for fixed-size array - contents.Append(type).Append(' ').Append(fieldInfo.Name + "0;").AppendLine(); + contents.Append(managedType).Append(' ').Append(fieldInfo.Name + "0;").AppendLine(); for (int i = 1; i < fieldInfo.Type.ArraySize; i++) { contents.AppendLine(); @@ -1724,19 +1751,19 @@ namespace Flax.Build.Bindings contents.Append(indent).Append(GenerateCSharpAccessLevel(fieldInfo.Access)); if (fieldInfo.IsStatic) contents.Append("static "); - contents.Append(type).Append(' ').Append(fieldInfo.Name + i).Append(';').AppendLine(); + contents.Append(managedType).Append(' ').Append(fieldInfo.Name + i).Append(';').AppendLine(); } } continue; } - type = GenerateCSharpNativeToManaged(buildData, fieldInfo.Type, structureInfo); - contents.Append(type).Append(' ').Append(fieldInfo.Name); + managedType = GenerateCSharpNativeToManaged(buildData, fieldInfo.Type, structureInfo); + contents.Append(managedType).Append(' ').Append(fieldInfo.Name); if (fieldInfo.IsConstexpr) { // Compile-time constant - var defaultValue = GenerateCSharpDefaultValueNativeToManaged(buildData, fieldInfo.DefaultValue, structureInfo, fieldInfo.Type); + var defaultValue = GenerateCSharpDefaultValueNativeToManaged(buildData, fieldInfo.DefaultValue, structureInfo, fieldInfo.Type, false, managedType); if (!string.IsNullOrEmpty(defaultValue)) contents.Append(" = ").Append(defaultValue); contents.AppendLine(";"); @@ -1950,7 +1977,7 @@ namespace Flax.Build.Bindings contents.Append(' '); contents.Append(parameterInfo.Name); - var defaultValue = GenerateCSharpDefaultValueNativeToManaged(buildData, parameterInfo.DefaultValue, interfaceInfo, parameterInfo.Type); + var defaultValue = GenerateCSharpDefaultValueNativeToManaged(buildData, parameterInfo.DefaultValue, interfaceInfo, parameterInfo.Type, false, managedType); if (!string.IsNullOrEmpty(defaultValue)) contents.Append(" = ").Append(defaultValue); } @@ -1968,9 +1995,11 @@ namespace Flax.Build.Bindings { string marshallerName = interfaceInfo.Name + "Marshaller"; contents.AppendLine(); - contents.Append(indent).AppendLine($"/// "); + contents.Append(indent).AppendLine("/// "); contents.Append(indent).AppendLine($"/// Marshaller for type ."); - contents.Append(indent).AppendLine($"/// "); + contents.Append(indent).AppendLine("/// "); + if (buildData.Target != null & buildData.Target.IsEditor) + contents.Append(indent).AppendLine("[HideInEditor]"); contents.Append(indent).AppendLine($"[CustomMarshaller(typeof({interfaceInfo.Name}), MarshalMode.Default, typeof({marshallerName}))]"); contents.Append(indent).AppendLine($"public static class {marshallerName}"); contents.Append(indent).AppendLine("{"); diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index 665063613..2c2c9b341 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -1017,7 +1017,7 @@ namespace Flax.Build.Bindings var signatureStart = contents.Length; if (!functionInfo.IsStatic) { - contents.Append(caller.Name).Append("* obj"); + contents.Append(caller.Name).Append("* __obj"); separator = true; } @@ -1127,7 +1127,7 @@ namespace Flax.Build.Bindings contents.Append(indent).AppendLine($"MSVC_FUNC_EXPORT(\"{libraryEntryPoint}\")"); // Export generated function binding under the C# name #endif if (!functionInfo.IsStatic) - contents.Append(indent).AppendLine("if (obj == nullptr) DebugLog::ThrowNullReference();"); + contents.Append(indent).AppendLine("if (__obj == nullptr) DebugLog::ThrowNullReference();"); string callBegin = indent; if (functionInfo.Glue.UseReferenceForResult) @@ -1165,7 +1165,7 @@ namespace Flax.Build.Bindings else { // Call native member method - call = $"obj->{functionInfo.Name}"; + call = $"__obj->{functionInfo.Name}"; } string callParams = string.Empty; separator = false; @@ -1925,7 +1925,7 @@ namespace Flax.Build.Bindings continue; var paramsCount = eventInfo.Type.GenericArgs?.Count ?? 0; CppIncludeFiles.Add("Engine/Profiler/ProfilerCPU.h"); - var bindPrefix = eventInfo.IsStatic ? classTypeNameNative + "::" : "obj->"; + var bindPrefix = eventInfo.IsStatic ? classTypeNameNative + "::" : "__obj->"; if (useCSharp) { @@ -2006,7 +2006,7 @@ namespace Flax.Build.Bindings if (buildData.Toolchain?.Compiler == TargetCompiler.Clang) useSeparateImpl = true; // DLLEXPORT doesn't properly export function thus separate implementation from declaration if (!eventInfo.IsStatic) - contents.AppendFormat("{0}* obj, ", classTypeNameNative); + contents.AppendFormat("{0}* __obj, ", classTypeNameNative); contents.Append("bool bind)"); var contentsPrev = contents; var indent = " "; @@ -2034,7 +2034,7 @@ namespace Flax.Build.Bindings if (eventInfo.IsStatic) contents.Append(indent).AppendFormat(" f.Bind<{0}_ManagedWrapper>();", eventInfo.Name).AppendLine(); else - contents.Append(indent).AppendFormat(" f.Bind<{1}, &{1}::{0}_ManagedWrapper>(({1}*)obj);", eventInfo.Name, internalTypeName).AppendLine(); + contents.Append(indent).AppendFormat(" f.Bind<{1}, &{1}::{0}_ManagedWrapper>(({1}*)__obj);", eventInfo.Name, internalTypeName).AppendLine(); contents.Append(indent).Append(" if (bind)").AppendLine(); contents.Append(indent).AppendFormat(" {0}{1}.Bind(f);", bindPrefix, eventInfo.Name).AppendLine(); contents.Append(indent).Append(" else").AppendLine(); @@ -2074,7 +2074,7 @@ namespace Flax.Build.Bindings // Scripting event wrapper binding method (binds/unbinds generic wrapper to C++ delegate) contents.AppendFormat(" static void {0}_Bind(", eventInfo.Name); - contents.AppendFormat("{0}* obj, void* instance, bool bind)", classTypeNameNative).AppendLine(); + contents.AppendFormat("{0}* __obj, void* instance, bool bind)", classTypeNameNative).AppendLine(); contents.Append(" {").AppendLine(); contents.Append(" FunctionGetTypeHandle();"); contents.AppendLine(" while (typeHandle)"); contents.AppendLine(" {"); - contents.AppendLine($" auto method = typeHandle.Module->FindMethod(typeHandle, \"{functionInfo.Name}\", {functionInfo.Parameters.Count});"); + contents.AppendLine($" auto method = typeHandle.Module->FindMethod(typeHandle, StringAnsiView(\"{functionInfo.Name}\", {functionInfo.Name.Length}), {functionInfo.Parameters.Count});"); contents.AppendLine(" if (method)"); contents.AppendLine(" {"); contents.AppendLine(" Variant __result;"); @@ -2565,10 +2565,10 @@ namespace Flax.Build.Bindings contents.AppendLine(" }").AppendLine(); // Interface implementation wrapper accessor for scripting types - contents.AppendLine(" static void* GetInterfaceWrapper(ScriptingObject* obj)"); + contents.AppendLine(" static void* GetInterfaceWrapper(ScriptingObject* __obj)"); contents.AppendLine(" {"); contents.AppendLine($" auto wrapper = New<{interfaceTypeNameInternal}Wrapper>();"); - contents.AppendLine(" wrapper->Object = obj;"); + contents.AppendLine(" wrapper->Object = __obj;"); contents.AppendLine(" return wrapper;"); contents.AppendLine(" }"); diff --git a/Source/Tools/Flax.Build/Build/NativeCpp/CompileEnvironment.cs b/Source/Tools/Flax.Build/Build/NativeCpp/CompileEnvironment.cs index 9ca96f4fe..72d0380e4 100644 --- a/Source/Tools/Flax.Build/Build/NativeCpp/CompileEnvironment.cs +++ b/Source/Tools/Flax.Build/Build/NativeCpp/CompileEnvironment.cs @@ -157,6 +157,11 @@ namespace Flax.Build.NativeCpp /// public readonly List IncludePaths = new List(); + /// + /// The collection of custom arguments to pass to the compilator. + /// + public readonly HashSet CustomArgs = new HashSet(); + /// public object Clone() { @@ -183,6 +188,7 @@ namespace Flax.Build.NativeCpp }; clone.PreprocessorDefinitions.AddRange(PreprocessorDefinitions); clone.IncludePaths.AddRange(IncludePaths); + clone.CustomArgs.AddRange(CustomArgs); return clone; } } diff --git a/Source/Tools/Flax.Build/Build/NativeCpp/LinkEnvironment.cs b/Source/Tools/Flax.Build/Build/NativeCpp/LinkEnvironment.cs index 34b7df03f..92f08d71f 100644 --- a/Source/Tools/Flax.Build/Build/NativeCpp/LinkEnvironment.cs +++ b/Source/Tools/Flax.Build/Build/NativeCpp/LinkEnvironment.cs @@ -106,6 +106,11 @@ namespace Flax.Build.NativeCpp /// public readonly List LibraryPaths = new List(); + /// + /// The collection of custom arguments to pass to the linker. + /// + public readonly HashSet CustomArgs = new HashSet(); + /// public object Clone() { @@ -127,6 +132,7 @@ namespace Flax.Build.NativeCpp clone.DocumentationFiles.AddRange(DocumentationFiles); clone.InputLibraries.AddRange(InputLibraries); clone.LibraryPaths.AddRange(LibraryPaths); + clone.CustomArgs.AddRange(CustomArgs); return clone; } } diff --git a/Source/Tools/Flax.Build/Platforms/Apple/AppleToolchain.cs b/Source/Tools/Flax.Build/Platforms/Apple/AppleToolchain.cs index bf7c0988c..6a0f42cfd 100644 --- a/Source/Tools/Flax.Build/Platforms/Apple/AppleToolchain.cs +++ b/Source/Tools/Flax.Build/Platforms/Apple/AppleToolchain.cs @@ -96,6 +96,7 @@ namespace Flax.Build.Platforms // Setup arguments shared by all source files var commonArgs = new List(); + commonArgs.AddRange(options.CompileEnv.CustomArgs); { commonArgs.Add("-c"); commonArgs.Add("-fmessage-length=0"); @@ -235,6 +236,7 @@ namespace Flax.Build.Platforms // Setup arguments var args = new List(); + args.AddRange(options.LinkEnv.CustomArgs); { args.Add(string.Format("-o \"{0}\"", outputFilePath)); AddArgsCommon(options, args); diff --git a/Source/Tools/Flax.Build/Platforms/Unix/UnixToolchain.cs b/Source/Tools/Flax.Build/Platforms/Unix/UnixToolchain.cs index 68520ef44..fc409858a 100644 --- a/Source/Tools/Flax.Build/Platforms/Unix/UnixToolchain.cs +++ b/Source/Tools/Flax.Build/Platforms/Unix/UnixToolchain.cs @@ -325,6 +325,7 @@ namespace Flax.Build.Platforms // Setup arguments shared by all source files var commonArgs = new List(); + commonArgs.AddRange(options.CompileEnv.CustomArgs); SetupCompileCppFilesArgs(graph, options, commonArgs, outputPath); { commonArgs.Add("-c"); @@ -511,6 +512,7 @@ namespace Flax.Build.Platforms // Setup arguments var args = new List(); + args.AddRange(options.LinkEnv.CustomArgs); { args.Add(string.Format("-o \"{0}\"", outputFilePath)); diff --git a/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchainBase.cs b/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchainBase.cs index 824b1b891..e7dbaac19 100644 --- a/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchainBase.cs +++ b/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchainBase.cs @@ -428,6 +428,7 @@ namespace Flax.Build.Platforms // Setup arguments shared by all source files var commonArgs = new List(); + commonArgs.AddRange(options.CompileEnv.CustomArgs); SetupCompileCppFilesArgs(graph, options, commonArgs); { // Suppress Startup Banner @@ -669,6 +670,7 @@ namespace Flax.Build.Platforms // Setup arguments var args = new List(); + args.AddRange(options.LinkEnv.CustomArgs); SetupLinkFilesArgs(graph, options, args); { // Suppress startup banner