From c1c870920627b864ac9a998715b23204da1f4dd4 Mon Sep 17 00:00:00 2001 From: kberg-amzn Date: Mon, 4 Oct 2021 16:57:21 -0700 Subject: [PATCH 01/26] Fix 3-way replication for when an object migrates in rapid succession Signed-off-by: kberg-amzn --- .../EntityReplicationManager.h | 1 - .../NetworkEntityUpdateMessage.h | 8 +------ .../EntityReplicationManager.cpp | 12 +++------- .../EntityReplication/EntityReplicator.cpp | 17 +++++++------- .../NetworkEntityUpdateMessage.cpp | 23 ++++--------------- 5 files changed, 17 insertions(+), 44 deletions(-) diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/EntityReplication/EntityReplicationManager.h b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/EntityReplication/EntityReplicationManager.h index 2e1f83ae38..10346ad777 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/EntityReplication/EntityReplicationManager.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/EntityReplication/EntityReplicationManager.h @@ -57,7 +57,6 @@ namespace Multiplayer EntityReplicationManager(AzNetworking::IConnection& connection, AzNetworking::IConnectionListener& connectionListener, Mode mode); ~EntityReplicationManager() = default; - void SetRemoteHostId(const HostId& hostId); const HostId& GetRemoteHostId() const; void ActivatePendingEntities(); diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/NetworkEntityUpdateMessage.h b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/NetworkEntityUpdateMessage.h index 90f622a8ae..139db2a949 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/NetworkEntityUpdateMessage.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/NetworkEntityUpdateMessage.h @@ -43,8 +43,7 @@ namespace Multiplayer //! Constructor for an entity delete message. //! @param entityId the networkId of the entity being deleted //! @param isMigrated whether or not the entity is being migrated or deleted - //! @param takeOwnership true if the remote replicator should take ownership of the entity - explicit NetworkEntityUpdateMessage(NetEntityId entityId, bool isMigrated, bool takeOwnership); + explicit NetworkEntityUpdateMessage(NetEntityId entityId, bool isMigrated); NetworkEntityUpdateMessage& operator =(NetworkEntityUpdateMessage&& rhs); NetworkEntityUpdateMessage& operator =(const NetworkEntityUpdateMessage& rhs); @@ -71,10 +70,6 @@ namespace Multiplayer //! @return whether or not the entity was migrated bool GetWasMigrated() const; - //! Gets the current value of TakeOwnership. - //! @return the current value of TakeOwnership - bool GetTakeOwnership() const; - //! Gets the current value of HasValidPrefabId. //! @return the current value of HasValidPrefabId bool GetHasValidPrefabId() const; @@ -110,7 +105,6 @@ namespace Multiplayer NetEntityId m_entityId = InvalidNetEntityId; bool m_isDelete = false; bool m_wasMigrated = false; - bool m_takeOwnership = false; bool m_hasValidPrefabId = false; PrefabEntityId m_prefabEntityId; diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp index 6f65d01e50..91ca1bff84 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp @@ -47,6 +47,9 @@ namespace Multiplayer , m_entityExitDomainEventHandler([this](const ConstNetworkEntityHandle& entityHandle) { OnEntityExitDomain(entityHandle); }) , m_notifyEntityMigrationHandler([this](const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId) { OnPostEntityMigration(entityHandle, remoteHostId); }) { + // Set up our remote host identifier, we use the IP address of the host + m_remoteHostId = connection.GetRemoteAddress(); + // Our max payload size is whatever is passed in, minus room for a udp packetheader m_maxPayloadSize = connection.GetConnectionMtu() - UdpPacketHeaderSerializeSize - ReplicationManagerPacketOverhead; @@ -65,11 +68,6 @@ namespace Multiplayer GetMultiplayer()->AddNotifyEntityMigrationEventHandler(m_notifyEntityMigrationHandler); } - void EntityReplicationManager::SetRemoteHostId(const HostId& hostId) - { - m_remoteHostId = hostId; - } - const HostId& EntityReplicationManager::GetRemoteHostId() const { return m_remoteHostId; @@ -516,10 +514,6 @@ namespace Multiplayer AZLOG(NET_RepDeletes, "Deleting replicater for entity id %u remote host %s", updateMessage.GetEntityId(), GetRemoteHostId().GetString().c_str()); } } - else - { - shouldDeleteEntity = updateMessage.GetTakeOwnership(); - } // Handle entity cleanup if (shouldDeleteEntity) diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicator.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicator.cpp index 05001b5960..807bbf9e21 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicator.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicator.cpp @@ -296,10 +296,10 @@ namespace Multiplayer bool EntityReplicator::OwnsReplicatorLifetime() const { bool ret(false); - if (GetBoundLocalNetworkRole() == NetEntityRole::Authority - || (GetBoundLocalNetworkRole() == NetEntityRole::Server + if (GetBoundLocalNetworkRole() == NetEntityRole::Authority // Authority always owns lifetime + || (GetBoundLocalNetworkRole() == NetEntityRole::Server // Server also owns lifetime if the remote endpoint is a client of some form && (GetRemoteNetworkRole() == NetEntityRole::Client - || GetRemoteNetworkRole() == NetEntityRole::Autonomous))) + || GetRemoteNetworkRole() == NetEntityRole::Autonomous))) { ret = true; } @@ -309,10 +309,9 @@ namespace Multiplayer bool EntityReplicator::RemoteManagerOwnsEntityLifetime() const { bool isServer = (GetBoundLocalNetworkRole() == NetEntityRole::Server) - && (GetRemoteNetworkRole() == NetEntityRole::Authority); + && (GetRemoteNetworkRole() == NetEntityRole::Authority); bool isClient = (GetBoundLocalNetworkRole() == NetEntityRole::Client) - || (GetBoundLocalNetworkRole() == NetEntityRole::Autonomous); - + || (GetBoundLocalNetworkRole() == NetEntityRole::Autonomous); return isServer || isClient; } @@ -477,14 +476,14 @@ namespace Multiplayer WasMigrated() ? 1 : 0, m_replicationManager.GetRemoteHostId().GetString().c_str() ); - return NetworkEntityUpdateMessage(GetEntityHandle().GetNetEntityId(), WasMigrated(), m_propertyPublisher->IsRemoteReplicatorEstablished()); + return NetworkEntityUpdateMessage(GetEntityHandle().GetNetEntityId(), WasMigrated()); } NetBindComponent* netBindComponent = GetNetBindComponent(); - //const bool sendSliceName = !m_propertyPublisher->IsRemoteReplicatorEstablished(); + const bool sendSliceName = !m_propertyPublisher->IsRemoteReplicatorEstablished(); NetworkEntityUpdateMessage updateMessage(GetRemoteNetworkRole(), GetEntityHandle().GetNetEntityId()); - //if (sendSliceName) + if (sendSliceName) { updateMessage.SetPrefabEntityId(netBindComponent->GetPrefabEntityId()); } diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityUpdateMessage.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityUpdateMessage.cpp index 4a2f12ce17..d635dbaf80 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityUpdateMessage.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityUpdateMessage.cpp @@ -18,7 +18,6 @@ namespace Multiplayer , m_entityId(rhs.m_entityId) , m_isDelete(rhs.m_isDelete) , m_wasMigrated(rhs.m_wasMigrated) - , m_takeOwnership(rhs.m_takeOwnership) , m_hasValidPrefabId(rhs.m_hasValidPrefabId) , m_prefabEntityId(rhs.m_prefabEntityId) , m_data(AZStd::move(rhs.m_data)) @@ -31,7 +30,6 @@ namespace Multiplayer , m_entityId(rhs.m_entityId) , m_isDelete(rhs.m_isDelete) , m_wasMigrated(rhs.m_wasMigrated) - , m_takeOwnership(rhs.m_takeOwnership) , m_hasValidPrefabId(rhs.m_hasValidPrefabId) , m_prefabEntityId(rhs.m_prefabEntityId) { @@ -58,11 +56,10 @@ namespace Multiplayer ; } - NetworkEntityUpdateMessage::NetworkEntityUpdateMessage(NetEntityId entityId, bool wasMigrated, bool takeOwnership) + NetworkEntityUpdateMessage::NetworkEntityUpdateMessage(NetEntityId entityId, bool wasMigrated) : m_entityId(entityId) , m_isDelete(true) , m_wasMigrated(wasMigrated) - , m_takeOwnership(takeOwnership) { // this is a delete entity message c-tor } @@ -73,7 +70,6 @@ namespace Multiplayer m_entityId = rhs.m_entityId; m_isDelete = rhs.m_isDelete; m_wasMigrated = rhs.m_wasMigrated; - m_takeOwnership = rhs.m_takeOwnership; m_hasValidPrefabId = rhs.m_hasValidPrefabId; m_prefabEntityId = rhs.m_prefabEntityId; m_data = AZStd::move(rhs.m_data); @@ -86,7 +82,6 @@ namespace Multiplayer m_entityId = rhs.m_entityId; m_isDelete = rhs.m_isDelete; m_wasMigrated = rhs.m_wasMigrated; - m_takeOwnership = rhs.m_takeOwnership; m_hasValidPrefabId = rhs.m_hasValidPrefabId; m_prefabEntityId = rhs.m_prefabEntityId; if (rhs.m_data != nullptr) @@ -104,7 +99,6 @@ namespace Multiplayer && (m_entityId == rhs.m_entityId) && (m_isDelete == rhs.m_isDelete) && (m_wasMigrated == rhs.m_wasMigrated) - && (m_takeOwnership == rhs.m_takeOwnership) && (m_hasValidPrefabId == rhs.m_hasValidPrefabId) && (m_prefabEntityId == rhs.m_prefabEntityId)); } @@ -160,11 +154,6 @@ namespace Multiplayer return m_wasMigrated; } - bool NetworkEntityUpdateMessage::GetTakeOwnership() const - { - return m_takeOwnership; - } - bool NetworkEntityUpdateMessage::GetHasValidPrefabId() const { return m_hasValidPrefabId; @@ -210,17 +199,15 @@ namespace Multiplayer serializer.Serialize(m_entityId, "EntityId"); // Use the upper 4 bits for boolean flags, and the lower 4 bits for the network role - uint8_t networkTypeAndFlags = (m_isDelete ? 0x80 : 0x00) - | (m_wasMigrated ? 0x40 : 0x00) - | (m_takeOwnership ? 0x20 : 0x00) + uint8_t networkTypeAndFlags = (m_isDelete ? 0x40 : 0x00) + | (m_wasMigrated ? 0x20 : 0x00) | (m_hasValidPrefabId ? 0x10 : 0x00) | static_cast(m_networkRole); if (serializer.Serialize(networkTypeAndFlags, "TypeAndFlags")) { - m_isDelete = (networkTypeAndFlags & 0x80) == 0x80; - m_wasMigrated = (networkTypeAndFlags & 0x40) == 0x40; - m_takeOwnership = (networkTypeAndFlags & 0x20) == 0x20; + m_isDelete = (networkTypeAndFlags & 0x40) == 0x40; + m_wasMigrated = (networkTypeAndFlags & 0x20) == 0x20; m_hasValidPrefabId = (networkTypeAndFlags & 0x10) == 0x10; m_networkRole = static_cast(networkTypeAndFlags & 0x0F); } From 18340f2b1bc7626daf91d5fea4282fc3433947eb Mon Sep 17 00:00:00 2001 From: kberg-amzn Date: Wed, 6 Oct 2021 18:57:45 -0700 Subject: [PATCH 02/26] Changes to get client migration partially functional Signed-off-by: kberg-amzn --- .../Multiplayer/Components/NetBindComponent.h | 4 +- .../Code/Include/Multiplayer/IMultiplayer.h | 16 +- .../Include/Multiplayer/MultiplayerTypes.h | 19 +++ .../EntityReplicationManager.h | 5 + .../AutoGen/Multiplayer.AutoPackets.xml | 1 + .../Source/Components/NetBindComponent.cpp | 4 +- .../ServerToClientConnectionData.cpp | 39 +++-- .../ServerToClientConnectionData.h | 7 +- .../Source/MultiplayerSystemComponent.cpp | 137 +++++++++++++----- .../Code/Source/MultiplayerSystemComponent.h | 10 +- .../EntityReplicationManager.cpp | 40 ++++- .../EntityReplication/EntityReplicator.cpp | 129 ++++++----------- .../NetworkEntity/NetworkEntityManager.cpp | 12 +- .../ServerToClientReplicationWindow.cpp | 16 +- .../ServerToClientReplicationWindow.h | 2 - 15 files changed, 273 insertions(+), 168 deletions(-) diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h index f604102e37..b22c8dc51c 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h @@ -32,7 +32,7 @@ namespace Multiplayer using EntityStopEvent = AZ::Event; using EntityDirtiedEvent = AZ::Event<>; using EntitySyncRewindEvent = AZ::Event<>; - using EntityServerMigrationEvent = AZ::Event; + using EntityServerMigrationEvent = AZ::Event; using EntityPreRenderEvent = AZ::Event; using EntityCorrectionEvent = AZ::Event<>; @@ -113,7 +113,7 @@ namespace Multiplayer void MarkDirty(); void NotifyLocalChanges(); void NotifySyncRewindState(); - void NotifyServerMigration(const HostId& hostId, AzNetworking::ConnectionId connectionId); + void NotifyServerMigration(const HostId& remoteHostId, const HostId& migrateHostId, AzNetworking::ConnectionId connectionId); void NotifyPreRender(float deltaTime); void NotifyCorrection(); diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h b/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h index 32af39a43b..b4be69ed4c 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h @@ -45,7 +45,7 @@ namespace Multiplayer using ClientMigrationStartEvent = AZ::Event; using ClientMigrationEndEvent = AZ::Event<>; using ClientDisconnectedEvent = AZ::Event<>; - using NotifyClientMigrationEvent = AZ::Event; + using NotifyClientMigrationEvent = AZ::Event; using NotifyEntityMigrationEvent = AZ::Event; using ConnectionAcquiredEvent = AZ::Event; using SessionInitEvent = AZ::Event; @@ -131,10 +131,11 @@ namespace Multiplayer virtual void AddSessionShutdownHandler(SessionShutdownEvent::Handler& handler) = 0; //! Signals a NotifyClientMigrationEvent with the provided parameters. - //! @param hostId the host id of the host the client is migrating to - //! @param userIdentifier the user identifier the client will provide the new host to validate identity - //! @param lastClientInputId the last processed clientInputId by the current host - virtual void SendNotifyClientMigrationEvent(const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId) = 0; + //! @param hostId the host id of the host the client is migrating to + //! @param userIdentifier the user identifier the client will provide the new host to validate identity + //! @param lastClientInputId the last processed clientInputId by the current host + //! @param controlledEntityId the entityId of the clients autonomous entity + virtual void SendNotifyClientMigrationEvent(const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId, NetEntityId controlledEntityId) = 0; //! Signals a NotifyEntityMigrationEvent with the provided parameters. //! @param entityHandle the network entity handle of the entity being migrated @@ -176,6 +177,11 @@ namespace Multiplayer //! @return pointer to the filtered entity manager, or nullptr if not set virtual IFilterEntityManager* GetFilterEntityManager() = 0; + //! Registers a temp userId to allow a host to look up a players controlled entity in the event of a rejoin or migration event. + //! @param temporaryUserIdentifier the temporary user identifier used to identify a player across hosts + //! @param controlledEntityId the controlled entityId of the players autonomous entity + virtual void RegisterPlayerIdentifierForRejoin(uint64_t temporaryUserIdentifier, NetEntityId controlledEntityId) = 0; + //! Enables or disables automatic instantiation of netbound entities. //! This setting is controlled by the networking layer and should not be touched //! If enabled, netbound entities will instantiate as spawnables are loaded into the game world, generally true for the server diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerTypes.h b/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerTypes.h index 96035083d8..8284dd7010 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerTypes.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerTypes.h @@ -68,6 +68,7 @@ namespace Multiplayer Server, // A simulated proxy on a server Authority // An authoritative proxy on a server (full authority) }; + const char* GetEnumString(NetEntityRole value); enum class ComponentSerializationType : uint8_t { @@ -113,6 +114,24 @@ namespace Multiplayer bool Serialize(AzNetworking::ISerializer& serializer); }; + inline const char* GetEnumString(NetEntityRole value) + { + switch (value) + { + case NetEntityRole::InvalidRole: + return "InvalidRole"; + case NetEntityRole::Client: + return "Client"; + case NetEntityRole::Autonomous: + return "Autonomous"; + case NetEntityRole::Server: + return "Server"; + case NetEntityRole::Authority: + return "Authority"; + } + return "Unknown"; + } + inline PrefabEntityId::PrefabEntityId(AZ::Name name, uint32_t entityOffset) : m_prefabName(name) , m_entityOffset(entityOffset) diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/EntityReplication/EntityReplicationManager.h b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/EntityReplication/EntityReplicationManager.h index 10346ad777..83f9e238f5 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/EntityReplication/EntityReplicationManager.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/EntityReplication/EntityReplicationManager.h @@ -57,6 +57,10 @@ namespace Multiplayer EntityReplicationManager(AzNetworking::IConnection& connection, AzNetworking::IConnectionListener& connectionListener, Mode mode); ~EntityReplicationManager() = default; + //! Used to override during client migration if your host has a specially assigned publically routable address. + //! @param remoteHostId the publically routable address to use in place of the remote HostId + void SetMigrateHostId(const HostId& remoteHostId); + const HostId& GetMigrateHostId() const; const HostId& GetRemoteHostId() const; void ActivatePendingEntities(); @@ -207,6 +211,7 @@ namespace Multiplayer AZ::TimeMs m_entityPendingRemovalMs = AZ::TimeMs{ 0 }; AZ::TimeMs m_frameTimeMs = AZ::TimeMs{ 0 }; HostId m_remoteHostId = InvalidHostId; + HostId m_migrateHostId = InvalidHostId; uint32_t m_maxRemoteEntitiesPendingCreationCount = AZStd::numeric_limits::max(); uint32_t m_maxPayloadSize = 0; Mode m_updateMode = Mode::Invalid; diff --git a/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml b/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml index 091043f034..a5bf169fb4 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml @@ -9,6 +9,7 @@ + diff --git a/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp index fbf83b6e6c..2e451793ed 100644 --- a/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp @@ -394,9 +394,9 @@ namespace Multiplayer m_syncRewindEvent.Signal(); } - void NetBindComponent::NotifyServerMigration(const HostId& hostId, AzNetworking::ConnectionId connectionId) + void NetBindComponent::NotifyServerMigration(const HostId& remoteHostId, const HostId& migrateHostId, AzNetworking::ConnectionId connectionId) { - m_entityServerMigrationEvent.Signal(m_netEntityHandle, hostId, connectionId); + m_entityServerMigrationEvent.Signal(m_netEntityHandle, remoteHostId, migrateHostId, connectionId); } void NetBindComponent::NotifyPreRender(float deltaTime) diff --git a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.cpp b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.cpp index a9b8e03126..29be64ac07 100644 --- a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.cpp +++ b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.cpp @@ -23,22 +23,16 @@ namespace Multiplayer ServerToClientConnectionData::ServerToClientConnectionData ( AzNetworking::IConnection* connection, - AzNetworking::IConnectionListener& connectionListener, - NetworkEntityHandle controlledEntity + AzNetworking::IConnectionListener& connectionListener ) : m_connection(connection) , m_controlledEntityRemovedHandler([this](const ConstNetworkEntityHandle&) { OnControlledEntityRemove(); }) - , m_controlledEntityMigrationHandler([this](const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId, AzNetworking::ConnectionId connectionId) { OnControlledEntityMigration(entityHandle, remoteHostId, connectionId); }) - , m_controlledEntity(controlledEntity) + , m_controlledEntityMigrationHandler([this](const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId, const HostId& migrateHostId, AzNetworking::ConnectionId connectionId) + { + OnControlledEntityMigration(entityHandle, remoteHostId, migrateHostId, connectionId); + }) , m_entityReplicationManager(*connection, connectionListener, EntityReplicationManager::Mode::LocalServerToRemoteClient) { - NetBindComponent* netBindComponent = m_controlledEntity.GetNetBindComponent(); - if (netBindComponent != nullptr) - { - netBindComponent->AddEntityStopEventHandler(m_controlledEntityRemovedHandler); - netBindComponent->AddEntityServerMigrationEventHandler(m_controlledEntityMigrationHandler); - } - m_entityReplicationManager.SetMaxRemoteEntitiesPendingCreationCount(sv_ClientMaxRemoteEntitiesPendingCreationCount); m_entityReplicationManager.SetEntityPendingRemovalMs(sv_ClientEntityReplicatorPendingRemovalTimeMs); } @@ -54,6 +48,20 @@ namespace Multiplayer m_controlledEntityRemovedHandler.Disconnect(); } + void ServerToClientConnectionData::SetControlledEntity(NetworkEntityHandle primaryPlayerEntity) + { + m_controlledEntityRemovedHandler.Disconnect(); + m_controlledEntityMigrationHandler.Disconnect(); + + m_controlledEntity = primaryPlayerEntity; + NetBindComponent* netBindComponent = m_controlledEntity.GetNetBindComponent(); + if (netBindComponent != nullptr) + { + netBindComponent->AddEntityStopEventHandler(m_controlledEntityRemovedHandler); + netBindComponent->AddEntityServerMigrationEventHandler(m_controlledEntityMigrationHandler); + } + } + ConnectionDataType ServerToClientConnectionData::GetConnectionDataType() const { return ConnectionDataType::ServerToClient; @@ -94,7 +102,8 @@ namespace Multiplayer void ServerToClientConnectionData::OnControlledEntityMigration ( [[maybe_unused]] const ConstNetworkEntityHandle& entityHandle, - [[maybe_unused]] const HostId& remoteHostId, + const HostId& remoteHostId, + const HostId& migrateHostId, [[maybe_unused]] AzNetworking::ConnectionId connectionId ) { @@ -109,13 +118,13 @@ namespace Multiplayer } // Generate crypto-rand user identifier, send to both server and client so they can negotiate the autonomous entity to assume predictive control over after migration - const uint64_t randomUserIdentifier = AzNetworking::CryptoRand64(); + const uint64_t temporaryUserIdentifier = AzNetworking::CryptoRand64(); // Tell the new host that a client is about to (re)join - GetMultiplayer()->SendNotifyClientMigrationEvent(remoteHostId, randomUserIdentifier, migratedClientInputId); + GetMultiplayer()->SendNotifyClientMigrationEvent(remoteHostId, temporaryUserIdentifier, migratedClientInputId, m_controlledEntity.GetNetEntityId()); // Tell the client who to join - MultiplayerPackets::ClientMigration clientMigration(remoteHostId, randomUserIdentifier, migratedClientInputId); + MultiplayerPackets::ClientMigration clientMigration(migrateHostId, temporaryUserIdentifier, migratedClientInputId); GetConnection()->SendReliablePacket(clientMigration); m_controlledEntity = NetworkEntityHandle(); diff --git a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.h b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.h index 8dcf08c480..a894d5e55e 100644 --- a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.h +++ b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.h @@ -20,11 +20,12 @@ namespace Multiplayer ServerToClientConnectionData ( AzNetworking::IConnection* connection, - AzNetworking::IConnectionListener& connectionListener, - NetworkEntityHandle controlledEntity + AzNetworking::IConnectionListener& connectionListener ); ~ServerToClientConnectionData() override; + void SetControlledEntity(NetworkEntityHandle primaryPlayerEntity); + //! IConnectionData interface //! @{ ConnectionDataType GetConnectionDataType() const override; @@ -42,7 +43,7 @@ namespace Multiplayer private: void OnControlledEntityRemove(); - void OnControlledEntityMigration(const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId, AzNetworking::ConnectionId connectionId); + void OnControlledEntityMigration(const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId, const HostId& migrateHostId, AzNetworking::ConnectionId connectionId); void OnGameplayStarted(); EntityReplicationManager m_entityReplicationManager; diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index b7dde3b9e7..da1ab1cb73 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -76,6 +76,7 @@ namespace Multiplayer "The address of the remote server or host to connect to"); AZ_CVAR(uint16_t, cl_serverport, DefaultServerPort, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port of the remote host to connect to for game traffic"); AZ_CVAR(uint16_t, sv_port, DefaultServerPort, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port that this multiplayer gem will bind to for game traffic"); + AZ_CVAR(uint16_t, sv_portRange, 999, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The range of ports the host will incrementally attempt to bind to when initializing"); AZ_CVAR(AZ::CVarFixedString, sv_map, "nolevel", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The map the server should load"); AZ_CVAR(ProtocolType, sv_protocol, ProtocolType::Udp, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "This flag controls whether we use TCP or UDP for game networking"); AZ_CVAR(bool, sv_isDedicated, true, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Whether the host command creates an independent or client hosted server"); @@ -168,6 +169,7 @@ namespace Multiplayer AZ::ConsoleFunctorFlags flags, AZ::ConsoleInvokedFrom invokedFrom ) { OnConsoleCommandInvoked(command, args, flags, invokedFrom); }) + , m_autonomousEntityReplicatorCreatedHandler([this]([[maybe_unused]] NetEntityId netEntityId) { OnAutonomousEntityReplicatorCreated(); }) { AZ::Interface::Register(this); } @@ -205,8 +207,23 @@ namespace Multiplayer bool MultiplayerSystemComponent::StartHosting(uint16_t port, bool isDedicated) { + if (port != sv_port) + { + sv_port = port; + } + InitializeMultiplayer(isDedicated ? MultiplayerAgentType::DedicatedServer : MultiplayerAgentType::ClientServer); - return m_networkInterface->Listen(port); + const uint16_t maxPort = sv_port + sv_portRange; + while (sv_port <= maxPort) + { + if (m_networkInterface->Listen(sv_port)) + { + return true; + } + AZLOG_WARN("Failed to start listening on port %u, port is in use?", static_cast(sv_port)); + sv_port = sv_port + 1; + } + return false; } bool MultiplayerSystemComponent::Connect(const AZStd::string& remoteAddress, uint16_t port) @@ -310,6 +327,11 @@ namespace Multiplayer void MultiplayerSystemComponent::OnTick(float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) { + if (bg_multiplayerDebugDraw) + { + m_networkEntityManager.DebugDraw(); + } + const AZ::TimeMs deltaTimeMs = aznumeric_cast(static_cast(deltaTime * 1000.0f)); const AZ::TimeMs serverRateMs = static_cast(sv_serverSendRateMs); const float serverRateSeconds = static_cast(serverRateMs) / 1000.0f; @@ -394,11 +416,6 @@ namespace Multiplayer { m_networkInterface->GetConnectionSet().VisitConnections(visitor); } - - if (bg_multiplayerDebugDraw) - { - m_networkEntityManager.DebugDraw(); - } } int MultiplayerSystemComponent::GetTickOrder() @@ -469,17 +486,41 @@ namespace Multiplayer auto visitor = [](IConnection& connection) { connection.Disconnect(DisconnectReason::TerminatedByUser, TerminationEndpoint::Local); }; m_networkInterface->GetConnectionSet().VisitConnections(visitor); return true; - } + } } reinterpret_cast(connection->GetUserData())->SetProviderTicket(packet.GetTicket().c_str()); + // Hosts will spawn a new default player prefab for the user that just connected + if (GetAgentType() == MultiplayerAgentType::ClientServer + || GetAgentType() == MultiplayerAgentType::DedicatedServer) + { + // We use a temporary userId so we can maintain client lookups even in the event of wifi handoff + NetworkEntityHandle controlledEntity = SpawnDefaultPlayerPrefab(packet.GetTemporaryUserId()); + if (controlledEntity.Exists()) + { + controlledEntity.GetNetBindComponent()->SetOwningConnectionId(connection->GetConnectionId()); + } + // Activate the entity if necessary + if (controlledEntity.GetEntity()->GetState() == AZ::Entity::State::Init) + { + controlledEntity.Activate(); + } + + ServerToClientConnectionData* connectionData = reinterpret_cast(connection->GetUserData()); + AZStd::unique_ptr window = AZStd::make_unique(controlledEntity, connection); + connectionData->GetReplicationManager().SetReplicationWindow(AZStd::move(window)); + connectionData->SetControlledEntity(controlledEntity); + } + if (connection->SendReliablePacket(MultiplayerPackets::Accept(sv_map))) { m_didHandshake = true; - - // Sync our console - ConsoleReplicator consoleReplicator(connection); - AZ::Interface::Get()->VisitRegisteredFunctors([&consoleReplicator](AZ::ConsoleFunctorBase* functor) { consoleReplicator.Visit(functor); }); + if (packet.GetTemporaryUserId() == 0) + { + // Sync our console + ConsoleReplicator consoleReplicator(connection); + AZ::Interface::Get()->VisitRegisteredFunctors([&consoleReplicator](AZ::ConsoleFunctorBase* functor) { consoleReplicator.Visit(functor); }); + } return true; } return false; @@ -493,10 +534,24 @@ namespace Multiplayer ) { m_didHandshake = true; - AZ::CVarFixedString commandString = "sv_map " + packet.GetMap(); - AZ::Interface::Get()->PerformCommand(commandString.c_str()); - AZ::CVarFixedString loadLevelString = "LoadLevel " + packet.GetMap(); - AZ::Interface::Get()->PerformCommand(loadLevelString.c_str()); + if (m_temporaryUserIdentifier == 0) + { + AZ::CVarFixedString commandString = "sv_map " + packet.GetMap(); + AZ::Interface::Get()->PerformCommand(commandString.c_str()); + AZ::CVarFixedString loadLevelString = "LoadLevel " + packet.GetMap(); + AZ::Interface::Get()->PerformCommand(loadLevelString.c_str()); + } + else + { + OnAutonomousEntityReplicatorCreated(); +// IConnectionData* connectionData = reinterpret_cast(connection->GetUserData()); +// if (connectionData) +// { +// // @nt: TODO - delete once dropped RPC problem fixed +// // Connection has migrated, we are now waiting for the autonomous entity replicator to be created +// connectionData->GetReplicationManager().AddAutonomousEntityReplicatorCreatedHandler(m_autonomousEntityReplicatorCreatedHandler); +// } + } return true; } @@ -617,13 +672,17 @@ namespace Multiplayer // Store the temporary user identifier so we can transmit it with our next Connect packet // The new server will use this to re-attach our set of autonomous entities + m_temporaryUserIdentifier = packet.GetTemporaryUserIdentifier(); // Disconnect our existing server connection auto visitor = [](IConnection& connection) { connection.Disconnect(DisconnectReason::ClientMigrated, TerminationEndpoint::Local); }; m_networkInterface->GetConnectionSet().VisitConnections(visitor); AZLOG_INFO("Migrating to new server shard"); m_clientMigrationStartEvent.Signal(packet.GetLastClientInputId()); - m_networkInterface->Connect(packet.GetRemoteServerAddress()); + if (m_networkInterface->Connect(packet.GetRemoteServerAddress()) == AzNetworking::InvalidConnectionId) + { + AZLOG_ERROR("Failed to connect to new host during client migration event"); + } return true; } @@ -653,7 +712,7 @@ namespace Multiplayer providerTicket = m_pendingConnectionTickets.front(); m_pendingConnectionTickets.pop(); } - connection->SendReliablePacket(MultiplayerPackets::Connect(0, providerTicket.c_str())); + connection->SendReliablePacket(MultiplayerPackets::Connect(0, m_temporaryUserIdentifier, providerTicket.c_str())); } else { @@ -661,20 +720,10 @@ namespace Multiplayer m_connectionAcquiredEvent.Signal(datum); } - // Hosts will spawn a new default player prefab for the user that just connected if (GetAgentType() == MultiplayerAgentType::ClientServer || GetAgentType() == MultiplayerAgentType::DedicatedServer) { - NetworkEntityHandle controlledEntity = SpawnDefaultPlayerPrefab(); - if (controlledEntity.Exists()) - { - controlledEntity.GetNetBindComponent()->SetOwningConnectionId(connection->GetConnectionId()); - } - controlledEntity.Activate(); - - connection->SetUserData(new ServerToClientConnectionData(connection, *this, controlledEntity)); - AZStd::unique_ptr window = AZStd::make_unique(controlledEntity, connection); - reinterpret_cast(connection->GetUserData())->GetReplicationManager().SetReplicationWindow(AZStd::move(window)); + connection->SetUserData(new ServerToClientConnectionData(connection, *this)); } else { @@ -696,9 +745,9 @@ namespace Multiplayer void MultiplayerSystemComponent::OnDisconnect(AzNetworking::IConnection* connection, DisconnectReason reason, TerminationEndpoint endpoint) { - const char* endpointString = (endpoint == TerminationEndpoint::Local) ? "Disconnecting" : "Remote host disconnected"; + const char* endpointString = (endpoint == TerminationEndpoint::Local) ? "Disconnecting" : "Remotely disconnected"; AZStd::string reasonString = ToString(reason); - AZLOG_INFO("%s due to %s from remote address: %s", endpointString, reasonString.c_str(), connection->GetRemoteAddress().GetString().c_str()); + AZLOG_INFO("%s from remote address %s due to %s", endpointString, connection->GetRemoteAddress().GetString().c_str(), reasonString.c_str()); // The client is disconnecting if (GetAgentType() == MultiplayerAgentType::Client) @@ -780,7 +829,7 @@ namespace Multiplayer // Spawn the default player for this host since the host is also a player (not a dedicated server) if (m_agentType == MultiplayerAgentType::ClientServer) { - NetworkEntityHandle controlledEntity = SpawnDefaultPlayerPrefab(); + NetworkEntityHandle controlledEntity = SpawnDefaultPlayerPrefab(0); if (NetBindComponent* controlledEntityNetBindComponent = controlledEntity.GetNetBindComponent()) { controlledEntityNetBindComponent->SetAllowAutonomy(true); @@ -831,9 +880,9 @@ namespace Multiplayer handler.Connect(m_shutdownEvent); } - void MultiplayerSystemComponent::SendNotifyClientMigrationEvent(const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId) + void MultiplayerSystemComponent::SendNotifyClientMigrationEvent(const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId, NetEntityId controlledEntityId) { - m_notifyClientMigrationEvent.Signal(hostId, userIdentifier, lastClientInputId); + m_notifyClientMigrationEvent.Signal(hostId, userIdentifier, lastClientInputId, controlledEntityId); } void MultiplayerSystemComponent::SendNotifyEntityMigrationEvent(const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId) @@ -887,6 +936,11 @@ namespace Multiplayer return m_filterEntityManager; } + void MultiplayerSystemComponent::RegisterPlayerIdentifierForRejoin(uint64_t temporaryUserIdentifier, NetEntityId controlledEntityId) + { + m_playerRejoinData[temporaryUserIdentifier] = controlledEntityId; + } + void MultiplayerSystemComponent::SetShouldSpawnNetworkEntities(bool value) { m_spawnNetboundEntities = value; @@ -1017,6 +1071,13 @@ namespace Multiplayer m_cvarCommands.PushBackItem(AZStd::move(replicateString)); } + void MultiplayerSystemComponent::OnAutonomousEntityReplicatorCreated() + { + m_autonomousEntityReplicatorCreatedHandler.Disconnect(); + //m_networkEntityManager.GetNetworkEntityAuthorityTracker()->ResetTimeoutTime(AZ::TimeMs{ 2000 }); + m_clientMigrationEndEvent.Signal(); + } + void MultiplayerSystemComponent::ExecuteConsoleCommandList(IConnection* connection, const AZStd::fixed_vector& commands) { AZ::IConsole* console = AZ::Interface::Get(); @@ -1028,8 +1089,14 @@ namespace Multiplayer } } - NetworkEntityHandle MultiplayerSystemComponent::SpawnDefaultPlayerPrefab() + NetworkEntityHandle MultiplayerSystemComponent::SpawnDefaultPlayerPrefab(uint64_t temporaryUserIdentifier) { + const auto node = m_playerRejoinData.find(temporaryUserIdentifier); + if (node != m_playerRejoinData.end()) + { + return m_networkEntityManager.GetNetworkEntityTracker()->Get(node->second); + } + PrefabEntityId playerPrefabEntityId(AZ::Name(static_cast(sv_defaultPlayerSpawnAsset).c_str())); INetworkEntityManager::EntityList entityList = m_networkEntityManager.CreateEntitiesImmediate(playerPrefabEntityId, NetEntityRole::Authority, AZ::Transform::CreateIdentity(), Multiplayer::AutoActivate::DoNotActivate); @@ -1045,7 +1112,7 @@ namespace Multiplayer { if (!AZ::Interface::Get()->StartHosting(sv_port, sv_isDedicated)) { - AZLOG_ERROR("Failed to start listening on port %u, port is in use?", static_cast(sv_port)); + AZLOG_ERROR("Failed to start listening on any allocated port"); } } AZ_CONSOLEFREEFUNC(host, AZ::ConsoleFunctorFlags::DontReplicate, "Opens a multiplayer connection as a host for other clients to connect to"); diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h index 262168b536..f07aa0b68f 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h @@ -116,7 +116,7 @@ namespace Multiplayer void AddConnectionAcquiredHandler(ConnectionAcquiredEvent::Handler& handler) override; void AddSessionInitHandler(SessionInitEvent::Handler& handler) override; void AddSessionShutdownHandler(SessionShutdownEvent::Handler& handler) override; - void SendNotifyClientMigrationEvent(const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId) override; + void SendNotifyClientMigrationEvent(const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId, NetEntityId controlledEntityId) override; void SendNotifyEntityMigrationEvent(const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId) override; void SendReadyForEntityUpdates(bool readyForEntityUpdates) override; AZ::TimeMs GetCurrentHostTimeMs() const override; @@ -125,6 +125,7 @@ namespace Multiplayer INetworkEntityManager* GetNetworkEntityManager() override; void SetFilterEntityManager(IFilterEntityManager* entityFilter) override; IFilterEntityManager* GetFilterEntityManager() override; + void RegisterPlayerIdentifierForRejoin(uint64_t temporaryUserIdentifier, NetEntityId controlledEntityId) override; void SetShouldSpawnNetworkEntities(bool value) override; bool GetShouldSpawnNetworkEntities() const override; //! @} @@ -138,8 +139,9 @@ namespace Multiplayer void TickVisibleNetworkEntities(float deltaTime, float serverRateSeconds); void OnConsoleCommandInvoked(AZStd::string_view command, const AZ::ConsoleCommandContainer& args, AZ::ConsoleFunctorFlags flags, AZ::ConsoleInvokedFrom invokedFrom); + void OnAutonomousEntityReplicatorCreated(); void ExecuteConsoleCommandList(AzNetworking::IConnection* connection, const AZStd::fixed_vector& commands); - NetworkEntityHandle SpawnDefaultPlayerPrefab(); + NetworkEntityHandle SpawnDefaultPlayerPrefab(uint64_t temporaryUserIdentifier); AZ_CONSOLEFUNC(MultiplayerSystemComponent, DumpStats, AZ::ConsoleFunctorFlags::Null, "Dumps stats for the current multiplayer session"); @@ -162,12 +164,16 @@ namespace Multiplayer ClientMigrationEndEvent m_clientMigrationEndEvent; NotifyClientMigrationEvent m_notifyClientMigrationEvent; NotifyEntityMigrationEvent m_notifyEntityMigrationEvent; + AZ::Event::Handler m_autonomousEntityReplicatorCreatedHandler; AZStd::queue m_pendingConnectionTickets; + AZStd::unordered_map m_playerRejoinData; AZ::TimeMs m_lastReplicatedHostTimeMs = AZ::TimeMs{ 0 }; HostFrameId m_lastReplicatedHostFrameId = HostFrameId(0); + uint64_t m_temporaryUserIdentifier = 0; // Used in the event of a migration or rejoin + double m_serverSendAccumulator = 0.0; float m_renderBlendFactor = 0.0f; float m_tickFactor = 0.0f; diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp index 91ca1bff84..31232cb3f4 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp @@ -47,8 +47,9 @@ namespace Multiplayer , m_entityExitDomainEventHandler([this](const ConstNetworkEntityHandle& entityHandle) { OnEntityExitDomain(entityHandle); }) , m_notifyEntityMigrationHandler([this](const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId) { OnPostEntityMigration(entityHandle, remoteHostId); }) { - // Set up our remote host identifier, we use the IP address of the host + // Set up our remote host identifier, by default we use the IP address of the remote host m_remoteHostId = connection.GetRemoteAddress(); + m_migrateHostId = m_remoteHostId; // Our max payload size is whatever is passed in, minus room for a udp packetheader m_maxPayloadSize = connection.GetConnectionMtu() - UdpPacketHeaderSerializeSize - ReplicationManagerPacketOverhead; @@ -65,7 +66,21 @@ namespace Multiplayer networkEntityManager->AddEntityExitDomainHandler(m_entityExitDomainEventHandler); } - GetMultiplayer()->AddNotifyEntityMigrationEventHandler(m_notifyEntityMigrationHandler); + if (m_updateMode == Mode::LocalServerToRemoteServer) + { + GetMultiplayer()->AddNotifyEntityMigrationEventHandler(m_notifyEntityMigrationHandler); + } + } + + void EntityReplicationManager::SetMigrateHostId(const HostId& remoteHostId) + { + // Allows overriding the remote HostId + m_migrateHostId = remoteHostId; + } + + const HostId& EntityReplicationManager::GetMigrateHostId() const + { + return m_migrateHostId; } const HostId& EntityReplicationManager::GetRemoteHostId() const @@ -362,15 +377,28 @@ namespace Multiplayer const bool changedRemoteRole = (remoteNetworkRole != entityReplicator->GetRemoteNetworkRole()); // Check if we've changed our bound local role - this can occur when we gain Autonomous or lose Autonomous on a client bool changedLocalRole(false); - if (AZ::Entity* localEnt = entityReplicator->GetEntityHandle().GetEntity()) + NetBindComponent* netBindComponent = entityReplicator->GetEntityHandle().GetNetBindComponent(); + if (netBindComponent != nullptr) { - NetBindComponent* netBindComponent = entityReplicator->GetEntityHandle().GetNetBindComponent(); - AZ_Assert(netBindComponent != nullptr, "No NetBindComponent"); changedLocalRole = (netBindComponent->GetNetEntityRole() != entityReplicator->GetBoundLocalNetworkRole()); } if (changedRemoteRole || changedLocalRole) { + const uint32_t intEntityId = static_cast(netBindComponent->GetNetEntityId()); + if (changedLocalRole) + { + const char* oldRoleString = GetEnumString(entityReplicator->GetRemoteNetworkRole()); + const char* newRoleString = GetEnumString(remoteNetworkRole); + AZLOG(NET_ReplicatorRoles, "Replicator %u changed local role, old role = %s, new role = %s", intEntityId, oldRoleString, newRoleString); + } + if (changedRemoteRole) + { + const char* oldRoleString = GetEnumString(entityReplicator->GetBoundLocalNetworkRole()); + const char* newRoleString = GetEnumString(netBindComponent->GetNetEntityRole()); + AZLOG(NET_ReplicatorRoles, "Replicator %u changed remote role, old role = %s, new role = %s", intEntityId, oldRoleString, newRoleString); + } + // If we changed roles, we need to reset everything if (!entityReplicator->IsMarkedForRemoval()) { @@ -1107,7 +1135,7 @@ namespace Multiplayer if (m_updateMode == EntityReplicationManager::Mode::LocalServerToRemoteServer) { - netBindComponent->NotifyServerMigration(GetRemoteHostId(), GetConnection().GetConnectionId()); + netBindComponent->NotifyServerMigration(GetRemoteHostId(), GetMigrateHostId(), GetConnection().GetConnectionId()); } bool didSucceed = true; diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicator.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicator.cpp index 807bbf9e21..0b97634183 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicator.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicator.cpp @@ -176,7 +176,6 @@ namespace Multiplayer switch (GetBoundLocalNetworkRole()) { case NetEntityRole::Authority: - { if (GetRemoteNetworkRole() == NetEntityRole::Client || GetRemoteNetworkRole() == NetEntityRole::Autonomous) { m_onSendRpcHandler.Connect(netBindComponent->GetSendAuthorityToClientRpcEvent()); @@ -189,10 +188,8 @@ namespace Multiplayer { m_onForwardRpcHandler.Connect(netBindComponent->GetSendAuthorityToClientRpcEvent()); } - } - break; + break; case NetEntityRole::Server: - { if (GetRemoteNetworkRole() == NetEntityRole::Authority) { m_onSendRpcHandler.Connect(netBindComponent->GetSendServerToAuthorityRpcEvent()); @@ -204,23 +201,21 @@ namespace Multiplayer // Listen for these to forward the rpc along to the other Client replicators m_onSendRpcHandler.Connect(netBindComponent->GetSendAuthorityToClientRpcEvent()); } - // NOTE: e_Autonomous is not connected to e_ServerProxy, it is always connected to an e_Authority - AZ_Assert(GetRemoteNetworkRole() != NetEntityRole::Autonomous, "Unexpected autonomous remote role") - } - break; + else if (GetRemoteNetworkRole() == NetEntityRole::Autonomous) + { + // NOTE: Autonomous is not connected to ServerProxy, it is always connected to an Authority + AZ_Assert(false, "Unexpected autonomous remote role") + } + break; case NetEntityRole::Client: - { // Nothing allowed, no Client to Server communication - } - break; + break; case NetEntityRole::Autonomous: - { if (GetRemoteNetworkRole() == NetEntityRole::Authority) { m_onSendRpcHandler.Connect(netBindComponent->GetSendAutonomousToAuthorityRpcEvent()); } - } - break; + break; default: AZ_Assert(false, "Unexpected network role"); } @@ -255,19 +250,6 @@ namespace Multiplayer AZLOG_WARN("Trying to activate an entity that is not in the Init state (%u)", GetEntityHandle().GetNetEntityId()); } - // First we need to make sure the transform component has been updated with the correct value prior to activation - // This is because vanilla az components may only depend on the transform component, not the multiplayer transform component - //if (auto* locationComponent = FindCommonComponent(GetEntityHandle())) - //{ - // AZ::Transform newTransform = locationComponent->GetTransform(); - // auto* transformComponent = entity->FindComponent(); - // if (transformComponent) - // { - // // We can't use EBus here since the TransFormBus does not get connected until the activate call below - // transformComponent->SetWorldTM(newTransform); - // } - //} - // Ugly, but this is the only time we need to call a non-const function on this entity entity->Activate(); m_replicationManager.m_orphanedEntityRpcs.DispatchOrphanedRpcs(*this); @@ -281,8 +263,7 @@ namespace Multiplayer NetBindComponent* netBindComponent = m_netBindComponent; AZ_Assert(netBindComponent, "No Multiplayer::NetBindComponent"); - bool isAuthority = (GetBoundLocalNetworkRole() == NetEntityRole::Authority) - && (GetBoundLocalNetworkRole() == netBindComponent->GetNetEntityRole()); + bool isAuthority = (GetBoundLocalNetworkRole() == NetEntityRole::Authority) && (GetBoundLocalNetworkRole() == netBindComponent->GetNetEntityRole()); bool isClient = GetRemoteNetworkRole() == NetEntityRole::Client; bool isAutonomous = GetBoundLocalNetworkRole() == NetEntityRole::Autonomous; if (isAuthority || isClient || isAutonomous) @@ -428,10 +409,8 @@ namespace Multiplayer if (const NetworkTransformComponent* networkTransform = entity->FindComponent()) { const NetEntityId parentId = networkTransform->GetParentEntityId(); - /* - * For root entities attached to a level, a network parent won't be set. - * In this case, this entity is the root entity of the hierarchy and it will be activated first. - */ + // For root entities attached to a level, a network parent won't be set. + // In this case, this entity is the root entity of the hierarchy and it will be activated first. if (parentId != InvalidNetEntityId) { ConstNetworkEntityHandle parentHandle = GetNetworkEntityManager()->GetEntity(parentId); @@ -552,42 +531,33 @@ namespace Multiplayer switch (entityRpcMessage.GetRpcDeliveryType()) { case RpcDeliveryType::AuthorityToClient: - { if (((GetBoundLocalNetworkRole() == NetEntityRole::Client) || (GetBoundLocalNetworkRole() == NetEntityRole::Autonomous)) && (GetRemoteNetworkRole() == NetEntityRole::Authority)) { // We are a local client, and we are connected to server, aka AuthorityToClient result = RpcValidationResult::HandleRpc; } - if ((GetBoundLocalNetworkRole() == NetEntityRole::Server) - && (GetRemoteNetworkRole() == NetEntityRole::Authority)) + if ((GetBoundLocalNetworkRole() == NetEntityRole::Server) && (GetRemoteNetworkRole() == NetEntityRole::Authority)) { // We are on a server, and we received this message from another server, therefore we should forward this to any connected clients result = RpcValidationResult::ForwardToClient; } - } - break; + break; case RpcDeliveryType::AuthorityToAutonomous: - { - if ((GetBoundLocalNetworkRole() == NetEntityRole::Autonomous) - && (GetRemoteNetworkRole() == NetEntityRole::Authority)) + if ((GetBoundLocalNetworkRole() == NetEntityRole::Autonomous) && (GetRemoteNetworkRole() == NetEntityRole::Authority)) { // We are an autonomous client, and we are connected to server, aka AuthorityToAutonomous result = RpcValidationResult::HandleRpc; } - if ((GetBoundLocalNetworkRole() == NetEntityRole::Authority) - && (GetRemoteNetworkRole() == NetEntityRole::Server)) + if ((GetBoundLocalNetworkRole() == NetEntityRole::Authority) && (GetRemoteNetworkRole() == NetEntityRole::Server)) { // We are on a server, and we received this message from another server, therefore we should forward this to our autonomous player // This can occur if we've recently migrated result = RpcValidationResult::ForwardToAutonomous; } - } - break; + break; case RpcDeliveryType::AutonomousToAuthority: - { - if ((GetBoundLocalNetworkRole() == NetEntityRole::Authority) - && (GetRemoteNetworkRole() == NetEntityRole::Autonomous)) + if ((GetBoundLocalNetworkRole() == NetEntityRole::Authority) && (GetRemoteNetworkRole() == NetEntityRole::Autonomous)) { if (IsMarkedForRemoval()) { @@ -609,12 +579,9 @@ namespace Multiplayer result = RpcValidationResult::HandleRpc; } } - } - break; + break; case RpcDeliveryType::ServerToAuthority: - { - if ((GetBoundLocalNetworkRole() == NetEntityRole::Authority) - && (GetRemoteNetworkRole() == NetEntityRole::Server)) + if ((GetBoundLocalNetworkRole() == NetEntityRole::Authority) && (GetRemoteNetworkRole() == NetEntityRole::Server)) { // if we're marked for removal, then we should forward to whomever now owns this entity if (IsMarkedForRemoval()) @@ -637,9 +604,9 @@ namespace Multiplayer result = RpcValidationResult::HandleRpc; } } + break; } - break; - } + if (result == RpcValidationResult::DropRpcAndDisconnect) { bool isLocalServer = (GetBoundLocalNetworkRole() == NetEntityRole::Authority) || (GetBoundLocalNetworkRole() == NetEntityRole::Server); @@ -653,30 +620,29 @@ namespace Multiplayer { AZLOG_ERROR ( - "Dropping RPC and Connection EntityId=%u LocalRole=%u RemoteRole=%u RpcDeliveryType=%u ComponentId=%u RpcType=%u IsReliable=%s IsMarkedForRemoval=%s", + "Dropping RPC and Connection EntityId=%u LocalRole=%s RemoteRole=%s RpcDeliveryType=%u RpcName=%s IsReliable=%s IsMarkedForRemoval=%s", aznumeric_cast(m_entityHandle.GetNetEntityId()), - aznumeric_cast(GetBoundLocalNetworkRole()), - aznumeric_cast(GetRemoteNetworkRole()), + GetEnumString(GetBoundLocalNetworkRole()), + GetEnumString(GetRemoteNetworkRole()), aznumeric_cast(entityRpcMessage.GetRpcDeliveryType()), - aznumeric_cast(entityRpcMessage.GetComponentId()), - aznumeric_cast(entityRpcMessage.GetRpcIndex()), + GetMultiplayerComponentRegistry()->GetComponentRpcName(entityRpcMessage.GetComponentId(), entityRpcMessage.GetRpcIndex()), entityRpcMessage.GetReliability() == ReliabilityType::Reliable ? "true" : "false", IsMarkedForRemoval() ? "true" : "false" ); } } + if (result == RpcValidationResult::DropRpc) { AZLOG ( NET_Rpc, - "Dropping RPC EntityId=%u LocalRole=%u RemoteRole=%u RpcDeliveryType=%u ComponentId=%u RpcType=%u IsReliable=%s IsMarkedForRemoval=%s", + "Dropping RPC EntityId=%u LocalRole=%s RemoteRole=%s RpcDeliveryType=%u RpcName=%s IsReliable=%s IsMarkedForRemoval=%s", aznumeric_cast(m_entityHandle.GetNetEntityId()), - aznumeric_cast(GetBoundLocalNetworkRole()), - aznumeric_cast(GetRemoteNetworkRole()), + GetEnumString(GetBoundLocalNetworkRole()), + GetEnumString(GetRemoteNetworkRole()), aznumeric_cast(entityRpcMessage.GetRpcDeliveryType()), - aznumeric_cast(entityRpcMessage.GetComponentId()), - aznumeric_cast(entityRpcMessage.GetRpcIndex()), + GetMultiplayerComponentRegistry()->GetComponentRpcName(entityRpcMessage.GetComponentId(), entityRpcMessage.GetRpcIndex()), entityRpcMessage.GetReliability() == ReliabilityType::Reliable ? "true" : "false", IsMarkedForRemoval() ? "true" : "false" ); @@ -695,13 +661,12 @@ namespace Multiplayer { AZLOG_WARN ( - "Dropping RPC since entity deleted EntityId=%u LocalRole=%u RemoteRole=%u RpcDeliveryType=%u ComponentId=%u RpcType=%u IsReliable=%s IsMarkedForRemoval=%s", + "Dropping RPC since entity deleted EntityId=%u LocalRole=%s RemoteRole=%s RpcDeliveryType=%u RpcName=%s IsReliable=%s IsMarkedForRemoval=%s", aznumeric_cast(m_entityHandle.GetNetEntityId()), - aznumeric_cast(GetBoundLocalNetworkRole()), - aznumeric_cast(GetRemoteNetworkRole()), + GetEnumString(GetBoundLocalNetworkRole()), + GetEnumString(GetRemoteNetworkRole()), aznumeric_cast(entityRpcMessage.GetRpcDeliveryType()), - aznumeric_cast(entityRpcMessage.GetComponentId()), - aznumeric_cast(entityRpcMessage.GetRpcIndex()), + GetMultiplayerComponentRegistry()->GetComponentRpcName(entityRpcMessage.GetComponentId(), entityRpcMessage.GetRpcIndex()), entityRpcMessage.GetReliability() == ReliabilityType::Reliable ? "true" : "false", IsMarkedForRemoval() ? "true" : "false" ); @@ -739,23 +704,23 @@ namespace Multiplayer case RpcValidationResult::DropRpcAndDisconnect: return false; case RpcValidationResult::ForwardToClient: - { - ScopedForwardingMessage forwarding(*this); - m_netBindComponent->GetSendAuthorityToClientRpcEvent().Signal(entityRpcMessage); + { + ScopedForwardingMessage forwarding(*this); + m_netBindComponent->GetSendAuthorityToClientRpcEvent().Signal(entityRpcMessage); + } return true; - } case RpcValidationResult::ForwardToAutonomous: - { - ScopedForwardingMessage forwarding(*this); - m_netBindComponent->GetSendAuthorityToAutonomousRpcEvent().Signal(entityRpcMessage); + { + ScopedForwardingMessage forwarding(*this); + m_netBindComponent->GetSendAuthorityToAutonomousRpcEvent().Signal(entityRpcMessage); + } return true; - } case RpcValidationResult::ForwardToAuthority: - { - ScopedForwardingMessage forwarding(*this); - m_netBindComponent->GetSendServerToAuthorityRpcEvent().Signal(entityRpcMessage); + { + ScopedForwardingMessage forwarding(*this); + m_netBindComponent->GetSendServerToAuthorityRpcEvent().Signal(entityRpcMessage); + } return true; - } default: break; } diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp index db7f7243cc..d1d915ca28 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp @@ -225,11 +225,19 @@ namespace Multiplayer { AZ::Entity* entity = it->second; NetBindComponent* netBindComponent = m_networkEntityTracker.GetNetBindComponent(entity); + AZ::Aabb entityBounds = AZ::Interface::Get()->GetEntityWorldBoundsUnion(entity->GetId()); + entityBounds.Expand(AZ::Vector3(0.01f)); if (netBindComponent->GetNetEntityRole() == NetEntityRole::Authority) { - const AZ::Aabb entityBounds = AZ::Interface::Get()->GetEntityWorldBoundsUnion(entity->GetId()); - debugDisplay->DrawWireBox(entityBounds.GetMin(), entityBounds.GetMax()); + debugDisplay->SetColor(AZ::Colors::Black); + debugDisplay->SetAlpha(0.5f); } + else + { + debugDisplay->SetColor(AZ::Colors::DeepSkyBlue); + debugDisplay->SetAlpha(0.25f); + } + debugDisplay->DrawWireBox(entityBounds.GetMin(), entityBounds.GetMax()); } if (m_entityDomain != nullptr) diff --git a/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.cpp b/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.cpp index 9d9cc74d2b..8807931ae1 100644 --- a/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.cpp +++ b/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.cpp @@ -91,17 +91,10 @@ namespace Multiplayer return m_isPoorConnection ? sv_MinEntitiesToReplicate : sv_MaxEntitiesToReplicate; } - bool ServerToClientReplicationWindow::IsInWindow(const ConstNetworkEntityHandle& entityHandle, NetEntityRole& outNetworkRole) const + bool ServerToClientReplicationWindow::IsInWindow([[maybe_unused]] const ConstNetworkEntityHandle& entityHandle, NetEntityRole& outNetworkRole) const { - // TODO: Clean up this interface, this function is used for server->server migrations, and probably shouldn't be exposed in it's current setup AZ_Assert(false, "IsInWindow should not be called on the ServerToClientReplicationWindow"); outNetworkRole = NetEntityRole::InvalidRole; - auto iter = m_replicationSet.find(entityHandle); - if (iter != m_replicationSet.end()) - { - outNetworkRole = iter->second.m_netEntityRole; - return true; - } return false; } @@ -145,7 +138,7 @@ namespace Multiplayer NetworkEntityTracker* networkEntityTracker = GetNetworkEntityTracker(); IFilterEntityManager* filterEntityManager = GetMultiplayer()->GetFilterEntityManager(); - // Add all the neighbors + // Add all the neighbours for (AzFramework::VisibilityEntry* visEntry : gatheredEntries) { AZ::Entity* entity = static_cast(visEntry->m_userData); @@ -300,7 +293,6 @@ namespace Multiplayer void ServerToClientReplicationWindow::AddEntityToReplicationSet(ConstNetworkEntityHandle& entityHandle, float priority, [[maybe_unused]] float distanceSquared) { // Assumption: the entity has been checked for filtering prior to this call. - if (!sv_ReplicateServerProxies) { NetBindComponent* netBindComponent = entityHandle.GetNetBindComponent(); @@ -311,11 +303,11 @@ namespace Multiplayer } } - const bool isQueueFull = (m_candidateQueue.size() >= sv_MaxEntitiesToTrackReplication); // See if have the maximum number of entities in our set + const bool isQueueFull = (m_candidateQueue.size() >= sv_MaxEntitiesToTrackReplication); // See if have the maximum number of entities in our set const bool isInReplicationSet = m_replicationSet.find(entityHandle) != m_replicationSet.end(); if (!isInReplicationSet) { - if (isQueueFull) // if our set is full, then we need to remove the worst priority in our set + if (isQueueFull) // If our set is full, then we need to remove the worst priority in our set { ConstNetworkEntityHandle removeEnt = m_candidateQueue.top().m_entityHandle; m_candidateQueue.pop(); diff --git a/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.h b/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.h index b034bde90c..3c3d7754f6 100644 --- a/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.h +++ b/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.h @@ -75,8 +75,6 @@ namespace Multiplayer AZ::EntityActivatedEvent::Handler m_entityActivatedEventHandler; AZ::EntityDeactivatedEvent::Handler m_entityDeactivatedEventHandler; - //NetBindComponent* m_controlledNetBindComponent = nullptr; - AzNetworking::IConnection* m_connection = nullptr; // Cached values to detect a poor network connection From 4449e83c3b287f691f3f9fe6bd1506536e684a0c Mon Sep 17 00:00:00 2001 From: kberg-amzn Date: Fri, 8 Oct 2021 21:27:31 -0700 Subject: [PATCH 03/26] Further fixes to get client migrations functional, plus a bug fix from the netBindComponent lookup optimization Signed-off-by: kberg-amzn --- .../Multiplayer/Components/NetBindComponent.h | 4 +- .../Code/Include/Multiplayer/IMultiplayer.h | 12 +++++- .../Include/Multiplayer/MultiplayerTypes.h | 2 +- .../Source/Components/NetBindComponent.cpp | 4 +- .../ServerToClientConnectionData.cpp | 16 +++----- .../ServerToClientConnectionData.h | 2 +- .../ServerToClientConnectionData.inl | 1 - .../Source/MultiplayerSystemComponent.cpp | 41 ++++++++++++++----- .../Code/Source/MultiplayerSystemComponent.h | 3 +- .../EntityReplicationManager.cpp | 18 ++++---- .../NetworkEntity/NetworkEntityHandle.cpp | 16 ++------ .../NetworkEntity/NetworkEntityManager.cpp | 14 +++++++ 12 files changed, 82 insertions(+), 51 deletions(-) diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h index b22c8dc51c..4102e4b850 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h @@ -32,7 +32,7 @@ namespace Multiplayer using EntityStopEvent = AZ::Event; using EntityDirtiedEvent = AZ::Event<>; using EntitySyncRewindEvent = AZ::Event<>; - using EntityServerMigrationEvent = AZ::Event; + using EntityServerMigrationEvent = AZ::Event; using EntityPreRenderEvent = AZ::Event; using EntityCorrectionEvent = AZ::Event<>; @@ -113,7 +113,7 @@ namespace Multiplayer void MarkDirty(); void NotifyLocalChanges(); void NotifySyncRewindState(); - void NotifyServerMigration(const HostId& remoteHostId, const HostId& migrateHostId, AzNetworking::ConnectionId connectionId); + void NotifyServerMigration(const HostId& remoteHostId); void NotifyPreRender(float deltaTime); void NotifyCorrection(); diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h b/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h index b4be69ed4c..8023d21242 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h @@ -45,7 +45,7 @@ namespace Multiplayer using ClientMigrationStartEvent = AZ::Event; using ClientMigrationEndEvent = AZ::Event<>; using ClientDisconnectedEvent = AZ::Event<>; - using NotifyClientMigrationEvent = AZ::Event; + using NotifyClientMigrationEvent = AZ::Event; using NotifyEntityMigrationEvent = AZ::Event; using ConnectionAcquiredEvent = AZ::Event; using SessionInitEvent = AZ::Event; @@ -131,11 +131,12 @@ namespace Multiplayer virtual void AddSessionShutdownHandler(SessionShutdownEvent::Handler& handler) = 0; //! Signals a NotifyClientMigrationEvent with the provided parameters. + //! @param connectionId the connection id of the client that is migrating //! @param hostId the host id of the host the client is migrating to //! @param userIdentifier the user identifier the client will provide the new host to validate identity //! @param lastClientInputId the last processed clientInputId by the current host //! @param controlledEntityId the entityId of the clients autonomous entity - virtual void SendNotifyClientMigrationEvent(const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId, NetEntityId controlledEntityId) = 0; + virtual void SendNotifyClientMigrationEvent(AzNetworking::ConnectionId connectionId, const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId, NetEntityId controlledEntityId) = 0; //! Signals a NotifyEntityMigrationEvent with the provided parameters. //! @param entityHandle the network entity handle of the entity being migrated @@ -182,6 +183,13 @@ namespace Multiplayer //! @param controlledEntityId the controlled entityId of the players autonomous entity virtual void RegisterPlayerIdentifierForRejoin(uint64_t temporaryUserIdentifier, NetEntityId controlledEntityId) = 0; + //! Completes a client migration event by informing the appropriate client to migrate between hosts. + //! @param temporaryUserIdentifier the temporary user identifier used to identify a player across hosts + //! @param connectionId the connection id of the player being migrated + //! @param publicHostId the public address of the new host the client should connect to + //! @param migratedClientInputId the last clientInputId processed prior to migration + virtual void CompleteClientMigration(uint64_t temporaryUserIdentifier, AzNetworking::ConnectionId connectionId, const HostId& publicHostId, ClientInputId migratedClientInputId) = 0; + //! Enables or disables automatic instantiation of netbound entities. //! This setting is controlled by the networking layer and should not be touched //! If enabled, netbound entities will instantiate as spawnables are loaded into the game world, generally true for the server diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerTypes.h b/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerTypes.h index 8284dd7010..58a0ae63fa 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerTypes.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerTypes.h @@ -29,7 +29,7 @@ namespace Multiplayer using HostId = AzNetworking::IpAddress; static const HostId InvalidHostId = HostId(); - AZ_TYPE_SAFE_INTEGRAL(NetEntityId, uint32_t); + AZ_TYPE_SAFE_INTEGRAL(NetEntityId, uint64_t); static constexpr NetEntityId InvalidNetEntityId = static_cast(-1); AZ_TYPE_SAFE_INTEGRAL(NetComponentId, uint16_t); diff --git a/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp index 2e451793ed..cc71000d33 100644 --- a/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp @@ -394,9 +394,9 @@ namespace Multiplayer m_syncRewindEvent.Signal(); } - void NetBindComponent::NotifyServerMigration(const HostId& remoteHostId, const HostId& migrateHostId, AzNetworking::ConnectionId connectionId) + void NetBindComponent::NotifyServerMigration(const HostId& remoteHostId) { - m_entityServerMigrationEvent.Signal(m_netEntityHandle, remoteHostId, migrateHostId, connectionId); + m_entityServerMigrationEvent.Signal(m_netEntityHandle, remoteHostId); } void NetBindComponent::NotifyPreRender(float deltaTime) diff --git a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.cpp b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.cpp index 29be64ac07..56eed58af7 100644 --- a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.cpp +++ b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.cpp @@ -27,9 +27,9 @@ namespace Multiplayer ) : m_connection(connection) , m_controlledEntityRemovedHandler([this](const ConstNetworkEntityHandle&) { OnControlledEntityRemove(); }) - , m_controlledEntityMigrationHandler([this](const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId, const HostId& migrateHostId, AzNetworking::ConnectionId connectionId) + , m_controlledEntityMigrationHandler([this](const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId) { - OnControlledEntityMigration(entityHandle, remoteHostId, migrateHostId, connectionId); + OnControlledEntityMigration(entityHandle, remoteHostId); }) , m_entityReplicationManager(*connection, connectionListener, EntityReplicationManager::Mode::LocalServerToRemoteClient) { @@ -102,9 +102,7 @@ namespace Multiplayer void ServerToClientConnectionData::OnControlledEntityMigration ( [[maybe_unused]] const ConstNetworkEntityHandle& entityHandle, - const HostId& remoteHostId, - const HostId& migrateHostId, - [[maybe_unused]] AzNetworking::ConnectionId connectionId + const HostId& remoteHostId ) { ClientInputId migratedClientInputId = ClientInputId{ 0 }; @@ -121,11 +119,9 @@ namespace Multiplayer const uint64_t temporaryUserIdentifier = AzNetworking::CryptoRand64(); // Tell the new host that a client is about to (re)join - GetMultiplayer()->SendNotifyClientMigrationEvent(remoteHostId, temporaryUserIdentifier, migratedClientInputId, m_controlledEntity.GetNetEntityId()); - - // Tell the client who to join - MultiplayerPackets::ClientMigration clientMigration(migrateHostId, temporaryUserIdentifier, migratedClientInputId); - GetConnection()->SendReliablePacket(clientMigration); + GetMultiplayer()->SendNotifyClientMigrationEvent(GetConnection()->GetConnectionId(), remoteHostId, temporaryUserIdentifier, migratedClientInputId, m_controlledEntity.GetNetEntityId()); + // We need to send a MultiplayerPackets::ClientMigration packet to complete this process + // This happens inside MultiplayerSystemComponent, once we're certain the remote host has appropriately prepared m_controlledEntity = NetworkEntityHandle(); m_canSendUpdates = false; diff --git a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.h b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.h index a894d5e55e..7349ee4244 100644 --- a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.h +++ b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.h @@ -43,7 +43,7 @@ namespace Multiplayer private: void OnControlledEntityRemove(); - void OnControlledEntityMigration(const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId, const HostId& migrateHostId, AzNetworking::ConnectionId connectionId); + void OnControlledEntityMigration(const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId); void OnGameplayStarted(); EntityReplicationManager m_entityReplicationManager; diff --git a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.inl b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.inl index 53ba51f36a..956fc4ca1a 100644 --- a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.inl +++ b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.inl @@ -18,7 +18,6 @@ namespace Multiplayer m_canSendUpdates = canSendUpdates; } - inline NetworkEntityHandle ServerToClientConnectionData::GetPrimaryPlayerEntity() { return m_controlledEntity; diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index da1ab1cb73..cdd61bd740 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -212,12 +212,12 @@ namespace Multiplayer sv_port = port; } - InitializeMultiplayer(isDedicated ? MultiplayerAgentType::DedicatedServer : MultiplayerAgentType::ClientServer); const uint16_t maxPort = sv_port + sv_portRange; while (sv_port <= maxPort) { if (m_networkInterface->Listen(sv_port)) { + InitializeMultiplayer(isDedicated ? MultiplayerAgentType::DedicatedServer : MultiplayerAgentType::ClientServer); return true; } AZLOG_WARN("Failed to start listening on port %u, port is in use?", static_cast(sv_port)); @@ -510,6 +510,12 @@ namespace Multiplayer AZStd::unique_ptr window = AZStd::make_unique(controlledEntity, connection); connectionData->GetReplicationManager().SetReplicationWindow(AZStd::move(window)); connectionData->SetControlledEntity(controlledEntity); + + // If this is a migrate or rejoin, immediately ready the connection for updates + if (packet.GetTemporaryUserId() != 0) + { + connectionData->SetCanSendUpdates(true); + } } if (connection->SendReliablePacket(MultiplayerPackets::Accept(sv_map))) @@ -543,14 +549,16 @@ namespace Multiplayer } else { - OnAutonomousEntityReplicatorCreated(); -// IConnectionData* connectionData = reinterpret_cast(connection->GetUserData()); -// if (connectionData) -// { -// // @nt: TODO - delete once dropped RPC problem fixed -// // Connection has migrated, we are now waiting for the autonomous entity replicator to be created -// connectionData->GetReplicationManager().AddAutonomousEntityReplicatorCreatedHandler(m_autonomousEntityReplicatorCreatedHandler); -// } + // Bypass map loading and immediately ready the connection for updates + IConnectionData* connectionData = reinterpret_cast(connection->GetUserData()); + if (connectionData) + { + connectionData->SetCanSendUpdates(true); + + // @nt: TODO - delete once dropped RPC problem fixed + // Connection has migrated, we are now waiting for the autonomous entity replicator to be created + connectionData->GetReplicationManager().AddAutonomousEntityReplicatorCreatedHandler(m_autonomousEntityReplicatorCreatedHandler); + } } return true; } @@ -880,9 +888,9 @@ namespace Multiplayer handler.Connect(m_shutdownEvent); } - void MultiplayerSystemComponent::SendNotifyClientMigrationEvent(const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId, NetEntityId controlledEntityId) + void MultiplayerSystemComponent::SendNotifyClientMigrationEvent(AzNetworking::ConnectionId connectionId, const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId, NetEntityId controlledEntityId) { - m_notifyClientMigrationEvent.Signal(hostId, userIdentifier, lastClientInputId, controlledEntityId); + m_notifyClientMigrationEvent.Signal(connectionId, hostId, userIdentifier, lastClientInputId, controlledEntityId); } void MultiplayerSystemComponent::SendNotifyEntityMigrationEvent(const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId) @@ -941,6 +949,17 @@ namespace Multiplayer m_playerRejoinData[temporaryUserIdentifier] = controlledEntityId; } + void MultiplayerSystemComponent::CompleteClientMigration(uint64_t temporaryUserIdentifier, AzNetworking::ConnectionId connectionId, const HostId& publicHostId, ClientInputId migratedClientInputId) + { + IConnection* connection = m_networkInterface->GetConnectionSet().GetConnection(connectionId); + if (connection != nullptr) // Make sure the player has not disconnected since the start of migration + { + // Tell the client who to join + MultiplayerPackets::ClientMigration clientMigration(publicHostId, temporaryUserIdentifier, migratedClientInputId); + connection->SendReliablePacket(clientMigration); + } + } + void MultiplayerSystemComponent::SetShouldSpawnNetworkEntities(bool value) { m_spawnNetboundEntities = value; diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h index f07aa0b68f..3af6babb18 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h @@ -116,7 +116,7 @@ namespace Multiplayer void AddConnectionAcquiredHandler(ConnectionAcquiredEvent::Handler& handler) override; void AddSessionInitHandler(SessionInitEvent::Handler& handler) override; void AddSessionShutdownHandler(SessionShutdownEvent::Handler& handler) override; - void SendNotifyClientMigrationEvent(const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId, NetEntityId controlledEntityId) override; + void SendNotifyClientMigrationEvent(AzNetworking::ConnectionId connectionId, const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId, NetEntityId controlledEntityId) override; void SendNotifyEntityMigrationEvent(const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId) override; void SendReadyForEntityUpdates(bool readyForEntityUpdates) override; AZ::TimeMs GetCurrentHostTimeMs() const override; @@ -126,6 +126,7 @@ namespace Multiplayer void SetFilterEntityManager(IFilterEntityManager* entityFilter) override; IFilterEntityManager* GetFilterEntityManager() override; void RegisterPlayerIdentifierForRejoin(uint64_t temporaryUserIdentifier, NetEntityId controlledEntityId) override; + void CompleteClientMigration(uint64_t temporaryUserIdentifier, AzNetworking::ConnectionId connectionId, const HostId& publicHostId, ClientInputId migratedClientInputId) override; void SetShouldSpawnNetworkEntities(bool value) override; bool GetShouldSpawnNetworkEntities() const override; //! @} diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp index 31232cb3f4..9042a3cf6b 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp @@ -386,17 +386,18 @@ namespace Multiplayer if (changedRemoteRole || changedLocalRole) { const uint32_t intEntityId = static_cast(netBindComponent->GetNetEntityId()); + const char* entityName = entityReplicator->GetEntityHandle().GetEntity()->GetName().c_str(); if (changedLocalRole) { const char* oldRoleString = GetEnumString(entityReplicator->GetRemoteNetworkRole()); const char* newRoleString = GetEnumString(remoteNetworkRole); - AZLOG(NET_ReplicatorRoles, "Replicator %u changed local role, old role = %s, new role = %s", intEntityId, oldRoleString, newRoleString); + AZLOG(NET_ReplicatorRoles, "Replicator %s(%u) changed local role, old role = %s, new role = %s", entityName, intEntityId, oldRoleString, newRoleString); } if (changedRemoteRole) { const char* oldRoleString = GetEnumString(entityReplicator->GetBoundLocalNetworkRole()); const char* newRoleString = GetEnumString(netBindComponent->GetNetEntityRole()); - AZLOG(NET_ReplicatorRoles, "Replicator %u changed remote role, old role = %s, new role = %s", intEntityId, oldRoleString, newRoleString); + AZLOG(NET_ReplicatorRoles, "Replicator %s(%u) changed remote role, old role = %s, new role = %s", entityName, intEntityId, oldRoleString, newRoleString); } // If we changed roles, we need to reset everything @@ -605,9 +606,9 @@ namespace Multiplayer NetBindComponent* netBindComponent = replicatorEntity.GetNetBindComponent(); AZ_Assert(netBindComponent != nullptr, "No NetBindComponent"); - if (createEntity) + if (netBindComponent->GetOwningConnectionId() != invokingConnection->GetConnectionId()) { - // Always set our invoking connectionId for any newly created entities, since this connection now 'owns' them from a rewind perspective + // Always ensure our owning connectionId is correct for correct rewind behaviour netBindComponent->SetOwningConnectionId(invokingConnection->GetConnectionId()); } @@ -617,10 +618,11 @@ namespace Multiplayer AZ_Assert(localNetworkRole != NetEntityRole::Authority, "UpdateMessage trying to set local role to Authority, this should only happen via migration"); AZLOG_INFO ( - "EntityReplicationManager: Changing network role on entity %u, old role %u new role %u", + "EntityReplicationManager: Changing network role on entity %s(%u), old role %s new role %s", + replicatorEntity.GetEntity()->GetName().c_str(), aznumeric_cast(netEntityId), - aznumeric_cast(netBindComponent->GetNetEntityRole()), - aznumeric_cast(localNetworkRole) + GetEnumString(netBindComponent->GetNetEntityRole()), + GetEnumString(localNetworkRole) ); if (NetworkRoleHasController(localNetworkRole)) @@ -1135,7 +1137,7 @@ namespace Multiplayer if (m_updateMode == EntityReplicationManager::Mode::LocalServerToRemoteServer) { - netBindComponent->NotifyServerMigration(GetRemoteHostId(), GetMigrateHostId(), GetConnection().GetConnectionId()); + netBindComponent->NotifyServerMigration(GetRemoteHostId()); } bool didSucceed = true; diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityHandle.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityHandle.cpp index ef338840f9..457395a61e 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityHandle.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityHandle.cpp @@ -18,22 +18,14 @@ namespace Multiplayer { ConstNetworkEntityHandle::ConstNetworkEntityHandle(AZ::Entity* entity, const NetworkEntityTracker* networkEntityTracker) : m_entity(entity) - , m_networkEntityTracker(networkEntityTracker) + , m_networkEntityTracker((networkEntityTracker != nullptr) ? networkEntityTracker : GetNetworkEntityTracker()) { - if (m_networkEntityTracker == nullptr) - { - m_networkEntityTracker = GetNetworkEntityTracker(); - } - - if (m_networkEntityTracker) - { - m_changeDirty = m_networkEntityTracker->GetChangeDirty(m_entity); - } + AZ_Assert(m_networkEntityTracker, "NetworkEntityTracker is not valid"); + m_changeDirty = m_networkEntityTracker->GetChangeDirty(m_entity); if (entity) { - AZ_Assert(networkEntityTracker, "NetworkEntityTracker is not valid"); - m_netBindComponent = networkEntityTracker->GetNetBindComponent(entity); + m_netBindComponent = m_networkEntityTracker->GetNetBindComponent(entity); if (m_netBindComponent != nullptr) { m_netEntityId = m_netBindComponent->GetNetEntityId(); diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp index d1d915ca28..5d76b4ed56 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp @@ -46,6 +46,20 @@ namespace Multiplayer void NetworkEntityManager::Initialize(const HostId& hostId, AZStd::unique_ptr entityDomain) { m_hostId = hostId; + + // Configure our vended NetEntityIds so that no two hosts generate the same NetEntityId + { + // Needs more thought + const uint64_t addrPortion = hostId.GetAddress(AzNetworking::ByteOrder::Host); + const uint64_t portPortion = hostId.GetPort(AzNetworking::ByteOrder::Host); + const uint64_t hostIdentifier = (portPortion << 32) | addrPortion; + const AZ::HashValue32 hostHash = AZ::TypeHash32(hostIdentifier); + + NetEntityId hostEntityIdOffset = static_cast(hostHash) << 32; + m_nextEntityId &= NetEntityId{ 0x0000000000000000FFFFFFFFFFFFFFFF }; + m_nextEntityId |= hostEntityIdOffset; + } + m_entityDomain = AZStd::move(entityDomain); m_updateEntityDomainEvent.Enqueue(net_EntityDomainUpdateMs, true); m_entityDomain->ActivateTracking(m_ownedEntities); From 05e8b2941abef490aadb4178b20c0c8f9294ca9e Mon Sep 17 00:00:00 2001 From: kberg-amzn Date: Fri, 8 Oct 2021 21:36:54 -0700 Subject: [PATCH 04/26] Removing unused member, simplify API Signed-off-by: kberg-amzn --- .../EntityReplication/EntityReplicationManager.h | 5 ----- .../EntityReplication/EntityReplicationManager.cpp | 12 ------------ 2 files changed, 17 deletions(-) diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/EntityReplication/EntityReplicationManager.h b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/EntityReplication/EntityReplicationManager.h index 83f9e238f5..10346ad777 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/EntityReplication/EntityReplicationManager.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/EntityReplication/EntityReplicationManager.h @@ -57,10 +57,6 @@ namespace Multiplayer EntityReplicationManager(AzNetworking::IConnection& connection, AzNetworking::IConnectionListener& connectionListener, Mode mode); ~EntityReplicationManager() = default; - //! Used to override during client migration if your host has a specially assigned publically routable address. - //! @param remoteHostId the publically routable address to use in place of the remote HostId - void SetMigrateHostId(const HostId& remoteHostId); - const HostId& GetMigrateHostId() const; const HostId& GetRemoteHostId() const; void ActivatePendingEntities(); @@ -211,7 +207,6 @@ namespace Multiplayer AZ::TimeMs m_entityPendingRemovalMs = AZ::TimeMs{ 0 }; AZ::TimeMs m_frameTimeMs = AZ::TimeMs{ 0 }; HostId m_remoteHostId = InvalidHostId; - HostId m_migrateHostId = InvalidHostId; uint32_t m_maxRemoteEntitiesPendingCreationCount = AZStd::numeric_limits::max(); uint32_t m_maxPayloadSize = 0; Mode m_updateMode = Mode::Invalid; diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp index 9042a3cf6b..e0023815cc 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp @@ -49,7 +49,6 @@ namespace Multiplayer { // Set up our remote host identifier, by default we use the IP address of the remote host m_remoteHostId = connection.GetRemoteAddress(); - m_migrateHostId = m_remoteHostId; // Our max payload size is whatever is passed in, minus room for a udp packetheader m_maxPayloadSize = connection.GetConnectionMtu() - UdpPacketHeaderSerializeSize - ReplicationManagerPacketOverhead; @@ -72,17 +71,6 @@ namespace Multiplayer } } - void EntityReplicationManager::SetMigrateHostId(const HostId& remoteHostId) - { - // Allows overriding the remote HostId - m_migrateHostId = remoteHostId; - } - - const HostId& EntityReplicationManager::GetMigrateHostId() const - { - return m_migrateHostId; - } - const HostId& EntityReplicationManager::GetRemoteHostId() const { return m_remoteHostId; From a6e7a81b7941e6251e30e2b6ccc15a87518e5b92 Mon Sep 17 00:00:00 2001 From: kberg-amzn Date: Thu, 14 Oct 2021 18:22:41 -0700 Subject: [PATCH 05/26] More fixes to migration, plus some quality of life fixes to TimeoutQueue to allow lambda based handlers and a quick fix to OctreeSystemComponent to more gracefully handle enumerate calls that lie outside the vis system bounds Signed-off-by: kberg-amzn --- .../Visibility/OctreeSystemComponent.cpp | 62 +++++-------------- .../DataStructures/TimeoutQueue.cpp | 10 ++- .../DataStructures/TimeoutQueue.h | 6 ++ .../LocalPredictionPlayerInputComponent.h | 8 +++ .../NetworkInput/NetworkInputArray.h | 0 .../NetworkInput/NetworkInputChild.h | 0 .../NetworkInput/NetworkInputHistory.h | 0 .../NetworkInputMigrationVector.h | 0 ...tionPlayerInputComponent.AutoComponent.xml | 6 +- .../LocalPredictionPlayerInputComponent.cpp | 10 +++ .../Debug/MultiplayerDebugSystemComponent.cpp | 10 ++- .../Source/NetworkInput/NetworkInputArray.cpp | 2 +- .../Source/NetworkInput/NetworkInputChild.cpp | 2 +- .../NetworkInput/NetworkInputHistory.cpp | 2 +- .../NetworkInputMigrationVector.cpp | 2 +- Gems/Multiplayer/Code/multiplayer_files.cmake | 8 +-- 16 files changed, 66 insertions(+), 62 deletions(-) rename Gems/Multiplayer/Code/{Source => Include/Multiplayer}/NetworkInput/NetworkInputArray.h (100%) rename Gems/Multiplayer/Code/{Source => Include/Multiplayer}/NetworkInput/NetworkInputChild.h (100%) rename Gems/Multiplayer/Code/{Source => Include/Multiplayer}/NetworkInput/NetworkInputHistory.h (100%) rename Gems/Multiplayer/Code/{Source => Include/Multiplayer}/NetworkInput/NetworkInputMigrationVector.h (100%) diff --git a/Code/Framework/AzFramework/AzFramework/Visibility/OctreeSystemComponent.cpp b/Code/Framework/AzFramework/AzFramework/Visibility/OctreeSystemComponent.cpp index b4cad8511f..cd52393df9 100644 --- a/Code/Framework/AzFramework/AzFramework/Visibility/OctreeSystemComponent.cpp +++ b/Code/Framework/AzFramework/AzFramework/Visibility/OctreeSystemComponent.cpp @@ -14,9 +14,8 @@ namespace AzFramework { AZ_CVAR(bool, bg_octreeUseQuadtree, false, nullptr, AZ::ConsoleFunctorFlags::ReadOnly, "If set to true, the visibility octrees will degenerate to a quadtree split along the X/Y plane"); AZ_CVAR(float, bg_octreeMaxWorldExtents, 16384.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "Maximum supported world size by the world octreeSystemComponent"); - AZ_CVAR(uint32_t, bg_octreeNodeMaxEntries, 64, nullptr, AZ::ConsoleFunctorFlags::Null, "Maximum number of entries to allow in any node before forcing a split"); - AZ_CVAR(uint32_t, bg_octreeNodeMinEntries, 32, nullptr, AZ::ConsoleFunctorFlags::Null, "Minimum number of entries to allow in a node resulting from a merge operation"); - + AZ_CVAR(uint32_t, bg_octreeNodeMaxEntries, 64, nullptr, AZ::ConsoleFunctorFlags::Null, "Maximum number of entries to allow in any node before forcing a split"); + AZ_CVAR(uint32_t, bg_octreeNodeMinEntries, 32, nullptr, AZ::ConsoleFunctorFlags::Null, "Minimum number of entries to allow in a node resulting from a merge operation"); static uint32_t GetChildNodeCount() { @@ -25,14 +24,12 @@ namespace AzFramework return (bg_octreeUseQuadtree) ? QuadtreeNodeChildCount : OctreeNodeChildCount; } - OctreeNode::OctreeNode(const AZ::Aabb& bounds) : m_bounds(bounds) { ; } - OctreeNode::OctreeNode(OctreeNode&& rhs) : m_bounds(rhs.m_bounds) , m_parent(rhs.m_parent) @@ -46,7 +43,6 @@ namespace AzFramework } } - OctreeNode& OctreeNode::operator=(OctreeNode&& rhs) { m_bounds = rhs.m_bounds; @@ -63,7 +59,6 @@ namespace AzFramework return *this; } - void OctreeNode::Insert(OctreeScene& octreeScene, VisibilityEntry* entry) { AZ_Assert(entry->m_internalNode == nullptr, "Double-insertion: Insert invoked for an entry already bound to the OctreeScene"); @@ -98,7 +93,6 @@ namespace AzFramework } } - void OctreeNode::Update(OctreeScene& octreeScene, VisibilityEntry* entry) { AZ_Assert(entry->m_internalNode == this, "Update invoked for an entry bound to a different OctreeNode"); @@ -129,7 +123,6 @@ namespace AzFramework } } - void OctreeNode::Remove(OctreeScene& octreeScene, VisibilityEntry* entry) { AZ_Assert(entry->m_internalNode == this, "Remove invoked for an entry bound to a different OctreeNode"); @@ -152,25 +145,30 @@ namespace AzFramework } } - void OctreeNode::Enumerate(const AZ::Aabb& aabb, const IVisibilityScene::EnumerateCallback& callback) const { - EnumerateHelper(aabb, callback); + if (AZ::ShapeIntersection::Overlaps(aabb, m_bounds)) + { + EnumerateHelper(aabb, callback); + } } - void OctreeNode::Enumerate(const AZ::Sphere& sphere, const IVisibilityScene::EnumerateCallback& callback) const { - EnumerateHelper(sphere, callback); + if (AZ::ShapeIntersection::Overlaps(sphere, m_bounds)) + { + EnumerateHelper(sphere, callback); + } } - void OctreeNode::Enumerate(const AZ::Frustum& frustum, const IVisibilityScene::EnumerateCallback& callback) const { - EnumerateHelper(frustum, callback); + if (AZ::ShapeIntersection::Overlaps(frustum, m_bounds)) + { + EnumerateHelper(frustum, callback); + } } - void OctreeNode::EnumerateNoCull(const IVisibilityScene::EnumerateCallback& callback) const { // Invoke the callback for the current node @@ -190,25 +188,21 @@ namespace AzFramework } } - const AZStd::vector& OctreeNode::GetEntries() const { return m_entries; } - OctreeNode* OctreeNode::GetChildren() const { return m_children; } - bool OctreeNode::IsLeaf() const { return m_children == nullptr; } - void OctreeNode::TryMerge(OctreeScene& octreeScene) { if (IsLeaf()) @@ -236,7 +230,6 @@ namespace AzFramework } } - template void OctreeNode::EnumerateHelper(const T& boundingVolume, const IVisibilityScene::EnumerateCallback& callback) const { @@ -262,7 +255,6 @@ namespace AzFramework } } - void OctreeNode::Split(OctreeScene& octreeScene) { AZ_Assert(m_children == nullptr, "Split invoked on an octreeScene node that has already been split"); @@ -312,7 +304,6 @@ namespace AzFramework } } - void OctreeNode::Merge(OctreeScene& octreeScene) { AZ_Assert(m_children != nullptr, "Merge invoked on an octreeScene node that does not have children"); @@ -371,7 +362,6 @@ namespace AzFramework } } - void OctreeScene::RemoveEntry(VisibilityEntry& entry) { AZStd::lock_guard lock(m_sharedMutex); @@ -382,35 +372,30 @@ namespace AzFramework } } - void OctreeScene::Enumerate(const AZ::Aabb& aabb, const IVisibilityScene::EnumerateCallback& callback) const { AZStd::shared_lock lock(m_sharedMutex); m_root.Enumerate(aabb, callback); } - void OctreeScene::Enumerate(const AZ::Sphere& sphere, const IVisibilityScene::EnumerateCallback& callback) const { AZStd::shared_lock lock(m_sharedMutex); m_root.Enumerate(sphere, callback); } - void OctreeScene::Enumerate(const AZ::Frustum& frustum, const IVisibilityScene::EnumerateCallback& callback) const { AZStd::shared_lock lock(m_sharedMutex); m_root.Enumerate(frustum, callback); } - void OctreeScene::EnumerateNoCull(const IVisibilityScene::EnumerateCallback& callback) const { AZStd::shared_lock lock(m_sharedMutex); m_root.EnumerateNoCull(callback); } - uint32_t OctreeScene::GetEntryCount() const { return m_entryCount; @@ -421,26 +406,22 @@ namespace AzFramework return m_nodeCount; } - uint32_t OctreeScene::GetFreeNodeCount() const { // Each entry represents GetChildNodeCount() nodes return aznumeric_cast(m_freeOctreeNodes.size() * GetChildNodeCount()); } - uint32_t OctreeScene::GetPageCount() const { return aznumeric_cast(m_nodeCache.size()); } - uint32_t OctreeScene::GetChildNodeCount() const { return AzFramework::GetChildNodeCount(); } - void OctreeScene::DumpStats() { AZ_TracePrintf("Console", "OctreeScene[\"%s\"]::EntryCount = %u", GetName().GetCStr(), GetEntryCount()); @@ -450,21 +431,18 @@ namespace AzFramework AZ_TracePrintf("Console", "OctreeScene[\"%s\"]::ChildNodeCount = %u", GetName().GetCStr(), GetChildNodeCount()); } - static inline uint32_t CreateNodeIndex(uint32_t page, uint32_t offset) { AZ_Assert(page <= 0xFFFF && offset <= 0xFFFF, "Out of range values passed to CreateNodeIndex"); return (page << 16) | offset; } - static inline void ExtractPageAndOffsetFromIndex(uint32_t index, uint32_t& page, uint32_t& offset) { offset = index & 0x0000FFFF; page = index >> 16; } - uint32_t OctreeScene::AllocateChildNodes() { const uint32_t childCount = GetChildNodeCount(); @@ -508,14 +486,12 @@ namespace AzFramework return CreateNodeIndex(nextChildPage, nextChildOffset); } - void OctreeScene::ReleaseChildNodes(uint32_t nodeIndex) { m_nodeCount -= GetChildNodeCount(); m_freeOctreeNodes.push(nodeIndex); } - OctreeNode* OctreeScene::GetChildNodesAtIndex(uint32_t nodeIndex) const { uint32_t childPage; @@ -524,7 +500,6 @@ namespace AzFramework return &(*m_nodeCache[childPage])[childOffset]; } - void OctreeSystemComponent::Reflect(AZ::ReflectContext* context) { if (auto* serializeContext = azrtti_cast(context)) @@ -534,19 +509,16 @@ namespace AzFramework } } - void OctreeSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) { provided.push_back(AZ_CRC("OctreeService")); } - void OctreeSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) { incompatible.push_back(AZ_CRC("OctreeService")); } - OctreeSystemComponent::OctreeSystemComponent() { AZ::Interface::Register(this); @@ -555,7 +527,6 @@ namespace AzFramework m_defaultScene = aznew OctreeScene(AZ::Name("DefaultVisibilityScene")); } - OctreeSystemComponent::~OctreeSystemComponent() { AZ_Assert(m_scenes.empty(), "All IVisibilityScenes must be destroyed before shutdown"); @@ -566,13 +537,11 @@ namespace AzFramework AZ::Interface::Unregister(this); } - void OctreeSystemComponent::Activate() { ; } - void OctreeSystemComponent::Deactivate() { ; @@ -591,7 +560,6 @@ namespace AzFramework return newScene; } - void OctreeSystemComponent::DestroyVisibilityScene(IVisibilityScene* visScene) { for (auto iter = m_scenes.begin(); iter != m_scenes.end(); ++iter) @@ -606,7 +574,6 @@ namespace AzFramework AZ_Assert(false, "visScene[\"%s\"] not found in the OctreeSystemComponent", visScene->GetName().GetCStr()); } - IVisibilityScene* OctreeSystemComponent::FindVisibilityScene(const AZ::Name& sceneName) { for (OctreeScene* scene : m_scenes) @@ -619,7 +586,6 @@ namespace AzFramework return nullptr; } - void OctreeSystemComponent::DumpStats([[maybe_unused]] const AZ::ConsoleCommandContainer& arguments) { for (OctreeScene* scene : m_scenes) diff --git a/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.cpp b/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.cpp index 88392c98c0..b0f316cf50 100644 --- a/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.cpp +++ b/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.cpp @@ -53,7 +53,7 @@ namespace AzNetworking m_timeoutItemMap.erase(timeoutId); } - void TimeoutQueue::UpdateTimeouts(ITimeoutHandler& timeoutHandler, int32_t maxTimeouts) + void TimeoutQueue::UpdateTimeouts(const TimeoutHandler& timeoutHandler, int32_t maxTimeouts) { int32_t numTimeouts = 0; if (maxTimeouts < 0) @@ -103,7 +103,7 @@ namespace AzNetworking // By this point, the item is definitely timed out // Invoke the timeout function to see how to proceed - const TimeoutResult result = timeoutHandler.HandleTimeout(mapItem); + const TimeoutResult result = timeoutHandler(mapItem); if (result == TimeoutResult::Refresh) { @@ -122,4 +122,10 @@ namespace AzNetworking m_timeoutItemMap.erase(itemTimeoutId); } } + + void TimeoutQueue::UpdateTimeouts(ITimeoutHandler& timeoutHandler, int32_t maxTimeouts) + { + TimeoutHandler handler([&timeoutHandler](TimeoutQueue::TimeoutItem& item) { return timeoutHandler.HandleTimeout(item); }); + UpdateTimeouts(handler, maxTimeouts); + } } diff --git a/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.h b/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.h index 239af5be0e..63417ea36f 100644 --- a/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.h +++ b/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.h @@ -64,6 +64,12 @@ namespace AzNetworking //! @param timeoutId the identifier of the item to remove void RemoveItem(TimeoutId timeoutId); + //! Updates timeouts for all items, invokes the provided timeout functor if required. + //! @param timeoutHandler lambda to invoke for all timeouts + //! @param maxTimeouts the maximum number of timeouts to process before breaking iteration + using TimeoutHandler = AZStd::function; + void UpdateTimeouts(const TimeoutHandler& timeoutHandler, int32_t maxTimeouts = -1); + //! Updates timeouts for all items, invokes timeout handlers if required. //! @param timeoutHandler listener instance to call back on for timeouts //! @param maxTimeouts the maximum number of timeouts to process before breaking iteration diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/LocalPredictionPlayerInputComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/LocalPredictionPlayerInputComponent.h index bcbea6542f..b300a2bbf7 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/Components/LocalPredictionPlayerInputComponent.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/LocalPredictionPlayerInputComponent.h @@ -57,6 +57,14 @@ namespace Multiplayer const AzNetworking::PacketEncodingBuffer& correction ) override; + //! Forcibly enables ProcessInput to execute on the entity. + //! Note that this function is quite dangerous and should normally never be used + void ForceEnableAutonomousUpdate(); + + //! Forcibly disables ProcessInput from executing on the entity. + //! Note that this function is quite dangerous and should normally never be used + void ForceDisableAutonomousUpdate(); + //! Return true if we're currently migrating from one host to another. //! @return boolean true if we're currently migrating from one host to another bool IsMigrating() const; diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputArray.h b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputArray.h similarity index 100% rename from Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputArray.h rename to Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputArray.h diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.h b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputChild.h similarity index 100% rename from Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.h rename to Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputChild.h diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputHistory.h b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputHistory.h similarity index 100% rename from Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputHistory.h rename to Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputHistory.h diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputMigrationVector.h b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputMigrationVector.h similarity index 100% rename from Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputMigrationVector.h rename to Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputMigrationVector.h diff --git a/Gems/Multiplayer/Code/Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml b/Gems/Multiplayer/Code/Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml index 1a7496a77d..55c7d37d49 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml @@ -12,9 +12,9 @@ - - - + + + diff --git a/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp b/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp index 747f53d37b..5006213e8e 100644 --- a/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp @@ -353,6 +353,16 @@ namespace Multiplayer } } + void LocalPredictionPlayerInputComponentController::ForceEnableAutonomousUpdate() + { + m_autonomousUpdateEvent.Enqueue(AZ::TimeMs{ 1 }, true); + } + + void LocalPredictionPlayerInputComponentController::ForceDisableAutonomousUpdate() + { + m_autonomousUpdateEvent.RemoveFromQueue(); + } + bool LocalPredictionPlayerInputComponentController::IsMigrating() const { return m_lastMigratedInputId != ClientInputId{ 0 }; diff --git a/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugSystemComponent.cpp b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugSystemComponent.cpp index 422b2f0cae..048b3ce6d2 100644 --- a/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugSystemComponent.cpp @@ -28,24 +28,29 @@ namespace Multiplayer ->Version(1); } } + void MultiplayerDebugSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) { provided.push_back(AZ_CRC_CE("MultiplayerDebugSystemComponent")); } + void MultiplayerDebugSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required) { ; } + void MultiplayerDebugSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatbile) { incompatbile.push_back(AZ_CRC_CE("MultiplayerDebugSystemComponent")); } + void MultiplayerDebugSystemComponent::Activate() { #ifdef IMGUI_ENABLED ImGui::ImGuiUpdateListenerBus::Handler::BusConnect(); #endif } + void MultiplayerDebugSystemComponent::Deactivate() { #ifdef IMGUI_ENABLED @@ -74,6 +79,7 @@ namespace Multiplayer ImGui::EndMenu(); } } + void AccumulatePerSecondValues(const MultiplayerStats& stats, const MultiplayerStats::Metric& metric, float& outCallsPerSecond, float& outBytesPerSecond) { uint64_t summedCalls = 0; @@ -106,6 +112,7 @@ namespace Multiplayer ImGui::Text("%11.2f", bytesPerSecond); return open; } + bool DrawSummaryRow(const char* name, const MultiplayerStats& stats) { const MultiplayerStats::Metric propertyUpdatesSent = stats.CalculateTotalPropertyUpdateSentMetrics(); @@ -122,6 +129,7 @@ namespace Multiplayer AccumulatePerSecondValues(stats, rpcsRecv, callsPerSecond, bytesPerSecond); return DrawMetricsRow(name, true, totalCalls, totalBytes, callsPerSecond, bytesPerSecond); } + bool DrawComponentRow(const char* name, const MultiplayerStats& stats, NetComponentId netComponentId) { const MultiplayerStats::Metric propertyUpdatesSent = stats.CalculateComponentPropertyUpdateSentMetrics(netComponentId); @@ -138,6 +146,7 @@ namespace Multiplayer AccumulatePerSecondValues(stats, rpcsRecv, callsPerSecond, bytesPerSecond); return DrawMetricsRow(name, true, totalCalls, totalBytes, callsPerSecond, bytesPerSecond); } + void DrawComponentDetails(const MultiplayerStats& stats, NetComponentId netComponentId) { MultiplayerComponentRegistry* componentRegistry = GetMultiplayerComponentRegistry(); @@ -479,4 +488,3 @@ void OnDebugEntities_ShowBandwidth_Changed(const bool& showBandwidth) AZ::Interface::Get()->HideEntityBandwidthDebugOverlay(); } } - diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputArray.cpp b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputArray.cpp index 638dc9a900..9d566537c5 100644 --- a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputArray.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputArray.cpp @@ -6,7 +6,7 @@ * */ -#include +#include #include #include #include diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.cpp b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.cpp index bea110f298..59fb62e1b5 100644 --- a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.cpp @@ -6,7 +6,7 @@ * */ -#include +#include #include #include diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputHistory.cpp b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputHistory.cpp index 54813327ab..00fd9f13b8 100644 --- a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputHistory.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputHistory.cpp @@ -6,7 +6,7 @@ * */ -#include +#include namespace Multiplayer { diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputMigrationVector.cpp b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputMigrationVector.cpp index d95c46261f..ec57891725 100644 --- a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputMigrationVector.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputMigrationVector.cpp @@ -6,7 +6,7 @@ * */ -#include +#include #include #include diff --git a/Gems/Multiplayer/Code/multiplayer_files.cmake b/Gems/Multiplayer/Code/multiplayer_files.cmake index f16483e663..a799278203 100644 --- a/Gems/Multiplayer/Code/multiplayer_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_files.cmake @@ -45,6 +45,10 @@ set(FILES Include/Multiplayer/NetworkEntity/NetworkEntityUpdateMessage.h Include/Multiplayer/NetworkInput/IMultiplayerComponentInput.h Include/Multiplayer/NetworkInput/NetworkInput.h + Include/Multiplayer/NetworkInput/NetworkInputArray.h + Include/Multiplayer/NetworkInput/NetworkInputChild.h + Include/Multiplayer/NetworkInput/NetworkInputHistory.h + Include/Multiplayer/NetworkInput/NetworkInputMigrationVector.h Include/Multiplayer/NetworkTime/INetworkTime.h Include/Multiplayer/NetworkTime/RewindableArray.h Include/Multiplayer/NetworkTime/RewindableArray.inl @@ -114,13 +118,9 @@ set(FILES Source/NetworkEntity/NetworkSpawnableLibrary.h Source/NetworkInput/NetworkInput.cpp Source/NetworkInput/NetworkInputArray.cpp - Source/NetworkInput/NetworkInputArray.h Source/NetworkInput/NetworkInputChild.cpp - Source/NetworkInput/NetworkInputChild.h Source/NetworkInput/NetworkInputHistory.cpp - Source/NetworkInput/NetworkInputHistory.h Source/NetworkInput/NetworkInputMigrationVector.cpp - Source/NetworkInput/NetworkInputMigrationVector.h Source/NetworkTime/NetworkTime.cpp Source/NetworkTime/NetworkTime.h Source/Pipeline/NetworkSpawnableHolderComponent.cpp From 3a0805254bce12051c7c3317e13841d1accb2514 Mon Sep 17 00:00:00 2001 From: kberg-amzn Date: Fri, 15 Oct 2021 13:42:46 -0700 Subject: [PATCH 06/26] Fixes for 64-bit printfs Signed-off-by: kberg-amzn --- .../EntityReplicationManager.cpp | 50 +++++++++---------- .../EntityReplication/EntityReplicator.cpp | 26 +++++----- .../NetworkEntityAuthorityTracker.cpp | 22 ++++---- 3 files changed, 49 insertions(+), 49 deletions(-) diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp index e0023815cc..03d5f52ff2 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp @@ -259,8 +259,8 @@ namespace Multiplayer { AZLOG_WARN ( - "Serializing extremely large entity (%u) - MaxPayload: %d NeededSize %d", - aznumeric_cast(replicator->GetEntityHandle().GetNetEntityId()), + "Serializing extremely large entity (%llu) - MaxPayload: %d NeededSize %d", + aznumeric_cast(replicator->GetEntityHandle().GetNetEntityId()), m_maxPayloadSize, nextMessageSize ); @@ -373,19 +373,19 @@ namespace Multiplayer if (changedRemoteRole || changedLocalRole) { - const uint32_t intEntityId = static_cast(netBindComponent->GetNetEntityId()); + const AZ::u64 intEntityId = static_cast(netBindComponent->GetNetEntityId()); const char* entityName = entityReplicator->GetEntityHandle().GetEntity()->GetName().c_str(); if (changedLocalRole) { const char* oldRoleString = GetEnumString(entityReplicator->GetRemoteNetworkRole()); const char* newRoleString = GetEnumString(remoteNetworkRole); - AZLOG(NET_ReplicatorRoles, "Replicator %s(%u) changed local role, old role = %s, new role = %s", entityName, intEntityId, oldRoleString, newRoleString); + AZLOG(NET_ReplicatorRoles, "Replicator %s(%llu) changed local role, old role = %s, new role = %s", entityName, intEntityId, oldRoleString, newRoleString); } if (changedRemoteRole) { const char* oldRoleString = GetEnumString(entityReplicator->GetBoundLocalNetworkRole()); const char* newRoleString = GetEnumString(netBindComponent->GetNetEntityRole()); - AZLOG(NET_ReplicatorRoles, "Replicator %s(%u) changed remote role, old role = %s, new role = %s", entityName, intEntityId, oldRoleString, newRoleString); + AZLOG(NET_ReplicatorRoles, "Replicator %s(%llu) changed remote role, old role = %s, new role = %s", entityName, intEntityId, oldRoleString, newRoleString); } // If we changed roles, we need to reset everything @@ -402,8 +402,8 @@ namespace Multiplayer AZLOG ( NET_RepDeletes, - "Reinited replicator for %u from remote host %s role %d", - entityHandle.GetNetEntityId(), + "Reinited replicator for netEntityId %llu from remote host %s role %d", + static_cast(entityHandle.GetNetEntityId()), GetRemoteHostId().GetString().c_str(), aznumeric_cast(remoteNetworkRole) ); @@ -419,8 +419,8 @@ namespace Multiplayer AZLOG ( NET_RepDeletes, - "Added replicator for %u from remote host %s role %d", - entityHandle.GetNetEntityId(), + "Added replicator for netEntityId %llu from remote host %s role %d", + static_cast(entityHandle.GetNetEntityId()), GetRemoteHostId().GetString().c_str(), aznumeric_cast(remoteNetworkRole) ); @@ -428,7 +428,7 @@ namespace Multiplayer } else { - AZLOG_ERROR("Failed to add entity replicator, entity does not exist, entity id %u", entityHandle.GetNetEntityId()); + AZLOG_ERROR("Failed to add entity replicator, entity does not exist, netEntityId %llu", static_cast(entityHandle.GetNetEntityId())); AZ_Assert(false, "Failed to add entity replicator, entity does not exist"); } return entityReplicator; @@ -517,18 +517,18 @@ namespace Multiplayer { if (entityReplicator->IsMarkedForRemoval()) { - AZLOG(NET_RepDeletes, "Got a replicator delete message that is a duplicate id %u remote host %s", updateMessage.GetEntityId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Got a replicator delete message that is a duplicate id %llu remote host %s", static_cast(updateMessage.GetEntityId()), GetRemoteHostId().GetString().c_str()); } else if (entityReplicator->OwnsReplicatorLifetime()) { // This can occur if we migrate entities quickly - if this is a replicator from C to A, A migrates to B, B then migrates to C, and A's delete replicator has not arrived at C - AZLOG(NET_RepDeletes, "Got a replicator delete message for a replicator we own id %u remote host %s", updateMessage.GetEntityId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Got a replicator delete message for a replicator we own id %llu remote host %s", static_cast(updateMessage.GetEntityId()), GetRemoteHostId().GetString().c_str()); } else { shouldDeleteEntity = true; entityReplicator->MarkForRemoval(); - AZLOG(NET_RepDeletes, "Deleting replicater for entity id %u remote host %s", updateMessage.GetEntityId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Deleting replicater for entity id %llu remote host %s", static_cast(updateMessage.GetEntityId()), GetRemoteHostId().GetString().c_str()); } } @@ -540,17 +540,17 @@ namespace Multiplayer { if (updateMessage.GetWasMigrated()) { - AZLOG(NET_RepDeletes, "Leaving id %u using timeout remote host %s", entity.GetNetEntityId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Leaving id %llu using timeout remote host %s", static_cast(entity.GetNetEntityId()), GetRemoteHostId().GetString().c_str()); } else { - AZLOG(NET_RepDeletes, "Deleting entity id %u remote host %s", entity.GetNetEntityId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Deleting entity id %llu remote host %s", static_cast(entity.GetNetEntityId()), GetRemoteHostId().GetString().c_str()); GetNetworkEntityManager()->MarkForRemoval(entity); } } else { - AZLOG(NET_RepDeletes, "Trying to delete entity id %u remote host %s, but it has been removed", entity.GetNetEntityId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Trying to delete entity id %llu remote host %s, but it has been removed", static_cast(entity.GetNetEntityId()), GetRemoteHostId().GetString().c_str()); } } @@ -606,9 +606,9 @@ namespace Multiplayer AZ_Assert(localNetworkRole != NetEntityRole::Authority, "UpdateMessage trying to set local role to Authority, this should only happen via migration"); AZLOG_INFO ( - "EntityReplicationManager: Changing network role on entity %s(%u), old role %s new role %s", + "EntityReplicationManager: Changing network role on entity %s(%llu), old role %s new role %s", replicatorEntity.GetEntity()->GetName().c_str(), - aznumeric_cast(netEntityId), + aznumeric_cast(netEntityId), GetEnumString(netBindComponent->GetNetEntityRole()), GetEnumString(localNetworkRole) ); @@ -720,9 +720,9 @@ namespace Multiplayer AZLOG_WARN ( "Dropping Packet and LocalServerToRemoteClient connection, unexpected packet " - "LocalShard=%s EntityId=%u RemoteNetworkRole=%u BoundLocalNetworkRole=%u ActualNetworkRole=%u IsMarkedForRemoval=%s", + "LocalShard=%s EntityId=%llu RemoteNetworkRole=%u BoundLocalNetworkRole=%u ActualNetworkRole=%u IsMarkedForRemoval=%s", GetNetworkEntityManager()->GetHostId().GetString().c_str(), - aznumeric_cast(entityReplicator->GetEntityHandle().GetNetEntityId()), + aznumeric_cast(entityReplicator->GetEntityHandle().GetNetEntityId()), aznumeric_cast(entityReplicator->GetRemoteNetworkRole()), aznumeric_cast(entityReplicator->GetBoundLocalNetworkRole()), aznumeric_cast(entityReplicator->GetNetBindComponent()->GetNetEntityRole()), @@ -772,13 +772,13 @@ namespace Multiplayer result = UpdateValidationResult::DropMessage; if (updateMessage.GetIsDelete()) { - AZLOG(NET_RepDeletes, "EntityReplicationManager: Received old DeleteProxy message for entity id %u, sequence %d latest sequence %d from remote host %s", - updateMessage.GetEntityId(), (uint32_t)packetId, (uint32_t)propSubscriber->GetLastReceivedPacketId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "EntityReplicationManager: Received old DeleteProxy message for entity id %llu, sequence %d latest sequence %d from remote host %s", + (AZ::u64)updateMessage.GetEntityId(), (uint32_t)packetId, (uint32_t)propSubscriber->GetLastReceivedPacketId(), GetRemoteHostId().GetString().c_str()); } else { - AZLOG(NET_RepUpdate, "EntityReplicationManager: Received old PropertyChangeMessage message for entity id %u, sequence %d latest sequence %d from remote host %s", - updateMessage.GetEntityId(), (uint32_t)packetId, (uint32_t)propSubscriber->GetLastReceivedPacketId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepUpdate, "EntityReplicationManager: Received old PropertyChangeMessage message for entity id %llu, sequence %d latest sequence %d from remote host %s", + (AZ::u64)updateMessage.GetEntityId(), (uint32_t)packetId, (uint32_t)propSubscriber->GetLastReceivedPacketId(), GetRemoteHostId().GetString().c_str()); } } } @@ -1213,7 +1213,7 @@ namespace Multiplayer // Change the role on the replicator AddEntityReplicator(entityHandle, NetEntityRole::Server); - AZLOG(NET_RepDeletes, "Handle Migration %u new authority from remote host %s", entityHandle.GetNetEntityId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Handle Migration %llu new authority from remote host %s", static_cast(entityHandle.GetNetEntityId()), GetRemoteHostId().GetString().c_str()); return true; } diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicator.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicator.cpp index 0b97634183..67527bf962 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicator.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicator.cpp @@ -103,8 +103,8 @@ namespace Multiplayer AZ_Assert ( m_boundLocalNetworkRole != m_remoteNetworkRole, - "Invalid configuration detected, bound local role must differ from remote network role Role: %d", - aznumeric_cast(m_boundLocalNetworkRole) + "Invalid configuration detected, bound local role must differ from remote network role: %s", + GetEnumString(m_boundLocalNetworkRole) ); if (RemoteManagerOwnsEntityLifetime()) @@ -247,7 +247,7 @@ namespace Multiplayer if (entity->GetState() != AZ::Entity::State::Init) { - AZLOG_WARN("Trying to activate an entity that is not in the Init state (%u)", GetEntityHandle().GetNetEntityId()); + AZLOG_WARN("Trying to activate an entity that is not in the Init state (%llu)", static_cast(GetEntityHandle().GetNetEntityId())); } entity->Activate(); @@ -430,9 +430,9 @@ namespace Multiplayer AZLOG ( NET_HierarchyActivationInfo, - "Hierchical entity %s asking for activation - waiting on the parent %u", + "Hierchical entity %s asking for activation - waiting on the parent %llu", entity->GetName().c_str(), - aznumeric_cast(parentId) + aznumeric_cast(parentId) ); return false; } @@ -450,8 +450,8 @@ namespace Multiplayer AZLOG ( NET_RepDeletes, - "Sending delete replicator id %u migrated %d to remote host %s", - aznumeric_cast(GetEntityHandle().GetNetEntityId()), + "Sending delete replicator id %llu migrated %d to remote host %s", + aznumeric_cast(GetEntityHandle().GetNetEntityId()), WasMigrated() ? 1 : 0, m_replicationManager.GetRemoteHostId().GetString().c_str() ); @@ -620,8 +620,8 @@ namespace Multiplayer { AZLOG_ERROR ( - "Dropping RPC and Connection EntityId=%u LocalRole=%s RemoteRole=%s RpcDeliveryType=%u RpcName=%s IsReliable=%s IsMarkedForRemoval=%s", - aznumeric_cast(m_entityHandle.GetNetEntityId()), + "Dropping RPC and Connection EntityId=%llu LocalRole=%s RemoteRole=%s RpcDeliveryType=%u RpcName=%s IsReliable=%s IsMarkedForRemoval=%s", + aznumeric_cast(m_entityHandle.GetNetEntityId()), GetEnumString(GetBoundLocalNetworkRole()), GetEnumString(GetRemoteNetworkRole()), aznumeric_cast(entityRpcMessage.GetRpcDeliveryType()), @@ -637,8 +637,8 @@ namespace Multiplayer AZLOG ( NET_Rpc, - "Dropping RPC EntityId=%u LocalRole=%s RemoteRole=%s RpcDeliveryType=%u RpcName=%s IsReliable=%s IsMarkedForRemoval=%s", - aznumeric_cast(m_entityHandle.GetNetEntityId()), + "Dropping RPC EntityId=%llu LocalRole=%s RemoteRole=%s RpcDeliveryType=%u RpcName=%s IsReliable=%s IsMarkedForRemoval=%s", + aznumeric_cast(m_entityHandle.GetNetEntityId()), GetEnumString(GetBoundLocalNetworkRole()), GetEnumString(GetRemoteNetworkRole()), aznumeric_cast(entityRpcMessage.GetRpcDeliveryType()), @@ -661,8 +661,8 @@ namespace Multiplayer { AZLOG_WARN ( - "Dropping RPC since entity deleted EntityId=%u LocalRole=%s RemoteRole=%s RpcDeliveryType=%u RpcName=%s IsReliable=%s IsMarkedForRemoval=%s", - aznumeric_cast(m_entityHandle.GetNetEntityId()), + "Dropping RPC since entity deleted EntityId=%llu LocalRole=%s RemoteRole=%s RpcDeliveryType=%u RpcName=%s IsReliable=%s IsMarkedForRemoval=%s", + aznumeric_cast(m_entityHandle.GetNetEntityId()), GetEnumString(GetBoundLocalNetworkRole()), GetEnumString(GetRemoteNetworkRole()), aznumeric_cast(entityRpcMessage.GetRpcDeliveryType()), diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityAuthorityTracker.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityAuthorityTracker.cpp index 0a99f4b0d4..b3f87ea9ab 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityAuthorityTracker.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityAuthorityTracker.cpp @@ -33,8 +33,8 @@ namespace Multiplayer AZLOG ( NET_AuthTracker, - "AuthTracker: Removing timeout for networkEntityId %u from %s, new owner is %s", - aznumeric_cast(entityHandle.GetNetEntityId()), + "AuthTracker: Removing timeout for networkEntityId %llu from %s, new owner is %s", + aznumeric_cast(entityHandle.GetNetEntityId()), timeoutData->second.m_previousOwner.GetString().c_str(), newOwner.GetString().c_str() ); @@ -48,8 +48,8 @@ namespace Multiplayer AZLOG ( NET_AuthTracker, - "AuthTracker: Assigning networkEntityId %u from %s to %s", - aznumeric_cast(entityHandle.GetNetEntityId()), + "AuthTracker: Assigning networkEntityId %llu from %s to %s", + aznumeric_cast(entityHandle.GetNetEntityId()), iter->second.back().GetString().c_str(), newOwner.GetString().c_str() ); @@ -59,8 +59,8 @@ namespace Multiplayer AZLOG ( NET_AuthTracker, - "AuthTracker: Assigning networkEntityId %u to %s", - aznumeric_cast(entityHandle.GetNetEntityId()), + "AuthTracker: Assigning networkEntityId %llu to %s", + aznumeric_cast(entityHandle.GetNetEntityId()), newOwner.GetString().c_str() ); } @@ -87,7 +87,7 @@ namespace Multiplayer } } - AZLOG(NET_AuthTracker, "AuthTracker: Removing networkEntityId %u from %s", aznumeric_cast(entityHandle.GetNetEntityId()), previousOwner.GetString().c_str()); + AZLOG(NET_AuthTracker, "AuthTracker: Removing networkEntityId %llu from %s", aznumeric_cast(entityHandle.GetNetEntityId()), previousOwner.GetString().c_str()); if (auto localEnt = entityHandle.GetEntity()) { if (authorityStack.empty()) @@ -114,14 +114,14 @@ namespace Multiplayer } else { - AZLOG(NET_AuthTracker, "AuthTracker: Skipping timeout for Autonomous networkEntityId %u", aznumeric_cast(entityHandle.GetNetEntityId())); + AZLOG(NET_AuthTracker, "AuthTracker: Skipping timeout for Autonomous networkEntityId %llu", aznumeric_cast(entityHandle.GetNetEntityId())); } } } } else { - AZLOG(NET_AuthTracker, "AuthTracker: Remove authority called on networkEntityId that was never added %u", aznumeric_cast(entityHandle.GetNetEntityId())); + AZLOG(NET_AuthTracker, "AuthTracker: Remove authority called on networkEntityId that was never added %llu", aznumeric_cast(entityHandle.GetNetEntityId())); AZ_Assert(false, "AuthTracker: Remove authority called on entity that was never added"); } } @@ -205,8 +205,8 @@ namespace Multiplayer { AZLOG_ERROR ( - "Timed out entity id %u during migration previous owner %s, removing it", - aznumeric_cast(entityHandle.GetNetEntityId()), + "Timed out entity id %llu during migration previous owner %s, removing it", + aznumeric_cast(entityHandle.GetNetEntityId()), timeoutData->second.m_previousOwner.GetString().c_str() ); m_networkEntityManager.MarkForRemoval(entityHandle); From e2a2ecff44041e24491b3838d504fd1902ad04aa Mon Sep 17 00:00:00 2001 From: kberg-amzn Date: Fri, 15 Oct 2021 14:56:06 -0700 Subject: [PATCH 07/26] missed a printf Signed-off-by: kberg-amzn --- .../EntityReplication/EntityReplicationManager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp index 03d5f52ff2..63a80e54fe 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp @@ -865,10 +865,10 @@ namespace Multiplayer { AZLOG_INFO ( - "EntityReplicationManager: Dropping remote RPC message for component %s of rpc index %s, entityId %u has already been deleted", + "EntityReplicationManager: Dropping remote RPC message for component %s of rpc index %s, entityId %llu has already been deleted", GetMultiplayerComponentRegistry()->GetComponentName(message.GetComponentId()), GetMultiplayerComponentRegistry()->GetComponentRpcName(message.GetComponentId(), message.GetRpcIndex()), - message.GetEntityId() + static_cast(message.GetEntityId()) ); return false; } From 47a00e9801dec1b3b8622280fcdb90a78a84329b Mon Sep 17 00:00:00 2001 From: kberg-amzn Date: Fri, 15 Oct 2021 16:53:58 -0700 Subject: [PATCH 08/26] Missed one more printf Signed-off-by: kberg-amzn --- .../EntityReplication/EntityReplicationManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp index 63a80e54fe..fa099842c5 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp @@ -1157,7 +1157,7 @@ namespace Multiplayer AZ_Assert(didSucceed, "Failed to migrate entity from server"); m_sendMigrateEntityEvent.Signal(m_connection, message); - AZLOG(NET_RepDeletes, "Migration packet sent %u to remote host %s", netEntityId, GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Migration packet sent %llu to remote host %s", static_cast(netEntityId), GetRemoteHostId().GetString().c_str()); // Notify all other EntityReplicationManagers that this entity has migrated so they can adjust their own replicators given our new proxy status GetMultiplayer()->SendNotifyEntityMigrationEvent(entityHandle, GetRemoteHostId()); From e6290436fcd954b10a89656b8f12d1f1ac830ef2 Mon Sep 17 00:00:00 2001 From: kberg-amzn Date: Fri, 15 Oct 2021 18:01:40 -0700 Subject: [PATCH 09/26] Updates mocks for interface changes Signed-off-by: kberg-amzn --- .../Code/Source/Debug/MultiplayerDebugHierarchyReporter.cpp | 2 +- Gems/Multiplayer/Code/Tests/CommonBenchmarkSetup.h | 5 ++++- Gems/Multiplayer/Code/Tests/MockInterfaces.h | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugHierarchyReporter.cpp b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugHierarchyReporter.cpp index 7c38a340eb..ad28307204 100644 --- a/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugHierarchyReporter.cpp +++ b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugHierarchyReporter.cpp @@ -74,7 +74,7 @@ namespace Multiplayer { ImGui::Text("%s", entity->GetId().ToString().c_str()); ImGui::NextColumn(); - ImGui::Text("%u", GetMultiplayer()->GetNetworkEntityManager()->GetNetEntityIdById(entity->GetId())); + ImGui::Text("%llu", static_cast(GetMultiplayer()->GetNetworkEntityManager()->GetNetEntityIdById(entity->GetId()))); ImGui::NextColumn(); ImGui::Text("%s", entity->GetName().c_str()); ImGui::NextColumn(); diff --git a/Gems/Multiplayer/Code/Tests/CommonBenchmarkSetup.h b/Gems/Multiplayer/Code/Tests/CommonBenchmarkSetup.h index 5a528ed497..803e6cabca 100644 --- a/Gems/Multiplayer/Code/Tests/CommonBenchmarkSetup.h +++ b/Gems/Multiplayer/Code/Tests/CommonBenchmarkSetup.h @@ -342,8 +342,11 @@ namespace Multiplayer void AddClientMigrationEndEventHandler([[maybe_unused]] ClientMigrationEndEvent::Handler& handler) override {} void AddNotifyClientMigrationHandler([[maybe_unused]] NotifyClientMigrationEvent::Handler& handler) override {} void AddNotifyEntityMigrationEventHandler([[maybe_unused]] NotifyEntityMigrationEvent::Handler& handler) override {} - void SendNotifyClientMigrationEvent([[maybe_unused]] const HostId& hostId, [[maybe_unused]] uint64_t userIdentifier, [[maybe_unused]] ClientInputId lastClientInputId) override {} + void SendNotifyClientMigrationEvent([[maybe_unused]] AzNetworking::ConnectionId connectionId, [[maybe_unused]] const HostId& hostId, + [[maybe_unused]] uint64_t userIdentifier, [[maybe_unused]] ClientInputId lastClientInputId, [[maybe_unused]] NetEntityId netEntityId) override {} void SendNotifyEntityMigrationEvent([[maybe_unused]] const ConstNetworkEntityHandle& entityHandle, [[maybe_unused]] const HostId& remoteHostId) override {} + void RegisterPlayerIdentifierForRejoin(uint64_t, NetEntityId) override {} + void CompleteClientMigration(uint64_t, AzNetworking::ConnectionId, const HostId&, ClientInputId) override {} void SetShouldSpawnNetworkEntities([[maybe_unused]] bool value) override {} bool GetShouldSpawnNetworkEntities() const override { return true; } diff --git a/Gems/Multiplayer/Code/Tests/MockInterfaces.h b/Gems/Multiplayer/Code/Tests/MockInterfaces.h index 527aeb51bc..8cebf280b9 100644 --- a/Gems/Multiplayer/Code/Tests/MockInterfaces.h +++ b/Gems/Multiplayer/Code/Tests/MockInterfaces.h @@ -33,7 +33,7 @@ namespace UnitTest MOCK_METHOD1(AddServerAcceptanceReceivedHandler, void(Multiplayer::ServerAcceptanceReceivedEvent::Handler&)); MOCK_METHOD1(AddSessionInitHandler, void(Multiplayer::SessionInitEvent::Handler&)); MOCK_METHOD1(AddSessionShutdownHandler, void(Multiplayer::SessionShutdownEvent::Handler&)); - MOCK_METHOD3(SendNotifyClientMigrationEvent, void(const Multiplayer::HostId&, uint64_t, Multiplayer::ClientInputId)); + MOCK_METHOD5(SendNotifyClientMigrationEvent, void(AzNetworking::ConnectionId, const Multiplayer::HostId&, uint64_t, Multiplayer::ClientInputId, Multiplayer::NetEntityId)); MOCK_METHOD2(SendNotifyEntityMigrationEvent, void(const Multiplayer::ConstNetworkEntityHandle&, const Multiplayer::HostId&)); MOCK_METHOD1(SendReadyForEntityUpdates, void(bool)); MOCK_CONST_METHOD0(GetCurrentHostTimeMs, AZ::TimeMs()); @@ -42,6 +42,8 @@ namespace UnitTest MOCK_METHOD0(GetNetworkEntityManager, Multiplayer::INetworkEntityManager* ()); MOCK_METHOD1(SetFilterEntityManager, void(Multiplayer::IFilterEntityManager*)); MOCK_METHOD0(GetFilterEntityManager, Multiplayer::IFilterEntityManager* ()); + MOCK_METHOD2(RegisterPlayerIdentifierForRejoin, void(uint64_t, Multiplayer::NetEntityId)); + MOCK_METHOD4(CompleteClientMigration, void(uint64_t, AzNetworking::ConnectionId, const Multiplayer::HostId&, Multiplayer::ClientInputId)); MOCK_METHOD1(SetShouldSpawnNetworkEntities, void(bool)); MOCK_CONST_METHOD0(GetShouldSpawnNetworkEntities, bool()); }; From c228e77e1c1236bfcf109542db90b526456572ec Mon Sep 17 00:00:00 2001 From: kberg-amzn Date: Tue, 19 Oct 2021 12:11:56 -0700 Subject: [PATCH 10/26] Fixes a bunch of bad casts in hierarchy tests Signed-off-by: kberg-amzn --- Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp | 5 ++--- Gems/Multiplayer/Code/Tests/CommonBenchmarkSetup.h | 10 ++++------ Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h | 10 ++++------ 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp b/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp index d6dd068c3f..d1f169cb88 100644 --- a/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp +++ b/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp @@ -211,9 +211,8 @@ namespace Multiplayer constexpr uint32_t bufferSize = 100; AZStd::array buffer = {}; NetworkInputSerializer inSerializer(buffer.begin(), bufferSize); - inSerializer.Serialize(reinterpret_cast(value), - "hierarchyRoot", /* Derived from NetworkHierarchyChildComponent.AutoComponent.xml */ - AZStd::numeric_limits::min(), AZStd::numeric_limits::max()); + ISerializer& serializer = inSerializer; + serializer.Serialize(value, "hierarchyRoot"); // Derived from NetworkHierarchyChildComponent.AutoComponent.xml NetworkOutputSerializer outSerializer(buffer.begin(), bufferSize); diff --git a/Gems/Multiplayer/Code/Tests/CommonBenchmarkSetup.h b/Gems/Multiplayer/Code/Tests/CommonBenchmarkSetup.h index 803e6cabca..3c3d77e011 100644 --- a/Gems/Multiplayer/Code/Tests/CommonBenchmarkSetup.h +++ b/Gems/Multiplayer/Code/Tests/CommonBenchmarkSetup.h @@ -538,9 +538,8 @@ namespace Multiplayer constexpr uint32_t bufferSize = 100; AZStd::array buffer = {}; NetworkInputSerializer inSerializer(buffer.begin(), bufferSize); - inSerializer.Serialize(reinterpret_cast(netParentId), - "parentEntityId", /* Derived from NetworkTransformComponent.AutoComponent.xml */ - AZStd::numeric_limits::min(), AZStd::numeric_limits::max()); + ISerializer& serializer = inSerializer; + serializer.Serialize(netParentId, "parentEntityId"); // Derived from NetworkTransformComponent.AutoComponent.xml NetworkOutputSerializer outSerializer(buffer.begin(), bufferSize); @@ -563,9 +562,8 @@ namespace Multiplayer constexpr uint32_t bufferSize = 100; AZStd::array buffer = {}; NetworkInputSerializer inSerializer(buffer.begin(), bufferSize); - inSerializer.Serialize(reinterpret_cast(value), - "hierarchyRoot", /* Derived from NetworkHierarchyChildComponent.AutoComponent.xml */ - AZStd::numeric_limits::min(), AZStd::numeric_limits::max()); + ISerializer& serializer = inSerializer; + serializer.Serialize(value, "hierarchyRoot"); // Derived from NetworkHierarchyChildComponent.AutoComponent.xml NetworkOutputSerializer outSerializer(buffer.begin(), bufferSize); diff --git a/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h b/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h index d6f65918c0..214b991dc9 100644 --- a/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h +++ b/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h @@ -303,9 +303,8 @@ namespace Multiplayer constexpr uint32_t bufferSize = 100; AZStd::array buffer = {}; NetworkInputSerializer inSerializer(buffer.begin(), bufferSize); - inSerializer.Serialize(reinterpret_cast(netParentId), - "parentEntityId", /* Derived from NetworkTransformComponent.AutoComponent.xml */ - AZStd::numeric_limits::min(), AZStd::numeric_limits::max()); + ISerializer& serializer = inSerializer; + serializer.Serialize(netParentId, "parentEntityId"); // Derived from NetworkTransformComponent.AutoComponent.xml NetworkOutputSerializer outSerializer(buffer.begin(), bufferSize); @@ -351,9 +350,8 @@ namespace Multiplayer constexpr uint32_t bufferSize = 100; AZStd::array buffer = {}; NetworkInputSerializer inSerializer(buffer.begin(), bufferSize); - inSerializer.Serialize(reinterpret_cast(value), - "hierarchyRoot", /* Derived from NetworkHierarchyChildComponent.AutoComponent.xml */ - AZStd::numeric_limits::min(), AZStd::numeric_limits::max()); + ISerializer& serializer = inSerializer; + serializer.Serialize(value, "hierarchyRoot"); // Derived from NetworkHierarchyChildComponent.AutoComponent.xml NetworkOutputSerializer outSerializer(buffer.begin(), bufferSize); From 37873f81cf8a540a42236ce1f2f52b42ea67c930 Mon Sep 17 00:00:00 2001 From: AMZN-Olex <5432499+AMZN-Olex@users.noreply.github.com> Date: Thu, 21 Oct 2021 15:09:46 -0400 Subject: [PATCH 11/26] Setting owning connection id based on the hierarchy root network entity. Signed-off-by: AMZN-Olex <5432499+AMZN-Olex@users.noreply.github.com> --- .../NetworkHierarchyChildComponent.h | 8 +- .../NetworkHierarchyRootComponent.h | 12 +- .../NetworkHierarchyChildComponent.cpp | 58 ++++++--- .../NetworkHierarchyRootComponent.cpp | 63 +++++---- .../Code/Tests/ClientHierarchyTests.cpp | 32 ++++- .../Code/Tests/ServerHierarchyTests.cpp | 122 +++++++++++++++++- 6 files changed, 241 insertions(+), 54 deletions(-) diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyChildComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyChildComponent.h index a31cd90efd..af1878dd49 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyChildComponent.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyChildComponent.h @@ -58,11 +58,10 @@ namespace Multiplayer void BindNetworkHierarchyLeaveEventHandler(NetworkHierarchyLeaveEvent::Handler& handler) override; //! @} - protected: + private: //! Used by @NetworkHierarchyRootComponent - void SetTopLevelHierarchyRootEntity(AZ::Entity* hierarchyRoot); + void SetTopLevelHierarchyRootEntity(AZ::Entity* previousHierarchyRoot, AZ::Entity* newHierarchyRoot); - private: AZ::ChildChangedEvent::Handler m_childChangedHandler; void OnChildChanged(AZ::ChildChangeType type, AZ::EntityId child); @@ -80,5 +79,8 @@ namespace Multiplayer bool m_isHierarchyEnabled = true; void NotifyChildrenHierarchyDisbanded(); + + AzNetworking::ConnectionId m_previousOwningConnectionId = AzNetworking::InvalidConnectionId; + void SetOwningConnectionId(AzNetworking::ConnectionId connectionId) override; }; } diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h index cffcd3e97d..874b8cd296 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h @@ -61,10 +61,9 @@ namespace Multiplayer bool SerializeEntityCorrection(AzNetworking::ISerializer& serializer); - protected: - void SetTopLevelHierarchyRootEntity(AZ::Entity* hierarchyRoot); - private: + void SetTopLevelHierarchyRootEntity(AZ::Entity* previousHierarchyRoot, AZ::Entity* newHierarchyRoot); + AZ::ChildChangedEvent::Handler m_childChangedHandler; AZ::ParentChangedEvent::Handler m_parentChangedHandler; @@ -81,16 +80,19 @@ namespace Multiplayer //! Rebuilds hierarchy starting from this root component's entity. void RebuildHierarchy(); - + //! @param underEntity Walk the child entities that belong to @underEntity and consider adding them to the hierarchy. //! Builds the hierarchy using breadth-first iterative method. void InternalBuildHierarchyList(AZ::Entity* underEntity); - void SetRootForEntity(AZ::Entity* root, const AZ::Entity* childEntity); + void SetRootForEntity(AZ::Entity* previousKnownRoot, AZ::Entity* newRoot, const AZ::Entity* childEntity); //! Set to false when deactivating or otherwise not to be included in hierarchy considerations. bool m_isHierarchyEnabled = true; + AzNetworking::ConnectionId m_previousOwningConnectionId = AzNetworking::InvalidConnectionId; + void SetOwningConnectionId(AzNetworking::ConnectionId connectionId) override; + friend class HierarchyBenchmarkBase; }; diff --git a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyChildComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyChildComponent.cpp index 1f32387893..37125a88b9 100644 --- a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyChildComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyChildComponent.cpp @@ -129,35 +129,53 @@ namespace Multiplayer handler.Connect(m_networkHierarchyLeaveEvent); } - void NetworkHierarchyChildComponent::SetTopLevelHierarchyRootEntity(AZ::Entity* hierarchyRoot) + void NetworkHierarchyChildComponent::SetTopLevelHierarchyRootEntity(AZ::Entity* previousHierarchyRoot, AZ::Entity* newHierarchyRoot) { - if (m_rootEntity != hierarchyRoot) + if (newHierarchyRoot) { - m_rootEntity = hierarchyRoot; - - if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) + if (m_rootEntity != newHierarchyRoot) { - NetworkHierarchyChildComponentController* controller = static_cast(GetController()); - if (m_rootEntity) + m_rootEntity = newHierarchyRoot; + + if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) { + NetworkHierarchyChildComponentController* controller = static_cast(GetController()); const NetEntityId netRootId = GetNetworkEntityManager()->GetNetEntityIdById(m_rootEntity->GetId()); controller->SetHierarchyRoot(netRootId); - - m_networkHierarchyChangedEvent.Signal(m_rootEntity->GetId()); } - else + + GetNetBindComponent()->SetOwningConnectionId(m_rootEntity->FindComponent()->GetOwningConnectionId()); + m_networkHierarchyChangedEvent.Signal(m_rootEntity->GetId()); + } + } + else + { + if (m_rootEntity == previousHierarchyRoot || !previousHierarchyRoot) + { + m_rootEntity = nullptr; + + if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) { + NetworkHierarchyChildComponentController* controller = static_cast(GetController()); controller->SetHierarchyRoot(InvalidNetEntityId); - - m_networkHierarchyLeaveEvent.Signal(); } - } - if (m_rootEntity == nullptr) - { + GetNetBindComponent()->SetOwningConnectionId(m_previousOwningConnectionId); + m_networkHierarchyLeaveEvent.Signal(); + NotifyChildrenHierarchyDisbanded(); } } + + } + + void NetworkHierarchyChildComponent::SetOwningConnectionId(AzNetworking::ConnectionId connectionId) + { + NetworkHierarchyChildComponentBase::SetOwningConnectionId(connectionId); + if (IsHierarchicalChild() == false) + { + m_previousOwningConnectionId = connectionId; + } } void NetworkHierarchyChildComponent::OnChildChanged([[maybe_unused]] AZ::ChildChangeType type, [[maybe_unused]] AZ::EntityId child) @@ -180,14 +198,18 @@ namespace Multiplayer if (m_rootEntity != newRoot) { m_rootEntity = newRoot; + + m_previousOwningConnectionId = GetNetBindComponent()->GetOwningConnectionId(); + GetNetBindComponent()->SetOwningConnectionId(m_rootEntity->FindComponent()->GetOwningConnectionId()); + m_networkHierarchyChangedEvent.Signal(m_rootEntity->GetId()); } } else { + GetNetBindComponent()->SetOwningConnectionId(m_previousOwningConnectionId); m_isHierarchyEnabled = false; m_rootEntity = nullptr; - m_networkHierarchyLeaveEvent.Signal(); } } @@ -203,11 +225,11 @@ namespace Multiplayer { if (auto* hierarchyChildComponent = childEntity->FindComponent()) { - hierarchyChildComponent->SetTopLevelHierarchyRootEntity(nullptr); + hierarchyChildComponent->SetTopLevelHierarchyRootEntity(nullptr, nullptr); } else if (auto* hierarchyRootComponent = childEntity->FindComponent()) { - hierarchyRootComponent->SetTopLevelHierarchyRootEntity(nullptr); + hierarchyRootComponent->SetTopLevelHierarchyRootEntity(nullptr, nullptr); } } } diff --git a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp index 8d485a973e..854aec521b 100644 --- a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp @@ -107,7 +107,7 @@ namespace Multiplayer { if (const AZ::Entity* childEntity = AZ::Interface::Get()->FindEntity(childEntityId)) { - SetRootForEntity(nullptr, childEntity); + SetRootForEntity(GetEntity(), nullptr, childEntity); } } } @@ -209,7 +209,7 @@ namespace Multiplayer auto [rootComponent, childComponent] = GetHierarchyComponents(parentEntity); if (rootComponent == nullptr && childComponent == nullptr) { - RebuildHierarchy(); + SetRootForEntity(nullptr, nullptr, GetEntity()); } else { @@ -219,7 +219,7 @@ namespace Multiplayer else { // Detached from parent - RebuildHierarchy(); + SetRootForEntity(nullptr, nullptr, GetEntity()); } } @@ -247,14 +247,14 @@ namespace Multiplayer { // This is a newly added entity to the network hierarchy. hierarchyChanged = true; - SetRootForEntity(GetEntity(), currentEntity); + SetRootForEntity(nullptr, GetEntity(), currentEntity); } } // These entities were removed since last rebuild. for (const AZ::Entity* previousEntity : previousEntities) { - SetRootForEntity(nullptr, previousEntity); + SetRootForEntity(GetEntity(), nullptr, previousEntity); } if (!previousEntities.empty()) @@ -307,42 +307,61 @@ namespace Multiplayer } } - void NetworkHierarchyRootComponent::SetRootForEntity(AZ::Entity* root, const AZ::Entity* childEntity) + void NetworkHierarchyRootComponent::SetRootForEntity(AZ::Entity* previousKnownRoot, AZ::Entity* newRoot, const AZ::Entity* childEntity) { auto [hierarchyRootComponent, hierarchyChildComponent] = GetHierarchyComponents(childEntity); if (hierarchyChildComponent) { - hierarchyChildComponent->SetTopLevelHierarchyRootEntity(root); + hierarchyChildComponent->SetTopLevelHierarchyRootEntity(previousKnownRoot, newRoot); } else if (hierarchyRootComponent) { - hierarchyRootComponent->SetTopLevelHierarchyRootEntity(root); + hierarchyRootComponent->SetTopLevelHierarchyRootEntity(previousKnownRoot, newRoot); } } - void NetworkHierarchyRootComponent::SetTopLevelHierarchyRootEntity(AZ::Entity* hierarchyRoot) + void NetworkHierarchyRootComponent::SetTopLevelHierarchyRootEntity(AZ::Entity* previousHierarchyRoot, AZ::Entity* newHierarchyRoot) { - m_rootEntity = hierarchyRoot; - - if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) + if (newHierarchyRoot) { - NetworkHierarchyChildComponentController* controller = static_cast(GetController()); - if (hierarchyRoot) + m_rootEntity = newHierarchyRoot; + + if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) { - const NetEntityId netRootId = GetNetworkEntityManager()->GetNetEntityIdById(hierarchyRoot->GetId()); + NetworkHierarchyChildComponentController* controller = static_cast(GetController()); + + const NetEntityId netRootId = GetNetworkEntityManager()->GetNetEntityIdById(m_rootEntity->GetId()); controller->SetHierarchyRoot(netRootId); + GetNetBindComponent()->SetOwningConnectionId(m_rootEntity->FindComponent()->GetOwningConnectionId()); } - else + } + else + { + if (previousHierarchyRoot && m_rootEntity == previousHierarchyRoot || !previousHierarchyRoot) { - controller->SetHierarchyRoot(InvalidNetEntityId); + m_rootEntity = nullptr; + + if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) + { + NetworkHierarchyChildComponentController* controller = static_cast(GetController()); + + controller->SetHierarchyRoot(InvalidNetEntityId); + GetNetBindComponent()->SetOwningConnectionId(m_previousOwningConnectionId); + } + + // We lost the parent hierarchical entity, so as a root we need to re-build our own hierarchy. + RebuildHierarchy(); } } + } - if (m_rootEntity == nullptr) + void NetworkHierarchyRootComponent::SetOwningConnectionId(AzNetworking::ConnectionId connectionId) + { + NetworkHierarchyRootComponentBase::SetOwningConnectionId(connectionId); + if (IsHierarchicalChild() == false) { - // We lost the parent hierarchical entity, so as a root we need to re-build our own hierarchy. - RebuildHierarchy(); + m_previousOwningConnectionId = connectionId; } } @@ -370,7 +389,7 @@ namespace Multiplayer void NetworkHierarchyRootComponentController::CreateInput(Multiplayer::NetworkInput& input, float deltaTime) { NetworkHierarchyRootComponent& component = GetParent(); - if(!component.IsHierarchicalRoot()) + if (!component.IsHierarchicalRoot()) { return; } @@ -386,7 +405,7 @@ namespace Multiplayer for (AZ::Entity* child : entities) { - if(child == component.GetEntity()) + if (child == component.GetEntity()) { continue; // Avoid infinite recursion } diff --git a/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp b/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp index badee73e04..d6ce8a3ff7 100644 --- a/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp +++ b/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp @@ -303,6 +303,36 @@ namespace Multiplayer SetHierarchyRootFieldOnNetworkHierarchyChildOnClient(m_child->m_entity, InvalidNetEntityId); } + TEST_F(ClientSimpleHierarchyTests, ChildHasOwningConnectionIdOfParent) + { + // disconnect and assign new connection ids + SetParentIdOnNetworkTransform(m_child->m_entity, InvalidNetEntityId); + SetHierarchyRootFieldOnNetworkHierarchyChildOnClient(m_child->m_entity, InvalidNetEntityId); + + m_root->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 1 }); + m_child->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 2 }); + + const ConnectionId previousConnectionId = m_child->m_entity->FindComponent()->GetOwningConnectionId(); + + // re-attach, child's owning connection id should then be root's connection id + SetParentIdOnNetworkTransform(m_child->m_entity, RootNetEntityId); + SetHierarchyRootFieldOnNetworkHierarchyChildOnClient(m_child->m_entity, RootNetEntityId); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetOwningConnectionId(), + m_root->m_entity->FindComponent()->GetOwningConnectionId() + ); + + // detach, the child should roll back to his previous owning connection id + SetParentIdOnNetworkTransform(m_child->m_entity, InvalidNetEntityId); + SetHierarchyRootFieldOnNetworkHierarchyChildOnClient(m_child->m_entity, InvalidNetEntityId); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetOwningConnectionId(), + previousConnectionId + ); + } + /* * Parent -> Child -> ChildOfChild */ @@ -397,7 +427,7 @@ namespace Multiplayer using MultiplayerTest::TestMultiplayerComponentNetworkInput; auto* rootNetBind = m_root->m_entity->FindComponent(); - + NetworkInputArray inputArray(rootNetBind->GetEntityHandle()); NetworkInput& input = inputArray[0]; diff --git a/Gems/Multiplayer/Code/Tests/ServerHierarchyTests.cpp b/Gems/Multiplayer/Code/Tests/ServerHierarchyTests.cpp index 1b5aa31780..2888071508 100644 --- a/Gems/Multiplayer/Code/Tests/ServerHierarchyTests.cpp +++ b/Gems/Multiplayer/Code/Tests/ServerHierarchyTests.cpp @@ -195,6 +195,49 @@ namespace Multiplayer m_child->m_entity.reset(); } + TEST_F(ServerSimpleHierarchyTests, ChildPointsToRootAfterReattachment) + { + m_child->m_entity->FindComponent()->SetParent(AZ::EntityId()); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetHierarchyRoot(), + InvalidNetEntityId + ); + + m_child->m_entity->FindComponent()->SetParent(m_root->m_entity->GetId()); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetHierarchyRoot(), + m_root->m_entity->FindComponent()->GetNetEntityId() + ); + } + + TEST_F(ServerSimpleHierarchyTests, ChildHasOwningConnectionIdOfParent) + { + // disconnect and assign new connection ids + m_child->m_entity->FindComponent()->SetParent(AZ::EntityId()); + m_root->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 1 }); + m_child->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 2 }); + + const ConnectionId previousConnectionId = m_child->m_entity->FindComponent()->GetOwningConnectionId(); + + // re-attach, child's owning connection id should then be root's connection id + m_child->m_entity->FindComponent()->SetParent(m_root->m_entity->GetId()); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetOwningConnectionId(), + m_root->m_entity->FindComponent()->GetOwningConnectionId() + ); + + // detach, the child should roll back to his previous owning connection id + m_child->m_entity->FindComponent()->SetParent(AZ::EntityId()); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetOwningConnectionId(), + previousConnectionId + ); + } + /* * Parent -> Child -> ChildOfChild */ @@ -410,7 +453,7 @@ namespace Multiplayer { MockNetworkHierarchyCallbackHandler mock; EXPECT_CALL(mock, OnNetworkHierarchyUpdated(m_root->m_entity->GetId())).Times(2); - + m_root->m_entity->FindComponent()->BindNetworkHierarchyChangedEventHandler(mock.m_changedHandler); m_child->m_entity->FindComponent()->SetParent(AZ::EntityId()); @@ -822,6 +865,22 @@ namespace Multiplayer } } + TEST_F(ServerHierarchyOfHierarchyTests, InnerChildrenPointToInnerRootAfterDetachmentFromTopRoot) + { + m_root2->m_entity->FindComponent()->SetParent(m_root->m_entity->GetId()); + // detach + m_root2->m_entity->FindComponent()->SetParent(AZ::EntityId()); + + EXPECT_EQ( + m_child2->m_entity->FindComponent()->GetHierarchyRoot(), + m_root2->m_entity->FindComponent()->GetNetEntityId() + ); + EXPECT_EQ( + m_childOfChild2->m_entity->FindComponent()->GetHierarchyRoot(), + m_root2->m_entity->FindComponent()->GetNetEntityId() + ); + } + TEST_F(ServerHierarchyOfHierarchyTests, Inner_Root_Has_Child_References_After_Detachment_From_Child_Of_Child) { m_root2->m_entity->FindComponent()->SetParent(m_childOfChild->m_entity->GetId()); @@ -1005,6 +1064,59 @@ namespace Multiplayer m_console->GetCvarValue("bg_hierarchyEntityMaxLimit", currentMaxLimit); } + TEST_F(ServerHierarchyOfHierarchyTests, InnerRootAndItsChildrenHaveOwningConnectionIdOfTopRoot) + { + // Assign new connection ids. + m_root->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 1 }); + m_root2->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 2 }); + + // Attach then inner hierarchy's owning connection id should then be top root's connection id. + m_root2->m_entity->FindComponent()->SetParent(m_childOfChild->m_entity->GetId()); + + EXPECT_EQ( + m_root2->m_entity->FindComponent()->GetOwningConnectionId(), + m_root->m_entity->FindComponent()->GetOwningConnectionId() + ); + + EXPECT_EQ( + m_child2->m_entity->FindComponent()->GetOwningConnectionId(), + m_root->m_entity->FindComponent()->GetOwningConnectionId() + ); + + EXPECT_EQ( + m_childOfChild2->m_entity->FindComponent()->GetOwningConnectionId(), + m_root->m_entity->FindComponent()->GetOwningConnectionId() + ); + } + + TEST_F(ServerHierarchyOfHierarchyTests, InnerRootAndItsChildrenHaveTheirOriginalOwningConnectionIdAfterDetachingFromTopRoot) + { + // Assign new connection ids. + m_root->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 1 }); + m_root2->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 2 }); + + // Attach then inner hierarchy's owning connection id should then be top root's connection id. + m_root2->m_entity->FindComponent()->SetParent(m_childOfChild->m_entity->GetId()); + + // detach, inner hierarchy should roll back to his previous owning connection id + m_root2->m_entity->FindComponent()->SetParent(AZ::EntityId()); + + EXPECT_EQ( + m_root2->m_entity->FindComponent()->GetOwningConnectionId(), + ConnectionId{ 2 } + ); + + EXPECT_EQ( + m_child2->m_entity->FindComponent()->GetOwningConnectionId(), + m_root2->m_entity->FindComponent()->GetOwningConnectionId() + ); + + EXPECT_EQ( + m_childOfChild2->m_entity->FindComponent()->GetOwningConnectionId(), + m_root2->m_entity->FindComponent()->GetOwningConnectionId() + ); + } + /* * Parent -> Child -> ChildOfChild (not marked as in a hierarchy) */ @@ -1242,15 +1354,15 @@ namespace Multiplayer ); } - TEST_F(ServerHierarchyWithThreeRoots, ReattachMiddleChildWhileLastChildGetsLeaveEventOnce) + TEST_F(ServerHierarchyWithThreeRoots, InnerRootLeftTopRootThenLastChildGetsJoinedEventOnce) { m_root2->m_entity->FindComponent()->SetParent(m_childOfChild->m_entity->GetId()); m_root3->m_entity->FindComponent()->SetParent(m_childOfChild->m_entity->GetId()); MockNetworkHierarchyCallbackHandler mock; - EXPECT_CALL(mock, OnNetworkHierarchyLeave()); - - m_childOfChild3->m_entity->FindComponent()->BindNetworkHierarchyLeaveEventHandler(mock.m_leaveHandler); + EXPECT_CALL(mock, OnNetworkHierarchyUpdated(m_root3->m_entity->GetId())); + + m_childOfChild3->m_entity->FindComponent()->BindNetworkHierarchyChangedEventHandler(mock.m_changedHandler); m_child->m_entity->FindComponent()->SetParent(AZ::EntityId()); } From 317bb7aa6726fa01afa1c33e1e733fae500e7a7b Mon Sep 17 00:00:00 2001 From: AMZN-Olex <5432499+AMZN-Olex@users.noreply.github.com> Date: Thu, 21 Oct 2021 15:14:00 -0400 Subject: [PATCH 12/26] Owning connection id will happen on clients as well. Signed-off-by: AMZN-Olex <5432499+AMZN-Olex@users.noreply.github.com> --- .../Source/Components/NetworkHierarchyRootComponent.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp index 854aec521b..6858242cd4 100644 --- a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp @@ -333,8 +333,9 @@ namespace Multiplayer const NetEntityId netRootId = GetNetworkEntityManager()->GetNetEntityIdById(m_rootEntity->GetId()); controller->SetHierarchyRoot(netRootId); - GetNetBindComponent()->SetOwningConnectionId(m_rootEntity->FindComponent()->GetOwningConnectionId()); } + + GetNetBindComponent()->SetOwningConnectionId(m_rootEntity->FindComponent()->GetOwningConnectionId()); } else { @@ -347,9 +348,10 @@ namespace Multiplayer NetworkHierarchyChildComponentController* controller = static_cast(GetController()); controller->SetHierarchyRoot(InvalidNetEntityId); - GetNetBindComponent()->SetOwningConnectionId(m_previousOwningConnectionId); } + GetNetBindComponent()->SetOwningConnectionId(m_previousOwningConnectionId); + // We lost the parent hierarchical entity, so as a root we need to re-build our own hierarchy. RebuildHierarchy(); } From 4cd6ce1e96e6174062fb4b002119688c05b62bee Mon Sep 17 00:00:00 2001 From: kberg-amzn Date: Thu, 21 Oct 2021 15:38:43 -0700 Subject: [PATCH 13/26] Fixing broken include path Signed-off-by: kberg-amzn --- .../AutoGen/NetworkHierarchyRootComponent.AutoComponent.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/Multiplayer/Code/Source/AutoGen/NetworkHierarchyRootComponent.AutoComponent.xml b/Gems/Multiplayer/Code/Source/AutoGen/NetworkHierarchyRootComponent.AutoComponent.xml index fbe6ff2135..b789506d0f 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/NetworkHierarchyRootComponent.AutoComponent.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/NetworkHierarchyRootComponent.AutoComponent.xml @@ -8,7 +8,7 @@ OverrideInclude="Multiplayer/Components/NetworkHierarchyRootComponent.h" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> - + From 2df466228aa7393f58a1d1c25c17ad77bb6a2132 Mon Sep 17 00:00:00 2001 From: kberg-amzn Date: Thu, 21 Oct 2021 15:58:01 -0700 Subject: [PATCH 14/26] fixing one more broken include Signed-off-by: kberg-amzn --- Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp b/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp index 2e91fecaac..248c97a60f 100644 --- a/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp +++ b/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp @@ -16,7 +16,7 @@ #include #include #include -#include +#include #include namespace Multiplayer From 214a2899ad195a3316091c65c0125f540a4e0fa9 Mon Sep 17 00:00:00 2001 From: kberg-amzn Date: Thu, 21 Oct 2021 16:41:01 -0700 Subject: [PATCH 15/26] oops =( Signed-off-by: kberg-amzn --- Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp | 3 --- Gems/Multiplayer/Code/Tests/NetworkInputTests.cpp | 6 +++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp b/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp index 248c97a60f..12a49a9ffc 100644 --- a/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp +++ b/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp @@ -276,9 +276,6 @@ namespace Multiplayer m_child->m_entity->FindComponent()->GetHierarchicalRoot(), nullptr ); - AZStd::size_t test; - AZStd::vector test2; - test2.erase } TEST_F(ClientSimpleHierarchyTests, Client_Sends_NetworkHierarchy_Updated_Event_On_Child_Detached_On_Server) diff --git a/Gems/Multiplayer/Code/Tests/NetworkInputTests.cpp b/Gems/Multiplayer/Code/Tests/NetworkInputTests.cpp index 64381bf195..0df0fce610 100644 --- a/Gems/Multiplayer/Code/Tests/NetworkInputTests.cpp +++ b/Gems/Multiplayer/Code/Tests/NetworkInputTests.cpp @@ -17,9 +17,9 @@ #include #include #include -#include -#include -#include +#include +#include +#include namespace Multiplayer { From 7b779619621a78baeca3afe2b4c41f6915fe7932 Mon Sep 17 00:00:00 2001 From: kberg-amzn Date: Thu, 21 Oct 2021 16:52:11 -0700 Subject: [PATCH 16/26] removing unused local variable causing a compiler warning Signed-off-by: kberg-amzn --- Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index c2f10684a3..f628119aef 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -1148,9 +1148,6 @@ namespace Multiplayer void MultiplayerSystemComponent::EnableAutonomousControl(NetworkEntityHandle entityHandle, AzNetworking::ConnectionId connectionId) { - INetworkEntityManager* networkEntityManager = AZ::Interface::Get(); - AZ_Assert(networkEntityManager, "NetworkEntityManager must be created."); - entityHandle.GetNetBindComponent()->SetOwningConnectionId(connectionId); if (connectionId == InvalidConnectionId) { From 15fbe97504ccbd5d61bc3937e92c69eff348fcd1 Mon Sep 17 00:00:00 2001 From: kberg-amzn Date: Thu, 21 Oct 2021 17:12:08 -0700 Subject: [PATCH 17/26] Adding a guard for this, since tests attempt to apply autonomy to an invalid entity Signed-off-by: kberg-amzn --- Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index f628119aef..fdd7602f71 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -1148,6 +1148,12 @@ namespace Multiplayer void MultiplayerSystemComponent::EnableAutonomousControl(NetworkEntityHandle entityHandle, AzNetworking::ConnectionId connectionId) { + if (!entityHandle.Exists()) + { + AZLOG_WARN("Attempting to enable autonomous control for an invalid entity"); + return; + } + entityHandle.GetNetBindComponent()->SetOwningConnectionId(connectionId); if (connectionId == InvalidConnectionId) { From d394f6da3ca723cc83bb98bb0ac11a5accecebb7 Mon Sep 17 00:00:00 2001 From: Benjamin Jillich <43751992+amzn-jillich@users.noreply.github.com> Date: Fri, 22 Oct 2021 09:22:19 +0200 Subject: [PATCH 18/26] EMotion FX: AnimGraphNode::CalcSyncFactors crashes because of improper MCORE_INVALIDINDEX32 initialization (#4869) Signed-off-by: Benjamin Jillich --- .../EMotionFX/Source/AnimGraphMotionNode.cpp | 2 +- .../Code/EMotionFX/Source/AnimGraphNode.h | 6 +- .../Code/EMotionFX/Source/AnimGraphNodeData.h | 66 +++++++++---------- .../Source/AnimGraphStateTransition.cpp | 2 +- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphMotionNode.cpp b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphMotionNode.cpp index c5e50ed9dd..a0a811eff0 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphMotionNode.cpp +++ b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphMotionNode.cpp @@ -597,7 +597,7 @@ namespace EMotionFX // reset several settings to rewind the motion instance motionInstance->ResetTimes(); motionInstance->SetIsFrozen(false); - SetSyncIndex(animGraphInstance, MCORE_INVALIDINDEX32); + SetSyncIndex(animGraphInstance, InvalidIndex); uniqueData->SetCurrentPlayTime(motionInstance->GetCurrentTime()); uniqueData->SetDuration(motionInstance->GetDuration()); uniqueData->SetPreSyncTime(uniqueData->GetCurrentPlayTime()); diff --git a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNode.h b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNode.h index b515621c5b..95d9c0d77d 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNode.h +++ b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNode.h @@ -213,12 +213,12 @@ namespace EMotionFX */ virtual void SkipOutput([[maybe_unused]] AnimGraphInstance* animGraphInstance) {} - MCORE_INLINE float GetDuration(AnimGraphInstance* animGraphInstance) const { return FindOrCreateUniqueNodeData(animGraphInstance)->GetDuration(); } + float GetDuration(AnimGraphInstance* animGraphInstance) const { return FindOrCreateUniqueNodeData(animGraphInstance)->GetDuration(); } virtual void SetCurrentPlayTime(AnimGraphInstance* animGraphInstance, float timeInSeconds) { FindOrCreateUniqueNodeData(animGraphInstance)->SetCurrentPlayTime(timeInSeconds); } virtual float GetCurrentPlayTime(AnimGraphInstance* animGraphInstance) const { return FindOrCreateUniqueNodeData(animGraphInstance)->GetCurrentPlayTime(); } - MCORE_INLINE size_t GetSyncIndex(AnimGraphInstance* animGraphInstance) const { return FindOrCreateUniqueNodeData(animGraphInstance)->GetSyncIndex(); } - MCORE_INLINE void SetSyncIndex(AnimGraphInstance* animGraphInstance, size_t syncIndex) { FindOrCreateUniqueNodeData(animGraphInstance)->SetSyncIndex(syncIndex); } + size_t GetSyncIndex(AnimGraphInstance* animGraphInstance) const { return FindOrCreateUniqueNodeData(animGraphInstance)->GetSyncIndex(); } + void SetSyncIndex(AnimGraphInstance* animGraphInstance, size_t syncIndex) { FindOrCreateUniqueNodeData(animGraphInstance)->SetSyncIndex(syncIndex); } virtual void SetPlaySpeed(AnimGraphInstance* animGraphInstance, float speedFactor) { FindOrCreateUniqueNodeData(animGraphInstance)->SetPlaySpeed(speedFactor); } virtual float GetPlaySpeed(AnimGraphInstance* animGraphInstance) const { return FindOrCreateUniqueNodeData(animGraphInstance)->GetPlaySpeed(); } diff --git a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNodeData.h b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNodeData.h index 1ad82b1d55..5c2f0b281b 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNodeData.h +++ b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNodeData.h @@ -51,52 +51,52 @@ namespace EMotionFX void Init(AnimGraphInstance* animGraphInstance, AnimGraphNode* node); void Init(AnimGraphNodeData* nodeData); - MCORE_INLINE AnimGraphNode* GetNode() const { return reinterpret_cast(m_object); } - MCORE_INLINE void SetNode(AnimGraphNode* node) { m_object = reinterpret_cast(node); } + AnimGraphNode* GetNode() const { return reinterpret_cast(m_object); } + void SetNode(AnimGraphNode* node) { m_object = reinterpret_cast(node); } - MCORE_INLINE void SetSyncIndex(size_t syncIndex) { m_syncIndex = syncIndex; } - MCORE_INLINE size_t GetSyncIndex() const { return m_syncIndex; } + void SetSyncIndex(size_t syncIndex) { m_syncIndex = syncIndex; } + size_t GetSyncIndex() const { return m_syncIndex; } - MCORE_INLINE void SetCurrentPlayTime(float absoluteTime) { m_currentTime = absoluteTime; } - MCORE_INLINE float GetCurrentPlayTime() const { return m_currentTime; } + void SetCurrentPlayTime(float absoluteTime) { m_currentTime = absoluteTime; } + float GetCurrentPlayTime() const { return m_currentTime; } - MCORE_INLINE void SetPlaySpeed(float speed) { m_playSpeed = speed; } - MCORE_INLINE float GetPlaySpeed() const { return m_playSpeed; } + void SetPlaySpeed(float speed) { m_playSpeed = speed; } + float GetPlaySpeed() const { return m_playSpeed; } - MCORE_INLINE void SetDuration(float durationInSeconds) { m_duration = durationInSeconds; } - MCORE_INLINE float GetDuration() const { return m_duration; } + void SetDuration(float durationInSeconds) { m_duration = durationInSeconds; } + float GetDuration() const { return m_duration; } - MCORE_INLINE void SetPreSyncTime(float timeInSeconds) { m_preSyncTime = timeInSeconds; } - MCORE_INLINE float GetPreSyncTime() const { return m_preSyncTime; } + void SetPreSyncTime(float timeInSeconds) { m_preSyncTime = timeInSeconds; } + float GetPreSyncTime() const { return m_preSyncTime; } - MCORE_INLINE void SetGlobalWeight(float weight) { m_globalWeight = weight; } - MCORE_INLINE float GetGlobalWeight() const { return m_globalWeight; } + void SetGlobalWeight(float weight) { m_globalWeight = weight; } + float GetGlobalWeight() const { return m_globalWeight; } - MCORE_INLINE void SetLocalWeight(float weight) { m_localWeight = weight; } - MCORE_INLINE float GetLocalWeight() const { return m_localWeight; } + void SetLocalWeight(float weight) { m_localWeight = weight; } + float GetLocalWeight() const { return m_localWeight; } - MCORE_INLINE uint8 GetInheritFlags() const { return m_inheritFlags; } + uint8 GetInheritFlags() const { return m_inheritFlags; } - MCORE_INLINE bool GetIsBackwardPlaying() const { return (m_inheritFlags & INHERITFLAGS_BACKWARD) != 0; } - MCORE_INLINE void SetBackwardFlag() { m_inheritFlags |= INHERITFLAGS_BACKWARD; } - MCORE_INLINE void ClearInheritFlags() { m_inheritFlags = 0; } + bool GetIsBackwardPlaying() const { return (m_inheritFlags & INHERITFLAGS_BACKWARD) != 0; } + void SetBackwardFlag() { m_inheritFlags |= INHERITFLAGS_BACKWARD; } + void ClearInheritFlags() { m_inheritFlags = 0; } - MCORE_INLINE uint8 GetPoseRefCount() const { return m_poseRefCount; } - MCORE_INLINE void IncreasePoseRefCount() { m_poseRefCount++; } - MCORE_INLINE void DecreasePoseRefCount() { m_poseRefCount--; } - MCORE_INLINE void SetPoseRefCount(uint8 refCount) { m_poseRefCount = refCount; } + uint8 GetPoseRefCount() const { return m_poseRefCount; } + void IncreasePoseRefCount() { m_poseRefCount++; } + void DecreasePoseRefCount() { m_poseRefCount--; } + void SetPoseRefCount(uint8 refCount) { m_poseRefCount = refCount; } - MCORE_INLINE uint8 GetRefDataRefCount() const { return m_refDataRefCount; } - MCORE_INLINE void IncreaseRefDataRefCount() { m_refDataRefCount++; } - MCORE_INLINE void DecreaseRefDataRefCount() { m_refDataRefCount--; } - MCORE_INLINE void SetRefDataRefCount(uint8 refCount) { m_refDataRefCount = refCount; } + uint8 GetRefDataRefCount() const { return m_refDataRefCount; } + void IncreaseRefDataRefCount() { m_refDataRefCount++; } + void DecreaseRefDataRefCount() { m_refDataRefCount--; } + void SetRefDataRefCount(uint8 refCount) { m_refDataRefCount = refCount; } - MCORE_INLINE void SetRefCountedData(AnimGraphRefCountedData* data) { m_refCountedData = data; } - MCORE_INLINE AnimGraphRefCountedData* GetRefCountedData() const { return m_refCountedData; } + void SetRefCountedData(AnimGraphRefCountedData* data) { m_refCountedData = data; } + AnimGraphRefCountedData* GetRefCountedData() const { return m_refCountedData; } - MCORE_INLINE const AnimGraphSyncTrack* GetSyncTrack() const { return m_syncTrack; } - MCORE_INLINE AnimGraphSyncTrack* GetSyncTrack() { return m_syncTrack; } - MCORE_INLINE void SetSyncTrack(AnimGraphSyncTrack* syncTrack) { m_syncTrack = syncTrack; } + const AnimGraphSyncTrack* GetSyncTrack() const { return m_syncTrack; } + AnimGraphSyncTrack* GetSyncTrack() { return m_syncTrack; } + void SetSyncTrack(AnimGraphSyncTrack* syncTrack) { m_syncTrack = syncTrack; } bool GetIsMirrorMotion() const { return m_isMirrorMotion; } void SetIsMirrorMotion(bool newValue) { m_isMirrorMotion = newValue; } diff --git a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphStateTransition.cpp b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphStateTransition.cpp index e90491b696..c6d9b4eeec 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphStateTransition.cpp +++ b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphStateTransition.cpp @@ -326,7 +326,7 @@ namespace EMotionFX uniqueData->m_totalSeconds = 0.0f; uniqueData->m_blendProgress = 0.0f; - m_targetNode->SetSyncIndex(animGraphInstance, MCORE_INVALIDINDEX32); + m_targetNode->SetSyncIndex(animGraphInstance, InvalidIndex); // Trigger action for (AnimGraphTriggerAction* action : m_actionSetup.GetActions()) From 618da447a6118e5101daaeba64fee7c95abe1beb Mon Sep 17 00:00:00 2001 From: Benjamin Jillich <43751992+amzn-jillich@users.noreply.github.com> Date: Fri, 22 Oct 2021 09:22:28 +0200 Subject: [PATCH 19/26] Added option to change the line color for plots and bar color for histograms in the histogram container (#4859) Signed-off-by: Benjamin Jillich --- Gems/ImGui/Code/Include/LYImGuiUtils/HistogramContainer.h | 5 +++++ Gems/ImGui/Code/Source/LYImGuiUtils/HistogramContainer.cpp | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/Gems/ImGui/Code/Include/LYImGuiUtils/HistogramContainer.h b/Gems/ImGui/Code/Include/LYImGuiUtils/HistogramContainer.h index 6129c3ec08..e31d5743c3 100644 --- a/Gems/ImGui/Code/Include/LYImGuiUtils/HistogramContainer.h +++ b/Gems/ImGui/Code/Include/LYImGuiUtils/HistogramContainer.h @@ -85,6 +85,10 @@ namespace ImGui //! Calculate the min and maximum values for the present samples. void CalcMinMaxValues(float& outMin, float& outMax); + //! Set/get color used by either the lines in case ViewType is Lines or bars in case or Histogram. + void SetBarLineColor(const ImColor& color) { m_barLineColor = color; } + ImColor GetBarLineColor() const { return m_barLineColor; } + private: // Set the Max Size and clear the container void SetMaxSize(int size); @@ -99,6 +103,7 @@ namespace ImGui bool m_dispalyOverlays; ScaleMode m_scaleMode; //! Determines if the vertical range of the histogram will be manually specified, auto-expanded or automatically scaled based on the samples. float m_autoScaleSpeed = 0.05f; //! Indicates how fast the min max values and the visible vertical range are adapting to new samples. + ImColor m_barLineColor = ImColor(66, 166, 178); //! Color used by either the lines in case ViewType is Lines or bars in case or Histogram. bool m_collapsed; bool m_drawMostRecentValueText; }; diff --git a/Gems/ImGui/Code/Source/LYImGuiUtils/HistogramContainer.cpp b/Gems/ImGui/Code/Source/LYImGuiUtils/HistogramContainer.cpp index 1915cc3721..51bcc29b99 100644 --- a/Gems/ImGui/Code/Source/LYImGuiUtils/HistogramContainer.cpp +++ b/Gems/ImGui/Code/Source/LYImGuiUtils/HistogramContainer.cpp @@ -147,6 +147,8 @@ namespace ImGui float imGuiHistoWidgetHeight = m_collapsed ? histogramHeight : (histogramHeight - 15); if (GetSize() > 0) { + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, m_barLineColor.Value); + switch (m_viewType) { default: @@ -160,6 +162,8 @@ namespace ImGui ImGui::PlotLines(AZStd::string::format("##%s_lines", m_histogramName.c_str()).c_str(), ImGui::LYImGuiUtils::s_histogramContainerGetter, this, GetSize(), 0, m_histogramName.c_str(), m_minScale, m_maxScale, ImVec2(histogramWidth - 10, imGuiHistoWidgetHeight)); break; } + + ImGui::PopStyleColor(); } From 63140dc2478de4fe01cffb77f49002d19397d071 Mon Sep 17 00:00:00 2001 From: galibzon <66021303+galibzon@users.noreply.github.com> Date: Fri, 22 Oct 2021 05:52:50 -0500 Subject: [PATCH 20/26] Atom/galibzon/atom 4608/inline to root constant (#4897) * Changed references to "InlineConstant" to "RootConstant". Updated AZSLC to version 1.7.34 for mac, linux & windows Signed-off-by: garrieta --- .../Shader/Code/Source/Editor/AzslCompiler.cpp | 16 ++++++++-------- .../Editor/AzslShaderBuilderSystemComponent.cpp | 4 ++-- .../Code/Source/Editor/ShaderBuilderUtility.cpp | 2 +- .../Source/Editor/ShaderVariantAssetBuilder.cpp | 2 +- .../StandardMultilayerPBR_ForwardPass.shader | 8 ++++++++ .../StandardMultilayerPBR_ForwardPass_EDS.shader | 8 ++++++++ .../Features/RayTracing/RayTracingSrgs.shader | 2 +- .../Platform/Linux/BuiltInPackages_linux.cmake | 2 +- .../Platform/Mac/BuiltInPackages_mac.cmake | 3 +-- .../Windows/BuiltInPackages_windows.cmake | 2 +- 10 files changed, 32 insertions(+), 17 deletions(-) diff --git a/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslCompiler.cpp b/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslCompiler.cpp index 79a39ff793..3edc7bbe67 100644 --- a/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslCompiler.cpp +++ b/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslCompiler.cpp @@ -404,18 +404,18 @@ namespace AZ bool AzslCompiler::ParseSrgPopulateRootConstantData(const rapidjson::Document& input, RootConstantData& rootConstantData) const { - if (input.HasMember("InlineConstantBuffer")) + if (input.HasMember("RootConstantBuffer")) { - const rapidjson::Value& rootConstantBufferValue = input["InlineConstantBuffer"]; - AZ_Assert(rootConstantBufferValue.IsObject(), "InlineConstantBuffer is not an object"); + const rapidjson::Value& rootConstantBufferValue = input["RootConstantBuffer"]; + AZ_Assert(rootConstantBufferValue.IsObject(), "RootConstantBuffer is not an object"); for (rapidjson::Value::ConstMemberIterator itr = rootConstantBufferValue.MemberBegin(); itr != rootConstantBufferValue.MemberEnd(); ++itr) { AZStd::string_view rootConstantBufferMemberName = itr->name.GetString(); const rapidjson::Value& rootConstantBufferMemberValue = itr->value; - if (rootConstantBufferMemberName == "bufferForInlineConstants") + if (rootConstantBufferMemberName == "bufferForRootConstants") { - AZ_Assert(rootConstantBufferMemberValue.IsObject(), "bufferForInlineConstants is not an object"); + AZ_Assert(rootConstantBufferMemberValue.IsObject(), "bufferForRootConstants is not an object"); for (rapidjson::Value::ConstMemberIterator itr2 = rootConstantBufferMemberValue.MemberBegin(); itr2 != rootConstantBufferMemberValue.MemberEnd(); ++itr2) { @@ -442,14 +442,14 @@ namespace AZ } } } - else if (rootConstantBufferMemberName == "inputsForInlineConstants") + else if (rootConstantBufferMemberName == "inputsForRootConstants") { - AZ_Assert(rootConstantBufferMemberValue.IsArray(), "inputsForInlineConstants is not an array"); + AZ_Assert(rootConstantBufferMemberValue.IsArray(), "inputsForRootConstants is not an array"); for (rapidjson::Value::ConstValueIterator itr2 = rootConstantBufferMemberValue.Begin(); itr2 != rootConstantBufferMemberValue.End(); ++itr2) { const rapidjson::Value& rootConstantBufferValue2 = *itr2; - AZ_Assert(rootConstantBufferValue2.IsObject(), "Entry in inputsForInlineConstants is not an object"); + AZ_Assert(rootConstantBufferValue2.IsObject(), "Entry in inputsForRootConstants is not an object"); SrgConstantData rootConstantInputs; diff --git a/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslShaderBuilderSystemComponent.cpp b/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslShaderBuilderSystemComponent.cpp index cacb310918..ffc5f40ef4 100644 --- a/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslShaderBuilderSystemComponent.cpp +++ b/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslShaderBuilderSystemComponent.cpp @@ -81,7 +81,7 @@ namespace AZ // Register Shader Asset Builder AssetBuilderSDK::AssetBuilderDesc shaderAssetBuilderDescriptor; shaderAssetBuilderDescriptor.m_name = "Shader Asset Builder"; - shaderAssetBuilderDescriptor.m_version = 104; // ATOM-15871 + shaderAssetBuilderDescriptor.m_version = 105; // [AZSL] Changing inlineConstant to rootConstant keyword work. // .shader file changes trigger rebuilds shaderAssetBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern( AZStd::string::format("*.%s", RPI::ShaderSourceData::Extension), AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard)); shaderAssetBuilderDescriptor.m_busId = azrtti_typeid(); @@ -96,7 +96,7 @@ namespace AZ shaderVariantAssetBuilderDescriptor.m_name = "Shader Variant Asset Builder"; // Both "Shader Variant Asset Builder" and "Shader Asset Builder" produce ShaderVariantAsset products. If you update // ShaderVariantAsset you will need to update BOTH version numbers, not just "Shader Variant Asset Builder". - shaderVariantAssetBuilderDescriptor.m_version = 25; // ATOM-15871 + shaderVariantAssetBuilderDescriptor.m_version = 26; // [AZSL] Changing inlineConstant to rootConstant keyword work. shaderVariantAssetBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern(AZStd::string::format("*.%s", RPI::ShaderVariantListSourceData::Extension), AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard)); shaderVariantAssetBuilderDescriptor.m_busId = azrtti_typeid(); shaderVariantAssetBuilderDescriptor.m_createJobFunction = AZStd::bind(&ShaderVariantAssetBuilder::CreateJobs, &m_shaderVariantAssetBuilder, AZStd::placeholders::_1, AZStd::placeholders::_2); diff --git a/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderBuilderUtility.cpp b/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderBuilderUtility.cpp index e9ac18a432..2c87a79068 100644 --- a/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderBuilderUtility.cpp +++ b/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderBuilderUtility.cpp @@ -183,7 +183,7 @@ namespace AZ // access the root constants reflection if (!azslc.ParseSrgPopulateRootConstantData( outcomes[AzslSubProducts::srg].GetValue(), - rootConstantData)) // consuming data from --srg ("InlineConstantBuffer" subjson section) + rootConstantData)) // consuming data from --srg ("RootConstantBuffer" subjson section) { AZ_Error(builderName, false, "Failed to obtain root constant data reflection"); return AssetBuilderSDK::ProcessJobResult_Failed; diff --git a/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderVariantAssetBuilder.cpp b/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderVariantAssetBuilder.cpp index 27ae90dcdc..bb40baca7d 100644 --- a/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderVariantAssetBuilder.cpp +++ b/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderVariantAssetBuilder.cpp @@ -561,7 +561,7 @@ namespace AZ // Access the root constants reflection if (!azslCompiler.ParseSrgPopulateRootConstantData( jsonOutcome.GetValue(), - rootConstantData)) // consuming data from --srg ("InlineConstantBuffer" subjson section) + rootConstantData)) // consuming data from --srg ("RootConstantBuffer" subjson section) { AZ_Error(ShaderVariantAssetBuilderName, false, "Failed to obtain root constant data reflection"); return false; diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass.shader b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass.shader index 28322d68ed..00efca6056 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass.shader +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass.shader @@ -48,6 +48,14 @@ } ] }, + + "Supervariants": + [ + { + "Name": "", + "PlusArguments": "--no-alignment-validation" + } + ], "DrawList" : "forward" } diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass_EDS.shader b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass_EDS.shader index 42366d6067..983245ffb0 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass_EDS.shader +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass_EDS.shader @@ -49,5 +49,13 @@ ] }, + "Supervariants": + [ + { + "Name": "", + "PlusArguments": "--no-alignment-validation" + } + ], + "DrawList" : "forward" } diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSrgs.shader b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSrgs.shader index 5512b1ad4d..475a1b1b54 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSrgs.shader +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSrgs.shader @@ -37,7 +37,7 @@ [ { "Name": "", - "PlusArguments": "", + "PlusArguments": "--no-alignment-validation", "MinusArguments": "--strip-unused-srgs" } ] diff --git a/cmake/3rdParty/Platform/Linux/BuiltInPackages_linux.cmake b/cmake/3rdParty/Platform/Linux/BuiltInPackages_linux.cmake index d6fdc5cd9b..6210b9cc18 100644 --- a/cmake/3rdParty/Platform/Linux/BuiltInPackages_linux.cmake +++ b/cmake/3rdParty/Platform/Linux/BuiltInPackages_linux.cmake @@ -39,7 +39,7 @@ ly_associate_package(PACKAGE_NAME libsamplerate-0.2.1-rev2-linux ly_associate_package(PACKAGE_NAME OpenSSL-1.1.1b-rev2-linux TARGETS OpenSSL PACKAGE_HASH b779426d1e9c5ddf71160d5ae2e639c3b956e0fb5e9fcaf9ce97c4526024e3bc) ly_associate_package(PACKAGE_NAME DirectXShaderCompilerDxc-1.6.2104-o3de-rev3-linux TARGETS DirectXShaderCompilerDxc PACKAGE_HASH 88c4a359325d749bc34090b9ac466424847f3b71ba0de15045cf355c17c07099) ly_associate_package(PACKAGE_NAME SPIRVCross-2021.04.29-rev1-linux TARGETS SPIRVCross PACKAGE_HASH 7889ee5460a688e9b910c0168b31445c0079d363affa07b25d4c8aeb608a0b80) -ly_associate_package(PACKAGE_NAME azslc-1.7.23-rev2-linux TARGETS azslc PACKAGE_HASH 1ba84d8321a566d35a1e9aa7400211ba8e6d1c11c08e4be3c93e6e74b8f7aef1) +ly_associate_package(PACKAGE_NAME azslc-1.7.34-rev1-linux TARGETS azslc PACKAGE_HASH 6d7dc671936c34ff70d2632196107ca1b8b2b41acdd021bfbc69a9fd56215c22) ly_associate_package(PACKAGE_NAME zlib-1.2.11-rev5-linux TARGETS ZLIB PACKAGE_HASH 9be5ea85722fc27a8645a9c8a812669d107c68e6baa2ca0740872eaeb6a8b0fc) ly_associate_package(PACKAGE_NAME squish-ccr-deb557d-rev1-linux TARGETS squish-ccr PACKAGE_HASH 85fecafbddc6a41a27c5f59ed4a5dfb123a94cb4666782cf26e63c0a4724c530) ly_associate_package(PACKAGE_NAME astc-encoder-3.2-rev1-linux TARGETS astc-encoder PACKAGE_HASH 2ba97a06474d609945f0ab4419af1f6bbffdd294ca6b869f5fcebec75c573c0f) diff --git a/cmake/3rdParty/Platform/Mac/BuiltInPackages_mac.cmake b/cmake/3rdParty/Platform/Mac/BuiltInPackages_mac.cmake index 41df718b71..d054ba22e1 100644 --- a/cmake/3rdParty/Platform/Mac/BuiltInPackages_mac.cmake +++ b/cmake/3rdParty/Platform/Mac/BuiltInPackages_mac.cmake @@ -17,7 +17,6 @@ ly_associate_package(PACKAGE_NAME cityhash-1.1-multiplatform ly_associate_package(PACKAGE_NAME expat-2.1.0-multiplatform TARGETS expat PACKAGE_HASH 452256acd1fd699cef24162575b3524fccfb712f5321c83f1df1ce878de5b418) ly_associate_package(PACKAGE_NAME zstd-1.35-multiplatform TARGETS zstd PACKAGE_HASH 45d466c435f1095898578eedde85acf1fd27190e7ea99aeaa9acfd2f09e12665) ly_associate_package(PACKAGE_NAME SQLite-3.32.2-rev3-multiplatform TARGETS SQLite PACKAGE_HASH dd4d3de6cbb4ce3d15fc504ba0ae0587e515dc89a25228037035fc0aef4831f4) -ly_associate_package(PACKAGE_NAME azslc-1.7.23-rev1-multiplatform TARGETS azslc PACKAGE_HASH 664439954bad54cc43731c684adbc1249d971ad7379fcd83ca8bba5e1cc4a2d0) ly_associate_package(PACKAGE_NAME glad-2.0.0-beta-rev2-multiplatform TARGETS glad PACKAGE_HASH ff97ee9664e97d0854b52a3734c2289329d9f2b4cd69478df6d0ca1f1c9392ee) ly_associate_package(PACKAGE_NAME lux_core-2.2-rev5-multiplatform TARGETS lux_core PACKAGE_HASH c8c13cf7bc351643e1abd294d0841b24dee60e51647dff13db7aec396ad1e0b5) ly_associate_package(PACKAGE_NAME xxhash-0.7.4-rev1-multiplatform TARGETS xxhash PACKAGE_HASH e81f3e6c4065975833996dd1fcffe46c3cf0f9e3a4207ec5f4a1b564ba75861e) @@ -43,4 +42,4 @@ ly_associate_package(PACKAGE_NAME squish-ccr-deb557d-rev1-mac ly_associate_package(PACKAGE_NAME astc-encoder-3.2-rev1-mac TARGETS astc-encoder PACKAGE_HASH 96f6ea8c3e45ec7fe525230c7c53ca665c8300d8e28456cc19bb3159ce6f8dcc) ly_associate_package(PACKAGE_NAME ISPCTexComp-36b80aa-rev1-mac TARGETS ISPCTexComp PACKAGE_HASH 8a4e93277b8face6ea2fd57c6d017bdb55643ed3d6387110bc5f6b3b884dd169) ly_associate_package(PACKAGE_NAME lz4-1.9.3-vcpkg-rev4-mac TARGETS lz4 PACKAGE_HASH 891ff630bf34f7ab1d8eaee2ea0a8f1fca89dbdc63fca41ee592703dd488a73b) - +ly_associate_package(PACKAGE_NAME azslc-1.7.34-rev1-mac TARGETS azslc PACKAGE_HASH a9d81946b42ffa55c0d14d6a9249b3340e59a8fb8835e7a96c31df80f14723bc) diff --git a/cmake/3rdParty/Platform/Windows/BuiltInPackages_windows.cmake b/cmake/3rdParty/Platform/Windows/BuiltInPackages_windows.cmake index cccd2591e8..fce2229771 100644 --- a/cmake/3rdParty/Platform/Windows/BuiltInPackages_windows.cmake +++ b/cmake/3rdParty/Platform/Windows/BuiltInPackages_windows.cmake @@ -17,7 +17,6 @@ ly_associate_package(PACKAGE_NAME cityhash-1.1-multiplatform ly_associate_package(PACKAGE_NAME expat-2.1.0-multiplatform TARGETS expat PACKAGE_HASH 452256acd1fd699cef24162575b3524fccfb712f5321c83f1df1ce878de5b418) ly_associate_package(PACKAGE_NAME zstd-1.35-multiplatform TARGETS zstd PACKAGE_HASH 45d466c435f1095898578eedde85acf1fd27190e7ea99aeaa9acfd2f09e12665) ly_associate_package(PACKAGE_NAME SQLite-3.32.2-rev3-multiplatform TARGETS SQLite PACKAGE_HASH dd4d3de6cbb4ce3d15fc504ba0ae0587e515dc89a25228037035fc0aef4831f4) -ly_associate_package(PACKAGE_NAME azslc-1.7.23-rev1-multiplatform TARGETS azslc PACKAGE_HASH 664439954bad54cc43731c684adbc1249d971ad7379fcd83ca8bba5e1cc4a2d0) ly_associate_package(PACKAGE_NAME glad-2.0.0-beta-rev2-multiplatform TARGETS glad PACKAGE_HASH ff97ee9664e97d0854b52a3734c2289329d9f2b4cd69478df6d0ca1f1c9392ee) ly_associate_package(PACKAGE_NAME lux_core-2.2-rev5-multiplatform TARGETS lux_core PACKAGE_HASH c8c13cf7bc351643e1abd294d0841b24dee60e51647dff13db7aec396ad1e0b5) ly_associate_package(PACKAGE_NAME xxhash-0.7.4-rev1-multiplatform TARGETS xxhash PACKAGE_HASH e81f3e6c4065975833996dd1fcffe46c3cf0f9e3a4207ec5f4a1b564ba75861e) @@ -50,3 +49,4 @@ ly_associate_package(PACKAGE_NAME squish-ccr-deb557d-rev1-windows ly_associate_package(PACKAGE_NAME astc-encoder-3.2-rev1-windows TARGETS astc-encoder PACKAGE_HASH 3addc6fc1a7eb0d6b7f3d530e962af967e6d92b3825ef485da243346357cf78e) ly_associate_package(PACKAGE_NAME ISPCTexComp-36b80aa-rev1-windows TARGETS ISPCTexComp PACKAGE_HASH b6fa6ea28a2808a9a5524c72c37789c525925e435770f2d94eb2d387360fa2d0) ly_associate_package(PACKAGE_NAME lz4-1.9.3-vcpkg-rev4-windows TARGETS lz4 PACKAGE_HASH 4ea457b833cd8cfaf8e8e06ed6df601d3e6783b606bdbc44a677f77e19e0db16) +ly_associate_package(PACKAGE_NAME azslc-1.7.34-rev1-windows TARGETS azslc PACKAGE_HASH 44eb2e0fc4b0f1c75d0fb6f24c93a5753655b84dbc3e6ad45389ed3b9cf7a4b0) From 54b9ed27375f9efc2b349e4b73a5a7c12f034747 Mon Sep 17 00:00:00 2001 From: AMZN-nggieber <52797929+AMZN-nggieber@users.noreply.github.com> Date: Fri, 22 Oct 2021 04:11:32 -0700 Subject: [PATCH 21/26] Added Menu to Gem Catalog with Action to Navigate to Gem Repo Screen (#4829) * Added menu to Gem Catalog that with option to navigate to Gem Repo screen Signed-off-by: nggieber * Changed Goto to GoTo and added a tr Signed-off-by: nggieber * Gem repo button works from new project creation workflow as well and users are warned if they have pending changes in the gem catalog before changing screens. Signed-off-by: nggieber --- .../Resources/ProjectManager.qss | 12 ++++++ .../Source/CreateProjectCtrl.cpp | 4 +- .../Source/EngineScreenCtrl.cpp | 38 +++++++++++++++---- .../ProjectManager/Source/EngineScreenCtrl.h | 5 +++ .../GemCatalog/GemCatalogHeaderWidget.cpp | 26 ++++++++++++- .../GemCatalog/GemCatalogHeaderWidget.h | 9 ++++- .../Source/GemCatalog/GemCatalogScreen.cpp | 23 +++++++++++ .../Source/GemCatalog/GemCatalogScreen.h | 4 ++ .../ProjectManager/Source/ScreenWidget.h | 10 ++++- .../ProjectManager/Source/ScreensCtrl.cpp | 28 ++++++++++++-- .../Tools/ProjectManager/Source/ScreensCtrl.h | 2 +- .../Source/UpdateProjectCtrl.cpp | 6 ++- 12 files changed, 151 insertions(+), 16 deletions(-) diff --git a/Code/Tools/ProjectManager/Resources/ProjectManager.qss b/Code/Tools/ProjectManager/Resources/ProjectManager.qss index 507cf38726..298d2a749a 100644 --- a/Code/Tools/ProjectManager/Resources/ProjectManager.qss +++ b/Code/Tools/ProjectManager/Resources/ProjectManager.qss @@ -218,6 +218,10 @@ QTabBar::tab:focus { color: #666666; } +#verticalSeparatingLine { + color: #888888; +} + /************** Project Settings **************/ #projectSettings { margin-top:42px; @@ -481,6 +485,14 @@ QProgressBar::chunk { font-weight: 600; } +#gemCatalogMenuButton { + qproperty-flat: true; + max-width:36px; + min-width:36px; + max-height:24px; + min-height:24px; +} + #GemCatalogCartOverlayGemDownloadHeader { margin:0; padding: 0px; diff --git a/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp b/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp index 84b931cb30..65e01803aa 100644 --- a/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp +++ b/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp @@ -49,6 +49,8 @@ namespace O3DE::ProjectManager m_stack->addWidget(m_gemCatalogScreen); vLayout->addWidget(m_stack); + connect(m_gemCatalogScreen, &ScreenWidget::ChangeScreenRequest, this, &CreateProjectCtrl::OnChangeScreenRequest); + // When there are multiple project templates present, we re-gather the gems when changing the selected the project template. connect(m_newProjectSettingsScreen, &NewProjectSettingsScreen::OnTemplateSelectionChanged, this, [=](int oldIndex, [[maybe_unused]] int newIndex) { @@ -133,7 +135,7 @@ namespace O3DE::ProjectManager } else { - emit GotoPreviousScreenRequest(); + emit GoToPreviousScreenRequest(); } } diff --git a/Code/Tools/ProjectManager/Source/EngineScreenCtrl.cpp b/Code/Tools/ProjectManager/Source/EngineScreenCtrl.cpp index c78a9426db..f30a8e0daa 100644 --- a/Code/Tools/ProjectManager/Source/EngineScreenCtrl.cpp +++ b/Code/Tools/ProjectManager/Source/EngineScreenCtrl.cpp @@ -29,17 +29,17 @@ namespace O3DE::ProjectManager topBarFrameWidget->setLayout(topBarHLayout); - QTabWidget* tabWidget = new QTabWidget(); - tabWidget->setObjectName("engineTab"); - tabWidget->tabBar()->setObjectName("engineTabBar"); - tabWidget->tabBar()->setFocusPolicy(Qt::TabFocus); + m_tabWidget = new QTabWidget(); + m_tabWidget->setObjectName("engineTab"); + m_tabWidget->tabBar()->setObjectName("engineTabBar"); + m_tabWidget->tabBar()->setFocusPolicy(Qt::TabFocus); m_engineSettingsScreen = new EngineSettingsScreen(); m_gemRepoScreen = new GemRepoScreen(); - tabWidget->addTab(m_engineSettingsScreen, tr("General")); - tabWidget->addTab(m_gemRepoScreen, tr("Gem Repositories")); - topBarHLayout->addWidget(tabWidget); + m_tabWidget->addTab(m_engineSettingsScreen, tr("General")); + m_tabWidget->addTab(m_gemRepoScreen, tr("Gem Repositories")); + topBarHLayout->addWidget(m_tabWidget); vLayout->addWidget(topBarFrameWidget); @@ -61,4 +61,28 @@ namespace O3DE::ProjectManager return true; } + bool EngineScreenCtrl::ContainsScreen(ProjectManagerScreen screen) + { + if (screen == m_engineSettingsScreen->GetScreenEnum() || screen == m_gemRepoScreen->GetScreenEnum()) + { + return true; + } + + return false; + } + + void EngineScreenCtrl::GoToScreen(ProjectManagerScreen screen) + { + if (screen == m_engineSettingsScreen->GetScreenEnum()) + { + m_tabWidget->setCurrentWidget(m_engineSettingsScreen); + m_engineSettingsScreen->NotifyCurrentScreen(); + } + else if (screen == m_gemRepoScreen->GetScreenEnum()) + { + m_tabWidget->setCurrentWidget(m_gemRepoScreen); + m_gemRepoScreen->NotifyCurrentScreen(); + } + } + } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/EngineScreenCtrl.h b/Code/Tools/ProjectManager/Source/EngineScreenCtrl.h index 9e799f13e7..b7142ba226 100644 --- a/Code/Tools/ProjectManager/Source/EngineScreenCtrl.h +++ b/Code/Tools/ProjectManager/Source/EngineScreenCtrl.h @@ -11,6 +11,8 @@ #include #endif +QT_FORWARD_DECLARE_CLASS(QTabWidget) + namespace O3DE::ProjectManager { QT_FORWARD_DECLARE_CLASS(EngineSettingsScreen) @@ -26,7 +28,10 @@ namespace O3DE::ProjectManager QString GetTabText() override; bool IsTab() override; + bool ContainsScreen(ProjectManagerScreen screen) override; + void GoToScreen(ProjectManagerScreen screen) override; + QTabWidget* m_tabWidget = nullptr; EngineSettingsScreen* m_engineSettingsScreen = nullptr; GemRepoScreen* m_gemRepoScreen = nullptr; }; diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp index 79ff7624b7..0ecea215bf 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp @@ -8,12 +8,14 @@ #include #include +#include + #include #include #include #include +#include #include -#include namespace O3DE::ProjectManager { @@ -404,6 +406,28 @@ namespace O3DE::ProjectManager CartButton* cartButton = new CartButton(gemModel, downloadController); hLayout->addWidget(cartButton); + + hLayout->addSpacing(16); + + // Separating line + QFrame* vLine = new QFrame(); + vLine->setFrameShape(QFrame::VLine); + vLine->setObjectName("verticalSeparatingLine"); + hLayout->addWidget(vLine); + + hLayout->addSpacing(16); + + QMenu* gemMenu = new QMenu(this); + m_openGemReposAction = gemMenu->addAction(tr("Show Gem Repos")); + + connect(m_openGemReposAction, &QAction::triggered, this,[this](){ emit OpenGemsRepo(); }); + + QPushButton* gemMenuButton = new QPushButton(this); + gemMenuButton->setObjectName("gemCatalogMenuButton"); + gemMenuButton->setMenu(gemMenu); + gemMenuButton->setIcon(QIcon(":/menu.svg")); + gemMenuButton->setIconSize(QSize(36, 24)); + hLayout->addWidget(gemMenuButton); } void GemCatalogHeaderWidget::ReinitForProject() diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h index 8e0eaa13ba..fa381e54ae 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h @@ -15,13 +15,15 @@ #include #include #include +#include + #include #include #include #include #include #include -#include +#include #endif namespace O3DE::ProjectManager @@ -84,8 +86,13 @@ namespace O3DE::ProjectManager void ReinitForProject(); + signals: + void OpenGemsRepo(); + private: AzQtComponents::SearchLineEdit* m_filterLineEdit = nullptr; inline constexpr static int s_height = 60; + + QAction* m_openGemReposAction = nullptr; }; } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp index 58f3b48c81..cbe36400cf 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp @@ -38,6 +38,8 @@ namespace O3DE::ProjectManager m_headerWidget = new GemCatalogHeaderWidget(m_gemModel, m_proxModel, m_downloadController); vLayout->addWidget(m_headerWidget); + connect(m_headerWidget, &GemCatalogHeaderWidget::OpenGemsRepo, this, &GemCatalogScreen::HandleOpenGemRepo); + QHBoxLayout* hLayout = new QHBoxLayout(); hLayout->setMargin(0); vLayout->addLayout(hLayout); @@ -194,6 +196,27 @@ namespace O3DE::ProjectManager return EnableDisableGemsResult::Success; } + void GemCatalogScreen::HandleOpenGemRepo() + { + QVector gemsToBeAdded = m_gemModel->GatherGemsToBeAdded(true); + QVector gemsToBeRemoved = m_gemModel->GatherGemsToBeRemoved(true); + + if (!gemsToBeAdded.empty() || !gemsToBeRemoved.empty()) + { + QMessageBox::StandardButton warningResult = QMessageBox::warning( + nullptr, "Pending Changes", + "There are some unsaved changes to the gem selection,
they will be lost if you change screens.
Are you sure?", + QMessageBox::No | QMessageBox::Yes); + + if (warningResult != QMessageBox::Yes) + { + return; + } + } + + emit ChangeScreenRequest(ProjectManagerScreen::GemRepos); + } + ProjectManagerScreen GemCatalogScreen::GetScreenEnum() { return ProjectManagerScreen::GemCatalog; diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h index 361e34b214..456a5fe91c 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h @@ -41,7 +41,11 @@ namespace O3DE::ProjectManager GemModel* GetGemModel() const { return m_gemModel; } DownloadController* GetDownloadController() const { return m_downloadController; } + private slots: + void HandleOpenGemRepo(); + private: + void FillModel(const QString& projectPath); GemListView* m_gemListView = nullptr; diff --git a/Code/Tools/ProjectManager/Source/ScreenWidget.h b/Code/Tools/ProjectManager/Source/ScreenWidget.h index 9563fc2f6a..148dcdb8c8 100644 --- a/Code/Tools/ProjectManager/Source/ScreenWidget.h +++ b/Code/Tools/ProjectManager/Source/ScreenWidget.h @@ -47,6 +47,14 @@ namespace O3DE::ProjectManager return tr("Missing"); } + virtual bool ContainsScreen([[maybe_unused]] ProjectManagerScreen screen) + { + return false; + } + virtual void GoToScreen([[maybe_unused]] ProjectManagerScreen screen) + { + } + //! Notify this screen it is the current screen virtual void NotifyCurrentScreen() { @@ -55,7 +63,7 @@ namespace O3DE::ProjectManager signals: void ChangeScreenRequest(ProjectManagerScreen screen); - void GotoPreviousScreenRequest(); + void GoToPreviousScreenRequest(); void ResetScreenRequest(ProjectManagerScreen screen); void NotifyCurrentProject(const QString& projectPath); void NotifyBuildProject(const ProjectInfo& projectInfo); diff --git a/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp b/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp index df0bdb29f4..314765def0 100644 --- a/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp +++ b/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp @@ -83,11 +83,28 @@ namespace O3DE::ProjectManager bool ScreensCtrl::ForceChangeToScreen(ProjectManagerScreen screen, bool addVisit) { + ScreenWidget* newScreen = nullptr; + const auto iterator = m_screenMap.find(screen); if (iterator != m_screenMap.end()) + { + newScreen = iterator.value(); + } + else + { + // Check if screen is contained by another screen + for (ScreenWidget* checkingScreen : m_screenMap) + { + if (checkingScreen->ContainsScreen(screen)) + { + newScreen = checkingScreen; + break; + } + } + } + if (newScreen) { ScreenWidget* currentScreen = GetCurrentScreen(); - ScreenWidget* newScreen = iterator.value(); if (currentScreen != newScreen) { @@ -109,6 +126,11 @@ namespace O3DE::ProjectManager newScreen->NotifyCurrentScreen(); + if (iterator == m_screenMap.end()) + { + newScreen->GoToScreen(screen); + } + return true; } } @@ -116,7 +138,7 @@ namespace O3DE::ProjectManager return false; } - bool ScreensCtrl::GotoPreviousScreen() + bool ScreensCtrl::GoToPreviousScreen() { if (!m_screenVisitOrder.isEmpty()) { @@ -171,7 +193,7 @@ namespace O3DE::ProjectManager m_screenMap.insert(screen, newScreen); connect(newScreen, &ScreenWidget::ChangeScreenRequest, this, &ScreensCtrl::ChangeToScreen); - connect(newScreen, &ScreenWidget::GotoPreviousScreenRequest, this, &ScreensCtrl::GotoPreviousScreen); + connect(newScreen, &ScreenWidget::GoToPreviousScreenRequest, this, &ScreensCtrl::GoToPreviousScreen); connect(newScreen, &ScreenWidget::ResetScreenRequest, this, &ScreensCtrl::ResetScreen); connect(newScreen, &ScreenWidget::NotifyCurrentProject, this, &ScreensCtrl::NotifyCurrentProject); connect(newScreen, &ScreenWidget::NotifyBuildProject, this, &ScreensCtrl::NotifyBuildProject); diff --git a/Code/Tools/ProjectManager/Source/ScreensCtrl.h b/Code/Tools/ProjectManager/Source/ScreensCtrl.h index 7132d64dd0..ab69a09ea3 100644 --- a/Code/Tools/ProjectManager/Source/ScreensCtrl.h +++ b/Code/Tools/ProjectManager/Source/ScreensCtrl.h @@ -41,7 +41,7 @@ namespace O3DE::ProjectManager public slots: bool ChangeToScreen(ProjectManagerScreen screen); bool ForceChangeToScreen(ProjectManagerScreen screen, bool addVisit = true); - bool GotoPreviousScreen(); + bool GoToPreviousScreen(); void ResetScreen(ProjectManagerScreen screen); void ResetAllScreens(); void DeleteScreen(ProjectManagerScreen screen); diff --git a/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.cpp b/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.cpp index fd2ebf340f..1c8f9a6931 100644 --- a/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.cpp +++ b/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.cpp @@ -40,6 +40,10 @@ namespace O3DE::ProjectManager m_updateSettingsScreen = new UpdateProjectSettingsScreen(); m_gemCatalogScreen = new GemCatalogScreen(); + connect(m_gemCatalogScreen, &ScreenWidget::ChangeScreenRequest, this, [this](ProjectManagerScreen screen){ + emit ChangeScreenRequest(screen); + }); + m_stack = new QStackedWidget(this); m_stack->setObjectName("body"); m_stack->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding)); @@ -118,7 +122,7 @@ namespace O3DE::ProjectManager { if (UpdateProjectSettings(true)) { - emit GotoPreviousScreenRequest(); + emit GoToPreviousScreenRequest(); } } } From d5431653aaf8f2c81f609f38638a8367f15a3aa8 Mon Sep 17 00:00:00 2001 From: Adi Bar-Lev Date: Fri, 22 Oct 2021 09:21:06 -0400 Subject: [PATCH 22/26] Hair and Tools Pipeline bug fixes (#4902) - Fixed fallback connections for hair pipeline to allow disabling the parent pass hierarchy when not required - Renamed the Thumbnail pipeline to be used as generic minimal tools pipeline - Reused the Tools pipeline for the preview renderer - minimal FPs and passes Remark: - The tools pipeline should have folloup submits for reducing passes to minimal required render passes Signed-off-by: Adi-Amazon Co-authored-by: Adi-Amazon --- .../Assets/Passes/PassTemplates.azasset | 8 +++---- ...mbnailPipeline.pass => ToolsPipeline.pass} | 2 +- ...pass => ToolsPipelineRenderToTexture.pass} | 4 ++-- .../atom_feature_common_asset_files.cmake | 4 ++-- .../PreviewRenderer/PreviewRenderer.cpp | 2 +- .../Assets/Passes/HairParentPass.pass | 6 +++++ .../Assets/Passes/HairParentShortCutPass.pass | 17 +++++++++---- .../Code/Rendering/HairFeatureProcessor.cpp | 24 ++++--------------- 8 files changed, 33 insertions(+), 34 deletions(-) rename Gems/Atom/Feature/Common/Assets/Passes/{ThumbnailPipeline.pass => ToolsPipeline.pass} (99%) rename Gems/Atom/Feature/Common/Assets/Passes/{ThumbnailPipelineRenderToTexture.pass => ToolsPipelineRenderToTexture.pass} (88%) diff --git a/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset b/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset index 7770b326a6..81b7d36484 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset +++ b/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset @@ -469,12 +469,12 @@ "Path": "Passes/OpaqueParent.pass" }, { - "Name": "ThumbnailPipeline", - "Path": "Passes/ThumbnailPipeline.pass" + "Name": "ToolsPipeline", + "Path": "Passes/ToolsPipeline.pass" }, { - "Name": "ThumbnailPipelineRenderToTexture", - "Path": "Passes/ThumbnailPipelineRenderToTexture.pass" + "Name": "ToolsPipelineRenderToTexture", + "Path": "Passes/ToolsPipelineRenderToTexture.pass" }, { "Name": "TransparentParentTemplate", diff --git a/Gems/Atom/Feature/Common/Assets/Passes/ThumbnailPipeline.pass b/Gems/Atom/Feature/Common/Assets/Passes/ToolsPipeline.pass similarity index 99% rename from Gems/Atom/Feature/Common/Assets/Passes/ThumbnailPipeline.pass rename to Gems/Atom/Feature/Common/Assets/Passes/ToolsPipeline.pass index 932b6ac435..51c169348b 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/ThumbnailPipeline.pass +++ b/Gems/Atom/Feature/Common/Assets/Passes/ToolsPipeline.pass @@ -4,7 +4,7 @@ "ClassName": "PassAsset", "ClassData": { "PassTemplate": { - "Name": "ThumbnailPipeline", + "Name": "ToolsPipeline", "PassClass": "ParentPass", "Slots": [ { diff --git a/Gems/Atom/Feature/Common/Assets/Passes/ThumbnailPipelineRenderToTexture.pass b/Gems/Atom/Feature/Common/Assets/Passes/ToolsPipelineRenderToTexture.pass similarity index 88% rename from Gems/Atom/Feature/Common/Assets/Passes/ThumbnailPipelineRenderToTexture.pass rename to Gems/Atom/Feature/Common/Assets/Passes/ToolsPipelineRenderToTexture.pass index 11e2cb717a..b98ec46d0e 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/ThumbnailPipelineRenderToTexture.pass +++ b/Gems/Atom/Feature/Common/Assets/Passes/ToolsPipelineRenderToTexture.pass @@ -4,7 +4,7 @@ "ClassName": "PassAsset", "ClassData": { "PassTemplate": { - "Name": "ThumbnailPipelineRenderToTexture", + "Name": "ToolsPipelineRenderToTexture", "PassClass": "RenderToTexturePass", "PassData": { "$type": "RenderToTexturePassData", @@ -15,7 +15,7 @@ "PassRequests": [ { "Name": "Pipeline", - "TemplateName": "ThumbnailPipeline", + "TemplateName": "ToolsPipeline", "Connections": [ { "LocalSlot": "SwapChainOutput", diff --git a/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake b/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake index 123e5da7ba..e13465307a 100644 --- a/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake +++ b/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake @@ -161,8 +161,8 @@ set(FILES Passes/LutGeneration.pass Passes/MainPipeline.pass Passes/MainPipelineRenderToTexture.pass - Passes/ThumbnailPipeline.pass - Passes/ThumbnailPipelineRenderToTexture.pass + Passes/ToolsPipeline.pass + Passes/ToolsPipelineRenderToTexture.pass Passes/MeshMotionVector.pass Passes/ModulateTexture.pass Passes/MorphTarget.pass diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRenderer.cpp b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRenderer.cpp index a0d2034082..2b39a87623 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRenderer.cpp +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRenderer.cpp @@ -61,7 +61,7 @@ namespace AtomToolsFramework AZ::RPI::RenderPipelineDescriptor pipelineDesc; pipelineDesc.m_mainViewTagName = "MainCamera"; pipelineDesc.m_name = pipelineName; - pipelineDesc.m_rootPassTemplate = "MainPipelineRenderToTexture"; + pipelineDesc.m_rootPassTemplate = "ToolsPipelineRenderToTexture"; // We have to set the samples to 4 to match the pipeline passes' setting, otherwise it may lead to device lost issue // [GFX TODO] [ATOM-13551] Default value sand validation required to prevent pipeline crash and device lost diff --git a/Gems/AtomTressFX/Assets/Passes/HairParentPass.pass b/Gems/AtomTressFX/Assets/Passes/HairParentPass.pass index 6ae8b9526e..c8c90e24f7 100644 --- a/Gems/AtomTressFX/Assets/Passes/HairParentPass.pass +++ b/Gems/AtomTressFX/Assets/Passes/HairParentPass.pass @@ -71,6 +71,12 @@ } } ], + "FallbackConnections": [ + { + "Input": "DepthLinearInput", + "Output": "DepthLinear" + } + ], "PassRequests": [ { "Name": "HairGlobalShapeConstraintsComputePass", diff --git a/Gems/AtomTressFX/Assets/Passes/HairParentShortCutPass.pass b/Gems/AtomTressFX/Assets/Passes/HairParentShortCutPass.pass index 7322611e2a..83f9f0d432 100644 --- a/Gems/AtomTressFX/Assets/Passes/HairParentShortCutPass.pass +++ b/Gems/AtomTressFX/Assets/Passes/HairParentShortCutPass.pass @@ -12,7 +12,8 @@ "SlotType": "InputOutput", "ScopeAttachmentUsage": "RenderTarget" }, - { // used for copy from MSAA to regular RT + { + // used for copy from MSAA to regular RT "Name": "RenderTargetInputOnly", "SlotType": "Input", "ScopeAttachmentUsage": "Shader" @@ -29,7 +30,7 @@ // If DepthLinear is not availbale - connect to another viewport (non MSAA) image. { "Name": "DepthLinearInput", - "SlotType": "InputOutput" + "SlotType": "Input" }, { "Name": "DepthLinear", @@ -71,6 +72,12 @@ } } ], + "FallbackConnections": [ + { + "Input": "DepthLinearInput", + "Output": "DepthLinear" + } + ], "PassRequests": [ { "Name": "HairGlobalShapeConstraintsComputePass", @@ -257,7 +264,8 @@ "Attachment": "HairColorRenderTarget" } }, - { // The final render target - this is MSAA mode RT - would it be cheaper to + { + // The final render target - this is MSAA mode RT - would it be cheaper to // use non-MSAA and then copy? "LocalSlot": "RenderTargetInputOutput", "AttachmentRef": { @@ -340,7 +348,8 @@ "TemplateName": "HairShortCutResolveColorPassTemplate", "Enabled": true, "Connections": [ - { // The final render target - this is MSAA mode RT - would it be cheaper to + { + // The final render target - this is MSAA mode RT - would it be cheaper to // use non-MSAA and then copy? "LocalSlot": "RenderTargetInputOutput", "AttachmentRef": { diff --git a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp index b7c6bbce13..a0be18f0e2 100644 --- a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp +++ b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp @@ -144,25 +144,11 @@ namespace AZ void HairFeatureProcessor::EnablePasses([[maybe_unused]] bool enable) { - return; - - // [To Do] - This part should be enabled (remove the return) to reduce overhead - // when Hair is disabled / doesn't exist in the scene. - // Currently it might break features such as fog that depend on the output and for some - // reason doesn't quite work for ShortCut. - // The current overhead is minimal (< 0.1 msec) and this Gem is disabled by default. -/* - if (!m_initialized) - { - return; - } - RPI::Ptr desiredPass = m_renderPipeline->GetRootPass()->FindPassByNameRecursive(HairParentPassName); if (desiredPass) { desiredPass->SetEnabled(enable); } -*/ } bool HairFeatureProcessor::RemoveHairRenderObject(Data::Instance renderObject) @@ -184,15 +170,13 @@ namespace AZ void HairFeatureProcessor::UpdateHairSkinning() { - // Copying CPU side m_SimCB content to the GPU buffer (matrices, wind parameters..) - - for (auto objIter = m_hairRenderObjects.begin(); objIter != m_hairRenderObjects.end(); ++objIter) + // Copying CPU side m_SimCB content to the GPU buffer (matrices, wind parameters..) + for (auto& hairRenderObject : m_hairRenderObjects) { - if (!objIter->get()->IsEnabled()) + if (hairRenderObject->IsEnabled()) { - return; + hairRenderObject->Update(); } - objIter->get()->Update(); } } From a2592e9ff8ecab11c0b11048d74ca68f018f7dbf Mon Sep 17 00:00:00 2001 From: AMZN-Olex <5432499+AMZN-Olex@users.noreply.github.com> Date: Fri, 22 Oct 2021 09:43:52 -0400 Subject: [PATCH 23/26] Correction in set hierarchy entity logic. Signed-off-by: AMZN-Olex <5432499+AMZN-Olex@users.noreply.github.com> --- .../NetworkHierarchyChildComponent.cpp | 24 +++++----- .../NetworkHierarchyRootComponent.cpp | 44 +++++++++---------- 2 files changed, 32 insertions(+), 36 deletions(-) diff --git a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyChildComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyChildComponent.cpp index 37125a88b9..59782b1f4d 100644 --- a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyChildComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyChildComponent.cpp @@ -148,25 +148,21 @@ namespace Multiplayer m_networkHierarchyChangedEvent.Signal(m_rootEntity->GetId()); } } - else + else if ((previousHierarchyRoot && m_rootEntity == previousHierarchyRoot) || !previousHierarchyRoot) { - if (m_rootEntity == previousHierarchyRoot || !previousHierarchyRoot) - { - m_rootEntity = nullptr; + m_rootEntity = nullptr; - if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) - { - NetworkHierarchyChildComponentController* controller = static_cast(GetController()); - controller->SetHierarchyRoot(InvalidNetEntityId); - } + if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) + { + NetworkHierarchyChildComponentController* controller = static_cast(GetController()); + controller->SetHierarchyRoot(InvalidNetEntityId); + } - GetNetBindComponent()->SetOwningConnectionId(m_previousOwningConnectionId); - m_networkHierarchyLeaveEvent.Signal(); + GetNetBindComponent()->SetOwningConnectionId(m_previousOwningConnectionId); + m_networkHierarchyLeaveEvent.Signal(); - NotifyChildrenHierarchyDisbanded(); - } + NotifyChildrenHierarchyDisbanded(); } - } void NetworkHierarchyChildComponent::SetOwningConnectionId(AzNetworking::ConnectionId connectionId) diff --git a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp index 6858242cd4..76f4bddb1a 100644 --- a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp @@ -325,36 +325,36 @@ namespace Multiplayer { if (newHierarchyRoot) { - m_rootEntity = newHierarchyRoot; - - if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) - { - NetworkHierarchyChildComponentController* controller = static_cast(GetController()); - - const NetEntityId netRootId = GetNetworkEntityManager()->GetNetEntityIdById(m_rootEntity->GetId()); - controller->SetHierarchyRoot(netRootId); - } - - GetNetBindComponent()->SetOwningConnectionId(m_rootEntity->FindComponent()->GetOwningConnectionId()); - } - else - { - if (previousHierarchyRoot && m_rootEntity == previousHierarchyRoot || !previousHierarchyRoot) + if (m_rootEntity != newHierarchyRoot) { - m_rootEntity = nullptr; + m_rootEntity = newHierarchyRoot; if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) { - NetworkHierarchyChildComponentController* controller = static_cast(GetController()); - - controller->SetHierarchyRoot(InvalidNetEntityId); + NetworkHierarchyRootComponentController* controller = static_cast(GetController()); + const NetEntityId netRootId = GetNetworkEntityManager()->GetNetEntityIdById(m_rootEntity->GetId()); + controller->SetHierarchyRoot(netRootId); } - GetNetBindComponent()->SetOwningConnectionId(m_previousOwningConnectionId); + GetNetBindComponent()->SetOwningConnectionId(m_rootEntity->FindComponent()->GetOwningConnectionId()); + m_networkHierarchyChangedEvent.Signal(m_rootEntity->GetId()); + } + } + else if ((previousHierarchyRoot && m_rootEntity == previousHierarchyRoot) || !previousHierarchyRoot) + { + m_rootEntity = nullptr; - // We lost the parent hierarchical entity, so as a root we need to re-build our own hierarchy. - RebuildHierarchy(); + if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) + { + NetworkHierarchyRootComponentController* controller = static_cast(GetController()); + controller->SetHierarchyRoot(InvalidNetEntityId); } + + GetNetBindComponent()->SetOwningConnectionId(m_previousOwningConnectionId); + m_networkHierarchyLeaveEvent.Signal(); + + // We lost the parent hierarchical entity, so as a root we need to re-build our own hierarchy. + RebuildHierarchy(); } } From 3d67be162ce584b7fe4a7d8a8a37032117b25831 Mon Sep 17 00:00:00 2001 From: John Jones-Steele <82226755+jjjoness@users.noreply.github.com> Date: Fri, 22 Oct 2021 16:19:36 +0100 Subject: [PATCH 24/26] Terrain Physics Heightfield support * New Heightfield Components Signed-off-by: John Jones-Steele * Misc PR fixes * Fixed linux build failure from bad #include * Renamed "Terrain Physics Collider" to "Terrain Physics Heightfield Collider" per physics team feedback * Fixed 1/5 -> 1/4 typo in a comment * Added missing member copies in HeightfieldShapeConfiguration Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Addressed PR feedback Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Changes from review Signed-off-by: John Jones-Steele * Remove tabs accidently added Signed-off-by: John Jones-Steele * Fixed overly complicated scaling math. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Added comments to make it more obvious what's happening on CreateEnd / DestroyBegin. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Moved Heightfield CreatePxGeometryFromConfig into its own function Signed-off-by: John Jones-Steele Co-authored-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> --- .../Physics/HeightfieldProviderBus.h | 97 ++ .../Mocks/MockHeightfieldProviderBus.h | 33 + .../Physics/ShapeConfiguration.cpp | 122 +++ .../AzFramework/Physics/ShapeConfiguration.h | 52 ++ .../AzFramework/Physics/SystemBus.h | 4 + .../AzFramework/AzFramework/Physics/Utils.cpp | 1 + .../Physics/physics_mock_files.cmake | 11 + .../AzFramework/azframework_files.cmake | 2 + Code/Framework/AzFramework/CMakeLists.txt | 3 +- Gems/Blast/Code/Tests/Mocks/BlastMocks.h | 1 + .../Code/Tests/Mocks/PhysicsSystem.h | 1 + Gems/GradientSignal/Code/CMakeLists.txt | 12 +- .../Ebuses/MockGradientRequestBus.h | 34 + .../Code/gradientsignal_mocks_files.cmake | 11 + .../Shape/AxisAlignedBoxShapeComponent.h | 6 - .../LmbrCentral/Shape/BoxShapeComponentBus.h | 6 + Gems/PhysX/Code/Editor/DebugDraw.cpp | 12 +- Gems/PhysX/Code/Editor/DebugDraw.h | 10 +- .../Code/Include/PhysX/SystemComponentBus.h | 13 +- .../Code/Source/ComponentDescriptors.cpp | 2 + .../Source/EditorComponentDescriptors.cpp | 2 + .../EditorHeightfieldColliderComponent.cpp | 341 +++++++ .../EditorHeightfieldColliderComponent.h | 104 +++ .../Source/HeightfieldColliderComponent.cpp | 365 ++++++++ .../Source/HeightfieldColliderComponent.h | 104 +++ Gems/PhysX/Code/Source/SystemComponent.cpp | 24 + Gems/PhysX/Code/Source/SystemComponent.h | 2 + Gems/PhysX/Code/Source/Utils.cpp | 193 ++++ Gems/PhysX/Code/Source/Utils.h | 2 + Gems/PhysX/Code/physx_editor_files.cmake | 2 + Gems/PhysX/Code/physx_files.cmake | 2 + Gems/Terrain/Code/CMakeLists.txt | 1 + Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h | 42 +- .../MockTerrainAreaSurfaceRequestBus.h | 34 + .../Mocks/Terrain/MockTerrainLayerSpawner.h | 39 + .../TerrainHeightGradientListComponent.cpp | 2 +- .../TerrainLayerSpawnerComponent.cpp | 12 +- .../Components/TerrainLayerSpawnerComponent.h | 6 - .../TerrainPhysicsColliderComponent.cpp | 321 +++++++ .../TerrainPhysicsColliderComponent.h | 91 ++ .../EditorTerrainPhysicsColliderComponent.cpp | 24 + .../EditorTerrainPhysicsColliderComponent.h | 32 + .../Code/Source/EditorTerrainModule.cpp | 2 + Gems/Terrain/Code/Source/TerrainModule.cpp | 2 + Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp | 7 +- .../Tests/TerrainHeightGradientListTests.cpp | 146 +++ .../Tests/TerrainPhysicsColliderTests.cpp | 292 ++++++ .../Tests/TerrainSurfaceGradientListTests.cpp | 129 +++ Gems/Terrain/Code/Tests/TerrainSystemTest.cpp | 839 ++++++++++-------- .../Code/terrain_editor_shared_files.cmake | 2 + Gems/Terrain/Code/terrain_files.cmake | 2 + Gems/Terrain/Code/terrain_mocks_files.cmake | 2 + Gems/Terrain/Code/terrain_tests_files.cmake | 3 + 53 files changed, 3193 insertions(+), 411 deletions(-) create mode 100644 Code/Framework/AzFramework/AzFramework/Physics/HeightfieldProviderBus.h create mode 100644 Code/Framework/AzFramework/AzFramework/Physics/Mocks/MockHeightfieldProviderBus.h create mode 100644 Code/Framework/AzFramework/AzFramework/Physics/physics_mock_files.cmake create mode 100644 Gems/GradientSignal/Code/Mocks/GradientSignal/Ebuses/MockGradientRequestBus.h create mode 100644 Gems/GradientSignal/Code/gradientsignal_mocks_files.cmake create mode 100644 Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.cpp create mode 100644 Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.h create mode 100644 Gems/PhysX/Code/Source/HeightfieldColliderComponent.cpp create mode 100644 Gems/PhysX/Code/Source/HeightfieldColliderComponent.h create mode 100644 Gems/Terrain/Code/Mocks/Terrain/MockTerrainAreaSurfaceRequestBus.h create mode 100644 Gems/Terrain/Code/Mocks/Terrain/MockTerrainLayerSpawner.h create mode 100644 Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.cpp create mode 100644 Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.h create mode 100644 Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.cpp create mode 100644 Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.h create mode 100644 Gems/Terrain/Code/Tests/TerrainHeightGradientListTests.cpp create mode 100644 Gems/Terrain/Code/Tests/TerrainPhysicsColliderTests.cpp create mode 100644 Gems/Terrain/Code/Tests/TerrainSurfaceGradientListTests.cpp diff --git a/Code/Framework/AzFramework/AzFramework/Physics/HeightfieldProviderBus.h b/Code/Framework/AzFramework/AzFramework/Physics/HeightfieldProviderBus.h new file mode 100644 index 0000000000..73523ee1ba --- /dev/null +++ b/Code/Framework/AzFramework/AzFramework/Physics/HeightfieldProviderBus.h @@ -0,0 +1,97 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace Physics +{ + //! The QuadMeshType specifies the property of the heightfield quad. + enum class QuadMeshType : uint8_t + { + SubdivideUpperLeftToBottomRight, //!< Subdivide the quad, from upper left to bottom right |\|, into two triangles. + SubdivideBottomLeftToUpperRight, //!< Subdivide the quad, from bottom left to upper right |/|, into two triangles. + Hole //!< The quad should be treated as a hole in the heightfield. + }; + + struct HeightMaterialPoint + { + float m_height{ 0.0f }; //!< Holds the height of this point in the heightfield relative to the heightfield entity location. + QuadMeshType m_quadMeshType{ QuadMeshType::SubdivideUpperLeftToBottomRight }; //!< By default, create two triangles like this |\|, where this point is in the upper left corner. + uint8_t m_materialIndex{ 0 }; //!< The surface material index for the upper left corner of this quad. + uint16_t m_padding{ 0 }; //!< available for future use. + }; + + //! An interface to provide heightfield values. + class HeightfieldProviderRequests + : public AZ::ComponentBus + { + public: + //! Returns the distance between each height in the map. + //! @return Vector containing Column Spacing, Rows Spacing. + virtual AZ::Vector2 GetHeightfieldGridSpacing() const = 0; + + //! Returns the height field gridsize. + //! @param numColumns contains the size of the grid in the x direction. + //! @param numRows contains the size of the grid in the y direction. + virtual void GetHeightfieldGridSize(int32_t& numColumns, int32_t& numRows) const = 0; + + //! Returns the height field min and max height bounds. + //! @param minHeightBounds contains the minimum height that the heightfield can contain. + //! @param maxHeightBounds contains the maximum height that the heightfield can contain. + virtual void GetHeightfieldHeightBounds(float& minHeightBounds, float& maxHeightBounds) const = 0; + + //! Returns the AABB of the heightfield. + //! This is provided separately from the shape AABB because the heightfield might choose to modify the AABB bounds. + //! @return AABB of the heightfield. + virtual AZ::Aabb GetHeightfieldAabb() const = 0; + + //! Returns the world transform for the heightfield. + //! This is provided separately from the entity transform because the heightfield might want to clear out the rotation or scale. + //! @return world transform that should be used with the heightfield data. + virtual AZ::Transform GetHeightfieldTransform() const = 0; + + //! Returns the list of materials used by the height field. + //! @return returns a vector of all materials. + virtual AZStd::vector GetMaterialList() const = 0; + + //! Returns the list of heights used by the height field. + //! @return the rows*columns vector of the heights. + virtual AZStd::vector GetHeights() const = 0; + + //! Returns the list of heights and materials used by the height field. + //! @return the rows*columns vector of the heights and materials. + virtual AZStd::vector GetHeightsAndMaterials() const = 0; + }; + + using HeightfieldProviderRequestsBus = AZ::EBus; + + //! Broadcasts notifications when heightfield data changes - heightfield providers implement HeightfieldRequests bus. + class HeightfieldProviderNotifications + : public AZ::ComponentBus + { + public: + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple; + + //! Called whenever the heightfield data changes. + //! @param the AABB of the area of data that changed. + virtual void OnHeightfieldDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion) + { + } + + protected: + ~HeightfieldProviderNotifications() = default; + }; + + using HeightfieldProviderNotificationBus = AZ::EBus; +} // namespace Physics diff --git a/Code/Framework/AzFramework/AzFramework/Physics/Mocks/MockHeightfieldProviderBus.h b/Code/Framework/AzFramework/AzFramework/Physics/Mocks/MockHeightfieldProviderBus.h new file mode 100644 index 0000000000..221c52258d --- /dev/null +++ b/Code/Framework/AzFramework/AzFramework/Physics/Mocks/MockHeightfieldProviderBus.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include + +#include + +namespace UnitTest +{ + class MockHeightfieldProviderNotificationBusListener + : private Physics::HeightfieldProviderNotificationBus::Handler + { + public: + MockHeightfieldProviderNotificationBusListener(AZ::EntityId entityid) + { + Physics::HeightfieldProviderNotificationBus::Handler::BusConnect(entityid); + } + + ~MockHeightfieldProviderNotificationBusListener() + { + Physics::HeightfieldProviderNotificationBus::Handler::BusDisconnect(); + } + + MOCK_METHOD1(OnHeightfieldDataChanged, void(const AZ::Aabb&)); + }; +} // namespace UnitTest diff --git a/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.cpp b/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.cpp index e90c9d4eed..9bf00d9707 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.cpp +++ b/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.cpp @@ -37,6 +37,7 @@ namespace Physics REFLECT_SHAPETYPE_ENUM_VALUE(Sphere); REFLECT_SHAPETYPE_ENUM_VALUE(Cylinder); REFLECT_SHAPETYPE_ENUM_VALUE(PhysicsAsset); + REFLECT_SHAPETYPE_ENUM_VALUE(Heightfield); #undef REFLECT_SHAPETYPE_ENUM_VALUE } @@ -305,4 +306,125 @@ namespace Physics m_cachedNativeMesh = nullptr; } } + + void HeightfieldShapeConfiguration::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext + ->RegisterGenericType>(); + + serializeContext->Class() + ->Version(1); + } + } + + HeightfieldShapeConfiguration::~HeightfieldShapeConfiguration() + { + SetCachedNativeHeightfield(nullptr); + } + + HeightfieldShapeConfiguration::HeightfieldShapeConfiguration(const HeightfieldShapeConfiguration& other) + : ShapeConfiguration(other) + , m_gridResolution(other.m_gridResolution) + , m_numColumns(other.m_numColumns) + , m_numRows(other.m_numRows) + , m_samples(other.m_samples) + , m_minHeightBounds(other.m_minHeightBounds) + , m_maxHeightBounds(other.m_maxHeightBounds) + , m_cachedNativeHeightfield(nullptr) + { + } + + HeightfieldShapeConfiguration& HeightfieldShapeConfiguration::operator=(const HeightfieldShapeConfiguration& other) + { + ShapeConfiguration::operator=(other); + + m_gridResolution = other.m_gridResolution; + m_numColumns = other.m_numColumns; + m_numRows = other.m_numRows; + m_samples = other.m_samples; + m_minHeightBounds = other.m_minHeightBounds; + m_maxHeightBounds = other.m_maxHeightBounds; + + // Prevent raw pointer from being copied + m_cachedNativeHeightfield = nullptr; + + return *this; + } + + void* HeightfieldShapeConfiguration::GetCachedNativeHeightfield() const + { + return m_cachedNativeHeightfield; + } + + void HeightfieldShapeConfiguration::SetCachedNativeHeightfield(void* cachedNativeHeightfield) const + { + if (m_cachedNativeHeightfield) + { + Physics::SystemRequestBus::Broadcast(&Physics::SystemRequests::ReleaseNativeHeightfieldObject, m_cachedNativeHeightfield); + } + + m_cachedNativeHeightfield = cachedNativeHeightfield; + } + + AZ::Vector2 HeightfieldShapeConfiguration::GetGridResolution() const + { + return m_gridResolution; + } + + void HeightfieldShapeConfiguration::SetGridResolution(const AZ::Vector2& gridResolution) + { + m_gridResolution = gridResolution; + } + + int32_t HeightfieldShapeConfiguration::GetNumColumns() const + { + return m_numColumns; + } + + void HeightfieldShapeConfiguration::SetNumColumns(int32_t numColumns) + { + m_numColumns = numColumns; + } + + int32_t HeightfieldShapeConfiguration::GetNumRows() const + { + return m_numRows; + } + + void HeightfieldShapeConfiguration::SetNumRows(int32_t numRows) + { + m_numRows = numRows; + } + + const AZStd::vector& HeightfieldShapeConfiguration::GetSamples() const + { + return m_samples; + } + + void HeightfieldShapeConfiguration::SetSamples(const AZStd::vector& samples) + { + m_samples = samples; + } + + float HeightfieldShapeConfiguration::GetMinHeightBounds() const + { + return m_minHeightBounds; + } + + void HeightfieldShapeConfiguration::SetMinHeightBounds(float minBounds) + { + m_minHeightBounds = minBounds; + } + + float HeightfieldShapeConfiguration::GetMaxHeightBounds() const + { + return m_maxHeightBounds; + } + + void HeightfieldShapeConfiguration::SetMaxHeightBounds(float maxBounds) + { + m_maxHeightBounds = maxBounds; + } } diff --git a/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.h b/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.h index b40a8b1edd..3bcf336af6 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.h +++ b/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.h @@ -9,10 +9,13 @@ #pragma once #include +#include #include #include #include +#include + namespace Physics { /// Used to identify shape configuration type from base class. @@ -27,6 +30,7 @@ namespace Physics Native, ///< Native shape configuration if user wishes to bypass generic shape configurations. PhysicsAsset, ///< Shapes configured in the asset. CookedMesh, ///< Stores a blob of mesh data cooked for the specific engine. + Heightfield ///< Interacts with the physics system heightfield }; class ShapeConfiguration @@ -196,4 +200,52 @@ namespace Physics mutable void* m_cachedNativeMesh = nullptr; }; + class HeightfieldShapeConfiguration + : public ShapeConfiguration + { + public: + AZ_CLASS_ALLOCATOR(HeightfieldShapeConfiguration, AZ::SystemAllocator, 0); + AZ_RTTI(HeightfieldShapeConfiguration, "{8DF47C83-D2A9-4E7C-8620-5E173E43C0B3}", ShapeConfiguration); + static void Reflect(AZ::ReflectContext* context); + HeightfieldShapeConfiguration() = default; + HeightfieldShapeConfiguration(const HeightfieldShapeConfiguration&); + HeightfieldShapeConfiguration& operator=(const HeightfieldShapeConfiguration&); + ~HeightfieldShapeConfiguration(); + + ShapeType GetShapeType() const override + { + return ShapeType::Heightfield; + } + + void* GetCachedNativeHeightfield() const; + void SetCachedNativeHeightfield(void* cachedNativeHeightfield) const; + AZ::Vector2 GetGridResolution() const; + void SetGridResolution(const AZ::Vector2& gridSpacing); + int32_t GetNumColumns() const; + void SetNumColumns(int32_t numColumns); + int32_t GetNumRows() const; + void SetNumRows(int32_t numRows); + const AZStd::vector& GetSamples() const; + void SetSamples(const AZStd::vector& samples); + float GetMinHeightBounds() const; + void SetMinHeightBounds(float minBounds); + float GetMaxHeightBounds() const; + void SetMaxHeightBounds(float maxBounds); + + private: + //! The number of meters between each heightfield sample. + AZ::Vector2 m_gridResolution{ 1.0f }; + //! The number of columns in the heightfield sample grid. + int32_t m_numColumns{ 0 }; + //! The number of rows in the heightfield sample grid. + int32_t m_numRows{ 0 }; + //! The minimum and maximum heights that can be used by this heightfield. + //! This can be used by the physics system to choose a more optimal heightfield data type internally (ex: int16, uint8) + float m_minHeightBounds{AZStd::numeric_limits::lowest()}; + float m_maxHeightBounds{AZStd::numeric_limits::max()}; + //! The grid of sample points for the heightfield. + AZStd::vector m_samples; + //! An optional storage pointer for the physics system to cache its native heightfield representation. + mutable void* m_cachedNativeHeightfield{ nullptr }; + }; } // namespace Physics diff --git a/Code/Framework/AzFramework/AzFramework/Physics/SystemBus.h b/Code/Framework/AzFramework/AzFramework/Physics/SystemBus.h index 868a27ed36..33313d612b 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/SystemBus.h +++ b/Code/Framework/AzFramework/AzFramework/Physics/SystemBus.h @@ -132,6 +132,10 @@ namespace Physics virtual AZStd::shared_ptr CreateMaterial(const Physics::MaterialConfiguration& materialConfiguration) = 0; + /// Releases the height field object created by the physics backend. + /// @param nativeHeightfieldObject Pointer to the height field object. + virtual void ReleaseNativeHeightfieldObject(void* nativeHeightfieldObject) = 0; + /// Releases the mesh object created by the physics backend. /// @param nativeMeshObject Pointer to the mesh object. virtual void ReleaseNativeMeshObject(void* nativeMeshObject) = 0; diff --git a/Code/Framework/AzFramework/AzFramework/Physics/Utils.cpp b/Code/Framework/AzFramework/AzFramework/Physics/Utils.cpp index 3dcb37c541..d989847ae8 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/Utils.cpp +++ b/Code/Framework/AzFramework/AzFramework/Physics/Utils.cpp @@ -107,6 +107,7 @@ namespace Physics PhysicsAssetShapeConfiguration::Reflect(context); NativeShapeConfiguration::Reflect(context); CookedMeshShapeConfiguration::Reflect(context); + HeightfieldShapeConfiguration::Reflect(context); AzPhysics::SystemInterface::Reflect(context); AzPhysics::Scene::Reflect(context); AzPhysics::CollisionLayer::Reflect(context); diff --git a/Code/Framework/AzFramework/AzFramework/Physics/physics_mock_files.cmake b/Code/Framework/AzFramework/AzFramework/Physics/physics_mock_files.cmake new file mode 100644 index 0000000000..162cc1ea86 --- /dev/null +++ b/Code/Framework/AzFramework/AzFramework/Physics/physics_mock_files.cmake @@ -0,0 +1,11 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(FILES + Mocks/MockHeightfieldProviderBus.h +) \ No newline at end of file diff --git a/Code/Framework/AzFramework/AzFramework/azframework_files.cmake b/Code/Framework/AzFramework/AzFramework/azframework_files.cmake index c5616d84c0..b338dbb0a8 100644 --- a/Code/Framework/AzFramework/AzFramework/azframework_files.cmake +++ b/Code/Framework/AzFramework/AzFramework/azframework_files.cmake @@ -228,6 +228,7 @@ set(FILES Physics/Configuration/SimulatedBodyConfiguration.cpp Physics/Configuration/SystemConfiguration.h Physics/Configuration/SystemConfiguration.cpp + Physics/HeightfieldProviderBus.h Physics/SimulatedBodies/RigidBody.h Physics/SimulatedBodies/RigidBody.cpp Physics/SimulatedBodies/StaticRigidBody.h @@ -251,6 +252,7 @@ set(FILES Physics/Shape.h Physics/ShapeConfiguration.h Physics/ShapeConfiguration.cpp + Physics/HeightfieldProviderBus.h Physics/SystemBus.h Physics/ColliderComponentBus.h Physics/RagdollPhysicsBus.h diff --git a/Code/Framework/AzFramework/CMakeLists.txt b/Code/Framework/AzFramework/CMakeLists.txt index b22586162b..c8eeac5c2d 100644 --- a/Code/Framework/AzFramework/CMakeLists.txt +++ b/Code/Framework/AzFramework/CMakeLists.txt @@ -42,6 +42,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) NAMESPACE AZ FILES_CMAKE Tests/framework_shared_tests_files.cmake + AzFramework/Physics/physics_mock_files.cmake INCLUDE_DIRECTORIES PUBLIC Tests @@ -53,7 +54,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) AZ::AzTest AZ::AzTestShared ) - + if(PAL_TRAIT_BUILD_HOST_TOOLS) ly_add_target( diff --git a/Gems/Blast/Code/Tests/Mocks/BlastMocks.h b/Gems/Blast/Code/Tests/Mocks/BlastMocks.h index 1fe6e35d75..949d3add0d 100644 --- a/Gems/Blast/Code/Tests/Mocks/BlastMocks.h +++ b/Gems/Blast/Code/Tests/Mocks/BlastMocks.h @@ -209,6 +209,7 @@ namespace Blast AZStd::shared_ptr( const Physics::ColliderConfiguration&, const Physics::ShapeConfiguration&)); MOCK_METHOD1(ReleaseNativeMeshObject, void(void*)); + MOCK_METHOD1(ReleaseNativeHeightfieldObject, void(void*)); MOCK_METHOD1(CreateMaterial, AZStd::shared_ptr(const Physics::MaterialConfiguration&)); MOCK_METHOD0(GetDefaultMaterial, AZStd::shared_ptr()); MOCK_METHOD1( diff --git a/Gems/EMotionFX/Code/Tests/Mocks/PhysicsSystem.h b/Gems/EMotionFX/Code/Tests/Mocks/PhysicsSystem.h index 882d198092..6649cea55e 100644 --- a/Gems/EMotionFX/Code/Tests/Mocks/PhysicsSystem.h +++ b/Gems/EMotionFX/Code/Tests/Mocks/PhysicsSystem.h @@ -46,6 +46,7 @@ namespace Physics } MOCK_METHOD2(CreateShape, AZStd::shared_ptr(const Physics::ColliderConfiguration& colliderConfiguration, const Physics::ShapeConfiguration& configuration)); MOCK_METHOD1(ReleaseNativeMeshObject, void(void* nativeMeshObject)); + MOCK_METHOD1(ReleaseNativeHeightfieldObject, void(void* nativeMeshObject)); MOCK_METHOD1(CreateMaterial, AZStd::shared_ptr(const Physics::MaterialConfiguration& materialConfiguration)); MOCK_METHOD3(CookConvexMeshToFile, bool(const AZStd::string& filePath, const AZ::Vector3* vertices, AZ::u32 vertexCount)); MOCK_METHOD3(CookConvexMeshToMemory, bool(const AZ::Vector3* vertices, AZ::u32 vertexCount, AZStd::vector& result)); diff --git a/Gems/GradientSignal/Code/CMakeLists.txt b/Gems/GradientSignal/Code/CMakeLists.txt index 64f565cf99..5b88044116 100644 --- a/Gems/GradientSignal/Code/CMakeLists.txt +++ b/Gems/GradientSignal/Code/CMakeLists.txt @@ -107,7 +107,16 @@ endif() # Tests ################################################################################ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) - + ly_add_target( + NAME GradientSignal.Mocks HEADERONLY + NAMESPACE Gem + FILES_CMAKE + gradientsignal_mocks_files.cmake + INCLUDE_DIRECTORIES + INTERFACE + Mocks + ) + ly_add_target( NAME GradientSignal.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} NAMESPACE Gem @@ -122,6 +131,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) AZ::AzTest Gem::GradientSignal.Static Gem::LmbrCentral + Gem::GradientSignal.Mocks ) ly_add_googletest( NAME Gem::GradientSignal.Tests diff --git a/Gems/GradientSignal/Code/Mocks/GradientSignal/Ebuses/MockGradientRequestBus.h b/Gems/GradientSignal/Code/Mocks/GradientSignal/Ebuses/MockGradientRequestBus.h new file mode 100644 index 0000000000..69e96268f6 --- /dev/null +++ b/Gems/GradientSignal/Code/Mocks/GradientSignal/Ebuses/MockGradientRequestBus.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ +#pragma once + +#include +#include +#include + +namespace UnitTest +{ + class MockGradientRequests + : private GradientSignal::GradientRequestBus::Handler + { + public: + MockGradientRequests(AZ::EntityId entityId) + { + GradientSignal::GradientRequestBus::Handler::BusConnect(entityId); + } + + ~MockGradientRequests() + { + GradientSignal::GradientRequestBus::Handler::BusDisconnect(); + } + + MOCK_CONST_METHOD1(GetValue, float(const GradientSignal::GradientSampleParams&)); + MOCK_CONST_METHOD1(IsEntityInHierarchy, bool(const AZ::EntityId&)); + }; +} // namespace UnitTest + diff --git a/Gems/GradientSignal/Code/gradientsignal_mocks_files.cmake b/Gems/GradientSignal/Code/gradientsignal_mocks_files.cmake new file mode 100644 index 0000000000..82c8c3793f --- /dev/null +++ b/Gems/GradientSignal/Code/gradientsignal_mocks_files.cmake @@ -0,0 +1,11 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(FILES + Mocks/GradientSignal/Ebuses/MockGradientRequestBus.h +) diff --git a/Gems/LmbrCentral/Code/Source/Shape/AxisAlignedBoxShapeComponent.h b/Gems/LmbrCentral/Code/Source/Shape/AxisAlignedBoxShapeComponent.h index 094f9591d1..e180e1d536 100644 --- a/Gems/LmbrCentral/Code/Source/Shape/AxisAlignedBoxShapeComponent.h +++ b/Gems/LmbrCentral/Code/Source/Shape/AxisAlignedBoxShapeComponent.h @@ -15,12 +15,6 @@ namespace LmbrCentral { - /// Type ID for the AxisAlignedBoxShapeComponent - static const AZ::Uuid AxisAlignedBoxShapeComponentTypeId = "{641D817E-1BC6-406A-BBB2-218541808E45}"; - - /// Type ID for the EditorAxisAlignedBoxShapeComponent - static const AZ::Uuid EditorAxisAlignedBoxShapeComponentTypeId = "{8C027DF6-E157-4159-9BF8-F1B925466F1F}"; - /// Provide a Component interface for AxisAlignedBoxShape functionality. class AxisAlignedBoxShapeComponent : public AZ::Component diff --git a/Gems/LmbrCentral/Code/include/LmbrCentral/Shape/BoxShapeComponentBus.h b/Gems/LmbrCentral/Code/include/LmbrCentral/Shape/BoxShapeComponentBus.h index 488e8b5617..2e13334a76 100644 --- a/Gems/LmbrCentral/Code/include/LmbrCentral/Shape/BoxShapeComponentBus.h +++ b/Gems/LmbrCentral/Code/include/LmbrCentral/Shape/BoxShapeComponentBus.h @@ -24,6 +24,12 @@ namespace LmbrCentral /// Type ID for the BoxShapeConfig static const AZ::Uuid BoxShapeConfigTypeId = "{F034FBA2-AC2F-4E66-8152-14DFB90D6283}"; + /// Type ID for the AxisAlignedBoxShapeComponent + static const AZ::Uuid AxisAlignedBoxShapeComponentTypeId = "{641D817E-1BC6-406A-BBB2-218541808E45}"; + + /// Type ID for the EditorAxisAlignedBoxShapeComponent + static const AZ::Uuid EditorAxisAlignedBoxShapeComponentTypeId = "{8C027DF6-E157-4159-9BF8-F1B925466F1F}"; + /// Configuration data for BoxShapeComponent class BoxShapeConfig : public ShapeComponentConfig diff --git a/Gems/PhysX/Code/Editor/DebugDraw.cpp b/Gems/PhysX/Code/Editor/DebugDraw.cpp index 5936312ecf..734557205a 100644 --- a/Gems/PhysX/Code/Editor/DebugDraw.cpp +++ b/Gems/PhysX/Code/Editor/DebugDraw.cpp @@ -676,7 +676,17 @@ namespace PhysX } } - AZ::Transform Collider::GetColliderLocalTransform(const Physics::ColliderConfiguration& colliderConfig, + void Collider::DrawHeightfield( + [[maybe_unused]] AzFramework::DebugDisplayRequests& debugDisplay, + [[maybe_unused]] const Physics::ColliderConfiguration& colliderConfig, + [[maybe_unused]] const Physics::HeightfieldShapeConfiguration& heightfieldShapeConfig, + [[maybe_unused]] const AZ::Vector3& colliderScale, + [[maybe_unused]] const bool forceUniformScaling) const + { + } + + AZ::Transform Collider::GetColliderLocalTransform( + const Physics::ColliderConfiguration& colliderConfig, const AZ::Vector3& colliderScale) const { // Apply entity world transform scale to collider offset diff --git a/Gems/PhysX/Code/Editor/DebugDraw.h b/Gems/PhysX/Code/Editor/DebugDraw.h index 774def6900..74f08aae99 100644 --- a/Gems/PhysX/Code/Editor/DebugDraw.h +++ b/Gems/PhysX/Code/Editor/DebugDraw.h @@ -104,7 +104,15 @@ namespace PhysX const AZ::Vector3& meshScale, AZ::u32 geomIndex) const; - void DrawPolygonPrism(AzFramework::DebugDisplayRequests& debugDisplay, + void DrawHeightfield( + AzFramework::DebugDisplayRequests& debugDisplay, + const Physics::ColliderConfiguration& colliderConfig, + const Physics::HeightfieldShapeConfiguration& heightfieldShapeConfig, + const AZ::Vector3& colliderScale = AZ::Vector3::CreateOne(), + const bool forceUniformScaling = false) const; + + void DrawPolygonPrism( + AzFramework::DebugDisplayRequests& debugDisplay, const Physics::ColliderConfiguration& colliderConfig, const AZStd::vector& points) const; AZ::Transform GetColliderLocalTransform(const Physics::ColliderConfiguration& colliderConfig, diff --git a/Gems/PhysX/Code/Include/PhysX/SystemComponentBus.h b/Gems/PhysX/Code/Include/PhysX/SystemComponentBus.h index f9bd2dbad5..a31a4e65b8 100644 --- a/Gems/PhysX/Code/Include/PhysX/SystemComponentBus.h +++ b/Gems/PhysX/Code/Include/PhysX/SystemComponentBus.h @@ -16,19 +16,21 @@ namespace AzPhysics { class CollisionGroup; class CollisionLayer; -} +} // namespace AzPhysics namespace physx { class PxScene; class PxSceneDesc; class PxConvexMesh; + class PxHeightField; class PxTriangleMesh; class PxShape; class PxCooking; class PxControllerManager; struct PxFilterData; -} + struct PxHeightFieldSample; +} // namespace physx namespace PhysX { @@ -63,6 +65,13 @@ namespace PhysX /// @return Pointer to the created mesh. virtual physx::PxTriangleMesh* CreateTriangleMeshFromCooked(const void* cookedMeshData, AZ::u32 bufferSize) = 0; + /// Creates a new heightfield. + /// @param samples Pointer to beginning of heightfield sample data. + /// @param numRows Number of rows in the heightfield. + /// @param numColumns Number of columns in the heightfield. + /// @return Pointer to the created heightfield. + virtual physx::PxHeightField* CreateHeightField(const physx::PxHeightFieldSample* samples, AZ::u32 numRows, AZ::u32 numColumns) = 0; + /// Creates PhysX collision filter data from generic collision filtering settings. /// @param layer The collision layer the object belongs to. /// @param group The set of collision layers the object will interact with. diff --git a/Gems/PhysX/Code/Source/ComponentDescriptors.cpp b/Gems/PhysX/Code/Source/ComponentDescriptors.cpp index ebe6efd310..83232edc40 100644 --- a/Gems/PhysX/Code/Source/ComponentDescriptors.cpp +++ b/Gems/PhysX/Code/Source/ComponentDescriptors.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ namespace PhysX BaseColliderComponent::CreateDescriptor(), MeshColliderComponent::CreateDescriptor(), BoxColliderComponent::CreateDescriptor(), + HeightfieldColliderComponent::CreateDescriptor(), SphereColliderComponent::CreateDescriptor(), CapsuleColliderComponent::CreateDescriptor(), ShapeColliderComponent::CreateDescriptor(), diff --git a/Gems/PhysX/Code/Source/EditorComponentDescriptors.cpp b/Gems/PhysX/Code/Source/EditorComponentDescriptors.cpp index d43d0edc23..362903f638 100644 --- a/Gems/PhysX/Code/Source/EditorComponentDescriptors.cpp +++ b/Gems/PhysX/Code/Source/EditorComponentDescriptors.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,7 @@ namespace PhysX EditorColliderComponent::CreateDescriptor(), EditorFixedJointComponent::CreateDescriptor(), EditorForceRegionComponent::CreateDescriptor(), + EditorHeightfieldColliderComponent::CreateDescriptor(), EditorHingeJointComponent::CreateDescriptor(), EditorJointComponent::CreateDescriptor(), EditorRigidBodyComponent::CreateDescriptor(), diff --git a/Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.cpp b/Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.cpp new file mode 100644 index 0000000000..96b1bd51f5 --- /dev/null +++ b/Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.cpp @@ -0,0 +1,341 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace PhysX +{ + void EditorHeightfieldColliderComponent::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(1) + ->Field("ColliderConfiguration", &EditorHeightfieldColliderComponent::m_colliderConfig) + ->Field("DebugDrawSettings", &EditorHeightfieldColliderComponent::m_colliderDebugDraw) + ->Field("ShapeConfig", &EditorHeightfieldColliderComponent::m_shapeConfig) + ; + + if (auto editContext = serializeContext->GetEditContext()) + { + editContext->Class( + "PhysX Heightfield Collider", "Creates geometry in the PhysX simulation based on an attached heightfield component") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Category, "PhysX") + ->Attribute(AZ::Edit::Attributes::Icon, "Icons/Components/PhysXCollider.svg") + ->Attribute(AZ::Edit::Attributes::ViewportIcon, "Icons/Components/Viewport/PhysXCollider.svg") + ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Game")) + ->Attribute( + AZ::Edit::Attributes::HelpPageURL, "https://o3de.org/docs/user-guide/components/reference/physx/heightfield-collider/") + ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + ->DataElement( + AZ::Edit::UIHandlers::Default, &EditorHeightfieldColliderComponent::m_colliderConfig, "Collider configuration", + "Configuration of the collider") + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ->Attribute(AZ::Edit::Attributes::ChangeNotify, &EditorHeightfieldColliderComponent::OnConfigurationChanged) + ->DataElement( + AZ::Edit::UIHandlers::Default, &EditorHeightfieldColliderComponent::m_colliderDebugDraw, "Debug draw settings", + "Debug draw settings") + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ; + } + } + } + + void EditorHeightfieldColliderComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(AZ_CRC_CE("PhysicsWorldBodyService")); + provided.push_back(AZ_CRC_CE("PhysXColliderService")); + provided.push_back(AZ_CRC_CE("PhysXHeightfieldColliderService")); + } + + void EditorHeightfieldColliderComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required) + { + required.push_back(AZ_CRC_CE("PhysicsHeightfieldProviderService")); + } + + void EditorHeightfieldColliderComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + incompatible.push_back(AZ_CRC_CE("PhysXColliderService")); + incompatible.push_back(AZ_CRC_CE("PhysXStaticRigidBodyService")); + incompatible.push_back(AZ_CRC_CE("PhysXRigidBodyService")); + } + + EditorHeightfieldColliderComponent::EditorHeightfieldColliderComponent() + : m_physXConfigChangedHandler( + []([[maybe_unused]] const AzPhysics::SystemConfiguration* config) + { + AzToolsFramework::PropertyEditorGUIMessages::Bus::Broadcast( + &AzToolsFramework::PropertyEditorGUIMessages::RequestRefresh, + AzToolsFramework::PropertyModificationRefreshLevel::Refresh_AttributesAndValues); + }) + , m_onMaterialLibraryChangedEventHandler( + [this](const AZ::Data::AssetId& defaultMaterialLibrary) + { + m_colliderConfig.m_materialSelection.OnMaterialLibraryChanged(defaultMaterialLibrary); + Physics::ColliderComponentEventBus::Event(GetEntityId(), &Physics::ColliderComponentEvents::OnColliderChanged); + + AzToolsFramework::PropertyEditorGUIMessages::Bus::Broadcast( + &AzToolsFramework::PropertyEditorGUIMessages::RequestRefresh, + AzToolsFramework::PropertyModificationRefreshLevel::Refresh_AttributesAndValues); + }) + { + } + + EditorHeightfieldColliderComponent ::~EditorHeightfieldColliderComponent() + { + ClearHeightfield(); + } + + // AZ::Component + void EditorHeightfieldColliderComponent::Activate() + { + AzToolsFramework::Components::EditorComponentBase::Activate(); + + // Heightfields don't support the following: + // - Offset: There shouldn't be a need to offset the data, since the heightfield provider is giving a physics representation + // - IsTrigger: PhysX heightfields don't support acting as triggers + // - MaterialSelection: The heightfield provider provides per-vertex material selection + m_colliderConfig.SetPropertyVisibility(Physics::ColliderConfiguration::Offset, false); + m_colliderConfig.SetPropertyVisibility(Physics::ColliderConfiguration::IsTrigger, false); + m_colliderConfig.SetPropertyVisibility(Physics::ColliderConfiguration::MaterialSelection, false); + + m_sceneInterface = AZ::Interface::Get(); + if (m_sceneInterface) + { + m_attachedSceneHandle = m_sceneInterface->GetSceneHandle(AzPhysics::EditorPhysicsSceneName); + } + + const AZ::EntityId entityId = GetEntityId(); + + AzToolsFramework::EntitySelectionEvents::Bus::Handler::BusConnect(entityId); + + // Debug drawing + m_colliderDebugDraw.Connect(entityId); + m_colliderDebugDraw.SetDisplayCallback(this); + + Physics::HeightfieldProviderNotificationBus::Handler::BusConnect(entityId); + PhysX::ColliderShapeRequestBus::Handler::BusConnect(entityId); + AzPhysics::SimulatedBodyComponentRequestsBus::Handler::BusConnect(entityId); + + RefreshHeightfield(); + } + + void EditorHeightfieldColliderComponent::Deactivate() + { + AzPhysics::SimulatedBodyComponentRequestsBus::Handler::BusDisconnect(); + PhysX::ColliderShapeRequestBus::Handler::BusDisconnect(); + Physics::HeightfieldProviderNotificationBus::Handler::BusDisconnect(); + + m_colliderDebugDraw.Disconnect(); + AzToolsFramework::EntitySelectionEvents::Bus::Handler::BusDisconnect(); + AzToolsFramework::Components::EditorComponentBase::Deactivate(); + + ClearHeightfield(); + } + + void EditorHeightfieldColliderComponent::BuildGameEntity(AZ::Entity* gameEntity) + { + auto* heightfieldColliderComponent = gameEntity->CreateComponent(); + heightfieldColliderComponent->SetShapeConfiguration( + { AZStd::make_shared(m_colliderConfig), m_shapeConfig }); + } + + void EditorHeightfieldColliderComponent::OnHeightfieldDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion) + { + RefreshHeightfield(); + } + + void EditorHeightfieldColliderComponent::ClearHeightfield() + { + // There are two references to the heightfield data, we need to clear both to make the heightfield clear out and deallocate: + // - The simulated body has a pointer to the shape, which has a GeometryHolder, which has the Heightfield inside it + // - The shape config is also holding onto a pointer to the Heightfield + + // We remove the simulated body first, since we don't want the heightfield to exist any more. + if (m_sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle) + { + m_sceneInterface->RemoveSimulatedBody(m_attachedSceneHandle, m_staticRigidBodyHandle); + } + + // Now we can safely clear out the cached heightfield pointer. + m_shapeConfig->SetCachedNativeHeightfield(nullptr); + } + + void EditorHeightfieldColliderComponent::InitStaticRigidBody() + { + // Get the transform from the HeightfieldProvider. Because rotation and scale can indirectly affect how the heightfield itself + // is computed and the size of the heightfield, it's possible that the HeightfieldProvider will provide a different transform + // back to us than the one that's directly on that entity. + AZ::Transform transform = AZ::Transform::CreateIdentity(); + Physics::HeightfieldProviderRequestsBus::EventResult( + transform, GetEntityId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldTransform); + + AzPhysics::StaticRigidBodyConfiguration configuration; + configuration.m_orientation = transform.GetRotation(); + configuration.m_position = transform.GetTranslation(); + configuration.m_entityId = GetEntityId(); + configuration.m_debugName = GetEntity()->GetName(); + + AzPhysics::ShapeColliderPairList colliderShapePairs; + colliderShapePairs.emplace_back(AZStd::make_shared(m_colliderConfig), m_shapeConfig); + configuration.m_colliderAndShapeData = colliderShapePairs; + + if (m_sceneInterface) + { + m_staticRigidBodyHandle = m_sceneInterface->AddSimulatedBody(m_attachedSceneHandle, &configuration); + } + } + + void EditorHeightfieldColliderComponent::InitHeightfieldShapeConfiguration() + { + Physics::HeightfieldShapeConfiguration& configuration = static_cast(*m_shapeConfig); + + Utils::InitHeightfieldShapeConfiguration(GetEntityId(), configuration); + } + + void EditorHeightfieldColliderComponent::RefreshHeightfield() + { + ClearHeightfield(); + InitHeightfieldShapeConfiguration(); + InitStaticRigidBody(); + Physics::ColliderComponentEventBus::Event(GetEntityId(), &Physics::ColliderComponentEvents::OnColliderChanged); + } + + AZ::u32 EditorHeightfieldColliderComponent::OnConfigurationChanged() + { + RefreshHeightfield(); + return AZ::Edit::PropertyRefreshLevels::None; + } + + // AzToolsFramework::EntitySelectionEvents + void EditorHeightfieldColliderComponent::OnSelected() + { + if (auto* physXSystem = GetPhysXSystem()) + { + if (!m_physXConfigChangedHandler.IsConnected()) + { + physXSystem->RegisterSystemConfigurationChangedEvent(m_physXConfigChangedHandler); + } + if (!m_onMaterialLibraryChangedEventHandler.IsConnected()) + { + physXSystem->RegisterOnMaterialLibraryChangedEventHandler(m_onMaterialLibraryChangedEventHandler); + } + } + } + + // AzToolsFramework::EntitySelectionEvents + void EditorHeightfieldColliderComponent::OnDeselected() + { + m_onMaterialLibraryChangedEventHandler.Disconnect(); + m_physXConfigChangedHandler.Disconnect(); + } + + // DisplayCallback + void EditorHeightfieldColliderComponent::Display(AzFramework::DebugDisplayRequests& debugDisplay) const + { + const auto& heightfieldConfig = static_cast(*m_shapeConfig); + m_colliderDebugDraw.DrawHeightfield(debugDisplay, m_colliderConfig, heightfieldConfig); + } + + // SimulatedBodyComponentRequestsBus + void EditorHeightfieldColliderComponent::EnablePhysics() + { + if (!IsPhysicsEnabled() && m_sceneInterface) + { + m_sceneInterface->EnableSimulationOfBody(m_attachedSceneHandle, m_staticRigidBodyHandle); + } + } + + // SimulatedBodyComponentRequestsBus + void EditorHeightfieldColliderComponent::DisablePhysics() + { + if (m_sceneInterface) + { + m_sceneInterface->DisableSimulationOfBody(m_attachedSceneHandle, m_staticRigidBodyHandle); + } + } + + // SimulatedBodyComponentRequestsBus + bool EditorHeightfieldColliderComponent::IsPhysicsEnabled() const + { + if (m_sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle) + { + if (auto* body = m_sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle)) + { + return body->m_simulating; + } + } + return false; + } + + // SimulatedBodyComponentRequestsBus + AzPhysics::SimulatedBodyHandle EditorHeightfieldColliderComponent::GetSimulatedBodyHandle() const + { + return m_staticRigidBodyHandle; + } + + // SimulatedBodyComponentRequestsBus + AzPhysics::SimulatedBody* EditorHeightfieldColliderComponent::GetSimulatedBody() + { + if (m_sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle) + { + if (auto* body = m_sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle)) + { + return body; + } + } + return nullptr; + } + + // SimulatedBodyComponentRequestsBus + AzPhysics::SceneQueryHit EditorHeightfieldColliderComponent::RayCast(const AzPhysics::RayCastRequest& request) + { + if (m_sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle) + { + if (auto* body = m_sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle)) + { + return body->RayCast(request); + } + } + return AzPhysics::SceneQueryHit(); + } + + // ColliderShapeRequestBus + AZ::Aabb EditorHeightfieldColliderComponent::GetColliderShapeAabb() + { + // Get the Collider AABB directly from the heightfield provider. + AZ::Aabb colliderAabb = AZ::Aabb::CreateNull(); + Physics::HeightfieldProviderRequestsBus::EventResult( + colliderAabb, GetEntityId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldAabb); + + return colliderAabb; + } + + // SimulatedBodyComponentRequestsBus + AZ::Aabb EditorHeightfieldColliderComponent::GetAabb() const + { + // On the SimulatedBodyComponentRequestsBus, get the AABB from the simulated body instead of the collider. + if (m_sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle) + { + if (auto* body = m_sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle)) + { + return body->GetAabb(); + } + } + return AZ::Aabb::CreateNull(); + } + +} // namespace PhysX diff --git a/Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.h b/Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.h new file mode 100644 index 0000000000..08b3ec5801 --- /dev/null +++ b/Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.h @@ -0,0 +1,104 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace PhysX +{ + //! Editor PhysX Heightfield Collider Component. + class EditorHeightfieldColliderComponent + : public AzToolsFramework::Components::EditorComponentBase + , protected AzToolsFramework::EntitySelectionEvents::Bus::Handler + , protected DebugDraw::DisplayCallback + , protected AzPhysics::SimulatedBodyComponentRequestsBus::Handler + , protected PhysX::ColliderShapeRequestBus::Handler + , protected Physics::HeightfieldProviderNotificationBus::Handler + { + public: + AZ_EDITOR_COMPONENT( + EditorHeightfieldColliderComponent, + "{C388C3DB-8D2E-4D26-96D3-198EDC799B77}", + AzToolsFramework::Components::EditorComponentBase); + static void Reflect(AZ::ReflectContext* context); + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + + EditorHeightfieldColliderComponent(); + ~EditorHeightfieldColliderComponent(); + + // AZ::Component + void Activate() override; + void Deactivate() override; + + // EditorComponentBase + void BuildGameEntity(AZ::Entity* gameEntity) override; + + protected: + + // AzToolsFramework::EntitySelectionEvents + void OnSelected() override; + void OnDeselected() override; + + // DisplayCallback + void Display(AzFramework::DebugDisplayRequests& debugDisplay) const; + + // ColliderShapeRequestBus + AZ::Aabb GetColliderShapeAabb() override; + bool IsTrigger() override + { + // PhysX Heightfields don't support triggers. + return false; + } + + // AzPhysics::SimulatedBodyComponentRequestsBus::Handler overrides ... + void EnablePhysics() override; + void DisablePhysics() override; + bool IsPhysicsEnabled() const override; + AZ::Aabb GetAabb() const override; + AzPhysics::SimulatedBody* GetSimulatedBody() override; + AzPhysics::SimulatedBodyHandle GetSimulatedBodyHandle() const override; + AzPhysics::SceneQueryHit RayCast(const AzPhysics::RayCastRequest& request) override; + + // Physics::HeightfieldProviderNotificationBus + void OnHeightfieldDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion) override; + + private: + AZ::u32 OnConfigurationChanged(); + + void ClearHeightfield(); + void InitHeightfieldShapeConfiguration(); + void InitStaticRigidBody(); + void RefreshHeightfield(); + + DebugDraw::Collider m_colliderDebugDraw; //!< Handles drawing the collider + AzPhysics::SceneInterface* m_sceneInterface{ nullptr }; + + AzPhysics::SystemEvents::OnConfigurationChangedEvent::Handler m_physXConfigChangedHandler; + AzPhysics::SystemEvents::OnMaterialLibraryChangedEvent::Handler m_onMaterialLibraryChangedEventHandler; + + Physics::ColliderConfiguration m_colliderConfig; //!< Stores collision layers, whether the collider is a trigger, etc. + AZStd::shared_ptr m_shapeConfig{ new Physics::HeightfieldShapeConfiguration() }; + + AzPhysics::SimulatedBodyHandle m_staticRigidBodyHandle = + AzPhysics::InvalidSimulatedBodyHandle; //!< Handle to the body in the editor physics scene if there is no rigid body component. + AzPhysics::SceneHandle m_attachedSceneHandle = AzPhysics::InvalidSceneHandle; + }; + +} // namespace PhysX diff --git a/Gems/PhysX/Code/Source/HeightfieldColliderComponent.cpp b/Gems/PhysX/Code/Source/HeightfieldColliderComponent.cpp new file mode 100644 index 0000000000..9bc209982d --- /dev/null +++ b/Gems/PhysX/Code/Source/HeightfieldColliderComponent.cpp @@ -0,0 +1,365 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace PhysX +{ + void HeightfieldColliderComponent::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(1) + ->Field("ShapeConfig", &HeightfieldColliderComponent::m_shapeConfig) + ; + } + } + + void HeightfieldColliderComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(AZ_CRC_CE("PhysicsWorldBodyService")); + provided.push_back(AZ_CRC_CE("PhysXColliderService")); + provided.push_back(AZ_CRC_CE("PhysXHeightfieldColliderService")); + provided.push_back(AZ_CRC_CE("PhysXStaticRigidBodyService")); + } + + void HeightfieldColliderComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required) + { + required.push_back(AZ_CRC_CE("PhysicsHeightfieldProviderService")); + } + + void HeightfieldColliderComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + incompatible.push_back(AZ_CRC_CE("PhysXColliderService")); + incompatible.push_back(AZ_CRC_CE("PhysXStaticRigidBodyService")); + incompatible.push_back(AZ_CRC_CE("PhysXRigidBodyService")); + } + + HeightfieldColliderComponent::~HeightfieldColliderComponent() + { + ClearHeightfield(); + } + + void HeightfieldColliderComponent::Activate() + { + const AZ::EntityId entityId = GetEntityId(); + + Physics::HeightfieldProviderNotificationBus::Handler::BusConnect(entityId); + ColliderComponentRequestBus::Handler::BusConnect(entityId); + ColliderShapeRequestBus::Handler::BusConnect(entityId); + Physics::CollisionFilteringRequestBus::Handler::BusConnect(entityId); + AzPhysics::SimulatedBodyComponentRequestsBus::Handler::BusConnect(entityId); + + RefreshHeightfield(); + } + + void HeightfieldColliderComponent::Deactivate() + { + AzPhysics::SimulatedBodyComponentRequestsBus::Handler::BusDisconnect(); + Physics::CollisionFilteringRequestBus::Handler::BusDisconnect(); + ColliderShapeRequestBus::Handler::BusDisconnect(); + ColliderComponentRequestBus::Handler::BusDisconnect(); + Physics::HeightfieldProviderNotificationBus::Handler::BusDisconnect(); + + ClearHeightfield(); + } + + void HeightfieldColliderComponent::OnHeightfieldDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion) + { + RefreshHeightfield(); + } + + void HeightfieldColliderComponent::ClearHeightfield() + { + // There are two references to the heightfield data, we need to clear both to make the heightfield clear out and deallocate: + // - The simulated body has a pointer to the shape, which has a GeometryHolder, which has the Heightfield inside it + // - The shape config is also holding onto a pointer to the Heightfield + + // We remove the simulated body first, since we don't want the heightfield to exist any more. + if (auto* sceneInterface = AZ::Interface::Get(); + sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle) + { + sceneInterface->RemoveSimulatedBody(m_attachedSceneHandle, m_staticRigidBodyHandle); + } + + // Now we can safely clear out the cached heightfield pointer. + Physics::HeightfieldShapeConfiguration& configuration = static_cast(*m_shapeConfig.second); + configuration.SetCachedNativeHeightfield(nullptr); + } + + void HeightfieldColliderComponent::InitStaticRigidBody() + { + // Get the transform from the HeightfieldProvider. Because rotation and scale can indirectly affect how the heightfield itself + // is computed and the size of the heightfield, it's possible that the HeightfieldProvider will provide a different transform + // back to us than the one that's directly on that entity. + AZ::Transform transform = AZ::Transform::CreateIdentity(); + Physics::HeightfieldProviderRequestsBus::EventResult( + transform, GetEntityId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldTransform); + + AzPhysics::StaticRigidBodyConfiguration configuration; + configuration.m_orientation = transform.GetRotation(); + configuration.m_position = transform.GetTranslation(); + configuration.m_entityId = GetEntityId(); + configuration.m_debugName = GetEntity()->GetName(); + configuration.m_colliderAndShapeData = GetShapeConfigurations(); + + if (m_attachedSceneHandle == AzPhysics::InvalidSceneHandle) + { + Physics::DefaultWorldBus::BroadcastResult(m_attachedSceneHandle, &Physics::DefaultWorldRequests::GetDefaultSceneHandle); + } + if (auto* sceneInterface = AZ::Interface::Get()) + { + m_staticRigidBodyHandle = sceneInterface->AddSimulatedBody(m_attachedSceneHandle, &configuration); + } + } + + void HeightfieldColliderComponent::InitHeightfieldShapeConfiguration() + { + Physics::HeightfieldShapeConfiguration& configuration = static_cast(*m_shapeConfig.second); + + Utils::InitHeightfieldShapeConfiguration(GetEntityId(), configuration); + } + + void HeightfieldColliderComponent::RefreshHeightfield() + { + ClearHeightfield(); + InitHeightfieldShapeConfiguration(); + InitStaticRigidBody(); + Physics::ColliderComponentEventBus::Event(GetEntityId(), &Physics::ColliderComponentEvents::OnColliderChanged); + } + + void HeightfieldColliderComponent::SetShapeConfiguration(const AzPhysics::ShapeColliderPair& shapeConfig) + { + if (GetEntity()->GetState() == AZ::Entity::State::Active) + { + AZ_Warning( + "PhysX", false, "Trying to call SetShapeConfiguration for entity \"%s\" while entity is active.", + GetEntity()->GetName().c_str()); + return; + } + m_shapeConfig = shapeConfig; + } + + // SimulatedBodyComponentRequestsBus + void HeightfieldColliderComponent::EnablePhysics() + { + if (IsPhysicsEnabled()) + { + return; + } + if (auto* sceneInterface = AZ::Interface::Get()) + { + sceneInterface->EnableSimulationOfBody(m_attachedSceneHandle, m_staticRigidBodyHandle); + } + } + + // SimulatedBodyComponentRequestsBus + void HeightfieldColliderComponent::DisablePhysics() + { + if (auto* sceneInterface = AZ::Interface::Get()) + { + sceneInterface->DisableSimulationOfBody(m_attachedSceneHandle, m_staticRigidBodyHandle); + } + } + + // SimulatedBodyComponentRequestsBus + bool HeightfieldColliderComponent::IsPhysicsEnabled() const + { + if (m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle) + { + if (auto* sceneInterface = AZ::Interface::Get(); + sceneInterface != nullptr && sceneInterface->IsEnabled(m_attachedSceneHandle)) // check if the scene is enabled + { + if (AzPhysics::SimulatedBody* body = + sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle)) + { + return body->m_simulating; + } + } + } + return false; + } + + // SimulatedBodyComponentRequestsBus + AzPhysics::SimulatedBodyHandle HeightfieldColliderComponent::GetSimulatedBodyHandle() const + { + return m_staticRigidBodyHandle; + } + + // SimulatedBodyComponentRequestsBus + AzPhysics::SimulatedBody* HeightfieldColliderComponent::GetSimulatedBody() + { + if (auto* sceneInterface = AZ::Interface::Get()) + { + return sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle); + } + return nullptr; + } + + // SimulatedBodyComponentRequestsBus + AzPhysics::SceneQueryHit HeightfieldColliderComponent::RayCast(const AzPhysics::RayCastRequest& request) + { + if (auto* body = azdynamic_cast(GetSimulatedBody())) + { + return body->RayCast(request); + } + return AzPhysics::SceneQueryHit(); + } + + // ColliderComponentRequestBus + AzPhysics::ShapeColliderPairList HeightfieldColliderComponent::GetShapeConfigurations() + { + AzPhysics::ShapeColliderPairList shapeConfigurationList({ m_shapeConfig }); + return shapeConfigurationList; + } + + AZStd::shared_ptr HeightfieldColliderComponent::GetHeightfieldShape() + { + if (auto* body = azdynamic_cast(GetSimulatedBody())) + { + // Heightfields should only have one shape + AZ_Assert(body->GetShapeCount() == 1, "Heightfield rigid body has the wrong number of shapes: %zu", body->GetShapeCount()); + return body->GetShape(0); + } + + return {}; + } + + // ColliderComponentRequestBus + AZStd::vector> HeightfieldColliderComponent::GetShapes() + { + return { GetHeightfieldShape() }; + } + + // PhysX::ColliderShapeBus + AZ::Aabb HeightfieldColliderComponent::GetColliderShapeAabb() + { + // Get the Collider AABB directly from the heightfield provider. + AZ::Aabb colliderAabb = AZ::Aabb::CreateNull(); + Physics::HeightfieldProviderRequestsBus::EventResult( + colliderAabb, GetEntityId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldAabb); + + return colliderAabb; + } + + // SimulatedBodyComponentRequestsBus + AZ::Aabb HeightfieldColliderComponent::GetAabb() const + { + // On the SimulatedBodyComponentRequestsBus, get the AABB from the simulated body instead of the collider. + if (auto* sceneInterface = AZ::Interface::Get()) + { + if (AzPhysics::SimulatedBody* body = sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle)) + { + return body->GetAabb(); + } + } + return AZ::Aabb::CreateNull(); + } + + // CollisionFilteringRequestBus + void HeightfieldColliderComponent::SetCollisionLayer(const AZStd::string& layerName, AZ::Crc32 colliderTag) + { + if (auto heightfield = GetHeightfieldShape()) + { + if (Physics::Utils::FilterTag(heightfield->GetTag(), colliderTag)) + { + bool success = false; + AzPhysics::CollisionLayer layer; + Physics::CollisionRequestBus::BroadcastResult( + success, &Physics::CollisionRequests::TryGetCollisionLayerByName, layerName, layer); + if (success) + { + heightfield->SetCollisionLayer(layer); + } + } + } + } + + // CollisionFilteringRequestBus + AZStd::string HeightfieldColliderComponent::GetCollisionLayerName() + { + AZStd::string layerName; + if (auto heightfield = GetHeightfieldShape()) + { + Physics::CollisionRequestBus::BroadcastResult( + layerName, &Physics::CollisionRequests::GetCollisionLayerName, heightfield->GetCollisionLayer()); + } + return layerName; + } + + // CollisionFilteringRequestBus + void HeightfieldColliderComponent::SetCollisionGroup(const AZStd::string& groupName, AZ::Crc32 colliderTag) + { + if (auto heightfield = GetHeightfieldShape()) + { + if (Physics::Utils::FilterTag(heightfield->GetTag(), colliderTag)) + { + bool success = false; + AzPhysics::CollisionGroup group; + Physics::CollisionRequestBus::BroadcastResult( + success, &Physics::CollisionRequests::TryGetCollisionGroupByName, groupName, group); + if (success) + { + heightfield->SetCollisionGroup(group); + } + } + } + } + + // CollisionFilteringRequestBus + AZStd::string HeightfieldColliderComponent::GetCollisionGroupName() + { + AZStd::string groupName; + if (auto heightfield = GetHeightfieldShape()) + { + Physics::CollisionRequestBus::BroadcastResult( + groupName, &Physics::CollisionRequests::GetCollisionGroupName, heightfield->GetCollisionGroup()); + } + + return groupName; + } + + // CollisionFilteringRequestBus + void HeightfieldColliderComponent::ToggleCollisionLayer(const AZStd::string& layerName, AZ::Crc32 colliderTag, bool enabled) + { + if (auto heightfield = GetHeightfieldShape()) + { + if (Physics::Utils::FilterTag(heightfield->GetTag(), colliderTag)) + { + bool success = false; + AzPhysics::CollisionLayer layer; + Physics::CollisionRequestBus::BroadcastResult( + success, &Physics::CollisionRequests::TryGetCollisionLayerByName, layerName, layer); + if (success) + { + auto group = heightfield->GetCollisionGroup(); + group.SetLayer(layer, enabled); + heightfield->SetCollisionGroup(group); + } + } + } + } + +} // namespace PhysX diff --git a/Gems/PhysX/Code/Source/HeightfieldColliderComponent.h b/Gems/PhysX/Code/Source/HeightfieldColliderComponent.h new file mode 100644 index 0000000000..3213f7a774 --- /dev/null +++ b/Gems/PhysX/Code/Source/HeightfieldColliderComponent.h @@ -0,0 +1,104 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include + +#include +#include +#include + +#include +#include + +namespace AzPhysics +{ + struct SimulatedBody; +} + +namespace PhysX +{ + class StaticRigidBody; + + //! Component that provides a Heightfield Collider and associated Static Rigid Body. + //! The heightfield collider is a bit different from the other shape colliders in that it gets the heightfield data from a + //! HeightfieldProvider, which can control position, rotation, size, and even change its data at runtime. + //! + //! Due to these differences, this component directly implements both the collider and static rigid body services instead of + //! using BaseColliderComponent and StaticRigidBodyComponent. + class HeightfieldColliderComponent + : public AZ::Component + , public ColliderComponentRequestBus::Handler + , public AzPhysics::SimulatedBodyComponentRequestsBus::Handler + , protected PhysX::ColliderShapeRequestBus::Handler + , protected Physics::CollisionFilteringRequestBus::Handler + , protected Physics::HeightfieldProviderNotificationBus::Handler + { + public: + using Configuration = Physics::HeightfieldShapeConfiguration; + AZ_COMPONENT(HeightfieldColliderComponent, "{9A42672C-281A-4CE8-BFDD-EAA1E0FCED76}"); + static void Reflect(AZ::ReflectContext* context); + + HeightfieldColliderComponent() = default; + ~HeightfieldColliderComponent() override; + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + + void Activate() override; + void Deactivate() override; + + void SetShapeConfiguration(const AzPhysics::ShapeColliderPair& shapeConfig); + + protected: + // ColliderComponentRequestBus + AzPhysics::ShapeColliderPairList GetShapeConfigurations() override; + AZStd::vector> GetShapes() override; + + // ColliderShapeRequestBus + AZ::Aabb GetColliderShapeAabb() override; + bool IsTrigger() override + { + // PhysX Heightfields don't support triggers. + return false; + } + + // CollisionFilteringRequestBus + void SetCollisionLayer(const AZStd::string& layerName, AZ::Crc32 filterTag) override; + AZStd::string GetCollisionLayerName() override; + void SetCollisionGroup(const AZStd::string& groupName, AZ::Crc32 filterTag) override; + AZStd::string GetCollisionGroupName() override; + void ToggleCollisionLayer(const AZStd::string& layerName, AZ::Crc32 filterTag, bool enabled) override; + + // SimulatedBodyComponentRequestsBus + void EnablePhysics() override; + void DisablePhysics() override; + bool IsPhysicsEnabled() const override; + AZ::Aabb GetAabb() const override; + AzPhysics::SimulatedBodyHandle GetSimulatedBodyHandle() const override; + AzPhysics::SimulatedBody* GetSimulatedBody() override; + AzPhysics::SceneQueryHit RayCast(const AzPhysics::RayCastRequest& request) override; + + // HeightfieldProviderNotificationBus + void OnHeightfieldDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion) override; + + private: + AZStd::shared_ptr GetHeightfieldShape(); + + void ClearHeightfield(); + void InitHeightfieldShapeConfiguration(); + void InitStaticRigidBody(); + void RefreshHeightfield(); + + AzPhysics::ShapeColliderPair m_shapeConfig; + AzPhysics::SimulatedBodyHandle m_staticRigidBodyHandle = AzPhysics::InvalidSimulatedBodyHandle; + AzPhysics::SceneHandle m_attachedSceneHandle = AzPhysics::InvalidSceneHandle; + }; +} // namespace PhysX diff --git a/Gems/PhysX/Code/Source/SystemComponent.cpp b/Gems/PhysX/Code/Source/SystemComponent.cpp index 2f4d082ccb..000d69d982 100644 --- a/Gems/PhysX/Code/Source/SystemComponent.cpp +++ b/Gems/PhysX/Code/Source/SystemComponent.cpp @@ -252,6 +252,22 @@ namespace PhysX return convex; } + physx::PxHeightField* SystemComponent::CreateHeightField(const physx::PxHeightFieldSample* samples, AZ::u32 numRows, AZ::u32 numColumns) + { + physx::PxHeightFieldDesc desc; + desc.format = physx::PxHeightFieldFormat::eS16_TM; + desc.nbColumns = numColumns; + desc.nbRows = numRows; + desc.samples.data = samples; + desc.samples.stride = sizeof(physx::PxHeightFieldSample); + + physx::PxHeightField* heightfield = + m_physXSystem->GetPxCooking()->createHeightField(desc, m_physXSystem->GetPxPhysics()->getPhysicsInsertionCallback()); + AZ_Error("PhysX", heightfield, "Error. Unable to create heightfield"); + + return heightfield; + } + bool SystemComponent::CookConvexMeshToFile(const AZStd::string& filePath, const AZ::Vector3* vertices, AZ::u32 vertexCount) { AZStd::vector physxData; @@ -342,6 +358,14 @@ namespace PhysX return AZStd::make_shared(materialConfiguration); } + void SystemComponent::ReleaseNativeHeightfieldObject(void* nativeHeightfieldObject) + { + if (nativeHeightfieldObject) + { + static_cast(nativeHeightfieldObject)->release(); + } + } + void SystemComponent::ReleaseNativeMeshObject(void* nativeMeshObject) { if (nativeMeshObject) diff --git a/Gems/PhysX/Code/Source/SystemComponent.h b/Gems/PhysX/Code/Source/SystemComponent.h index 3661655bb3..d581af7e67 100644 --- a/Gems/PhysX/Code/Source/SystemComponent.h +++ b/Gems/PhysX/Code/Source/SystemComponent.h @@ -76,6 +76,7 @@ namespace PhysX physx::PxConvexMesh* CreateConvexMesh(const void* vertices, AZ::u32 vertexNum, AZ::u32 vertexStride) override; // should we use AZ::Vector3* or physx::PxVec3 here? physx::PxConvexMesh* CreateConvexMeshFromCooked(const void* cookedMeshData, AZ::u32 bufferSize) override; physx::PxTriangleMesh* CreateTriangleMeshFromCooked(const void* cookedMeshData, AZ::u32 bufferSize) override; + physx::PxHeightField* CreateHeightField(const physx::PxHeightFieldSample* samples, AZ::u32 numRows, AZ::u32 numColumns) override; bool CookConvexMeshToFile(const AZStd::string& filePath, const AZ::Vector3* vertices, AZ::u32 vertexCount) override; @@ -112,6 +113,7 @@ namespace PhysX AZStd::shared_ptr CreateMaterial(const Physics::MaterialConfiguration& materialConfiguration) override; void ReleaseNativeMeshObject(void* nativeMeshObject) override; + void ReleaseNativeHeightfieldObject(void* nativeHeightfieldObject) override; // Assets related data AZStd::vector> m_assetHandlers; diff --git a/Gems/PhysX/Code/Source/Utils.cpp b/Gems/PhysX/Code/Source/Utils.cpp index 8a76d6d986..72e17aff70 100644 --- a/Gems/PhysX/Code/Source/Utils.cpp +++ b/Gems/PhysX/Code/Source/Utils.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -25,6 +26,7 @@ #include #include #include +#include #include #include @@ -64,6 +66,136 @@ namespace PhysX } } + void CreatePxGeometryFromHeightfield( + const Physics::HeightfieldShapeConfiguration& heightfieldConfig, physx::PxGeometryHolder& pxGeometry) + { + physx::PxHeightField* heightfield = nullptr; + + const AZ::Vector2 gridSpacing = heightfieldConfig.GetGridResolution(); + + const int32_t numCols = heightfieldConfig.GetNumColumns(); + const int32_t numRows = heightfieldConfig.GetNumRows(); + + const float rowScale = gridSpacing.GetX(); + const float colScale = gridSpacing.GetY(); + + const float minHeightBounds = heightfieldConfig.GetMinHeightBounds(); + const float maxHeightBounds = heightfieldConfig.GetMaxHeightBounds(); + const float halfBounds{ (maxHeightBounds - minHeightBounds) / 2.0f }; + + // We're making the assumption right now that the min/max bounds are centered around 0. + // If we ever want to allow off-center bounds, we'll need to fix up the float-to-int16 height math below + // to account for it. + AZ_Assert( + AZ::IsClose(-halfBounds, minHeightBounds) && AZ::IsClose(halfBounds, maxHeightBounds), + "Min/Max height bounds aren't centered around 0, the height conversions below will be incorrect."); + + AZ_Assert( + maxHeightBounds >= minHeightBounds, + "Max height bounds is less than min height bounds, the height conversions below will be incorrect."); + + // To convert our floating-point heights to fixed-point representation inside of an int16, we need a scale factor + // for the conversion. The scale factor is used to map the most important bits of our floating-point height to the + // full 16-bit range. + // Note that the scaleFactor choice here affects overall precision. For each bit that the integer part of our max + // height uses, that's one less bit for the fractional part. + const float scaleFactor = (maxHeightBounds <= minHeightBounds) ? 1.0f : AZStd::numeric_limits::max() / halfBounds; + const float heightScale{ 1.0f / scaleFactor }; + + const uint8_t physxMaximumMaterialIndex = 0x7f; + + // Delete the cached heightfield object if it is there, and create a new one and save in the shape configuration + heightfieldConfig.SetCachedNativeHeightfield(nullptr); + + const AZStd::vector& samples = heightfieldConfig.GetSamples(); + AZ_Assert(samples.size() == numRows * numCols, "GetHeightsAndMaterials returned wrong sized heightfield"); + + if (!samples.empty()) + { + AZStd::vector physxSamples(samples.size()); + + for (int32_t row = 0; row < numRows; row++) + { + const bool lastRowIndex = (row == (numRows - 1)); + + for (int32_t col = 0; col < numCols; col++) + { + const bool lastColumnIndex = (col == (numCols - 1)); + + auto GetIndex = [numCols](int32_t row, int32_t col) + { + return (row * numCols) + col; + }; + + const int32_t sampleIndex = GetIndex(row, col); + + const Physics::HeightMaterialPoint& currentSample = samples[sampleIndex]; + physx::PxHeightFieldSample& currentPhysxSample = physxSamples[sampleIndex]; + AZ_Assert(currentSample.m_materialIndex < physxMaximumMaterialIndex, "MaterialIndex must be less than 128"); + currentPhysxSample.height = azlossy_cast( + AZ::GetClamp(currentSample.m_height, minHeightBounds, maxHeightBounds) * scaleFactor); + if (lastRowIndex || lastColumnIndex) + { + // In PhysX, the material indices refer to the quad down and to the right of the sample. + // If we're in the last row or last column, there aren't any quads down or to the right, + // so just clear these out. + currentPhysxSample.materialIndex0 = 0; + currentPhysxSample.materialIndex1 = 0; + } + else + { + // Our source data is providing one material index per vertex, but PhysX wants one material index + // per triangle. The heuristic that we'll go with for selecting the material index is to choose + // the material for the vertex that's not on the diagonal of each triangle. + // Ex: A *---* B + // | / | For this, we'll use A for index0 and D for index1. + // C *---* D + // + // Ex: A *---* B + // | \ | For this, we'll use C for index0 and B for index1. + // C *---* D + // + // This is a pretty arbitrary choice, so the heuristic might need to be revisited over time if this + // causes incorrect or unpredictable physics material mappings. + + switch (currentSample.m_quadMeshType) + { + case Physics::QuadMeshType::SubdivideUpperLeftToBottomRight: + currentPhysxSample.materialIndex0 = samples[GetIndex(row + 1, col)].m_materialIndex; + currentPhysxSample.materialIndex1 = samples[GetIndex(row, col + 1)].m_materialIndex; + // Set the tesselation flag to say that we need to go from UL to BR + currentPhysxSample.materialIndex0.setBit(); + break; + case Physics::QuadMeshType::SubdivideBottomLeftToUpperRight: + currentPhysxSample.materialIndex0 = currentSample.m_materialIndex; + currentPhysxSample.materialIndex1 = samples[GetIndex(row + 1, col + 1)].m_materialIndex; + break; + case Physics::QuadMeshType::Hole: + currentPhysxSample.materialIndex0 = physx::PxHeightFieldMaterial::eHOLE; + currentPhysxSample.materialIndex1 = physx::PxHeightFieldMaterial::eHOLE; + break; + default: + AZ_Warning("PhysX Heightfield", false, "Unhandled case in CreatePxGeometryFromConfig"); + currentPhysxSample.materialIndex0 = 0; + currentPhysxSample.materialIndex1 = 0; + break; + } + } + } + } + + SystemRequestsBus::BroadcastResult(heightfield, &SystemRequests::CreateHeightField, physxSamples.data(), numRows, numCols); + } + if (heightfield) + { + heightfieldConfig.SetCachedNativeHeightfield(heightfield); + + physx::PxHeightFieldGeometry hfGeom(heightfield, physx::PxMeshGeometryFlags(), heightScale, rowScale, colScale); + + pxGeometry.storeAny(hfGeom); + } + } + bool CreatePxGeometryFromConfig(const Physics::ShapeConfiguration& shapeConfiguration, physx::PxGeometryHolder& pxGeometry) { if (!shapeConfiguration.m_scale.IsGreaterThan(AZ::Vector3::CreateZero())) @@ -170,6 +302,14 @@ namespace PhysX "Please iterate over m_colliderShapes in the asset and call this function for each of them."); return false; } + case Physics::ShapeType::Heightfield: + { + const Physics::HeightfieldShapeConfiguration& heightfieldConfig = + static_cast(shapeConfiguration); + + CreatePxGeometryFromHeightfield(heightfieldConfig, pxGeometry); + break; + } default: AZ_Warning("PhysX Rigid Body", false, "Shape not supported in PhysX. Shape Type: %d", shapeType); return false; @@ -219,6 +359,26 @@ namespace PhysX physx::PxQuat pxQuat(AZ::Constants::HalfPi, physx::PxVec3(0.0f, 1.0f, 0.0f)); shape->setLocalPose(physx::PxTransform(pxQuat)); } + else if (pxGeomHolder.getType() == physx::PxGeometryType::eHEIGHTFIELD) + { + const Physics::HeightfieldShapeConfiguration& heightfieldConfig = + static_cast(shapeConfiguration); + + // PhysX heightfields have the origin at the corner, not the center, so add an offset to the passed-in transform + // to account for this difference. + const AZ::Vector2 gridSpacing = heightfieldConfig.GetGridResolution(); + AZ::Vector3 offset( + -(gridSpacing.GetX() * heightfieldConfig.GetNumColumns() / 2.0f), + -(gridSpacing.GetY() * heightfieldConfig.GetNumRows() / 2.0f), + 0.0f); + + // PhysX heightfields are always defined to have the height in the Y direction, not the Z direction, so we need + // to provide additional rotations to make it Z-up. + physx::PxQuat pxQuat = PxMathConvert( + AZ::Quaternion::CreateFromEulerAnglesRadians(AZ::Vector3(AZ::Constants::HalfPi, AZ::Constants::HalfPi, 0.0f))); + physx::PxTransform pxHeightfieldTransform = physx::PxTransform(PxMathConvert(offset), pxQuat); + shape->setLocalPose(pxHeightfieldTransform); + } // Handle a possible misconfiguration when a shape is set to be both simulated & trigger. This is illegal in PhysX. shape->setFlag(physx::PxShapeFlag::eSIMULATION_SHAPE, colliderConfiguration.m_isSimulated && !colliderConfiguration.m_isTrigger); @@ -1357,6 +1517,39 @@ namespace PhysX return entityWorldTransformWithoutScale * jointLocalTransformWithoutScale; } + + void InitHeightfieldShapeConfiguration(AZ::EntityId entityId, Physics::HeightfieldShapeConfiguration& configuration) + { + configuration = Physics::HeightfieldShapeConfiguration(); + + AZ::Vector2 gridSpacing(1.0f); + Physics::HeightfieldProviderRequestsBus::EventResult( + gridSpacing, entityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSpacing); + + configuration.SetGridResolution(gridSpacing); + + int32_t numRows = 0; + int32_t numColumns = 0; + Physics::HeightfieldProviderRequestsBus::Event( + entityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, numColumns, numRows); + + configuration.SetNumRows(numRows); + configuration.SetNumColumns(numColumns); + + float minHeightBounds = 0.0f; + float maxHeightBounds = 0.0f; + Physics::HeightfieldProviderRequestsBus::Event( + entityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldHeightBounds, minHeightBounds, maxHeightBounds); + + configuration.SetMinHeightBounds(minHeightBounds); + configuration.SetMaxHeightBounds(maxHeightBounds); + + AZStd::vector samples; + Physics::HeightfieldProviderRequestsBus::EventResult( + samples, entityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightsAndMaterials); + + configuration.SetSamples(samples); + } } // namespace Utils namespace ReflectionUtils diff --git a/Gems/PhysX/Code/Source/Utils.h b/Gems/PhysX/Code/Source/Utils.h index ed63605bc8..54a81bb28d 100644 --- a/Gems/PhysX/Code/Source/Utils.h +++ b/Gems/PhysX/Code/Source/Utils.h @@ -188,6 +188,8 @@ namespace PhysX //! Returns defaultValue if the input is infinite or NaN, otherwise returns the input unchanged. const AZ::Vector3& Sanitize(const AZ::Vector3& input, const AZ::Vector3& defaultValue = AZ::Vector3::CreateZero()); + void InitHeightfieldShapeConfiguration(AZ::EntityId entityId, Physics::HeightfieldShapeConfiguration& configuration); + namespace Geometry { using PointList = AZStd::vector; diff --git a/Gems/PhysX/Code/physx_editor_files.cmake b/Gems/PhysX/Code/physx_editor_files.cmake index 5c0c038976..1b22c0ce99 100644 --- a/Gems/PhysX/Code/physx_editor_files.cmake +++ b/Gems/PhysX/Code/physx_editor_files.cmake @@ -27,6 +27,8 @@ set(FILES Source/EditorFixedJointComponent.h Source/EditorHingeJointComponent.cpp Source/EditorHingeJointComponent.h + Source/EditorHeightfieldColliderComponent.cpp + Source/EditorHeightfieldColliderComponent.h Source/EditorJointComponent.cpp Source/EditorJointComponent.h Source/Pipeline/MeshExporter.cpp diff --git a/Gems/PhysX/Code/physx_files.cmake b/Gems/PhysX/Code/physx_files.cmake index 654f1d9767..efefaf3105 100644 --- a/Gems/PhysX/Code/physx_files.cmake +++ b/Gems/PhysX/Code/physx_files.cmake @@ -35,6 +35,8 @@ set(FILES Source/MeshColliderComponent.h Source/BoxColliderComponent.h Source/BoxColliderComponent.cpp + Source/HeightfieldColliderComponent.h + Source/HeightfieldColliderComponent.cpp Source/SphereColliderComponent.h Source/SphereColliderComponent.cpp Source/CapsuleColliderComponent.h diff --git a/Gems/Terrain/Code/CMakeLists.txt b/Gems/Terrain/Code/CMakeLists.txt index 442093a885..4b2f32e172 100644 --- a/Gems/Terrain/Code/CMakeLists.txt +++ b/Gems/Terrain/Code/CMakeLists.txt @@ -111,6 +111,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) AZ::AzTest AZ::AzFramework Gem::LmbrCentral.Mocks + Gem::GradientSignal.Mocks Gem::Terrain.Mocks Gem::Terrain.Static ) diff --git a/Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h b/Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h index ea0413be9a..bb1fa5683a 100644 --- a/Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h +++ b/Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h @@ -10,11 +10,12 @@ #include #include +#include #include +#include namespace UnitTest { - class MockTerrainSystemService : private Terrain::TerrainSystemServiceRequestBus::Handler { public: @@ -69,11 +70,7 @@ namespace UnitTest Terrain::TerrainAreaHeightRequestBus::Handler::BusDisconnect(); } - MOCK_METHOD3(GetHeight, void( - const AZ::Vector3& inPosition, - AZ::Vector3& outPosition, - bool& terrainExists)); - + MOCK_METHOD3(GetHeight, void(const AZ::Vector3& inPosition, AZ::Vector3& outPosition, bool& terrainExists)); }; class MockTerrainSpawnerRequests : public Terrain::TerrainSpawnerRequestBus::Handler @@ -92,4 +89,35 @@ namespace UnitTest MOCK_METHOD2(GetPriority, void(AZ::u32& outLayer, AZ::u32& outPriority)); MOCK_METHOD0(GetUseGroundPlane, bool()); }; -} + + class MockTerrainDataRequests : public AzFramework::Terrain::TerrainDataRequestBus::Handler + { + public: + MockTerrainDataRequests() + { + AzFramework::Terrain::TerrainDataRequestBus::Handler::BusConnect(); + } + + ~MockTerrainDataRequests() + { + AzFramework::Terrain::TerrainDataRequestBus::Handler::BusDisconnect(); + } + + MOCK_CONST_METHOD0(GetTerrainHeightQueryResolution, AZ::Vector2()); + MOCK_METHOD1(SetTerrainHeightQueryResolution, void(AZ::Vector2)); + MOCK_CONST_METHOD0(GetTerrainAabb, AZ::Aabb()); + MOCK_METHOD1(SetTerrainAabb, void(const AZ::Aabb&)); + MOCK_CONST_METHOD3(GetHeight, float(AZ::Vector3, Sampler, bool*)); + MOCK_CONST_METHOD4(GetHeightFromFloats, float(float, float, Sampler, bool*)); + MOCK_CONST_METHOD3(GetMaxSurfaceWeight, AzFramework::SurfaceData::SurfaceTagWeight(AZ::Vector3, Sampler, bool*)); + MOCK_CONST_METHOD3(GetMaxSurfaceWeightFromVector2, AzFramework::SurfaceData::SurfaceTagWeight(const AZ::Vector2&, Sampler, bool*)); + MOCK_CONST_METHOD4(GetMaxSurfaceWeightFromFloats, AzFramework::SurfaceData::SurfaceTagWeight(float, float, Sampler, bool*)); + MOCK_CONST_METHOD4(GetSurfaceWeights, void(const AZ::Vector3&, AzFramework::SurfaceData::OrderedSurfaceTagWeightSet&, Sampler, bool*)); + MOCK_CONST_METHOD4(GetSurfaceWeightsFromVector2, void(const AZ::Vector2&, AzFramework::SurfaceData::OrderedSurfaceTagWeightSet&, Sampler, bool*)); + MOCK_CONST_METHOD5(GetSurfaceWeightsFromFloats, void(float, float, AzFramework::SurfaceData::OrderedSurfaceTagWeightSet&, Sampler, bool*)); + MOCK_CONST_METHOD3(GetMaxSurfaceName, const char*(AZ::Vector3, Sampler, bool*)); + MOCK_CONST_METHOD3(GetIsHoleFromFloats, bool(float, float, Sampler)); + MOCK_CONST_METHOD3(GetNormal, AZ::Vector3(AZ::Vector3, Sampler, bool*)); + MOCK_CONST_METHOD4(GetNormalFromFloats, AZ::Vector3(float, float, Sampler, bool*)); + }; +} // namespace UnitTest diff --git a/Gems/Terrain/Code/Mocks/Terrain/MockTerrainAreaSurfaceRequestBus.h b/Gems/Terrain/Code/Mocks/Terrain/MockTerrainAreaSurfaceRequestBus.h new file mode 100644 index 0000000000..c5a04b4265 --- /dev/null +++ b/Gems/Terrain/Code/Mocks/Terrain/MockTerrainAreaSurfaceRequestBus.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ +#pragma once + +#include +#include +#include + +namespace UnitTest +{ + class MockTerrainAreaSurfaceRequestBus : public Terrain::TerrainAreaSurfaceRequestBus::Handler + { + public: + MockTerrainAreaSurfaceRequestBus(AZ::EntityId entityId) + { + Terrain::TerrainAreaSurfaceRequestBus::Handler::BusConnect(entityId); + } + + ~MockTerrainAreaSurfaceRequestBus() + { + Terrain::TerrainAreaSurfaceRequestBus::Handler::BusDisconnect(); + } + + MOCK_METHOD0(Activate, void()); + MOCK_METHOD0(Deactivate, void()); + MOCK_CONST_METHOD2(GetSurfaceWeights, void(const AZ::Vector3&, AzFramework::SurfaceData::OrderedSurfaceTagWeightSet&)); + }; + +} // namespace UnitTest diff --git a/Gems/Terrain/Code/Mocks/Terrain/MockTerrainLayerSpawner.h b/Gems/Terrain/Code/Mocks/Terrain/MockTerrainLayerSpawner.h new file mode 100644 index 0000000000..d0551d0881 --- /dev/null +++ b/Gems/Terrain/Code/Mocks/Terrain/MockTerrainLayerSpawner.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ +#pragma once + +#include + +namespace UnitTest +{ + class MockTerrainLayerSpawnerComponent + : public AZ::Component + { + public: + AZ_COMPONENT(MockTerrainLayerSpawnerComponent, "{9F27C980-9826-4063-86D8-E981C1E842A3}"); + + static void Reflect([[maybe_unused]] AZ::ReflectContext* context) + { + } + + void Activate() override + { + } + + void Deactivate() override + { + } + + private: + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services) + { + services.push_back(AZ_CRC_CE("TerrainAreaService")); + } + }; + +} //namespace UnitTest diff --git a/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp index 4ea16018d1..01ad0bb77f 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp +++ b/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp @@ -65,7 +65,7 @@ namespace Terrain void TerrainHeightGradientListComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services) { services.push_back(AZ_CRC_CE("TerrainAreaService")); - services.push_back(AZ_CRC_CE("BoxShapeService")); + services.push_back(AZ_CRC_CE("AxisAlignedBoxShapeService")); } void TerrainHeightGradientListComponent::Reflect(AZ::ReflectContext* context) diff --git a/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.cpp index 245c98ccac..00ed9c004f 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.cpp +++ b/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.cpp @@ -102,7 +102,6 @@ namespace Terrain void TerrainLayerSpawnerComponent::Activate() { - AZ::TransformNotificationBus::Handler::BusConnect(GetEntityId()); LmbrCentral::ShapeComponentNotificationsBus::Handler::BusConnect(GetEntityId()); TerrainSpawnerRequestBus::Handler::BusConnect(GetEntityId()); @@ -114,8 +113,6 @@ namespace Terrain TerrainSystemServiceRequestBus::Broadcast(&TerrainSystemServiceRequestBus::Events::UnregisterArea, GetEntityId()); TerrainSpawnerRequestBus::Handler::BusDisconnect(); LmbrCentral::ShapeComponentNotificationsBus::Handler::BusDisconnect(); - AZ::TransformNotificationBus::Handler::BusDisconnect(); - } bool TerrainLayerSpawnerComponent::ReadInConfig(const AZ::ComponentConfig* baseConfig) @@ -138,13 +135,12 @@ namespace Terrain return false; } - void TerrainLayerSpawnerComponent::OnTransformChanged([[maybe_unused]] const AZ::Transform& local, [[maybe_unused]] const AZ::Transform& world) - { - RefreshArea(); - } - void TerrainLayerSpawnerComponent::OnShapeChanged([[maybe_unused]] ShapeChangeReasons changeReason) { + // This will notify us of both shape changes and transform changes. + // It's important to use this event for transform changes instead of listening to OnTransformChanged, because we need to guarantee + // the shape has received the transform change message and updated its internal state before passing it along to us. + RefreshArea(); } diff --git a/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.h b/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.h index c7398bf93e..683f9e9f06 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.h +++ b/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.h @@ -18,7 +18,6 @@ #include #include -#include #include #include @@ -56,7 +55,6 @@ namespace Terrain class TerrainLayerSpawnerComponent : public AZ::Component - , private AZ::TransformNotificationBus::Handler , private LmbrCentral::ShapeComponentNotificationsBus::Handler , private Terrain::TerrainSpawnerRequestBus::Handler { @@ -81,10 +79,6 @@ namespace Terrain bool WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const override; protected: - ////////////////////////////////////////////////////////////////////////// - // AZ::TransformNotificationBus::Handler - void OnTransformChanged(const AZ::Transform& local, const AZ::Transform& world) override; - // ShapeComponentNotificationsBus void OnShapeChanged(ShapeChangeReasons changeReason) override; diff --git a/Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.cpp new file mode 100644 index 0000000000..e9c235c1bd --- /dev/null +++ b/Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.cpp @@ -0,0 +1,321 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace Terrain +{ + void TerrainPhysicsColliderConfig::Reflect(AZ::ReflectContext* context) + { + if (auto serialize = azrtti_cast(context)) + { + serialize->Class() + ->Version(1) + ; + + if (auto edit = serialize->GetEditContext()) + { + edit->Class( + "Terrain Physics Collider Component", + "Provides terrain data to a physics collider with configurable surface mappings.") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ->Attribute(AZ::Edit::Attributes::AutoExpand, true); + } + } + } + + void TerrainPhysicsColliderComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services) + { + services.push_back(AZ_CRC_CE("PhysicsHeightfieldProviderService")); + } + + void TerrainPhysicsColliderComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services) + { + services.push_back(AZ_CRC_CE("PhysicsHeightfieldProviderService")); + } + + void TerrainPhysicsColliderComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services) + { + services.push_back(AZ_CRC_CE("AxisAlignedBoxShapeService")); + } + + void TerrainPhysicsColliderComponent::Reflect(AZ::ReflectContext* context) + { + TerrainPhysicsColliderConfig::Reflect(context); + + if (auto serialize = azrtti_cast(context)) + { + serialize->Class() + ->Version(0) + ->Field("Configuration", &TerrainPhysicsColliderComponent::m_configuration) + ; + } + } + + TerrainPhysicsColliderComponent::TerrainPhysicsColliderComponent(const TerrainPhysicsColliderConfig& configuration) + : m_configuration(configuration) + { + } + + TerrainPhysicsColliderComponent::TerrainPhysicsColliderComponent() + { + + } + + void TerrainPhysicsColliderComponent::Activate() + { + const auto entityId = GetEntityId(); + LmbrCentral::ShapeComponentNotificationsBus::Handler::BusConnect(entityId); + Physics::HeightfieldProviderRequestsBus::Handler::BusConnect(entityId); + AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusConnect(); + + NotifyListenersOfHeightfieldDataChange(); + } + + void TerrainPhysicsColliderComponent::Deactivate() + { + AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusDisconnect(); + Physics::HeightfieldProviderRequestsBus::Handler ::BusDisconnect(); + LmbrCentral::ShapeComponentNotificationsBus::Handler::BusDisconnect(); + } + + bool TerrainPhysicsColliderComponent::ReadInConfig(const AZ::ComponentConfig* baseConfig) + { + if (auto config = azrtti_cast(baseConfig)) + { + m_configuration = *config; + return true; + } + return false; + } + + bool TerrainPhysicsColliderComponent::WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const + { + if (auto config = azrtti_cast(outBaseConfig)) + { + *config = m_configuration; + return true; + } + return false; + } + + void TerrainPhysicsColliderComponent::NotifyListenersOfHeightfieldDataChange() + { + AZ::Aabb worldSize = AZ::Aabb::CreateNull(); + + LmbrCentral::ShapeComponentRequestsBus::EventResult( + worldSize, GetEntityId(), &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb); + + Physics::HeightfieldProviderNotificationBus::Broadcast( + &Physics::HeightfieldProviderNotificationBus::Events::OnHeightfieldDataChanged, worldSize); + } + + void TerrainPhysicsColliderComponent::OnShapeChanged([[maybe_unused]] ShapeChangeReasons changeReason) + { + // This will notify us of both shape changes and transform changes. + // It's important to use this event for transform changes instead of listening to OnTransformChanged, because we need to guarantee + // the shape has received the transform change message and updated its internal state before passing it along to us. + + NotifyListenersOfHeightfieldDataChange(); + } + + void TerrainPhysicsColliderComponent::OnTerrainDataCreateEnd() + { + // The terrain system has finished creating itself, so we should now have data for creating a heightfield. + NotifyListenersOfHeightfieldDataChange(); + } + + void TerrainPhysicsColliderComponent::OnTerrainDataDestroyBegin() + { + // The terrain system is starting to destroy itself, so notify listeners of a change since the heightfield + // will no longer have any valid data. + NotifyListenersOfHeightfieldDataChange(); + } + + void TerrainPhysicsColliderComponent::OnTerrainDataChanged( + [[maybe_unused]] const AZ::Aabb& dirtyRegion, [[maybe_unused]] TerrainDataChangedMask dataChangedMask) + { + NotifyListenersOfHeightfieldDataChange(); + } + + AZ::Aabb TerrainPhysicsColliderComponent::GetHeightfieldAabb() const + { + AZ::Aabb worldSize = AZ::Aabb::CreateNull(); + + LmbrCentral::ShapeComponentRequestsBus::EventResult( + worldSize, GetEntityId(), &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb); + + auto vector2Floor = [](const AZ::Vector2& in) + { + return AZ::Vector2(floor(in.GetX()), floor(in.GetY())); + }; + auto vector2Ceil = [](const AZ::Vector2& in) + { + return AZ::Vector2(ceil(in.GetX()), ceil(in.GetY())); + }; + + const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing(); + const AZ::Vector3 boundsMin = worldSize.GetMin(); + const AZ::Vector3 boundsMax = worldSize.GetMax(); + + const AZ::Vector2 gridMinBoundLower = vector2Floor(AZ::Vector2(boundsMin) / gridResolution) * gridResolution; + const AZ::Vector2 gridMaxBoundUpper = vector2Ceil(AZ::Vector2(boundsMax) / gridResolution) * gridResolution; + + return AZ::Aabb::CreateFromMinMaxValues( + gridMinBoundLower.GetX(), gridMinBoundLower.GetY(), boundsMin.GetZ(), + gridMaxBoundUpper.GetX(), gridMaxBoundUpper.GetY(), boundsMax.GetZ() + ); + } + + void TerrainPhysicsColliderComponent::GetHeightfieldHeightBounds(float& minHeightBounds, float& maxHeightBounds) const + { + AZ::Aabb heightfieldAabb = GetHeightfieldAabb(); + + // Because our terrain heights are relative to the center of the bounding box, the min and max allowable heights are also + // relative to the center. They are also clamped to the size of the bounding box. + minHeightBounds = -(heightfieldAabb.GetZExtent() / 2.0f); + maxHeightBounds = heightfieldAabb.GetZExtent() / 2.0f; + } + + AZ::Transform TerrainPhysicsColliderComponent::GetHeightfieldTransform() const + { + // We currently don't support rotation of terrain heightfields. + AZ::Vector3 translate; + AZ::TransformBus::EventResult(translate, GetEntityId(), &AZ::TransformBus::Events::GetWorldTranslation); + + AZ::Transform transform = AZ::Transform::CreateTranslation(translate); + + return transform; + } + + void TerrainPhysicsColliderComponent::GenerateHeightsInBounds(AZStd::vector& heights) const + { + const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing(); + + AZ::Aabb worldSize = GetHeightfieldAabb(); + + const float worldCenterZ = worldSize.GetCenter().GetZ(); + + int32_t gridWidth, gridHeight; + GetHeightfieldGridSize(gridWidth, gridHeight); + + heights.clear(); + heights.reserve(gridWidth * gridHeight); + + for (int32_t row = 0; row < gridHeight; row++) + { + const float y = row * gridResolution.GetY() + worldSize.GetMin().GetY(); + for (int32_t col = 0; col < gridWidth; col++) + { + const float x = col * gridResolution.GetX() + worldSize.GetMin().GetX(); + float height = 0.0f; + + AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( + height, &AzFramework::Terrain::TerrainDataRequests::GetHeightFromFloats, x, y, + AzFramework::Terrain::TerrainDataRequests::Sampler::DEFAULT, nullptr); + + heights.emplace_back(height - worldCenterZ); + } + } + } + + void TerrainPhysicsColliderComponent::GenerateHeightsAndMaterialsInBounds( + AZStd::vector& heightMaterials) const + { + const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing(); + + AZ::Aabb worldSize = GetHeightfieldAabb(); + + const float worldCenterZ = worldSize.GetCenter().GetZ(); + const float worldHeightBoundsMin = worldSize.GetMin().GetZ(); + const float worldHeightBoundsMax = worldSize.GetMax().GetZ(); + + int32_t gridWidth, gridHeight; + GetHeightfieldGridSize(gridWidth, gridHeight); + + heightMaterials.clear(); + heightMaterials.reserve(gridWidth * gridHeight); + + for (int32_t row = 0; row < gridHeight; row++) + { + const float y = row * gridResolution.GetY() + worldSize.GetMin().GetY(); + for (int32_t col = 0; col < gridWidth; col++) + { + const float x = col * gridResolution.GetX() + worldSize.GetMin().GetX(); + float height = 0.0f; + + bool terrainExists = true; + AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( + height, &AzFramework::Terrain::TerrainDataRequests::GetHeightFromFloats, x, y, + AzFramework::Terrain::TerrainDataRequests::Sampler::DEFAULT, &terrainExists); + + // Any heights that fall outside the range of our bounding box will get turned into holes. + if ((height < worldHeightBoundsMin) || (height > worldHeightBoundsMax)) + { + height = worldHeightBoundsMin; + terrainExists = false; + } + + Physics::HeightMaterialPoint point; + point.m_height = height - worldCenterZ; + point.m_quadMeshType = terrainExists ? Physics::QuadMeshType::SubdivideUpperLeftToBottomRight : Physics::QuadMeshType::Hole; + heightMaterials.emplace_back(point); + } + } + } + + AZ::Vector2 TerrainPhysicsColliderComponent::GetHeightfieldGridSpacing() const + { + AZ::Vector2 gridResolution = AZ::Vector2(1.0f); + AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( + gridResolution, &AzFramework::Terrain::TerrainDataRequests::GetTerrainHeightQueryResolution); + + return gridResolution; + } + + void TerrainPhysicsColliderComponent::GetHeightfieldGridSize(int32_t& numColumns, int32_t& numRows) const + { + const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing(); + const AZ::Aabb bounds = GetHeightfieldAabb(); + + numColumns = aznumeric_cast((bounds.GetMax().GetX() - bounds.GetMin().GetX()) / gridResolution.GetX()); + numRows = aznumeric_cast((bounds.GetMax().GetY() - bounds.GetMin().GetY()) / gridResolution.GetY()); + } + + AZStd::vector TerrainPhysicsColliderComponent::GetMaterialList() const + { + return AZStd::vector(); + } + + AZStd::vector TerrainPhysicsColliderComponent::GetHeights() const + { + AZStd::vector heights; + GenerateHeightsInBounds(heights); + + return heights; + } + + AZStd::vector TerrainPhysicsColliderComponent::GetHeightsAndMaterials() const + { + AZStd::vector heightMaterials; + GenerateHeightsAndMaterialsInBounds(heightMaterials); + + return heightMaterials; + } +} diff --git a/Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.h b/Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.h new file mode 100644 index 0000000000..e268223689 --- /dev/null +++ b/Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.h @@ -0,0 +1,91 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include + +#include +#include +#include + +#include + +namespace LmbrCentral +{ + template + class EditorWrappedComponentBase; +} + +namespace Terrain +{ + class TerrainPhysicsColliderConfig + : public AZ::ComponentConfig + { + public: + AZ_CLASS_ALLOCATOR(TerrainPhysicsColliderConfig, AZ::SystemAllocator, 0); + AZ_RTTI(TerrainPhysicsColliderConfig, "{E9EADB8F-C3A5-4B9C-A62D-2DBC86B4CE59}", AZ::ComponentConfig); + static void Reflect(AZ::ReflectContext* context); + + }; + + + class TerrainPhysicsColliderComponent + : public AZ::Component + , public Physics::HeightfieldProviderRequestsBus::Handler + , protected LmbrCentral::ShapeComponentNotificationsBus::Handler + , protected AzFramework::Terrain::TerrainDataNotificationBus::Handler + { + public: + template + friend class LmbrCentral::EditorWrappedComponentBase; + AZ_COMPONENT(TerrainPhysicsColliderComponent, "{33C20287-1D37-44D0-96A0-2C3766E23624}"); + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services); + static void Reflect(AZ::ReflectContext* context); + + TerrainPhysicsColliderComponent(const TerrainPhysicsColliderConfig& configuration); + TerrainPhysicsColliderComponent(); + ~TerrainPhysicsColliderComponent() = default; + + // HeightfieldProviderRequestsBus + AZ::Vector2 GetHeightfieldGridSpacing() const override; + void GetHeightfieldGridSize(int32_t& numColumns, int32_t& numRows) const override; + void GetHeightfieldHeightBounds(float& minHeightBounds, float& maxHeightBounds) const override; + AZ::Aabb GetHeightfieldAabb() const override; + AZ::Transform GetHeightfieldTransform() const override; + AZStd::vector GetMaterialList() const override; + AZStd::vector GetHeights() const override; + AZStd::vector GetHeightsAndMaterials() const override; + + protected: + ////////////////////////////////////////////////////////////////////////// + // AZ::Component interface implementation + void Activate() override; + void Deactivate() override; + bool ReadInConfig(const AZ::ComponentConfig* baseConfig) override; + bool WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const override; + + void GenerateHeightsInBounds(AZStd::vector& heights) const; + void GenerateHeightsAndMaterialsInBounds(AZStd::vector& heightMaterials) const; + + void NotifyListenersOfHeightfieldDataChange(); + + // ShapeComponentNotificationsBus + void OnShapeChanged(ShapeChangeReasons changeReason) override; + + void OnTerrainDataCreateEnd() override; + void OnTerrainDataDestroyBegin() override; + void OnTerrainDataChanged(const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask) override; + + private: + TerrainPhysicsColliderConfig m_configuration; + }; +} diff --git a/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.cpp b/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.cpp new file mode 100644 index 0000000000..6df6c3b440 --- /dev/null +++ b/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include + +namespace Terrain +{ + void EditorTerrainPhysicsColliderComponent::Reflect(AZ::ReflectContext* context) + { + // Call ReflectSubClass in EditorWrappedComponentBase to handle all the boilerplate reflection. + BaseClassType::ReflectSubClass( + context, 1, + &LmbrCentral::EditorWrappedComponentBaseVersionConverter + ); + } +} diff --git a/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.h b/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.h new file mode 100644 index 0000000000..d2254161a0 --- /dev/null +++ b/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include + +namespace Terrain +{ + class EditorTerrainPhysicsColliderComponent + : public LmbrCentral::EditorWrappedComponentBase + { + public: + using BaseClassType = LmbrCentral::EditorWrappedComponentBase; + AZ_EDITOR_COMPONENT(EditorTerrainPhysicsColliderComponent, "{C43FAB8F-3968-46A6-920E-E84AEDED3DF5}", BaseClassType); + static void Reflect(AZ::ReflectContext* context); + + static constexpr auto s_categoryName = "Terrain"; + static constexpr auto s_componentName = "Terrain Physics Heightfield Collider"; + static constexpr auto s_componentDescription = "Provides terrain data to a physics collider in the form of a heightfield and surface->material mapping."; + static constexpr auto s_icon = "Editor/Icons/Components/TerrainLayerSpawner.svg"; + static constexpr auto s_viewportIcon = "Editor/Icons/Components/Viewport/TerrainLayerSpawner.svg"; + static constexpr auto s_helpUrl = ""; + }; +} diff --git a/Gems/Terrain/Code/Source/EditorTerrainModule.cpp b/Gems/Terrain/Code/Source/EditorTerrainModule.cpp index 6fd8979e4a..0ea822e8b5 100644 --- a/Gems/Terrain/Code/Source/EditorTerrainModule.cpp +++ b/Gems/Terrain/Code/Source/EditorTerrainModule.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -32,6 +33,7 @@ namespace Terrain Terrain::EditorTerrainSurfaceMaterialsListComponent::CreateDescriptor(), Terrain::EditorTerrainWorldComponent::CreateDescriptor(), Terrain::EditorTerrainWorldDebuggerComponent::CreateDescriptor(), + Terrain::EditorTerrainPhysicsColliderComponent::CreateDescriptor(), Terrain::EditorTerrainWorldRendererComponent::CreateDescriptor(), }); diff --git a/Gems/Terrain/Code/Source/TerrainModule.cpp b/Gems/Terrain/Code/Source/TerrainModule.cpp index c3a7890565..2524f3684c 100644 --- a/Gems/Terrain/Code/Source/TerrainModule.cpp +++ b/Gems/Terrain/Code/Source/TerrainModule.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -37,6 +38,7 @@ namespace Terrain TerrainMacroMaterialComponent::CreateDescriptor(), TerrainSurfaceGradientListComponent::CreateDescriptor(), TerrainSurfaceDataSystemComponent::CreateDescriptor(), + TerrainPhysicsColliderComponent::CreateDescriptor() }); } diff --git a/Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp b/Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp index ee4b18e2fb..0ca5e7d99a 100644 --- a/Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp +++ b/Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp @@ -7,6 +7,7 @@ */ #include +#include #include #include @@ -195,8 +196,10 @@ TEST_F(LayerSpawnerComponentTest, LayerSpawnerTransformChangedUpdatesTerrainSyst m_entity->Activate(); - AZ::TransformNotificationBus::Event( - m_entity->GetId(), &AZ::TransformNotificationBus::Events::OnTransformChanged, AZ::Transform(), AZ::Transform()); + // The component gets transform change notifications via the shape bus. + LmbrCentral::ShapeComponentNotificationsBus::Event( + m_entity->GetId(), &LmbrCentral::ShapeComponentNotificationsBus::Events::OnShapeChanged, + LmbrCentral::ShapeComponentNotifications::ShapeChangeReasons::TransformChanged); m_entity->Deactivate(); } diff --git a/Gems/Terrain/Code/Tests/TerrainHeightGradientListTests.cpp b/Gems/Terrain/Code/Tests/TerrainHeightGradientListTests.cpp new file mode 100644 index 0000000000..c314e4e968 --- /dev/null +++ b/Gems/Terrain/Code/Tests/TerrainHeightGradientListTests.cpp @@ -0,0 +1,146 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +using ::testing::_; +using ::testing::AtLeast; +using ::testing::Mock; +using ::testing::NiceMock; +using ::testing::Return; + +class TerrainHeightGradientListComponentTest : public ::testing::Test +{ +protected: + AZ::ComponentApplication m_app; + + AZStd::unique_ptr m_entity; + + void SetUp() override + { + AZ::ComponentApplication::Descriptor appDesc; + appDesc.m_memoryBlocksByteSize = 20 * 1024 * 1024; + appDesc.m_recordingMode = AZ::Debug::AllocationRecords::RECORD_NO_RECORDS; + appDesc.m_stackRecordLevels = 20; + + m_app.Create(appDesc); + } + + void TearDown() override + { + m_app.Destroy(); + } + + void CreateEntity() + { + m_entity = AZStd::make_unique(); + ASSERT_TRUE(m_entity); + + // Create the required box component. + UnitTest::MockAxisAlignedBoxShapeComponent* boxComponent = m_entity->CreateComponent(); + m_app.RegisterComponentDescriptor(boxComponent->CreateDescriptor()); + + // Create the TerrainHeightGradientListComponent with an entity in its configuration. + Terrain::TerrainHeightGradientListConfig config; + config.m_gradientEntities.push_back(m_entity->GetId()); + + Terrain::TerrainHeightGradientListComponent* heightGradientListComponent = m_entity->CreateComponent(config); + m_app.RegisterComponentDescriptor(heightGradientListComponent->CreateDescriptor()); + + // Create a MockTerrainLayerSpawnerComponent to provide the required TerrainAreaService. + UnitTest::MockTerrainLayerSpawnerComponent* layerSpawner = m_entity->CreateComponent(); + m_app.RegisterComponentDescriptor(layerSpawner->CreateDescriptor()); + + m_entity->Init(); + } +}; + +TEST_F(TerrainHeightGradientListComponentTest, ActivateEntityActivateSuccess) +{ + // Check that the entity activates. + CreateEntity(); + + m_entity->Activate(); + EXPECT_EQ(m_entity->GetState(), AZ::Entity::State::Active); + + m_entity.reset(); +} + +TEST_F(TerrainHeightGradientListComponentTest, TerrainHeightGradientRefreshesTerrainSystem) +{ + // Check that the HeightGradientListComponent informs the TerrainSystem when the composition changes. + CreateEntity(); + + m_entity->Activate(); + + NiceMock terrainSystem; + + // As the TerrainHeightGradientListComponent subscribes to the dependency monitor, RefreshArea will be called twice: + // once due to OnCompositionChanged being picked up by the the dependency monitor and resending the notification, + // and once when the HeightGradientListComponent gets the OnCompositionChanged directly through the DependencyNotificationBus. + EXPECT_CALL(terrainSystem, RefreshArea(_)).Times(2); + + LmbrCentral::DependencyNotificationBus::Event(m_entity->GetId(), &LmbrCentral::DependencyNotificationBus::Events::OnCompositionChanged); + + // Stop the EXPECT_CALL check now, as OnCompositionChanged will get called twice again during the reset. + Mock::VerifyAndClearExpectations(&terrainSystem); + + m_entity.reset(); +} + +TEST_F(TerrainHeightGradientListComponentTest, TerrainHeightGradientListReturnsHeights) +{ + // Check that the HeightGradientListComponent returns expected height values. + CreateEntity(); + + NiceMock heightfieldRequestBus(m_entity->GetId()); + + m_entity->Activate(); + + const float mockGradientValue = 0.25f; + NiceMock gradientRequests(m_entity->GetId()); + ON_CALL(gradientRequests, GetValue).WillByDefault(Return(mockGradientValue)); + + // Setup a mock to provide the encompassing Aabb to the HeightGradientListComponent. + const float min = 0.0f; + const float max = 1000.0f; + const AZ::Aabb aabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3(min), AZ::Vector3(max)); + NiceMock mockShapeRequests(m_entity->GetId()); + ON_CALL(mockShapeRequests, GetEncompassingAabb).WillByDefault(Return(aabb)); + + const float worldMax = 10000.0f; + const AZ::Aabb worldAabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3(min), AZ::Vector3(worldMax)); + NiceMock mockterrainDataRequests; + ON_CALL(mockterrainDataRequests, GetTerrainHeightQueryResolution).WillByDefault(Return(AZ::Vector2(1.0f))); + ON_CALL(mockterrainDataRequests, GetTerrainAabb).WillByDefault(Return(worldAabb)); + + // Ensure the cached values in the HeightGradientListComponent are up to date. + LmbrCentral::DependencyNotificationBus::Event(m_entity->GetId(), &LmbrCentral::DependencyNotificationBus::Events::OnCompositionChanged); + + const AZ::Vector3 inPosition = AZ::Vector3::CreateZero(); + AZ::Vector3 outPosition = AZ::Vector3::CreateZero(); + bool terrainExists = false; + Terrain::TerrainAreaHeightRequestBus::Event(m_entity->GetId(), &Terrain::TerrainAreaHeightRequestBus::Events::GetHeight, inPosition, outPosition, terrainExists); + + const float height = outPosition.GetZ(); + + EXPECT_NEAR(height, mockGradientValue * max, 0.01f); + + m_entity.reset(); +} + diff --git a/Gems/Terrain/Code/Tests/TerrainPhysicsColliderTests.cpp b/Gems/Terrain/Code/Tests/TerrainPhysicsColliderTests.cpp new file mode 100644 index 0000000000..d1deca897c --- /dev/null +++ b/Gems/Terrain/Code/Tests/TerrainPhysicsColliderTests.cpp @@ -0,0 +1,292 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +using ::testing::NiceMock; +using ::testing::AtLeast; +using ::testing::_; +using ::testing::Return; + +class TerrainPhysicsColliderComponentTest + : public ::testing::Test +{ +protected: + AZ::ComponentApplication m_app; + + AZStd::unique_ptr m_entity; + Terrain::TerrainPhysicsColliderComponent* m_colliderComponent; + UnitTest::MockAxisAlignedBoxShapeComponent* m_boxComponent; + + void SetUp() override + { + AZ::ComponentApplication::Descriptor appDesc; + appDesc.m_memoryBlocksByteSize = 20 * 1024 * 1024; + appDesc.m_recordingMode = AZ::Debug::AllocationRecords::RECORD_NO_RECORDS; + appDesc.m_stackRecordLevels = 20; + + m_app.Create(appDesc); + } + + void TearDown() override + { + m_app.Destroy(); + } + + void CreateEntity() + { + m_entity = AZStd::make_unique(); + ASSERT_TRUE(m_entity); + + m_entity->Init(); + } + + void AddTerrainPhysicsColliderAndShapeComponentToEntity() + { + m_boxComponent = m_entity->CreateComponent(); + m_app.RegisterComponentDescriptor(m_boxComponent->CreateDescriptor()); + + m_colliderComponent = m_entity->CreateComponent(Terrain::TerrainPhysicsColliderConfig()); + m_app.RegisterComponentDescriptor(m_colliderComponent->CreateDescriptor()); + } +}; + +TEST_F(TerrainPhysicsColliderComponentTest, ActivateEntityActivateSuccess) +{ + // Check that the entity activates with a collider and the required shape attached. + CreateEntity(); + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + EXPECT_EQ(m_entity->GetState(), AZ::Entity::State::Active); + + m_entity.reset(); +} + +TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderTransformChangedNotifiesHeightfieldBus) +{ + // Check that the HeightfieldBus is notified when the transform of the entity changes. + CreateEntity(); + + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + + NiceMock heightfieldListener(m_entity->GetId()); + EXPECT_CALL(heightfieldListener, OnHeightfieldDataChanged(_)).Times(1); + + // The component gets transform change notifications via the shape bus. + LmbrCentral::ShapeComponentNotificationsBus::Event( + m_entity->GetId(), &LmbrCentral::ShapeComponentNotificationsBus::Events::OnShapeChanged, + LmbrCentral::ShapeComponentNotifications::ShapeChangeReasons::TransformChanged); + + m_entity.reset(); +} + +TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderShapeChangedNotifiesHeightfieldBus) +{ + // Check that the Heightfield bus is notified when the shape component changes. + CreateEntity(); + + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + + NiceMock heightfieldListener(m_entity->GetId()); + EXPECT_CALL(heightfieldListener, OnHeightfieldDataChanged(_)).Times(1); + + LmbrCentral::ShapeComponentNotificationsBus::Event( + m_entity->GetId(), &LmbrCentral::ShapeComponentNotificationsBus::Events::OnShapeChanged, + LmbrCentral::ShapeComponentNotifications::ShapeChangeReasons::ShapeChanged); + + m_entity.reset(); +} + +TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderReturnsAlignedRowBoundsCorrectly) +{ + // Check that the heightfield grid size is correct when the shape bounds match the grid resolution. + CreateEntity(); + + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + + const float boundsMin = 0.0f; + const float boundsMax = 1024.0f; + + NiceMock boxShape(m_entity->GetId()); + const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax)); + ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds)); + + const AZ::Vector2 mockHeightResolution = AZ::Vector2(1.0f); + NiceMock terrainListener; + ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution)); + + int32_t cols, rows; + Physics::HeightfieldProviderRequestsBus::Event( + m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows); + + // With the bounds set at 0-1024 and a resolution of 1.0, the heightfield grid should be 1024x1024. + EXPECT_EQ(cols, 1024); + EXPECT_EQ(rows, 1024); + + m_entity.reset(); +} + +TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderExpandsMinBoundsCorrectly) +{ + // Check that the heightfield grid is correctly expanded if the minimum value of the bounds needs expanding + // to correctly encompass it. + CreateEntity(); + + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + + const float boundsMin = 0.1f; + const float boundsMax = 1024.0f; + + NiceMock boxShape(m_entity->GetId()); + const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax)); + ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds)); + + AZ::Vector2 mockHeightResolution = AZ::Vector2(1.0f); + NiceMock terrainListener; + ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution)); + + int32_t cols, rows; + Physics::HeightfieldProviderRequestsBus::Event( + m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows); + + // If the heightfield is not expanded to ensure it encompasses the shape bounds, + // the values returned would be 1023. + EXPECT_EQ(cols, 1024); + EXPECT_EQ(rows, 1024); + + m_entity.reset(); +} + +TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderExpandsMaxBoundsCorrectly) +{ + // Check that the heightfield grid is correctly expanded if the maximum value of the bounds needs expanding + // to correctly encompass it. + CreateEntity(); + + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + + const float boundsMin = 0.0f; + const float boundsMax = 1023.5f; + + NiceMock boxShape(m_entity->GetId()); + const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax)); + ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds)); + + AZ::Vector2 mockHeightResolution = AZ::Vector2(1.0f); + NiceMock terrainListener; + ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution)); + + int32_t cols, rows; + Physics::HeightfieldProviderRequestsBus::Event( + m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows); + + // If the heightfield is not expanded to ensure it encompasses the shape bounds, + // the values returned would be 1023. + EXPECT_EQ(cols, 1024); + EXPECT_EQ(rows, 1024); + + m_entity.reset(); +} + +TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderGetHeightsReturnsHeights) +{ + // Check that the TerrainPhysicsCollider returns a heightfield of the expected size. + CreateEntity(); + + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + + const float boundsMin = 0.0f; + const float boundsMax = 1024.0f; + + NiceMock boxShape(m_entity->GetId()); + const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax)); + ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds)); + + AZ::Vector2 mockHeightResolution = AZ::Vector2(1.0f); + NiceMock terrainListener; + ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution)); + + int32_t cols, rows; + Physics::HeightfieldProviderRequestsBus::Event( + m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows); + + AZStd::vector heights; + + Physics::HeightfieldProviderRequestsBus::EventResult( + heights, m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeights); + + EXPECT_EQ(cols, 1024); + EXPECT_EQ(rows, 1024); + EXPECT_EQ(heights.size(), cols * rows); + + m_entity.reset(); +} + +TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderReturnsRelativeHeightsCorrectly) +{ + // Check that the values stored in the heightfield returned by the TerrainPhysicsCollider are correct. + CreateEntity(); + + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + + const AZ::Vector3 boundsMin = AZ::Vector3(0.0f); + const AZ::Vector3 boundsMax = AZ::Vector3(256.0f, 256.0f, 32768.0f); + + const float mockHeight = 32768.0f; + AZ::Vector2 mockHeightResolution = AZ::Vector2(1.0f); + + NiceMock terrainListener; + ON_CALL(terrainListener, GetHeightFromFloats).WillByDefault(Return(mockHeight)); + ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution)); + + // Just return the bounds as setup. This is equivalent to the box being at the origin. + NiceMock boxShape(m_entity->GetId()); + const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax)); + ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds)); + + AZStd::vector heights; + + Physics::HeightfieldProviderRequestsBus::EventResult(heights, m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeights); + + ASSERT_FALSE(heights.empty()); + + const float expectedHeightValue = 16384.0f; + EXPECT_NEAR(heights[0], expectedHeightValue, 0.01f); + + m_entity->Reset(); +} diff --git a/Gems/Terrain/Code/Tests/TerrainSurfaceGradientListTests.cpp b/Gems/Terrain/Code/Tests/TerrainSurfaceGradientListTests.cpp new file mode 100644 index 0000000000..2a645b3f94 --- /dev/null +++ b/Gems/Terrain/Code/Tests/TerrainSurfaceGradientListTests.cpp @@ -0,0 +1,129 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include + +using ::testing::NiceMock; +using ::testing::AtLeast; +using ::testing::_; +using ::testing::Return; + +namespace UnitTest +{ + class TerrainSurfaceGradientListTest : public ::testing::Test + { + protected: + AZ::ComponentApplication m_app; + + AZStd::unique_ptr m_entity; + UnitTest::MockTerrainLayerSpawnerComponent* m_layerSpawnerComponent = nullptr; + AZStd::unique_ptr m_gradientEntity1, m_gradientEntity2; + + const AZStd::string surfaceTag1 = "testtag1"; + const AZStd::string surfaceTag2 = "testtag2"; + + void SetUp() override + { + AZ::ComponentApplication::Descriptor appDesc; + appDesc.m_memoryBlocksByteSize = 20 * 1024 * 1024; + appDesc.m_recordingMode = AZ::Debug::AllocationRecords::RECORD_NO_RECORDS; + appDesc.m_stackRecordLevels = 20; + + m_app.Create(appDesc); + + CreateEntities(); + } + + void TearDown() override + { + m_gradientEntity2.reset(); + m_gradientEntity1.reset(); + m_entity.reset(); + + m_app.Destroy(); + } + + void CreateEntities() + { + m_entity = AZStd::make_unique(); + ASSERT_TRUE(m_entity); + + m_entity->Init(); + + m_gradientEntity1 = AZStd::make_unique(); + ASSERT_TRUE(m_gradientEntity1); + + m_gradientEntity1->Init(); + + m_gradientEntity2 = AZStd::make_unique(); + ASSERT_TRUE(m_gradientEntity2); + + m_gradientEntity2->Init(); + } + + void AddSurfaceGradientListToEntities() + { + m_layerSpawnerComponent = m_entity->CreateComponent(); + m_app.RegisterComponentDescriptor(m_layerSpawnerComponent->CreateDescriptor()); + + Terrain::TerrainSurfaceGradientListConfig config; + + Terrain::TerrainSurfaceGradientMapping mapping1; + mapping1.m_gradientEntityId = m_gradientEntity1->GetId(); + mapping1.m_surfaceTag = SurfaceData::SurfaceTag(surfaceTag1); + config.m_gradientSurfaceMappings.emplace_back(mapping1); + + Terrain::TerrainSurfaceGradientMapping mapping2; + mapping2.m_gradientEntityId = m_gradientEntity2->GetId(); + mapping2.m_surfaceTag = SurfaceData::SurfaceTag(surfaceTag2); + config.m_gradientSurfaceMappings.emplace_back(mapping2); + + Terrain::TerrainSurfaceGradientListComponent* terrainSurfaceGradientListComponent = + m_entity->CreateComponent(config); + m_app.RegisterComponentDescriptor(terrainSurfaceGradientListComponent->CreateDescriptor()); + } + }; + + TEST_F(TerrainSurfaceGradientListTest, SurfaceGradientReturnsSurfaceWeightsInOrder) + { + // When there is more that one surface/weight defined and added to the component, they should all + // be returned in descending weight order. + AddSurfaceGradientListToEntities(); + + m_entity->Activate(); + m_gradientEntity1->Activate(); + m_gradientEntity2->Activate(); + + const float gradient1Value = 0.3f; + NiceMock mockGradientRequests1(m_gradientEntity1->GetId()); + ON_CALL(mockGradientRequests1, GetValue).WillByDefault(Return(gradient1Value)); + + const float gradient2Value = 1.0f; + NiceMock mockGradientRequests2(m_gradientEntity2->GetId()); + ON_CALL(mockGradientRequests2, GetValue).WillByDefault(Return(gradient2Value)); + + AzFramework::SurfaceData::OrderedSurfaceTagWeightSet weightSet; + Terrain::TerrainAreaSurfaceRequestBus::Event( + m_entity->GetId(), &Terrain::TerrainAreaSurfaceRequestBus::Events::GetSurfaceWeights, AZ::Vector3::CreateZero(), weightSet); + + AZ::Crc32 expectedCrcList[] = { AZ::Crc32(surfaceTag2), AZ::Crc32(surfaceTag1) }; + const float expectedWeightList[] = { gradient2Value, gradient1Value }; + + int index = 0; + for (const auto& surfaceWeight : weightSet) + { + EXPECT_EQ(surfaceWeight.m_surfaceType, expectedCrcList[index]); + EXPECT_NEAR(surfaceWeight.m_weight, expectedWeightList[index], 0.01f); + index++; + } + } +} // namespace UnitTest + + diff --git a/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp b/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp index e4fb0f348e..f273a85e26 100644 --- a/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp +++ b/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp @@ -13,9 +13,10 @@ #include #include -#include +#include #include +#include #include using ::testing::AtLeast; @@ -25,284 +26,284 @@ using ::testing::IsFalse; using ::testing::Ne; using ::testing::NiceMock; using ::testing::Return; +using ::testing::SetArgReferee; -class TerrainSystemTest : public ::testing::Test +namespace UnitTest { -protected: - // Defines a structure for defining both an XY position and the expected height for that position. - struct HeightTestPoint + class TerrainSystemTest : public ::testing::Test { - AZ::Vector2 m_testLocation; - float m_expectedHeight; - }; + protected: + // Defines a structure for defining both an XY position and the expected height for that position. + struct HeightTestPoint + { + AZ::Vector2 m_testLocation = AZ::Vector2::CreateZero(); + float m_expectedHeight = 0.0f; + }; - AZ::ComponentApplication m_app; - AZStd::unique_ptr m_terrainSystem; + AZ::ComponentApplication m_app; + AZStd::unique_ptr m_terrainSystem; - AZStd::unique_ptr> m_boxShapeRequests; - AZStd::unique_ptr> m_shapeRequests; - AZStd::unique_ptr> m_terrainAreaHeightRequests; + AZStd::unique_ptr> m_boxShapeRequests; + AZStd::unique_ptr> m_shapeRequests; + AZStd::unique_ptr> m_terrainAreaHeightRequests; + void SetUp() override + { + AZ::ComponentApplication::Descriptor appDesc; + appDesc.m_memoryBlocksByteSize = 20 * 1024 * 1024; + appDesc.m_recordingMode = AZ::Debug::AllocationRecords::RECORD_NO_RECORDS; + appDesc.m_stackRecordLevels = 20; - void SetUp() override - { - AZ::ComponentApplication::Descriptor appDesc; - appDesc.m_memoryBlocksByteSize = 20 * 1024 * 1024; - appDesc.m_recordingMode = AZ::Debug::AllocationRecords::RECORD_NO_RECORDS; - appDesc.m_stackRecordLevels = 20; + m_app.Create(appDesc); + } - m_app.Create(appDesc); - } + void TearDown() override + { + m_terrainSystem.reset(); + m_boxShapeRequests.reset(); + m_shapeRequests.reset(); + m_terrainAreaHeightRequests.reset(); + m_app.Destroy(); + } - void TearDown() override - { - m_terrainSystem.reset(); - m_boxShapeRequests.reset(); - m_shapeRequests.reset(); - m_terrainAreaHeightRequests.reset(); - m_app.Destroy(); - } + AZStd::unique_ptr CreateEntity() + { + return AZStd::make_unique(); + } - AZStd::unique_ptr CreateEntity() - { - return AZStd::make_unique(); - } + void ActivateEntity(AZ::Entity* entity) + { + entity->Init(); + EXPECT_EQ(AZ::Entity::State::Init, entity->GetState()); - void ActivateEntity(AZ::Entity* entity) - { - entity->Init(); - EXPECT_EQ(AZ::Entity::State::Init, entity->GetState()); + entity->Activate(); + EXPECT_EQ(AZ::Entity::State::Active, entity->GetState()); + } - entity->Activate(); - EXPECT_EQ(AZ::Entity::State::Active, entity->GetState()); - } + template + AZ::Component* CreateComponent(AZ::Entity* entity, const Configuration& config) + { + m_app.RegisterComponentDescriptor(Component::CreateDescriptor()); + return entity->CreateComponent(config); + } - template - AZ::Component* CreateComponent(AZ::Entity* entity, const Configuration& config) - { - m_app.RegisterComponentDescriptor(Component::CreateDescriptor()); - return entity->CreateComponent(config); - } + template + AZ::Component* CreateComponent(AZ::Entity* entity) + { + m_app.RegisterComponentDescriptor(Component::CreateDescriptor()); + return entity->CreateComponent(); + } - template - AZ::Component* CreateComponent(AZ::Entity* entity) + // Create a terrain system with reasonable defaults for testing, but with the ability to override the defaults + // on a test-by-test basis. + void CreateAndActivateTerrainSystem( + AZ::Vector2 queryResolution = AZ::Vector2(1.0f), + AZ::Aabb worldBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-128.0f), AZ::Vector3(128.0f))) + { + // Create the terrain system and give it one tick to fully initialize itself. + m_terrainSystem = AZStd::make_unique(); + m_terrainSystem->SetTerrainAabb(worldBounds); + m_terrainSystem->SetTerrainHeightQueryResolution(queryResolution); + m_terrainSystem->Activate(); + AZ::TickBus::Broadcast(&AZ::TickBus::Events::OnTick, 0.f, AZ::ScriptTimePoint{}); + } + + AZStd::unique_ptr CreateAndActivateMockTerrainLayerSpawner( + const AZ::Aabb& spawnerBox, const AZStd::function& mockHeights) + { + // Create the base entity with a mock box shape, Terrain Layer Spawner, and height provider. + auto entity = CreateEntity(); + CreateComponent(entity.get()); + CreateComponent(entity.get()); + + m_boxShapeRequests = AZStd::make_unique>(entity->GetId()); + m_shapeRequests = AZStd::make_unique>(entity->GetId()); + + // Set up the box shape to return whatever spawnerBox was passed in. + ON_CALL(*m_shapeRequests, GetEncompassingAabb).WillByDefault(Return(spawnerBox)); + + // Set up a mock height provider to use the passed-in mock height function to generate a height. + m_terrainAreaHeightRequests = AZStd::make_unique>(entity->GetId()); + ON_CALL(*m_terrainAreaHeightRequests, GetHeight) + .WillByDefault( + [mockHeights](const AZ::Vector3& inPosition, AZ::Vector3& outPosition, bool& terrainExists) + { + // By default, set the outPosition to the input position and terrain to always exist. + outPosition = inPosition; + terrainExists = true; + // Let the test function modify these values based on the needs of the specific test. + mockHeights(outPosition, terrainExists); + }); + + ActivateEntity(entity.get()); + return entity; + } + }; + + TEST_F(TerrainSystemTest, TrivialCreateDestroy) { - m_app.RegisterComponentDescriptor(Component::CreateDescriptor()); - return entity->CreateComponent(); + // Trivially verify that the terrain system can successfully be constructed and destructed without errors. + + m_terrainSystem = AZStd::make_unique(); } - // Create a terrain system with reasonable defaults for testing, but with the ability to override the defaults - // on a test-by-test basis. - void CreateAndActivateTerrainSystem( - AZ::Vector2 queryResolution = AZ::Vector2(1.0f), - AZ::Aabb worldBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-128.0f), AZ::Vector3(128.0f))) + TEST_F(TerrainSystemTest, TrivialActivateDeactivate) { - // Create the terrain system and give it one tick to fully initialize itself. + // Verify that the terrain system can be activated and deactivated without errors. + m_terrainSystem = AZStd::make_unique(); - m_terrainSystem->SetTerrainAabb(worldBounds); - m_terrainSystem->SetTerrainHeightQueryResolution(queryResolution); m_terrainSystem->Activate(); - AZ::TickBus::Broadcast(&AZ::TickBus::Events::OnTick, 0.f, AZ::ScriptTimePoint{}); + m_terrainSystem->Deactivate(); } - - AZStd::unique_ptr CreateAndActivateMockTerrainLayerSpawner( - const AZ::Aabb& spawnerBox, - const AZStd::function& mockHeights) + TEST_F(TerrainSystemTest, CreateEventsCalledOnActivation) { - // Create the base entity with a mock box shape, Terrain Layer Spawner, and height provider. - auto entity = CreateEntity(); - CreateComponent(entity.get()); - CreateComponent(entity.get()); - - m_boxShapeRequests = AZStd::make_unique>(entity->GetId()); - m_shapeRequests = AZStd::make_unique>(entity->GetId()); - - // Set up the box shape to return whatever spawnerBox was passed in. - ON_CALL(*m_shapeRequests, GetEncompassingAabb).WillByDefault(Return(spawnerBox)); - - // Set up a mock height provider to use the passed-in mock height function to generate a height. - m_terrainAreaHeightRequests = AZStd::make_unique>(entity->GetId()); - ON_CALL(*m_terrainAreaHeightRequests, GetHeight) - .WillByDefault( - [mockHeights](const AZ::Vector3& inPosition, AZ::Vector3& outPosition, bool& terrainExists) - { - // By default, set the outPosition to the input position and terrain to always exist. - outPosition = inPosition; - terrainExists = true; - // Let the test function modify these values based on the needs of the specific test. - mockHeights(outPosition, terrainExists); - }); - - ActivateEntity(entity.get()); - return entity; - } -}; - -TEST_F(TerrainSystemTest, TrivialCreateDestroy) -{ - // Trivially verify that the terrain system can successfully be constructed and destructed without errors. - - m_terrainSystem = AZStd::make_unique(); -} + // Verify that when the terrain system is activated, the OnTerrainDataCreate* ebus notifications are generated. -TEST_F(TerrainSystemTest, TrivialActivateDeactivate) -{ - // Verify that the terrain system can be activated and deactivated without errors. - - m_terrainSystem = AZStd::make_unique(); - m_terrainSystem->Activate(); - m_terrainSystem->Deactivate(); -} - -TEST_F(TerrainSystemTest, CreateEventsCalledOnActivation) -{ - // Verify that when the terrain system is activated, the OnTerrainDataCreate* ebus notifications are generated. + NiceMock mockTerrainListener; + EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateBegin()).Times(AtLeast(1)); + EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateEnd()).Times(AtLeast(1)); - NiceMock mockTerrainListener; - EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateBegin()).Times(AtLeast(1)); - EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateEnd()).Times(AtLeast(1)); - - m_terrainSystem = AZStd::make_unique(); - m_terrainSystem->Activate(); -} + m_terrainSystem = AZStd::make_unique(); + m_terrainSystem->Activate(); + } -TEST_F(TerrainSystemTest, DestroyEventsCalledOnDeactivation) -{ - // Verify that when the terrain system is deactivated, the OnTerrainDataDestroy* ebus notifications are generated. + TEST_F(TerrainSystemTest, DestroyEventsCalledOnDeactivation) + { + // Verify that when the terrain system is deactivated, the OnTerrainDataDestroy* ebus notifications are generated. - NiceMock mockTerrainListener; - EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyBegin()).Times(AtLeast(1)); - EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyEnd()).Times(AtLeast(1)); + NiceMock mockTerrainListener; + EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyBegin()).Times(AtLeast(1)); + EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyEnd()).Times(AtLeast(1)); - m_terrainSystem = AZStd::make_unique(); - m_terrainSystem->Activate(); - m_terrainSystem->Deactivate(); -} + m_terrainSystem = AZStd::make_unique(); + m_terrainSystem->Activate(); + m_terrainSystem->Deactivate(); + } -TEST_F(TerrainSystemTest, TerrainDoesNotExistWhenNoTerrainLayerSpawnersAreRegistered) -{ - // For the terrain system, terrain should only exist where terrain layer spawners are present. + TEST_F(TerrainSystemTest, TerrainDoesNotExistWhenNoTerrainLayerSpawnersAreRegistered) + { + // For the terrain system, terrain should only exist where terrain layer spawners are present. - // Verify that in the active terrain system, if there are no terrain layer spawners, any arbitrary point - // will return false for terrainExists, returns a height equal to the min world bounds of the terrain system, and returns - // a normal facing up the Z axis. + // Verify that in the active terrain system, if there are no terrain layer spawners, any arbitrary point + // will return false for terrainExists, returns a height equal to the min world bounds of the terrain system, and returns + // a normal facing up the Z axis. - // Create and activate the terrain system with our testing defaults for world bounds and query resolution. - CreateAndActivateTerrainSystem(); + // Create and activate the terrain system with our testing defaults for world bounds and query resolution. + CreateAndActivateTerrainSystem(); - AZ::Aabb worldBounds = m_terrainSystem->GetTerrainAabb(); + AZ::Aabb worldBounds = m_terrainSystem->GetTerrainAabb(); - // Loop through several points within the world bounds, including on the edges, and verify that they all return false for - // terrainExists with default heights and normals. - for (float y = worldBounds.GetMin().GetY(); y <= worldBounds.GetMax().GetY(); y += (worldBounds.GetExtents().GetY() / 4.0f)) - { - for (float x = worldBounds.GetMin().GetX(); x <= worldBounds.GetMax().GetX(); x += (worldBounds.GetExtents().GetX() / 4.0f)) + // Loop through several points within the world bounds, including on the edges, and verify that they all return false for + // terrainExists with default heights and normals. + for (float y = worldBounds.GetMin().GetY(); y <= worldBounds.GetMax().GetY(); y += (worldBounds.GetExtents().GetY() / 4.0f)) { - AZ::Vector3 position(x, y, 0.0f); - bool terrainExists = true; - float height = m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists); - EXPECT_FALSE(terrainExists); - EXPECT_FLOAT_EQ(height, worldBounds.GetMin().GetZ()); - - terrainExists = true; - AZ::Vector3 normal = m_terrainSystem->GetNormal( - position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists); - EXPECT_FALSE(terrainExists); - EXPECT_EQ(normal, AZ::Vector3::CreateAxisZ()); - - bool isHole = m_terrainSystem->GetIsHoleFromFloats( - position.GetX(), position.GetY(), AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT); - EXPECT_TRUE(isHole); + for (float x = worldBounds.GetMin().GetX(); x <= worldBounds.GetMax().GetX(); x += (worldBounds.GetExtents().GetX() / 4.0f)) + { + AZ::Vector3 position(x, y, 0.0f); + bool terrainExists = true; + float height = + m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists); + EXPECT_FALSE(terrainExists); + EXPECT_FLOAT_EQ(height, worldBounds.GetMin().GetZ()); + + terrainExists = true; + AZ::Vector3 normal = + m_terrainSystem->GetNormal(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists); + EXPECT_FALSE(terrainExists); + EXPECT_EQ(normal, AZ::Vector3::CreateAxisZ()); + + bool isHole = m_terrainSystem->GetIsHoleFromFloats( + position.GetX(), position.GetY(), AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT); + EXPECT_TRUE(isHole); + } } } -} -TEST_F(TerrainSystemTest, TerrainExistsOnlyWithinTerrainLayerSpawnerBounds) -{ - // Verify that the presence of a TerrainLayerSpawner causes terrain to exist in (and *only* in) the box where the TerrainLayerSpawner - // is defined. - - // The terrain system should only query Heights from the TerrainAreaHeightRequest bus within the - // TerrainLayerSpawner region, and so those values should only get returned from GetHeight for queries inside that region. - - // Create a mock terrain layer spawner that uses a box of (0,0,5) - (10,10,15) and always returns a height of 5. - constexpr float spawnerHeight = 5.0f; - const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 10.0f, 10.0f, 15.0f); - auto entity = CreateAndActivateMockTerrainLayerSpawner( - spawnerBox, - [](AZ::Vector3& position, bool& terrainExists) - { - position.SetZ(spawnerHeight); - terrainExists = true; - }); + TEST_F(TerrainSystemTest, TerrainExistsOnlyWithinTerrainLayerSpawnerBounds) + { + // Verify that the presence of a TerrainLayerSpawner causes terrain to exist in (and *only* in) the box where the + // TerrainLayerSpawner is defined. + + // The terrain system should only query Heights from the TerrainAreaHeightRequest bus within the + // TerrainLayerSpawner region, and so those values should only get returned from GetHeight for queries inside that region. + + // Create a mock terrain layer spawner that uses a box of (0,0,5) - (10,10,15) and always returns a height of 5. + constexpr float spawnerHeight = 5.0f; + const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 10.0f, 10.0f, 15.0f); + auto entity = CreateAndActivateMockTerrainLayerSpawner( + spawnerBox, + [](AZ::Vector3& position, bool& terrainExists) + { + position.SetZ(spawnerHeight); + terrainExists = true; + }); - // Verify that terrain exists within the layer spawner bounds, and doesn't exist outside of it. + // Verify that terrain exists within the layer spawner bounds, and doesn't exist outside of it. - // Create and activate the terrain system with our testing defaults for world bounds and query resolution. - CreateAndActivateTerrainSystem(); + // Create and activate the terrain system with our testing defaults for world bounds and query resolution. + CreateAndActivateTerrainSystem(); - // Create a box that's twice as big as the layer spawner box. Loop through it and verify that points within the layer box contain - // terrain and the expected height & normal values, and points outside the layer box don't contain terrain. - const AZ::Aabb encompassingBox = - AZ::Aabb::CreateFromMinMax(spawnerBox.GetMin() - (spawnerBox.GetExtents() / 2.0f), - spawnerBox.GetMax() + (spawnerBox.GetExtents() / 2.0f)); + // Create a box that's twice as big as the layer spawner box. Loop through it and verify that points within the layer box contain + // terrain and the expected height & normal values, and points outside the layer box don't contain terrain. + const AZ::Aabb encompassingBox = AZ::Aabb::CreateFromMinMax( + spawnerBox.GetMin() - (spawnerBox.GetExtents() / 2.0f), spawnerBox.GetMax() + (spawnerBox.GetExtents() / 2.0f)); - for (float y = encompassingBox.GetMin().GetY(); y < encompassingBox.GetMax().GetY(); y += 1.0f) - { - for (float x = encompassingBox.GetMin().GetX(); x < encompassingBox.GetMax().GetX(); x += 1.0f) + for (float y = encompassingBox.GetMin().GetY(); y < encompassingBox.GetMax().GetY(); y += 1.0f) { - AZ::Vector3 position(x, y, 0.0f); - bool heightQueryTerrainExists = false; - float height = - m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists); - bool isHole = m_terrainSystem->GetIsHoleFromFloats( - position.GetX(), position.GetY(), AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT); - - if (spawnerBox.Contains(AZ::Vector3(position.GetX(), position.GetY(), spawnerBox.GetMin().GetZ()))) - { - EXPECT_TRUE(heightQueryTerrainExists); - EXPECT_FALSE(isHole); - EXPECT_FLOAT_EQ(height, spawnerHeight); - } - else + for (float x = encompassingBox.GetMin().GetX(); x < encompassingBox.GetMax().GetX(); x += 1.0f) { - EXPECT_FALSE(heightQueryTerrainExists); - EXPECT_TRUE(isHole); + AZ::Vector3 position(x, y, 0.0f); + bool heightQueryTerrainExists = false; + float height = m_terrainSystem->GetHeight( + position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists); + bool isHole = m_terrainSystem->GetIsHoleFromFloats( + position.GetX(), position.GetY(), AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT); + + if (spawnerBox.Contains(AZ::Vector3(position.GetX(), position.GetY(), spawnerBox.GetMin().GetZ()))) + { + EXPECT_TRUE(heightQueryTerrainExists); + EXPECT_FALSE(isHole); + EXPECT_FLOAT_EQ(height, spawnerHeight); + } + else + { + EXPECT_FALSE(heightQueryTerrainExists); + EXPECT_TRUE(isHole); + } } } } -} -TEST_F(TerrainSystemTest, TerrainHeightQueriesWithExactSamplersIgnoreQueryGrid) -{ - // Verify that when using the "EXACT" height sampler, the returned heights come directly from the height provider at the exact - // requested location, instead of the position being quantized to the height query grid. - - // Create a mock terrain layer spawner that uses a box of (0,0,5) - (10,10,15) and generates a height based on a sine wave - // using a frequency of 1m and an amplitude of 10m. i.e. Heights will range between -10 to 10 meters, but will have a value of 0 - // every 0.5 meters. The sine wave value is based on the absolute X position only, for simplicity. - constexpr float amplitudeMeters = 10.0f; - constexpr float frequencyMeters = 1.0f; - const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 10.0f, 10.0f, 15.0f); - auto entity = CreateAndActivateMockTerrainLayerSpawner( - spawnerBox, - [](AZ::Vector3& position, bool& terrainExists) - { - position.SetZ(amplitudeMeters * sin(AZ::Constants::TwoPi * (position.GetX() / frequencyMeters))); - terrainExists = true; - }); - - // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution that exactly matches - // the frequency of our sine wave. If our height queries rely on the query resolution, we should always get a value of 0. - const AZ::Vector2 queryResolution(frequencyMeters); - CreateAndActivateTerrainSystem(queryResolution); - - // Test an arbitrary set of points that should all produce non-zero heights with the EXACT sampler. They're not aligned with the - // query resolution, or with the 0 points on the sine wave. - const AZ::Vector2 nonZeroPoints[] = { AZ::Vector2(0.3f), AZ::Vector2(2.8f), AZ::Vector2(5.9f), AZ::Vector2(7.7f) }; - for (auto& nonZeroPoint : nonZeroPoints) + TEST_F(TerrainSystemTest, TerrainHeightQueriesWithExactSamplersIgnoreQueryGrid) { + // Verify that when using the "EXACT" height sampler, the returned heights come directly from the height provider at the exact + // requested location, instead of the position being quantized to the height query grid. + + // Create a mock terrain layer spawner that uses a box of (0,0,5) - (10,10,15) and generates a height based on a sine wave + // using a frequency of 1m and an amplitude of 10m. i.e. Heights will range between -10 to 10 meters, but will have a value of 0 + // every 0.5 meters. The sine wave value is based on the absolute X position only, for simplicity. + constexpr float amplitudeMeters = 10.0f; + constexpr float frequencyMeters = 1.0f; + const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 10.0f, 10.0f, 15.0f); + auto entity = CreateAndActivateMockTerrainLayerSpawner( + spawnerBox, + [](AZ::Vector3& position, bool& terrainExists) + { + position.SetZ(amplitudeMeters * sin(AZ::Constants::TwoPi * (position.GetX() / frequencyMeters))); + terrainExists = true; + }); + + // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution that exactly matches + // the frequency of our sine wave. If our height queries rely on the query resolution, we should always get a value of 0. + const AZ::Vector2 queryResolution(frequencyMeters); + CreateAndActivateTerrainSystem(queryResolution); + + // Test an arbitrary set of points that should all produce non-zero heights with the EXACT sampler. They're not aligned with the + // query resolution, or with the 0 points on the sine wave. + const AZ::Vector2 nonZeroPoints[] = { AZ::Vector2(0.3f), AZ::Vector2(2.8f), AZ::Vector2(5.9f), AZ::Vector2(7.7f) }; + for (auto& nonZeroPoint : nonZeroPoints) + { AZ::Vector3 position(nonZeroPoint.GetX(), nonZeroPoint.GetY(), 0.0f); bool heightQueryTerrainExists = false; float height = @@ -311,165 +312,253 @@ TEST_F(TerrainSystemTest, TerrainHeightQueriesWithExactSamplersIgnoreQueryGrid) // We've chosen a bunch of places on the sine wave that should return a non-zero positive or negative value. constexpr float epsilon = 0.0001f; EXPECT_GT(fabsf(height), epsilon); + } + + // Test an arbitrary set of points that should all produce zero heights with the EXACT sampler, since they align with 0 points on + // the sine wave, regardless of whether or not they align to the query resolution. + const AZ::Vector2 zeroPoints[] = { AZ::Vector2(0.5f), AZ::Vector2(1.0f), AZ::Vector2(5.0f), AZ::Vector2(7.5f) }; + for (auto& zeroPoint : zeroPoints) + { + AZ::Vector3 position(zeroPoint.GetX(), zeroPoint.GetY(), 0.0f); + bool heightQueryTerrainExists = false; + float height = + m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists); + + constexpr float epsilon = 0.0001f; + EXPECT_NEAR(height, 0.0f, epsilon); + } } - // Test an arbitrary set of points that should all produce zero heights with the EXACT sampler, since they align with 0 points on the - // sine wave, regardless of whether or not they align to the query resolution. - const AZ::Vector2 zeroPoints[] = { AZ::Vector2(0.5f), AZ::Vector2(1.0f), AZ::Vector2(5.0f), AZ::Vector2(7.5f) }; - for (auto& zeroPoint : zeroPoints) + TEST_F(TerrainSystemTest, TerrainHeightQueriesWithClampSamplersUseQueryGrid) { - AZ::Vector3 position(zeroPoint.GetX(), zeroPoint.GetY(), 0.0f); - bool heightQueryTerrainExists = false; - float height = - m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists); + // Verify that when using the "CLAMP" height sampler, the requested location is quantized to the height query grid before fetching + // the height. + + // Create a mock terrain layer spawner that uses a box of (-10,-10,-5) - (10,10,15) and generates a height equal + // to the X + Y position, so if either one doesn't get clamped we'll get an unexpected result. + const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f); + auto entity = CreateAndActivateMockTerrainLayerSpawner( + spawnerBox, + [](AZ::Vector3& position, bool& terrainExists) + { + position.SetZ(position.GetX() + position.GetY()); + terrainExists = true; + }); + + // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 0.25 meter + // intervals. + const AZ::Vector2 queryResolution(0.25f); + CreateAndActivateTerrainSystem(queryResolution); + + // Test some points and verify that the results always go "downward", whether they're in positive or negative space. + // (Z contains the the expected result for convenience). + const HeightTestPoint testPoints[] = { + { AZ::Vector2(0.0f, 0.0f), 0.0f }, // Should return a height of 0.00 + 0.00 + { AZ::Vector2(0.3f, 0.3f), 0.5f }, // Should return a height of 0.25 + 0.25 + { AZ::Vector2(2.8f, 2.8f), 5.5f }, // Should return a height of 2.75 + 2.75 + { AZ::Vector2(5.5f, 5.5f), 11.0f }, // Should return a height of 5.50 + 5.50 + { AZ::Vector2(7.7f, 7.7f), 15.0f }, // Should return a height of 7.50 + 7.50 + + { AZ::Vector2(-0.3f, -0.3f), -1.0f }, // Should return a height of -0.50 + -0.50 + { AZ::Vector2(-2.8f, -2.8f), -6.0f }, // Should return a height of -3.00 + -3.00 + { AZ::Vector2(-5.5f, -5.5f), -11.0f }, // Should return a height of -5.50 + -5.50 + { AZ::Vector2(-7.7f, -7.7f), -15.5f } // Should return a height of -7.75 + -7.75 + }; + for (auto& testPoint : testPoints) + { + const float expectedHeight = testPoint.m_expectedHeight; + + AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f); + bool heightQueryTerrainExists = false; + float height = + m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP, &heightQueryTerrainExists); - constexpr float epsilon = 0.0001f; - EXPECT_NEAR(height, 0.0f, epsilon); + constexpr float epsilon = 0.0001f; + EXPECT_NEAR(height, expectedHeight, epsilon); + } } -} -TEST_F(TerrainSystemTest, TerrainHeightQueriesWithClampSamplersUseQueryGrid) -{ - // Verify that when using the "CLAMP" height sampler, the requested location is quantized to the height query grid before fetching - // the height. - - // Create a mock terrain layer spawner that uses a box of (-10,-10,-5) - (10,10,15) and generates a height equal - // to the X + Y position, so if either one doesn't get clamped we'll get an unexpected result. - const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f); - auto entity = CreateAndActivateMockTerrainLayerSpawner( - spawnerBox, - [](AZ::Vector3& position, bool& terrainExists) + TEST_F(TerrainSystemTest, TerrainHeightQueriesWithBilinearSamplersUseQueryGridToInterpolate) + { + // Verify that when using the "BILINEAR" height sampler, the heights are interpolated from points sampled from the query grid. + + // Create a mock terrain layer spawner that uses a box of (-10,-10,-5) - (10,10,15) and generates a height equal + // to the X + Y position, so we'll have heights that look like this on our grid: + // 0 *---* 1 + // | | + // 1 *---* 2 + // However, everywhere inside the grid box, we'll generate heights much larger than X + Y. It will have no effect on exact grid + // points, but it will noticeably affect the expected height values if any points get sampled in-between grid points. + + const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f); + const float amplitudeMeters = 10.0f; + const float frequencyMeters = 1.0f; + auto entity = CreateAndActivateMockTerrainLayerSpawner( + spawnerBox, + [amplitudeMeters, frequencyMeters](AZ::Vector3& position, bool& terrainExists) + { + // Our generated height will be X + Y. + float expectedHeight = position.GetX() + position.GetY(); + + // If either X or Y aren't evenly divisible by the query frequency, add a scaled value to our generated height. + // This will show up as an unexpected height "spike" if it gets used in any bilinear filter queries. + float unexpectedVariance = + amplitudeMeters * (fmodf(position.GetX(), frequencyMeters) + fmodf(position.GetY(), frequencyMeters)); + position.SetZ(expectedHeight + unexpectedVariance); + terrainExists = true; + }); + + // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals. + const AZ::Vector2 queryResolution(frequencyMeters); + CreateAndActivateTerrainSystem(queryResolution); + + // Test some points and verify that the results are the expected bilinear filtered result, + // whether they're in positive or negative space. + // (Z contains the the expected result for convenience). + const HeightTestPoint testPoints[] = { + + // Queries directly on grid points. These should return values of X + Y. + { AZ::Vector2(0.0f, 0.0f), 0.0f }, // Should return a height of 0 + 0 + { AZ::Vector2(1.0f, 0.0f), 1.0f }, // Should return a height of 1 + 0 + { AZ::Vector2(0.0f, 1.0f), 1.0f }, // Should return a height of 0 + 1 + { AZ::Vector2(1.0f, 1.0f), 2.0f }, // Should return a height of 1 + 1 + { AZ::Vector2(3.0f, 5.0f), 8.0f }, // Should return a height of 3 + 5 + + { AZ::Vector2(-1.0f, 0.0f), -1.0f }, // Should return a height of -1 + 0 + { AZ::Vector2(0.0f, -1.0f), -1.0f }, // Should return a height of 0 + -1 + { AZ::Vector2(-1.0f, -1.0f), -2.0f }, // Should return a height of -1 + -1 + { AZ::Vector2(-3.0f, -5.0f), -8.0f }, // Should return a height of -3 + -5 + + // Queries that are on a grid edge (one axis on the grid, the other somewhere in-between). + // These should just be a linear interpolation of the points, so it should still be X + Y. + + { AZ::Vector2(0.25f, 0.0f), 0.25f }, // Should return a height of -0.25 + 0 + { AZ::Vector2(3.75f, 0.0f), 3.75f }, // Should return a height of -3.75 + 0 + { AZ::Vector2(0.0f, 0.25f), 0.25f }, // Should return a height of 0 + -0.25 + { AZ::Vector2(0.0f, 3.75f), 3.75f }, // Should return a height of 0 + -3.75 + + { AZ::Vector2(2.0f, 3.75f), 5.75f }, // Should return a height of -2 + -3.75 + { AZ::Vector2(2.25f, 4.0f), 6.25f }, // Should return a height of -2.25 + -4 + + { AZ::Vector2(-0.25f, 0.0f), -0.25f }, // Should return a height of -0.25 + 0 + { AZ::Vector2(-3.75f, 0.0f), -3.75f }, // Should return a height of -3.75 + 0 + { AZ::Vector2(0.0f, -0.25f), -0.25f }, // Should return a height of 0 + -0.25 + { AZ::Vector2(0.0f, -3.75f), -3.75f }, // Should return a height of 0 + -3.75 + + { AZ::Vector2(-2.0f, -3.75f), -5.75f }, // Should return a height of -2 + -3.75 + { AZ::Vector2(-2.25f, -4.0f), -6.25f }, // Should return a height of -2.25 + -4 + + // Queries inside a grid square (both axes are in-between grid points) + // This is a full bilinear interpolation, but because we're using X + Y for our heights, the interpolated values + // should *still* be X + Y assuming the points were sampled correctly from the grid points. + + { AZ::Vector2(3.25f, 5.25f), 8.5f }, // Should return a height of 3.25 + 5.25 + { AZ::Vector2(7.71f, 9.74f), 17.45f }, // Should return a height of 7.71 + 9.74 + + { AZ::Vector2(-3.25f, -5.25f), -8.5f }, // Should return a height of -3.25 + -5.25 + { AZ::Vector2(-7.71f, -9.74f), -17.45f }, // Should return a height of -7.71 + -9.74 + }; + + // Loop through every test point and validate it. + for (auto& testPoint : testPoints) { - position.SetZ(position.GetX() + position.GetY()); - terrainExists = true; - }); + const float expectedHeight = testPoint.m_expectedHeight; - // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 0.25 meter intervals. - const AZ::Vector2 queryResolution(0.25f); - CreateAndActivateTerrainSystem(queryResolution); + AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f); + bool heightQueryTerrainExists = false; + float height = m_terrainSystem->GetHeight( + position, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR, &heightQueryTerrainExists); - // Test some points and verify that the results always go "downward", whether they're in positive or negative space. - // (Z contains the the expected result for convenience). - const HeightTestPoint testPoints[] = - { - { AZ::Vector2(0.0f, 0.0f), 0.0f }, // Should return a height of 0.00 + 0.00 - { AZ::Vector2(0.3f, 0.3f), 0.5f }, // Should return a height of 0.25 + 0.25 - { AZ::Vector2(2.8f, 2.8f), 5.5f }, // Should return a height of 2.75 + 2.75 - { AZ::Vector2(5.5f, 5.5f), 11.0f }, // Should return a height of 5.50 + 5.50 - { AZ::Vector2(7.7f, 7.7f), 15.0f }, // Should return a height of 7.50 + 7.50 - - { AZ::Vector2(-0.3f, -0.3f), -1.0f }, // Should return a height of -0.50 + -0.50 - { AZ::Vector2(-2.8f, -2.8f), -6.0f }, // Should return a height of -3.00 + -3.00 - { AZ::Vector2(-5.5f, -5.5f), -11.0f }, // Should return a height of -5.50 + -5.50 - { AZ::Vector2(-7.7f, -7.7f), -15.5f } // Should return a height of -7.75 + -7.75 - }; - for (auto& testPoint : testPoints) + // Verify that our height query returned the bilinear filtered result we expect. + constexpr float epsilon = 0.0001f; + EXPECT_NEAR(height, expectedHeight, epsilon); + } + } + + TEST_F(TerrainSystemTest, GetSurfaceWeightsReturnsAllValidSurfaceWeights) { - const float expectedHeight = testPoint.m_expectedHeight; + CreateAndActivateTerrainSystem(); - AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f); - bool heightQueryTerrainExists = false; - float height = - m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP, &heightQueryTerrainExists); + const AZ::Aabb aabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3::CreateZero(), AZ::Vector3::CreateOne()); + auto entity = CreateAndActivateMockTerrainLayerSpawner( + aabb, + [](AZ::Vector3& position, bool& terrainExists) + { + position.SetZ(1.0f); + terrainExists = true; + }); - constexpr float epsilon = 0.0001f; - EXPECT_NEAR(height, expectedHeight, epsilon); - } -} + const AZ::Crc32 tag1("tag1"); + const AZ::Crc32 tag2("tag2"); -TEST_F(TerrainSystemTest, TerrainHeightQueriesWithBilinearSamplersUseQueryGridToInterpolate) -{ - // Verify that when using the "BILINEAR" height sampler, the heights are interpolated from points sampled from the query grid. - - // Create a mock terrain layer spawner that uses a box of (-10,-10,-5) - (10,10,15) and generates a height equal - // to the X + Y position, so we'll have heights that look like this on our grid: - // 0 *---* 1 - // | | - // 1 *---* 2 - // However, everywhere inside the grid box, we'll generate heights much larger than X + Y. It will have no effect on exact grid - // points, but it will noticeably affect the expected height values if any points get sampled in-between grid points. - - const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f); - const float amplitudeMeters = 10.0f; - const float frequencyMeters = 1.0f; - auto entity = CreateAndActivateMockTerrainLayerSpawner( - spawnerBox, - [amplitudeMeters, frequencyMeters](AZ::Vector3& position, bool& terrainExists) - { - // Our generated height will be X + Y. - float expectedHeight = position.GetX() + position.GetY(); - - // If either X or Y aren't evenly divisible by the query frequency, add a scaled value to our generated height. - // This will show up as an unexpected height "spike" if it gets used in any bilinear filter queries. - float unexpectedVariance = amplitudeMeters * - (fmodf(position.GetX(), frequencyMeters) + fmodf(position.GetY(), frequencyMeters)); - position.SetZ(expectedHeight + unexpectedVariance); - terrainExists = true; - }); - - // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals. - const AZ::Vector2 queryResolution(frequencyMeters); - CreateAndActivateTerrainSystem(queryResolution); - - // Test some points and verify that the results are the expected bilinear filtered result, - // whether they're in positive or negative space. - // (Z contains the the expected result for convenience). - const HeightTestPoint testPoints[] = { - - // Queries directly on grid points. These should return values of X + Y. - { AZ::Vector2(0.0f, 0.0f), 0.0f }, // Should return a height of 0 + 0 - { AZ::Vector2(1.0f, 0.0f), 1.0f }, // Should return a height of 1 + 0 - { AZ::Vector2(0.0f, 1.0f), 1.0f }, // Should return a height of 0 + 1 - { AZ::Vector2(1.0f, 1.0f), 2.0f }, // Should return a height of 1 + 1 - { AZ::Vector2(3.0f, 5.0f), 8.0f }, // Should return a height of 3 + 5 - - { AZ::Vector2(-1.0f, 0.0f), -1.0f }, // Should return a height of -1 + 0 - { AZ::Vector2(0.0f, -1.0f), -1.0f }, // Should return a height of 0 + -1 - { AZ::Vector2(-1.0f, -1.0f), -2.0f }, // Should return a height of -1 + -1 - { AZ::Vector2(-3.0f, -5.0f), -8.0f }, // Should return a height of -3 + -5 - - // Queries that are on a grid edge (one axis on the grid, the other somewhere in-between). - // These should just be a linear interpolation of the points, so it should still be X + Y. - - { AZ::Vector2(0.25f, 0.0f), 0.25f }, // Should return a height of -0.25 + 0 - { AZ::Vector2(3.75f, 0.0f), 3.75f }, // Should return a height of -3.75 + 0 - { AZ::Vector2(0.0f, 0.25f), 0.25f }, // Should return a height of 0 + -0.25 - { AZ::Vector2(0.0f, 3.75f), 3.75f }, // Should return a height of 0 + -3.75 - - { AZ::Vector2(2.0f, 3.75f), 5.75f }, // Should return a height of -2 + -3.75 - { AZ::Vector2(2.25f, 4.0f), 6.25f }, // Should return a height of -2.25 + -4 - - { AZ::Vector2(-0.25f, 0.0f), -0.25f }, // Should return a height of -0.25 + 0 - { AZ::Vector2(-3.75f, 0.0f), -3.75f }, // Should return a height of -3.75 + 0 - { AZ::Vector2(0.0f, -0.25f), -0.25f }, // Should return a height of 0 + -0.25 - { AZ::Vector2(0.0f, -3.75f), -3.75f }, // Should return a height of 0 + -3.75 - - { AZ::Vector2(-2.0f, -3.75f), -5.75f }, // Should return a height of -2 + -3.75 - { AZ::Vector2(-2.25f, -4.0f), -6.25f }, // Should return a height of -2.25 + -4 - - // Queries inside a grid square (both axes are in-between grid points) - // This is a full bilinear interpolation, but because we're using X + Y for our heights, the interpolated values - // should *still* be X + Y assuming the points were sampled correctly from the grid points. - - { AZ::Vector2(3.25f, 5.25f), 8.5f }, // Should return a height of 3.25 + 5.25 - { AZ::Vector2(7.71f, 9.74f), 17.45f }, // Should return a height of 7.71 + 9.74 - - { AZ::Vector2(-3.25f, -5.25f), -8.5f }, // Should return a height of -3.25 + -5.25 - { AZ::Vector2(-7.71f, -9.74f), -17.45f }, // Should return a height of -7.71 + -9.74 - }; + AzFramework::SurfaceData::OrderedSurfaceTagWeightSet orderedSurfaceWeights; + + AzFramework::SurfaceData::SurfaceTagWeight tagWeight1; + tagWeight1.m_surfaceType = tag1; + tagWeight1.m_weight = 1.0f; + orderedSurfaceWeights.emplace(tagWeight1); + + AzFramework::SurfaceData::SurfaceTagWeight tagWeight2; + tagWeight2.m_surfaceType = tag2; + tagWeight2.m_weight = 0.8f; + orderedSurfaceWeights.emplace(tagWeight2); - // Loop through every test point and validate it. - for (auto& testPoint : testPoints) + NiceMock mockSurfaceRequests(entity->GetId()); + ON_CALL(mockSurfaceRequests, GetSurfaceWeights).WillByDefault(SetArgReferee<1>(orderedSurfaceWeights)); + + AzFramework::SurfaceData::OrderedSurfaceTagWeightSet outSurfaceWeights; + + // Asking for values outside the layer spawner bounds, should result in no results. + m_terrainSystem->GetSurfaceWeights(aabb.GetMax() + AZ::Vector3::CreateOne(), outSurfaceWeights); + EXPECT_TRUE(outSurfaceWeights.empty()); + + // Inside the layer spawner box should give us both the added surface weights. + m_terrainSystem->GetSurfaceWeights(aabb.GetCenter(), outSurfaceWeights); + + EXPECT_EQ(outSurfaceWeights.size(), 2); + } + + TEST_F(TerrainSystemTest, GetMaxSurfaceWeightsReturnsBiggestValidSurfaceWeight) { - const float expectedHeight = testPoint.m_expectedHeight; + CreateAndActivateTerrainSystem(); + + const AZ::Aabb aabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3::CreateZero(), AZ::Vector3::CreateOne()); + auto entity = CreateAndActivateMockTerrainLayerSpawner( + aabb, + [](AZ::Vector3& position, bool& terrainExists) + { + position.SetZ(1.0f); + terrainExists = true; + }); + + const AZ::Crc32 tag1("tag1"); + const AZ::Crc32 tag2("tag2"); + + AzFramework::SurfaceData::OrderedSurfaceTagWeightSet orderedSurfaceWeights; + + AzFramework::SurfaceData::SurfaceTagWeight tagWeight1; + tagWeight1.m_surfaceType = tag1; + tagWeight1.m_weight = 1.0f; + orderedSurfaceWeights.emplace(tagWeight1); + + AzFramework::SurfaceData::SurfaceTagWeight tagWeight2; + tagWeight2.m_surfaceType = tag2; + tagWeight2.m_weight = 0.8f; + orderedSurfaceWeights.emplace(tagWeight2); + + NiceMock mockSurfaceRequests(entity->GetId()); + ON_CALL(mockSurfaceRequests, GetSurfaceWeights).WillByDefault(SetArgReferee<1>(orderedSurfaceWeights)); + + // Asking for values outside the layer spawner bounds, should result in an invalid result. + AzFramework::SurfaceData::SurfaceTagWeight tagWeight = + m_terrainSystem->GetMaxSurfaceWeight(aabb.GetMax() + AZ::Vector3::CreateOne()); + + EXPECT_EQ(tagWeight.m_surfaceType, AZ::Crc32(AzFramework::SurfaceData::Constants::s_unassignedTagName)); - AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f); - bool heightQueryTerrainExists = false; - float height = - m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR, &heightQueryTerrainExists); + // Inside the layer spawner box should give us the highest weighted tag (tag1). + tagWeight = m_terrainSystem->GetMaxSurfaceWeight(aabb.GetCenter()); - // Verify that our height query returned the bilinear filtered result we expect. - constexpr float epsilon = 0.0001f; - EXPECT_NEAR(height, expectedHeight, epsilon); + EXPECT_EQ(tagWeight.m_surfaceType, tagWeight1.m_surfaceType); + EXPECT_NEAR(tagWeight.m_weight, tagWeight1.m_weight, 0.01f); } -} +} // namespace UnitTest diff --git a/Gems/Terrain/Code/terrain_editor_shared_files.cmake b/Gems/Terrain/Code/terrain_editor_shared_files.cmake index 334c89ec73..09724751a9 100644 --- a/Gems/Terrain/Code/terrain_editor_shared_files.cmake +++ b/Gems/Terrain/Code/terrain_editor_shared_files.cmake @@ -11,6 +11,8 @@ set(FILES Source/EditorComponents/EditorTerrainHeightGradientListComponent.h Source/EditorComponents/EditorTerrainLayerSpawnerComponent.cpp Source/EditorComponents/EditorTerrainLayerSpawnerComponent.h + Source/EditorComponents/EditorTerrainPhysicsColliderComponent.cpp + Source/EditorComponents/EditorTerrainPhysicsColliderComponent.h Source/EditorComponents/EditorTerrainSurfaceGradientListComponent.cpp Source/EditorComponents/EditorTerrainSurfaceGradientListComponent.h Source/EditorComponents/EditorTerrainWorldComponent.cpp diff --git a/Gems/Terrain/Code/terrain_files.cmake b/Gems/Terrain/Code/terrain_files.cmake index 780dd1bdb0..893477e10d 100644 --- a/Gems/Terrain/Code/terrain_files.cmake +++ b/Gems/Terrain/Code/terrain_files.cmake @@ -12,6 +12,8 @@ set(FILES Source/Components/TerrainHeightGradientListComponent.h Source/Components/TerrainLayerSpawnerComponent.cpp Source/Components/TerrainLayerSpawnerComponent.h + Source/Components/TerrainPhysicsColliderComponent.cpp + Source/Components/TerrainPhysicsColliderComponent.h Source/Components/TerrainSurfaceDataSystemComponent.cpp Source/Components/TerrainSurfaceDataSystemComponent.h Source/Components/TerrainSurfaceGradientListComponent.cpp diff --git a/Gems/Terrain/Code/terrain_mocks_files.cmake b/Gems/Terrain/Code/terrain_mocks_files.cmake index 2aedd1c5d8..874e17f028 100644 --- a/Gems/Terrain/Code/terrain_mocks_files.cmake +++ b/Gems/Terrain/Code/terrain_mocks_files.cmake @@ -8,4 +8,6 @@ set(FILES Mocks/Terrain/MockTerrain.h + Mocks/Terrain/MockTerrainLayerSpawner.h + Mocks/Terrain/MockTerrainAreaSurfaceRequestBus.h ) diff --git a/Gems/Terrain/Code/terrain_tests_files.cmake b/Gems/Terrain/Code/terrain_tests_files.cmake index 793bc01ac5..3e37e509a7 100644 --- a/Gems/Terrain/Code/terrain_tests_files.cmake +++ b/Gems/Terrain/Code/terrain_tests_files.cmake @@ -10,6 +10,9 @@ set(FILES Tests/TerrainTest.cpp Tests/TerrainSystemTest.cpp Tests/LayerSpawnerTests.cpp + Tests/TerrainPhysicsColliderTests.cpp Tests/SurfaceMaterialsListTest.cpp Tests/MockAxisAlignedBoxShapeComponent.h + Tests/TerrainHeightGradientListTests.cpp + Tests/TerrainSurfaceGradientListTests.cpp ) From 136788bccfed237f51a9c41074d72a95a827db70 Mon Sep 17 00:00:00 2001 From: allisaurus <34254888+allisaurus@users.noreply.github.com> Date: Fri, 22 Oct 2021 08:53:40 -0700 Subject: [PATCH 25/26] Update env var setting instructions in AWS Gem test README (#4791) Signed-off-by: Stanko --- AutomatedTesting/Gem/PythonTests/AWS/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/AutomatedTesting/Gem/PythonTests/AWS/README.md b/AutomatedTesting/Gem/PythonTests/AWS/README.md index 1bb36f178d..0d046cbe4c 100644 --- a/AutomatedTesting/Gem/PythonTests/AWS/README.md +++ b/AutomatedTesting/Gem/PythonTests/AWS/README.md @@ -8,11 +8,14 @@ ## Deploy CDK Applications 1. Go to the AWS IAM console and create an IAM role called o3de-automation-tests which adds your own account as as a trusted entity and uses the "AdministratorAccess" permissions policy. 2. Copy {engine_root}\scripts\build\Platform\Windows\deploy_cdk_applications.cmd to your engine root folder. -3. Open a new Command Prompt window at the engine root and set the following environment variables: +3. Open a new Command Prompt window at the engine root and set the following environment variables: +``` Set O3DE_AWS_PROJECT_NAME=AWSAUTO Set O3DE_AWS_DEPLOY_REGION=us-east-1 + Set O3DE_AWS_DEPLOY_ACCOUNT={your_aws_account_id} Set ASSUME_ROLE_ARN=arn:aws:iam::{your_aws_account_id}:role/o3de-automation-tests Set COMMIT_ID=HEAD +``` 4. In the same Command Prompt window, Deploy the CDK applications for AWS gems by running deploy_cdk_applications.cmd. ## Run Automation Tests From 1989316cac4d723048d85a31327ccfab2e04dd21 Mon Sep 17 00:00:00 2001 From: Alex Peterson <26804013+AMZN-alexpete@users.noreply.github.com> Date: Fri, 22 Oct 2021 09:11:28 -0700 Subject: [PATCH 26/26] Added toast notifications to the gem catalog --- .../Components/ToastNotification.cpp | 33 +++++++++ .../Components/ToastNotification.h | 3 + .../Components/ToastNotification.ui | 2 +- .../ToastNotificationConfiguration.h | 1 + .../Notifications/ToastNotificationsView.cpp | 10 +++ .../UI/Notifications/ToastNotificationsView.h | 3 + Code/Tools/ProjectManager/CMakeLists.txt | 2 +- .../Resources/ProjectManager.qrc | 1 + .../Resources/ProjectManager.qss | 18 +++++ Code/Tools/ProjectManager/Resources/gem.svg | 3 + .../Source/GemCatalog/GemCatalogScreen.cpp | 73 +++++++++++++++++++ .../Source/GemCatalog/GemCatalogScreen.h | 16 +++- .../Source/GemCatalog/GemModel.cpp | 35 ++++++++- .../Source/GemCatalog/GemModel.h | 3 + 14 files changed, 196 insertions(+), 7 deletions(-) create mode 100644 Code/Tools/ProjectManager/Resources/gem.svg diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.cpp b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.cpp index 670607900f..f79f355ccb 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.cpp +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.cpp @@ -13,6 +13,7 @@ #include #include #include +#include namespace AzQtComponents { @@ -27,6 +28,13 @@ namespace AzQtComponents setAttribute(Qt::WA_ShowWithoutActivating); setAttribute(Qt::WA_DeleteOnClose); + m_borderRadius = toastConfiguration.m_borderRadius; + if (m_borderRadius > 0) + { + setWindowFlags(Qt::FramelessWindowHint | Qt::Dialog); + setAttribute(Qt::WA_TranslucentBackground); + } + m_ui->setupUi(this); QIcon toastIcon; @@ -53,6 +61,13 @@ namespace AzQtComponents m_ui->titleLabel->setText(toastConfiguration.m_title); m_ui->mainLabel->setText(toastConfiguration.m_description); + // hide the optional description if none is provided so the title is centered vertically + if (toastConfiguration.m_description.isEmpty()) + { + m_ui->mainLabel->setVisible(false); + m_ui->verticalLayout->removeWidget(m_ui->mainLabel); + } + m_lifeSpan.setInterval(aznumeric_cast(toastConfiguration.m_duration.count())); m_closeOnClick = toastConfiguration.m_closeOnClick; @@ -68,6 +83,24 @@ namespace AzQtComponents { } + void ToastNotification::paintEvent(QPaintEvent* event) + { + if (m_borderRadius > 0) + { + QPainter p(this); + p.setPen(Qt::transparent); + QColor painterColor; + painterColor.setRgbF(0, 0, 0, 255); + p.setBrush(painterColor); + p.setRenderHint(QPainter::Antialiasing); + p.drawRoundedRect(rect(), m_borderRadius, m_borderRadius); + } + else + { + QDialog::paintEvent(event); + } + } + void ToastNotification::ShowToastAtCursor() { QPoint globalCursorPos = QCursor::pos(); diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.h b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.h index eaf8a0751d..7f2701a803 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.h +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.h @@ -52,6 +52,8 @@ namespace AzQtComponents void mousePressEvent(QMouseEvent* mouseEvent) override; bool eventFilter(QObject* object, QEvent* event) override; + void paintEvent(QPaintEvent* event) override; + public slots: void StartTimer(); void FadeOut(); @@ -65,6 +67,7 @@ namespace AzQtComponents bool m_closeOnClick; QTimer m_lifeSpan; + uint32_t m_borderRadius = 0; AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING AZStd::chrono::milliseconds m_fadeDuration; diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.ui b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.ui index 107281bdc2..7aefddbd98 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.ui +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.ui @@ -191,7 +191,7 @@ - 40 + 20 20 diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotificationConfiguration.h b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotificationConfiguration.h index 05999017e4..5ace9d1be2 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotificationConfiguration.h +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotificationConfiguration.h @@ -37,6 +37,7 @@ namespace AzQtComponents QString m_title; QString m_description; QString m_customIconImage; + uint32_t m_borderRadius = 0; AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING AZStd::chrono::milliseconds m_duration = AZStd::chrono::milliseconds(5000); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.cpp index 0ef0bf9a7b..e039230783 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.cpp @@ -177,4 +177,14 @@ namespace AzToolsFramework DisplayQueuedNotification(); } } + + void ToastNotificationsView::SetOffset(const QPoint& offset) + { + m_offset = offset; + } + + void ToastNotificationsView::SetAnchorPoint(const QPointF& anchorPoint) + { + m_anchorPoint = anchorPoint; + } } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.h b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.h index c4220331d8..e13f129467 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.h @@ -50,6 +50,9 @@ namespace AzToolsFramework void OnShow(); void UpdateToastPosition(); + void SetOffset(const QPoint& offset); + void SetAnchorPoint(const QPointF& anchorPoint); + private: ToastId CreateToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration); void DisplayQueuedNotification(); diff --git a/Code/Tools/ProjectManager/CMakeLists.txt b/Code/Tools/ProjectManager/CMakeLists.txt index 28c1871616..d34abcbc6c 100644 --- a/Code/Tools/ProjectManager/CMakeLists.txt +++ b/Code/Tools/ProjectManager/CMakeLists.txt @@ -43,7 +43,7 @@ ly_add_target( 3rdParty::pybind11 AZ::AzCore AZ::AzFramework - AZ::AzQtComponents + AZ::AzToolsFramework ) ly_add_target( diff --git a/Code/Tools/ProjectManager/Resources/ProjectManager.qrc b/Code/Tools/ProjectManager/Resources/ProjectManager.qrc index aeaf9a9248..8dd7e4c9b5 100644 --- a/Code/Tools/ProjectManager/Resources/ProjectManager.qrc +++ b/Code/Tools/ProjectManager/Resources/ProjectManager.qrc @@ -40,5 +40,6 @@ Delete.svg Download.svg in_progress.gif + gem.svg diff --git a/Code/Tools/ProjectManager/Resources/ProjectManager.qss b/Code/Tools/ProjectManager/Resources/ProjectManager.qss index 298d2a749a..712c01aa02 100644 --- a/Code/Tools/ProjectManager/Resources/ProjectManager.qss +++ b/Code/Tools/ProjectManager/Resources/ProjectManager.qss @@ -61,6 +61,24 @@ QTabBar::tab:focus { color: #4082eb; } +#ToastNotification { + background-color: black; + border-radius: 20px; + border:1px solid #dddddd; + qproperty-minimumSize: 100px 50px; +} + +#ToastNotification #icon_frame { + border-radius: 4px; + qproperty-minimumSize: 44px 20px; +} + +#ToastNotification #iconLabel { + qproperty-minimumSize: 30px 20px; + qproperty-maximumSize: 30px 20px; + margin-left: 6px; +} + /************** General (Forms) **************/ #formLineEditWidget, diff --git a/Code/Tools/ProjectManager/Resources/gem.svg b/Code/Tools/ProjectManager/Resources/gem.svg new file mode 100644 index 0000000000..2b688d5db5 --- /dev/null +++ b/Code/Tools/ProjectManager/Resources/gem.svg @@ -0,0 +1,3 @@ + + + diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp index cbe36400cf..69c5844e84 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp @@ -66,6 +66,9 @@ namespace O3DE::ProjectManager hLayout->addWidget(filterWidget); hLayout->addLayout(middleVLayout); hLayout->addWidget(m_gemInspector); + + m_notificationsView = AZStd::make_unique(this, AZ_CRC("GemCatalogNotificationsView")); + m_notificationsView->SetOffset(QPoint(10, 70)); } void GemCatalogScreen::ReinitForProject(const QString& projectPath) @@ -86,6 +89,7 @@ namespace O3DE::ProjectManager m_headerWidget->ReinitForProject(); connect(m_gemModel, &GemModel::dataChanged, m_filterWidget, &GemFilterWidget::ResetGemStatusFilter); + connect(m_gemModel, &GemModel::gemStatusChanged, this, &GemCatalogScreen::OnGemStatusChanged); // Select the first entry after everything got correctly sized QTimer::singleShot(200, [=]{ @@ -94,6 +98,72 @@ namespace O3DE::ProjectManager }); } + void GemCatalogScreen::OnGemStatusChanged(const QModelIndex& modelIndex, uint32_t numChangedDependencies) + { + if (m_notificationsEnabled) + { + bool added = GemModel::IsAdded(modelIndex); + bool dependency = GemModel::IsAddedDependency(modelIndex); + + bool gemStateChanged = (added && !dependency) || (!added && !dependency); + if (!gemStateChanged && !numChangedDependencies) + { + // no actual changes made + return; + } + + QString notification; + if (gemStateChanged) + { + notification = GemModel::GetDisplayName(modelIndex); + if (numChangedDependencies > 0) + { + notification += " " + tr("and") + " "; + } + } + + if (numChangedDependencies == 1 ) + { + notification += "1 Gem " + tr("dependency"); + } + else if (numChangedDependencies > 1) + { + notification += QString("%d Gem ").arg(numChangedDependencies) + tr("dependencies"); + } + notification += " " + (added ? tr("activated") : tr("deactivated")); + + AzQtComponents::ToastConfiguration toastConfiguration(AzQtComponents::ToastType::Custom, notification, ""); + toastConfiguration.m_customIconImage = ":/gem.svg"; + toastConfiguration.m_borderRadius = 4; + toastConfiguration.m_duration = AZStd::chrono::milliseconds(3000); + m_notificationsView->ShowToastNotification(toastConfiguration); + } + } + + void GemCatalogScreen::hideEvent(QHideEvent* event) + { + ScreenWidget::hideEvent(event); + m_notificationsView->OnHide(); + } + + void GemCatalogScreen::showEvent(QShowEvent* event) + { + ScreenWidget::showEvent(event); + m_notificationsView->OnShow(); + } + + void GemCatalogScreen::resizeEvent(QResizeEvent* event) + { + ScreenWidget::resizeEvent(event); + m_notificationsView->UpdateToastPosition(); + } + + void GemCatalogScreen::moveEvent(QMoveEvent* event) + { + ScreenWidget::moveEvent(event); + m_notificationsView->UpdateToastPosition(); + } + void GemCatalogScreen::FillModel(const QString& projectPath) { AZ::Outcome, AZStd::string> allGemInfosResult = PythonBindingsInterface::Get()->GetAllGemInfos(projectPath); @@ -107,6 +177,7 @@ namespace O3DE::ProjectManager } m_gemModel->UpdateGemDependencies(); + m_notificationsEnabled = false; // Gather enabled gems for the given project. auto enabledGemNamesResult = PythonBindingsInterface::Get()->GetEnabledGemNames(projectPath); @@ -133,6 +204,8 @@ namespace O3DE::ProjectManager { QMessageBox::critical(nullptr, tr("Operation failed"), QString("Cannot retrieve enabled gems for project %1.\n\nError:\n%2").arg(projectPath, enabledGemNamesResult.GetError().c_str())); } + + m_notificationsEnabled = true; } else { diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h index 456a5fe91c..62528ba942 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h @@ -10,6 +10,8 @@ #if !defined(Q_MOC_RUN) #include +#include +#include #include #include #include @@ -41,13 +43,24 @@ namespace O3DE::ProjectManager GemModel* GetGemModel() const { return m_gemModel; } DownloadController* GetDownloadController() const { return m_downloadController; } + public slots: + void OnGemStatusChanged(const QModelIndex& modelIndex, uint32_t numChangedDependencies); + + protected: + void hideEvent(QHideEvent* event) override; + void showEvent(QShowEvent* event) override; + void resizeEvent(QResizeEvent* event) override; + void moveEvent(QMoveEvent* event) override; + private slots: void HandleOpenGemRepo(); - private: + private: void FillModel(const QString& projectPath); + AZStd::unique_ptr m_notificationsView; + GemListView* m_gemListView = nullptr; GemInspector* m_gemInspector = nullptr; GemModel* m_gemModel = nullptr; @@ -56,5 +69,6 @@ namespace O3DE::ProjectManager QVBoxLayout* m_filterWidgetLayout = nullptr; GemFilterWidget* m_filterWidget = nullptr; DownloadController* m_downloadController = nullptr; + bool m_notificationsEnabled = true; }; } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp index 35491f4ddd..acdef483ae 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace O3DE::ProjectManager { @@ -299,23 +300,50 @@ namespace O3DE::ProjectManager AZ_Assert(gemModel, "Failed to obtain GemModel"); QVector dependencies = gemModel->GatherGemDependencies(modelIndex); + uint32_t numChangedDependencies = 0; + if (IsAdded(modelIndex)) { for (const QModelIndex& dependency : dependencies) { - SetIsAddedDependency(*gemModel, dependency, true); + if (!IsAddedDependency(dependency)) + { + SetIsAddedDependency(*gemModel, dependency, true); + + // if the gem was already added then the state didn't really change + if (!IsAdded(dependency)) + { + numChangedDependencies++; + } + } } } else { // still a dependency if some added gem depends on this one - SetIsAddedDependency(model, modelIndex, gemModel->HasDependentGems(modelIndex)); + bool hasDependentGems = gemModel->HasDependentGems(modelIndex); + if (IsAddedDependency(modelIndex) != hasDependentGems) + { + SetIsAddedDependency(model, modelIndex, hasDependentGems); + } for (const QModelIndex& dependency : dependencies) { - SetIsAddedDependency(*gemModel, dependency, gemModel->HasDependentGems(dependency)); + hasDependentGems = gemModel->HasDependentGems(dependency); + if (IsAddedDependency(dependency) != hasDependentGems) + { + SetIsAddedDependency(*gemModel, dependency, hasDependentGems); + + // if the gem was already added then the state didn't really change + if (!IsAdded(dependency)) + { + numChangedDependencies++; + } + } } } + + gemModel->emit gemStatusChanged(modelIndex, numChangedDependencies); } void GemModel::SetIsAddedDependency(QAbstractItemModel& model, const QModelIndex& modelIndex, bool isAdded) @@ -488,5 +516,4 @@ namespace O3DE::ProjectManager } return result; } - } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h index 0d1c225f74..938543eb39 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h @@ -77,6 +77,9 @@ namespace O3DE::ProjectManager int TotalAddedGems(bool includeDependencies = false) const; + signals: + void gemStatusChanged(const QModelIndex& modelIndex, uint32_t numChangedDependencies); + private: void FindGemDisplayNamesByNameStrings(QStringList& inOutGemNames); void GetAllDependingGems(const QModelIndex& modelIndex, QSet& inOutGems);