UE4 Spline based Velocity Field Generator

Created a tool that read curve data of UE4 and create a velocity field that game object can follow with animation.

Originally, used for test in Riot Games.


C++, Unreal Engine 4

// Fill out your copyright notice in the Description page of Project Settings.

#include "MyProject.h"
#include "SplineMovementActor.h"

ASplineMovementActor::ASplineMovementActor(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
    , Time(2.0f)
{
    SceneComp = ObjectInitializer.CreateDefaultSubobject<USceneComponent>(this, TEXT("SceneComp"));
    RootComponent = SceneComp;

    SceneComp->Mobility = EComponentMobility::Movable;

    SplineComp = ObjectInitializer.CreateDefaultSubobject<USplineComponent>(this, TEXT("JumpPathSpline"));
    SplineComp->SetupAttachment(RootComponent);

    StaticMeshComp = ObjectInitializer.CreateDefaultSubobject<UStaticMeshComponent>(this, TEXT("StaticMeshComp"));
    StaticMeshComp->SetupAttachment(RootComponent);

    // Make sure all the spline comp to be absolute to world space not to this component
    SplineComp->bAbsoluteLocation = true;
    SplineComp->bAbsoluteRotation = true;
    SplineComp->bAbsoluteScale = true;

    SplinePointCount = 2;

    PlayedTime = 0.0f;

    HeadMeshScale = 1.0f;

    IsPlaying = false;

    bEndParticlePlayed = false;

    PrimaryActorTick.bCanEverTick = true;

    StartLocation = FVector::ZeroVector;
    EndLocation = FVector::ZeroVector;
    //SetActorTickEnabled(false);
    //RegisterAllActorTickFunctions(false, true);
}

void ASplineMovementActor::Tick(float DeltaSeconds)
{
    // Make sure tick only execute while its playing.
    if (!IsPlaying)
        return;

    MovementImplementation(DeltaSeconds);
}

#if WITH_EDITOR
void ASplineMovementActor::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
    Super::PostEditChangeProperty(PropertyChangedEvent);

    InitSpline(); // just to preview the updated spline
}
#endif

void ASplineMovementActor::StartMovement()
{
    // Make sure start only its not playing.
    if (IsPlaying)
        return;
    
    StaticMeshComp->SetStaticMesh(HeadMesh);
    StaticMeshComp->SetWorldScale3D(FVector(HeadMeshScale, HeadMeshScale, HeadMeshScale));

    InitSpline();

    if (bDelayAfterStartParticle) {
        GetWorld()->GetTimerManager().SetTimer(ToggleMovementHandle, this, &ASplineMovementActor::ActivateTrail, DelayAfterStartParticle, false);
    }
    else {
        ActivateTrail();
    }

    UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), StartParticle, StartLocation, FRotator::ZeroRotator, true);
}

void ASplineMovementActor::ActivateTrail()
{
    IsPlaying = true; // let the trail/spline animation actually start

    UGameplayStatics::SpawnEmitterAttached(TrailParticle, StaticMeshComp);
}

void ASplineMovementActor::MovementImplementation(float DeltaSeconds)
{
    if (HeadMesh && StaticMeshComp->StaticMesh)
    {
        PlayedTime += DeltaSeconds;

        FVector2D TimeRange = FVector2D(0.0f, Time);
        FVector2D TargetRange = FVector2D(0.0f, SplineLength);

        if (PlayedTime >= Time)
        {
            FinishSplineMovement();
        }
        else
        {
            if (EaseType == EEaseType::ET_EaseIn) 
            { 
                DistAtSpline = EaseIn(PlayedTime, 0.0f, SplineLength, Time); 
            }
            else if (EaseType == EEaseType::ET_EaseOut) 
            {
                DistAtSpline = EaseOut(PlayedTime, 0.0f, SplineLength, Time); 
            }
            else 
            {
             DistAtSpline = FMath::GetMappedRangeValueClamped(TimeRange, TargetRange, PlayedTime); 
            }

            FVector Location = SplineComp->GetWorldLocationAtDistanceAlongSpline(DistAtSpline);
            StaticMeshComp->SetWorldLocation(Location);
        }
    }

    if (PlayedTime > EndParticleTime && !bEndParticlePlayed)
    {
        UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), EndParticle, EndLocation, FRotator::ZeroRotator, true);
        bEndParticlePlayed = true;
    }
}

