Browse Source

Energy Ball Fixes (#363)

* Added guards to downgrade a sporadic Editor crash to an assert.
When transitioning in and out of game mode in the Editor, the order of operations can cause PopcornFX to delete the m_emitter pointer before GameEffect is done using it. The IsEffectAlive() checks detect when this case occurs and prevents the pointer from getting dereferenced and crashing. The asserts exist because the root cause should still get fixed as well so that there aren't lifetime management issues.

Signed-off-by: Mike Balfour <[email protected]>

* Fixes the disappearing energy balls.
This actually contains a number of fixes:
- The disappearing energy balls were caused if Kill() got called on a particle effect in the first frame, which could happen depending on RPC timing. This replaces Kill() with Terminate() as a workaround to prevent that from happening.
- The BallExplosion / LaunchBall events weren't guaranteed to arrive in order, so the logic wasn't always necessarily managing the particle state correctly. By switching to a "BallActive" property and removing "LaunchBall", the order of operations problem goes away.
- The physics call to disable/enable physics should use the SimulatedBody bus, not the RigidBody bus.
- There were multiple redundant calls to BallExplosion and the collision checking that could happen during a single launched ball due to bad state management.
- Added Karl's optional debug draws to make it easier to visualize network problems vs particle problems.

Signed-off-by: Mike Balfour <[email protected]>

* Switched from tick bus to event handlers

Signed-off-by: Mike Balfour <[email protected]>

---------

Signed-off-by: Mike Balfour <[email protected]>
Mike Balfour 2 years ago
parent
commit
263d4afb95

+ 3 - 5
Gem/Code/Source/AutoGen/EnergyBallComponent.AutoComponent.xml

@@ -17,7 +17,9 @@
     <ArchetypeProperty Type="GatherParams" Name="GatherParams" Init="" ExposeToEditor="true" Description="Specifies the types of intersections to test for on the projectile" />
     <ArchetypeProperty Type="GatherParams" Name="GatherParams" Init="" ExposeToEditor="true" Description="Specifies the types of intersections to test for on the projectile" />
     <ArchetypeProperty Type="HitEffect" Name="HitEffect" Init="" ExposeToEditor="true" Description="Specifies the damage effects to apply on hit" />
     <ArchetypeProperty Type="HitEffect" Name="HitEffect" Init="" ExposeToEditor="true" Description="Specifies the damage effects to apply on hit" />
 
 
-    <RemoteProcedure Name="RPC_LaunchBall" InvokeFrom="Server" HandleOn="Authority" IsPublic="true" IsReliable="true" GenerateEventBindings="true" Description="Launching an energy from a specified position in a specified direction.">
+    <NetworkProperty Type="bool" Name="BallActive" Init="false" ReplicateFrom="Authority" ReplicateTo="Client" Container="Object" IsPublic="true" IsRewindable="true" IsPredictable="false" ExposeToScript="false" ExposeToEditor="false" GenerateEventBindings="true" Description="Track whether or not the energy ball is currently active" />
+
+  <RemoteProcedure Name="RPC_LaunchBall" InvokeFrom="Server" HandleOn="Authority" IsPublic="true" IsReliable="true" GenerateEventBindings="true" Description="Launch an energy ball from a specified position in a specified direction.">
         <Param Type="AZ::Vector3" Name="StartingPosition"/>
         <Param Type="AZ::Vector3" Name="StartingPosition"/>
         <Param Type="AZ::Vector3" Name="Direction"/>
         <Param Type="AZ::Vector3" Name="Direction"/>
         <Param Type="Multiplayer::NetEntityId" Name="OwningNetEntityId" />
         <Param Type="Multiplayer::NetEntityId" Name="OwningNetEntityId" />
@@ -25,10 +27,6 @@
 
 
     <RemoteProcedure Name="RPC_KillBall" InvokeFrom="Server" HandleOn="Authority" IsPublic="true" IsReliable="true" GenerateEventBindings="true" Description="Kills a launched energy ball." />
     <RemoteProcedure Name="RPC_KillBall" InvokeFrom="Server" HandleOn="Authority" IsPublic="true" IsReliable="true" GenerateEventBindings="true" Description="Kills a launched energy ball." />
 
 
-    <RemoteProcedure Name="RPC_BallLaunched" InvokeFrom="Authority" HandleOn="Client" IsPublic="true" IsReliable="true" GenerateEventBindings="true" Description="Triggered on clients whenever an energy ball launches.">
-        <Param Type="AZ::Vector3" Name="Location"/>
-    </RemoteProcedure>
-
     <RemoteProcedure Name="RPC_BallExplosion" InvokeFrom="Authority" HandleOn="Client" IsPublic="true" IsReliable="true" GenerateEventBindings="true" Description="Triggered on clients whenever an energy ball explodes.">
     <RemoteProcedure Name="RPC_BallExplosion" InvokeFrom="Authority" HandleOn="Client" IsPublic="true" IsReliable="true" GenerateEventBindings="true" Description="Triggered on clients whenever an energy ball explodes.">
         <Param Type="HitEvent" Name="HitEvent"/>
         <Param Type="HitEvent" Name="HitEvent"/>
     </RemoteProcedure>
     </RemoteProcedure>

+ 112 - 18
Gem/Code/Source/Components/Multiplayer/EnergyBallComponent.cpp

@@ -11,16 +11,19 @@
 #include <Multiplayer/Components/NetworkRigidBodyComponent.h>
 #include <Multiplayer/Components/NetworkRigidBodyComponent.h>
 #include <MultiplayerSampleTypes.h>
 #include <MultiplayerSampleTypes.h>
 #include <AzCore/Component/TransformBus.h>
 #include <AzCore/Component/TransformBus.h>
+#include <AzFramework/Physics/Components/SimulatedBodyComponentBus.h>
 #include <AzFramework/Physics/RigidBodyBus.h>
 #include <AzFramework/Physics/RigidBodyBus.h>
 #include <WeaponNotificationBus.h>
 #include <WeaponNotificationBus.h>
 
 
 #if AZ_TRAIT_CLIENT
 #if AZ_TRAIT_CLIENT
 #   include <PopcornFX/PopcornFXBus.h>
 #   include <PopcornFX/PopcornFXBus.h>
+#   include <DebugDraw/DebugDrawBus.h>
 #endif
 #endif
 
 
 namespace MultiplayerSample
 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(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)
     void EnergyBallComponent::Reflect(AZ::ReflectContext* context)
     {
     {
@@ -37,27 +40,62 @@ namespace MultiplayerSample
     {
     {
         m_effect = GetExplosionEffect();
         m_effect = GetExplosionEffect();
         m_effect.Initialize();
         m_effect.Initialize();
+
+#if AZ_TRAIT_CLIENT
+        BallActiveAddEvent(m_ballActiveHandler);
+#endif
     }
     }
 
 
     void EnergyBallComponent::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating)
     void EnergyBallComponent::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating)
     {
     {
+#if AZ_TRAIT_CLIENT
+        m_ballActiveHandler.Disconnect();
+#endif
     }
     }
 
 
 #if AZ_TRAIT_CLIENT
 #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)
     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);
         AZ::Transform transform = AZ::Transform::CreateFromQuaternionAndTranslation(AZ::Quaternion::CreateIdentity(), hitEvent.m_target);
         m_effect.TriggerEffect(transform);
         m_effect.TriggerEffect(transform);
 
 
