Slate widget STextBlock does not refresh

After the migration from 4.27.2 to 5.0.1 the Slate rendering behavior changed to reduce the refresh.

The problem, a STextBlock never refreshed.
I have a STextBlock displaying the game time or the remaining count down.

const int32 TimeSlotIndex = 10;
void SKInGameMenuWidget::AddTimeSlot()
{
	TimeOverlay->AddSlot(TimeSlotIndex) // The time
		.HAlign(HAlign_Center)
		.VAlign(VAlign_Center)
		[
			SAssignNew(TimeBox, STextBlock)
			.Margin(FMargin(2.0f, 2.0f, 2.0f, 2.0f))
			.TextStyle(FKHUDStyles::Get(), "KSGMGame.Match.TimeTextStyle")
			.Text_Lambda([this] { return (secs > 0) ? ConvertTime(FText::GetEmpty(), FText::GetEmpty(), secs, false, (secs < 60) ? false : true, true) : FText::GetEmpty(); })
			.SimpleTextMode(true)
			.ForceVolatile(true) // Force the time to be updated every frame
		];
	prevSecs = 0;
} 

As I understood, to force the widget to display every frame you have to set ForceVolatile to true.

Even with ForceVolatile=true, the time isn’t displayed.

To solve the problem, I have to force the refresh each time the secs changes

void SKInGameMenuWidget::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
	// Do nothing if the match ended
	if (bMatchEnded)
		return;

	if (GameState && GameState->IsValidLowLevel() && CharState && CharState-IsValidLowLevel())
	{
		// Do we have to initialize the time overlay ?
		if (bInitialized == false && CharState->GetTeamNum() != IDX_INVALIDTEAM)
			Initialize();
		// Takes the seconds in function of the match state
		if (GameState->IsMatchInProgress())
		{
			secs = GameState->GetClockTime();
			if (bFinalCountDownStarted)
			{
				StopFinalCountDown();
			}
		}
		else
			if (GameState->IsMatchInCountdown())
			{
				secs = GameState->GetRemainingCountDownTime();
				if (secs > 0 && secs < 10 && bFinalCountDownStarted == false)
					StartFinalCountDown();
			}
		// Invalidate the time box each time the second changes
		if (secs != prevSecs)
		{
			if (TimeBox.IsValid())
				TimeBox->Invalidate(EInvalidateWidgetReason::Paint | EInvalidateWidgetReason::Volatility | EInvalidateWidgetReason::Prepass);
			prevSecs = secs;
		}
	}
}

If I specify only EInvalidateWidgetReason::Paint | EInvalidateWidgetReason::Volatility the time never refreshes.
I know the EInvalidateWidgetReason::Prepass has a big cost.

Then, I would like to know how to force the widget to refresh without having to call Invalidate(). I don’t understand what the flag ForceVolatile does.

For the final count down, below 10 seconds, the STextBlock is displayed correctly without having to force the invalidation.

void SKInGameMenuWidget::StartFinalCountDown()
{
	// First, remove the time slot
	RemoveTimeSlot();

	TimeOverlay->AddSlot(TimeSlotIndex) // The final count down time 9 - 0
		.HAlign(HAlign_Center)
		.VAlign(VAlign_Center)
		[
			SNew(SKTextBlockAnimated)
			.AnimationSeconds(1.0f)
			.ScaleXFactor(2.0f)
			.ScaleYFactor(2.0f)
			.Margin(FMargin(2.0f, 2.0f, 2.0f, 2.0f))
			.TextStyle(FKHUDStyles::Get(), "KSGMGame.Match.TimeTextStyle")
			.Text_Lambda([this] { return (secs > 0) ? FText::AsNumber(secs) : FText::GetEmpty(); })
		];
	bFinalCountDownStarted = true;
	prevSecs = 0;
}

The only difference with the SKTextBlockAnimated resides in the fact the text block is scaled on Tick().

