diff --git a/Gems/Multiplayer/Code/CMakeLists.txt b/Gems/Multiplayer/Code/CMakeLists.txt index e8b38c8799..49b404fb1c 100644 --- a/Gems/Multiplayer/Code/CMakeLists.txt +++ b/Gems/Multiplayer/Code/CMakeLists.txt @@ -25,6 +25,9 @@ ly_add_target( AZ::AzCore AZ::AzFramework AZ::AzNetworking + PRIVATE + Gem::EMotionFXStaticLib + Gem::PhysX.Static AUTOGEN_RULES *.AutoPackets.xml,AutoPackets_Header.jinja,$path/$fileprefix.AutoPackets.h *.AutoPackets.xml,AutoPackets_Inline.jinja,$path/$fileprefix.AutoPackets.inl diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkCharacterComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkCharacterComponent.h new file mode 100644 index 0000000000..478c925299 --- /dev/null +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkCharacterComponent.h @@ -0,0 +1,82 @@ +/* + * 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 Physics +{ + class Character; +} + +namespace Multiplayer +{ + //! NetworkCharacterComponent + //! Provides multiplayer support for game-play player characters. + class NetworkCharacterComponent + : public NetworkCharacterComponentBase + , private PhysX::CharacterGameplayRequestBus::Handler + { + friend class NetworkCharacterComponentController; + + public: + AZ_MULTIPLAYER_COMPONENT(Multiplayer::NetworkCharacterComponent, s_networkCharacterComponentConcreteUuid, Multiplayer::NetworkCharacterComponentBase) + + static void Reflect(AZ::ReflectContext* context); + + NetworkCharacterComponent(); + + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + incompatible.push_back(AZ_CRC_CE("NetworkRigidBodyService")); + } + + // AZ::Component + void OnInit() override {} + void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + + private: + void OnTranslationChangedEvent(const AZ::Vector3& translation); + void OnSyncRewind(); + + // CharacterGameplayRequestBus + bool IsOnGround() const override; + float GetGravityMultiplier() const override { return {}; } + void SetGravityMultiplier([[maybe_unused]] float gravityMultiplier) override {} + AZ::Vector3 GetFallingVelocity() const override { return {}; } + void SetFallingVelocity([[maybe_unused]] const AZ::Vector3& fallingVelocity) override {} + + Physics::Character* m_physicsCharacter = nullptr; + Multiplayer::EntitySyncRewindEvent::Handler m_syncRewindHandler = Multiplayer::EntitySyncRewindEvent::Handler([this]() { OnSyncRewind(); }); + AZ::Event::Handler m_translationEventHandler; + }; + + //! NetworkCharacterComponentController + //! This is the network controller for NetworkCharacterComponent. + //! Class provides the ability to move characters in physical space while keeping the network in-sync. + class NetworkCharacterComponentController + : public NetworkCharacterComponentControllerBase + { + public: + NetworkCharacterComponentController(NetworkCharacterComponent& parent); + + // NetworkCharacterComponentControllerBase + void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + + //! TryMoveWithVelocity + //! Will move this character entity kinematically through physical world while also ensuring the network stays in-sync. + //! Velocity will be applied over delta-time to determine the movement amount. + //! Returns this entity's world-space position after the move. + AZ::Vector3 TryMoveWithVelocity(const AZ::Vector3& velocity, float deltaTime); + }; +} diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHitVolumesComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHitVolumesComponent.h new file mode 100644 index 0000000000..1b80ba85c0 --- /dev/null +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHitVolumesComponent.h @@ -0,0 +1,90 @@ +/* + * 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 + +namespace Physics +{ + class CharacterRequests; + class CharacterHitDetectionConfiguration; +} + +namespace Multiplayer +{ + class NetworkHitVolumesComponent + : public NetworkHitVolumesComponentBase + , private EMotionFX::Integration::ActorComponentNotificationBus::Handler + { + public: + struct AnimatedHitVolume final + { + AnimatedHitVolume + ( + AzNetworking::ConnectionId connectionId, + Physics::CharacterRequests* character, + const char* hitVolumeName, + const Physics::ColliderConfiguration* colliderConfig, + const Physics::ShapeConfiguration* shapeConfig, + const uint32_t jointIndex + ); + + ~AnimatedHitVolume() = default; + + void UpdateTransform(const AZ::Transform& transform); + void SyncToCurrentTransform(); + + Multiplayer::RewindableObject m_transform; + AZStd::shared_ptr m_physicsShape; + + // Cached so we don't have to do subsequent lookups by name + const Physics::ColliderConfiguration* m_colliderConfig = nullptr; + const Physics::ShapeConfiguration* m_shapeConfig = nullptr; + AZ::Transform m_colliderOffSetTransform; + const AZ::u32 m_jointIndex = 0; + }; + + AZ_MULTIPLAYER_COMPONENT(Multiplayer::NetworkHitVolumesComponent, s_networkHitVolumesComponentConcreteUuid, Multiplayer::NetworkHitVolumesComponentBase); + + static void Reflect(AZ::ReflectContext* context); + + NetworkHitVolumesComponent(); + + void OnInit() override; + void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + + private: + void OnPreRender(float deltaTime, float blendFactor); + void OnTransformUpdate(const AZ::Transform& transform); + void OnSyncRewind(); + + void CreateHitVolumes(); + void DestroyHitVolumes(); + + //! ActorComponentNotificationBus::Handler + //! @{ + void OnActorInstanceCreated(EMotionFX::ActorInstance* actorInstance) override; + void OnActorInstanceDestroyed(EMotionFX::ActorInstance* actorInstance) override; + //! @} + + Physics::CharacterRequests* m_physicsCharacter = nullptr; + EMotionFX::Integration::ActorComponentRequests* m_actorComponent = nullptr; + const Physics::CharacterColliderConfiguration* m_hitDetectionConfig = nullptr; + + AZStd::vector m_animatedHitVolumes; + + Multiplayer::EntitySyncRewindEvent::Handler m_syncRewindHandler; + Multiplayer::EntityPreRenderEvent::Handler m_preRenderHandler; + AZ::TransformChangedEvent::Handler m_transformChangedHandler; + }; +} diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkRigidBodyComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkRigidBodyComponent.h new file mode 100644 index 0000000000..19379fc959 --- /dev/null +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkRigidBodyComponent.h @@ -0,0 +1,68 @@ +/* + * 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 Physics +{ + class RigidBodyRequests; +} + +namespace Multiplayer +{ + //! Bus for requests to the network rigid body component. + class NetworkRigidBodyRequests : public AZ::ComponentBus + { + }; + using NetworkRigidBodyRequestBus = AZ::EBus; + + class NetworkRigidBodyComponent final + : public NetworkRigidBodyComponentBase + , private NetworkRigidBodyRequestBus::Handler + { + friend class NetworkRigidBodyComponentController; + + public: + AZ_MULTIPLAYER_COMPONENT( + Multiplayer::NetworkRigidBodyComponent, s_networkRigidBodyComponentConcreteUuid, Multiplayer::NetworkRigidBodyComponentBase); + + static void Reflect(AZ::ReflectContext* context); + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); + + NetworkRigidBodyComponent(); + + void OnInit() override; + void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + + private: + void OnTransformUpdate(const AZ::Transform& worldTm); + void OnSyncRewind(); + + Multiplayer::EntitySyncRewindEvent::Handler m_syncRewindHandler; + AZ::TransformChangedEvent::Handler m_transformChangedHandler; + Physics::RigidBodyRequests* m_physicsRigidBodyComponent = nullptr; + Multiplayer::RewindableObject m_transform; + }; + + class NetworkRigidBodyComponentController + : public NetworkRigidBodyComponentControllerBase + { + public: + NetworkRigidBodyComponentController(NetworkRigidBodyComponent& parent); + + void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + + void HandleSendApplyImpulse(AzNetworking::IConnection* invokingConnection, const AZ::Vector3& impulse, const AZ::Vector3& worldPoint) override; + }; +} // namespace Multiplayer diff --git a/Gems/Multiplayer/Code/Source/AutoGen/AutoComponentTypes_Source.jinja b/Gems/Multiplayer/Code/Source/AutoGen/AutoComponentTypes_Source.jinja index 7bb4bdcc2c..c551c8bc20 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/AutoComponentTypes_Source.jinja +++ b/Gems/Multiplayer/Code/Source/AutoGen/AutoComponentTypes_Source.jinja @@ -14,7 +14,7 @@ {% set Namespace = dataFiles[0].attrib['Namespace'] %} {% for Component in dataFiles %} {% if Component.attrib['Namespace'] != Namespace %} -#error "mismatched component namespaces detected in declared multiplayer components, expected {{ Namespace }} but found {{ Component.attrib['Namespace'] }}" +#error "mismatched component namespaces detected in declared multiplayer components, expected {{ Namespace }} but {{ Component.attrib['Name'] }} is using {{ Component.attrib['Namespace'] }} namespace." {% endif %} {% endfor %} namespace {{ Namespace }} diff --git a/Gems/Multiplayer/Code/Source/AutoGen/NetworkCharacterComponent.AutoComponent.xml b/Gems/Multiplayer/Code/Source/AutoGen/NetworkCharacterComponent.AutoComponent.xml new file mode 100644 index 0000000000..83e15800e0 --- /dev/null +++ b/Gems/Multiplayer/Code/Source/AutoGen/NetworkCharacterComponent.AutoComponent.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/Gems/Multiplayer/Code/Source/AutoGen/NetworkHitVolumesComponent.AutoComponent.xml b/Gems/Multiplayer/Code/Source/AutoGen/NetworkHitVolumesComponent.AutoComponent.xml new file mode 100644 index 0000000000..d3c31a5bc0 --- /dev/null +++ b/Gems/Multiplayer/Code/Source/AutoGen/NetworkHitVolumesComponent.AutoComponent.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/Gems/Multiplayer/Code/Source/AutoGen/NetworkRigidBodyComponent.AutoComponent.xml b/Gems/Multiplayer/Code/Source/AutoGen/NetworkRigidBodyComponent.AutoComponent.xml new file mode 100644 index 0000000000..b6cdfca9c2 --- /dev/null +++ b/Gems/Multiplayer/Code/Source/AutoGen/NetworkRigidBodyComponent.AutoComponent.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/Gems/Multiplayer/Code/Source/Components/NetworkCharacterComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetworkCharacterComponent.cpp new file mode 100644 index 0000000000..b14fc8761f --- /dev/null +++ b/Gems/Multiplayer/Code/Source/Components/NetworkCharacterComponent.cpp @@ -0,0 +1,209 @@ +/* + * 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 + +namespace Multiplayer +{ + + bool CollisionLayerBasedControllerFilter(const physx::PxController& controllerA, const physx::PxController& controllerB) + { + PHYSX_SCENE_READ_LOCK(controllerA.getActor()->getScene()); + physx::PxRigidDynamic* actorA = controllerA.getActor(); + physx::PxRigidDynamic* actorB = controllerB.getActor(); + + if (actorA && actorA->getNbShapes() > 0 && actorB && actorB->getNbShapes() > 0) + { + physx::PxShape* shapeA = nullptr; + actorA->getShapes(&shapeA, 1, 0); + physx::PxFilterData filterDataA = shapeA->getSimulationFilterData(); + physx::PxShape* shapeB = nullptr; + actorB->getShapes(&shapeB, 1, 0); + physx::PxFilterData filterDataB = shapeB->getSimulationFilterData(); + return PhysX::Utils::Collision::ShouldCollide(filterDataA, filterDataB); + } + + return true; + } + + physx::PxQueryHitType::Enum CollisionLayerBasedObjectPreFilter( + const physx::PxFilterData& filterData, + const physx::PxShape* shape, + const physx::PxRigidActor* actor, + [[maybe_unused]] physx::PxHitFlags& queryFlags) + { + // non-kinematic dynamic bodies should not impede the movement of the character + if (actor->getConcreteType() == physx::PxConcreteType::eRIGID_DYNAMIC) + { + const physx::PxRigidDynamic* rigidDynamic = static_cast(actor); + + bool isKinematic = (rigidDynamic->getRigidBodyFlags() & physx::PxRigidBodyFlag::eKINEMATIC); + if (isKinematic) + { + const PhysX::ActorData* actorData = PhysX::Utils::GetUserData(rigidDynamic); + if (actorData) + { + const AZ::EntityId entityId = actorData->GetEntityId(); + + if (Multiplayer::NetworkRigidBodyRequestBus::FindFirstHandler(entityId) != nullptr) + { + // Network rigid bodies are kinematic on the client but dynamic on the server, + // hence filtering treats these actors as dynamic to support client prediction and avoid desyncs + isKinematic = false; + } + } + } + + if (!isKinematic) + { + return physx::PxQueryHitType::eNONE; + } + } + + // all other cases should be determined by collision filters + if (PhysX::Utils::Collision::ShouldCollide(filterData, shape->getSimulationFilterData())) + { + return physx::PxQueryHitType::eBLOCK; + } + + return physx::PxQueryHitType::eNONE; + } + + void NetworkCharacterComponent::NetworkCharacterComponent::Reflect(AZ::ReflectContext* context) + { + AZ::SerializeContext* serializeContext = azrtti_cast(context); + if (serializeContext) + { + serializeContext->Class() + ->Version(1); + } + NetworkCharacterComponentBase::Reflect(context); + } + + NetworkCharacterComponent::NetworkCharacterComponent() + : m_translationEventHandler([this](const AZ::Vector3& translation) { OnTranslationChangedEvent(translation); }) + { + } + + void NetworkCharacterComponent::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + Physics::CharacterRequests* characterRequests = Physics::CharacterRequestBus::FindFirstHandler(GetEntityId()); + m_physicsCharacter = (characterRequests != nullptr) ? characterRequests->GetCharacter() : nullptr; + GetNetBindComponent()->AddEntitySyncRewindEventHandler(m_syncRewindHandler); + + if (m_physicsCharacter) + { + auto controller = static_cast(m_physicsCharacter); + controller->SetFilterFlags(physx::PxQueryFlag::eSTATIC | physx::PxQueryFlag::eDYNAMIC | physx::PxQueryFlag::ePREFILTER); + if (auto callbackManager = controller->GetCallbackManager()) + { + callbackManager->SetControllerFilter(CollisionLayerBasedControllerFilter); + callbackManager->SetObjectPreFilter(CollisionLayerBasedObjectPreFilter); + } + } + + if (!HasController()) + { + GetNetworkTransformComponent()->TranslationAddEvent(m_translationEventHandler); + } + } + + void NetworkCharacterComponent::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + ; + } + + void NetworkCharacterComponent::OnTranslationChangedEvent([[maybe_unused]] const AZ::Vector3& translation) + { + OnSyncRewind(); + } + + void NetworkCharacterComponent::OnSyncRewind() + { + if (m_physicsCharacter == nullptr) + { + return; + } + + const AZ::Vector3 currPosition = m_physicsCharacter->GetBasePosition(); + if (!currPosition.IsClose(GetNetworkTransformComponent()->GetTranslation())) + { + uint32_t frameId = static_cast(Multiplayer::GetNetworkTime()->GetHostFrameId()); + m_physicsCharacter->SetFrameId(frameId); + //m_physicsCharacter->SetBasePosition(GetNetworkTransformComponent()->GetTranslation()); + } + } + + bool NetworkCharacterComponent::IsOnGround() const + { + auto pxController = static_cast(m_physicsCharacter->GetNativePointer()); + if (!pxController) + { + return true; + } + + physx::PxControllerState state; + pxController->getState(state); + return state.touchedActor != nullptr || (state.collisionFlags & physx::PxControllerCollisionFlag::eCOLLISION_DOWN) != 0; + } + + NetworkCharacterComponentController::NetworkCharacterComponentController(NetworkCharacterComponent& parent) + : NetworkCharacterComponentControllerBase(parent) + { + ; + } + + void NetworkCharacterComponentController::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + ; + } + + void NetworkCharacterComponentController::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + ; + } + + AZ::Vector3 NetworkCharacterComponentController::TryMoveWithVelocity(const AZ::Vector3& velocity, [[maybe_unused]] float deltaTime) + { + // Ensure any entities that we might interact with are properly synchronized to their rewind state + if (IsAuthority()) + { + const AZ::Aabb entityStartBounds = AZ::Interface::Get()->GetEntityLocalBoundsUnion(GetEntity()->GetId()); + const AZ::Aabb entityFinalBounds = entityStartBounds.GetTranslated(velocity); + AZ::Aabb entitySweptBounds = entityStartBounds; + entitySweptBounds.AddAabb(entityFinalBounds); + Multiplayer::GetNetworkTime()->SyncEntitiesToRewindState(entitySweptBounds); + } + + if ((GetParent().m_physicsCharacter == nullptr) || (velocity.GetLengthSq() <= 0.0f)) + { + return GetEntity()->GetTransform()->GetWorldTranslation(); + } + GetParent().m_physicsCharacter->AddVelocity(velocity); + GetParent().m_physicsCharacter->ApplyRequestedVelocity(deltaTime); + GetEntity()->GetTransform()->SetWorldTranslation(GetParent().m_physicsCharacter->GetBasePosition()); + AZLOG + ( + NET_Movement, + "Moved to position %f x %f x %f", + GetParent().m_physicsCharacter->GetBasePosition().GetX(), + GetParent().m_physicsCharacter->GetBasePosition().GetY(), + GetParent().m_physicsCharacter->GetBasePosition().GetZ() + ); + return GetEntity()->GetTransform()->GetWorldTranslation(); + } +} diff --git a/Gems/Multiplayer/Code/Source/Components/NetworkHitVolumesComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetworkHitVolumesComponent.cpp new file mode 100644 index 0000000000..0dd5fa8a01 --- /dev/null +++ b/Gems/Multiplayer/Code/Source/Components/NetworkHitVolumesComponent.cpp @@ -0,0 +1,221 @@ +/* + * 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 Multiplayer +{ + AZ_CVAR(bool, bg_DrawArticulatedHitVolumes, false, nullptr, AZ::ConsoleFunctorFlags::Null, "Enables debug draw of articulated hit volumes"); + AZ_CVAR(float, bg_DrawDebugHitVolumeLifetime, 0.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "The lifetime for hit volume draw-debug shapes"); + + AZ_CVAR(float, bg_RewindPositionTolerance, 0.0001f, nullptr, AZ::ConsoleFunctorFlags::Null, "Don't sync the physx entity if the square of delta position is less than this value"); + AZ_CVAR(float, bg_RewindOrientationTolerance, 0.001f, nullptr, AZ::ConsoleFunctorFlags::Null, "Don't sync the physx entity if the square of delta orientation is less than this value"); + + NetworkHitVolumesComponent::AnimatedHitVolume::AnimatedHitVolume + ( + AzNetworking::ConnectionId connectionId, + Physics::CharacterRequests* character, + const char* hitVolumeName, + const Physics::ColliderConfiguration* colliderConfig, + const Physics::ShapeConfiguration* shapeConfig, + const uint32_t jointIndex + ) + : m_colliderConfig(colliderConfig) + , m_shapeConfig(shapeConfig) + , m_jointIndex(jointIndex) + { + m_transform.SetOwningConnectionId(connectionId); + + m_colliderOffSetTransform = AZ::Transform::CreateFromQuaternionAndTranslation(m_colliderConfig->m_rotation, m_colliderConfig->m_position); + + if (m_colliderConfig->m_isExclusive) + { + Physics::SystemRequestBus::BroadcastResult(m_physicsShape, &Physics::SystemRequests::CreateShape, *m_colliderConfig, *m_shapeConfig); + } + else + { + Physics::ColliderConfiguration colliderConfiguration = *m_colliderConfig; + colliderConfiguration.m_isExclusive = true; + colliderConfiguration.m_isSimulated = false; + colliderConfiguration.m_isInSceneQueries = true; + Physics::SystemRequestBus::BroadcastResult(m_physicsShape, &Physics::SystemRequests::CreateShape, colliderConfiguration, *m_shapeConfig); + } + + if (m_physicsShape) + { + m_physicsShape->SetName(hitVolumeName); + character->GetCharacter()->AttachShape(m_physicsShape); + } + } + + void NetworkHitVolumesComponent::AnimatedHitVolume::UpdateTransform(const AZ::Transform& transform) + { + m_transform = transform; + m_physicsShape->SetLocalPose(transform.GetTranslation(), transform.GetRotation()); + } + + void NetworkHitVolumesComponent::AnimatedHitVolume::SyncToCurrentTransform() + { + AZ::Transform rewoundTransform; + const AZ::Transform& targetTransform = m_transform.Get(); + const float blendFactor = Multiplayer::GetNetworkTime()->GetHostBlendFactor(); + if (blendFactor < 1.f) + { + // If a blend factor was supplied, interpolate the transform appropriately + const AZ::Transform& previousTransform = m_transform.GetPrevious(); + rewoundTransform.SetRotation(previousTransform.GetRotation().Slerp(targetTransform.GetRotation(), blendFactor)); + rewoundTransform.SetTranslation(previousTransform.GetTranslation().Lerp(targetTransform.GetTranslation(), blendFactor)); + rewoundTransform.SetUniformScale(AZ::Lerp(previousTransform.GetUniformScale(), targetTransform.GetUniformScale(), blendFactor)); + } + else + { + rewoundTransform = m_transform.Get(); + } + + const AZ::Transform physicsTransform = AZ::Transform::CreateFromQuaternionAndTranslation(m_physicsShape->GetLocalPose().second, m_physicsShape->GetLocalPose().first); + + // Don't call SetLocalPose unless the transforms are actually different + const AZ::Vector3 positionDelta = physicsTransform.GetTranslation() - rewoundTransform.GetTranslation(); + const AZ::Quaternion orientationDelta = physicsTransform.GetRotation() - rewoundTransform.GetRotation(); + + if ((positionDelta.GetLengthSq() >= bg_RewindPositionTolerance) || (orientationDelta.GetLengthSq() >= bg_RewindOrientationTolerance)) + { + m_physicsShape->SetLocalPose(rewoundTransform.GetTranslation(), rewoundTransform.GetRotation()); + } + } + + void NetworkHitVolumesComponent::NetworkHitVolumesComponent::Reflect(AZ::ReflectContext* context) + { + AZ::SerializeContext* serializeContext = azrtti_cast(context); + if (serializeContext) + { + serializeContext->Class() + ->Version(1); + } + NetworkHitVolumesComponentBase::Reflect(context); + } + + NetworkHitVolumesComponent::NetworkHitVolumesComponent() + : m_syncRewindHandler([this]() { OnSyncRewind(); }) + , m_preRenderHandler([this](float deltaTime, float blendFactor) { OnPreRender(deltaTime, blendFactor); }) + , m_transformChangedHandler([this](const AZ::Transform&, const AZ::Transform& worldTm) { OnTransformUpdate(worldTm); }) + { + ; + } + + void NetworkHitVolumesComponent::OnInit() + { + ; + } + + void NetworkHitVolumesComponent::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + EMotionFX::Integration::ActorComponentNotificationBus::Handler::BusConnect(GetEntityId()); + GetNetBindComponent()->AddEntitySyncRewindEventHandler(m_syncRewindHandler); + m_physicsCharacter = Physics::CharacterRequestBus::FindFirstHandler(GetEntityId()); + GetTransformComponent()->BindTransformChangedEventHandler(m_transformChangedHandler); + OnTransformUpdate(GetTransformComponent()->GetWorldTM()); + } + + void NetworkHitVolumesComponent::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + DestroyHitVolumes(); + EMotionFX::Integration::ActorComponentNotificationBus::Handler::BusDisconnect(); + } + + void NetworkHitVolumesComponent::OnPreRender([[maybe_unused]] float deltaTime, [[maybe_unused]] float blendFactor) + { + if (m_animatedHitVolumes.size() <= 0) + { + CreateHitVolumes(); + } + + AZ::Vector3 position, scale; + AZ::Quaternion rotation; + for (AnimatedHitVolume& hitVolume : m_animatedHitVolumes) + { + m_actorComponent->GetJointTransformComponents(hitVolume.m_jointIndex, EMotionFX::Integration::Space::ModelSpace, position, rotation, scale); + hitVolume.UpdateTransform(AZ::Transform::CreateFromQuaternionAndTranslation(rotation, position) * hitVolume.m_colliderOffSetTransform); + } + } + + void NetworkHitVolumesComponent::OnTransformUpdate([[maybe_unused]] const AZ::Transform& transform) + { + OnSyncRewind(); + } + + void NetworkHitVolumesComponent::OnSyncRewind() + { + if (m_physicsCharacter && m_physicsCharacter->GetCharacter()) + { + uint32_t frameId = static_cast(Multiplayer::GetNetworkTime()->GetHostFrameId()); + m_physicsCharacter->GetCharacter()->SetFrameId(frameId); + } + + for (AnimatedHitVolume& hitVolume : m_animatedHitVolumes) + { + hitVolume.SyncToCurrentTransform(); + } + } + + void NetworkHitVolumesComponent::CreateHitVolumes() + { + if (m_physicsCharacter == nullptr || m_actorComponent == nullptr) + { + return; + } + + const Physics::AnimationConfiguration* physicsConfig = m_actorComponent->GetPhysicsConfig(); + if (physicsConfig == nullptr) + { + return; + } + + m_hitDetectionConfig = &physicsConfig->m_hitDetectionConfig; + const AzNetworking::ConnectionId owningConnectionId = GetNetBindComponent()->GetOwningConnectionId(); + + m_animatedHitVolumes.reserve(m_hitDetectionConfig->m_nodes.size()); + for (const Physics::CharacterColliderNodeConfiguration& nodeConfig : m_hitDetectionConfig->m_nodes) + { + const AZStd::size_t jointIndex = m_actorComponent->GetJointIndexByName(nodeConfig.m_name.c_str()); + if (jointIndex == EMotionFX::Integration::ActorComponentRequests::s_invalidJointIndex) + { + continue; + } + + for (const AzPhysics::ShapeColliderPair& coliderPair : nodeConfig.m_shapes) + { + const Physics::ColliderConfiguration* colliderConfig = coliderPair.first.get(); + Physics::ShapeConfiguration* shapeConfig = coliderPair.second.get(); + m_animatedHitVolumes.emplace_back(owningConnectionId, m_physicsCharacter, nodeConfig.m_name.c_str(), colliderConfig, shapeConfig, aznumeric_cast(jointIndex)); + } + } + } + + void NetworkHitVolumesComponent::DestroyHitVolumes() + { + m_animatedHitVolumes.clear(); + } + + void NetworkHitVolumesComponent::OnActorInstanceCreated([[maybe_unused]] EMotionFX::ActorInstance* actorInstance) + { + m_actorComponent = EMotionFX::Integration::ActorComponentRequestBus::FindFirstHandler(GetEntity()->GetId()); + } + + void NetworkHitVolumesComponent::OnActorInstanceDestroyed([[maybe_unused]] EMotionFX::ActorInstance* actorInstance) + { + m_actorComponent = nullptr; + } +} diff --git a/Gems/Multiplayer/Code/Source/Components/NetworkRigidBodyComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetworkRigidBodyComponent.cpp new file mode 100644 index 0000000000..bcf855d834 --- /dev/null +++ b/Gems/Multiplayer/Code/Source/Components/NetworkRigidBodyComponent.cpp @@ -0,0 +1,147 @@ +/* + * 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 + +namespace Multiplayer +{ + AZ_CVAR_EXTERNED(float, bg_RewindPositionTolerance); + AZ_CVAR_EXTERNED(float, bg_RewindOrientationTolerance); + + void NetworkRigidBodyComponent::NetworkRigidBodyComponent::Reflect(AZ::ReflectContext* context) + { + AZ::SerializeContext* serializeContext = azrtti_cast(context); + if (serializeContext) + { + serializeContext->Class()->Version(1); + } + NetworkRigidBodyComponentBase::Reflect(context); + } + + void NetworkRigidBodyComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(AZ_CRC_CE("NetworkRigidBodyService")); + } + + void NetworkRigidBodyComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required) + { + required.push_back(AZ_CRC_CE("PhysXRigidBodyService")); + } + + void NetworkRigidBodyComponent::GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent) + { + dependent.push_back(AZ_CRC_CE("TransformService")); + dependent.push_back(AZ_CRC_CE("PhysXRigidBodyService")); + } + + NetworkRigidBodyComponent::NetworkRigidBodyComponent() + : m_syncRewindHandler([this](){ OnSyncRewind(); }) + , m_transformChangedHandler([this]([[maybe_unused]] const AZ::Transform& localTm, const AZ::Transform& worldTm){ OnTransformUpdate(worldTm); }) + { + } + + void NetworkRigidBodyComponent::OnInit() + { + } + + void NetworkRigidBodyComponent::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + NetworkRigidBodyRequestBus::Handler::BusConnect(GetEntityId()); + + GetNetBindComponent()->AddEntitySyncRewindEventHandler(m_syncRewindHandler); + GetEntity()->FindComponent()->BindTransformChangedEventHandler(m_transformChangedHandler); + + m_physicsRigidBodyComponent = + Physics::RigidBodyRequestBus::FindFirstHandler(GetEntity()->GetId()); + AZ_Assert(m_physicsRigidBodyComponent, "PhysX Rigid Body Component is required on entity %s", GetEntity()->GetName().c_str()); + + if (!HasController()) + { + m_physicsRigidBodyComponent->SetKinematic(true); + } + } + + void NetworkRigidBodyComponent::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + NetworkRigidBodyRequestBus::Handler::BusDisconnect(); + } + + void NetworkRigidBodyComponent::OnTransformUpdate(const AZ::Transform& worldTm) + { + m_transform = worldTm; + + if (!HasController()) + { + m_physicsRigidBodyComponent->SetKinematicTarget(worldTm); + } + } + + void NetworkRigidBodyComponent::OnSyncRewind() + { + uint32_t frameId = static_cast(Multiplayer::GetNetworkTime()->GetHostFrameId()); + + AzPhysics::RigidBody* rigidBody = m_physicsRigidBodyComponent->GetRigidBody(); + rigidBody->SetFrameId(frameId); + + AZ::Transform rewoundTransform; + const AZ::Transform& targetTransform = m_transform.Get(); + const float blendFactor = Multiplayer::GetNetworkTime()->GetHostBlendFactor(); + if (blendFactor < 1.f) + { + // If a blend factor was supplied, interpolate the transform appropriately + const AZ::Transform& previousTransform = m_transform.GetPrevious(); + rewoundTransform.SetRotation(previousTransform.GetRotation().Slerp(targetTransform.GetRotation(), blendFactor)); + rewoundTransform.SetTranslation(previousTransform.GetTranslation().Lerp(targetTransform.GetTranslation(), blendFactor)); + rewoundTransform.SetUniformScale(AZ::Lerp(previousTransform.GetUniformScale(), targetTransform.GetUniformScale(), blendFactor)); + } + else + { + rewoundTransform = m_transform.Get(); + } + const AZ::Transform& physicsTransform = rigidBody->GetTransform(); + + // Don't call SetLocalPose unless the transforms are actually different + const AZ::Vector3 positionDelta = physicsTransform.GetTranslation() - rewoundTransform.GetTranslation(); + const AZ::Quaternion orientationDelta = physicsTransform.GetRotation() - rewoundTransform.GetRotation(); + + if ((positionDelta.GetLengthSq() >= bg_RewindPositionTolerance) || + (orientationDelta.GetLengthSq() >= bg_RewindOrientationTolerance)) + { + rigidBody->SetTransform(rewoundTransform); + } + } + + NetworkRigidBodyComponentController::NetworkRigidBodyComponentController(NetworkRigidBodyComponent& parent) + : NetworkRigidBodyComponentControllerBase(parent) + { + ; + } + + void NetworkRigidBodyComponentController::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + ; + } + + void NetworkRigidBodyComponentController::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + ; + } + + void NetworkRigidBodyComponentController::HandleSendApplyImpulse + ( + [[maybe_unused]] AzNetworking::IConnection* invokingConnection, + const AZ::Vector3& impulse, + const AZ::Vector3& worldPoint + ) + { + AzPhysics::RigidBody* rigidBody = GetParent().m_physicsRigidBodyComponent->GetRigidBody(); + rigidBody->ApplyLinearImpulseAtWorldPoint(impulse, worldPoint); + } +} // namespace Multiplayer diff --git a/Gems/Multiplayer/Code/multiplayer_files.cmake b/Gems/Multiplayer/Code/multiplayer_files.cmake index c03773d5b3..c1d27402f5 100644 --- a/Gems/Multiplayer/Code/multiplayer_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_files.cmake @@ -18,6 +18,9 @@ set(FILES Include/Multiplayer/Components/MultiplayerController.h Include/Multiplayer/Components/MultiplayerComponentRegistry.h Include/Multiplayer/Components/NetBindComponent.h + Include/Multiplayer/Components/NetworkCharacterComponent.h + Include/Multiplayer/Components/NetworkHitVolumesComponent.h + Include/Multiplayer/Components/NetworkRigidBodyComponent.h Include/Multiplayer/Components/NetworkTransformComponent.h Include/Multiplayer/ConnectionData/IConnectionData.h Include/Multiplayer/EntityDomains/IEntityDomain.h @@ -54,12 +57,18 @@ set(FILES Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml Source/AutoGen/Multiplayer.AutoPackets.xml Source/AutoGen/MultiplayerEditor.AutoPackets.xml + Source/AutoGen/NetworkCharacterComponent.AutoComponent.xml + Source/AutoGen/NetworkHitVolumesComponent.AutoComponent.xml + Source/AutoGen/NetworkRigidBodyComponent.AutoComponent.xml Source/AutoGen/NetworkTransformComponent.AutoComponent.xml Source/Components/LocalPredictionPlayerInputComponent.cpp Source/Components/MultiplayerComponent.cpp Source/Components/MultiplayerController.cpp Source/Components/MultiplayerComponentRegistry.cpp Source/Components/NetBindComponent.cpp + Source/Components/NetworkCharacterComponent.cpp + Source/Components/NetworkHitVolumesComponent.cpp + Source/Components/NetworkRigidBodyComponent.cpp Source/Components/NetworkTransformComponent.cpp Source/ConnectionData/ClientToServerConnectionData.cpp Source/ConnectionData/ClientToServerConnectionData.h