This spell class is like a bullet or a rocket. When it touch the ground, I spawn the effect in a circle or ground actors, it depends on the spell configuration.
The OnImpact method is called with the HitResult as a parameter.
TouchedGround() is called from the server in case you have a client/server game.
WeaponConfig.Spell.SpellLife is the time for the spell. (effects or ground actors).
WeaponConfig.Spell.DistanceGapInRadius defines the gap between effects or ground actors. If 0, then only one is spawned immediatly. If not, then the gap is used to fill the circle and effects or ground actors are spawned using a timer.
For spell and ground actor, the algorithm is the same. Start from the center and spawn effects or actors using the gap. Keep the spawn objects within an array to be able to destroy then once the lifetime is reached.
For filling the circle it is very simple.
-
randomize the angle to avoid linear effect
-
NumPoints is calculated and used to known how many objects I have to spawn
-
DeltaPoints is used to spawn the number of objects during on call of the SpawnEffectActors()
-
Once the number of object is reached, calculate the new number of object to spawn during the next call of SpawnEffectActors()
-
Once the DistanceFromCenter is greather than the spell radius, stop the timer to stop spawning effects or ground actors
-
Finally, the function DisableAndDestroy is in charge of deactivating and destroying the effects kept within the array and the ground actor also
void AKSpell::OnImpact(const FHitResult& HitResult)
{
UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT(“AKSpell::OnImpact - Role=%s HitResult.ImpactPoint=%s HitResult.ImpactNormal=%s”), NETROLETOSTRING(Role), *HitResult.ImpactPoint.ToString(), *HitResult.ImpactNormal.ToString());
if (Role == ROLE_Authority && !bTouchedGround)
{
TouchedGround(HitResult);
// Since we have loop effects in the emitters
// we cannot destroy the particles only when we are on the server
// we need also to deactivate from the client
// otherwise the effect still on the clients
}
}void AKSpell::TouchedGround(const FHitResult& Impact)
{
UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT(“AKSpell::TouchedGround - Role=%s”), NETROLETOSTRING(Role));
if (ParticleComp)
{
ParticleComp->Deactivate();
}
// Keep track of the time this spell touch the ground
TouchTime = GetWorld()->GetTimeSeconds();// effects and damage origin shouldn't be placed inside mesh at impact point NudgedImpactLocation = Impact.ImpactPoint + Impact.ImpactNormal * 10.0f; UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::TouchedGround - NudgedImpactLocation=%f"), *NudgedImpactLocation.ToString()); // Apply the spell damage at impact if (WeaponConfig.Spell.SpellImpactDamage > 0 && WeaponConfig.Spell.SpellRadius > 0 && WeaponConfig.DamageType) { UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::TouchedGround - apply damage at impact World time=%f"), TouchTime); UGameplayStatics::ApplyRadialDamage(this, WeaponConfig.Spell.SpellImpactDamage, NudgedImpactLocation, WeaponConfig.Spell.SpellRadius, WeaponConfig.DamageType, TArray<AActor*>(), this, MyController.Get(), WeaponConfig.FullDamage); } // Apply alterations at impact if (WeaponConfig.Alterations.Num() > 0 && WeaponConfig.Spell.SpellRadius > 0 && WeaponConfig.DamageType) { UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::TouchedGround - apply alterations at impact World time=%f"), TouchTime); UKAttributeHelper::ApplyRadialAlterations(this, SpellGuid, WeaponConfig.Alterations, NudgedImpactLocation, WeaponConfig.Spell.SpellRadius, WeaponConfig.DamageType, TArray<AActor*>(), this, MyController.Get()); } // Start the timer to destroy at the life time end if (WeaponConfig.Spell.SpellLife > 0) { // Start the timer to disable and destroy after the spell life time UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::TouchedGround - launch timer to DisableAndDestroy after %f seconds"), WeaponConfig.Spell.SpellLife); FTimerHandle TempHandle2; GetWorldTimerManager().SetTimer(TempHandle2, this, &AKSpell::DisableAndDestroy, WeaponConfig.Spell.SpellLife, false); } else { // if no life, we can disable and destroy DisableAndDestroy(); } // Start the timer to apply recurring spell damage if (WeaponConfig.Spell.SpellLife > 0 && WeaponConfig.Spell.SpellRecurringDamageInterval > 0 && WeaponConfig.Spell.SpellRecurringDamage > 0 && WeaponConfig.Spell.SpellRadius > 0 && WeaponConfig.DamageType) { UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::TouchedGround - launch timer to apply recurring damage World time=%f"), TouchTime); FTimerHandle TempHandle; GetWorldTimerManager().SetTimer(TempHandle, this, &AKSpell::ApplyRecurringDamage, WeaponConfig.Spell.SpellRecurringDamageInterval, false); } // Do we have the spell effect ? if (GroundTemplate) { // If we have a distance gap defined we will need to spawn several actors if (WeaponConfig.Spell.DistanceGapInRadius > 0.0f) { // Initialize DistanceFromCenter = 0.0f; NumPoints = 1; // Call a loop timer function to spawn the effect actors GetWorldTimerManager().SetTimer(SpawnEffectActors_Handle, FTimerDelegate::CreateUObject(this, &AKSpell::SpawnEffectActors, Impact), 0.2f, true); } else // Otherwise, we have only one effect to spawn { FTransform const SpawnTransform(FRotator(0, 0, 0), NudgedImpactLocation); AKWeaponFireGroundEffect* EffectActor = GetWorld()->SpawnActorDeferred<AKWeaponFireGroundEffect>(GroundTemplate, SpawnTransform); if (EffectActor) { EffectActor->SurfaceHit = Impact; // Set its life time EffectActor->GroundEffectLifeTime = WeaponConfig.Spell.SpellLife; // Set its color based on the damage type EffectActor->SetColor(GetWeaponConfigDamageTypeColor(WeaponConfig)); UGameplayStatics::FinishSpawningActor(EffectActor, SpawnTransform); // Add the actor to the array EffectActors.Add(EffectActor); } } } else { UE_LOG(LogKSGMWeapon, Warning, TEXT("AKSpell::TouchedGround - GroundTemplate is not set.")); } // Do we have a ground actor ? // For an actor only spawned on the server side // never on the client. if (Role == ROLE_Authority && GroundActor) { // If we have a distance gap defined we will need to spawn several actors if (WeaponConfig.Spell.DistanceGapInRadius > 0.0f) { // Initialize GDistanceFromCenter = 0.0f; GNumPoints = 1; // Call a loop timer function to spawn the ground actors GetWorldTimerManager().SetTimer(SpawnGroundActors_Handle, FTimerDelegate::CreateUObject(this, &AKSpell::SpawnGroundActors, Impact), 0.2f, true); } else // Otherwise, we have only one actor to spawn { // We apply a scale to fill the area since we have only one actor FTransform const SpawnTransform(FRotator(0, 0, 0), NudgedImpactLocation, FVector(WeaponConfig.Spell.SpellRadius / GroundActorBoundSize, WeaponConfig.Spell.SpellRadius / GroundActorBoundSize, 1.0f)); UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::TouchedGround - GetActorLocation=%s"), *SpawnTransform.GetLocation().ToString()); UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::TouchedGround - GetActorScale=%s"), *SpawnTransform.GetScale3D().ToString()); AKGroundActor* GActor = GetWorld()->SpawnActorDeferred<AKGroundActor>(GroundActor, SpawnTransform); if (GActor) { UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::TouchedGround - Ground Actor spawned=%p"), GActor); // Do we have deferred collision if (GActor->DeferredCollisionTime > 0.f) { GActor->SetActorEnableCollision(false); SpawnedGroundActors.Add(GActor); } UGameplayStatics::FinishSpawningActor(GActor, SpawnTransform); // Add the actor to the array GroundActors.Add(GActor); // Do we have collisions to sets back if (SpawnedGroundActors.Num() > 0) { // Start the timer to enable back the collision FTimerHandle TempHandle2; GetWorldTimerManager().SetTimer(TempHandle2, this, &AKSpell::SetBackCollision2GroundActors, GActor->DeferredCollisionTime, false); } } } } bTouchedGround = true;}
void AKSpell::SpawnEffectActors(const FHitResult Impact)
{
FVector Distance(DistanceFromCenter, 0.0f, 0.0f);
int32 count = EffectActors.Num();
int32 DeltaPoints = NumPoints - count;
UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT(“AKSpell::SpawnEffectActors - Role=%s DistanceFromCenter=%f count=%i NumPoints=%i DeltaPoints=%i”), NETROLETOSTRING(Role), DistanceFromCenter, count, NumPoints, DeltaPoints);for (int32 i = 0; i < DeltaPoints; ++i) { float Percent = (float)i / (float)DeltaPoints; float Angle = FMath::Lerp(0.0f, 360.0f, Percent); float AngleDeg = FRotator::ClampAxis(Angle * 180.f / PI); // Randomize the angle to avoid linear effects Angle = FMath::RandRange(Angle - (360.0f / DeltaPoints / 4), Angle + (360.0f / DeltaPoints / 4)); FVector VertexDir = (i > 0) ? Distance.RotateAngleAxis(Angle, FVector(0.f, 0.f, 1.f)) : FVector(DistanceFromCenter,0.f,0.f); //VertexDir.Normalize(); FVector FinalImpactLocation = NudgedImpactLocation + VertexDir; // Set the rotation to zero FTransform const SpawnTransform(FRotator(0,0,0), FinalImpactLocation); AKWeaponFireGroundEffect* EffectActor = GetWorld()->SpawnActorDeferred<AKWeaponFireGroundEffect>(GroundTemplate, SpawnTransform); if (EffectActor) { EffectActor->SurfaceHit = Impact; // Set its life time EffectActor->GroundEffectLifeTime = WeaponConfig.Spell.SpellLife; // Set its color based on the damage type EffectActor->SetColor(GetWeaponConfigDamageTypeColor(WeaponConfig)); UGameplayStatics::FinishSpawningActor(EffectActor, SpawnTransform); // Add the actor to the array count = EffectActors.Add(EffectActor) + 1; // Check if we have to increase the distance from the center // and calculate the new number of points to reach if (count >= NumPoints) { DistanceFromCenter += WeaponConfig.Spell.DistanceGapInRadius; UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::SpawnEffectActors - DistanceFromCenter=%f WeaponConfig.Spell.DistanceGapInRadius=%f"), DistanceFromCenter, WeaponConfig.Spell.DistanceGapInRadius); NumPoints += (int32)(DistanceFromCenter / WeaponConfig.Spell.DistanceGapInRadius) * 4; } } } // Do we reach the end of the circle if (DistanceFromCenter >= WeaponConfig.Spell.SpellRadius) { GetWorldTimerManager().ClearTimer(SpawnEffectActors_Handle); UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::SpawnEffectActors - Reach radius DistanceFromCenter=%f WeaponConfig.Spell.SpellRadius=%f Count=%i"), DistanceFromCenter, WeaponConfig.Spell.SpellRadius, EffectActors.Num()); }}
void AKSpell::SpawnGroundActors(const FHitResult Impact)
{
FVector Distance(GDistanceFromCenter, 0.0f, 0.0f);
int32 count = EffectActors.Num();
int32 DeltaPoints = GNumPoints - count;
UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT(“AKSpell::SpawnGroundActors - Role=%s DistanceFromCenter=%f count=%i NumPoints=%i DeltaPoints=%i”), NETROLETOSTRING(Role), GDistanceFromCenter, count, GNumPoints, DeltaPoints);float DeferredCollisionTime = 0.0f; for (int32 i = 0; i < DeltaPoints; ++i) { float Percent = (float)i / (float)DeltaPoints; float Angle = FMath::Lerp(0.0f, 360.0f, Percent); float AngleDeg = FRotator::ClampAxis(Angle * 180.f / PI); // Randomize the angle to avoid linear effects Angle = FMath::RandRange(Angle - (360.0f / DeltaPoints / 4), Angle + (360.0f / DeltaPoints / 4)); FVector VertexDir = (i > 0) ? Distance.RotateAngleAxis(Angle, FVector(0.f, 0.f, 1.f)) : FVector(GDistanceFromCenter, 0.f, 0.f); FVector FinalImpactLocation = NudgedImpactLocation + VertexDir; // Set the rotation to zero // Don't modify the scale since we have several actors to fill the spell area FTransform const SpawnTransform(FRotator(0, 0, 0), FinalImpactLocation); AKGroundActor* GActor = GetWorld()->SpawnActorDeferred<AKGroundActor>(GroundActor, SpawnTransform); if (GActor) { // Do we have deferred collision if ((DeferredCollisionTime = GActor->DeferredCollisionTime) > 0.f) { GActor->SetActorCollision(false); SpawnedGroundActors.Add(GActor); } UGameplayStatics::FinishSpawningActor(GActor, SpawnTransform); // Add the actor to the array count = GroundActors.Add(GActor) + 1; // Check if we have to increase the distance from the center // and calculate the new number of points to reach if (count >= GNumPoints) { GDistanceFromCenter += WeaponConfig.Spell.DistanceGapInRadius; UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::SpawnGroundActors - DistanceFromCenter=%f WeaponConfig.Spell.DistanceGapInRadius=%f"), GDistanceFromCenter, WeaponConfig.Spell.DistanceGapInRadius); GNumPoints += (int32)(GDistanceFromCenter / WeaponConfig.Spell.DistanceGapInRadius) * 4; } } } // After having spwaned ground actors in this cycle // Do we have collisions to sets back if (SpawnedGroundActors.Num() > 0) { // Start the timer to enable back the collision FTimerHandle TempHandle2; GetWorldTimerManager().SetTimer(TempHandle2, this, &AKSpell::SetBackCollision2GroundActors, DeferredCollisionTime, false); } // Do we reach the end of the circle if (GDistanceFromCenter >= WeaponConfig.Spell.SpellRadius) { GetWorldTimerManager().ClearTimer(SpawnGroundActors_Handle); UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::SpawnGroundActors - Reach radius DistanceFromCenter=%f WeaponConfig.Spell.SpellRadius=%f Count=%i"), GDistanceFromCenter, WeaponConfig.Spell.SpellRadius, GroundActors.Num()); }}
void AKSpell::DisableAndDestroy()
{
UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT(“AKSpell::DisableAndDestroy - Role=%s”), NETROLETOSTRING(Role));
UAudioComponent* ProjAudioComp = FindComponentByClass();
if (ProjAudioComp && ProjAudioComp->IsPlaying())
{
ProjAudioComp->FadeOut(0.1f, 0.f);
}MovementComp->StopMovementImmediately(); // give clients some time to show the effect in case no recurring damage // in case we had no recurring damages if (WeaponConfig.Spell.SpellLife > 0 && WeaponConfig.Spell.SpellRecurringDamageInterval > 0 && WeaponConfig.Spell.SpellRecurringDamage > 0 && WeaponConfig.Spell.SpellRadius > 0 && WeaponConfig.DamageType) SetLifeSpan( 2.0f ); // We can deactivate and destroy all the created effects int32 count = EffectActors.Num(); for (int32 idx = 0; idx < count; ++idx) { AKWeaponFireGroundEffect* EffectActor = EffectActors[idx]; if (EffectActor) EffectActor->DeactivateAndDestroy(); } EffectActors.Reset(); // and destroy the ground actors count = GroundActors.Num(); for (int32 idx = 0; idx < count; ++idx) { AActor* GActor = GroundActors[idx]; UE_LOG(LogKSGMWeapon, Verbose, TEXT("AKSpell::DisableAndDestroy - Destroy GActor=%p"), GActor); if (GActor) GActor->Destroy(); } GroundActors.Reset();}