diff --git a/Source/Editor/CustomEditors/Editors/IBrushEditor.cs b/Source/Editor/CustomEditors/Editors/IBrushEditor.cs
index beb28ca80..387364df6 100644
--- a/Source/Editor/CustomEditors/Editors/IBrushEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/IBrushEditor.cs
@@ -16,6 +16,7 @@ namespace FlaxEditor.CustomEditors.Editors
///
protected override OptionType[] Options => new[]
{
+ new OptionType("null", null),
new OptionType("Texture", typeof(TextureBrush)),
new OptionType("Sprite", typeof(SpriteBrush)),
new OptionType("GPU Texture", typeof(GPUTextureBrush)),
diff --git a/Source/Editor/CustomEditors/Editors/ObjectSwitcherEditor.cs b/Source/Editor/CustomEditors/Editors/ObjectSwitcherEditor.cs
index 25a058d86..4d0c0f662 100644
--- a/Source/Editor/CustomEditors/Editors/ObjectSwitcherEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/ObjectSwitcherEditor.cs
@@ -158,7 +158,9 @@ namespace FlaxEditor.CustomEditors.Editors
if (comboBox.SelectedIndex != -1)
{
var option = _options[comboBox.SelectedIndex];
- value = option.Creator(option.Type);
+ if (option.Type != null)
+ value = option.Creator(option.Type);
+
}
SetValue(value);
RebuildLayoutOnRefresh();
diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuSingleSelectGroup.cs b/Source/Editor/GUI/ContextMenu/ContextMenuSingleSelectGroup.cs
index 5abb52b4a..c89e1bb61 100644
--- a/Source/Editor/GUI/ContextMenu/ContextMenuSingleSelectGroup.cs
+++ b/Source/Editor/GUI/ContextMenu/ContextMenuSingleSelectGroup.cs
@@ -23,6 +23,7 @@ namespace FlaxEditor.GUI.ContextMenu
private List _menus = new List();
private List _items = new List();
+ private bool _hasSelected = false;
private SingleSelectGroupItem _selectedItem;
public T Selected
@@ -31,7 +32,7 @@ namespace FlaxEditor.GUI.ContextMenu
set
{
var index = _items.FindIndex(x => x.Value.Equals(value));
- if (index != -1 && !_selectedItem.Value.Equals(value))
+ if (index != -1 && (!_hasSelected || !_selectedItem.Value.Equals(value)))
{
SetSelected(_items[index]);
}
@@ -70,7 +71,7 @@ namespace FlaxEditor.GUI.ContextMenu
if (item.Tooltip != null)
btn.TooltipText = item.Tooltip;
item.Buttons.Add(btn);
- if (item.Equals(_selectedItem))
+ if (_hasSelected && item.Equals(_selectedItem))
btn.Checked = true;
}
@@ -82,6 +83,7 @@ namespace FlaxEditor.GUI.ContextMenu
btn.Checked = false;
}
_selectedItem = item;
+ _hasSelected = true;
SelectedChanged?.Invoke(item.Value);
item.Selected?.Invoke();
diff --git a/Source/Engine/Networking/NetworkInternal.h b/Source/Engine/Networking/NetworkInternal.h
index efcabad5f..521e8a7a2 100644
--- a/Source/Engine/Networking/NetworkInternal.h
+++ b/Source/Engine/Networking/NetworkInternal.h
@@ -12,6 +12,7 @@ enum class NetworkMessageIDs : uint8
ObjectReplicate,
ObjectReplicatePart,
ObjectSpawn,
+ ObjectSpawnPart,
ObjectDespawn,
ObjectRole,
ObjectRpc,
@@ -30,6 +31,7 @@ public:
static void OnNetworkMessageObjectReplicate(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer);
static void OnNetworkMessageObjectReplicatePart(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer);
static void OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer);
+ static void OnNetworkMessageObjectSpawnPart(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer);
static void OnNetworkMessageObjectDespawn(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer);
static void OnNetworkMessageObjectRole(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer);
static void OnNetworkMessageObjectRpc(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer);
diff --git a/Source/Engine/Networking/NetworkManager.cpp b/Source/Engine/Networking/NetworkManager.cpp
index b18136e23..ee2395f2a 100644
--- a/Source/Engine/Networking/NetworkManager.cpp
+++ b/Source/Engine/Networking/NetworkManager.cpp
@@ -15,7 +15,7 @@
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Scripting/Scripting.h"
-#define NETWORK_PROTOCOL_VERSION 2
+#define NETWORK_PROTOCOL_VERSION 3
float NetworkManager::NetworkFPS = 60.0f;
NetworkPeer* NetworkManager::Peer = nullptr;
@@ -131,6 +131,7 @@ namespace
NetworkInternal::OnNetworkMessageObjectReplicate,
NetworkInternal::OnNetworkMessageObjectReplicatePart,
NetworkInternal::OnNetworkMessageObjectSpawn,
+ NetworkInternal::OnNetworkMessageObjectSpawnPart,
NetworkInternal::OnNetworkMessageObjectDespawn,
NetworkInternal::OnNetworkMessageObjectRole,
NetworkInternal::OnNetworkMessageObjectRpc,
diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp
index ada6a4bd3..e83a5b8c8 100644
--- a/Source/Engine/Networking/NetworkReplicator.cpp
+++ b/Source/Engine/Networking/NetworkReplicator.cpp
@@ -66,8 +66,17 @@ PACK_STRUCT(struct NetworkMessageObjectSpawn
{
NetworkMessageIDs ID = NetworkMessageIDs::ObjectSpawn;
uint32 OwnerClientId;
+ uint32 OwnerSpawnId; // Unique for peer who spawned it and matches OwnerSpawnId inside following part messages
Guid PrefabId;
- uint16 ItemsCount;
+ uint16 ItemsCount; // Total items count
+ uint8 UseParts : 1; // True if spawn message is header-only and all items come in the separate parts
+ });
+
+PACK_STRUCT(struct NetworkMessageObjectSpawnPart
+ {
+ NetworkMessageIDs ID = NetworkMessageIDs::ObjectSpawnPart;
+ uint32 OwnerClientId;
+ uint32 OwnerSpawnId;
});
PACK_STRUCT(struct NetworkMessageObjectSpawnItem
@@ -173,6 +182,12 @@ struct SpawnItem
NetworkObjectRole Role;
};
+struct SpawnItemParts
+{
+ NetworkMessageObjectSpawn MsgData;
+ Array Items;
+};
+
struct SpawnGroup
{
Array> Items;
@@ -198,6 +213,7 @@ namespace
CriticalSection ObjectsLock;
HashSet Objects;
Array ReplicationParts;
+ Array SpawnParts;
Array SpawnQueue;
Array DespawnQueue;
Array RpcQueue;
@@ -213,6 +229,7 @@ namespace
Dictionary CSharpCachedNames;
#endif
Array DespawnedObjects;
+ uint32 SpawnId = 0;
}
class NetworkReplicationService : public EngineService
@@ -258,7 +275,7 @@ NetworkReplicatedObject* ResolveObject(Guid objectId)
return it != Objects.End() ? &it->Item : nullptr;
}
-NetworkReplicatedObject* ResolveObject(Guid objectId, Guid parentId, char objectTypeName[128])
+NetworkReplicatedObject* ResolveObject(Guid objectId, Guid parentId, const char objectTypeName[128])
{
// Lookup object
NetworkReplicatedObject* obj = ResolveObject(objectId);
@@ -398,8 +415,33 @@ FORCE_INLINE void GetNetworkName(char buffer[128], const StringAnsiView& name)
buffer[name.Length()] = 0;
}
+void SetupObjectSpawnMessageItem(SpawnItem* e, NetworkMessage& msg)
+{
+ ScriptingObject* obj = e->Object.Get();
+ auto it = Objects.Find(obj->GetID());
+ const auto& item = it->Item;
+
+ // Add object into spawn message
+ NetworkMessageObjectSpawnItem msgDataItem;
+ msgDataItem.ObjectId = item.ObjectId;
+ msgDataItem.ParentId = item.ParentId;
+ if (NetworkManager::IsClient())
+ {
+ // Remap local client object ids into server ids
+ IdsRemappingTable.KeyOf(msgDataItem.ObjectId, &msgDataItem.ObjectId);
+ IdsRemappingTable.KeyOf(msgDataItem.ParentId, &msgDataItem.ParentId);
+ }
+ msgDataItem.PrefabObjectID = Guid::Empty;
+ auto* objScene = ScriptingObject::Cast(obj);
+ if (objScene && objScene->HasPrefabLink())
+ msgDataItem.PrefabObjectID = objScene->GetPrefabObjectID();
+ GetNetworkName(msgDataItem.ObjectTypeName, obj->GetType().Fullname);
+ msg.WriteStructure(msgDataItem);
+}
+
void SendObjectSpawnMessage(const SpawnGroup& group, const Array& clients)
{
+ PROFILE_CPU();
const bool isClient = NetworkManager::IsClient();
auto* peer = NetworkManager::Peer;
NetworkMessage msg = peer->BeginSendMessage();
@@ -415,37 +457,56 @@ void SendObjectSpawnMessage(const SpawnGroup& group, const Array
// Setup clients that should receive this spawn message
auto it = Objects.Find(obj->GetID());
- auto& item = it->Item;
+ const auto& item = it->Item;
BuildCachedTargets(clients, item.TargetClientIds);
}
- msg.WriteStructure(msgData);
- for (SpawnItem* e : group.Items)
- {
- ScriptingObject* obj = e->Object.Get();
- auto it = Objects.Find(obj->GetID());
- auto& item = it->Item;
- // Add object into spawn message
- NetworkMessageObjectSpawnItem msgDataItem;
- msgDataItem.ObjectId = item.ObjectId;
- msgDataItem.ParentId = item.ParentId;
+ // Network Peer has fixed size of messages so split spawn message into parts if there are too many objects to fit at once
+ msgData.OwnerSpawnId = ++SpawnId;
+ msgData.UseParts = msg.BufferSize - msg.Position < group.Items.Count() * sizeof(NetworkMessageObjectSpawnItem);
+ msg.WriteStructure(msgData);
+ if (msgData.UseParts)
+ {
if (isClient)
+ peer->EndSendMessage(NetworkChannelType::Reliable, msg);
+ else
+ peer->EndSendMessage(NetworkChannelType::Reliable, msg, CachedTargets);
+
+ // Send spawn items in separate parts
+ NetworkMessageObjectSpawnPart msgDataPart;
+ msgDataPart.OwnerClientId = msgData.OwnerClientId;
+ msgDataPart.OwnerSpawnId = msgData.OwnerSpawnId;
+ uint16 itemIndex = 0;
+ constexpr uint32 spawnItemMaxSize = sizeof(uint16) + sizeof(NetworkMessageObjectSpawnItem); // Index + Data
+ while (itemIndex < msgData.ItemsCount)
{
- // Remap local client object ids into server ids
- IdsRemappingTable.KeyOf(msgDataItem.ObjectId, &msgDataItem.ObjectId);
- IdsRemappingTable.KeyOf(msgDataItem.ParentId, &msgDataItem.ParentId);
+ msg = peer->BeginSendMessage();
+ msg.WriteStructure(msgDataPart);
+
+ // Write as many items as possible into this message
+ while (msg.Position + spawnItemMaxSize <= msg.BufferSize && itemIndex < msgData.ItemsCount)
+ {
+ msg.WriteUInt16(itemIndex);
+ SetupObjectSpawnMessageItem(group.Items[itemIndex], msg);
+ itemIndex++;
+ }
+
+ if (isClient)
+ peer->EndSendMessage(NetworkChannelType::Reliable, msg);
+ else
+ peer->EndSendMessage(NetworkChannelType::Reliable, msg, CachedTargets);
}
- msgDataItem.PrefabObjectID = Guid::Empty;
- auto* objScene = ScriptingObject::Cast(obj);
- if (objScene && objScene->HasPrefabLink())
- msgDataItem.PrefabObjectID = objScene->GetPrefabObjectID();
- GetNetworkName(msgDataItem.ObjectTypeName, obj->GetType().Fullname);
- msg.WriteStructure(msgDataItem);
}
- if (isClient)
- peer->EndSendMessage(NetworkChannelType::Reliable, msg);
else
- peer->EndSendMessage(NetworkChannelType::Reliable, msg, CachedTargets);
+ {
+ // Send all spawn items within the spawn message
+ for (SpawnItem* e : group.Items)
+ SetupObjectSpawnMessageItem(e, msg);
+ if (isClient)
+ peer->EndSendMessage(NetworkChannelType::Reliable, msg);
+ else
+ peer->EndSendMessage(NetworkChannelType::Reliable, msg, CachedTargets);
+ }
}
void SendObjectRoleMessage(const NetworkReplicatedObject& item, const NetworkClient* excludedClient = nullptr)
@@ -655,6 +716,235 @@ void InvokeObjectReplication(NetworkReplicatedObject& item, uint32 ownerFrame, b
DirtyObjectImpl(item, obj);
}
+void InvokeObjectSpawn(const NetworkMessageObjectSpawn& msgData, const NetworkMessageObjectSpawnItem* msgDataItems)
+{
+ ScopeLock lock(ObjectsLock);
+
+ // Check if that object has been already spawned
+ auto& rootItem = msgDataItems[0];
+ NetworkReplicatedObject* root = ResolveObject(rootItem.ObjectId, rootItem.ParentId, rootItem.ObjectTypeName);
+ if (root)
+ {
+ // Object already exists locally so just synchronize the ownership (and mark as spawned)
+ for (int32 i = 0; i < msgData.ItemsCount; i++)
+ {
+ auto& msgDataItem = msgDataItems[i];
+ NetworkReplicatedObject* e = ResolveObject(msgDataItem.ObjectId, msgDataItem.ParentId, msgDataItem.ObjectTypeName);
+ auto& item = *e;
+ item.Spawned = true;
+ if (NetworkManager::IsClient())
+ {
+ // 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)
+ {
+ // Other client spawned object with a different owner
+ // TODO: send reply message to inform about proper object ownership that client
+ }
+ }
+ return;
+ }
+
+ // Recreate object locally (spawn only root)
+ Actor* prefabInstance = nullptr;
+ Array objects;
+ if (msgData.PrefabId.IsValid())
+ {
+ const NetworkReplicatedObject* parent = ResolveObject(rootItem.ParentId);
+ Actor* parentActor = parent && parent->Object && parent->Object->Is() ? parent->Object.As() : nullptr;
+ if (parentActor && parentActor->GetPrefabID() == msgData.PrefabId)
+ {
+ // Reuse parent object as prefab instance
+ prefabInstance = parentActor;
+ }
+ else if ((parentActor = Scripting::TryFindObject(rootItem.ParentId)))
+ {
+ // Try to find that spawned prefab (eg. prefab with networked script was spawned before so now we need to link it)
+ for (Actor* child : parentActor->Children)
+ {
+ if (child->GetPrefabID() == msgData.PrefabId)
+ {
+ if (Objects.Contains(child->GetID()))
+ {
+ ScriptingObject* obj = FindPrefabObject(child, rootItem.PrefabObjectID);
+ if (Objects.Contains(obj->GetID()))
+ {
+ // Other instance with already spawned network object
+ obj = nullptr;
+ }
+ else
+ {
+ // Reuse already spawned object within a parent
+ prefabInstance = child;
+ break;
+ }
+ }
+ }
+ }
+ }
+ if (!prefabInstance)
+ {
+ // Spawn prefab
+ auto prefab = (Prefab*)LoadAsset(msgData.PrefabId, Prefab::TypeInitializer);
+ if (!prefab)
+ {
+ NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find prefab {}", msgData.PrefabId.ToString());
+ return;
+ }
+ prefabInstance = PrefabManager::SpawnPrefab(prefab, nullptr, nullptr);
+ if (!prefabInstance)
+ {
+ NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", msgData.PrefabId.ToString());
+ return;
+ }
+ }
+
+ // Resolve objects from prefab instance
+ objects.Resize(msgData.ItemsCount);
+ for (int32 i = 0; i < msgData.ItemsCount; i++)
+ {
+ auto& msgDataItem = msgDataItems[i];
+ ScriptingObject* obj = FindPrefabObject(prefabInstance, msgDataItem.PrefabObjectID);
+ if (!obj)
+ {
+ NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} in prefab {}", msgDataItem.PrefabObjectID.ToString(), msgData.PrefabId.ToString());
+ Delete(prefabInstance);
+ return;
+ }
+ objects[i] = obj;
+ }
+ }
+ else if (msgData.ItemsCount == 1)
+ {
+ // Spawn object
+ const ScriptingTypeHandle objectType = Scripting::FindScriptingType(rootItem.ObjectTypeName);
+ ScriptingObject* obj = ScriptingObject::NewObject(objectType);
+ if (!obj)
+ {
+ NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", String(rootItem.ObjectTypeName));
+ return;
+ }
+ objects.Add(obj);
+ }
+ else
+ {
+ // Spawn objects
+ objects.Resize(msgData.ItemsCount);
+ for (int32 i = 0; i < msgData.ItemsCount; i++)
+ {
+ auto& msgDataItem = msgDataItems[i];
+ const ScriptingTypeHandle objectType = Scripting::FindScriptingType(msgDataItem.ObjectTypeName);
+ ScriptingObject* obj = ScriptingObject::NewObject(objectType);
+ if (!obj)
+ {
+ NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", String(msgDataItem.ObjectTypeName));
+ for (ScriptingObject* e : objects)
+ Delete(e);
+ return;
+ }
+ objects[i] = obj;
+ if (i != 0)
+ {
+ // Link hierarchy of spawned objects before calling any networking code for them
+ if (auto sceneObject = ScriptingObject::Cast(obj))
+ {
+ Actor* parent = nullptr;
+ for (int32 j = 0; j < i; j++)
+ {
+ if (msgDataItems[j].ObjectId == msgDataItem.ParentId)
+ {
+ parent = ScriptingObject::Cast(objects[j]);
+ break;
+ }
+ }
+ if (parent)
+ sceneObject->SetParent(parent);
+ }
+ }
+ }
+ }
+
+ // Add all newly spawned objects
+ for (int32 i = 0; i < msgData.ItemsCount; i++)
+ {
+ auto& msgDataItem = msgDataItems[i];
+ ScriptingObject* obj = objects[i];
+ if (!obj->IsRegistered())
+ obj->RegisterObject();
+ const NetworkReplicatedObject* parent = ResolveObject(msgDataItem.ParentId);
+
+ // Add object to the list
+ NetworkReplicatedObject item;
+ item.Object = obj;
+ item.AsNetworkObject = ScriptingObject::ToInterface(obj);
+ item.ObjectId = obj->GetID();
+ item.ParentId = parent ? parent->ObjectId : Guid::Empty;
+ item.OwnerClientId = msgData.OwnerClientId;
+ item.Role = NetworkObjectRole::Replicated;
+ if (item.OwnerClientId == NetworkManager::LocalClientId)
+ {
+ // Upgrade ownership automatically (eg. server spawned object that local client should own)
+ item.Role = NetworkObjectRole::OwnedAuthoritative;
+ }
+ 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);
+ if (sceneObject)
+ {
+ if (parent && parent->Object.Get() && parent->Object->Is())
+ sceneObject->SetParent(parent->Object.As());
+ else if (auto* parentActor = Scripting::TryFindObject(msgDataItem.ParentId))
+ sceneObject->SetParent(parentActor);
+ else if (msgDataItem.ParentId.IsValid())
+ {
+#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"))
+ {
+ NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} as parent to spawned object", msgDataItem.ParentId.ToString());
+ }
+#endif
+ }
+ }
+ else if (!parent && msgDataItem.ParentId.IsValid())
+ {
+ NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} as parent to spawned object", msgDataItem.ParentId.ToString());
+ }
+
+ if (item.AsNetworkObject)
+ item.AsNetworkObject->OnNetworkSpawn();
+ }
+
+ // TODO: if we're server then spawn this object further on other clients (use TargetClientIds for that object - eg. object spawned by client on client for certain set of other clients only)
+}
+
NetworkRpcParams::NetworkRpcParams(const NetworkStream* stream)
: SenderId(stream->SenderId)
{
@@ -1388,7 +1678,7 @@ void NetworkInternal::NetworkReplicatorUpdate()
auto& e = ReplicationParts[i];
if (e.PartsLeft > 0)
{
- // TODO: remove replication items after some TTL to prevent memory leaks
+ // TODO: remove replication items after some TTL to reduce memory usage
continue;
}
ScriptingObject* obj = e.Object.Get();
@@ -1408,6 +1698,8 @@ void NetworkInternal::NetworkReplicatorUpdate()
}
}
+ // TODO: remove items from SpawnParts after some TTL to reduce memory usage
+
// Replicate all owned networked objects with other clients or server
if (!CachedReplicationResult)
CachedReplicationResult = New();
@@ -1655,234 +1947,60 @@ void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkCl
PROFILE_CPU();
NetworkMessageObjectSpawn msgData;
event.Message.ReadStructure(msgData);
- auto* msgDataItems = (NetworkMessageObjectSpawnItem*)event.Message.SkipBytes(msgData.ItemsCount * sizeof(NetworkMessageObjectSpawnItem));
if (msgData.ItemsCount == 0)
return;
- ScopeLock lock(ObjectsLock);
-
- // Check if that object has been already spawned
- auto& rootItem = msgDataItems[0];
- NetworkReplicatedObject* root = ResolveObject(rootItem.ObjectId, rootItem.ParentId, rootItem.ObjectTypeName);
- if (root)
+ if (msgData.UseParts)
{
- // Object already exists locally so just synchronize the ownership (and mark as spawned)
- for (int32 i = 0; i < msgData.ItemsCount; i++)
- {
- auto& msgDataItem = msgDataItems[i];
- NetworkReplicatedObject* e = ResolveObject(msgDataItem.ObjectId, msgDataItem.ParentId, msgDataItem.ObjectTypeName);
- auto& item = *e;
- item.Spawned = true;
- if (NetworkManager::IsClient())
- {
- // 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)
- {
- // Other client spawned object with a different owner
- // TODO: send reply message to inform about proper object ownership that client
- }
- }
- return;
- }
-
- // Recreate object locally (spawn only root)
- Actor* prefabInstance = nullptr;
- Array objects;
- if (msgData.PrefabId.IsValid())
- {
- const NetworkReplicatedObject* parent = ResolveObject(rootItem.ParentId);
- Actor* parentActor = parent && parent->Object && parent->Object->Is() ? parent->Object.As() : nullptr;
- if (parentActor && parentActor->GetPrefabID() == msgData.PrefabId)
- {
- // Reuse parent object as prefab instance
- prefabInstance = parentActor;
- }
- else if ((parentActor = Scripting::TryFindObject(rootItem.ParentId)))
- {
- // Try to find that spawned prefab (eg. prefab with networked script was spawned before so now we need to link it)
- for (Actor* child : parentActor->Children)
- {
- if (child->GetPrefabID() == msgData.PrefabId)
- {
- if (Objects.Contains(child->GetID()))
- {
- ScriptingObject* obj = FindPrefabObject(child, rootItem.PrefabObjectID);
- if (Objects.Contains(obj->GetID()))
- {
- // Other instance with already spawned network object
- obj = nullptr;
- }
- else
- {
- // Reuse already spawned object within a parent
- prefabInstance = child;
- break;
- }
- }
- }
- }
- }
- if (!prefabInstance)
- {
- // Spawn prefab
- auto prefab = (Prefab*)LoadAsset(msgData.PrefabId, Prefab::TypeInitializer);
- if (!prefab)
- {
- NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find prefab {}", msgData.PrefabId.ToString());
- return;
- }
- prefabInstance = PrefabManager::SpawnPrefab(prefab, nullptr, nullptr);
- if (!prefabInstance)
- {
- NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", msgData.PrefabId.ToString());
- return;
- }
- }
-
- // Resolve objects from prefab instance
- objects.Resize(msgData.ItemsCount);
- for (int32 i = 0; i < msgData.ItemsCount; i++)
- {
- auto& msgDataItem = msgDataItems[i];
- ScriptingObject* obj = FindPrefabObject(prefabInstance, msgDataItem.PrefabObjectID);
- if (!obj)
- {
- NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} in prefab {}", msgDataItem.PrefabObjectID.ToString(), msgData.PrefabId.ToString());
- Delete(prefabInstance);
- return;
- }
- objects[i] = obj;
- }
- }
- else if (msgData.ItemsCount == 1)
- {
- // Spawn object
- const ScriptingTypeHandle objectType = Scripting::FindScriptingType(rootItem.ObjectTypeName);
- ScriptingObject* obj = ScriptingObject::NewObject(objectType);
- if (!obj)
- {
- NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", String(rootItem.ObjectTypeName));
- return;
- }
- objects.Add(obj);
+ // Allocate spawn message parts collecting
+ auto& parts = SpawnParts.AddOne();
+ parts.MsgData = msgData;
+ parts.Items.Resize(msgData.ItemsCount);
+ for (auto& item : parts.Items)
+ item.ObjectId = Guid::Empty; // Mark as not yet received
}
else
{
- // Spawn objects
- objects.Resize(msgData.ItemsCount);
- for (int32 i = 0; i < msgData.ItemsCount; i++)
- {
- auto& msgDataItem = msgDataItems[i];
- const ScriptingTypeHandle objectType = Scripting::FindScriptingType(msgDataItem.ObjectTypeName);
- ScriptingObject* obj = ScriptingObject::NewObject(objectType);
- if (!obj)
- {
- NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", String(msgDataItem.ObjectTypeName));
- for (ScriptingObject* e : objects)
- Delete(e);
- return;
- }
- objects[i] = obj;
- if (i != 0)
- {
- // Link hierarchy of spawned objects before calling any networking code for them
- if (auto sceneObject = ScriptingObject::Cast(obj))
- {
- Actor* parent = nullptr;
- for (int32 j = 0; j < i; j++)
- {
- if (msgDataItems[j].ObjectId == msgDataItem.ParentId)
- {
- parent = ScriptingObject::Cast(objects[j]);
- break;
- }
- }
- if (parent)
- sceneObject->SetParent(parent);
- }
- }
- }
+ const auto* msgDataItems = (NetworkMessageObjectSpawnItem*)event.Message.SkipBytes(msgData.ItemsCount * sizeof(NetworkMessageObjectSpawnItem));
+ InvokeObjectSpawn(msgData, msgDataItems);
}
+}
- // Add all newly spawned objects
- for (int32 i = 0; i < msgData.ItemsCount; i++)
+void NetworkInternal::OnNetworkMessageObjectSpawnPart(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)
+{
+ PROFILE_CPU();
+ NetworkMessageObjectSpawnPart msgData;
+ event.Message.ReadStructure(msgData);
+ int32 spawnPartsIndex;
+ for (spawnPartsIndex = 0; spawnPartsIndex < SpawnParts.Count(); spawnPartsIndex++)
{
- auto& msgDataItem = msgDataItems[i];
- ScriptingObject* obj = objects[i];
- if (!obj->IsRegistered())
- obj->RegisterObject();
- const NetworkReplicatedObject* parent = ResolveObject(msgDataItem.ParentId);
-
- // Add object to the list
- NetworkReplicatedObject item;
- item.Object = obj;
- item.AsNetworkObject = ScriptingObject::ToInterface(obj);
- item.ObjectId = obj->GetID();
- item.ParentId = parent ? parent->ObjectId : Guid::Empty;
- item.OwnerClientId = msgData.OwnerClientId;
- item.Role = NetworkObjectRole::Replicated;
- if (item.OwnerClientId == NetworkManager::LocalClientId)
- {
- // Upgrade ownership automatically (eg. server spawned object that local client should own)
- item.Role = NetworkObjectRole::OwnedAuthoritative;
- }
- 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);
+ // Find spawn parts container that matches this spawn message (unique pair of sender and id assigned by sender)
+ const auto& e = SpawnParts.Get()[spawnPartsIndex];
+ if (e.MsgData.OwnerClientId == msgData.OwnerClientId && e.MsgData.OwnerSpawnId == msgData.OwnerSpawnId)
+ break;
}
-
- // Spawn all newly spawned objects (ensure to have valid ownership hierarchy set before spawning object)
- for (int32 i = 0; i < msgData.ItemsCount; i++)
+ if (spawnPartsIndex >= SpawnParts.Count())
{
- auto& msgDataItem = msgDataItems[i];
- ScriptingObject* obj = objects[i];
- auto it = Objects.Find(obj->GetID());
- auto& item = it->Item;
- const NetworkReplicatedObject* parent = ResolveObject(msgDataItem.ParentId);
+ // Invalid part or data, ignore it
+ return;
+ }
+ auto& spawnParts = SpawnParts.Get()[spawnPartsIndex];
- // Automatic parenting for scene objects
- auto sceneObject = ScriptingObject::Cast(obj);
- if (sceneObject)
- {
- if (parent && parent->Object.Get() && parent->Object->Is())
- sceneObject->SetParent(parent->Object.As());
- else if (auto* parentActor = Scripting::TryFindObject(msgDataItem.ParentId))
- sceneObject->SetParent(parentActor);
- else if (msgDataItem.ParentId.IsValid())
- {
-#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"))
- {
- NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} as parent to spawned object", msgDataItem.ParentId.ToString());
- }
-#endif
- }
- }
- else if (!parent && msgDataItem.ParentId.IsValid())
- {
- NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} as parent to spawned object", msgDataItem.ParentId.ToString());
- }
-
- if (item.AsNetworkObject)
- item.AsNetworkObject->OnNetworkSpawn();
+ // Read all items from this part
+ constexpr uint32 spawnItemMaxSize = sizeof(uint16) + sizeof(NetworkMessageObjectSpawnItem); // Index + Data
+ while (event.Message.Position + spawnItemMaxSize <= event.Message.BufferSize)
+ {
+ const uint16 itemIndex = event.Message.ReadUInt16();
+ event.Message.ReadStructure(spawnParts.Items[itemIndex]);
}
- // TODO: if we're server then spawn this object further on other clients (use TargetClientIds for that object - eg. object spawned by client on client for certain set of other clients only)
+ // Invoke spawning if we've got all items
+ for (auto& e : spawnParts.Items)
+ {
+ if (!e.ObjectId.IsValid())
+ return;
+ }
+ InvokeObjectSpawn(spawnParts.MsgData, spawnParts.Items.Get());
+ SpawnParts.RemoveAt(spawnPartsIndex);
}
void NetworkInternal::OnNetworkMessageObjectDespawn(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)
diff --git a/Source/Engine/Physics/Actors/WheeledVehicle.cpp b/Source/Engine/Physics/Actors/WheeledVehicle.cpp
index f5f729d39..286f5b24e 100644
--- a/Source/Engine/Physics/Actors/WheeledVehicle.cpp
+++ b/Source/Engine/Physics/Actors/WheeledVehicle.cpp
@@ -40,6 +40,31 @@ const Array& WheeledVehicle::GetWheels() const
void WheeledVehicle::SetWheels(const Array& value)
{
+#if WITH_VEHICLE
+ // Don't recreate whole vehicle when some wheel properties are only changed (eg. suspension)
+ if (_actor && _vehicle && _wheels.Count() == value.Count() && _wheelsData.Count() == value.Count())
+ {
+ bool softUpdate = true;
+ for (int32 wheelIndex = 0; wheelIndex < value.Count(); wheelIndex++)
+ {
+ auto& oldWheel = _wheels.Get()[wheelIndex];
+ auto& newWheel = value.Get()[wheelIndex];
+ if (oldWheel.Type != newWheel.Type ||
+ Math::NotNearEqual(oldWheel.SuspensionForceOffset, newWheel.SuspensionForceOffset) ||
+ oldWheel.Collider != newWheel.Collider)
+ {
+ softUpdate = false;
+ break;
+ }
+ }
+ if (softUpdate)
+ {
+ _wheels = value;
+ PhysicsBackend::UpdateVehicleWheels(this);
+ return;
+ }
+ }
+#endif
_wheels = value;
Setup();
}
@@ -51,6 +76,10 @@ WheeledVehicle::EngineSettings WheeledVehicle::GetEngine() const
void WheeledVehicle::SetEngine(const EngineSettings& value)
{
+#if WITH_VEHICLE
+ if (_vehicle)
+ PhysicsBackend::SetVehicleEngine(_vehicle, &value);
+#endif
_engine = value;
}
@@ -61,6 +90,10 @@ WheeledVehicle::DifferentialSettings WheeledVehicle::GetDifferential() const
void WheeledVehicle::SetDifferential(const DifferentialSettings& value)
{
+#if WITH_VEHICLE
+ if (_vehicle)
+ PhysicsBackend::SetVehicleDifferential(_vehicle, &value);
+#endif
_differential = value;
}
diff --git a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp
index 2fb6cc5d6..25aa7dd77 100644
--- a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp
+++ b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp
@@ -29,6 +29,7 @@
#include
#include
#if WITH_VEHICLE
+#include "Engine/Core/Collections/Sorting.h"
#include "Engine/Physics/Actors/WheeledVehicle.h"
#include
#include
@@ -1351,7 +1352,7 @@ void PhysicsBackend::EndSimulateScene(void* scene)
int32 wheelsCount = 0;
for (auto wheelVehicle : scenePhysX->WheelVehicles)
{
- if (!wheelVehicle->IsActiveInHierarchy())
+ if (!wheelVehicle->IsActiveInHierarchy() || !wheelVehicle->GetEnableSimulation())
continue;
auto drive = (PxVehicleWheels*)wheelVehicle->_vehicle;
ASSERT(drive);
@@ -1566,7 +1567,7 @@ void PhysicsBackend::EndSimulateScene(void* scene)
for (int32 i = 0, ii = 0; i < scenePhysX->WheelVehicles.Count(); i++)
{
auto wheelVehicle = scenePhysX->WheelVehicles[i];
- if (!wheelVehicle->IsActiveInHierarchy())
+ if (!wheelVehicle->IsActiveInHierarchy() || !wheelVehicle->GetEnableSimulation())
continue;
auto drive = (PxVehicleWheels*)scenePhysX->WheelVehicles[ii]->_vehicle;
auto& perVehicle = WheelVehiclesResultsPerVehicle[ii];
@@ -1587,7 +1588,7 @@ void PhysicsBackend::EndSimulateScene(void* scene)
for (int32 i = 0, ii = 0; i < scenePhysX->WheelVehicles.Count(); i++)
{
auto wheelVehicle = scenePhysX->WheelVehicles[i];
- if (!wheelVehicle->IsActiveInHierarchy())
+ if (!wheelVehicle->IsActiveInHierarchy() || !wheelVehicle->GetEnableSimulation())
continue;
auto drive = WheelVehiclesCache[ii];
auto& perVehicle = WheelVehiclesResultsPerVehicle[ii];
@@ -3058,8 +3059,20 @@ int32 PhysicsBackend::MoveController(void* controller, void* shape, const Vector
#if WITH_VEHICLE
+bool SortWheels(WheeledVehicle::Wheel const& a, WheeledVehicle::Wheel const& b)
+{
+ return (int32)a.Type < (int32)b.Type;
+}
+
void* PhysicsBackend::CreateVehicle(WheeledVehicle* actor)
{
+ // TODO: handle PxVehicleDrive4WWheelOrder internally rather than sorting wheels directly on the vehicle
+ if (actor->_driveType == WheeledVehicle::DriveTypes::Drive4W)
+ {
+ // Drive4W requires wheels to match order from PxVehicleDrive4WWheelOrder enum
+ Sorting::QuickSort(actor->_wheels.Get(), actor->_wheels.Count(), SortWheels);
+ }
+
// Get wheels
Array> wheels;
for (auto& wheel : actor->_wheels)
@@ -3104,10 +3117,7 @@ void* PhysicsBackend::CreateVehicle(WheeledVehicle* actor)
// Initialize wheels simulation data
PxVec3 offsets[PX_MAX_NB_WHEELS];
for (int32 i = 0; i < wheels.Count(); i++)
- {
- auto& wheel = *wheels[i];
- offsets[i] = C2P(wheel.Collider->GetLocalPosition());
- }
+ offsets[i] = C2P(wheels[i]->Collider->GetLocalPosition());
PxF32 sprungMasses[PX_MAX_NB_WHEELS];
const float mass = actorPhysX->getMass();
// TODO: get gravityDirection from scenePhysX->Scene->getGravity()
@@ -3351,12 +3361,163 @@ void PhysicsBackend::DestroyVehicle(void* vehicle, int32 driveType)
}
}
+void PhysicsBackend::UpdateVehicleWheels(WheeledVehicle* actor)
+{
+ auto drive = (PxVehicleWheels*)actor->_vehicle;
+ PxVehicleWheelsSimData* wheelsSimData = &drive->mWheelsSimData;
+ for (uint32 i = 0; i < wheelsSimData->getNbWheels(); i++)
+ {
+ auto& wheel = actor->_wheels[i];
+
+ // Update suspension data
+ PxVehicleSuspensionData suspensionData = wheelsSimData->getSuspensionData(i);
+ const float suspensionFrequency = 7.0f;
+ suspensionData.mMaxCompression = wheel.SuspensionMaxRaise;
+ suspensionData.mMaxDroop = wheel.SuspensionMaxDrop;
+ suspensionData.mSpringStrength = Math::Square(suspensionFrequency) * suspensionData.mSprungMass;
+ suspensionData.mSpringDamperRate = wheel.SuspensionDampingRate * 2.0f * Math::Sqrt(suspensionData.mSpringStrength * suspensionData.mSprungMass);
+ wheelsSimData->setSuspensionData(i, suspensionData);
+
+ // Update tire data
+ PxVehicleTireData tire;
+ int32 tireIndex = WheelTireTypes.Find(wheel.TireFrictionScale);
+ if (tireIndex == -1)
+ {
+ // New tire type
+ tireIndex = WheelTireTypes.Count();
+ WheelTireTypes.Add(wheel.TireFrictionScale);
+ WheelTireFrictionsDirty = true;
+ }
+ tire.mType = tireIndex;
+ tire.mLatStiffX = wheel.TireLateralMax;
+ tire.mLatStiffY = wheel.TireLateralStiffness;
+ tire.mLongitudinalStiffnessPerUnitGravity = wheel.TireLongitudinalStiffness;
+ wheelsSimData->setTireData(i, tire);
+
+ // Update wheel data
+ PxVehicleWheelData wheelData;
+ wheelData.mMass = wheel.Mass;
+ wheelData.mRadius = wheel.Radius;
+ wheelData.mWidth = wheel.Width;
+ wheelData.mMOI = 0.5f * wheelData.mMass * Math::Square(wheelData.mRadius);
+ wheelData.mDampingRate = M2ToCm2(wheel.DampingRate);
+ wheelData.mMaxSteer = wheel.MaxSteerAngle * DegreesToRadians;
+ wheelData.mMaxBrakeTorque = M2ToCm2(wheel.MaxBrakeTorque);
+ wheelData.mMaxHandBrakeTorque = M2ToCm2(wheel.MaxHandBrakeTorque);
+ wheelsSimData->setWheelData(i, wheelData);
+ }
+}
+
+void PhysicsBackend::SetVehicleEngine(void* vehicle, const void* value)
+{
+ auto drive = (PxVehicleDrive*)vehicle;
+ auto& engine = *(const WheeledVehicle::EngineSettings*)value;
+ switch (drive->getVehicleType())
+ {
+ case PxVehicleTypes::eDRIVE4W:
+ {
+ auto drive4W = (PxVehicleDrive4W*)drive;
+ PxVehicleDriveSimData4W& driveSimData = drive4W->mDriveSimData;
+ PxVehicleEngineData engineData;
+ engineData.mMOI = M2ToCm2(engine.MOI);
+ engineData.mPeakTorque = M2ToCm2(engine.MaxTorque);
+ engineData.mMaxOmega = RpmToRadPerS(engine.MaxRotationSpeed);
+ engineData.mDampingRateFullThrottle = M2ToCm2(0.15f);
+ engineData.mDampingRateZeroThrottleClutchEngaged = M2ToCm2(2.0f);
+ engineData.mDampingRateZeroThrottleClutchDisengaged = M2ToCm2(0.35f);
+ driveSimData.setEngineData(engineData);
+ break;
+ }
+ case PxVehicleTypes::eDRIVENW:
+ {
+ auto drive4W = (PxVehicleDriveNW*)drive;
+ PxVehicleDriveSimDataNW& driveSimData = drive4W->mDriveSimData;
+ PxVehicleEngineData engineData;
+ engineData.mMOI = M2ToCm2(engine.MOI);
+ engineData.mPeakTorque = M2ToCm2(engine.MaxTorque);
+ engineData.mMaxOmega = RpmToRadPerS(engine.MaxRotationSpeed);
+ engineData.mDampingRateFullThrottle = M2ToCm2(0.15f);
+ engineData.mDampingRateZeroThrottleClutchEngaged = M2ToCm2(2.0f);
+ engineData.mDampingRateZeroThrottleClutchDisengaged = M2ToCm2(0.35f);
+ driveSimData.setEngineData(engineData);
+ break;
+ }
+ }
+}
+
+void PhysicsBackend::SetVehicleDifferential(void* vehicle, const void* value)
+{
+ auto drive = (PxVehicleDrive*)vehicle;
+ auto& differential = *(const WheeledVehicle::DifferentialSettings*)value;
+ switch (drive->getVehicleType())
+ {
+ case PxVehicleTypes::eDRIVE4W:
+ {
+ auto drive4W = (PxVehicleDrive4W*)drive;
+ PxVehicleDriveSimData4W& driveSimData = drive4W->mDriveSimData;
+ PxVehicleDifferential4WData differential4WData;
+ differential4WData.mType = (PxVehicleDifferential4WData::Enum)differential.Type;
+ differential4WData.mFrontRearSplit = differential.FrontRearSplit;
+ differential4WData.mFrontLeftRightSplit = differential.FrontLeftRightSplit;
+ differential4WData.mRearLeftRightSplit = differential.RearLeftRightSplit;
+ differential4WData.mCentreBias = differential.CentreBias;
+ differential4WData.mFrontBias = differential.FrontBias;
+ differential4WData.mRearBias = differential.RearBias;
+ driveSimData.setDiffData(differential4WData);
+ break;
+ }
+ }
+}
+
void PhysicsBackend::SetVehicleGearbox(void* vehicle, const void* value)
{
auto drive = (PxVehicleDrive*)vehicle;
auto& gearbox = *(const WheeledVehicle::GearboxSettings*)value;
drive->mDriveDynData.setUseAutoGears(gearbox.AutoGear);
drive->mDriveDynData.setAutoBoxSwitchTime(Math::Max(gearbox.SwitchTime, 0.0f));
+ switch (drive->getVehicleType())
+ {
+ case PxVehicleTypes::eDRIVE4W:
+ {
+ auto drive4W = (PxVehicleDrive4W*)drive;
+ PxVehicleDriveSimData4W& driveSimData = drive4W->mDriveSimData;
+
+ // Gears
+ PxVehicleGearsData gears;
+ gears.mSwitchTime = Math::Max(gearbox.SwitchTime, 0.0f);
+ driveSimData.setGearsData(gears);
+
+ // Auto Box
+ PxVehicleAutoBoxData autoBox;
+ driveSimData.setAutoBoxData(autoBox);
+
+ // Clutch
+ PxVehicleClutchData clutch;
+ clutch.mStrength = M2ToCm2(gearbox.ClutchStrength);
+ driveSimData.setClutchData(clutch);
+ break;
+ }
+ case PxVehicleTypes::eDRIVENW:
+ {
+ auto drive4W = (PxVehicleDriveNW*)drive;
+ PxVehicleDriveSimDataNW& driveSimData = drive4W->mDriveSimData;
+
+ // Gears
+ PxVehicleGearsData gears;
+ gears.mSwitchTime = Math::Max(gearbox.SwitchTime, 0.0f);
+ driveSimData.setGearsData(gears);
+
+ // Auto Box
+ PxVehicleAutoBoxData autoBox;
+ driveSimData.setAutoBoxData(autoBox);
+
+ // Clutch
+ PxVehicleClutchData clutch;
+ clutch.mStrength = M2ToCm2(gearbox.ClutchStrength);
+ driveSimData.setClutchData(clutch);
+ break;
+ }
+ }
}
int32 PhysicsBackend::GetVehicleTargetGear(void* vehicle)
diff --git a/Source/Engine/Physics/PhysicsBackend.h b/Source/Engine/Physics/PhysicsBackend.h
index 2de719183..f20683e31 100644
--- a/Source/Engine/Physics/PhysicsBackend.h
+++ b/Source/Engine/Physics/PhysicsBackend.h
@@ -261,6 +261,9 @@ public:
// Vehicles
static void* CreateVehicle(class WheeledVehicle* actor);
static void DestroyVehicle(void* vehicle, int32 driveType);
+ static void UpdateVehicleWheels(WheeledVehicle* actor);
+ static void SetVehicleEngine(void* vehicle, const void* value);
+ static void SetVehicleDifferential(void* vehicle, const void* value);
static void SetVehicleGearbox(void* vehicle, const void* value);
static int32 GetVehicleTargetGear(void* vehicle);
static void SetVehicleTargetGear(void* vehicle, int32 value);
diff --git a/Source/Engine/Physics/PhysicsBackendEmpty.cpp b/Source/Engine/Physics/PhysicsBackendEmpty.cpp
index 516562e28..1a418af0c 100644
--- a/Source/Engine/Physics/PhysicsBackendEmpty.cpp
+++ b/Source/Engine/Physics/PhysicsBackendEmpty.cpp
@@ -732,6 +732,18 @@ void PhysicsBackend::DestroyVehicle(void* vehicle, int32 driveType)
{
}
+void PhysicsBackend::UpdateVehicleWheels(WheeledVehicle* actor)
+{
+}
+
+void PhysicsBackend::SetVehicleEngine(void* vehicle, const void* value)
+{
+}
+
+void PhysicsBackend::SetVehicleDifferential(void* vehicle, const void* value)
+{
+}
+
void PhysicsBackend::SetVehicleGearbox(void* vehicle, const void* value)
{
}
diff --git a/Source/Engine/UI/GUI/Common/ProgressBar.cs b/Source/Engine/UI/GUI/Common/ProgressBar.cs
index a742d3b43..760f031a7 100644
--- a/Source/Engine/UI/GUI/Common/ProgressBar.cs
+++ b/Source/Engine/UI/GUI/Common/ProgressBar.cs
@@ -10,6 +10,48 @@ namespace FlaxEngine.GUI
///
public class ProgressBar : ContainerControl
{
+ ///
+ /// The method used to effect the bar.
+ ///
+ public enum BarMethod
+ {
+ ///
+ /// Stretch the bar.
+ ///
+ Stretch,
+
+ ///
+ /// Clip the bar.
+ ///
+ Clip,
+ }
+
+ ///
+ /// The origin to move the progress bar to.
+ ///
+ public enum BarOrigin
+ {
+ ///
+ /// Move the bar horizontally to the left.
+ ///
+ HorizontalLeft,
+
+ ///
+ /// Move the bar horizontally to the right.
+ ///
+ HorizontalRight,
+
+ ///
+ /// Move the bar vertically up.
+ ///
+ VerticalTop,
+
+ ///
+ /// Move the bar vertically down.
+ ///
+ VerticalBottom,
+ }
+
///
/// The value.
///
@@ -41,6 +83,18 @@ namespace FlaxEngine.GUI
///
public bool UseSmoothing => !Mathf.IsZero(SmoothingScale);
+ ///
+ /// The method used to effect the bar.
+ ///
+ [EditorOrder(41), Tooltip("The method used to effect the bar.")]
+ public BarMethod Method = BarMethod.Stretch;
+
+ ///
+ /// The origin or where the bar decreases to.
+ ///
+ [EditorOrder(42), Tooltip("The origin or where the bar decreases to.")]
+ public BarOrigin Origin = BarOrigin.HorizontalLeft;
+
///
/// Gets or sets the minimum value.
///
@@ -168,12 +222,44 @@ namespace FlaxEngine.GUI
float progressNormalized = (_current - _minimum) / _maximum;
if (progressNormalized > 0.001f)
{
- var barRect = new Rectangle(0, 0, Width * progressNormalized, Height);
- BarMargin.ShrinkRectangle(ref barRect);
- if (BarBrush != null)
- BarBrush.Draw(barRect, BarColor);
- else
- Render2D.FillRectangle(barRect, BarColor);
+ Rectangle barRect = new Rectangle(0, 0, Width * progressNormalized, Height);
+ switch (Origin)
+ {
+ case BarOrigin.HorizontalLeft:
+ break;
+ case BarOrigin.HorizontalRight:
+ barRect = new Rectangle(Width - Width * progressNormalized, 0, Width * progressNormalized, Height);
+ break;
+ case BarOrigin.VerticalTop:
+ barRect = new Rectangle(0, 0, Width, Height * progressNormalized);
+ break;
+ case BarOrigin.VerticalBottom:
+ barRect = new Rectangle(0, Height - Height * progressNormalized, Width, Height * progressNormalized);
+ break;
+ default: break;
+ }
+
+ switch (Method)
+ {
+ case BarMethod.Stretch:
+ BarMargin.ShrinkRectangle(ref barRect);
+ if (BarBrush != null)
+ BarBrush.Draw(barRect, BarColor);
+ else
+ Render2D.FillRectangle(barRect, BarColor);
+ break;
+ case BarMethod.Clip:
+ var rect = new Rectangle(0, 0, Width, Height);
+ BarMargin.ShrinkRectangle(ref rect);
+ Render2D.PushClip(ref barRect);
+ if (BarBrush != null)
+ BarBrush.Draw(rect, BarColor);
+ else
+ Render2D.FillRectangle(rect, BarColor);
+ Render2D.PopClip();
+ break;
+ default: break;
+ }
}
}
}