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.");
+ }
}
}