+        // Notify every entity that was hit that they've received a weapon impact.
         for (const HitEntity& hitEntity : hitEvent.m_hitEntities)
         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);
             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();
             const AZ::EntityId hitEntityId = handle.Exists() ? handle.GetEntity()->GetId() : AZ::EntityId();
             WeaponNotificationBus::Broadcast(&WeaponNotificationBus::Events::OnWeaponImpact, GetEntity()->GetId(), hitTransform, hitEntityId);
             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<const Physics::SphereShapeConfiguration*>(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<const Physics::BoxShapeConfiguration*>(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
 #endif
@@ -83,20 +159,29 @@ namespace MultiplayerSample
     void EnergyBallComponentController::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating)
     void EnergyBallComponentController::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating)
     {
     {
 #if AZ_TRAIT_SERVER
 #if AZ_TRAIT_SERVER
-        m_collisionCheckEvent.Enqueue(AZ::TimeMs{ 10 }, true);
+        SetBallActive(false);
 #endif
 #endif
     }
     }
 
 
     void EnergyBallComponentController::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating)
     void EnergyBallComponentController::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating)
     {
     {
 #if AZ_TRAIT_SERVER
 #if AZ_TRAIT_SERVER
-        m_collisionCheckEvent.RemoveFromQueue();
+        SetBallActive(false);
 #endif
 #endif
     }
     }
 
 
 #if AZ_TRAIT_SERVER
 #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_shooterNetEntityId = owningNetEntityId;
         m_hitEvent.m_hitEntities.clear();
         m_hitEvent.m_hitEntities.clear();
 
 
@@ -106,15 +191,14 @@ namespace MultiplayerSample
         m_direction = direction;
         m_direction = direction;
 
 
         // Move the entity to the start position
         // 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
         // We want to sweep our transform during intersect tests to avoid the ball tunneling through targets
         m_lastSweepTransform = GetEntity()->GetTransform()->GetWorldTM();
         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);
         Physics::RigidBodyRequestBus::Event(GetEntityId(), &Physics::RigidBodyRequestBus::Events::SetLinearVelocity, direction * GetGatherParams().m_travelSpeed);
 
 
