Merge commit 'f2ecefb7ee9b9e6c5daac9f44fe40ebdccbb1c76' into 1.6

This commit is contained in:
Wojtek Figat
2023-06-01 01:06:14 +02:00
121 changed files with 2553 additions and 958 deletions

1
.gitignore vendored
View File

@@ -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)

Binary file not shown.

BIN
Content/Shaders/VolumetricFog.flax (Stored with Git LFS)

Binary file not shown.

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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;
}
}
}

View File

@@ -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;
}

View File

@@ -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++)

View File

@@ -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();

View File

@@ -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;

View File

@@ -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)

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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";
}
}
}

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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");
}
}

View 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>

View File

@@ -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()
{

View File

@@ -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)

View File

@@ -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))

View File

@@ -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();
}

View File

@@ -30,7 +30,7 @@ namespace FlaxEditor.Surface.Elements
{
ParentNode = parentNode;
Archetype = archetype;
UseDynamicEditing = false;
ParentNode.ValuesChanged += OnNodeValuesChanged;
}

View File

@@ -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();
}

View 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

View 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;
};

View File

@@ -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>

View File

@@ -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);

View File

@@ -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 />

View File

@@ -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;

View File

@@ -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()

View File

@@ -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);

View File

@@ -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)...);

View File

@@ -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;

View File

@@ -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.

View File

@@ -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");

View File

@@ -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>

View File

@@ -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);

View File

@@ -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);

View File

@@ -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");
}
}
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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")

View File

@@ -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");

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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);

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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;
};

View File

@@ -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() {};
};

View File

@@ -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;
}

View File

@@ -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);
}

View 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);
}
}
}

View 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;
}
}
}
}

View 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);
};

View File

@@ -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);

View File

@@ -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>

View File

@@ -11,6 +11,7 @@ class INetworkSerializable;
class NetworkPeer;
class NetworkClient;
class NetworkStream;
class NetworkReplicationHierarchy;
struct NetworkEvent;
struct NetworkConnection;

View File

@@ -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();
}

View File

@@ -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>

View File

@@ -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];

View File

@@ -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();

View File

@@ -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());

View File

@@ -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>

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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]

View File

@@ -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

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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

View File

@@ -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
};

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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.

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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>

View File

@@ -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();

View File

@@ -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();

View 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);
}
}
}

View 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

View File

@@ -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

View File

@@ -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());

View File

@@ -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();
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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