top of page

The Freeze Time System

  • Mike
  • Jul 5
  • 3 min read

Updated: 10 hours ago


The Freeze Time ability was implemented because playtesters requesting a way to "pause" while thinking about a solution. I wasn't a big fan of the idea as it takes away the platformer aspect of the game.


The compromise I came up with was that it should be okay to pause the game if you are not possessing a character, but when you actually have possessed a character, now you are in a different state of the game, an unpausable state.


The Technical Aspect


Unreal Engine has a simple way of "pausing" the game, and that is putting the time dilation of the world to almost 0. That means that everything that uses the delta time gets its delta time multiplied by the time dilations value.


If time dilation is 1 (normal speed), delta time behaves normally, but if delta time is almost 0, now everything basically comes to an halt.


This approach has its cons, mainly, the rendering pipeline seems to also be affected by this. So if we put the time dilation to 0, the visuals will get some weird artifacts, not to mention that the player wouldn't be able to move around as the player controller is also affected by this.


To solve this, we can simply utilize an Actors own custom time dilation, meaning, each actor can set its own time dilation which is not the same as the worlds time dilation.


TimeDilationComponent


UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class TIMEDILATIONSYSTEM_API UTimeDilationComponent : public UActorComponent
{
	GENERATED_BODY()

public:
	UTimeDilationComponent();

	void UpdateTimeDilation(const float NewTimeDilation, const ETimeDilation InTimeDilation) const;

	UPROPERTY(BlueprintAssignable)
	FOnTimeDilationCallbackChange_Signature OnTimeDilationChange;
};

The TimeDilationComponent is only the birde between the actual in-game time dilation and an actor.



void UTimeDilationComponent::BeginPlay()
{
	Super::BeginPlay();

	if (UTimeDilationSubsystem* Subsystem = UWorld::GetSubsystem<UTimeDilationSubsystem>(GetWorld()))
	{
		Subsystem->RegisterTimeDilationComponent(this);
	}
}

// And the EndPlay unregisters the component

void UTimeDilationComponent::UpdateTimeDilation(const float NewTimeDilation, const ETimeDilation InTimeDilation) const
{
	AActor* Owner = GetOwner();
	Owner->CustomTimeDilation = NewTimeDilation;

	if (OnTimeDilationChange.IsBound())
	{
		OnTimeDilationChange.Broadcast(NewTimeDilation, InTimeDilation);
	}
}

The concept is simple, add a TimeDilationComponent to an actor, and the component will change the Actors custom time dilation when the player asks to pause the game. Any attached component to the actor will be affected by this, however, the callback that broadcasts the new timedilation exists for any component that might need additional data from the time dilation transition.


Time DIlation Subsystem


The actual time dilation state is held within a subsystem.


UCLASS()
class TIMEDILATIONSYSTEM_API UTimeDilationSubsystem : public UTickableWorldSubsystem
{
	GENERATED_BODY()

public:
	//~ Begin Tickable Interface
	virtual bool IsTickable() const override;
	virtual void Tick(const float DeltaTime) override;
	//~ End Tickable Interface

	void RegisterTimeDilationComponent(UTimeDilationComponent* InTimeDilationComponent);
	void UnregisterTimeDilationComponent(UTimeDilationComponent* InTimeDilationComponent);

	bool RequestTimeDilationChange(const ETimeDilation InNewTimeDilationType);

protected:	
	UPROPERTY(Transient)
	TObjectPtr<UTimeDilationSettings> TimeDilationSettings;

	ETimeDilation TimeDilation;
	float CurrentTimeDilationValue;
};

void UTimeDilationSubsystem::Tick(const float DeltaTime)
{
	Super::Tick(DeltaTime);

	ElapsedSeconds += DeltaTime;
	const float Alpha = CalculateAlpha();
	CurrentTimeDilationValue = CalculateTimeDilationValue(Alpha);

	BroadcastTimeDilation();
}

float UTimeDilationSubsystem::CalculateAlpha() const
{
	if (!IsValid(TimeDilationSettings))
	{
		return 1.0f;
	}

	const float AlphaRaw = UKismetMathLibrary::SafeDivide(ElapsedSeconds, TransitionDurationSeconds);
	return FMath::Clamp(AlphaRaw, 0.0f, 1.0f);
}

void UTimeDilationSubsystem::BroadcastTimeDilation()
{
	for (const UTimeDilationComponent* Component : TimeDilationComponents)
	{
		if (!IsValid(Component))
		{
			continue;
		}

		Component->UpdateTimeDilation(CurrentTimeDilationValue, TimeDilation);
	}

	OnTimeDilationChange.Broadcast(CurrentTimeDilationValue, TimeDilation);
}


The CalculateAlpha is to give the freeze time a smooth transition rather than a hard stop. This was to make the ability feel good when pressing the button. I wanted the same feeling when unfreezing time was pressed, meaning, the player should feel the time slowly being reset to normal rather than an instant continue.


This also made it a little bit more satifsying for the sound design as the different entities in the game slowly changes its pitch based on the TimeDilation multiplier value.


As other systems other than Actors depend on the time dilation system, I made a callback for them and also a way to get the current Time Dilation Multiplier and what state the freeze time system is in.


Final Thoughts


It's a small but effective system. The second question to this is who decides a request is valid or not, but in my head, the Time Dilation System doesn't care if the request is valid or not, it just do whatever it is told to do and is really good at it.


One last thing to note here is that the time dilation state exists only in the subsystem, so there is only one truth to what time dilation we should use.


This makes the system very robust.

 
 
 

Comments


bottom of page