diff --git a/Source/Editor/Content/Items/ContentItem.cs b/Source/Editor/Content/Items/ContentItem.cs
index 4fe75d2c2..66825fb42 100644
--- a/Source/Editor/Content/Items/ContentItem.cs
+++ b/Source/Editor/Content/Items/ContentItem.cs
@@ -360,7 +360,7 @@ namespace FlaxEditor.Content
}
///
- /// Updates the tooltip text.
+ /// Updates the tooltip text text.
///
public virtual void UpdateTooltipText()
{
@@ -384,7 +384,8 @@ namespace FlaxEditor.Content
protected virtual void OnBuildTooltipText(StringBuilder sb)
{
sb.Append("Type: ").Append(TypeDescription).AppendLine();
- sb.Append("Size: ").Append(Utilities.Utils.FormatBytesCount((int)new FileInfo(Path).Length)).AppendLine();
+ if (File.Exists(Path))
+ sb.Append("Size: ").Append(Utilities.Utils.FormatBytesCount((int)new FileInfo(Path).Length)).AppendLine();
sb.Append("Path: ").Append(Utilities.Utils.GetAssetNamePathWithExt(Path)).AppendLine();
}
@@ -718,7 +719,7 @@ namespace FlaxEditor.Content
public override bool OnMouseDoubleClick(Float2 location, MouseButton button)
{
Focus();
-
+
// Open
(Parent as ContentView).OnItemDoubleClick(this);
diff --git a/Source/Editor/Content/Items/NewItem.cs b/Source/Editor/Content/Items/NewItem.cs
index 94d95f15b..7040adf44 100644
--- a/Source/Editor/Content/Items/NewItem.cs
+++ b/Source/Editor/Content/Items/NewItem.cs
@@ -1,5 +1,6 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
+using System.Text;
using FlaxEngine;
namespace FlaxEditor.Content
@@ -47,5 +48,11 @@ namespace FlaxEditor.Content
///
protected override bool DrawShadow => true;
+
+ ///
+ public override void UpdateTooltipText()
+ {
+ TooltipText = null;
+ }
}
}
diff --git a/Source/Editor/CustomEditors/CustomEditor.cs b/Source/Editor/CustomEditors/CustomEditor.cs
index 1ece31f4d..03b21e12b 100644
--- a/Source/Editor/CustomEditors/CustomEditor.cs
+++ b/Source/Editor/CustomEditors/CustomEditor.cs
@@ -148,7 +148,7 @@ namespace FlaxEditor.CustomEditors
return;
// Special case for root objects to run normal layout build
- if (_presenter.Selection == Values)
+ if (_presenter != null && _presenter.Selection == Values)
{
_presenter.BuildLayout();
return;
@@ -159,7 +159,7 @@ namespace FlaxEditor.CustomEditors
var layout = _layout;
var control = layout.ContainerControl;
var parent = _parent;
- var parentScrollV = (_presenter.Panel.Parent as Panel)?.VScrollBar?.Value ?? -1;
+ var parentScrollV = (_presenter?.Panel.Parent as Panel)?.VScrollBar?.Value ?? -1;
control.IsLayoutLocked = true;
control.DisposeChildren();
@@ -249,6 +249,28 @@ namespace FlaxEditor.CustomEditors
internal virtual void RefreshRootChild()
{
+ // Check if need to update value
+ if (_hasValueDirty)
+ {
+ IsSettingValue = true;
+ try
+ {
+ // Cleanup (won't retry update in case of exception)
+ object val = _valueToSet;
+ _hasValueDirty = false;
+ _valueToSet = null;
+
+ // Assign value
+ for (int i = 0; i < _values.Count; i++)
+ _values[i] = val;
+ }
+ finally
+ {
+ OnUnDirty();
+ IsSettingValue = false;
+ }
+ }
+
Refresh();
for (int i = 0; i < _children.Count; i++)
diff --git a/Source/Editor/CustomEditors/CustomEditorsUtil.cs b/Source/Editor/CustomEditors/CustomEditorsUtil.cs
index 4c664031f..5f76b07e2 100644
--- a/Source/Editor/CustomEditors/CustomEditorsUtil.cs
+++ b/Source/Editor/CustomEditors/CustomEditorsUtil.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;
+using FlaxEditor.CustomEditors.Dedicated;
using FlaxEditor.CustomEditors.Editors;
using FlaxEditor.Scripting;
using FlaxEngine;
@@ -110,7 +111,7 @@ namespace FlaxEditor.CustomEditors
// Select default editor (based on type)
if (targetType.IsEnum)
return new EnumEditor();
- if (targetType.IsGenericType)
+ if (targetType.IsGenericType)
{
if (targetTypeType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
return new DictionaryEditor();
@@ -121,6 +122,8 @@ namespace FlaxEditor.CustomEditors
if (customEditorType != null)
return (CustomEditor)Activator.CreateInstance(customEditorType);
}
+ if (typeof(FlaxEngine.Object).IsAssignableFrom(targetTypeType))
+ return new ScriptingObjectEditor();
// The most generic editor
return new GenericEditor();
diff --git a/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs b/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs
index 6a2986d2b..f9325fb26 100644
--- a/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs
+++ b/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs
@@ -21,7 +21,7 @@ namespace FlaxEditor.CustomEditors.Dedicated
///
///
[CustomEditor(typeof(Actor)), DefaultEditor]
- public class ActorEditor : GenericEditor
+ public class ActorEditor : ScriptingObjectEditor
{
private Guid _linkedPrefabId;
diff --git a/Source/Editor/CustomEditors/Dedicated/ScriptingObjectEditor.cs b/Source/Editor/CustomEditors/Dedicated/ScriptingObjectEditor.cs
new file mode 100644
index 000000000..477deb708
--- /dev/null
+++ b/Source/Editor/CustomEditors/Dedicated/ScriptingObjectEditor.cs
@@ -0,0 +1,29 @@
+// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
+
+using FlaxEditor.CustomEditors.Editors;
+using FlaxEngine.Networking;
+
+namespace FlaxEditor.CustomEditors.Dedicated
+{
+ ///
+ /// Custom editor for .
+ ///
+ public class ScriptingObjectEditor : GenericEditor
+ {
+ ///
+ public override void Initialize(LayoutElementsContainer layout)
+ {
+ // Network objects debugging
+ var obj = Values[0] as FlaxEngine.Object;
+ if (Editor.IsPlayMode && NetworkManager.IsConnected && NetworkReplicator.HasObject(obj))
+ {
+ var group = layout.Group("Network");
+ group.Panel.Open();
+ group.Label("Role", Utilities.Utils.GetPropertyNameUI(NetworkReplicator.GetObjectRole(obj).ToString()));
+ group.Label("Owner Client Id", NetworkReplicator.GetObjectOwnerClientId(obj).ToString());
+ }
+
+ base.Initialize(layout);
+ }
+ }
+}
diff --git a/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs b/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs
index 41dc1dc08..bf82e19da 100644
--- a/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs
+++ b/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
-using System.Linq;
using FlaxEditor.Actions;
using FlaxEditor.Content;
using FlaxEditor.GUI;
diff --git a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs
index 2612440a4..076b94b0a 100644
--- a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs
@@ -78,7 +78,7 @@ namespace FlaxEditor.CustomEditors.Editors
///
public class ScaleEditor : Float3Editor
{
- private Image _linkImage;
+ private Button _linkButton;
///
public override void Initialize(LayoutElementsContainer layout)
@@ -87,19 +87,20 @@ namespace FlaxEditor.CustomEditors.Editors
LinkValues = Editor.Instance.Windows.PropertiesWin.ScaleLinked;
- _linkImage = new Image
+ // Add button with the link icon
+ _linkButton = new Button
{
+ BackgroundBrush = new SpriteBrush(Editor.Instance.Icons.Link32),
Parent = LinkedLabel,
Width = 18,
Height = 18,
- Brush = LinkValues ? new SpriteBrush(Editor.Instance.Icons.Link32) : new SpriteBrush(),
AnchorPreset = AnchorPresets.TopLeft,
- TooltipText = "Scale values are linked together.",
};
+ _linkButton.Clicked += ToggleLink;
+ SetLinkStyle();
var x = LinkedLabel.Text.Value.Length * 7 + 5;
- _linkImage.LocalX += x;
- _linkImage.LocalY += 1;
-
+ _linkButton.LocalX += x;
+ _linkButton.LocalY += 1;
LinkedLabel.SetupContextMenu += (label, menu, editor) =>
{
menu.AddSeparator();
@@ -127,7 +128,16 @@ namespace FlaxEditor.CustomEditors.Editors
{
LinkValues = !LinkValues;
Editor.Instance.Windows.PropertiesWin.ScaleLinked = LinkValues;
- _linkImage.Brush = LinkValues ? new SpriteBrush(Editor.Instance.Icons.Link32) : new SpriteBrush();
+ SetLinkStyle();
+ }
+
+ private void SetLinkStyle()
+ {
+ var style = FlaxEngine.GUI.Style.Current;
+ var backgroundColor = LinkValues ? style.Foreground : style.ForegroundDisabled;
+ _linkButton.SetColors(backgroundColor);
+ _linkButton.BorderColor = _linkButton.BorderColorSelected = _linkButton.BorderColorHighlighted = Color.Transparent;
+ _linkButton.TooltipText = LinkValues ? "Unlinks scale components from uniform scaling" : "Links scale components for uniform scaling";
}
}
}
diff --git a/Source/Editor/CustomEditors/Editors/DictionaryEditor.cs b/Source/Editor/CustomEditors/Editors/DictionaryEditor.cs
index f26bf25a4..73089ff04 100644
--- a/Source/Editor/CustomEditors/Editors/DictionaryEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/DictionaryEditor.cs
@@ -2,7 +2,6 @@
using System;
using System.Collections;
-using System.Collections.Generic;
using System.Linq;
using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.CustomEditors.GUI;
@@ -46,8 +45,8 @@ namespace FlaxEditor.CustomEditors.Editors
private void OnSetupContextMenu(PropertyNameLabel label, ContextMenu menu, CustomEditor linkedEditor)
{
- menu.AddSeparator();
-
+ if (menu.Items.Any())
+ menu.AddSeparator();
menu.AddButton("Remove", OnRemoveClicked).Enabled = !_editor._readOnly;
menu.AddButton("Edit", OnEditClicked).Enabled = _editor._canEditKeys;
}
@@ -62,6 +61,7 @@ namespace FlaxEditor.CustomEditors.Editors
var keyType = _editor.Values.Type.GetGenericArguments()[0];
if (keyType == typeof(string) || keyType.IsPrimitive)
{
+ // Edit as text
var popup = RenamePopup.Show(Parent, Rectangle.Margin(Bounds, Margin), Text, false);
popup.Validate += (renamePopup, value) =>
{
@@ -79,7 +79,6 @@ namespace FlaxEditor.CustomEditors.Editors
newKey = JsonSerializer.Deserialize(renamePopup.Text, keyType);
else
newKey = renamePopup.Text;
-
_editor.ChangeKey(_key, newKey);
_key = newKey;
Text = _key.ToString();
@@ -87,6 +86,7 @@ namespace FlaxEditor.CustomEditors.Editors
}
else if (keyType.IsEnum)
{
+ // Edit via enum picker
var popup = RenamePopup.Show(Parent, Rectangle.Margin(Bounds, Margin), Text, false);
var picker = new EnumComboBox(keyType)
{
@@ -109,7 +109,21 @@ namespace FlaxEditor.CustomEditors.Editors
}
else
{
- throw new NotImplementedException("Missing editing for dictionary key type " + keyType);
+ // Generic editor
+ var popup = ContextMenuBase.ShowEmptyMenu(Parent, Rectangle.Margin(Bounds, Margin));
+ var presenter = new CustomEditorPresenter(null);
+ presenter.Panel.AnchorPreset = AnchorPresets.StretchAll;
+ presenter.Panel.IsScrollable = false;
+ presenter.Panel.Parent = popup;
+ presenter.Select(_key);
+ presenter.Modified += () =>
+ {
+ popup.Hide();
+ object newKey = presenter.Selection[0];
+ _editor.ChangeKey(_key, newKey);
+ _key = newKey;
+ Text = _key?.ToString();
+ };
}
}
@@ -160,7 +174,7 @@ namespace FlaxEditor.CustomEditors.Editors
var argTypes = type.GetGenericArguments();
var keyType = argTypes[0];
var valueType = argTypes[1];
- _canEditKeys = keyType == typeof(string) || keyType.IsPrimitive || keyType.IsEnum;
+ _canEditKeys = keyType == typeof(string) || keyType.IsPrimitive || keyType.IsEnum || keyType.IsValueType;
_background = FlaxEngine.GUI.Style.Current.CollectionBackgroundColor;
_readOnly = false;
_notNullItems = false;
@@ -383,6 +397,7 @@ namespace FlaxEditor.CustomEditors.Editors
int newItemsLeft = newSize - oldSize;
while (newItemsLeft-- > 0)
{
+ object newKey = null;
if (keyType.IsPrimitive)
{
long uniqueKey = 0;
@@ -401,8 +416,7 @@ namespace FlaxEditor.CustomEditors.Editors
}
}
} while (!isUnique);
-
- newValues[Convert.ChangeType(uniqueKey, keyType)] = TypeUtils.GetDefaultValue(new ScriptType(valueType));
+ newKey = Convert.ChangeType(uniqueKey, keyType);
}
else if (keyType.IsEnum)
{
@@ -422,8 +436,7 @@ namespace FlaxEditor.CustomEditors.Editors
}
}
} while (!isUnique && uniqueKeyIndex < enumValues.Length);
-
- newValues[enumValues.GetValue(uniqueKeyIndex)] = TypeUtils.GetDefaultValue(new ScriptType(valueType));
+ newKey = enumValues.GetValue(uniqueKeyIndex);
}
else if (keyType == typeof(string))
{
@@ -442,13 +455,13 @@ namespace FlaxEditor.CustomEditors.Editors
}
}
} while (!isUnique);
-
- newValues[uniqueKey] = TypeUtils.GetDefaultValue(new ScriptType(valueType));
+ newKey = uniqueKey;
}
else
{
- throw new InvalidOperationException();
+ newKey = TypeUtils.GetDefaultValue(new ScriptType(keyType));
}
+ newValues[newKey] = TypeUtils.GetDefaultValue(new ScriptType(valueType));
}
SetValue(newValues);
diff --git a/Source/Editor/CustomEditors/Editors/TypeEditor.cs b/Source/Editor/CustomEditors/Editors/TypeEditor.cs
index eb0c839ed..ab17c4ae1 100644
--- a/Source/Editor/CustomEditors/Editors/TypeEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/TypeEditor.cs
@@ -394,11 +394,8 @@ namespace FlaxEditor.CustomEditors.Editors
if (_element != null)
{
_element.CustomControl.ValueChanged += () => SetValue(_element.CustomControl.Value.Type);
-
if (_element.CustomControl.Type == ScriptType.Object)
- {
_element.CustomControl.Type = Values.Type.Type != typeof(object) || Values[0] == null ? ScriptType.Object : TypeUtils.GetObjectType(Values[0]);
- }
}
}
@@ -408,9 +405,7 @@ namespace FlaxEditor.CustomEditors.Editors
base.Refresh();
if (!HasDifferentValues)
- {
_element.CustomControl.Value = new ScriptType(Values[0] as Type);
- }
}
}
@@ -426,9 +421,7 @@ namespace FlaxEditor.CustomEditors.Editors
base.Initialize(layout);
if (_element != null)
- {
_element.CustomControl.ValueChanged += () => SetValue(_element.CustomControl.Value);
- }
}
///
@@ -437,9 +430,32 @@ namespace FlaxEditor.CustomEditors.Editors
base.Refresh();
if (!HasDifferentValues)
- {
_element.CustomControl.Value = (ScriptType)Values[0];
- }
+ }
+ }
+
+ ///
+ /// Default implementation of the inspector used to edit reference to the . Used to pick classes.
+ ///
+ [CustomEditor(typeof(SoftTypeReference)), DefaultEditor]
+ public class SoftTypeReferenceEditor : TypeEditorBase
+ {
+ ///
+ public override void Initialize(LayoutElementsContainer layout)
+ {
+ base.Initialize(layout);
+
+ if (_element != null)
+ _element.CustomControl.ValueChanged += () => SetValue(new SoftTypeReference(_element.CustomControl.ValueTypeName));
+ }
+
+ ///
+ public override void Refresh()
+ {
+ base.Refresh();
+
+ if (!HasDifferentValues)
+ _element.CustomControl.ValueTypeName = ((SoftTypeReference)Values[0]).TypeName;
}
}
diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs
index 27d47345d..7ecd197eb 100644
--- a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs
+++ b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs
@@ -110,6 +110,25 @@ namespace FlaxEditor.GUI.ContextMenu
_isSubMenu = true;
}
+ ///
+ /// Shows the empty menu popup o na screen.
+ ///
+ /// The target control.
+ /// The target control area to cover.
+ /// Created popup.
+ public static ContextMenuBase ShowEmptyMenu(Control control, Rectangle area)
+ {
+ // Calculate the control size in the window space to handle scaled controls
+ var upperLeft = control.PointToWindow(area.UpperLeft);
+ var bottomRight = control.PointToWindow(area.BottomRight);
+ var size = bottomRight - upperLeft;
+
+ var popup = new ContextMenuBase();
+ popup.Size = size;
+ popup.Show(control, area.Location + new Float2(0, (size.Y - popup.Height) * 0.5f));
+ return popup;
+ }
+
///
/// Show context menu over given control.
///
diff --git a/Source/Editor/Windows/OutputLogWindow.cs b/Source/Editor/Windows/OutputLogWindow.cs
index 91da504e8..0b2249a33 100644
--- a/Source/Editor/Windows/OutputLogWindow.cs
+++ b/Source/Editor/Windows/OutputLogWindow.cs
@@ -542,7 +542,7 @@ namespace FlaxEditor.Windows
{
ref var line = ref lines[j];
textBlock.Range.StartIndex = startIndex + line.FirstCharIndex;
- textBlock.Range.EndIndex = startIndex + line.LastCharIndex;
+ textBlock.Range.EndIndex = startIndex + line.LastCharIndex + 1;
textBlock.Bounds = new Rectangle(new Float2(0.0f, prevBlockBottom), line.Size);
if (textBlock.Range.Length > 0)
@@ -551,7 +551,7 @@ namespace FlaxEditor.Windows
var regexStart = line.FirstCharIndex;
if (j == 0)
regexStart += prefixLength;
- var regexLength = line.LastCharIndex - regexStart;
+ var regexLength = line.LastCharIndex + 1 - regexStart;
if (regexLength > 0)
{
var match = _compileRegex.Match(entryText, regexStart, regexLength);
diff --git a/Source/Editor/Windows/Profiler/Network.cs b/Source/Editor/Windows/Profiler/Network.cs
index 662ccf445..f0b93a03c 100644
--- a/Source/Editor/Windows/Profiler/Network.cs
+++ b/Source/Editor/Windows/Profiler/Network.cs
@@ -52,7 +52,7 @@ namespace FlaxEditor.Windows.Profiler
private static string FormatSampleBytes(float v)
{
- return (uint)v + " bytes";
+ return Utilities.Utils.FormatBytesCount((ulong)v);
}
///
diff --git a/Source/Engine/Core/Collections/BitArray.h b/Source/Engine/Core/Collections/BitArray.h
index 6058dc8a5..01238d434 100644
--- a/Source/Engine/Core/Collections/BitArray.h
+++ b/Source/Engine/Core/Collections/BitArray.h
@@ -214,7 +214,7 @@ public:
///
/// The index of the item.
/// The value to set.
- void Set(int32 index, bool value) const
+ void Set(int32 index, bool value)
{
ASSERT(index >= 0 && index < _count);
const ItemType offset = index / sizeof(ItemType);
diff --git a/Source/Engine/Core/Delegate.h b/Source/Engine/Core/Delegate.h
index 21a4374ea..4fcf60390 100644
--- a/Source/Engine/Core/Delegate.h
+++ b/Source/Engine/Core/Delegate.h
@@ -203,23 +203,12 @@ public:
return _function != nullptr;
}
- ///
- /// Calls the binded function if any has been assigned.
- ///
- /// A list of parameters for the function invocation.
- /// Function result
- void TryCall(Params ... params) const
- {
- if (_function)
- _function(_callee, Forward(params)...);
- }
-
///
/// Calls the binded function (it must be assigned).
///
/// A list of parameters for the function invocation.
/// Function result
- ReturnType operator()(Params ... params) const
+ FORCE_INLINE ReturnType operator()(Params ... params) const
{
ASSERT(_function);
return _function(_callee, Forward(params)...);
diff --git a/Source/Engine/Networking/NetworkManager.cpp b/Source/Engine/Networking/NetworkManager.cpp
index 88a104c3e..88c59ad9a 100644
--- a/Source/Engine/Networking/NetworkManager.cpp
+++ b/Source/Engine/Networking/NetworkManager.cpp
@@ -317,11 +317,12 @@ bool NetworkManager::StartHost()
LocalClient = New(LocalClientId, NetworkConnection{ 0 });
// Auto-connect host
+ LocalClient->State = NetworkConnectionState::Connecting;
+ State = NetworkConnectionState::Connected;
+ StateChanged();
LocalClient->State = NetworkConnectionState::Connected;
ClientConnected(LocalClient);
- State = NetworkConnectionState::Connected;
- StateChanged();
return false;
}
diff --git a/Source/Engine/Networking/NetworkPeer.cpp b/Source/Engine/Networking/NetworkPeer.cpp
index 342cba674..d86824156 100644
--- a/Source/Engine/Networking/NetworkPeer.cpp
+++ b/Source/Engine/Networking/NetworkPeer.cpp
@@ -6,6 +6,7 @@
#include "Engine/Core/Log.h"
#include "Engine/Core/Math/Math.h"
#include "Engine/Platform/CPUInfo.h"
+#include "Engine/Profiler/ProfilerCPU.h"
namespace
{
@@ -131,6 +132,7 @@ void NetworkPeer::Disconnect(const NetworkConnection& connection)
bool NetworkPeer::PopEvent(NetworkEvent& eventRef)
{
+ PROFILE_CPU();
return NetworkDriver->PopEvent(&eventRef);
}
diff --git a/Source/Engine/Networking/NetworkReplicationHierarchy.cpp b/Source/Engine/Networking/NetworkReplicationHierarchy.cpp
new file mode 100644
index 000000000..d86acfc7f
--- /dev/null
+++ b/Source/Engine/Networking/NetworkReplicationHierarchy.cpp
@@ -0,0 +1,205 @@
+// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
+
+#include "NetworkReplicationHierarchy.h"
+#include "NetworkManager.h"
+#include "Engine/Level/Actor.h"
+#include "Engine/Level/SceneObject.h"
+
+uint16 NetworkReplicationNodeObjectCounter = 0;
+NetworkClientsMask NetworkClientsMask::All = { MAX_uint64, MAX_uint64 };
+
+Actor* NetworkReplicationHierarchyObject::GetActor() const
+{
+ auto* actor = ScriptingObject::Cast(Object);
+ if (!actor)
+ {
+ if (const auto* sceneObject = ScriptingObject::Cast(Object))
+ actor = sceneObject->GetParent();
+ }
+ return actor;
+}
+
+void NetworkReplicationHierarchyUpdateResult::Init()
+{
+ _clientsHaveLocation = false;
+ _clients.Resize(NetworkManager::Clients.Count());
+ _clientsMask = NetworkClientsMask();
+ for (int32 i = 0; i < _clients.Count(); i++)
+ _clientsMask.SetBit(i);
+ _entries.Clear();
+ ReplicationScale = 1.0f;
+}
+
+void NetworkReplicationHierarchyUpdateResult::SetClientLocation(int32 clientIndex, const Vector3& location)
+{
+ CHECK(clientIndex >= 0 && clientIndex < _clients.Count());
+ _clientsHaveLocation = true;
+ Client& client = _clients[clientIndex];
+ client.HasLocation = true;
+ client.Location = location;
+}
+
+bool NetworkReplicationHierarchyUpdateResult::GetClientLocation(int32 clientIndex, Vector3& location) const
+{
+ CHECK_RETURN(clientIndex >= 0 && clientIndex < _clients.Count(), false);
+ const Client& client = _clients[clientIndex];
+ location = client.Location;
+ return client.HasLocation;
+}
+
+void NetworkReplicationNode::AddObject(NetworkReplicationHierarchyObject obj)
+{
+ if (obj.ReplicationFPS > 0.0f)
+ {
+ // Randomize initial replication update to spread rep rates more evenly for large scenes that register all objects within the same frame
+ obj.ReplicationUpdatesLeft = NetworkReplicationNodeObjectCounter++ % Math::Clamp(Math::RoundToInt(NetworkManager::NetworkFPS / obj.ReplicationFPS), 1, 60);
+ }
+
+ Objects.Add(obj);
+}
+
+bool NetworkReplicationNode::RemoveObject(ScriptingObject* obj)
+{
+ return !Objects.Remove(obj);
+}
+
+bool NetworkReplicationNode::DirtyObject(ScriptingObject* obj)
+{
+ const int32 index = Objects.Find(obj);
+ if (index != -1)
+ {
+ NetworkReplicationHierarchyObject& e = Objects[index];
+ e.ReplicationUpdatesLeft = 0;
+ }
+ return index != -1;
+}
+
+void NetworkReplicationNode::Update(NetworkReplicationHierarchyUpdateResult* result)
+{
+ CHECK(result);
+ const float networkFPS = NetworkManager::NetworkFPS / result->ReplicationScale;
+ for (NetworkReplicationHierarchyObject& obj : Objects)
+ {
+ if (obj.ReplicationFPS <= 0.0f)
+ {
+ // Always relevant
+ result->AddObject(obj.Object);
+ }
+ else if (obj.ReplicationUpdatesLeft > 0)
+ {
+ // Move to the next frame
+ obj.ReplicationUpdatesLeft--;
+ }
+ else
+ {
+ NetworkClientsMask targetClients = result->GetClientsMask();
+ if (result->_clientsHaveLocation && obj.CullDistance > 0.0f)
+ {
+ // Cull object against viewers locations
+ if (const Actor* actor = obj.GetActor())
+ {
+ const Vector3 objPosition = actor->GetPosition();
+ const Real cullDistanceSq = Math::Square(obj.CullDistance);
+ for (int32 clientIndex = 0; clientIndex < result->_clients.Count(); clientIndex++)
+ {
+ const auto& client = result->_clients[clientIndex];
+ if (client.HasLocation)
+ {
+ const Real distanceSq = Vector3::DistanceSquared(objPosition, client.Location);
+ // TODO: scale down replication FPS when object is far away from all clients (eg. by 10-50%)
+ if (distanceSq >= cullDistanceSq)
+ {
+ // Object is too far from this viewer so don't send data to him
+ targetClients.UnsetBit(clientIndex);
+ }
+ }
+ }
+ }
+ }
+ if (targetClients && obj.Object)
+ {
+ // Replicate this frame
+ result->AddObject(obj.Object, targetClients);
+ }
+
+ // Calculate frames until next replication
+ obj.ReplicationUpdatesLeft = (uint16)Math::Clamp(Math::RoundToInt(networkFPS / obj.ReplicationFPS) - 1, 0, MAX_uint16);
+ }
+ }
+}
+
+NetworkReplicationGridNode::~NetworkReplicationGridNode()
+{
+ for (const auto& e : _children)
+ Delete(e.Value.Node);
+}
+
+void NetworkReplicationGridNode::AddObject(NetworkReplicationHierarchyObject obj)
+{
+ // Chunk actors locations into a grid coordinates
+ Int3 coord = Int3::Zero;
+ if (const Actor* actor = obj.GetActor())
+ {
+ coord = actor->GetPosition() / CellSize;
+ }
+
+ Cell* cell = _children.TryGet(coord);
+ if (!cell)
+ {
+ // Allocate new cell
+ cell = &_children[coord];
+ cell->Node = New();
+ cell->MinCullDistance = obj.CullDistance;
+ }
+ cell->Node->AddObject(obj);
+
+ // Cache minimum culling distance for a whole cell to skip it at once
+ cell->MinCullDistance = Math::Min(cell->MinCullDistance, obj.CullDistance);
+}
+
+bool NetworkReplicationGridNode::RemoveObject(ScriptingObject* obj)
+{
+ for (const auto& e : _children)
+ {
+ if (e.Value.Node->RemoveObject(obj))
+ {
+ // TODO: remove empty cells?
+ // TODO: update MinCullDistance for cell?
+ return true;
+ }
+ }
+ return false;
+}
+
+void NetworkReplicationGridNode::Update(NetworkReplicationHierarchyUpdateResult* result)
+{
+ CHECK(result);
+ if (result->_clientsHaveLocation)
+ {
+ // Update only cells within a range
+ const Real cellRadiusSq = Math::Square(CellSize * 1.414f);
+ for (const auto& e : _children)
+ {
+ const Vector3 cellPosition = (e.Key * CellSize) + (CellSize * 0.5f);
+ Real distanceSq = MAX_Real;
+ for (auto& client : result->_clients)
+ {
+ if (client.HasLocation)
+ distanceSq = Math::Min(distanceSq, Vector3::DistanceSquared(cellPosition, client.Location));
+ }
+ const Real minCullDistanceSq = Math::Square(e.Value.MinCullDistance);
+ if (distanceSq < minCullDistanceSq + cellRadiusSq)
+ {
+ e.Value.Node->Update(result);
+ }
+ }
+ }
+ else
+ {
+ // Brute-force over all cells
+ for (const auto& e : _children)
+ {
+ e.Value.Node->Update(result);
+ }
+ }
+}
diff --git a/Source/Engine/Networking/NetworkReplicationHierarchy.cs b/Source/Engine/Networking/NetworkReplicationHierarchy.cs
new file mode 100644
index 000000000..f03419275
--- /dev/null
+++ b/Source/Engine/Networking/NetworkReplicationHierarchy.cs
@@ -0,0 +1,25 @@
+// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
+
+namespace FlaxEngine.Networking
+{
+ partial struct NetworkReplicationHierarchyObject
+ {
+ ///
+ /// Gets the actors context (object itself or parent actor).
+ ///
+ public Actor Actor
+ {
+ get
+ {
+ var actor = Object as Actor;
+ if (actor == null)
+ {
+ var sceneObject = Object as SceneObject;
+ if (sceneObject != null)
+ actor = sceneObject.Parent;
+ }
+ return actor;
+ }
+ }
+ }
+}
diff --git a/Source/Engine/Networking/NetworkReplicationHierarchy.h b/Source/Engine/Networking/NetworkReplicationHierarchy.h
new file mode 100644
index 000000000..55cd6b7b1
--- /dev/null
+++ b/Source/Engine/Networking/NetworkReplicationHierarchy.h
@@ -0,0 +1,260 @@
+// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
+
+#pragma once
+
+#include "Types.h"
+#include "Engine/Core/Math/Vector3.h"
+#include "Engine/Core/Collections/Array.h"
+#include "Engine/Core/Collections/Dictionary.h"
+#include "Engine/Scripting/ScriptingObject.h"
+#include "Engine/Scripting/ScriptingObjectReference.h"
+
+class Actor;
+
+///
+/// Network replication hierarchy object data.
+///
+API_STRUCT(NoDefault, Namespace = "FlaxEngine.Networking") struct FLAXENGINE_API NetworkReplicationHierarchyObject
+{
+ DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkReplicationObjectInfo);
+
+ // The object to replicate.
+ API_FIELD() ScriptingObjectReference Object;
+ // The target amount of the replication updates per second (frequency of the replication). Constrained by NetworkManager::NetworkFPS. Use 0 for 'always relevant' object.
+ API_FIELD() float ReplicationFPS = 60;
+ // The minimum distance from the player to the object at which it can process replication. For example, players further away won't receive object data. Use 0 if unused.
+ API_FIELD() float CullDistance = 15000;
+ // Runtime value for update frames left for the next replication of this object. Matches NetworkManager::NetworkFPS calculated from ReplicationFPS.
+ API_FIELD(Attributes="HideInEditor") uint16 ReplicationUpdatesLeft = 0;
+
+ FORCE_INLINE NetworkReplicationHierarchyObject(const ScriptingObjectReference& obj)
+ : Object(obj.Get())
+ {
+ }
+
+ FORCE_INLINE NetworkReplicationHierarchyObject(ScriptingObject* obj = nullptr)
+ : Object(obj)
+ {
+ }
+
+ // Gets the actors context (object itself or parent actor).
+ Actor* GetActor() const;
+
+ bool operator==(const NetworkReplicationHierarchyObject& other) const
+ {
+ return Object == other.Object;
+ }
+
+ bool operator==(const ScriptingObject* other) const
+ {
+ return Object == other;
+ }
+};
+
+inline uint32 GetHash(const NetworkReplicationHierarchyObject& key)
+{
+ return GetHash(key.Object);
+}
+
+///
+/// Bit mask for NetworkClient list (eg. to selectively send object replication).
+///
+API_STRUCT(NoDefault, Namespace = "FlaxEngine.Networking") struct FLAXENGINE_API NetworkClientsMask
+{
+ DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkClientsMask);
+
+ // The first 64 bits (each for one client).
+ API_FIELD() uint64 Word0 = 0;
+ // The second 64 bits (each for one client).
+ API_FIELD() uint64 Word1 = 0;
+
+ // All bits set for all clients.
+ API_FIELD() static NetworkClientsMask All;
+
+ FORCE_INLINE bool HasBit(int32 bitIndex) const
+ {
+ const int32 wordIndex = bitIndex / 64;
+ const uint64 wordMask = 1ull << (uint64)(bitIndex - wordIndex * 64);
+ const uint64 word = *(&Word0 + wordIndex);
+ return (word & wordMask) == wordMask;
+ }
+
+ FORCE_INLINE void SetBit(int32 bitIndex)
+ {
+ const int32 wordIndex = bitIndex / 64;
+ const uint64 wordMask = 1ull << (uint64)(bitIndex - wordIndex * 64);
+ uint64& word = *(&Word0 + wordIndex);
+ word |= wordMask;
+ }
+
+ FORCE_INLINE void UnsetBit(int32 bitIndex)
+ {
+ const int32 wordIndex = bitIndex / 64;
+ const uint64 wordMask = 1ull << (uint64)(bitIndex - wordIndex * 64);
+ uint64& word = *(&Word0 + wordIndex);
+ word &= ~wordMask;
+ }
+
+ FORCE_INLINE operator bool() const
+ {
+ return Word0 + Word1 != 0;
+ }
+
+ bool operator==(const NetworkClientsMask& other) const
+ {
+ return Word0 == other.Word0 && Word1 == other.Word1;
+ }
+};
+
+///
+/// Network replication hierarchy output data to send.
+///
+API_CLASS(Namespace = "FlaxEngine.Networking") class FLAXENGINE_API NetworkReplicationHierarchyUpdateResult : public ScriptingObject
+{
+ DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(NetworkReplicationHierarchyUpdateResult, ScriptingObject);
+ friend class NetworkInternal;
+ friend class NetworkReplicationNode;
+ friend class NetworkReplicationGridNode;
+
+private:
+ struct Client
+ {
+ bool HasLocation;
+ Vector3 Location;
+ };
+
+ struct Entry
+ {
+ ScriptingObject* Object;
+ NetworkClientsMask TargetClients;
+ };
+
+ bool _clientsHaveLocation;
+ NetworkClientsMask _clientsMask;
+ Array _clients;
+ Array _entries;
+
+ void Init();
+
+public:
+ // Scales the ReplicationFPS property of objects in hierarchy. Can be used to slow down or speed up replication rate.
+ API_FIELD() float ReplicationScale = 1.0f;
+
+ // Adds object to the update results.
+ API_FUNCTION() void AddObject(ScriptingObject* obj)
+ {
+ Entry& e = _entries.AddOne();
+ e.Object = obj;
+ e.TargetClients = NetworkClientsMask::All;
+ }
+
+ // Adds object to the update results. Defines specific clients to receive the update (server-only, unused on client). Mask matches NetworkManager::Clients.
+ API_FUNCTION() void AddObject(ScriptingObject* obj, NetworkClientsMask targetClients)
+ {
+ Entry& e = _entries.AddOne();
+ e.Object = obj;
+ e.TargetClients = targetClients;
+ }
+
+ // Gets amount of the clients to use. Matches NetworkManager::Clients.
+ API_PROPERTY() int32 GetClientsCount() const
+ {
+ return _clients.Count();
+ }
+
+ // Gets mask with all client bits set. Matches NetworkManager::Clients.
+ API_PROPERTY() NetworkClientsMask GetClientsMask() const
+ {
+ return _clientsMask;
+ }
+
+ // Sets the viewer location for a certain client. Client index must match NetworkManager::Clients.
+ API_FUNCTION() void SetClientLocation(int32 clientIndex, const Vector3& location);
+
+ // Gets the viewer location for a certain client. Client index must match NetworkManager::Clients. Returns true if got a location set, otherwise false.
+ API_FUNCTION() bool GetClientLocation(int32 clientIndex, API_PARAM(out) Vector3& location) const;
+};
+
+///
+/// Base class for the network objects replication hierarchy nodes. Contains a list of objects.
+///
+API_CLASS(Abstract, Namespace = "FlaxEngine.Networking") class FLAXENGINE_API NetworkReplicationNode : public ScriptingObject
+{
+ DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(NetworkReplicationNode, ScriptingObject);
+
+ ///
+ /// List with objects stored in this node.
+ ///
+ API_FIELD() Array Objects;
+
+ ///
+ /// Adds an object into the hierarchy.
+ ///
+ /// The object to add.
+ API_FUNCTION() virtual void AddObject(NetworkReplicationHierarchyObject obj);
+
+ ///
+ /// Removes object from the hierarchy.
+ ///
+ /// The object to remove.
+ /// True on successful removal, otherwise false.
+ API_FUNCTION() virtual bool RemoveObject(ScriptingObject* obj);
+
+ ///
+ /// Force replicates the object during the next update. Resets any internal tracking state to force the synchronization.
+ ///
+ /// The object to update.
+ /// True on successful update, otherwise false.
+ API_FUNCTION() virtual bool DirtyObject(ScriptingObject* obj);
+
+ ///
+ /// Iterates over all objects and adds them to the replication work.
+ ///
+ /// The update results container.
+ API_FUNCTION() virtual void Update(NetworkReplicationHierarchyUpdateResult* result);
+};
+
+inline uint32 GetHash(const Int3& key)
+{
+ uint32 hash = GetHash(key.X);
+ CombineHash(hash, GetHash(key.Y));
+ CombineHash(hash, GetHash(key.Z));
+ return hash;
+}
+
+///
+/// Network replication hierarchy node with 3D grid spatialization. Organizes static objects into chunks to improve performance in large worlds.
+///
+API_CLASS(Namespace = "FlaxEngine.Networking") class FLAXENGINE_API NetworkReplicationGridNode : public NetworkReplicationNode
+{
+ DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(NetworkReplicationGridNode, NetworkReplicationNode);
+ ~NetworkReplicationGridNode();
+
+private:
+ struct Cell
+ {
+ NetworkReplicationNode* Node;
+ float MinCullDistance;
+ };
+
+ Dictionary _children;
+
+public:
+ ///
+ /// Size of the grid cell (in world units). Used to chunk the space for separate nodes.
+ ///
+ API_FIELD() float CellSize = 10000.0f;
+
+ void AddObject(NetworkReplicationHierarchyObject obj) override;
+ bool RemoveObject(ScriptingObject* obj) override;
+ void Update(NetworkReplicationHierarchyUpdateResult* result) override;
+};
+
+///
+/// Defines the network objects replication hierarchy (tree structure) that controls chunking and configuration of the game objects replication.
+/// Contains only 'owned' objects. It's used by the networking system only on a main thread.
+///
+API_CLASS(Namespace = "FlaxEngine.Networking") class FLAXENGINE_API NetworkReplicationHierarchy : public NetworkReplicationNode
+{
+ DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(NetworkReplicationHierarchy, NetworkReplicationNode);
+};
diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp
index a61d7577f..6813eeb54 100644
--- a/Source/Engine/Networking/NetworkReplicator.cpp
+++ b/Source/Engine/Networking/NetworkReplicator.cpp
@@ -12,6 +12,7 @@
#include "NetworkRpc.h"
#include "INetworkSerializable.h"
#include "INetworkObject.h"
+#include "NetworkReplicationHierarchy.h"
#include "Engine/Core/Collections/HashSet.h"
#include "Engine/Core/Collections/Dictionary.h"
#include "Engine/Core/Collections/ChunkedArray.h"
@@ -199,6 +200,8 @@ namespace
Dictionary IdsRemappingTable;
NetworkStream* CachedWriteStream = nullptr;
NetworkStream* CachedReadStream = nullptr;
+ NetworkReplicationHierarchyUpdateResult* CachedReplicationResult = nullptr;
+ NetworkReplicationHierarchy* Hierarchy = nullptr;
Array NewClients;
Array CachedTargets;
Dictionary SerializersTable;
@@ -307,14 +310,15 @@ void BuildCachedTargets(const Array& clients, const NetworkClien
}
}
-void BuildCachedTargets(const Array& clients, const DataContainer& clientIds, const uint32 excludedClientId = NetworkManager::ServerClientId)
+void BuildCachedTargets(const Array& clients, const DataContainer& clientIds, const uint32 excludedClientId = NetworkManager::ServerClientId, const NetworkClientsMask clientsMask = NetworkClientsMask::All)
{
CachedTargets.Clear();
if (clientIds.IsValid())
{
- for (const NetworkClient* client : clients)
+ for (int32 clientIndex = 0; clientIndex < clients.Count(); clientIndex++)
{
- if (client->State == NetworkConnectionState::Connected && client->ClientId != excludedClientId)
+ const NetworkClient* client = clients.Get()[clientIndex];
+ if (client->State == NetworkConnectionState::Connected && client->ClientId != excludedClientId && clientsMask.HasBit(clientIndex))
{
for (int32 i = 0; i < clientIds.Length(); i++)
{
@@ -329,9 +333,10 @@ void BuildCachedTargets(const Array& clients, const DataContaine
}
else
{
- for (const NetworkClient* client : clients)
+ for (int32 clientIndex = 0; clientIndex < clients.Count(); clientIndex++)
{
- if (client->State == NetworkConnectionState::Connected && client->ClientId != excludedClientId)
+ const NetworkClient* client = clients.Get()[clientIndex];
+ if (client->State == NetworkConnectionState::Connected && client->ClientId != excludedClientId && clientsMask.HasBit(clientIndex))
CachedTargets.Add(client->Connection);
}
}
@@ -377,10 +382,10 @@ void BuildCachedTargets(const Array& clients, const DataContaine
}
}
-FORCE_INLINE void BuildCachedTargets(const NetworkReplicatedObject& item)
+FORCE_INLINE void BuildCachedTargets(const NetworkReplicatedObject& item, const NetworkClientsMask clientsMask = NetworkClientsMask::All)
{
// By default send object to all connected clients excluding the owner but with optional TargetClientIds list
- BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds, item.OwnerClientId);
+ BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds, item.OwnerClientId, clientsMask);
}
FORCE_INLINE void GetNetworkName(char buffer[128], const StringAnsiView& name)
@@ -561,9 +566,10 @@ void FindObjectsForSpawn(SpawnGroup& group, ChunkedArray& spawnI
}
}
-void DirtyObjectImpl(NetworkReplicatedObject& item, ScriptingObject* obj)
+FORCE_INLINE void DirtyObjectImpl(NetworkReplicatedObject& item, ScriptingObject* obj)
{
- // TODO: implement objects state replication frequency and dirtying
+ if (Hierarchy)
+ Hierarchy->DirtyObject(obj);
}
template
@@ -703,6 +709,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 +779,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 +808,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 +837,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 +908,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 +948,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 +966,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 +980,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 +997,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 +1029,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 +1067,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 +1151,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 +1167,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 +1187,7 @@ void NetworkInternal::NetworkReplicatorClear()
IdsRemappingTable.Clear();
SAFE_DELETE(CachedWriteStream);
SAFE_DELETE(CachedReadStream);
+ SAFE_DELETE(CachedReplicationResult);
NewClients.Clear();
CachedTargets.Clear();
DespawnedObjects.Clear();
@@ -1182,9 +1283,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 +1316,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 +1357,104 @@ void NetworkInternal::NetworkReplicatorUpdate()
}
// Apply parts replication
- for (int32 i = ReplicationParts.Count() - 1; i >= 0; i--)
{
- auto& e = ReplicationParts[i];
- if (e.PartsLeft > 0)
+ PROFILE_CPU_NAMED("ReplicationParts");
+ for (int32 i = ReplicationParts.Count() - 1; i >= 0; i--)
{
- // TODO: remove replication items after some TTL to prevent memory leaks
- continue;
- }
- ScriptingObject* obj = e.Object.Get();
- if (obj)
- {
- auto it = Objects.Find(obj->GetID());
- if (it != Objects.End())
+ auto& e = ReplicationParts[i];
+ if (e.PartsLeft > 0)
{
- auto& item = it->Item;
-
- // Replicate from all collected parts data
- InvokeObjectReplication(item, e.OwnerFrame, e.Data.Get(), e.Data.Count(), e.OwnerClientId);
+ // TODO: remove replication items after some TTL to prevent memory leaks
+ continue;
}
- }
+ ScriptingObject* obj = e.Object.Get();
+ if (obj)
+ {
+ auto it = Objects.Find(obj->GetID());
+ if (it != Objects.End())
+ {
+ auto& item = it->Item;
- ReplicationParts.RemoveAt(i);
+ // Replicate from all collected parts data
+ InvokeObjectReplication(item, e.OwnerFrame, e.Data.Get(), e.Data.Count(), e.OwnerClientId);
+ }
+ }
+
+ ReplicationParts.RemoveAt(i);
+ }
}
- // Brute force synchronize all networked objects with clients
- if (CachedWriteStream == nullptr)
- CachedWriteStream = New();
- NetworkStream* stream = CachedWriteStream;
- stream->SenderId = NetworkManager::LocalClientId;
- // TODO: introduce NetworkReplicationHierarchy to optimize objects replication in large worlds (eg. batched culling networked scene objects that are too far from certain client to be relevant)
- // TODO: per-object sync interval (in frames) - could be scaled by hierarchy (eg. game could slow down sync rate for objects far from player)
- for (auto it = Objects.Begin(); it.IsNotEnd(); ++it)
+ // Replicate all owned networked objects with other clients or server
+ if (!CachedReplicationResult)
+ CachedReplicationResult = New();
+ CachedReplicationResult->Init();
+ if (!isClient && NetworkManager::Clients.IsEmpty())
{
- auto& item = it->Item;
- ScriptingObject* obj = item.Object.Get();
- if (!obj)
+ // No need to update replication when nobody's around
+ }
+ else if (Hierarchy)
+ {
+ // Tick using hierarchy
+ PROFILE_CPU_NAMED("ReplicationHierarchyUpdate");
+ Hierarchy->Update(CachedReplicationResult);
+ }
+ else
+ {
+ // Tick all owned objects
+ PROFILE_CPU_NAMED("ReplicationUpdate");
+ for (auto it = Objects.Begin(); it.IsNotEnd(); ++it)
{
- // Object got deleted
- NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remove object {}, owned by {}", item.ToString(), item.ParentId.ToString());
- Objects.Remove(it);
- continue;
+ auto& item = it->Item;
+ ScriptingObject* obj = item.Object.Get();
+ if (!obj)
+ {
+ // Object got deleted
+ NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remove object {}, owned by {}", item.ToString(), item.ParentId.ToString());
+ Objects.Remove(it);
+ continue;
+ }
+ if (item.Role != NetworkObjectRole::OwnedAuthoritative)
+ continue; // Send replication messages of only owned objects or from other client objects
+ CachedReplicationResult->AddObject(obj);
}
- if (item.Role != NetworkObjectRole::OwnedAuthoritative && (!isClient && item.OwnerClientId != NetworkManager::LocalClientId))
- continue; // Send replication messages of only owned objects or from other client objects
-
- if (item.AsNetworkObject)
- item.AsNetworkObject->OnNetworkSerialize();
-
- // Serialize object
- stream->Initialize();
- const bool failed = NetworkReplicator::InvokeSerializer(obj->GetTypeHandle(), obj, stream, true);
- if (failed)
+ }
+ if (CachedReplicationResult->_entries.HasItems())
+ {
+ PROFILE_CPU_NAMED("Replication");
+ if (CachedWriteStream == nullptr)
+ CachedWriteStream = New();
+ NetworkStream* stream = CachedWriteStream;
+ stream->SenderId = NetworkManager::LocalClientId;
+ // TODO: use Job System when replicated objects count is large
+ for (auto& e : CachedReplicationResult->_entries)
{
- //NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Cannot serialize object {} of type {} (missing serialization logic)", item.ToString(), obj->GetType().ToString());
- continue;
- }
+ ScriptingObject* obj = e.Object;
+ auto it = Objects.Find(obj->GetID());
+ if (it.IsEnd())
+ continue;
+ auto& item = it->Item;
- // Send object to clients
- {
+ // Skip serialization of objects that none will receive
+ if (!isClient)
+ {
+ BuildCachedTargets(item, e.TargetClients);
+ if (CachedTargets.Count() == 0)
+ continue;
+ }
+
+ if (item.AsNetworkObject)
+ item.AsNetworkObject->OnNetworkSerialize();
+
+ // Serialize object
+ stream->Initialize();
+ const bool failed = NetworkReplicator::InvokeSerializer(obj->GetTypeHandle(), obj, stream, true);
+ if (failed)
+ {
+ //NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Cannot serialize object {} of type {} (missing serialization logic)", item.ToString(), obj->GetType().ToString());
+ continue;
+ }
+
+ // Send object to clients
const uint32 size = stream->GetPosition();
ASSERT(size <= MAX_uint16)
NetworkMessageObjectReplicate msgData;
@@ -1344,11 +1493,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 +1521,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 +1574,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 +1606,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 +1619,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 +1644,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 +1778,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 +1803,22 @@ void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkCl
item.Spawned = true;
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Add new object {}:{}, parent {}:{}", item.ToString(), obj->GetType().ToString(), item.ParentId.ToString(), parent ? parent->Object->GetType().ToString() : String::Empty);
Objects.Add(MoveTemp(item));
+ if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative)
+ Hierarchy->AddObject(obj);
// Boost future lookups by using indirection
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remap object ID={} into object {}:{}", msgDataItem.ObjectId, item.ToString(), obj->GetType().ToString());
IdsRemappingTable.Add(msgDataItem.ObjectId, item.ObjectId);
+ }
+
+ // Spawn all newly spawned objects (ensure to have valid ownership hierarchy set before spawning object)
+ for (int32 i = 0; i < msgData.ItemsCount; i++)
+ {
+ auto& msgDataItem = msgDataItems[i];
+ ScriptingObject* obj = objects[i];
+ auto it = Objects.Find(obj->GetID());
+ auto& item = it->Item;
+ const NetworkReplicatedObject* parent = ResolveObject(msgDataItem.ParentId);
// Automatic parenting for scene objects
auto sceneObject = ScriptingObject::Cast(obj);
@@ -1666,7 +1833,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 +1854,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 +1872,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 +1888,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 +1910,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 +1936,7 @@ void NetworkInternal::OnNetworkMessageObjectRole(NetworkEvent& event, NetworkCli
void NetworkInternal::OnNetworkMessageObjectRpc(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)
{
+ PROFILE_CPU();
NetworkMessageObjectRpc msgData;
event.Message.ReadStructure(msgData);
ScopeLock lock(ObjectsLock);
diff --git a/Source/Engine/Networking/NetworkReplicator.h b/Source/Engine/Networking/NetworkReplicator.h
index 2ab97a51b..156e7eeea 100644
--- a/Source/Engine/Networking/NetworkReplicator.h
+++ b/Source/Engine/Networking/NetworkReplicator.h
@@ -42,6 +42,17 @@ public:
API_FIELD() static bool EnableLog;
#endif
+ ///
+ /// Gets the network replication hierarchy.
+ ///
+ API_PROPERTY() static NetworkReplicationHierarchy* GetHierarchy();
+
+ ///
+ /// Sets the network replication hierarchy.
+ ///
+ API_PROPERTY() static void SetHierarchy(NetworkReplicationHierarchy* value);
+
+public:
///
/// Adds the network replication serializer for a given type.
///
@@ -68,7 +79,7 @@ public:
/// Does nothing if network is offline.
/// The object to replicate.
/// The parent of the object (eg. player that spawned it).
- API_FUNCTION() static void AddObject(ScriptingObject* obj, ScriptingObject* parent = nullptr);
+ API_FUNCTION() static void AddObject(ScriptingObject* obj, const ScriptingObject* parent = nullptr);
///
/// Removes the object from the network replication system.
@@ -80,14 +91,14 @@ public:
///
/// Spawns the object to the other clients. Can be spawned by the owner who locally created it (eg. from prefab).
///
- /// Does nothing if network is offline.
+ /// Does nothing if network is offline. Doesn't spawn actor in a level - but in network replication system.
/// The object to spawn on other clients.
API_FUNCTION() static void SpawnObject(ScriptingObject* obj);
///
/// Spawns the object to the other clients. Can be spawned by the owner who locally created it (eg. from prefab).
///
- /// Does nothing if network is offline.
+ /// Does nothing if network is offline. Doesn't spawn actor in a level - but in network replication system.
/// The object to spawn on other clients.
/// List with network client IDs that should receive network spawn event. Empty to spawn on all clients.
API_FUNCTION() static void SpawnObject(ScriptingObject* obj, const DataContainer& clientIds);
@@ -99,6 +110,13 @@ public:
/// The object to despawn on other clients.
API_FUNCTION() static void DespawnObject(ScriptingObject* obj);
+ ///
+ /// Checks if the network object is spawned or added to the network replication system.
+ ///
+ /// The network object.
+ /// True if object exists in networking, otherwise false.
+ API_FUNCTION() static bool HasObject(const ScriptingObject* obj);
+
///
/// Gets the Client Id of the network object owner.
///
diff --git a/Source/Engine/Networking/Types.h b/Source/Engine/Networking/Types.h
index 11675023a..0d754e727 100644
--- a/Source/Engine/Networking/Types.h
+++ b/Source/Engine/Networking/Types.h
@@ -11,6 +11,7 @@ class INetworkSerializable;
class NetworkPeer;
class NetworkClient;
class NetworkStream;
+class NetworkReplicationHierarchy;
struct NetworkEvent;
struct NetworkConnection;
diff --git a/Source/Engine/Render2D/Font.h b/Source/Engine/Render2D/Font.h
index d3806f0b4..3f42fd396 100644
--- a/Source/Engine/Render2D/Font.h
+++ b/Source/Engine/Render2D/Font.h
@@ -17,17 +17,17 @@ class FontAsset;
///
/// The text range.
///
-API_STRUCT() struct TextRange
+API_STRUCT(NoDefault) struct TextRange
{
DECLARE_SCRIPTING_TYPE_MINIMAL(TextRange);
///
- /// The start index.
+ /// The start index (inclusive).
///
API_FIELD() int32 StartIndex;
///
- /// The end index.
+ /// The end index (exclusive).
///
API_FIELD() int32 EndIndex;
@@ -70,7 +70,7 @@ DECLARE_SCRIPTING_TYPE_MINIMAL(TextRange);
///
/// Gets the substring from the source text.
///
- /// The text.
+ /// The text.
/// The substring of the original text of the defined range.
StringView Substring(const StringView& text) const
{
@@ -87,7 +87,7 @@ struct TIsPODType
///
/// The font line info generated during text processing.
///
-API_STRUCT() struct FontLineCache
+API_STRUCT(NoDefault) struct FontLineCache
{
DECLARE_SCRIPTING_TYPE_MINIMAL(FontLineCache);
@@ -151,7 +151,7 @@ struct TIsPODType
///
/// The cached font character entry (read for rendering and further processing).
///
-API_STRUCT() struct FontCharacterEntry
+API_STRUCT(NoDefault) struct FontCharacterEntry
{
DECLARE_SCRIPTING_TYPE_MINIMAL(FontCharacterEntry);
diff --git a/Source/Engine/Scripting/SoftTypeReference.cs b/Source/Engine/Scripting/SoftTypeReference.cs
new file mode 100644
index 000000000..f12f2bdb7
--- /dev/null
+++ b/Source/Engine/Scripting/SoftTypeReference.cs
@@ -0,0 +1,87 @@
+// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
+
+using System;
+
+namespace FlaxEngine
+{
+ ///
+ /// The soft reference to the scripting type contained in the scripting assembly.
+ ///
+ public struct SoftTypeReference : IComparable, IComparable
+ {
+ private string _typeName;
+
+ ///
+ /// Gets or sets the type full name (eg. FlaxEngine.Actor).
+ ///
+ public string TypeName
+ {
+ get => _typeName;
+ set => _typeName = value;
+ }
+
+ ///
+ /// Gets or sets the type (resolves soft reference).
+ ///
+ public Type Type
+ {
+ get => _typeName != null ? Type.GetType(_typeName) : null;
+ set => _typeName = value?.FullName;
+ }
+
+ ///
+ /// Initializes a new instance of the .
+ ///
+ /// The type name.
+ public SoftTypeReference(string typeName)
+ {
+ _typeName = typeName;
+ }
+
+ ///
+ /// Gets the soft type reference from full name.
+ ///
+ /// The type name.
+ /// The soft type reference.
+ public static implicit operator SoftTypeReference(string s)
+ {
+ return new SoftTypeReference { _typeName = s };
+ }
+
+ ///
+ /// Gets the soft type reference from runtime type.
+ ///
+ /// The type.
+ /// The soft type reference.
+ public static implicit operator SoftTypeReference(Type s)
+ {
+ return new SoftTypeReference { _typeName = s?.FullName };
+ }
+
+ ///
+ public override string ToString()
+ {
+ return _typeName;
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ return _typeName?.GetHashCode() ?? 0;
+ }
+
+ ///
+ public int CompareTo(object obj)
+ {
+ if (obj is SoftTypeReference other)
+ return CompareTo(other);
+ return 0;
+ }
+
+ ///
+ public int CompareTo(SoftTypeReference other)
+ {
+ return string.Compare(_typeName, other._typeName, StringComparison.Ordinal);
+ }
+ }
+}
diff --git a/Source/Engine/Scripting/SoftTypeReference.h b/Source/Engine/Scripting/SoftTypeReference.h
new file mode 100644
index 000000000..30508f639
--- /dev/null
+++ b/Source/Engine/Scripting/SoftTypeReference.h
@@ -0,0 +1,151 @@
+// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
+
+#pragma once
+
+#include "Scripting.h"
+#include "ScriptingObject.h"
+#include "Engine/Core/Log.h"
+#include "Engine/Core/Types/String.h"
+#include "Engine/Serialization/SerializationFwd.h"
+
+///
+/// The soft reference to the scripting type contained in the scripting assembly.
+///
+template
+API_STRUCT(InBuild) struct SoftTypeReference
+{
+protected:
+ StringAnsi _typeName;
+
+public:
+ SoftTypeReference() = default;
+
+ SoftTypeReference(const SoftTypeReference& s)
+ : _typeName(s._typeName)
+ {
+ }
+
+ SoftTypeReference(SoftTypeReference&& s) noexcept
+ : _typeName(MoveTemp(s._typeName))
+ {
+ }
+
+ SoftTypeReference(const StringView& s)
+ : _typeName(s)
+ {
+ }
+
+ SoftTypeReference(const StringAnsiView& s)
+ : _typeName(s)
+ {
+ }
+
+ SoftTypeReference(const char* s)
+ : _typeName(s)
+ {
+ }
+
+public:
+ FORCE_INLINE SoftTypeReference& operator=(SoftTypeReference&& s) noexcept
+ {
+ _typeName = MoveTemp(s._typeName);
+ return *this;
+ }
+
+ FORCE_INLINE SoftTypeReference& operator=(StringAnsi&& s) noexcept
+ {
+ _typeName = MoveTemp(s);
+ return *this;
+ }
+
+ FORCE_INLINE SoftTypeReference& operator=(const SoftTypeReference& s)
+ {
+ _typeName = s._typeName;
+ return *this;
+ }
+
+ FORCE_INLINE SoftTypeReference& operator=(const StringAnsiView& s)
+ {
+ _typeName = s;
+ return *this;
+ }
+
+ FORCE_INLINE bool operator==(const SoftTypeReference& other) const
+ {
+ return _typeName == other._typeName;
+ }
+
+ FORCE_INLINE bool operator!=(const SoftTypeReference& other) const
+ {
+ return _typeName != other._typeName;
+ }
+
+ FORCE_INLINE bool operator==(const StringAnsiView& other) const
+ {
+ return _typeName == other;
+ }
+
+ FORCE_INLINE bool operator!=(const StringAnsiView& other) const
+ {
+ return _typeName != other;
+ }
+
+ FORCE_INLINE operator bool() const
+ {
+ return _typeName.HasChars();
+ }
+
+public:
+ // Gets the type full name (eg. FlaxEngine.Actor).
+ StringAnsiView GetTypeName() const
+ {
+ return StringAnsiView(_typeName);
+ }
+
+ // Gets the type (resolves soft reference).
+ ScriptingTypeHandle GetType() const
+ {
+ return Scripting::FindScriptingType(_typeName);
+ }
+
+ // Creates a new objects of that type (or of type T if failed to solve typename).
+ T* NewObject() const
+ {
+ const ScriptingTypeHandle type = Scripting::FindScriptingType(_typeName);
+ auto obj = ScriptingObject::NewObject(type);
+ if (!obj)
+ {
+ if (_typeName.HasChars())
+ LOG(Error, "Unknown or invalid type {0}", String(_typeName));
+ obj = ScriptingObject::NewObject();
+ }
+ return obj;
+ }
+};
+
+template
+uint32 GetHash(const SoftTypeReference& key)
+{
+ return GetHash(key.GetTypeName());
+}
+
+// @formatter:off
+namespace Serialization
+{
+ template
+ bool ShouldSerialize(const SoftTypeReference& v, const void* otherObj)
+ {
+ return !otherObj || v != *(SoftTypeReference*)otherObj;
+ }
+ template
+ void Serialize(ISerializable::SerializeStream& stream, const SoftTypeReference& v, const void* otherObj)
+ {
+ stream.String(v.GetTypeName());
+ }
+ template
+ void Deserialize(ISerializable::DeserializeStream& stream, SoftTypeReference& v, ISerializeModifier* modifier)
+ {
+ v = stream.GetTextAnsi();
+ }
+}
+// @formatter:on
diff --git a/Source/Engine/Serialization/JsonConverters.cs b/Source/Engine/Serialization/JsonConverters.cs
index 07388a09b..40f9cb5a2 100644
--- a/Source/Engine/Serialization/JsonConverters.cs
+++ b/Source/Engine/Serialization/JsonConverters.cs
@@ -7,7 +7,7 @@ using Newtonsoft.Json;
namespace FlaxEngine.Json
{
///
- /// Serialize references to the FlaxEngine.Object as Guid.
+ /// Serialize references to the as Guid.
///
///
internal class FlaxObjectConverter : JsonConverter
@@ -46,7 +46,7 @@ namespace FlaxEngine.Json
}
///
- /// Serialize SceneReference as Guid in internal format.
+ /// Serialize as Guid in internal format.
///
///
internal class SceneReferenceConverter : JsonConverter
@@ -79,7 +79,7 @@ namespace FlaxEngine.Json
}
///
- /// Serialize SoftObjectReference as Guid in internal format.
+ /// Serialize as Guid in internal format.
///
///
internal class SoftObjectReferenceConverter : JsonConverter
@@ -111,7 +111,36 @@ namespace FlaxEngine.Json
}
///
- /// Serialize SoftObjectReference as Guid in internal format.
+ /// Serialize as typename string in internal format.
+ ///
+ ///
+ internal class SoftTypeReferenceConverter : JsonConverter
+ {
+ ///
+ public override void WriteJson(JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
+ {
+ writer.WriteValue(((SoftTypeReference)value).TypeName);
+ }
+
+ ///
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
+ {
+ var result = new SoftTypeReference();
+ if (reader.TokenType == JsonToken.String)
+ result.TypeName = (string)reader.Value;
+
+ return result;
+ }
+
+ ///
+ public override bool CanConvert(Type objectType)
+ {
+ return objectType == typeof(SoftTypeReference);
+ }
+ }
+
+ ///
+ /// Serialize as Guid in internal format.
///
///
internal class MarginConverter : JsonConverter
@@ -237,7 +266,7 @@ namespace FlaxEngine.Json
}
///
- /// Serialize LocalizedString as inlined text is not using localization (Id member is empty).
+ /// Serialize as inlined text is not using localization (Id member is empty).
///
///
internal class LocalizedStringConverter : JsonConverter
diff --git a/Source/Engine/Serialization/JsonSerializer.cs b/Source/Engine/Serialization/JsonSerializer.cs
index b9706b724..619862b1f 100644
--- a/Source/Engine/Serialization/JsonSerializer.cs
+++ b/Source/Engine/Serialization/JsonSerializer.cs
@@ -123,6 +123,7 @@ namespace FlaxEngine.Json
settings.Converters.Add(ObjectConverter);
settings.Converters.Add(new SceneReferenceConverter());
settings.Converters.Add(new SoftObjectReferenceConverter());
+ settings.Converters.Add(new SoftTypeReferenceConverter());
settings.Converters.Add(new MarginConverter());
settings.Converters.Add(new VersionConverter());
settings.Converters.Add(new LocalizedStringConverter());
diff --git a/Source/Engine/UI/GUI/Common/Button.cs b/Source/Engine/UI/GUI/Common/Button.cs
index e3bee0017..8ab6366eb 100644
--- a/Source/Engine/UI/GUI/Common/Button.cs
+++ b/Source/Engine/UI/GUI/Common/Button.cs
@@ -10,7 +10,7 @@ namespace FlaxEngine.GUI
public class Button : ContainerControl
{
///
- /// The default height fro the buttons.
+ /// The default height for the buttons.
///
public const float DefaultHeight = 24.0f;
@@ -195,7 +195,7 @@ namespace FlaxEngine.GUI
/// Sets the button colors palette based on a given main color.
///
/// The main color.
- public void SetColors(Color color)
+ public virtual void SetColors(Color color)
{
BackgroundColor = color;
BorderColor = color.RGBMultiplied(0.5f);
diff --git a/Source/ThirdParty/tracy/tracy.Build.cs b/Source/ThirdParty/tracy/tracy.Build.cs
index d6119a5ca..ca5f485b2 100644
--- a/Source/ThirdParty/tracy/tracy.Build.cs
+++ b/Source/ThirdParty/tracy/tracy.Build.cs
@@ -57,6 +57,7 @@ public class tracy : ThirdPartyModule
files.Add(Path.Combine(FolderPath, "tracy", "Tracy.hpp"));
files.Add(Path.Combine(FolderPath, "common", "TracySystem.hpp"));
+ files.Add(Path.Combine(FolderPath, "common", "TracyQueue.hpp"));
files.Add(Path.Combine(FolderPath, "client", "TracyCallstack.h"));
}
}
diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs
index 8ccb6105e..25d30a37a 100644
--- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs
+++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs
@@ -541,7 +541,7 @@ namespace Flax.Build.Bindings
var separator = false;
if (!functionInfo.IsStatic)
{
- contents.Append("IntPtr obj");
+ contents.Append("IntPtr __obj");
separator = true;
}
@@ -1511,7 +1511,8 @@ namespace Flax.Build.Bindings
if (fieldInfo.Type.IsObjectRef)
{
- toManagedContent.Append($"managed.{fieldInfo.Name} != IntPtr.Zero ? Unsafe.As<{fieldInfo.Type.GenericArgs[0].Type}>(ManagedHandle.FromIntPtr(managed.{fieldInfo.Name}).Target) : null");
+ var managedType = GenerateCSharpNativeToManaged(buildData, fieldInfo.Type.GenericArgs[0], structureInfo);
+ toManagedContent.Append($"managed.{fieldInfo.Name} != IntPtr.Zero ? Unsafe.As<{managedType}>(ManagedHandle.FromIntPtr(managed.{fieldInfo.Name}).Target) : null");
toNativeContent.Append($"managed.{fieldInfo.Name} != null ? ManagedHandle.ToIntPtr(managed.{fieldInfo.Name}, GCHandleType.Weak) : IntPtr.Zero");
freeContents.AppendLine($"if (unmanaged.{fieldInfo.Name} != IntPtr.Zero) {{ ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}).Free(); }}");
diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs
index 665063613..2c2c9b341 100644
--- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs
+++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs
@@ -1017,7 +1017,7 @@ namespace Flax.Build.Bindings
var signatureStart = contents.Length;
if (!functionInfo.IsStatic)
{
- contents.Append(caller.Name).Append("* obj");
+ contents.Append(caller.Name).Append("* __obj");
separator = true;
}
@@ -1127,7 +1127,7 @@ namespace Flax.Build.Bindings
contents.Append(indent).AppendLine($"MSVC_FUNC_EXPORT(\"{libraryEntryPoint}\")"); // Export generated function binding under the C# name
#endif
if (!functionInfo.IsStatic)
- contents.Append(indent).AppendLine("if (obj == nullptr) DebugLog::ThrowNullReference();");
+ contents.Append(indent).AppendLine("if (__obj == nullptr) DebugLog::ThrowNullReference();");
string callBegin = indent;
if (functionInfo.Glue.UseReferenceForResult)
@@ -1165,7 +1165,7 @@ namespace Flax.Build.Bindings
else
{
// Call native member method
- call = $"obj->{functionInfo.Name}";
+ call = $"__obj->{functionInfo.Name}";
}
string callParams = string.Empty;
separator = false;
@@ -1925,7 +1925,7 @@ namespace Flax.Build.Bindings
continue;
var paramsCount = eventInfo.Type.GenericArgs?.Count ?? 0;
CppIncludeFiles.Add("Engine/Profiler/ProfilerCPU.h");
- var bindPrefix = eventInfo.IsStatic ? classTypeNameNative + "::" : "obj->";
+ var bindPrefix = eventInfo.IsStatic ? classTypeNameNative + "::" : "__obj->";
if (useCSharp)
{
@@ -2006,7 +2006,7 @@ namespace Flax.Build.Bindings
if (buildData.Toolchain?.Compiler == TargetCompiler.Clang)
useSeparateImpl = true; // DLLEXPORT doesn't properly export function thus separate implementation from declaration
if (!eventInfo.IsStatic)
- contents.AppendFormat("{0}* obj, ", classTypeNameNative);
+ contents.AppendFormat("{0}* __obj, ", classTypeNameNative);
contents.Append("bool bind)");
var contentsPrev = contents;
var indent = " ";
@@ -2034,7 +2034,7 @@ namespace Flax.Build.Bindings
if (eventInfo.IsStatic)
contents.Append(indent).AppendFormat(" f.Bind<{0}_ManagedWrapper>();", eventInfo.Name).AppendLine();
else
- contents.Append(indent).AppendFormat(" f.Bind<{1}, &{1}::{0}_ManagedWrapper>(({1}*)obj);", eventInfo.Name, internalTypeName).AppendLine();
+ contents.Append(indent).AppendFormat(" f.Bind<{1}, &{1}::{0}_ManagedWrapper>(({1}*)__obj);", eventInfo.Name, internalTypeName).AppendLine();
contents.Append(indent).Append(" if (bind)").AppendLine();
contents.Append(indent).AppendFormat(" {0}{1}.Bind(f);", bindPrefix, eventInfo.Name).AppendLine();
contents.Append(indent).Append(" else").AppendLine();
@@ -2074,7 +2074,7 @@ namespace Flax.Build.Bindings
// Scripting event wrapper binding method (binds/unbinds generic wrapper to C++ delegate)
contents.AppendFormat(" static void {0}_Bind(", eventInfo.Name);
- contents.AppendFormat("{0}* obj, void* instance, bool bind)", classTypeNameNative).AppendLine();
+ contents.AppendFormat("{0}* __obj, void* instance, bool bind)", classTypeNameNative).AppendLine();
contents.Append(" {").AppendLine();
contents.Append(" FunctionGetTypeHandle();");
contents.AppendLine(" while (typeHandle)");
contents.AppendLine(" {");
- contents.AppendLine($" auto method = typeHandle.Module->FindMethod(typeHandle, \"{functionInfo.Name}\", {functionInfo.Parameters.Count});");
+ contents.AppendLine($" auto method = typeHandle.Module->FindMethod(typeHandle, StringAnsiView(\"{functionInfo.Name}\", {functionInfo.Name.Length}), {functionInfo.Parameters.Count});");
contents.AppendLine(" if (method)");
contents.AppendLine(" {");
contents.AppendLine(" Variant __result;");
@@ -2565,10 +2565,10 @@ namespace Flax.Build.Bindings
contents.AppendLine(" }").AppendLine();
// Interface implementation wrapper accessor for scripting types
- contents.AppendLine(" static void* GetInterfaceWrapper(ScriptingObject* obj)");
+ contents.AppendLine(" static void* GetInterfaceWrapper(ScriptingObject* __obj)");
contents.AppendLine(" {");
contents.AppendLine($" auto wrapper = New<{interfaceTypeNameInternal}Wrapper>();");
- contents.AppendLine(" wrapper->Object = obj;");
+ contents.AppendLine(" wrapper->Object = __obj;");
contents.AppendLine(" return wrapper;");
contents.AppendLine(" }");