Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Energy Ball Fixes #363

Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions Gem/Code/Source/AutoGen/EnergyBallComponent.AutoComponent.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,16 @@
<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" />

<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="Direction"/>
<Param Type="Multiplayer::NetEntityId" Name="OwningNetEntityId" />
</RemoteProcedure>

<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.">
<Param Type="HitEvent" Name="HitEvent"/>
</RemoteProcedure>
Expand Down
141 changes: 122 additions & 19 deletions Gem/Code/Source/Components/Multiplayer/EnergyBallComponent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,19 @@
#include <Multiplayer/Components/NetworkRigidBodyComponent.h>
#include <MultiplayerSampleTypes.h>
#include <AzCore/Component/TransformBus.h>
#include <AzFramework/Physics/Components/SimulatedBodyComponentBus.h>
#include <AzFramework/Physics/RigidBodyBus.h>
#include <WeaponNotificationBus.h>

#if AZ_TRAIT_CLIENT
# include <PopcornFX/PopcornFXBus.h>
# include <DebugDraw/DebugDrawBus.h>
#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)
{
Expand All @@ -37,39 +40,121 @@ namespace MultiplayerSample
{
m_effect = GetExplosionEffect();
m_effect.Initialize();

#if AZ_TRAIT_CLIENT
// The energy ball particle effect defaults to active in the component, so initialize wasActive to true for the first state.
m_wasActive = true;
AZ::TickBus::Handler::BusConnect();
#endif
}

void EnergyBallComponent::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating)
{
#if AZ_TRAIT_CLIENT
AZ::TickBus::Handler::BusDisconnect();
#endif
}

#if AZ_TRAIT_CLIENT
void EnergyBallComponent::HandleRPC_BallLaunched([[maybe_unused]] AzNetworking::IConnection* invokingConnection, [[maybe_unused]] const AZ::Vector3& location)
void EnergyBallComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could actually, if you wanted, attach an event handler to the BallActive network property so that when the value changes your callback gets invoked. Since it's just an on/off thing I think it's safe to just say if (active) startfx else killfx although you've clearly run into some hair pulling bugs with pkfx.

Only benefit is if somebody goes crazy and adds 100,000 energy balls to a level the game doesn't have to go through and tick all 100,000 every frame on the client and the server, you'd only process effects changes when the state of BallActive changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooh, neat, i didn't know the "on changed" events existed! i'll switch over to using that.

{
PopcornFX::PopcornFXEmitterComponentRequests* emitterRequests = PopcornFX::PopcornFXEmitterComponentRequestBus::FindFirstHandler(GetEntity()->GetId());
if (emitterRequests != nullptr)
#if AZ_TRAIT_CLIENT
bool active = GetBallActive();

// Turn on the energy ball VFX. Physics on the server side will cause the energy ball to move.
if (active != m_wasActive)
{
emitterRequests->Start();
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.");
}
else
{
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_wasActive = active;
}

if (active && cl_EnergyBallDebugDraw)
{
DebugDraw();
}
#endif
}

#if AZ_TRAIT_CLIENT
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);
const Multiplayer::ConstNetworkEntityHandle handle = Multiplayer::GetNetworkEntityManager()->GetEntity(hitEntity.m_hitNetEntityId);
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<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
Expand All @@ -83,20 +168,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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, that makes a lot more sense..


SetBallActive(true);

m_shooterNetEntityId = owningNetEntityId;
m_hitEvent.m_hitEntities.clear();

Expand All @@ -106,15 +200,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)
Expand All @@ -124,6 +217,11 @@ namespace MultiplayerSample

void EnergyBallComponentController::CheckForCollisions()
{
if (!GetBallActive())
{
return;
}

const AZ::Vector3& position = GetEntity()->GetTransform()->GetWorldTM().GetTranslation();
const HitEffect& effect = GetHitEffect();

Expand Down Expand Up @@ -174,17 +272,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
}
9 changes: 8 additions & 1 deletion Gem/Code/Source/Components/Multiplayer/EnergyBallComponent.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@

#pragma once

#include <AzCore/Component/TickBus.h>
#include <Source/AutoGen/EnergyBallComponent.AutoComponent.h>
#include <Source/Weapons/WeaponGathers.h>

namespace MultiplayerSample
{
class EnergyBallComponent
: public EnergyBallComponentBase
, public AZ::TickBus::Handler
{
public:
AZ_MULTIPLAYER_COMPONENT(MultiplayerSample::EnergyBallComponent, s_energyBallComponentConcreteUuid, MultiplayerSample::EnergyBallComponentBase);
Expand All @@ -23,12 +25,17 @@ namespace MultiplayerSample
void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override;
void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override;

void OnTick(float deltaTime, AZ::ScriptTimePoint time) 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();
#endif
// Track the previous "ball active" state so we know whether or not to flip the energy ball particle effect state.
bool m_wasActive = true;
GameEffect m_effect;
};

Expand Down
Loading