Skip to content

Instantly share code, notes, and snippets.

@zr0n
Last active June 19, 2024 20:41
Show Gist options
  • Save zr0n/b5622662e30daaa2198ac1a1b8fa61f3 to your computer and use it in GitHub Desktop.
Save zr0n/b5622662e30daaa2198ac1a1b8fa61f3 to your computer and use it in GitHub Desktop.
Custom projectile movement, wall penetration and damage using Unreal Engine 4
// Fill out your copyright notice in the Description page of Project Settings.
#include "Projectile.h"
#include "Kismet/KismetMathLibrary.h"
#include "Kismet/KismetSystemLibrary.h"
#include "Kismet/KismetStringLibrary.h"
#include "Kismet/KismetMathLibrary.h"
#include "Kismet/GameplayStatics.h"
#include "Curves/CurveFloat.h"
#include "PhysicalMaterials/PhysicalMaterial.h"
#include "Components/SceneComponent.h"
#include "TimerManager.h"
#include "Runtime/Engine/Private/KismetTraceUtils.h"
#include "Runtime/Engine/Private/KismetTraceUtils.cpp"
#include "Runtime/Engine/Public/CollisionQueryParams.h"
// Sets default values
AProjectile::AProjectile()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
InstancedStaticMesh = CreateDefaultSubobject<UInstancedStaticMeshComponent>(TEXT("InstancedStaticMesh"));
InstancedStaticMesh->SetupAttachment(SphereCollision);
SetRootComponent(InstancedStaticMesh);
SetDefaultValues();
}
void AProjectile::SetDefaultValues()
{
Force = 3000.f;
ForceLossOverTime = 75;
Gravity = 980.f;
MinForceToStartApplyGravity = 2900.f;
ForceLossWhenHitWall = 1000.f;
DevianceFactorWhenHitWall = 30.f;
BaseDamage = 30.f;
bDebugPath = false;
bDebugHit = true;
ForceLossWhenBounce = 1000.f;
DamageDistanceMultiplier = .01f;
}
// Called when the game starts or when spawned
void AProjectile::BeginPlay()
{
Super::BeginPlay();
SetupInitialVelocity();
SaveInitialLocation();
InitialForce = Force;
}
// Called every frame
void AProjectile::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
DeltaT = DeltaTime;
LookForward();
Move();
DecrementForceOverTime();
ApplyGravity();
}
void AProjectile::SetupInitialVelocity()
{
Velocity = GetActorForwardVector() * Force;
}
void AProjectile::SaveInitialLocation()
{
InitialLocation = GetActorLocation();
}
void AProjectile::LookForward()
{
SetActorRotation(
UKismetMathLibrary::MakeRotFromX(Velocity.GetSafeNormal())
);
}
void AProjectile::Move()
{
AddActorWorldOffset(
Velocity * DeltaT,
true
);
FHitResult hitResult;
FVector start = GetActorLocation();
FVector end = GetActorLocation() + Velocity * DeltaT;
TArray<AActor*> actorsToIgnore;
actorsToIgnore.Add(this);
if (bDebugPath)
UKismetSystemLibrary::DrawDebugLine(
this,
start,
end,
UKismetMathLibrary::LinearColorLerp(FLinearColor::Black, FLinearColor(1.f, 0.f, 1.f, 1.f), Force / InitialForce),
InitialLifeSpan,
1.f
);
bool bResult = UKismetSystemLibrary::LineTraceSingleForObjects(
this,
start,
end,
ObjectsToCollide,
false,
actorsToIgnore,
EDrawDebugTrace::None,
hitResult,
true
);
if (bResult)
CheckCollision(hitResult);
}
void AProjectile::DecrementForceOverTime()
{
Force -= ForceLossOverTime * DeltaT;
if (Force <= .0f)
Destroy(this);
Velocity = Velocity.GetSafeNormal() * Force;
}
void AProjectile::ApplyGravity()
{
if (Force > MinForceToStartApplyGravity)
return;
Velocity += FVector(0.f, 0.f, -(Gravity * DeltaT));
}
void AProjectile::OnHitPenetrable()
{
Force -= ForceLossWhenHitWall;
ApplyDevianceByWallPenetration();
}
void AProjectile::ApplyDevianceByWallPenetration()
{
FVector Deviance = FVector(
CalculateDeviance(),
CalculateDeviance(),
CalculateDeviance()
);
Velocity += Deviance;
}
void AProjectile::OnHitSomething(AActor* ActorHitted, FHitResult HitResult, bool bIsPenetrable)
{
AController* ownerController = Cast<AController>(GetOwner());
float damage = GetDamageByDistance();
UGameplayStatics::ApplyPointDamage(
ActorHitted,
damage,
GetActorForwardVector(),
HitResult,
ownerController, //Instigator
this, //DamageCauser
DamageType );
/*
DEACTIVATED
if (!bIsPenetrable)
Bounce(HitResult.Normal);
*/
}
void AProjectile::CheckCollision(const FHitResult& HitResult)
{
AActor* OtherActor = HitResult.Actor.Get();
if (!IsValid(OtherActor))
return;
if (AlreadyHitThisActor(OtherActor))
return;
ActorsAlreadyHitted.AddUnique(OtherActor);
if (GetInstigator() == OtherActor)
return;
if (bDebugHit)
UKismetSystemLibrary::DrawDebugSphere(this, GetActorLocation(), 10, 12, FLinearColor::Red, 5.f, 3.f);
UPhysicalMaterial* physicalMaterial = IsValid(HitResult.PhysMaterial.Get()) ? HitResult.PhysMaterial.Get() : nullptr;
if (!IsValid(physicalMaterial))
{
OnHitSomething(OtherActor, HitResult, false);
return;
}
float penetrationResistance = GetPenetrationResistance(physicalMaterial->SurfaceType);
if (penetrationResistance > .0f)
{
float penetrationDepth = CalculatePenetrationDepth();
Force -= penetrationResistance * penetrationDepth;
if(bDebugPenetration)
UKismetSystemLibrary::PrintString(this, TEXT("Penetration Depth: ") + UKismetStringLibrary::Conv_FloatToString(penetrationDepth));
OnHitPenetrable();
OnHitSomething(OtherActor, HitResult, true);
}
else
{
OnHitSomething(OtherActor, HitResult, false);
}
}
void AProjectile::DebugPath()
{
FLinearColor color = FLinearColor(
FMath::RandRange(0.f, 1.f),
FMath::RandRange(0.f, 1.f),
FMath::RandRange(0.f, 1.f),
1.f
);
UKismetSystemLibrary::DrawDebugSphere(this, GetActorLocation(), 10, 12, color, 5.f, 3.f);
}
float AProjectile::GetDistanceTravelled()
{
return (GetActorLocation() - InitialLocation).Size();
}
float AProjectile::GetDamageByDistance()
{
if (!ensure(IsValid(DamageCurve)))
{
UE_LOG(LogClass, Error, TEXT("Invalid Damage Curve"));
return 0.f;
}
return DamageCurve->GetFloatValue(GetDistanceTravelled()) * BaseDamage * DamageDistanceMultiplier;
}
float AProjectile::GetDamageByDistanceAndBone(FName BoneName)
{
if (!BonesDamageMultiplier.Contains(BoneName))
return GetDamageByDistance();
float multiplier = *BonesDamageMultiplier.Find(BoneName);
return GetDamageByDistance() * multiplier;
}
float AProjectile::CalculatePenetrationDepth()
{
FVector start = GetActorLocation();
FVector end = start + (GetActorForwardVector() * 999999.f);
TArray<AActor*> ignoredActors = TArray<AActor*>();
TArray<FHitResult> hitResults;
FHitResult startHit;
FHitResult endHit;
bool bResult = UKismetSystemLibrary::LineTraceMultiForObjects(
this,
start,
end,
ObjectsToCollide,
true,
ignoredActors,
EDrawDebugTrace::None,
hitResults,
true
);
if (!bResult)
return .0f;
startHit = hitResults[0];
if (hitResults.Num() < 2)
{
start = startHit.TraceEnd;
}
else
{
start = hitResults[1].Location;
//ignoredActors.Add(hitResults[1].Actor.Get());
}
end = startHit.Location;
if (!bResult)
return .0f;
bResult = UKismetSystemLibrary::LineTraceMultiForObjects(
this,
start,
end,
ObjectsToCollide,
true,
ignoredActors,
EDrawDebugTrace::None,
hitResults,
true
);
if (!bResult)
return .0f;
AActor* actorHitted = startHit.Actor.Get();
for (int i = 0; i < hitResults.Num(); i++)
{
endHit = hitResults[i];
AActor* actorBehind = endHit.Actor.Get();
if (!ensure(actorBehind) || !ensure(actorHitted))
{
return .0f;
}
if (actorHitted == actorBehind)
{
return (
endHit.Location - startHit.Location
).Size();
}
}
return .0f;
}
float AProjectile::CalculateDeviance()
{
float min = DevianceFactorWhenHitWall * -1;
float max = DevianceFactorWhenHitWall;
return UKismetMathLibrary::RandomFloatInRange(min, max);
}
bool AProjectile::AlreadyHitThisActor(AActor* ActorToCheck)
{
for (int i = 0; i < ActorsAlreadyHitted.Num(); i++)
{
if (ActorToCheck == ActorsAlreadyHitted[i])
return true;
}
return false;
}
float AProjectile::GetPenetrationResistance(EPhysicalSurface SurfaceType)
{
if (PenetrationResistanceBySurfaceType.Find(SurfaceType))
return PenetrationResistanceBySurfaceType[SurfaceType];
else
return -1.f;
}
void AProjectile::Bounce(FVector Normal)
{
//Reflection Vector r= v − 2 * projection
//projection = (v⋅n)n
//v = vector
//n = normal
//r = reflection
FVector projection = UKismetMathLibrary::Dot_VectorVector(Velocity.GetSafeNormal(), Normal) * Normal;
FVector reflection = Velocity.GetSafeNormal() - (2 * projection);
Velocity = reflection * Force;
Force -= ForceLossWhenBounce;
}
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Components/SphereComponent.h"
#include "Components/InstancedStaticMeshComponent.h"
#include "Projectile.generated.h"
UCLASS()
class SCA_API AProjectile : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AProjectile();
void SetDefaultValues();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
public:
//Components
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Projectile Components")
USphereComponent* SphereCollision;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Projectile Components")
UInstancedStaticMeshComponent* InstancedStaticMesh;
//Public Properties
UPROPERTY(BlueprintReadWrite, VisibleAnywhere, Category = "Projectile")
//Where the projectile was spawned
FVector InitialLocation;
UPROPERTY(BlueprintReadWrite, VisibleAnywhere, Category = "Projectile")
//Current Velocity (not normalized)
FVector Velocity;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Projectile Debug", meta = (ExposeOnSpawn = true))
//If true draw a sphere everytime a new object is hitted
bool bDebugHit;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Projectile Debug", meta = (ExposeOnSpawn = true))
//If true will draw a line every frame to indicate the path traveled
bool bDebugPath;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Projectile", meta = (ExposeOnSpawn = true))
//If true prints a string everytime we calculate the penetration depth
bool bDebugPenetration;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Projectile", meta = (ExposeOnSpawn = true))
//Force loss when bounce on something (not being used in this project)
float ForceLossWhenBounce;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Projectile", meta = (ExposeOnSpawn = true))
//Current Force of the projectile
float Force;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Projectile", meta = (ExposeOnSpawn = true))
//Force loss by seconds
float ForceLossOverTime;
UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "Projectile")
//Delta Seconds
float DeltaT;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Projectile", meta = (ExposeOnSpawn = true))
//Gravity we will starting apply when the Force variable is lesser than MinForceToStartApplyGravity
float Gravity;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Projectile", meta = (ExposeOnSpawn = true))
//We will start to apply Gravity when the current force is lesser than this value
float MinForceToStartApplyGravity;
UPROPERTY(BlueprintReadWrite, VisibleAnywhere, Category = "Projectile", meta = (ExposeOnSpawn = true))
//How much force will be lost when something penetrable is hitted
float ForceLossWhenHitWall;
UPROPERTY(BlueprintReadWrite, VisibleAnywhere, Category = "Projectile", meta = (ExposeOnSpawn = true))
//Deviance threshold when something penetrable is hitted
float DevianceFactorWhenHitWall;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Projectile", meta = (ExposeOnSpawn = true))
//This value is multiplied by the distance traveled to calculate damage.
float DamageDistanceMultiplier;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Projectile", meta = (ExposeOnSpawn = true))
//How much damage this projectile will have. This value is multiplied by the distance traveled to calculate damage.
float BaseDamage;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Projectile", meta = (ExposeOnSpawn = true))
//Damage curve Horizontal axis is distance and the vertical is the damage.
class UCurveFloat* DamageCurve;
UPROPERTY(BlueprintReadWrite, VisibleAnywhere, Category = "Projectile")
//This map contains bones factor.
TMap<FName, float> BonesDamageMultiplier;
UPROPERTY(BlueprintReadWrite, VisibleAnywhere, Category = "Projectile", meta = (ExposeOnSpawn = true))
//This map contais the penetration resistance factor. How much higher more force/velocity will be lost.
TMap<TEnumAsByte<EPhysicalSurface>, float> PenetrationResistanceBySurfaceType;
UPROPERTY(BlueprintReadWrite, VisibleAnywhere, Category = "Projectile", meta = (ExposeOnSpawn = true))
//Actors hitted. We use this to avoid re-applying damage on a actor hitted b4.
TArray<AActor*> ActorsAlreadyHitted;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Projectile", meta = (ExposeOnSpawn = true))
//The Damage Type (wooow)
TSubclassOf<class UDamageType> DamageType;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Projectile", meta = (ExposeOnSpawn = true))
//Object Types we will check when ray casting
TArray<TEnumAsByte<EObjectTypeQuery>> ObjectsToCollide;
//Protected Properties
protected:
UPROPERTY()
//Timer Handle used internal
FTimerHandle DebugTimerHandle;
protected:
//BeginPlay
void SetupInitialVelocity();
void SaveInitialLocation();
//Tick
void LookForward();
void Move();
void DecrementForceOverTime();
void ApplyGravity();
//Events
void OnHitPenetrable();
void ApplyDevianceByWallPenetration();
void OnHitSomething(class AActor* ActorHitted, FHitResult HitResult, bool bIsPenetrable);
UFUNCTION()
void CheckCollision(const FHitResult& HitResult);
UFUNCTION()
void DebugPath();
//Pure Functions
UFUNCTION(BlueprintPure, Category = "Projectile")
//Get Distance travelled since the projectile spawned
float GetDistanceTravelled();
UFUNCTION(BlueprintPure, Category = "Projectile")
//Get damage based on distance using the Damage Curve
float GetDamageByDistance();
UFUNCTION(BlueprintPure, Category = "Projectile")
//Calculate random Deviance when projectile hits something
float CalculateDeviance();
UFUNCTION(BlueprintPure, Category = "Projectile")
//Get damage based on distance and Bone using the BonesDamageMultiplier
float GetDamageByDistanceAndBone(FName BoneName);
UFUNCTION(BlueprintPure, Category = "Projectile")
//Check if actor was hitted before by this projectile
bool AlreadyHitThisActor(AActor* ActorToCheck);
UFUNCTION(BlueprintPure, Category = "Projectile Penetration")
//Get the penetration resistance by the Surface we are trying to go through. Check the variable PenetrationResistanceBySurfaceType
float GetPenetrationResistance(EPhysicalSurface SurfaceType);
//Callable Functions
UFUNCTION(BlueprintCallable, Category = "Projectile Penetration")
//Calculate the penetration depth of the object we are going through
float CalculatePenetrationDepth();
UFUNCTION(BlueprintCallable, Category = "Projectile")
//Calculate a reflection vector to get the new velocity the object is going after hitting an unpenetrable thimg. Not being used.
void Bounce(FVector Normal);
private:
float InitialForce;
};
@zr0n
Copy link
Author

zr0n commented Jun 24, 2019

A resistência dos materiais é feita pelo TMap PenetrationResistanceBySurfaceType que pode ser configurado no spawn ou como as propriedades padrões de um blueprint filho dessa classe.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment