Having trouble using a timeline in a UObject

Hi! I’ve been trying to create a small UObject to act as a template class for actions my actors can perform. Some of them are movement-based, so I have tried to add a timeline. Even though I have bound functions to its update and end, it doesn’t seem to work at all. None of its log messages appear, only those of the UObject’s tick.

At this point, I’m just messing around, trying different solutions, but I haven’t found an answer yet to this one. Is there a way for me to somehow bind the timeline’s tick to the UObject’s, or something of the sort?

CPP_ObjectAction.h

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

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Components/TimelineComponent.h"
#include "CPP_ObjectAction.generated.h"

/**
*
*/
UCLASS(BlueprintType, Blueprintable)
class TRPG_GAME_API UCPP_ObjectAction : public UObject, public FTickableGameObject
{
GENERATED_BODY()
public:
virtual void Tick(float DeltaTime) override;
virtual TStatId GetStatId() const override;
virtual ETickableTickType GetTickableTickType() const override;
virtual bool IsTickable() const override;
virtual bool IsTickableWhenPaused() const override;
virtual bool IsTickableInEditor() const override;
UFUNCTION(BlueprintCallable, Category = "Action")
void Initialize();
UFUNCTION(BlueprintCallable, Category = "Action")
virtual void StartAction();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Timeline")
class UTimelineComponent* Timeline;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Timeline")
class UCurveFloat\* FloatCurve;
protected:
UFUNCTION(BlueprintCallable, Category = "Action")
virtual void EndAction();
FOnTimelineEvent TimelineEnded;
UFUNCTION()
void TimelineEnd();
FOnTimelineEvent TimelineUpdated;
UFUNCTION()
void TimelineUpdate();

private:
FOnTimelineFloat TimelineFloat;
UCPP_ObjectAction();
uint32 LastFrame = INDEX_NONE;
bool CanTick;
};

CPP_ObjectAction.cpp

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


#include "CPP_ObjectAction.h"
#include "CPP_GridObject.h"
#include "Curves/CurveFloat.h"

UCPP_ObjectAction::UCPP_ObjectAction()
{
	CanTick = false;
	Timeline = CreateDefaultSubobject<UTimelineComponent>(TEXT("TimelineComponent"));
	if (Timeline && FloatCurve)
	{
		Timeline->AddInterpFloat(FloatCurve, TimelineFloat, TEXT("Alpha"), TEXT("Alpha"));
		Timeline->SetPlayRate(1.0f);
		TimelineEnded.BindUFunction(this, FName("TimelineEnd"));
		Timeline->SetTimelineFinishedFunc(TimelineEnded);
		TimelineUpdated.BindUFunction(this, FName("TimelineUpdate"));
		Timeline->SetTimelinePostUpdateFunc(TimelineUpdated);
		Timeline->PrimaryComponentTick.bCanEverTick = true;
		Timeline->SetComponentTickEnabled(true);
		Timeline->RegisterComponent();
	}
	return;
	//Initialize();
}

void UCPP_ObjectAction::Tick(float DeltaTime)
{
	if (LastFrame == GFrameCounter) return;
	UE_LOG(LogTemp, Warning, TEXT("OBJECT TICK!"));
        // This does show up, it shows the UObject's ticks do indeed work.
	LastFrame = GFrameCounter;
}

TStatId UCPP_ObjectAction::GetStatId() const
{
	RETURN_QUICK_DECLARE_CYCLE_STAT(UCPP_ObjectAction, STATGROUP_Tickables);
}

ETickableTickType UCPP_ObjectAction::GetTickableTickType() const
{
	return ETickableTickType();
}

bool UCPP_ObjectAction::IsTickable() const
{
	return CanTick;
}

bool UCPP_ObjectAction::IsTickableWhenPaused() const
{
	return false;
}

bool UCPP_ObjectAction::IsTickableInEditor() const
{
	return false;
}

void UCPP_ObjectAction::Initialize()
{
}

void UCPP_ObjectAction::StartAction()
{
	UE_LOG(LogTemp, Warning, TEXT("We're starting!"));
        // This does show up, it marks the start of the ticks.
	CanTick = true;
	Timeline->PlayFromStart();
	return;
}

void UCPP_ObjectAction::EndAction()
{
	return;
}

void UCPP_ObjectAction::TimelineEnd()
{
	CanTick = false;
	UE_LOG(LogTemp, Warning, TEXT("This happened right at the end!"));
        // This doesn't show up, the end of the timeline is busted.
}

void UCPP_ObjectAction::TimelineUpdate()
{
	UE_LOG(LogTemp, Warning, TEXT("This happens every tick, yes?"));
        // This doesn't show up, the update of the timeline is busted.
}

Timeline is an actor component, and when you create it as a default subobject in an actor, the actor is responsible for registering and activating it

When you create it like this outside of an actor, you have to register it yourself using RegisterComponentWithWorld(), which also registers its tick function

Sorry for the late response. I added it to the bottom of the constructor, but the the timeline still won’t budge.

UCPP_ObjectAction::UCPP_ObjectAction()
{
	CanTick = false;
	Timeline = CreateDefaultSubobject<UTimelineComponent>(TEXT("TimelineComponent"));
	if (Timeline && FloatCurve)
	{
		Timeline->AddInterpFloat(FloatCurve, TimelineFloat, TEXT("Alpha"), TEXT("Alpha"));
		Timeline->SetPlayRate(1.0f);
		TimelineEnded.BindUFunction(this, FName("TimelineEnd"));
		Timeline->SetTimelineFinishedFunc(TimelineEnded);
		TimelineUpdated.BindUFunction(this, FName("TimelineUpdate"));
		Timeline->SetTimelinePostUpdateFunc(TimelineUpdated);
		Timeline->PrimaryComponentTick.bCanEverTick = true;
		Timeline->PrimaryComponentTick.SetTickFunctionEnable(true);

        /*Added this for good measure.*/

		Timeline->SetComponentTickEnabled(true);

        /*Added the func right down here!*/

		Timeline->RegisterComponentWithWorld(GetWorld());
	}
	return;
}

Edit: Should I maybe switch to FTimeline instead? The TickTimeline() function seems to be exactly what I’m looking for here, but I’m unsure as to how I can implement it.

UObject’s GetWorld returns the outer’s GetWorld by default. If you are not creating the object with a valid outer, then it uses the Transient Package which shouldn’t have a world

I wouldn’t call register on the constructor, but rather in Initialize(). To be honest I’d probably do all the delegate bindings in Initialize too, and maybe even create the timeline component in there using NewObject. That’s how the engine does it in places outside of actors. You’d probably have to extend initialize to pass in a valid object, or use the outer object if the Action is created inside the actor (with it as the outer). If the outer is the actor then GetWorld will also get the world from it by default

But from a design perspective as well, I would guess that the actor actions should have some context of who is performing them, so they should have at least a reference to the actor

You can take a lot of inspiration from UGameplayTask, and in extension UAbilityTask, which are very similar to what you want to do

In fact you might be able to just use Gameplay Tasks, but sadly they cannot be created in blueprint, if that’s something you want

WOOO!! It worked, thank you so much! Moving it all to Initialize() and switching to NewObject<>() sealed the deal!

I even got to remove the FTickableGameObject part!

For posterity’s sake, here are the .h and .cpp:

CPP_ObjectAction.h

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

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Components/TimelineComponent.h"
#include "CPP_ObjectAction.generated.h"

/**
 * 
 */
UCLASS(BlueprintType, Blueprintable)
class TRPG_GAME_API UCPP_ObjectAction : public UObject
{
	GENERATED_BODY()
public:
	UFUNCTION(BlueprintCallable, Category = "Action")
	void Initialize();
	UFUNCTION(BlueprintCallable, Category = "Action")
	virtual void StartAction();
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Timeline")
	class UTimelineComponent* Timeline;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Timeline")
	class UCurveFloat* FloatCurve;
protected:
	UFUNCTION(BlueprintCallable, Category = "Action")
	virtual void EndAction();
	FOnTimelineEvent TimelineEnded;
	UFUNCTION()
	void TimelineEnd();
	FOnTimelineEvent TimelineUpdated;
	UFUNCTION()
	void TimelineUpdate();

private:
	UPROPERTY()
	FOnTimelineFloat TimelineFloat;
	UCPP_ObjectAction();
};

CPP_ObjectAction.cpp

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


#include "CPP_ObjectAction.h"
#include "CPP_GridObject.h"
#include "Curves/CurveFloat.h"

UCPP_ObjectAction::UCPP_ObjectAction()
{
}

void UCPP_ObjectAction::Initialize()
{
	Timeline = NewObject<UTimelineComponent>(this);
	if (Timeline && FloatCurve)
	{
		Timeline->AddInterpFloat(FloatCurve, TimelineFloat, TEXT("Alpha"), TEXT("Alpha"));
		Timeline->SetPlayRate(1.0f);
		Timeline->PrimaryComponentTick.bCanEverTick = true;
		Timeline->PrimaryComponentTick.SetTickFunctionEnable(true);
		Timeline->SetComponentTickEnabled(true);
		TimelineEnded.BindUFunction(this, FName("TimelineEnd"));
		Timeline->SetTimelineFinishedFunc(TimelineEnded);
		TimelineUpdated.BindUFunction(this, FName("TimelineUpdate"));
		Timeline->SetTimelinePostUpdateFunc(TimelineUpdated);
		Timeline->RegisterComponentWithWorld(GetWorld());
	}
	return;
}

void UCPP_ObjectAction::StartAction()
{
	UE_LOG(LogTemp, Warning, TEXT("We're starting!"));

	Timeline->PlayFromStart();
	return;
}

void UCPP_ObjectAction::EndAction()
{
	return;
}

void UCPP_ObjectAction::TimelineEnd()
{
	UE_LOG(LogTemp, Warning, TEXT("This happened right at the end!"));
}

void UCPP_ObjectAction::TimelineUpdate()
{
	UE_LOG(LogTemp, Warning, TEXT("This happens every tick, yes?"));
}

Again, I can’t thank you enough!