void ASplineMovementActor::InitSpline()
{
    SetCustomSpline();

    SplineComp->UpdateSpline();
    SplineLength = SplineComp->GetSplineLength();
}

void ASplineMovementActor::InitSplinePoints(int32 PointCount)
{
    if (SplinePoints.Num() > 0)
    {
        SplinePoints.Empty();
    }

    SplineComp->ClearSplinePoints();

    for (int32 i = 0; i < PointCount; ++i)
    {
        SplinePoints.Add(FVector::ZeroVector);
        SplineComp->AddSplineLocalPoint(SplinePoints[i]);
    }
}

void ASplineMovementActor::SetCustomSpline()
{
    // Look for USplineComp that has name "SplineMesh"
    USplineComponent* CustomSplineComp = NULL;

    if (!CustomSpline)
        return;

    USceneComponent* CustomSceneComp = CustomSpline->GetRootComponent();
    for (int32 i = 0; i < CustomSceneComp->GetNumChildrenComponents(); ++i)
    {
        if (CustomSceneComp->GetChildComponent(i))
        {
            if (CustomSceneComp->GetChildComponent(i)->GetName() == "SplineMesh")
            {
                CustomSplineComp = Cast<USplineComponent>(CustomSceneComp->GetChildComponent(i));
            }
        }
    }

    // Make sure the custom spline comp is there and its points to be bigger than 1 (line need at least 2 points)
    if (CustomSplineComp && CustomSplineComp->GetNumberOfSplinePoints() > 1)
    {
        InitSplinePoints(CustomSplineComp->GetNumberOfSplinePoints());

        TArray<FVector> NewSplinePoints;
        NewSplinePoints.Reserve(CustomSplineComp->GetNumberOfSplinePoints());

        for (int32 i = 0; i < CustomSplineComp->GetNumberOfSplinePoints(); ++i)
        {
            // For the start point, make sure it starts from the right position for FX
            if (i == 0)
            {
                NewSplinePoints.Add(CustomSplineComp->GetWorldLocationAtSplinePoint(i));
                StartLocation = CustomSplineComp->GetWorldLocationAtSplinePoint(i);
                /*
                FVector Offset = FVector::ZeroVector;
                CalculateOffset(TargetCharacter, Offset);
                FVector StartLocation = GetActorLocation() + Offset;
                NewSplinePoints.Add(StartLocation);
                */
            }
            // For the end point, make sure it gets stored for FX
            else if (i == CustomSplineComp->GetNumberOfSplinePoints() - 1)
            {
                NewSplinePoints.Add(CustomSplineComp->GetWorldLocationAtSplinePoint(i));
                EndLocation = CustomSplineComp->GetWorldLocationAtSplinePoint(i);
            }
            else
            {
                NewSplinePoints.Add(CustomSplineComp->GetWorldLocationAtSplinePoint(i));
            }
        }
        SplineComp->SetSplineLocalPoints(NewSplinePoints);
    }
}

void ASplineMovementActor::FinishSplineMovement()
{
    // Things to do when spline movement is done
    PlayedTime = 0.0f;
    IsPlaying = false;
    bEndParticlePlayed = false;

    // play sequential fx
    if (bPlaySeqFX) {
        GetWorld()->GetTimerManager().SetTimer(ToggleSeqFX, this, &ASplineMovementActor::RunSeq, DelaySeqFXTime, false);
    }
}

void ASplineMovementActor::RunSeq()
{
    TArray<ASplineMovementActor*> SplineMovementActors;

    for (TActorIterator<ASplineMovementActor> Itr(GetWorld()); Itr; ++Itr)
    {
        SplineMovementActors.Push(*Itr);
    }

    int NextID = ID + 1;
    for (int i = 0; i < SplineMovementActors.Num(); i++)
    {
        if (SplineMovementActors[i]->ID == NextID)
        {
            SplineMovementActors[i]->StartMovement();
        }
    }
}

/*
// Sets default values
ASplineMovementActor::ASplineMovementActor()
{
   // 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;

}
*/

// Called when the game starts or when spawned
void ASplineMovementActor::BeginPlay()
{
    Super::BeginPlay();
    
}

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "GameFramework/Actor.h"
#include "ParticleDefinitions.h"
//#include "Runtime/Engine/Classes/Particles/ParticleSystem.h"
#include "Components/SplineComponent.h"
#include "SplineMovementActor.generated.h"

