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.