-        RPC_BallLaunched(startingPosition);
     }
     }
 
 
     void EnergyBallComponentController::HandleRPC_KillBall([[maybe_unused]] AzNetworking::IConnection* invokingConnection)
     void EnergyBallComponentController::HandleRPC_KillBall([[maybe_unused]] AzNetworking::IConnection* invokingConnection)
@@ -124,6 +208,11 @@ namespace MultiplayerSample
 
 
     void EnergyBallComponentController::CheckForCollisions()
     void EnergyBallComponentController::CheckForCollisions()
     {
     {
+        if (!GetBallActive())
+        {
+            return;
+        }
+
         const AZ::Vector3& position = GetEntity()->GetTransform()->GetWorldTM().GetTranslation();
         const AZ::Vector3& position = GetEntity()->GetTransform()->GetWorldTM().GetTranslation();
         const HitEffect& effect = GetHitEffect();
         const HitEffect& effect = GetHitEffect();
 
 
@@ -174,17 +263,22 @@ namespace MultiplayerSample
 
 
     void EnergyBallComponentController::HideEnergyBall()
     void EnergyBallComponentController::HideEnergyBall()
     {
     {
+        if (!GetBallActive())
+        {
+            return;
+        }
+
+        SetBallActive(false);
+        m_collisionCheckEvent.RemoveFromQueue();
+
         m_hitEvent.m_target = GetEntity()->GetTransform()->GetWorldTM().GetTranslation();
         m_hitEvent.m_target = GetEntity()->GetTransform()->GetWorldTM().GetTranslation();
         m_hitEvent.m_shooterNetEntityId = m_shooterNetEntityId;
         m_hitEvent.m_shooterNetEntityId = m_shooterNetEntityId;
         m_hitEvent.m_projectileNetEntityId = GetNetEntityId();
         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());
         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
 #endif
 }
 }

+ 15 - 1
Gem/Code/Source/Components/Multiplayer/EnergyBallComponent.h

@@ -24,11 +24,25 @@ namespace MultiplayerSample
         void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override;
         void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override;
 
 
 #if AZ_TRAIT_CLIENT
 #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;
         void HandleRPC_BallExplosion(AzNetworking::IConnection* invokingConnection, const HitEvent& hitEvent) override;
 #endif
 #endif
 
 
     private:
     private:
+#if AZ_TRAIT_CLIENT
+        void DebugDraw();
+        void OnBallActiveChanged(bool active);
+
+        AZ::ScheduledEvent m_debugDrawEvent{ [this]()
+        {
+            DebugDraw();
+        }, AZ::Name("EnergyBallDebugDraw") };
+
+        AZ::Event<bool>::Handler m_ballActiveHandler{ [this](bool active)
+        {
+            OnBallActiveChanged(active);
+        } };
+#endif
+
         GameEffect m_effect;
         GameEffect m_effect;
     };
     };
 
 

+ 63 - 17
Gem/Code/Source/Effects/GameEffect.cpp

@@ -48,7 +48,10 @@ namespace MultiplayerSample
 #if AZ_TRAIT_CLIENT
 #if AZ_TRAIT_CLIENT
         if (m_popcornFx != nullptr)
         if (m_popcornFx != nullptr)
         {
         {
-            m_popcornFx->DestroyEffect(m_emitter);
+            if (m_popcornFx->IsEffectAlive(m_emitter))
+            {
+                m_popcornFx->DestroyEffect(m_emitter);
+            }
             m_emitter = nullptr;
             m_emitter = nullptr;
         }
         }
 
 
@@ -72,8 +75,11 @@ namespace MultiplayerSample
 
 
         if (m_popcornFx != nullptr)
         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)
         if (m_audioSystem != nullptr)
@@ -91,10 +97,18 @@ namespace MultiplayerSample
 #if AZ_TRAIT_CLIENT
 #if AZ_TRAIT_CLIENT
         if (m_popcornFx != nullptr)
         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
 #endif
@@ -106,10 +120,18 @@ namespace MultiplayerSample
 #if AZ_TRAIT_CLIENT
 #if AZ_TRAIT_CLIENT
         if (m_popcornFx != nullptr)
         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
 #endif
@@ -121,10 +143,18 @@ namespace MultiplayerSample
 #if AZ_TRAIT_CLIENT
 #if AZ_TRAIT_CLIENT
         if (m_popcornFx != nullptr)
         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
 #endif
@@ -136,10 +166,18 @@ namespace MultiplayerSample
 #if AZ_TRAIT_CLIENT
 #if AZ_TRAIT_CLIENT
         if (m_popcornFx != nullptr)
         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
 #endif
@@ -156,8 +194,16 @@ namespace MultiplayerSample
         {
         {
             if (PopcornFX::PopcornFXRequests* popcornFx = PopcornFX::PopcornFXRequestBus::FindFirstHandler())
             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.");
+                }
             }
             }
         }
         }