void SKTextBlockAnimated::Construct(const FArguments& InArgs)
{
      ScaleXFactor = InArgs._ScaleXFactor;
	ScaleYFactor = InArgs._ScaleYFactor;
	// The curve used for animating the text block
	Curve = FCurveSequence(0.0f, InArgs._AnimationSeconds, InArgs._AnimationType);
	LoopMode = InArgs._LoopMode;
	TSharedPtr<SWidget> ScaleBox = SNew(SScaleBox)
		.Stretch(EStretch::ScaleToFit)
		[
			SAssignNew(TextBlock, STextBlock)
			.Text(InArgs._Text)
			.TextStyle(InArgs._TextStyle)
			.Font(InArgs._Font)
			.ColorAndOpacity(InArgs._ColorAndOpacity)
			.ShadowOffset(InArgs._ShadowOffset)
			.ShadowColorAndOpacity(InArgs._ShadowColorAndOpacity)
			.HighlightColor(InArgs._HighlightColor)
			.HighlightShape(InArgs._HighlightShape)
			.HighlightText(InArgs._HighlightText)
			.WrapTextAt(InArgs._WrapTextAt)
			.AutoWrapText(InArgs._AutoWrapText)
			.WrappingPolicy(InArgs._WrappingPolicy)
			.Margin(InArgs._Margin)
			.LineHeightPercentage(InArgs._LineHeightPercentage)
			.Justification(InArgs._Justification)
			.MinDesiredWidth(InArgs._MinDesiredWidth)
			.TextShapingMethod(InArgs._TextShapingMethod)
			.TextFlowDirection(InArgs._TextFlowDirection)
			.LineBreakPolicy(InArgs._LineBreakPolicy)
			.OnDoubleClicked(InArgs._OnDoubleClicked)
			// Sets a simple text mode, otherwise the text
			// won't be refreshed in real time in UE5
			.SimpleTextMode(true)
		];
	// Does the border resize with the animation
	if (InArgs._ProgressiveSizing)
	{
		SBorder::Construct(SBorder::FArguments()
			.ContentScale(this, &SKTextBlockAnimated::GetDesiredSizeScale)
			.DesiredSizeScale(this, &SKTextBlockAnimated::GetDesiredSizeScale)
			.BorderImage(nullptr)
			.HAlign(HAlign_Center)
			.VAlign(VAlign_Center)
			[
				ScaleBox.ToSharedRef()
			]
		);
	}
	else
	{
		SBorder::Construct(SBorder::FArguments()
			.ContentScale(this, &SKTextBlockAnimated::GetDesiredSizeScale)
			.DesiredSizeScale(FVector2D(ScaleXFactor.Get(), ScaleYFactor.Get()))
			.BorderImage(nullptr)
			.HAlign(HAlign_Center)
			.VAlign(VAlign_Center)
			[
				ScaleBox.ToSharedRef()
			]
		);
	}
	// If auto play, play the animation immediately
	if (InArgs._AutoPlay)
		PlayAnimation();
}

Play animation is simply playing a curve

void SKTextBlockAnimated::PlayAnimation()
{
	if (!Curve.IsPlaying())
	{
		Curve.Play(this->AsShared(), LoopMode);
	}
}
FVector2D SKTextBlockAnimated::GetDesiredSizeScale() const
{
	if (Curve.IsPlaying())
	{
		float Scale = Curve.GetLerp();
		float X = FMath::Lerp(1.0f, ScaleXFactor.Get(), Scale);
		float Y = FMath::Lerp(1.0f, ScaleYFactor.Get(), Scale);
		return FVector2D(X, Y);
	}
	return FVector2D(1.0f, 1.0f);
}

It might be due to the fact the widget resizes constantly and the SWidget detects the size changing and invalidate by itself.

I would like to know how you can, for some widgets displaying frequently changing information, avoid the widget to cache itself.

1 Like