diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkTransformComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkTransformComponent.h index 35bf6e9f50..b0370e9027 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkTransformComponent.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkTransformComponent.h @@ -50,7 +50,7 @@ namespace Multiplayer void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; private: - void OnTransformChangedEvent(const AZ::Transform& worldTm); + void OnTransformChangedEvent(const AZ::Transform& localTm, const AZ::Transform& worldTm); void OnParentIdChangedEvent(AZ::EntityId oldParent, AZ::EntityId newParent); AZ::TransformChangedEvent::Handler m_transformChangedHandler; diff --git a/Gems/Multiplayer/Code/Source/Components/NetworkTransformComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetworkTransformComponent.cpp index d284f100ff..bd1e1bf0d9 100644 --- a/Gems/Multiplayer/Code/Source/Components/NetworkTransformComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/NetworkTransformComponent.cpp @@ -14,7 +14,7 @@ namespace Multiplayer { - void NetworkTransformComponent::NetworkTransformComponent::Reflect(AZ::ReflectContext* context) + void NetworkTransformComponent::Reflect(AZ::ReflectContext* context) { AZ::SerializeContext* serializeContext = azrtti_cast(context); if (serializeContext) @@ -43,6 +43,11 @@ namespace Multiplayer GetNetBindComponent()->AddEntityPreRenderEventHandler(m_entityPreRenderEventHandler); GetNetBindComponent()->AddEntityCorrectionEventHandler(m_entityCorrectionEventHandler); ParentEntityIdAddEvent(m_parentChangedEventHandler); + + if (!HasController()) + { + OnParentChanged(GetParentEntityId()); + } } void NetworkTransformComponent::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) @@ -77,9 +82,20 @@ namespace Multiplayer } } - if (!GetTransformComponent()->GetWorldTM().IsClose(blendTransform)) + AzFramework::TransformComponent* transformComponent = GetTransformComponent(); + if (GetParentEntityId() == InvalidNetEntityId) { - GetTransformComponent()->SetWorldTM(blendTransform); + if (!transformComponent->GetWorldTM().IsClose(blendTransform)) + { + transformComponent->SetWorldTM(blendTransform); + } + } + else + { + if (!transformComponent->GetLocalTM().IsClose(blendTransform)) + { + transformComponent->SetLocalTM(blendTransform); + } } } } @@ -93,31 +109,45 @@ namespace Multiplayer targetTransform.SetUniformScale(GetScale()); // Hard set the entities transform - if (!GetTransformComponent()->GetWorldTM().IsClose(targetTransform)) + AzFramework::TransformComponent* transformComponent = GetTransformComponent(); + if (GetParentEntityId() == InvalidNetEntityId) { - GetTransformComponent()->SetWorldTM(targetTransform); + if (!transformComponent->GetWorldTM().IsClose(targetTransform)) + { + transformComponent->SetWorldTM(targetTransform); + } + } + else + { + if (!transformComponent->GetLocalTM().IsClose(targetTransform)) + { + transformComponent->SetLocalTM(targetTransform); + } } } void NetworkTransformComponent::OnParentChanged(NetEntityId parentId) { - const ConstNetworkEntityHandle parentEntityHandle = GetNetworkEntityManager()->GetEntity(parentId); - if (parentEntityHandle.Exists()) + if (AZ::TransformInterface* transformComponent = GetEntity()->GetTransform()) { - if (const AZ::Entity* parentEntity = parentEntityHandle.GetEntity()) + const ConstNetworkEntityHandle parentEntityHandle = GetNetworkEntityManager()->GetEntity(parentId); + if (parentEntityHandle.Exists()) { - GetEntity()->GetTransform()->SetParent(parentEntity->GetId()); + if (const AZ::Entity* parentEntity = parentEntityHandle.GetEntity()) + { + transformComponent->SetParent(parentEntity->GetId()); + } + } + else + { + transformComponent->SetParent(AZ::EntityId()); } - } - else - { - GetEntity()->GetTransform()->SetParent(AZ::EntityId()); } } NetworkTransformComponentController::NetworkTransformComponentController(NetworkTransformComponent& parent) : NetworkTransformComponentControllerBase(parent) - , m_transformChangedHandler([this](const AZ::Transform&, const AZ::Transform& worldTm) { OnTransformChangedEvent(worldTm); }) + , m_transformChangedHandler([this](const AZ::Transform& localTm, const AZ::Transform& worldTm) { OnTransformChangedEvent(localTm, worldTm); }) , m_parentIdChangedHandler([this](AZ::EntityId oldParent, AZ::EntityId newParent) { OnParentIdChangedEvent(oldParent, newParent); }) { ; @@ -125,11 +155,14 @@ namespace Multiplayer void NetworkTransformComponentController::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) { - GetParent().GetTransformComponent()->BindTransformChangedEventHandler(m_transformChangedHandler); - OnTransformChangedEvent(GetParent().GetTransformComponent()->GetWorldTM()); + if (AzFramework::TransformComponent* parentTransform = GetParent().GetTransformComponent()) + { + parentTransform->BindTransformChangedEventHandler(m_transformChangedHandler); + OnTransformChangedEvent(parentTransform->GetLocalTM(), parentTransform->GetWorldTM()); - GetParent().GetTransformComponent()->BindParentChangedEventHandler(m_parentIdChangedHandler); - OnParentIdChangedEvent(AZ::EntityId(), GetParent().GetTransformComponent()->GetParentId()); + parentTransform->BindParentChangedEventHandler(m_parentIdChangedHandler); + OnParentIdChangedEvent(AZ::EntityId(), parentTransform->GetParentId()); + } } void NetworkTransformComponentController::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) @@ -137,11 +170,12 @@ namespace Multiplayer ; } - void NetworkTransformComponentController::OnTransformChangedEvent(const AZ::Transform& worldTm) + void NetworkTransformComponentController::OnTransformChangedEvent(const AZ::Transform& localTm, const AZ::Transform& worldTm) { - SetRotation(worldTm.GetRotation()); - SetTranslation(worldTm.GetTranslation()); - SetScale(worldTm.GetUniformScale()); + const AZ::Transform& localOrWorld = GetParentEntityId() == InvalidNetEntityId ? worldTm : localTm; + SetRotation(localOrWorld.GetRotation()); + SetTranslation(localOrWorld.GetTranslation()); + SetScale(localOrWorld.GetUniformScale()); } void NetworkTransformComponentController::OnParentIdChangedEvent([[maybe_unused]] AZ::EntityId oldParent, AZ::EntityId newParent) @@ -150,7 +184,10 @@ namespace Multiplayer if (parentEntity) { const ConstNetworkEntityHandle parentHandle(parentEntity, GetNetworkEntityTracker()); - SetParentEntityId(parentHandle.GetNetEntityId()); + if (parentHandle.Exists()) + { + SetParentEntityId(parentHandle.GetNetEntityId()); + } } } } diff --git a/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h b/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h index 2deac1aa27..3a5409c7ed 100644 --- a/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h +++ b/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -248,17 +249,19 @@ namespace Multiplayer void SetupEntity(const AZStd::unique_ptr& entity, NetEntityId netId, NetEntityRole role) { - const auto netBindComponent = entity->FindComponent(); - EXPECT_NE(netBindComponent, nullptr); - netBindComponent->PreInit(entity.get(), PrefabEntityId{ AZ::Name("test"), 1 }, netId, role); - entity->Init(); + if (const auto netBindComponent = entity->FindComponent()) + { + netBindComponent->PreInit(entity.get(), PrefabEntityId{ AZ::Name("test"), 1 }, netId, role); + entity->Init(); + } } static void StopEntity(const AZStd::unique_ptr& entity) { - const auto netBindComponent = entity->FindComponent(); - EXPECT_NE(netBindComponent, nullptr); - netBindComponent->StopEntity(); + if (const auto netBindComponent = entity->FindComponent()) + { + netBindComponent->StopEntity(); + } } static void StopAndDeactivateEntity(AZStd::unique_ptr& entity) @@ -311,6 +314,29 @@ namespace Multiplayer entity->FindComponent()->NotifyStateDeltaChanges(notifyRecord); } + void SetTranslationOnNetworkTransform(const AZStd::unique_ptr& entity, AZ::Vector3 translation) + { + /* Derived from NetworkTransformComponent.AutoComponent.xml */ + constexpr int totalBits = 6 /*NetworkTransformComponentInternal::AuthorityToClientDirtyEnum::Count*/; + constexpr int translationBit = 1 /*NetworkTransformComponentInternal::AuthorityToClientDirtyEnum::translation_DirtyFlag*/; + + ReplicationRecord currentRecord; + currentRecord.m_authorityToClient.AddBits(totalBits); + currentRecord.m_authorityToClient.SetBit(translationBit, true); + + constexpr uint32_t bufferSize = 100; + AZStd::array buffer = {}; + NetworkInputSerializer inSerializer(buffer.begin(), bufferSize); + static_cast(&inSerializer)->Serialize(translation, + "translation" /* Derived from NetworkTransformComponent.AutoComponent.xml */); + + NetworkOutputSerializer outSerializer(buffer.begin(), bufferSize); + + ReplicationRecord notifyRecord = currentRecord; + entity->FindComponent()->SerializeStateDeltaMessage(currentRecord, outSerializer); + entity->FindComponent()->NotifyStateDeltaChanges(notifyRecord); + } + template void SetHierarchyRootFieldOnNetworkHierarchyChild(const AZStd::unique_ptr& entity, NetEntityId value) { diff --git a/Gems/Multiplayer/Code/Tests/MultiplayerSystemTests.cpp b/Gems/Multiplayer/Code/Tests/MultiplayerSystemTests.cpp index 2463a8e4cb..d6c3117d6f 100644 --- a/Gems/Multiplayer/Code/Tests/MultiplayerSystemTests.cpp +++ b/Gems/Multiplayer/Code/Tests/MultiplayerSystemTests.cpp @@ -95,8 +95,9 @@ namespace UnitTest TEST_F(MultiplayerSystemTests, TestConnectionDatum) { - IMultiplayerConnectionMock connMock1 = IMultiplayerConnectionMock(aznumeric_cast(10), AzNetworking::IpAddress(), AzNetworking::ConnectionRole::Acceptor); - IMultiplayerConnectionMock connMock2 = IMultiplayerConnectionMock(aznumeric_cast(15), AzNetworking::IpAddress(), AzNetworking::ConnectionRole::Acceptor); + using namespace testing; + NiceMock connMock1(aznumeric_cast(10), AzNetworking::IpAddress(), AzNetworking::ConnectionRole::Acceptor); + NiceMock connMock2(aznumeric_cast(15), AzNetworking::IpAddress(), AzNetworking::ConnectionRole::Acceptor); m_mpComponent->OnConnect(&connMock1); m_mpComponent->OnConnect(&connMock2); diff --git a/Gems/Multiplayer/Code/Tests/NetworkTransformTests.cpp b/Gems/Multiplayer/Code/Tests/NetworkTransformTests.cpp new file mode 100644 index 0000000000..94cb94e000 --- /dev/null +++ b/Gems/Multiplayer/Code/Tests/NetworkTransformTests.cpp @@ -0,0 +1,320 @@ +/* + * 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 + +namespace Multiplayer +{ + using namespace testing; + using namespace ::UnitTest; + + /* + * (Networked) Parent -> (Networked) Child + */ + class ServerNetTransformTests : public HierarchyTests + { + public: + void SetUp() override + { + HierarchyTests::SetUp(); + + m_root = AZStd::make_unique(1, "root", NetEntityId{ 1 }, EntityInfo::Role::Root); + m_child = AZStd::make_unique(2, "child", NetEntityId{ 2 }, EntityInfo::Role::Child); + + CreateNetworkParentChild(*m_root, *m_child); + + AZ::Transform rootTransform = AZ::Transform::CreateIdentity(); + rootTransform.SetTranslation(AZ::Vector3::CreateOne()); + m_root->m_entity->FindComponent()->SetWorldTM(rootTransform); + m_child->m_entity->FindComponent()->SetWorldTM(rootTransform); + + m_child->m_entity->FindComponent()->SetParent(m_root->m_entity->GetId()); + m_child->m_entity->FindComponent()->SetLocalTM(AZ::Transform::CreateIdentity()); + + AZ::EntityBus::Broadcast(&AZ::EntityBus::Events::OnEntityActivated, m_root->m_entity->GetId()); + + MultiplayerTick(); + } + + void TearDown() override + { + m_child.reset(); + m_root.reset(); + + HierarchyTests::TearDown(); + } + + void PopulateNetworkEntity(const EntityInfo& entityInfo) + { + entityInfo.m_entity->CreateComponent(); + entityInfo.m_entity->CreateComponent(); + entityInfo.m_entity->CreateComponent(); + } + + void CreateNetworkParentChild(EntityInfo& root, EntityInfo& child) + { + PopulateNetworkEntity(root); + SetupEntity(root.m_entity, root.m_netId, NetEntityRole::Authority); + + PopulateNetworkEntity(child); + SetupEntity(child.m_entity, child.m_netId, NetEntityRole::Authority); + + // Create an entity replicator for the child entity + const NetworkEntityHandle childHandle(child.m_entity.get(), m_networkEntityTracker.get()); + child.m_replicator = AZStd::make_unique(*m_entityReplicationManager, m_mockConnection.get(), NetEntityRole::Client, childHandle); + child.m_replicator->Initialize(childHandle); + + // Create an entity replicator for the root entity + const NetworkEntityHandle rootHandle(root.m_entity.get(), m_networkEntityTracker.get()); + root.m_replicator = AZStd::make_unique(*m_entityReplicationManager, m_mockConnection.get(), NetEntityRole::Client, rootHandle); + root.m_replicator->Initialize(rootHandle); + + root.m_entity->Activate(); + child.m_entity->Activate(); + } + + AZStd::unique_ptr m_root; + AZStd::unique_ptr m_child; + + void MultiplayerTick() + { + m_root->m_entity->FindComponent()->NotifyPreRender(0.1f); + m_child->m_entity->FindComponent()->NotifyPreRender(0.1f); + } + }; + + TEST_F(ServerNetTransformTests, SanityCheck) + { + EXPECT_EQ( + m_root->m_entity->FindComponent()->GetWorldTM().GetTranslation(), + AZ::Vector3::CreateOne() + ); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetWorldTM().GetTranslation(), + AZ::Vector3::CreateOne() + ); + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetLocalTM().GetTranslation(), + AZ::Vector3::CreateZero() + ); + } + + TEST_F(ServerNetTransformTests, NetTransformSavesLocalTransformWhenParentSet) + { + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetTranslation(), + AZ::Vector3::CreateZero() + ); + } + + TEST_F(ServerNetTransformTests, NetTransformSavesWorldTransformWhenParentIsNotSet) + { + m_child->m_entity->FindComponent()->SetParent(AZ::EntityId()); + MultiplayerTick(); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetTranslation(), + AZ::Vector3::CreateOne() // back at the parent translation + ); + } + + TEST_F(ServerNetTransformTests, ParentMovesChildNetTransformDoesntChange) + { + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetTranslation(), + AZ::Vector3::CreateZero() + ); + + // move the parent + AZ::Transform rootTransform = AZ::Transform::CreateIdentity(); + rootTransform.SetTranslation(AZ::Vector3::CreateOne() * 10.f); + m_root->m_entity->FindComponent()->SetWorldTM(rootTransform); + + MultiplayerTick(); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetWorldTM().GetTranslation(), + AZ::Vector3::CreateOne() * 10.f + ); + // child local tm doesn't change + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetLocalTM().GetTranslation(), + AZ::Vector3::CreateZero() + ); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetTranslation(), + AZ::Vector3::CreateZero() + ); + } + + /* + * (Networked) Parent -> (Networked) Child + */ + class ClientNetTransformTests : public HierarchyTests + { + public: + void SetUp() override + { + HierarchyTests::SetUp(); + + m_root = AZStd::make_unique(1, "root", NetEntityId{ 1 }, EntityInfo::Role::Root); + m_child = AZStd::make_unique(2, "child", NetEntityId{ 2 }, EntityInfo::Role::Child); + + CreateNetworkParentChild(*m_root, *m_child); + } + + void TearDown() override + { + m_child.reset(); + m_root.reset(); + + HierarchyTests::TearDown(); + } + + void PopulateNetworkEntity(const EntityInfo& entityInfo) + { + entityInfo.m_entity->CreateComponent(); + entityInfo.m_entity->CreateComponent(); + entityInfo.m_entity->CreateComponent(); + } + + void CreateNetworkParentChild(EntityInfo& root, EntityInfo& child) + { + PopulateNetworkEntity(root); + SetupEntity(root.m_entity, root.m_netId, NetEntityRole::Client); + + PopulateNetworkEntity(child); + SetupEntity(child.m_entity, child.m_netId, NetEntityRole::Client); + + // Create an entity replicator for the child entity + const NetworkEntityHandle childHandle(child.m_entity.get(), m_networkEntityTracker.get()); + child.m_replicator = AZStd::make_unique(*m_entityReplicationManager, m_mockConnection.get(), NetEntityRole::Authority, childHandle); + child.m_replicator->Initialize(childHandle); + + // Create an entity replicator for the root entity + const NetworkEntityHandle rootHandle(root.m_entity.get(), m_networkEntityTracker.get()); + root.m_replicator = AZStd::make_unique(*m_entityReplicationManager, m_mockConnection.get(), NetEntityRole::Authority, rootHandle); + root.m_replicator->Initialize(rootHandle); + } + + AZStd::unique_ptr m_root; + AZStd::unique_ptr m_child; + + void MultiplayerTick() + { + m_root->m_entity->FindComponent()->NotifyPreRender(0.1f); + m_child->m_entity->FindComponent()->NotifyPreRender(0.1f); + } + }; + + TEST_F(ClientNetTransformTests, ClientSetsLocalTmWhenParentIsSet) + { + m_root->m_entity->Activate(); + m_child->m_entity->Activate(); + + SetTranslationOnNetworkTransform(m_root->m_entity, AZ::Vector3::CreateOne()); + + SetParentIdOnNetworkTransform(m_child->m_entity, NetEntityId{ 1 }); + SetTranslationOnNetworkTransform(m_child->m_entity, AZ::Vector3::CreateZero()); + + AZ::EntityBus::Broadcast(&AZ::EntityBus::Events::OnEntityActivated, m_root->m_entity->GetId()); + + MultiplayerTick(); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetWorldTM().GetTranslation(), + AZ::Vector3::CreateOne() + ); + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetLocalTM().GetTranslation(), + AZ::Vector3::CreateZero() + ); + } + + TEST_F(ClientNetTransformTests, ClientSetsWorldTmWhenParentIsNotSet) + { + m_root->m_entity->Activate(); + m_child->m_entity->Activate(); + + SetTranslationOnNetworkTransform(m_root->m_entity, AZ::Vector3::CreateOne()); + SetTranslationOnNetworkTransform(m_child->m_entity, AZ::Vector3::CreateZero()); + + AZ::EntityBus::Broadcast(&AZ::EntityBus::Events::OnEntityActivated, m_root->m_entity->GetId()); + + MultiplayerTick(); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetWorldTM().GetTranslation(), + AZ::Vector3::CreateZero() + ); + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetLocalTM().GetTranslation(), + AZ::Vector3::CreateZero() + ); + } + + TEST_F(ClientNetTransformTests, ChildFollowsWhenParentMovesOnServer) + { + m_root->m_entity->Activate(); + m_child->m_entity->Activate(); + + SetTranslationOnNetworkTransform(m_root->m_entity, AZ::Vector3::CreateOne()); + + SetParentIdOnNetworkTransform(m_child->m_entity, NetEntityId{ 1 }); + SetTranslationOnNetworkTransform(m_child->m_entity, AZ::Vector3::CreateZero()); + + AZ::EntityBus::Broadcast(&AZ::EntityBus::Events::OnEntityActivated, m_root->m_entity->GetId()); + + MultiplayerTick(); + + // now parent moves + SetTranslationOnNetworkTransform(m_root->m_entity, AZ::Vector3::CreateOne() * 2.f); + MultiplayerTick(); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetWorldTM().GetTranslation(), + AZ::Vector3::CreateOne() * 2.f + ); + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetLocalTM().GetTranslation(), + AZ::Vector3::CreateZero() + ); + } + + TEST_F(ClientNetTransformTests, ChildAttachesToParentIfParentIdIsSetBeforeActivation) + { + m_root->m_entity->Activate(); + + SetTranslationOnNetworkTransform(m_root->m_entity, AZ::Vector3::CreateOne()); + + SetParentIdOnNetworkTransform(m_child->m_entity, NetEntityId{ 1 }); + SetTranslationOnNetworkTransform(m_child->m_entity, AZ::Vector3::CreateZero()); + + m_child->m_entity->Activate(); + + AZ::EntityBus::Broadcast(&AZ::EntityBus::Events::OnEntityActivated, m_root->m_entity->GetId()); + + MultiplayerTick(); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetParentId(), + AZ::EntityId(1) + ); + } +} diff --git a/Gems/Multiplayer/Code/multiplayer_tests_files.cmake b/Gems/Multiplayer/Code/multiplayer_tests_files.cmake index 3f4fcc9efa..93fb807b66 100644 --- a/Gems/Multiplayer/Code/multiplayer_tests_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_tests_files.cmake @@ -7,13 +7,14 @@ # set(FILES - Tests/Main.cpp - Tests/MockInterfaces.h Tests/ClientHierarchyTests.cpp - Tests/ServerHierarchyTests.cpp Tests/CommonHierarchySetup.h Tests/IMultiplayerConnectionMock.h + Tests/Main.cpp + Tests/MockInterfaces.h Tests/MultiplayerSystemTests.cpp + Tests/NetworkTransformTests.cpp Tests/RewindableContainerTests.cpp Tests/RewindableObjectTests.cpp + Tests/ServerHierarchyTests.cpp )