Last active
June 19, 2024 20:41
-
-
Save zr0n/b5622662e30daaa2198ac1a1b8fa61f3 to your computer and use it in GitHub Desktop.
Custom projectile movement, wall penetration and damage using Unreal Engine 4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.