diff --git a/Gem/Code/Source/AutoGen/EnergyBallComponent.AutoComponent.xml b/Gem/Code/Source/AutoGen/EnergyBallComponent.AutoComponent.xml index a49a5278d..cec1a6b06 100644 --- a/Gem/Code/Source/AutoGen/EnergyBallComponent.AutoComponent.xml +++ b/Gem/Code/Source/AutoGen/EnergyBallComponent.AutoComponent.xml @@ -17,7 +17,9 @@ - + + + @@ -25,10 +27,6 @@ - - - - diff --git a/Gem/Code/Source/Components/Multiplayer/EnergyBallComponent.cpp b/Gem/Code/Source/Components/Multiplayer/EnergyBallComponent.cpp index 3728329a1..9f9eb24f1 100644 --- a/Gem/Code/Source/Components/Multiplayer/EnergyBallComponent.cpp +++ b/Gem/Code/Source/Components/Multiplayer/EnergyBallComponent.cpp @@ -11,16 +11,19 @@ #include #include #include +#include #include #include #if AZ_TRAIT_CLIENT # include +# include #endif namespace MultiplayerSample { AZ_CVAR(float, sv_EnergyBallImpulseScalar, 500.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "A fudge factor for imparting impulses on rigid bodies due to weapon hits"); + AZ_CVAR(bool, cl_EnergyBallDebugDraw, false, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "When turned on this will draw the current energy ball location"); void EnergyBallComponent::Reflect(AZ::ReflectContext* context) { @@ -37,27 +40,62 @@ namespace MultiplayerSample { m_effect = GetExplosionEffect(); m_effect.Initialize(); + +#if AZ_TRAIT_CLIENT + BallActiveAddEvent(m_ballActiveHandler); +#endif } void EnergyBallComponent::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) { +#if AZ_TRAIT_CLIENT + m_ballActiveHandler.Disconnect(); +#endif } #if AZ_TRAIT_CLIENT - void EnergyBallComponent::HandleRPC_BallLaunched([[maybe_unused]] AzNetworking::IConnection* invokingConnection, [[maybe_unused]] const AZ::Vector3& location) + void EnergyBallComponent::OnBallActiveChanged(bool active) { - PopcornFX::PopcornFXEmitterComponentRequests* emitterRequests = PopcornFX::PopcornFXEmitterComponentRequestBus::FindFirstHandler(GetEntity()->GetId()); - if (emitterRequests != nullptr) + if (active) + { + bool startSuccess = false; + + // Set to true to call "Kill" which is deferred, or false to call "Terminate" which is immediate. + constexpr bool KillOnRestart = true; + + PopcornFX::PopcornFXEmitterComponentRequestBus::EventResult(startSuccess, + GetEntity()->GetId(), &PopcornFX::PopcornFXEmitterComponentRequestBus::Events::Restart, KillOnRestart); + + AZ_Error("EnergyBall", startSuccess, "Restart call for Energy Ball was unsuccessful."); + + if (cl_EnergyBallDebugDraw) + { + m_debugDrawEvent.Enqueue(AZ::TimeMs{ 0 }, true); + } + } + else { - emitterRequests->Start(); + bool killSuccess = false; + + // This would ideally use Kill instead of Terminate, but there is a bug in PopcornFX 2.15.4 that if Kill is + // called on the first tick (which can happen), then the effect will get stuck in a permanent waiting-to-die state, + // and no amount of Restart calls will ever make it show up again. + PopcornFX::PopcornFXEmitterComponentRequestBus::EventResult(killSuccess, + GetEntity()->GetId(), &PopcornFX::PopcornFXEmitterComponentRequestBus::Events::Terminate); + + AZ_Error("EnergyBall", killSuccess, "Kill call for Energy Ball was unsuccessful."); + + m_debugDrawEvent.RemoveFromQueue(); } } void EnergyBallComponent::HandleRPC_BallExplosion([[maybe_unused]] AzNetworking::IConnection* invokingConnection, const HitEvent& hitEvent) { + // Crate an explosion effect wherever the ball was last at. AZ::Transform transform = AZ::Transform::CreateFromQuaternionAndTranslation(AZ::Quaternion::CreateIdentity(), hitEvent.m_target); m_effect.TriggerEffect(transform); + // Notify every entity that was hit that they've received a weapon impact. for (const HitEntity& hitEntity : hitEvent.m_hitEntities) { const AZ::Transform hitTransform = AZ::Transform::CreateLookAt(hitEntity.m_hitPosition, hitEntity.m_hitPosition + hitEntity.m_hitNormal, AZ::Transform::Axis::ZPositive); @@ -65,11 +103,49 @@ namespace MultiplayerSample const AZ::EntityId hitEntityId = handle.Exists() ? handle.GetEntity()->GetId() : AZ::EntityId(); WeaponNotificationBus::Broadcast(&WeaponNotificationBus::Events::OnWeaponImpact, GetEntity()->GetId(), hitTransform, hitEntityId); } + } - PopcornFX::PopcornFXEmitterComponentRequests* emitterRequests = PopcornFX::PopcornFXEmitterComponentRequestBus::FindFirstHandler(GetEntity()->GetId()); - if (emitterRequests != nullptr) + void EnergyBallComponent::DebugDraw() + { + if (cl_EnergyBallDebugDraw) { - emitterRequests->Kill(); + // Each draw only lasts one frame. + constexpr float DrawDuration = 0.0f; + + auto* shapeConfig = GetGatherParams().GetCurrentShapeConfiguration(); + if (shapeConfig->GetShapeType() == Physics::ShapeType::Sphere) + { + const Physics::SphereShapeConfiguration* sphere = static_cast(shapeConfig); + float debugRadius = sphere->m_radius; + + DebugDraw::DebugDrawRequestBus::Broadcast( + &DebugDraw::DebugDrawRequestBus::Events::DrawSphereAtLocation, + GetEntity()->GetTransform()->GetWorldTM().GetTranslation(), + debugRadius, + AZ::Colors::Green, + DrawDuration + ); + } + else if (shapeConfig->GetShapeType() == Physics::ShapeType::Box) + { + const Physics::BoxShapeConfiguration* box = static_cast(shapeConfig); + AZ::Obb obb = AZ::Obb::CreateFromPositionRotationAndHalfLengths( + GetEntity()->GetTransform()->GetWorldTM().GetTranslation(), + GetEntity()->GetTransform()->GetWorldTM().GetRotation(), + box->m_dimensions / 2.0f + ); + + DebugDraw::DebugDrawRequestBus::Broadcast( + &DebugDraw::DebugDrawRequestBus::Events::DrawObb, + obb, + AZ::Colors::Green, + DrawDuration + ); + } + else if (shapeConfig->GetShapeType() == Physics::ShapeType::Capsule) + { + AZ_Error("EnergyBall", false, "Capsule shape type not currently supported with energy ball debug visualization."); + } } } #endif @@ -83,20 +159,29 @@ namespace MultiplayerSample void EnergyBallComponentController::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) { #if AZ_TRAIT_SERVER - m_collisionCheckEvent.Enqueue(AZ::TimeMs{ 10 }, true); + SetBallActive(false); #endif } void EnergyBallComponentController::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) { #if AZ_TRAIT_SERVER - m_collisionCheckEvent.RemoveFromQueue(); + SetBallActive(false); #endif } #if AZ_TRAIT_SERVER - void EnergyBallComponentController::HandleRPC_LaunchBall([[maybe_unused]] AzNetworking::IConnection* invokingConnection, const AZ::Vector3& startingPosition, const AZ::Vector3& direction, const Multiplayer::NetEntityId& owningNetEntityId) + void EnergyBallComponentController::HandleRPC_LaunchBall(AzNetworking::IConnection* invokingConnection, const AZ::Vector3& startingPosition, const AZ::Vector3& direction, const Multiplayer::NetEntityId& owningNetEntityId) { + if (GetBallActive()) + { + return; + } + + m_collisionCheckEvent.Enqueue(AZ::TimeMs{ 10 }, true); + + SetBallActive(true); + m_shooterNetEntityId = owningNetEntityId; m_hitEvent.m_hitEntities.clear(); @@ -106,15 +191,14 @@ namespace MultiplayerSample m_direction = direction; // Move the entity to the start position - GetEntity()->GetTransform()->SetWorldTranslation(startingPosition); + GetNetworkTransformComponentController()->HandleMultiplayerTeleport(invokingConnection, startingPosition); // We want to sweep our transform during intersect tests to avoid the ball tunneling through targets m_lastSweepTransform = GetEntity()->GetTransform()->GetWorldTM(); - Physics::RigidBodyRequestBus::Event(GetEntityId(), &Physics::RigidBodyRequestBus::Events::EnablePhysics); + AzPhysics::SimulatedBodyComponentRequestsBus::Event(GetEntityId(), &AzPhysics::SimulatedBodyComponentRequestsBus::Events::EnablePhysics); Physics::RigidBodyRequestBus::Event(GetEntityId(), &Physics::RigidBodyRequestBus::Events::SetLinearVelocity, direction * GetGatherParams().m_travelSpeed); - RPC_BallLaunched(startingPosition); } void EnergyBallComponentController::HandleRPC_KillBall([[maybe_unused]] AzNetworking::IConnection* invokingConnection) @@ -124,6 +208,11 @@ namespace MultiplayerSample void EnergyBallComponentController::CheckForCollisions() { + if (!GetBallActive()) + { + return; + } + const AZ::Vector3& position = GetEntity()->GetTransform()->GetWorldTM().GetTranslation(); const HitEffect& effect = GetHitEffect(); @@ -174,17 +263,22 @@ namespace MultiplayerSample void EnergyBallComponentController::HideEnergyBall() { + if (!GetBallActive()) + { + return; + } + + SetBallActive(false); + m_collisionCheckEvent.RemoveFromQueue(); + m_hitEvent.m_target = GetEntity()->GetTransform()->GetWorldTM().GetTranslation(); m_hitEvent.m_shooterNetEntityId = m_shooterNetEntityId; m_hitEvent.m_projectileNetEntityId = GetNetEntityId(); - RPC_BallExplosion(m_hitEvent); - Physics::RigidBodyRequestBus::Event(GetEntityId(), &Physics::RigidBodyRequestBus::Events::DisablePhysics); + AzPhysics::SimulatedBodyComponentRequestsBus::Event(GetEntityId(), &AzPhysics::SimulatedBodyComponentRequestsBus::Events::DisablePhysics); Physics::RigidBodyRequestBus::Event(GetEntityId(), &Physics::RigidBodyRequestBus::Events::SetLinearVelocity, AZ::Vector3::CreateZero()); - // move self and increment resetCount to prevent transform interpolation - AZ::TransformBus::Event(GetEntityId(), &AZ::TransformBus::Events::SetWorldTranslation, AZ::Vector3::CreateAxisZ(-1000.f)); - GetNetworkTransformComponentController()->ModifyResetCount()++; + RPC_BallExplosion(m_hitEvent); } #endif } diff --git a/Gem/Code/Source/Components/Multiplayer/EnergyBallComponent.h b/Gem/Code/Source/Components/Multiplayer/EnergyBallComponent.h index 6784fa421..f4777431a 100644 --- a/Gem/Code/Source/Components/Multiplayer/EnergyBallComponent.h +++ b/Gem/Code/Source/Components/Multiplayer/EnergyBallComponent.h @@ -24,11 +24,25 @@ namespace MultiplayerSample void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; #if AZ_TRAIT_CLIENT - void HandleRPC_BallLaunched(AzNetworking::IConnection* invokingConnection, const AZ::Vector3& location) override; void HandleRPC_BallExplosion(AzNetworking::IConnection* invokingConnection, const HitEvent& hitEvent) override; #endif private: +#if AZ_TRAIT_CLIENT + void DebugDraw(); + void OnBallActiveChanged(bool active); + + AZ::ScheduledEvent m_debugDrawEvent{ [this]() + { + DebugDraw(); + }, AZ::Name("EnergyBallDebugDraw") }; + + AZ::Event::Handler m_ballActiveHandler{ [this](bool active) + { + OnBallActiveChanged(active); + } }; +#endif + GameEffect m_effect; }; diff --git a/Gem/Code/Source/Effects/GameEffect.cpp b/Gem/Code/Source/Effects/GameEffect.cpp index bb6bf7a02..43ae33e64 100644 --- a/Gem/Code/Source/Effects/GameEffect.cpp +++ b/Gem/Code/Source/Effects/GameEffect.cpp @@ -48,7 +48,10 @@ namespace MultiplayerSample #if AZ_TRAIT_CLIENT if (m_popcornFx != nullptr) { - m_popcornFx->DestroyEffect(m_emitter); + if (m_popcornFx->IsEffectAlive(m_emitter)) + { + m_popcornFx->DestroyEffect(m_emitter); + } m_emitter = nullptr; } @@ -72,8 +75,11 @@ namespace MultiplayerSample if (m_popcornFx != nullptr) { - const PopcornFX::SpawnParams params = PopcornFX::SpawnParams(true, false, AZ::Transform::CreateIdentity()); - m_emitter = m_popcornFx->SpawnEffectById(m_particleAssetId, params); + if (m_particleAssetId.IsValid()) + { + const PopcornFX::SpawnParams params = PopcornFX::SpawnParams(true, false, AZ::Transform::CreateIdentity()); + m_emitter = m_popcornFx->SpawnEffectById(m_particleAssetId, params); + } } if (m_audioSystem != nullptr) @@ -91,10 +97,18 @@ namespace MultiplayerSample #if AZ_TRAIT_CLIENT if (m_popcornFx != nullptr) { - int32_t attrId = m_popcornFx->EffectGetAttributeId(m_emitter, attributeName); - if (attrId >= 0) + if (m_popcornFx->IsEffectAlive(m_emitter)) + { + int32_t attrId = m_popcornFx->EffectGetAttributeId(m_emitter, attributeName); + if (attrId >= 0) + { + return m_popcornFx->EffectSetAttributeAsFloat(m_emitter, attrId, value); + } + } + else { - return m_popcornFx->EffectSetAttributeAsFloat(m_emitter, attrId, value); + AZ_Assert(false, "Setting attribute on an emitter that isn't active."); + return false; } } #endif @@ -106,10 +120,18 @@ namespace MultiplayerSample #if AZ_TRAIT_CLIENT if (m_popcornFx != nullptr) { - int32_t attrId = m_popcornFx->EffectGetAttributeId(m_emitter, attributeName); - if (attrId >= 0) + if (m_popcornFx->IsEffectAlive(m_emitter)) { - return m_popcornFx->EffectSetAttributeAsFloat2(m_emitter, attrId, value); + int32_t attrId = m_popcornFx->EffectGetAttributeId(m_emitter, attributeName); + if (attrId >= 0) + { + return m_popcornFx->EffectSetAttributeAsFloat2(m_emitter, attrId, value); + } + } + else + { + AZ_Assert(false, "Setting attribute on an emitter that isn't active."); + return false; } } #endif @@ -121,10 +143,18 @@ namespace MultiplayerSample #if AZ_TRAIT_CLIENT if (m_popcornFx != nullptr) { - int32_t attrId = m_popcornFx->EffectGetAttributeId(m_emitter, attributeName); - if (attrId >= 0) + if (m_popcornFx->IsEffectAlive(m_emitter)) + { + int32_t attrId = m_popcornFx->EffectGetAttributeId(m_emitter, attributeName); + if (attrId >= 0) + { + return m_popcornFx->EffectSetAttributeAsFloat3(m_emitter, attrId, value); + } + } + else { - return m_popcornFx->EffectSetAttributeAsFloat3(m_emitter, attrId, value); + AZ_Assert(false, "Setting attribute on an emitter that isn't active."); + return false; } } #endif @@ -136,10 +166,18 @@ namespace MultiplayerSample #if AZ_TRAIT_CLIENT if (m_popcornFx != nullptr) { - int32_t attrId = m_popcornFx->EffectGetAttributeId(m_emitter, attributeName); - if (attrId >= 0) + if (m_popcornFx->IsEffectAlive(m_emitter)) + { + int32_t attrId = m_popcornFx->EffectGetAttributeId(m_emitter, attributeName); + if (attrId >= 0) + { + return m_popcornFx->EffectSetAttributeAsFloat4(m_emitter, attrId, value); + } + } + else { - return m_popcornFx->EffectSetAttributeAsFloat4(m_emitter, attrId, value); + AZ_Assert(false, "Setting attribute on an emitter that isn't active."); + return false; } } #endif @@ -156,8 +194,16 @@ namespace MultiplayerSample { if (PopcornFX::PopcornFXRequests* popcornFx = PopcornFX::PopcornFXRequestBus::FindFirstHandler()) { - popcornFx->EffectSetTransform(m_emitter, transformOffset); - popcornFx->EffectRestart(m_emitter, cl_KillEffectOnRestart); + if (m_popcornFx->IsEffectAlive(m_emitter)) + { + popcornFx->EffectSetTransform(m_emitter, transformOffset); + popcornFx->EffectSetTeleportThisFrame(m_emitter); + popcornFx->EffectRestart(m_emitter, cl_KillEffectOnRestart); + } + else + { + AZ_Assert(false, "Triggering an inactive emitter."); + } } }