Merge commit 'f2ecefb7ee9b9e6c5daac9f44fe40ebdccbb1c76' into 1.6
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@ Cache/
|
||||
Binaries/
|
||||
Output/
|
||||
Logs/
|
||||
Source/*.Gen.*
|
||||
Source/*.csproj
|
||||
/Package_*/
|
||||
!Source/Engine/Debug
|
||||
|
||||
BIN
Content/Shaders/Lights.flax
(Stored with Git LFS)
BIN
Content/Shaders/Lights.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Shaders/VolumetricFog.flax
(Stored with Git LFS)
BIN
Content/Shaders/VolumetricFog.flax
(Stored with Git LFS)
Binary file not shown.
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,7 +360,7 @@ namespace FlaxEditor.Content
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the tooltip text.
|
||||
/// Updates the tooltip text text.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool DrawShadow => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void UpdateTooltipText()
|
||||
{
|
||||
TooltipText = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace FlaxEditor.Content
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string NewItemName => "Script";
|
||||
public override string NewItemName => "MyScript";
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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++)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace FlaxEditor.CustomEditors.Dedicated
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.CustomEditors.Editors.GenericEditor" />
|
||||
[CustomEditor(typeof(Actor)), DefaultEditor]
|
||||
public class ActorEditor : GenericEditor
|
||||
public class ActorEditor : ScriptingObjectEditor
|
||||
{
|
||||
private Guid _linkedPrefabId;
|
||||
|
||||
|
||||
@@ -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<ParticleEffect> func)
|
||||
{
|
||||
foreach (var value in Values)
|
||||
{
|
||||
if (value is ParticleEffect player)
|
||||
func(player);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<UniformGridPanel>();
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEditor.CustomEditors.Editors;
|
||||
using FlaxEngine.Networking;
|
||||
|
||||
namespace FlaxEditor.CustomEditors.Dedicated
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom editor for <see cref="FlaxEngine.Object"/>.
|
||||
/// </summary>
|
||||
public class ScriptingObjectEditor : GenericEditor
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -78,7 +78,7 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
/// <seealso cref="FlaxEditor.CustomEditors.Editors.Float3Editor" />
|
||||
public class ScaleEditor : Float3Editor
|
||||
{
|
||||
private Image _linkImage;
|
||||
private Button _linkButton;
|
||||
|
||||
/// <inheritdoc />
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -437,9 +430,32 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
base.Refresh();
|
||||
|
||||
if (!HasDifferentValues)
|
||||
{
|
||||
_element.CustomControl.Value = (ScriptType)Values[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of the inspector used to edit reference to the <see cref="FlaxEngine.SoftTypeReference"/>. Used to pick classes.
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(SoftTypeReference)), DefaultEditor]
|
||||
public class SoftTypeReferenceEditor : TypeEditorBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Initialize(LayoutElementsContainer layout)
|
||||
{
|
||||
base.Initialize(layout);
|
||||
|
||||
if (_element != null)
|
||||
_element.CustomControl.ValueChanged += () => SetValue(new SoftTypeReference(_element.CustomControl.ValueTypeName));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Refresh()
|
||||
{
|
||||
base.Refresh();
|
||||
|
||||
if (!HasDifferentValues)
|
||||
_element.CustomControl.ValueTypeName = ((SoftTypeReference)Values[0]).TypeName;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,25 @@ namespace FlaxEditor.GUI.ContextMenu
|
||||
_isSubMenu = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the empty menu popup o na screen.
|
||||
/// </summary>
|
||||
/// <param name="control">The target control.</param>
|
||||
/// <param name="area">The target control area to cover.</param>
|
||||
/// <returns>Created popup.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show context menu over given control.
|
||||
/// </summary>
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
base.Update(deltaTime);
|
||||
|
||||
// Update eye dropper tool
|
||||
if (_activeEyedropper)
|
||||
{
|
||||
Float2 mousePosition = Platform.MousePosition;
|
||||
SelectedColor = ScreenUtilities.GetColorAt(mousePosition);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Draw()
|
||||
{
|
||||
@@ -274,6 +324,20 @@ namespace FlaxEditor.GUI.Dialogs
|
||||
base.OnShow();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnSubmit()
|
||||
{
|
||||
|
||||
@@ -57,6 +57,11 @@ namespace FlaxEditor.GUI.Input
|
||||
/// </summary>
|
||||
protected Color _value;
|
||||
|
||||
/// <summary>
|
||||
/// Enables live preview of the selected value from the picker. Otherwise will update the value only when user confirms it on dialog closing.
|
||||
/// </summary>
|
||||
public bool UseDynamicEditing = true;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when value gets changed.
|
||||
/// </summary>
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace FlaxEditor.Surface.Elements
|
||||
{
|
||||
ParentNode = parentNode;
|
||||
Archetype = archetype;
|
||||
|
||||
UseDynamicEditing = false;
|
||||
ParentNode.ValuesChanged += OnNodeValuesChanged;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
130
Source/Editor/Utilities/ScreenUtilities.cpp
Normal file
130
Source/Editor/Utilities/ScreenUtilities.cpp
Normal file
@@ -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<Color32> ScreenUtilities::PickColorDone;
|
||||
|
||||
#if PLATFORM_WINDOWS
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#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 <Cocoa/Cocoa.h>
|
||||
#include <AppKit/AppKit.h>
|
||||
|
||||
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
|
||||
33
Source/Editor/Utilities/ScreenUtilities.h
Normal file
33
Source/Editor/Utilities/ScreenUtilities.h
Normal file
@@ -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"
|
||||
|
||||
/// <summary>
|
||||
/// Platform-dependent screen utilities.
|
||||
/// </summary>
|
||||
API_CLASS(Static) class FLAXENGINE_API ScreenUtilities
|
||||
{
|
||||
DECLARE_SCRIPTING_TYPE_MINIMAL(ScreenUtilities);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pixel color at the specified coordinates.
|
||||
/// </summary>
|
||||
/// <param name="pos">Screen-space coordinate to read.</param>
|
||||
/// <returns>Pixel color at the specified coordinates.</returns>
|
||||
API_FUNCTION() static Color32 GetColorAt(const Float2& pos);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
API_FUNCTION() static void PickColor();
|
||||
|
||||
/// <summary>
|
||||
/// Called when PickColor action is finished.
|
||||
/// </summary>
|
||||
API_EVENT() static Delegate<Color32> PickColorDone;
|
||||
};
|
||||
@@ -612,7 +612,16 @@ namespace FlaxEditor.Windows
|
||||
/// <param name="files">The files paths to import.</param>
|
||||
public void Paste(string[] files)
|
||||
{
|
||||
Editor.ContentImporting.Import(files, CurrentViewFolder);
|
||||
var importFiles = new List<string>();
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace FlaxEditor.Windows.Profiler
|
||||
|
||||
private static string FormatSampleBytes(float v)
|
||||
{
|
||||
return (uint)v + " bytes";
|
||||
return Utilities.Utils.FormatBytesCount((ulong)v);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -214,7 +214,7 @@ public:
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the item.</param>
|
||||
/// <param name="value">The value to set.</param>
|
||||
void Set(int32 index, bool value) const
|
||||
void Set(int32 index, bool value)
|
||||
{
|
||||
ASSERT(index >= 0 && index < _count);
|
||||
const ItemType offset = index / sizeof(ItemType);
|
||||
|
||||
@@ -203,23 +203,12 @@ public:
|
||||
return _function != nullptr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls the binded function if any has been assigned.
|
||||
/// </summary>
|
||||
/// <param name="params">A list of parameters for the function invocation.</param>
|
||||
/// <returns>Function result</returns>
|
||||
void TryCall(Params ... params) const
|
||||
{
|
||||
if (_function)
|
||||
_function(_callee, Forward<Params>(params)...);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls the binded function (it must be assigned).
|
||||
/// </summary>
|
||||
/// <param name="params">A list of parameters for the function invocation.</param>
|
||||
/// <returns>Function result</returns>
|
||||
ReturnType operator()(Params ... params) const
|
||||
FORCE_INLINE ReturnType operator()(Params ... params) const
|
||||
{
|
||||
ASSERT(_function);
|
||||
return _function(_callee, Forward<Params>(params)...);
|
||||
|
||||
@@ -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<const Char*> Utilities::Private::BytesSizes(BytesSizesData, ARRAY_COUNT(BytesSizesData));
|
||||
Span<const Char*> Utilities::Private::HertzSizes(HertzSizesData, ARRAY_COUNT(HertzSizesData));
|
||||
|
||||
namespace ObjectsRemovalServiceImpl
|
||||
{
|
||||
CriticalSection PoolLocker;
|
||||
|
||||
@@ -4,12 +4,19 @@
|
||||
|
||||
#include "Types/BaseTypes.h"
|
||||
#include "Types/String.h"
|
||||
#include "Types/Span.h"
|
||||
#if _MSC_VER && PLATFORM_SIMD_SSE4_2
|
||||
#include <intrin.h>
|
||||
#endif
|
||||
|
||||
namespace Utilities
|
||||
{
|
||||
struct Private
|
||||
{
|
||||
static FLAXENGINE_API Span<const Char*> BytesSizes;
|
||||
static FLAXENGINE_API Span<const Char*> HertzSizes;
|
||||
};
|
||||
|
||||
// Round floating point value up to 1 decimal place
|
||||
template<typename T>
|
||||
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<typename T>
|
||||
String UnitsToText(T units, int32 divider, const Span<const Char*> sizes)
|
||||
{
|
||||
if (sizes.Length() == 0)
|
||||
return String::Format(TEXT("{0}"), units);
|
||||
int32 i = 0;
|
||||
double dblSUnits = static_cast<double>(units);
|
||||
for (; static_cast<uint64>(units / static_cast<double>(divider)) > 0; i++, units /= divider)
|
||||
dblSUnits = units / static_cast<double>(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<typename T>
|
||||
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<double>(bytes);
|
||||
for (; static_cast<uint64>(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<typename T>
|
||||
String HertzToText(T hertz)
|
||||
{
|
||||
return UnitsToText(hertz, 1000, Private::HertzSizes);
|
||||
}
|
||||
|
||||
// Returns the amount of set bits in 32-bit integer.
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -54,6 +54,11 @@ public:
|
||||
/// </summary>
|
||||
static Action LateUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// Event called after engine update.
|
||||
/// </summary>
|
||||
static Action LateFixedUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// Event called during frame rendering and can be used to invoke custom rendering with GPUDevice.
|
||||
/// </summary>
|
||||
@@ -107,6 +112,11 @@ public:
|
||||
/// </summary>
|
||||
static void OnLateUpdate();
|
||||
|
||||
/// <summary>
|
||||
/// Late fixed update callback.
|
||||
/// </summary>
|
||||
static void OnLateFixedUpdate();
|
||||
|
||||
/// <summary>
|
||||
/// Draw callback.
|
||||
/// </summary>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -16,6 +16,9 @@ namespace FlaxEngine.Interop
|
||||
/// <summary>
|
||||
/// Wrapper for managed arrays which are passed to unmanaged code.
|
||||
/// </summary>
|
||||
#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
|
||||
/// <summary>
|
||||
/// Handle to managed objects which can be stored in native code.
|
||||
/// </summary>
|
||||
#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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<FlaxEngine.Object>(ManagedHandle.FromIntPtr(unmanaged).Target) : null;
|
||||
}
|
||||
|
||||
#if FLAX_EDITOR
|
||||
[HideInEditor]
|
||||
#endif
|
||||
public static class ManagedToNative
|
||||
{
|
||||
public static IntPtr ConvertToUnmanaged(FlaxEngine.Object managed) => Unsafe.As<object>(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<T, U>
|
||||
{
|
||||
#if FLAX_EDITOR
|
||||
[HideInEditor]
|
||||
#endif
|
||||
public static class NativeToManaged
|
||||
{
|
||||
public static Dictionary<T, U> ConvertToManaged(IntPtr unmanaged) => DictionaryMarshaller<T, U>.ToManaged(unmanaged);
|
||||
public static void Free(IntPtr unmanaged) => DictionaryMarshaller<T, U>.Free(unmanaged);
|
||||
}
|
||||
|
||||
#if FLAX_EDITOR
|
||||
[HideInEditor]
|
||||
#endif
|
||||
public static class ManagedToNative
|
||||
{
|
||||
public static IntPtr ConvertToUnmanaged(Dictionary<T, U> managed) => DictionaryMarshaller<T, U>.ToNative(managed, GCHandleType.Weak);
|
||||
|
||||
public static void Free(IntPtr unmanaged)
|
||||
{
|
||||
//DictionaryMarshaller<T, U>.Free(unmanaged); // No need to free weak handles
|
||||
}
|
||||
}
|
||||
|
||||
#if FLAX_EDITOR
|
||||
[HideInEditor]
|
||||
#endif
|
||||
public struct Bidirectional
|
||||
{
|
||||
Dictionary<T, U> 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<T, TUnmanagedElement> 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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<T>.toManagedFieldMarshallers[i](MarshalHelper<T>.marshallableFields[i], ref managedValue, fieldPtr, out int fieldOffset);
|
||||
fieldPtr += fieldOffset;
|
||||
}
|
||||
Assert.IsTrue((fieldPtr - nativePtr) == Unsafe.SizeOf<T>());
|
||||
Assert.IsTrue((fieldPtr - nativePtr) <= Unsafe.SizeOf<T>());
|
||||
}
|
||||
else
|
||||
managedValue = Unsafe.Read<T>(nativePtr.ToPointer());
|
||||
@@ -724,7 +745,7 @@ namespace FlaxEngine.Interop
|
||||
MarshalHelper<T>.toNativeFieldMarshallers[i](MarshalHelper<T>.marshallableFields[i], ref managedValue, nativePtr, out int fieldOffset);
|
||||
nativePtr += fieldOffset;
|
||||
}
|
||||
Assert.IsTrue((nativePtr - fieldPtr) == Unsafe.SizeOf<T>());
|
||||
Assert.IsTrue((nativePtr - fieldPtr) <= Unsafe.SizeOf<T>());
|
||||
}
|
||||
else
|
||||
Unsafe.AsRef<T>(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
|
||||
|
||||
@@ -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<GPUAdapterDX> 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;
|
||||
}
|
||||
|
||||
@@ -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<GPUAdapterDX> 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,9 +40,11 @@ typedef IGraphicsUnknown IDXGISwapChain3;
|
||||
#include <d3d11_1.h>
|
||||
#include <dxgi1_3.h>
|
||||
#include <dxgi1_5.h>
|
||||
#include <dxgi1_6.h>
|
||||
#endif
|
||||
#if GRAPHICS_API_DIRECTX12
|
||||
#include <dxgi1_5.h>
|
||||
#include <dxgi1_6.h>
|
||||
#endif
|
||||
|
||||
#pragma comment(lib, "DXGI.lib")
|
||||
|
||||
@@ -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<GPUDeviceVulkan>(ShaderProfile::Vulkan_SM5, New<GPUAdapterVulkan>(adapters[selectedAdapter]));
|
||||
auto device = New<GPUDeviceVulkan>(ShaderProfile::Vulkan_SM5, New<GPUAdapterVulkan>(adapters[selectedAdapterIndex]));
|
||||
if (device->Init())
|
||||
{
|
||||
LOG(Warning, "Graphics Device init failed");
|
||||
|
||||
@@ -263,6 +263,37 @@ API_ENUM() enum class InputActionMode
|
||||
Release = 2,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The input action event phases.
|
||||
/// </summary>
|
||||
API_ENUM() enum class InputActionState
|
||||
{
|
||||
/// <summary>
|
||||
/// The key/button is not assigned.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The key/button is waiting for input.
|
||||
/// </summary>
|
||||
Waiting = 1,
|
||||
|
||||
/// <summary>
|
||||
/// User is pressing the key/button.
|
||||
/// </summary>
|
||||
Pressing = 2,
|
||||
|
||||
/// <summary>
|
||||
/// User pressed the key/button (but wasn't pressing it in the previous frame).
|
||||
/// </summary>
|
||||
Press = 3,
|
||||
|
||||
/// <summary>
|
||||
/// User released the key/button (was pressing it in the previous frame).
|
||||
/// </summary>
|
||||
Release = 4,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The input gamepad index.
|
||||
/// </summary>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -309,6 +309,14 @@ public:
|
||||
/// <seealso cref="ActionMappings"/>
|
||||
API_FUNCTION() static bool GetAction(const StringView& name);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of the virtual action identified by name. Use <see cref="ActionMappings"/> to get the current config.
|
||||
/// </summary>
|
||||
/// <param name="name">The action name.</param>
|
||||
/// <returns>A InputActionPhase determining the current phase of the Action (e.g If it was just pressed, is being held or just released).</returns>
|
||||
/// <seealso cref="ActionMappings"/>
|
||||
API_FUNCTION() static InputActionState GetActionState(const StringView& name);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of the virtual axis identified by name. Use <see cref="AxisMappings"/> to get the current config.
|
||||
/// </summary>
|
||||
|
||||
@@ -26,9 +26,8 @@ protected:
|
||||
|
||||
public:
|
||||
/// <summary>
|
||||
/// Gets the text entered during the current frame.
|
||||
/// Gets the text entered during the current frame (Unicode format).
|
||||
/// </summary>
|
||||
/// <returns>The input text (Unicode).</returns>
|
||||
API_PROPERTY() StringView GetInputText() const
|
||||
{
|
||||
return StringView(_state.InputText, _state.InputTextLength);
|
||||
|
||||
@@ -58,7 +58,6 @@ public:
|
||||
/// <summary>
|
||||
/// Gets the position of the mouse in the screen-space coordinates.
|
||||
/// </summary>
|
||||
/// <returns>The mouse position</returns>
|
||||
API_PROPERTY() FORCE_INLINE Float2 GetPosition() const
|
||||
{
|
||||
return _state.MousePosition;
|
||||
@@ -72,7 +71,6 @@ public:
|
||||
/// <summary>
|
||||
/// Gets the delta position of the mouse in the screen-space coordinates.
|
||||
/// </summary>
|
||||
/// <returns>The mouse position delta</returns>
|
||||
API_PROPERTY() FORCE_INLINE Float2 GetPositionDelta() const
|
||||
{
|
||||
return _state.MousePosition - _prevState.MousePosition;
|
||||
@@ -81,7 +79,6 @@ public:
|
||||
/// <summary>
|
||||
/// Gets the mouse wheel change during the last frame.
|
||||
/// </summary>
|
||||
/// <returns>Mouse wheel value delta</returns>
|
||||
API_PROPERTY() FORCE_INLINE float GetScrollDelta() const
|
||||
{
|
||||
return _state.MouseWheelDelta;
|
||||
|
||||
@@ -33,7 +33,7 @@ public:
|
||||
/// <summary>
|
||||
/// Controls the radial falloff of light when UseInverseSquaredFalloff is disabled.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
@@ -62,9 +62,8 @@ public:
|
||||
|
||||
public:
|
||||
/// <summary>
|
||||
/// Computes light brightness value
|
||||
/// Computes light brightness value.
|
||||
/// </summary>
|
||||
/// <returns>Brightness</returns>
|
||||
float ComputeBrightness() const;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -32,7 +32,7 @@ public:
|
||||
/// <summary>
|
||||
/// Controls the radial falloff of light when UseInverseSquaredFalloff is disabled.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -121,6 +121,19 @@ void SceneTicking::LateUpdateTickData::TickScripts(const Array<Script*>& scripts
|
||||
}
|
||||
}
|
||||
|
||||
SceneTicking::LateFixedUpdateTickData::LateFixedUpdateTickData()
|
||||
: TickData(64)
|
||||
{
|
||||
}
|
||||
|
||||
void SceneTicking::LateFixedUpdateTickData::TickScripts(const Array<Script*>& 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();
|
||||
}
|
||||
|
||||
@@ -109,6 +109,13 @@ public:
|
||||
void TickScripts(const Array<Script*>& scripts) override;
|
||||
};
|
||||
|
||||
class FLAXENGINE_API LateFixedUpdateTickData : public TickData
|
||||
{
|
||||
public:
|
||||
LateFixedUpdateTickData();
|
||||
void TickScripts(const Array<Script*>& scripts) override;
|
||||
};
|
||||
|
||||
public:
|
||||
/// <summary>
|
||||
/// Adds the script to scene ticking system.
|
||||
@@ -142,4 +149,9 @@ public:
|
||||
/// The late update tick function.
|
||||
/// </summary>
|
||||
LateUpdateTickData LateUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// The late fixed update tick function.
|
||||
/// </summary>
|
||||
LateFixedUpdateTickData LateFixedUpdate;
|
||||
};
|
||||
|
||||
@@ -13,22 +13,27 @@ API_INTERFACE(Namespace = "FlaxEngine.Networking") class FLAXENGINE_API INetwork
|
||||
DECLARE_SCRIPTING_TYPE_MINIMAL(INetworkObject);
|
||||
public:
|
||||
/// <summary>
|
||||
/// Event called when network objects gets spawned.
|
||||
/// Event called when network object gets spawned.
|
||||
/// </summary>
|
||||
API_FUNCTION() virtual void OnNetworkSpawn() = 0;
|
||||
API_FUNCTION() virtual void OnNetworkSpawn() {};
|
||||
|
||||
/// <summary>
|
||||
/// Event called when network objects gets despawned.
|
||||
/// Event called when network object gets despawned.
|
||||
/// </summary>
|
||||
API_FUNCTION() virtual void OnNetworkDespawn() = 0;
|
||||
API_FUNCTION() virtual void OnNetworkDespawn() {};
|
||||
|
||||
/// <summary>
|
||||
/// Event called before network object gets replicated (before reading data).
|
||||
/// </summary>
|
||||
API_FUNCTION() virtual void OnNetworkSerialize() = 0;
|
||||
API_FUNCTION() virtual void OnNetworkSerialize() {};
|
||||
|
||||
/// <summary>
|
||||
/// Event called when network objects gets replicated (after reading data).
|
||||
/// Event called when network object gets replicated (after reading data).
|
||||
/// </summary>
|
||||
API_FUNCTION() virtual void OnNetworkDeserialize() = 0;
|
||||
API_FUNCTION() virtual void OnNetworkDeserialize() {};
|
||||
|
||||
/// <summary>
|
||||
/// Event called when network object gets synced (called only once upon initial sync).
|
||||
/// </summary>
|
||||
API_FUNCTION() virtual void OnNetworkSync() {};
|
||||
};
|
||||
|
||||
@@ -317,11 +317,12 @@ bool NetworkManager::StartHost()
|
||||
LocalClient = New<NetworkClient>(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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
205
Source/Engine/Networking/NetworkReplicationHierarchy.cpp
Normal file
205
Source/Engine/Networking/NetworkReplicationHierarchy.cpp
Normal file
@@ -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<Actor>(Object);
|
||||
if (!actor)
|
||||
{
|
||||
if (const auto* sceneObject = ScriptingObject::Cast<SceneObject>(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<int32>(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<NetworkReplicationNode>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Source/Engine/Networking/NetworkReplicationHierarchy.cs
Normal file
25
Source/Engine/Networking/NetworkReplicationHierarchy.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
||||
|
||||
namespace FlaxEngine.Networking
|
||||
{
|
||||
partial struct NetworkReplicationHierarchyObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the actors context (object itself or parent actor).
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
260
Source/Engine/Networking/NetworkReplicationHierarchy.h
Normal file
260
Source/Engine/Networking/NetworkReplicationHierarchy.h
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Network replication hierarchy object data.
|
||||
/// </summary>
|
||||
API_STRUCT(NoDefault, Namespace = "FlaxEngine.Networking") struct FLAXENGINE_API NetworkReplicationHierarchyObject
|
||||
{
|
||||
DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkReplicationObjectInfo);
|
||||
|
||||
// The object to replicate.
|
||||
API_FIELD() ScriptingObjectReference<ScriptingObject> 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<ScriptingObject>& 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bit mask for NetworkClient list (eg. to selectively send object replication).
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Network replication hierarchy output data to send.
|
||||
/// </summary>
|
||||
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<Client> _clients;
|
||||
Array<Entry> _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;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Base class for the network objects replication hierarchy nodes. Contains a list of objects.
|
||||
/// </summary>
|
||||
API_CLASS(Abstract, Namespace = "FlaxEngine.Networking") class FLAXENGINE_API NetworkReplicationNode : public ScriptingObject
|
||||
{
|
||||
DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(NetworkReplicationNode, ScriptingObject);
|
||||
|
||||
/// <summary>
|
||||
/// List with objects stored in this node.
|
||||
/// </summary>
|
||||
API_FIELD() Array<NetworkReplicationHierarchyObject> Objects;
|
||||
|
||||
/// <summary>
|
||||
/// Adds an object into the hierarchy.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to add.</param>
|
||||
API_FUNCTION() virtual void AddObject(NetworkReplicationHierarchyObject obj);
|
||||
|
||||
/// <summary>
|
||||
/// Removes object from the hierarchy.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to remove.</param>
|
||||
/// <returns>True on successful removal, otherwise false.</returns>
|
||||
API_FUNCTION() virtual bool RemoveObject(ScriptingObject* obj);
|
||||
|
||||
/// <summary>
|
||||
/// Force replicates the object during the next update. Resets any internal tracking state to force the synchronization.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to update.</param>
|
||||
/// <returns>True on successful update, otherwise false.</returns>
|
||||
API_FUNCTION() virtual bool DirtyObject(ScriptingObject* obj);
|
||||
|
||||
/// <summary>
|
||||
/// Iterates over all objects and adds them to the replication work.
|
||||
/// </summary>
|
||||
/// <param name="result">The update results container.</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Network replication hierarchy node with 3D grid spatialization. Organizes static objects into chunks to improve performance in large worlds.
|
||||
/// </summary>
|
||||
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<Int3, Cell> _children;
|
||||
|
||||
public:
|
||||
/// <summary>
|
||||
/// Size of the grid cell (in world units). Used to chunk the space for separate nodes.
|
||||
/// </summary>
|
||||
API_FIELD() float CellSize = 10000.0f;
|
||||
|
||||
void AddObject(NetworkReplicationHierarchyObject obj) override;
|
||||
bool RemoveObject(ScriptingObject* obj) override;
|
||||
void Update(NetworkReplicationHierarchyUpdateResult* result) override;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
API_CLASS(Namespace = "FlaxEngine.Networking") class FLAXENGINE_API NetworkReplicationHierarchy : public NetworkReplicationNode
|
||||
{
|
||||
DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(NetworkReplicationHierarchy, NetworkReplicationNode);
|
||||
};
|
||||
@@ -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<uint32> TargetClientIds;
|
||||
INetworkObject* AsNetworkObject;
|
||||
|
||||
NetworkReplicatedObject()
|
||||
{
|
||||
Spawned = 0;
|
||||
Synced = 0;
|
||||
}
|
||||
|
||||
bool operator==(const NetworkReplicatedObject& other) const
|
||||
@@ -199,6 +202,8 @@ namespace
|
||||
Dictionary<Guid, Guid> IdsRemappingTable;
|
||||
NetworkStream* CachedWriteStream = nullptr;
|
||||
NetworkStream* CachedReadStream = nullptr;
|
||||
NetworkReplicationHierarchyUpdateResult* CachedReplicationResult = nullptr;
|
||||
NetworkReplicationHierarchy* Hierarchy = nullptr;
|
||||
Array<NetworkClient*> NewClients;
|
||||
Array<NetworkConnection> CachedTargets;
|
||||
Dictionary<ScriptingTypeHandle, Serializer> SerializersTable;
|
||||
@@ -307,14 +312,15 @@ void BuildCachedTargets(const Array<NetworkClient*>& clients, const NetworkClien
|
||||
}
|
||||
}
|
||||
|
||||
void BuildCachedTargets(const Array<NetworkClient*>& clients, const DataContainer<uint32>& clientIds, const uint32 excludedClientId = NetworkManager::ServerClientId)
|
||||
void BuildCachedTargets(const Array<NetworkClient*>& clients, const DataContainer<uint32>& 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<NetworkClient*>& 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<NetworkClient*>& 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<SpawnItem, 256>& 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<typename MessageType>
|
||||
@@ -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>();
|
||||
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<NetworkReplicationHierarchyUpdateResult>();
|
||||
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>();
|
||||
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<SceneObject>(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);
|
||||
|
||||
@@ -42,6 +42,17 @@ public:
|
||||
API_FIELD() static bool EnableLog;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Gets the network replication hierarchy.
|
||||
/// </summary>
|
||||
API_PROPERTY() static NetworkReplicationHierarchy* GetHierarchy();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the network replication hierarchy.
|
||||
/// </summary>
|
||||
API_PROPERTY() static void SetHierarchy(NetworkReplicationHierarchy* value);
|
||||
|
||||
public:
|
||||
/// <summary>
|
||||
/// Adds the network replication serializer for a given type.
|
||||
/// </summary>
|
||||
@@ -68,7 +79,7 @@ public:
|
||||
/// <remarks>Does nothing if network is offline.</remarks>
|
||||
/// <param name="obj">The object to replicate.</param>
|
||||
/// <param name="parent">The parent of the object (eg. player that spawned it).</param>
|
||||
API_FUNCTION() static void AddObject(ScriptingObject* obj, ScriptingObject* parent = nullptr);
|
||||
API_FUNCTION() static void AddObject(ScriptingObject* obj, const ScriptingObject* parent = nullptr);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the object from the network replication system.
|
||||
@@ -80,14 +91,14 @@ public:
|
||||
/// <summary>
|
||||
/// Spawns the object to the other clients. Can be spawned by the owner who locally created it (eg. from prefab).
|
||||
/// </summary>
|
||||
/// <remarks>Does nothing if network is offline.</remarks>
|
||||
/// <remarks>Does nothing if network is offline. Doesn't spawn actor in a level - but in network replication system.</remarks>
|
||||
/// <param name="obj">The object to spawn on other clients.</param>
|
||||
API_FUNCTION() static void SpawnObject(ScriptingObject* obj);
|
||||
|
||||
/// <summary>
|
||||
/// Spawns the object to the other clients. Can be spawned by the owner who locally created it (eg. from prefab).
|
||||
/// </summary>
|
||||
/// <remarks>Does nothing if network is offline.</remarks>
|
||||
/// <remarks>Does nothing if network is offline. Doesn't spawn actor in a level - but in network replication system.</remarks>
|
||||
/// <param name="obj">The object to spawn on other clients.</param>
|
||||
/// <param name="clientIds">List with network client IDs that should receive network spawn event. Empty to spawn on all clients.</param>
|
||||
API_FUNCTION() static void SpawnObject(ScriptingObject* obj, const DataContainer<uint32>& clientIds);
|
||||
@@ -99,6 +110,13 @@ public:
|
||||
/// <param name="obj">The object to despawn on other clients.</param>
|
||||
API_FUNCTION() static void DespawnObject(ScriptingObject* obj);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the network object is spawned or added to the network replication system.
|
||||
/// </summary>
|
||||
/// <param name="obj">The network object.</param>
|
||||
/// <returns>True if object exists in networking, otherwise false.</returns>
|
||||
API_FUNCTION() static bool HasObject(const ScriptingObject* obj);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Client Id of the network object owner.
|
||||
/// </summary>
|
||||
|
||||
@@ -11,6 +11,7 @@ class INetworkSerializable;
|
||||
class NetworkPeer;
|
||||
class NetworkClient;
|
||||
class NetworkStream;
|
||||
class NetworkReplicationHierarchy;
|
||||
|
||||
struct NetworkEvent;
|
||||
struct NetworkConnection;
|
||||
|
||||
@@ -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<ParameterOverride>& 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<ParticleEffect, &ParticleEffect::UpdateExecuteInEditor>(this);
|
||||
#endif
|
||||
|
||||
if (PlayOnStart)
|
||||
Play();
|
||||
|
||||
// Base
|
||||
Actor::OnEnable();
|
||||
}
|
||||
|
||||
@@ -184,6 +184,7 @@ private:
|
||||
uint32 _parametersVersion = 0; // Version number for _parameters to be in sync with Instance.ParametersVersion
|
||||
Array<ParticleEffectParameter> _parameters; // Cached for scripting API
|
||||
Array<ParameterOverride> _parametersOverrides; // Cached parameter modifications to be applied to the parameters
|
||||
bool _isPlaying = false;
|
||||
|
||||
public:
|
||||
/// <summary>
|
||||
@@ -235,9 +236,15 @@ public:
|
||||
bool IsLooping = true;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
API_FIELD(Attributes="EditorDisplay(\"Particle Effect\"), DefaultValue(true), EditorOrder(60)")
|
||||
bool PlayOnStart = true;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
API_FIELD(Attributes="EditorDisplay(\"Particle Effect\"), DefaultValue(true), EditorOrder(70)")
|
||||
bool UpdateWhenOffscreen = true;
|
||||
|
||||
/// <summary>
|
||||
@@ -326,6 +333,11 @@ public:
|
||||
/// </summary>
|
||||
API_PROPERTY() int32 GetParticlesCount() const;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether or not the particle effect is playing.
|
||||
/// </summary>
|
||||
API_PROPERTY(Attributes="NoSerialize, HideInEditor") bool GetIsPlaying() const;
|
||||
|
||||
/// <summary>
|
||||
/// Resets the particles simulation state (clears the instance state data but preserves the instance parameters values).
|
||||
/// </summary>
|
||||
@@ -337,6 +349,21 @@ public:
|
||||
/// <param name="singleFrame">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.</param>
|
||||
API_FUNCTION() void UpdateSimulation(bool singleFrame = false);
|
||||
|
||||
/// <summary>
|
||||
/// Plays the simulation.
|
||||
/// </summary>
|
||||
API_FUNCTION() void Play();
|
||||
|
||||
/// <summary>
|
||||
/// Pauses the simulation.
|
||||
/// </summary>
|
||||
API_FUNCTION() void Pause();
|
||||
|
||||
/// <summary>
|
||||
/// Stops and resets the simulation.
|
||||
/// </summary>
|
||||
API_FUNCTION() void Stop();
|
||||
|
||||
/// <summary>
|
||||
/// Updates the actor bounds.
|
||||
/// </summary>
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -167,7 +167,6 @@ DECLARE_SCRIPTING_TYPE_MINIMAL(PlatformBase);
|
||||
static void Exit();
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Copy memory region
|
||||
/// </summary>
|
||||
@@ -334,7 +333,6 @@ public:
|
||||
static void FreePages(void* ptr);
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current runtime platform type. It's compile-time constant.
|
||||
/// </summary>
|
||||
@@ -409,7 +407,6 @@ public:
|
||||
static void Sleep(int32 milliseconds) = delete;
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current time in seconds.
|
||||
/// </summary>
|
||||
@@ -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:
|
||||
|
||||
/// <summary>
|
||||
/// Shows the fatal error message to the user.
|
||||
/// </summary>
|
||||
@@ -482,7 +478,6 @@ public:
|
||||
static void Info(const Char* msg);
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Shows the fatal error message to the user.
|
||||
/// </summary>
|
||||
@@ -520,7 +515,6 @@ public:
|
||||
static bool IsDebuggerPresent();
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Performs a fatal crash.
|
||||
/// </summary>
|
||||
@@ -560,7 +554,6 @@ public:
|
||||
static void CheckFailed(const char* message, const char* file, int line);
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Sets the High DPI awareness.
|
||||
/// </summary>
|
||||
@@ -628,7 +621,6 @@ public:
|
||||
static void CreateGuid(Guid& result);
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// The list of users.
|
||||
/// </summary>
|
||||
@@ -645,21 +637,31 @@ public:
|
||||
API_EVENT() static Delegate<User*> UserRemoved;
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether can open a given URL in a web browser.
|
||||
/// </summary>
|
||||
/// <param name="url">The URI to assign to web browser.</param>
|
||||
/// <returns>True if can open URL, otherwise false.</returns>
|
||||
API_FUNCTION() static bool CanOpenUrl(const StringView& url) = delete;
|
||||
API_FUNCTION() static bool CanOpenUrl(const StringView& url);
|
||||
|
||||
/// <summary>
|
||||
/// Launches a web browser and opens a given URL.
|
||||
/// </summary>
|
||||
/// <param name="url">The URI to assign to web browser.</param>
|
||||
API_FUNCTION() static void OpenUrl(const StringView& url) = delete;
|
||||
API_FUNCTION() static void OpenUrl(const StringView& url);
|
||||
|
||||
public:
|
||||
/// <summary>
|
||||
/// Gets the mouse cursor position in screen-space coordinates.
|
||||
/// </summary>
|
||||
/// <returns>Mouse cursor coordinates.</returns>
|
||||
API_PROPERTY() static Float2 GetMousePosition();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the mouse cursor position in screen-space coordinates.
|
||||
/// </summary>
|
||||
/// <param name="position">Cursor position to set.</param>
|
||||
API_PROPERTY() static void SetMousePosition(const Float2& position);
|
||||
|
||||
/// <summary>
|
||||
/// 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:
|
||||
|
||||
/// <summary>
|
||||
/// Gets full path of the main engine directory.
|
||||
/// </summary>
|
||||
@@ -719,7 +720,6 @@ public:
|
||||
static bool SetWorkingDirectory(const String& path);
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Gets the process environment variables (pairs of key and value).
|
||||
/// </summary>
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
#include <string>
|
||||
#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
|
||||
|
||||
@@ -89,6 +89,7 @@ X11::Cursor Cursors[(int32)CursorType::MAX];
|
||||
X11::XcursorImage* CursorsImg[(int32)CursorType::MAX];
|
||||
Dictionary<StringAnsi, X11::KeyCode> KeyNameMap;
|
||||
Array<KeyboardKeys> KeyCodeMap;
|
||||
Delegate<void*> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,11 @@ public:
|
||||
/// <returns>The user home directory.</returns>
|
||||
static const String& GetHomeDirectory();
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired when an XEvent is received during platform tick.
|
||||
/// </summary>
|
||||
static Delegate<void*> xEventRecieved;
|
||||
|
||||
public:
|
||||
|
||||
// [UnixPlatform]
|
||||
|
||||
@@ -21,12 +21,11 @@ enum class StringSearchCase
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The string operations utilities collection.
|
||||
/// The string operations utilities.
|
||||
/// </summary>
|
||||
class FLAXENGINE_API StringUtils
|
||||
{
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the hash code for input string.
|
||||
/// </summary>
|
||||
@@ -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:
|
||||
|
||||
/// <summary>
|
||||
/// Convert integer value to string
|
||||
/// </summary>
|
||||
/// <param name="value">Value to convert</param>
|
||||
/// <param name="base">Base (8,10,16)</param>
|
||||
/// <param name="buffer">Input buffer</param>
|
||||
/// <param name="length">Result string length</param>
|
||||
template<typename CharType>
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<PlatformBase::StackFrame> WindowsPlatform::GetStackFrames(int32 skipCount, int32 maxDepth, void* context)
|
||||
{
|
||||
Array<StackFrame> result;
|
||||
#if CRASH_LOG_ENABLE
|
||||
DbgHelpLock();
|
||||
|
||||
// Initialize
|
||||
@@ -1350,12 +1363,9 @@ Array<PlatformBase::StackFrame> 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
|
||||
|
||||
@@ -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<StackFrame, HeapAllocation> GetStackFrames(int32 skipCount = 0, int32 maxDepth = 60, void* context = nullptr);
|
||||
#if CRASH_LOG_ENABLE
|
||||
static Array<StackFrame, HeapAllocation> GetStackFrames(int32 skipCount = 0, int32 maxDepth = 60, void* context = nullptr);
|
||||
static void CollectCrashData(const String& crashDataFolder, void* context = nullptr);
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -118,16 +118,10 @@ void Font::ProcessText(const StringView& text, Array<FontLineCache>& 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<FontLineCache>& 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<FontLineCache>& 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<FontLineCache>& 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,17 +17,17 @@ class FontAsset;
|
||||
/// <summary>
|
||||
/// The text range.
|
||||
/// </summary>
|
||||
API_STRUCT() struct TextRange
|
||||
API_STRUCT(NoDefault) struct TextRange
|
||||
{
|
||||
DECLARE_SCRIPTING_TYPE_MINIMAL(TextRange);
|
||||
|
||||
/// <summary>
|
||||
/// The start index.
|
||||
/// The start index (inclusive).
|
||||
/// </summary>
|
||||
API_FIELD() int32 StartIndex;
|
||||
|
||||
/// <summary>
|
||||
/// The end index.
|
||||
/// The end index (exclusive).
|
||||
/// </summary>
|
||||
API_FIELD() int32 EndIndex;
|
||||
|
||||
@@ -70,7 +70,7 @@ DECLARE_SCRIPTING_TYPE_MINIMAL(TextRange);
|
||||
/// <summary>
|
||||
/// Gets the substring from the source text.
|
||||
/// </summary>
|
||||
/// <param name="other">The text.</param>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <returns>The substring of the original text of the defined range.</returns>
|
||||
StringView Substring(const StringView& text) const
|
||||
{
|
||||
@@ -87,7 +87,7 @@ struct TIsPODType<TextRange>
|
||||
/// <summary>
|
||||
/// The font line info generated during text processing.
|
||||
/// </summary>
|
||||
API_STRUCT() struct FontLineCache
|
||||
API_STRUCT(NoDefault) struct FontLineCache
|
||||
{
|
||||
DECLARE_SCRIPTING_TYPE_MINIMAL(FontLineCache);
|
||||
|
||||
@@ -151,7 +151,7 @@ struct TIsPODType<FontLineCache>
|
||||
/// <summary>
|
||||
/// The cached font character entry (read for rendering and further processing).
|
||||
/// </summary>
|
||||
API_STRUCT() struct FontCharacterEntry
|
||||
API_STRUCT(NoDefault) struct FontCharacterEntry
|
||||
{
|
||||
DECLARE_SCRIPTING_TYPE_MINIMAL(FontCharacterEntry);
|
||||
|
||||
|
||||
@@ -45,6 +45,11 @@ public:
|
||||
/// </summary>
|
||||
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:
|
||||
/// <summary>
|
||||
/// Utilities for C# object management.
|
||||
|
||||
@@ -165,9 +165,8 @@ extern MDomain* MActiveDomain;
|
||||
extern Array<MDomain*, FixedAllocation<4>> MDomains;
|
||||
|
||||
Dictionary<String, void*> CachedFunctions;
|
||||
|
||||
Dictionary<void*, MClass*> classHandles;
|
||||
Dictionary<void*, MAssembly*> assemblyHandles;
|
||||
Dictionary<void*, MClass*> CachedClassHandles;
|
||||
Dictionary<void*, MAssembly*> CachedAssemblyHandles;
|
||||
|
||||
/// <summary>
|
||||
/// 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<void, const void*>(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<bool, void*>(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<MObject*>& MClass::GetAttributes() const
|
||||
int numAttributes;
|
||||
static void* GetClassAttributesPtr = GetStaticMethodPointer(TEXT("GetClassAttributes"));
|
||||
CallStaticMethod<void, void*, MObject***, int*>(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<MObject*>& 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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called every fixed framerate frame (after FixedUpdate) if object is enabled.
|
||||
/// </summary>
|
||||
API_FUNCTION(Attributes = "NoAnimate") virtual void OnLateFixedUpdate()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called during drawing debug shapes in editor. Use <see cref="DebugDraw"/> to draw debug shapes and other visualization.
|
||||
/// </summary>
|
||||
|
||||
@@ -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<BinaryModule*, InlinedAllocation<64>> _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();
|
||||
|
||||
@@ -80,17 +80,22 @@ namespace FlaxEngine
|
||||
public static event Action Update;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs on scripting 'late' update.
|
||||
/// Occurs on scripting late update.
|
||||
/// </summary>
|
||||
public static event Action LateUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs on scripting `fixed` update.
|
||||
/// Occurs on scripting fixed update.
|
||||
/// </summary>
|
||||
public static event Action FixedUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static event Action LateFixedUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs on scripting draw update. Called during frame rendering and can be used to invoke custom rendering with GPUDevice.
|
||||
/// </summary>
|
||||
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();
|
||||
|
||||
87
Source/Engine/Scripting/SoftTypeReference.cs
Normal file
87
Source/Engine/Scripting/SoftTypeReference.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace FlaxEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// The soft reference to the scripting type contained in the scripting assembly.
|
||||
/// </summary>
|
||||
public struct SoftTypeReference : IComparable, IComparable<SoftTypeReference>
|
||||
{
|
||||
private string _typeName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type full name (eg. FlaxEngine.Actor).
|
||||
/// </summary>
|
||||
public string TypeName
|
||||
{
|
||||
get => _typeName;
|
||||
set => _typeName = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type (resolves soft reference).
|
||||
/// </summary>
|
||||
public Type Type
|
||||
{
|
||||
get => _typeName != null ? Type.GetType(_typeName) : null;
|
||||
set => _typeName = value?.FullName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SoftTypeReference"/>.
|
||||
/// </summary>
|
||||
/// <param name="typeName">The type name.</param>
|
||||
public SoftTypeReference(string typeName)
|
||||
{
|
||||
_typeName = typeName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the soft type reference from full name.
|
||||
/// </summary>
|
||||
/// <param name="s">The type name.</param>
|
||||
/// <returns>The soft type reference.</returns>
|
||||
public static implicit operator SoftTypeReference(string s)
|
||||
{
|
||||
return new SoftTypeReference { _typeName = s };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the soft type reference from runtime type.
|
||||
/// </summary>
|
||||
/// <param name="s">The type.</param>
|
||||
/// <returns>The soft type reference.</returns>
|
||||
public static implicit operator SoftTypeReference(Type s)
|
||||
{
|
||||
return new SoftTypeReference { _typeName = s?.FullName };
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return _typeName;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return _typeName?.GetHashCode() ?? 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CompareTo(object obj)
|
||||
{
|
||||
if (obj is SoftTypeReference other)
|
||||
return CompareTo(other);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CompareTo(SoftTypeReference other)
|
||||
{
|
||||
return string.Compare(_typeName, other._typeName, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
151
Source/Engine/Scripting/SoftTypeReference.h
Normal file
151
Source/Engine/Scripting/SoftTypeReference.h
Normal file
@@ -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"
|
||||
|
||||
/// <summary>
|
||||
/// The soft reference to the scripting type contained in the scripting assembly.
|
||||
/// </summary>
|
||||
template<typename T = ScriptingObject>
|
||||
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<T>(type);
|
||||
if (!obj)
|
||||
{
|
||||
if (_typeName.HasChars())
|
||||
LOG(Error, "Unknown or invalid type {0}", String(_typeName));
|
||||
obj = ScriptingObject::NewObject<T>();
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
uint32 GetHash(const SoftTypeReference<T>& key)
|
||||
{
|
||||
return GetHash(key.GetTypeName());
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
namespace Serialization
|
||||
{
|
||||
template<typename T>
|
||||
bool ShouldSerialize(const SoftTypeReference<T>& v, const void* otherObj)
|
||||
{
|
||||
return !otherObj || v != *(SoftTypeReference<T>*)otherObj;
|
||||
}
|
||||
template<typename T>
|
||||
void Serialize(ISerializable::SerializeStream& stream, const SoftTypeReference<T>& v, const void* otherObj)
|
||||
{
|
||||
stream.String(v.GetTypeName());
|
||||
}
|
||||
template<typename T>
|
||||
void Deserialize(ISerializable::DeserializeStream& stream, SoftTypeReference<T>& v, ISerializeModifier* modifier)
|
||||
{
|
||||
v = stream.GetTextAnsi();
|
||||
}
|
||||
}
|
||||
// @formatter:on
|
||||
@@ -7,7 +7,7 @@ using Newtonsoft.Json;
|
||||
namespace FlaxEngine.Json
|
||||
{
|
||||
/// <summary>
|
||||
/// Serialize references to the FlaxEngine.Object as Guid.
|
||||
/// Serialize references to the <see cref="FlaxEngine.Object"/> as Guid.
|
||||
/// </summary>
|
||||
/// <seealso cref="Newtonsoft.Json.JsonConverter" />
|
||||
internal class FlaxObjectConverter : JsonConverter
|
||||
@@ -46,7 +46,7 @@ namespace FlaxEngine.Json
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize SceneReference as Guid in internal format.
|
||||
/// Serialize <see cref="SceneReference"/> as Guid in internal format.
|
||||
/// </summary>
|
||||
/// <seealso cref="Newtonsoft.Json.JsonConverter" />
|
||||
internal class SceneReferenceConverter : JsonConverter
|
||||
@@ -79,7 +79,7 @@ namespace FlaxEngine.Json
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize SoftObjectReference as Guid in internal format.
|
||||
/// Serialize <see cref="SoftObjectReference"/> as Guid in internal format.
|
||||
/// </summary>
|
||||
/// <seealso cref="Newtonsoft.Json.JsonConverter" />
|
||||
internal class SoftObjectReferenceConverter : JsonConverter
|
||||
@@ -111,7 +111,36 @@ namespace FlaxEngine.Json
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize SoftObjectReference as Guid in internal format.
|
||||
/// Serialize <see cref="SoftTypeReference"/> as typename string in internal format.
|
||||
/// </summary>
|
||||
/// <seealso cref="Newtonsoft.Json.JsonConverter" />
|
||||
internal class SoftTypeReferenceConverter : JsonConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteValue(((SoftTypeReference)value).TypeName);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(SoftTypeReference);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize <see cref="Margin"/> as Guid in internal format.
|
||||
/// </summary>
|
||||
/// <seealso cref="Newtonsoft.Json.JsonConverter" />
|
||||
internal class MarginConverter : JsonConverter
|
||||
@@ -237,7 +266,7 @@ namespace FlaxEngine.Json
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize LocalizedString as inlined text is not using localization (Id member is empty).
|
||||
/// Serialize <see cref="LocalizedString"/> as inlined text is not using localization (Id member is empty).
|
||||
/// </summary>
|
||||
/// <seealso cref="Newtonsoft.Json.JsonConverter" />
|
||||
internal class LocalizedStringConverter : JsonConverter
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace FlaxEngine.GUI
|
||||
public class Button : ContainerControl
|
||||
{
|
||||
/// <summary>
|
||||
/// The default height fro the buttons.
|
||||
/// The default height for the buttons.
|
||||
/// </summary>
|
||||
public const float DefaultHeight = 24.0f;
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace FlaxEngine.GUI
|
||||
/// <summary>
|
||||
/// Gets or sets the font used to draw button text.
|
||||
/// </summary>
|
||||
[EditorDisplay("Style"), EditorOrder(2000)]
|
||||
[EditorDisplay("Text Style"), EditorOrder(2022), ExpandGroups]
|
||||
public FontReference Font
|
||||
{
|
||||
get => _font;
|
||||
@@ -52,15 +52,51 @@ namespace FlaxEngine.GUI
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color used to draw button text.
|
||||
/// </summary>
|
||||
[EditorDisplay("Style"), EditorOrder(2000)]
|
||||
[EditorDisplay("Text Style"), EditorOrder(2020)]
|
||||
public Color TextColor;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the brush used for background drawing.
|
||||
/// </summary>
|
||||
[EditorDisplay("Background Style"), EditorOrder(1999), Tooltip("The brush used for background drawing."), ExpandGroups]
|
||||
public IBrush BackgroundBrush { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the background color when button is highlighted.
|
||||
/// </summary>
|
||||
[EditorDisplay("Background Style"), EditorOrder(2001)]
|
||||
public Color BackgroundColorHighlighted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the background color when button is selected.
|
||||
/// </summary>
|
||||
[EditorDisplay("Background Style"), EditorOrder(2002)]
|
||||
public Color BackgroundColorSelected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color of the border.
|
||||
/// </summary>
|
||||
[EditorDisplay("Border Style"), EditorOrder(2010), ExpandGroups]
|
||||
public Color BorderColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the border color when button is highlighted.
|
||||
/// </summary>
|
||||
[EditorDisplay("Border Style"), EditorOrder(2011)]
|
||||
public Color BorderColorHighlighted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the border color when button is selected.
|
||||
/// </summary>
|
||||
[EditorDisplay("Border Style"), EditorOrder(2012)]
|
||||
public Color BorderColorSelected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when user clicks on the button.
|
||||
/// </summary>
|
||||
@@ -81,42 +117,6 @@ namespace FlaxEngine.GUI
|
||||
/// </summary>
|
||||
public event Action HoverEnd;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the brush used for background drawing.
|
||||
/// </summary>
|
||||
[EditorDisplay("Style"), EditorOrder(2000), Tooltip("The brush used for background drawing.")]
|
||||
public IBrush BackgroundBrush { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color of the border.
|
||||
/// </summary>
|
||||
[EditorDisplay("Style"), EditorOrder(2000)]
|
||||
public Color BorderColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the background color when button is selected.
|
||||
/// </summary>
|
||||
[EditorDisplay("Style"), EditorOrder(2010)]
|
||||
public Color BackgroundColorSelected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the border color when button is selected.
|
||||
/// </summary>
|
||||
[EditorDisplay("Style"), EditorOrder(2020)]
|
||||
public Color BorderColorSelected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the background color when button is highlighted.
|
||||
/// </summary>
|
||||
[EditorDisplay("Style"), EditorOrder(2000)]
|
||||
public Color BackgroundColorHighlighted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the border color when button is highlighted.
|
||||
/// </summary>
|
||||
[EditorDisplay("Style"), EditorOrder(2000)]
|
||||
public Color BorderColorHighlighted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this button is being pressed (by mouse or touch).
|
||||
/// </summary>
|
||||
@@ -195,7 +195,7 @@ namespace FlaxEngine.GUI
|
||||
/// Sets the button colors palette based on a given main color.
|
||||
/// </summary>
|
||||
/// <param name="color">The main color.</param>
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ namespace FlaxEngine.GUI
|
||||
/// <summary>
|
||||
/// Gets or sets the size of the box.
|
||||
/// </summary>
|
||||
[EditorOrder(20)]
|
||||
public float BoxSize
|
||||
{
|
||||
get => _boxSize;
|
||||
@@ -107,34 +108,34 @@ namespace FlaxEngine.GUI
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color of the checkbox icon.
|
||||
/// </summary>
|
||||
[EditorDisplay("Style"), EditorOrder(2000)]
|
||||
public Color ImageColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color of the border.
|
||||
/// </summary>
|
||||
[EditorDisplay("Style"), EditorOrder(2000)]
|
||||
[EditorDisplay("Border Style"), EditorOrder(2010), ExpandGroups]
|
||||
public Color BorderColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the border color when checkbox is hovered.
|
||||
/// </summary>
|
||||
[EditorDisplay("Style"), EditorOrder(2000)]
|
||||
[EditorDisplay("Border Style"), EditorOrder(2011)]
|
||||
public Color BorderColorHighlighted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color of the checkbox icon.
|
||||
/// </summary>
|
||||
[EditorDisplay("Image Style"), EditorOrder(2020), ExpandGroups]
|
||||
public Color ImageColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the image used to render checkbox checked state.
|
||||
/// </summary>
|
||||
[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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the image used to render checkbox intermediate state.
|
||||
/// </summary>
|
||||
[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; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -246,79 +246,79 @@ namespace FlaxEngine.GUI
|
||||
/// <summary>
|
||||
/// Gets or sets the font used to draw text.
|
||||
/// </summary>
|
||||
[EditorDisplay("Style"), EditorOrder(2000)]
|
||||
[EditorDisplay("Text Style"), EditorOrder(2021)]
|
||||
public FontReference Font { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color of the text.
|
||||
/// </summary>
|
||||
[EditorDisplay("Style"), EditorOrder(2000)]
|
||||
[EditorDisplay("Text Style"), EditorOrder(2020), ExpandGroups]
|
||||
public Color TextColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color of the border.
|
||||
/// </summary>
|
||||
[EditorDisplay("Style"), EditorOrder(2000)]
|
||||
[EditorDisplay("Border Style"), EditorOrder(2010), ExpandGroups]
|
||||
public Color BorderColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the background color when dropdown popup is opened.
|
||||
/// </summary>
|
||||
[EditorDisplay("Style"), EditorOrder(2010)]
|
||||
[EditorDisplay("Background Style"), EditorOrder(2002)]
|
||||
public Color BackgroundColorSelected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the border color when dropdown popup is opened.
|
||||
/// </summary>
|
||||
[EditorDisplay("Style"), EditorOrder(2020)]
|
||||
[EditorDisplay("Border Style"), EditorOrder(2012)]
|
||||
public Color BorderColorSelected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the background color when dropdown is highlighted.
|
||||
/// </summary>
|
||||
[EditorDisplay("Style"), EditorOrder(2000)]
|
||||
[EditorDisplay("Background Style"), EditorOrder(2001), ExpandGroups]
|
||||
public Color BackgroundColorHighlighted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the border color when dropdown is highlighted.
|
||||
/// </summary>
|
||||
[EditorDisplay("Style"), EditorOrder(2000)]
|
||||
[EditorDisplay("Border Style"), EditorOrder(2011)]
|
||||
public Color BorderColorHighlighted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the image used to render dropdown drop arrow icon.
|
||||
/// </summary>
|
||||
[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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color used to render dropdown drop arrow icon.
|
||||
/// </summary>
|
||||
[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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color used to render dropdown drop arrow icon (menu is opened).
|
||||
/// </summary>
|
||||
[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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color used to render dropdown drop arrow icon (menu is highlighted).
|
||||
/// </summary>
|
||||
[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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the image used to render dropdown checked item icon.
|
||||
/// </summary>
|
||||
[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; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -25,19 +25,19 @@ namespace FlaxEngine.GUI
|
||||
/// <summary>
|
||||
/// Gets or sets the color used to multiply the image pixels.
|
||||
/// </summary>
|
||||
[EditorDisplay("Style"), EditorOrder(2000)]
|
||||
[EditorDisplay("Image Style"), EditorOrder(2010), ExpandGroups]
|
||||
public Color Color { get; set; } = Color.White;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color used to multiply the image pixels when mouse is over the image.
|
||||
/// </summary>
|
||||
[EditorDisplay("Style"), EditorOrder(2000)]
|
||||
[EditorDisplay("Image Style"), EditorOrder(2011)]
|
||||
public Color MouseOverColor { get; set; } = Color.White;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color used to multiply the image pixels when control is disabled.
|
||||
/// </summary>
|
||||
[EditorDisplay("Style"), EditorOrder(2000)]
|
||||
[EditorDisplay("Image Style"), EditorOrder(2012)]
|
||||
public Color DisabledTint { get; set; } = Color.Gray;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -47,37 +47,37 @@ namespace FlaxEngine.GUI
|
||||
/// <summary>
|
||||
/// Gets or sets the color of the text.
|
||||
/// </summary>
|
||||
[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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color of the text when it is highlighted (mouse is over).
|
||||
/// </summary>
|
||||
[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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the horizontal text alignment within the control bounds.
|
||||
/// </summary>
|
||||
[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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the vertical text alignment within the control bounds.
|
||||
/// </summary>
|
||||
[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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text wrapping within the control bounds.
|
||||
/// </summary>
|
||||
[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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the font.
|
||||
/// </summary>
|
||||
[EditorDisplay("Style"), EditorOrder(2000)]
|
||||
[EditorDisplay("Text Style"), EditorOrder(2023)]
|
||||
public FontReference Font
|
||||
{
|
||||
get => _font;
|
||||
@@ -99,7 +99,7 @@ namespace FlaxEngine.GUI
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[EditorDisplay("Style"), EditorOrder(2000)]
|
||||
[EditorDisplay("Text Style"), EditorOrder(2024)]
|
||||
public MaterialBase Material { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user