NetworkTransformComponent replicates local transform for child entities

+ NetworkTransformComponent replicates world transform for parent entities
+ NetworkTransformComponent replicates local transform for child entities
+ Server-side and client-side unittests
This commit is contained in:
Olex Lozitskiy
2021-10-02 09:50:10 -04:00
committed by GitHub
6 changed files with 421 additions and 36 deletions
@@ -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;
@@ -14,7 +14,7 @@
namespace Multiplayer
{
void NetworkTransformComponent::NetworkTransformComponent::Reflect(AZ::ReflectContext* context)
void NetworkTransformComponent::Reflect(AZ::ReflectContext* context)
{
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(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());
}
}
}
}
@@ -11,6 +11,7 @@
#include <MockInterfaces.h>
#include <AzCore/Component/Entity.h>
#include <AzCore/Console/Console.h>
#include <AzCore/Math/Vector3.h>
#include <AzCore/Name/Name.h>
#include <AzCore/Name/NameDictionary.h>
#include <AzCore/Serialization/SerializeContext.h>
@@ -248,17 +249,19 @@ namespace Multiplayer
void SetupEntity(const AZStd::unique_ptr<AZ::Entity>& entity, NetEntityId netId, NetEntityRole role)
{
const auto netBindComponent = entity->FindComponent<Multiplayer::NetBindComponent>();
EXPECT_NE(netBindComponent, nullptr);
netBindComponent->PreInit(entity.get(), PrefabEntityId{ AZ::Name("test"), 1 }, netId, role);
entity->Init();
if (const auto netBindComponent = entity->FindComponent<Multiplayer::NetBindComponent>())
{
netBindComponent->PreInit(entity.get(), PrefabEntityId{ AZ::Name("test"), 1 }, netId, role);
entity->Init();
}
}
static void StopEntity(const AZStd::unique_ptr<AZ::Entity>& entity)
{
const auto netBindComponent = entity->FindComponent<Multiplayer::NetBindComponent>();
EXPECT_NE(netBindComponent, nullptr);
netBindComponent->StopEntity();
if (const auto netBindComponent = entity->FindComponent<Multiplayer::NetBindComponent>())
{
netBindComponent->StopEntity();
}
}
static void StopAndDeactivateEntity(AZStd::unique_ptr<AZ::Entity>& entity)
@@ -311,6 +314,29 @@ namespace Multiplayer
entity->FindComponent<NetworkTransformComponent>()->NotifyStateDeltaChanges(notifyRecord);
}
void SetTranslationOnNetworkTransform(const AZStd::unique_ptr<AZ::Entity>& 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<uint8_t, bufferSize> buffer = {};
NetworkInputSerializer inSerializer(buffer.begin(), bufferSize);
static_cast<ISerializer*>(&inSerializer)->Serialize(translation,
"translation" /* Derived from NetworkTransformComponent.AutoComponent.xml */);
NetworkOutputSerializer outSerializer(buffer.begin(), bufferSize);
ReplicationRecord notifyRecord = currentRecord;
entity->FindComponent<NetworkTransformComponent>()->SerializeStateDeltaMessage(currentRecord, outSerializer);
entity->FindComponent<NetworkTransformComponent>()->NotifyStateDeltaChanges(notifyRecord);
}
template <typename Component>
void SetHierarchyRootFieldOnNetworkHierarchyChild(const AZStd::unique_ptr<AZ::Entity>& entity, NetEntityId value)
{
@@ -95,8 +95,9 @@ namespace UnitTest
TEST_F(MultiplayerSystemTests, TestConnectionDatum)
{
IMultiplayerConnectionMock connMock1 = IMultiplayerConnectionMock(aznumeric_cast<AzNetworking::ConnectionId>(10), AzNetworking::IpAddress(), AzNetworking::ConnectionRole::Acceptor);
IMultiplayerConnectionMock connMock2 = IMultiplayerConnectionMock(aznumeric_cast<AzNetworking::ConnectionId>(15), AzNetworking::IpAddress(), AzNetworking::ConnectionRole::Acceptor);
using namespace testing;
NiceMock<IMultiplayerConnectionMock> connMock1(aznumeric_cast<AzNetworking::ConnectionId>(10), AzNetworking::IpAddress(), AzNetworking::ConnectionRole::Acceptor);
NiceMock<IMultiplayerConnectionMock> connMock2(aznumeric_cast<AzNetworking::ConnectionId>(15), AzNetworking::IpAddress(), AzNetworking::ConnectionRole::Acceptor);
m_mpComponent->OnConnect(&connMock1);
m_mpComponent->OnConnect(&connMock2);
@@ -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 <CommonHierarchySetup.h>
#include <MockInterfaces.h>
#include <AzCore/Component/Entity.h>
#include <AzCore/Console/Console.h>
#include <AzCore/Name/Name.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <AzCore/UnitTest/UnitTest.h>
#include <AzFramework/Components/TransformComponent.h>
#include <AzTest/AzTest.h>
#include <Multiplayer/Components/NetBindComponent.h>
#include <NetworkEntity/EntityReplication/EntityReplicator.h>
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<EntityInfo>(1, "root", NetEntityId{ 1 }, EntityInfo::Role::Root);
m_child = AZStd::make_unique<EntityInfo>(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<AzFramework::TransformComponent>()->SetWorldTM(rootTransform);
m_child->m_entity->FindComponent<AzFramework::TransformComponent>()->SetWorldTM(rootTransform);
m_child->m_entity->FindComponent<AzFramework::TransformComponent>()->SetParent(m_root->m_entity->GetId());
m_child->m_entity->FindComponent<AzFramework::TransformComponent>()->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<AzFramework::TransformComponent>();
entityInfo.m_entity->CreateComponent<NetBindComponent>();
entityInfo.m_entity->CreateComponent<NetworkTransformComponent>();
}
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<EntityReplicator>(*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<EntityReplicator>(*m_entityReplicationManager, m_mockConnection.get(), NetEntityRole::Client, rootHandle);
root.m_replicator->Initialize(rootHandle);
root.m_entity->Activate();
child.m_entity->Activate();
}
AZStd::unique_ptr<EntityInfo> m_root;
AZStd::unique_ptr<EntityInfo> m_child;
void MultiplayerTick()
{
m_root->m_entity->FindComponent<NetBindComponent>()->NotifyPreRender(0.1f);
m_child->m_entity->FindComponent<NetBindComponent>()->NotifyPreRender(0.1f);
}
};
TEST_F(ServerNetTransformTests, SanityCheck)
{
EXPECT_EQ(
m_root->m_entity->FindComponent<AzFramework::TransformComponent>()->GetWorldTM().GetTranslation(),
AZ::Vector3::CreateOne()
);
EXPECT_EQ(
m_child->m_entity->FindComponent<AzFramework::TransformComponent>()->GetWorldTM().GetTranslation(),
AZ::Vector3::CreateOne()
);
EXPECT_EQ(
m_child->m_entity->FindComponent<AzFramework::TransformComponent>()->GetLocalTM().GetTranslation(),
AZ::Vector3::CreateZero()
);
}
TEST_F(ServerNetTransformTests, NetTransformSavesLocalTransformWhenParentSet)
{
EXPECT_EQ(
m_child->m_entity->FindComponent<NetworkTransformComponent>()->GetTranslation(),
AZ::Vector3::CreateZero()
);
}
TEST_F(ServerNetTransformTests, NetTransformSavesWorldTransformWhenParentIsNotSet)
{
m_child->m_entity->FindComponent<AzFramework::TransformComponent>()->SetParent(AZ::EntityId());
MultiplayerTick();
EXPECT_EQ(
m_child->m_entity->FindComponent<NetworkTransformComponent>()->GetTranslation(),
AZ::Vector3::CreateOne() // back at the parent translation
);
}
TEST_F(ServerNetTransformTests, ParentMovesChildNetTransformDoesntChange)
{
EXPECT_EQ(
m_child->m_entity->FindComponent<NetworkTransformComponent>()->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<AzFramework::TransformComponent>()->SetWorldTM(rootTransform);
MultiplayerTick();
EXPECT_EQ(
m_child->m_entity->FindComponent<AzFramework::TransformComponent>()->GetWorldTM().GetTranslation(),
AZ::Vector3::CreateOne() * 10.f
);
// child local tm doesn't change
EXPECT_EQ(
m_child->m_entity->FindComponent<AzFramework::TransformComponent>()->GetLocalTM().GetTranslation(),
AZ::Vector3::CreateZero()
);
EXPECT_EQ(
m_child->m_entity->FindComponent<NetworkTransformComponent>()->GetTranslation(),
AZ::Vector3::CreateZero()
);
}
/*
* (Networked) Parent -> (Networked) Child
*/
class ClientNetTransformTests : public HierarchyTests
{
public:
void SetUp() override
{
HierarchyTests::SetUp();
m_root = AZStd::make_unique<EntityInfo>(1, "root", NetEntityId{ 1 }, EntityInfo::Role::Root);
m_child = AZStd::make_unique<EntityInfo>(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<AzFramework::TransformComponent>();
entityInfo.m_entity->CreateComponent<NetBindComponent>();
entityInfo.m_entity->CreateComponent<NetworkTransformComponent>();
}
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<EntityReplicator>(*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<EntityReplicator>(*m_entityReplicationManager, m_mockConnection.get(), NetEntityRole::Authority, rootHandle);
root.m_replicator->Initialize(rootHandle);
}
AZStd::unique_ptr<EntityInfo> m_root;
AZStd::unique_ptr<EntityInfo> m_child;
void MultiplayerTick()
{
m_root->m_entity->FindComponent<NetBindComponent>()->NotifyPreRender(0.1f);
m_child->m_entity->FindComponent<NetBindComponent>()->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<AzFramework::TransformComponent>()->GetWorldTM().GetTranslation(),
AZ::Vector3::CreateOne()
);
EXPECT_EQ(
m_child->m_entity->FindComponent<AzFramework::TransformComponent>()->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<AzFramework::TransformComponent>()->GetWorldTM().GetTranslation(),
AZ::Vector3::CreateZero()
);
EXPECT_EQ(
m_child->m_entity->FindComponent<AzFramework::TransformComponent>()->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<AzFramework::TransformComponent>()->GetWorldTM().GetTranslation(),
AZ::Vector3::CreateOne() * 2.f
);
EXPECT_EQ(
m_child->m_entity->FindComponent<AzFramework::TransformComponent>()->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<AzFramework::TransformComponent>()->GetParentId(),
AZ::EntityId(1)
);
}
}
@@ -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
)