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