UENUM(BlueprintType)
enum class EEaseType : uint8 
{
    ET_EaseIn   UMETA(DisplayName = "EaseIn"),
    ET_EaseOut  UMETA(DisplayName = "EaseOut"),
    ET_None     UMETA(DisplayName = "None")
};

UCLASS()
class MYPROJECT_API ASplineMovementActor : public AActor
{
    GENERATED_UCLASS_BODY()

public:

    UPROPERTY()
    USceneComponent* SceneComp;

    UPROPERTY()
    USplineComponent* SplineComp;

    UPROPERTY()
    UStaticMeshComponent* StaticMeshComp;

    // General 
    UPROPERTY(EditAnywhere, Category = "General")
    uint32 ID;

    // Spline Movement
    UPROPERTY(EditAnywhere, Category = "SplineMovement")
    AActor* CustomSpline;

    // How long the spline movement will take
    UPROPERTY(EditAnywhere, Category = "SplineMovement")
    float Time;

    // Spline Animation type
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SplineMovement")
    EEaseType EaseType;

    // Are we going to delay after the first particle activation
    UPROPERTY(EditAnywhere, Category = "SplineMovement")
    bool bDelayAfterStartParticle;

    // How long the spiline movement will be delayed after the first particle activation
    UPROPERTY(EditAnywhere, Category = "SplineMovement")
    float DelayAfterStartParticle;

    // trail effects moving object
    UPROPERTY(EditAnywhere, Category = "Effects")
    UStaticMesh* HeadMesh;

    // scale of the moving object
    UPROPERTY(EditAnywhere, Category = "Effects")
    float HeadMeshScale;

    // start activation particle system
    UPROPERTY(EditAnywhere, Category = "Effects")
    UParticleSystem* StartParticle;

    // trail particle system which will be attached to the moving object
    UPROPERTY(EditAnywhere, Category = "Effects")
    UParticleSystem* TrailParticle;

    // particle system for end of the spline movement
    UPROPERTY(EditAnywhere, Category = "Effects")
    UParticleSystem* EndParticle;

    // time offset for trigger time to the end of the spline movement
    UPROPERTY(EditAnywhere, Category = "Effects")
    float EndParticleTime;

    // Are we going to play another spline related fx after this spline movement
    UPROPERTY(EditAnywhere, Category = "SeqFX")
    bool bPlaySeqFX;

    // If so, how many time later?
    UPROPERTY(EditAnywhere, Category = "SeqFX")
    float DelaySeqFXTime;

    // This func could be used in BP so having it callable.
    UFUNCTION(BlueprintCallable, Category = "SplineMovement")
    virtual void StartMovement();

    // Sets default values for this actor's properties
    ASplineMovementActor();

    // Called when the game starts or when spawned
    virtual void BeginPlay() override;
    
    // Called every frame
    virtual void Tick( float DeltaSeconds ) override;

    virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;

protected:
    bool IsPlaying;
    bool bEndParticlePlayed;

    int32 SplinePointCount;

    float DistAtSpline;
    float SplineLength;
    float PlayedTime;

    FVector StartLocation;
    FVector EndLocation;

    TArray<FVector> SplinePoints;

    void InitSpline();

    // Custom generated spline
    virtual void SetCustomSpline();

    void ActivateTrail();

    virtual void MovementImplementation(float DeltaSeconds);

    virtual void FinishSplineMovement();

    void InitSplinePoints(int32 PointCount);

    virtual void RunSeq();
    
    /*
    UFUNCTION(Reliable, Server, WithValidation)
    virtual void ServerSplineMovement(AShooterCharacter* InCharacter);

    UFUNCTION(Reliable, netmulticast, WithValidation)
    virtual void MulticastSplineMovement(AShooterCharacter* InCharacter);
    */

private:
    FTimerHandle ToggleMovementHandle;
    FTimerHandle ToggleSeqFX;
    
    // This ease functions have to be separated into Math class but for this test it will be part of this class
    /*
        @t - current time of the tween
        @b - beginning of the value
        @c - destination of the value
        @d - total time of the tween
    */
    float EaseOut(float t, float b, float c, float d) {
        float diff = c - b; // destination value - beginning value = diff of start and end
        return diff * ((t = t / d - 1)*t*t + 1) + b;
    }

    /*
    @t - current time of the tween
    @b - beginning of the value
    @c - destination of the value
    @d - total time of the tween
    */
    float EaseIn(float t, float b, float c, float d) {
        float diff = c - b; // destination value - beginning value = diff of start and end
        return diff * (t /= d)*t*t + b;
    }
};