mirror of
https://github.com/sinbad/StevesUEHelpers.git
synced 2025-02-23 09:35:25 +00:00
Add typewriter text widget
This commit is contained in:
parent
047d90d66b
commit
e810f2509e
BIN
Resources/typewriterexample.gif
Normal file
BIN
Resources/typewriterexample.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.5 MiB |
314
Source/StevesUEHelpers/Private/StevesUI/TypewriterTextWidget.cpp
Normal file
314
Source/StevesUEHelpers/Private/StevesUI/TypewriterTextWidget.cpp
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
// Original Copyright (c) Sam Bloomberg https://github.com/redxdev/UnrealRichTextDialogueBox (MIT License)
|
||||||
|
|
||||||
|
#include "StevesUI/TypewriterTextWidget.h"
|
||||||
|
#include "Engine/Font.h"
|
||||||
|
#include "Styling/SlateStyle.h"
|
||||||
|
#include "Widgets/Text/SRichTextBlock.h"
|
||||||
|
|
||||||
|
//PRAGMA_DISABLE_OPTIMIZATION
|
||||||
|
|
||||||
|
TSharedRef<SWidget> URichTextBlockForTypewriter::RebuildWidget()
|
||||||
|
{
|
||||||
|
// Copied from URichTextBlock::RebuildWidget
|
||||||
|
UpdateStyleData();
|
||||||
|
|
||||||
|
TArray< TSharedRef< class ITextDecorator > > CreatedDecorators;
|
||||||
|
CreateDecorators(CreatedDecorators);
|
||||||
|
|
||||||
|
TextMarshaller = FRichTextLayoutMarshaller::Create(CreateMarkupParser(), CreateMarkupWriter(), CreatedDecorators, StyleInstance.Get());
|
||||||
|
|
||||||
|
MyRichTextBlock =
|
||||||
|
SNew(SRichTextBlock)
|
||||||
|
.TextStyle(bOverrideDefaultStyle ? &DefaultTextStyleOverride : &DefaultTextStyle)
|
||||||
|
.Marshaller(TextMarshaller)
|
||||||
|
.CreateSlateTextLayout(
|
||||||
|
FCreateSlateTextLayout::CreateWeakLambda(this, [this] (SWidget* InOwner, const FTextBlockStyle& InDefaultTextStyle) mutable
|
||||||
|
{
|
||||||
|
TextLayout = FSlateTextLayout::Create(InOwner, InDefaultTextStyle);
|
||||||
|
return StaticCastSharedPtr<FSlateTextLayout>(TextLayout).ToSharedRef();
|
||||||
|
}));
|
||||||
|
|
||||||
|
return MyRichTextBlock.ToSharedRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
UTypewriterTextWidget::UTypewriterTextWidget(const FObjectInitializer& ObjectInitializer)
|
||||||
|
: Super(ObjectInitializer)
|
||||||
|
{
|
||||||
|
bHasFinishedPlaying = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UTypewriterTextWidget::SetText(const FText& InText)
|
||||||
|
{
|
||||||
|
if (IsValid(LineText))
|
||||||
|
{
|
||||||
|
FTimerManager& TimerManager = GetWorld()->GetTimerManager();
|
||||||
|
TimerManager.ClearTimer(LetterTimer);
|
||||||
|
|
||||||
|
LineText->SetText(InText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FText UTypewriterTextWidget::GetText() const
|
||||||
|
{
|
||||||
|
if (IsValid(LineText))
|
||||||
|
{
|
||||||
|
return LineText->GetText();
|
||||||
|
}
|
||||||
|
|
||||||
|
return FText();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UTypewriterTextWidget::PlayLine(const FText& InLine)
|
||||||
|
{
|
||||||
|
check(GetWorld());
|
||||||
|
|
||||||
|
FTimerManager& TimerManager = GetWorld()->GetTimerManager();
|
||||||
|
TimerManager.ClearTimer(LetterTimer);
|
||||||
|
|
||||||
|
CurrentLine = InLine;
|
||||||
|
CurrentLetterIndex = 0;
|
||||||
|
CachedLetterIndex = 0;
|
||||||
|
CurrentSegmentIndex = 0;
|
||||||
|
MaxLetterIndex = 0;
|
||||||
|
NumberOfLines = 0;
|
||||||
|
CombinedTextHeight = 0;
|
||||||
|
Segments.Empty();
|
||||||
|
CachedSegmentText.Empty();
|
||||||
|
|
||||||
|
if (CurrentLine.IsEmpty())
|
||||||
|
{
|
||||||
|
if (IsValid(LineText))
|
||||||
|
{
|
||||||
|
LineText->SetText(FText::GetEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
bHasFinishedPlaying = true;
|
||||||
|
OnTypewriterLineFinished.Broadcast(this);
|
||||||
|
OnLineFinishedPlaying();
|
||||||
|
|
||||||
|
SetVisibility(ESlateVisibility::Hidden);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (IsValid(LineText))
|
||||||
|
{
|
||||||
|
LineText->SetText(FText::GetEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
bHasFinishedPlaying = false;
|
||||||
|
|
||||||
|
FTimerDelegate Delegate;
|
||||||
|
Delegate.BindUObject(this, &ThisClass::PlayNextLetter);
|
||||||
|
|
||||||
|
TimerManager.SetTimer(LetterTimer, Delegate, LetterPlayTime, true);
|
||||||
|
|
||||||
|
SetVisibility(ESlateVisibility::SelfHitTestInvisible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UTypewriterTextWidget::SkipToLineEnd()
|
||||||
|
{
|
||||||
|
FTimerManager& TimerManager = GetWorld()->GetTimerManager();
|
||||||
|
TimerManager.ClearTimer(LetterTimer);
|
||||||
|
|
||||||
|
CurrentLetterIndex = MaxLetterIndex - 1;
|
||||||
|
if (IsValid(LineText))
|
||||||
|
{
|
||||||
|
LineText->SetText(FText::FromString(CalculateSegments()));
|
||||||
|
}
|
||||||
|
|
||||||
|
bHasFinishedPlaying = true;
|
||||||
|
OnTypewriterLineFinished.Broadcast(this);
|
||||||
|
OnLineFinishedPlaying();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UTypewriterTextWidget::PlayNextLetter()
|
||||||
|
{
|
||||||
|
if (Segments.Num() == 0)
|
||||||
|
{
|
||||||
|
CalculateWrappedString();
|
||||||
|
}
|
||||||
|
|
||||||
|
FString WrappedString = CalculateSegments();
|
||||||
|
|
||||||
|
// TODO: How do we keep indexing of text i18n-friendly?
|
||||||
|
if (CurrentLetterIndex < MaxLetterIndex)
|
||||||
|
{
|
||||||
|
if (IsValid(LineText))
|
||||||
|
{
|
||||||
|
LineText->SetText(FText::FromString(WrappedString));
|
||||||
|
}
|
||||||
|
|
||||||
|
OnPlayLetter();
|
||||||
|
++CurrentLetterIndex;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (IsValid(LineText))
|
||||||
|
{
|
||||||
|
LineText->SetText(FText::FromString(CalculateSegments()));
|
||||||
|
}
|
||||||
|
|
||||||
|
FTimerManager& TimerManager = GetWorld()->GetTimerManager();
|
||||||
|
TimerManager.ClearTimer(LetterTimer);
|
||||||
|
|
||||||
|
FTimerDelegate Delegate;
|
||||||
|
Delegate.BindUObject(this, &ThisClass::SkipToLineEnd);
|
||||||
|
|
||||||
|
TimerManager.SetTimer(LetterTimer, Delegate, EndHoldTime, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UTypewriterTextWidget::CalculateWrappedString()
|
||||||
|
{
|
||||||
|
// Rich Text views give you:
|
||||||
|
// - A blank block at the start for some reason
|
||||||
|
// - One block per line (newline characters stripped)
|
||||||
|
// - Split for different runs (decorators)
|
||||||
|
// - The newlines we add are the only newlines in the output so that's the number of lines
|
||||||
|
// If we've got here, that means the text isn't empty so 1 line at least
|
||||||
|
NumberOfLines = 1;
|
||||||
|
if (IsValid(LineText) && LineText->GetTextLayout().IsValid())
|
||||||
|
{
|
||||||
|
TSharedPtr<FSlateTextLayout> Layout = LineText->GetTextLayout();
|
||||||
|
TSharedPtr<FRichTextLayoutMarshaller> Marshaller = LineText->GetTextMarshaller();
|
||||||
|
|
||||||
|
const FGeometry& TextBoxGeometry = LineText->GetCachedGeometry();
|
||||||
|
const FVector2D TextBoxSize = TextBoxGeometry.GetLocalSize();
|
||||||
|
|
||||||
|
Layout->SetWrappingWidth(TextBoxSize.X);
|
||||||
|
Marshaller->SetText(CurrentLine.ToString(), *Layout.Get());
|
||||||
|
Layout->UpdateIfNeeded();
|
||||||
|
|
||||||
|
bool bHasWrittenText = false;
|
||||||
|
auto Views = Layout->GetLineViews();
|
||||||
|
for (int v = 0; v < Views.Num(); ++v)
|
||||||
|
{
|
||||||
|
const FTextLayout::FLineView& View = Views[v];
|
||||||
|
|
||||||
|
for (int b = 0; b < View.Blocks.Num(); ++b)
|
||||||
|
{
|
||||||
|
TSharedRef<ILayoutBlock> Block = View.Blocks[b];
|
||||||
|
TSharedRef<IRun> Run = Block->GetRun();
|
||||||
|
|
||||||
|
FTypewriterTextSegment Segment;
|
||||||
|
Run->AppendTextTo(Segment.Text, Block->GetTextRange());
|
||||||
|
|
||||||
|
// HACK: For some reason image decorators (and possibly other decorators that don't
|
||||||
|
// have actual text inside them) result in the run containing a zero width space instead of
|
||||||
|
// nothing. This messes up our checks for whether the text is empty or not, which doesn't
|
||||||
|
// have an effect on image decorators but might cause issues for other custom ones.
|
||||||
|
// Instead of emptying text, which might have some unknown effect, just mark it as empty
|
||||||
|
const bool bTextIsEmpty = Segment.Text.IsEmpty() ||
|
||||||
|
(Segment.Text.Len() == 1 && Segment.Text[0] == 0x200B);
|
||||||
|
const int TextLen = bTextIsEmpty ? 0 : Segment.Text.Len();
|
||||||
|
const bool bRunIsEmpty = Segment.RunInfo.Name.IsEmpty();
|
||||||
|
|
||||||
|
Segment.RunInfo = Run->GetRunInfo();
|
||||||
|
Segments.Add(Segment);
|
||||||
|
|
||||||
|
// A segment with a named run should still take up time for the typewriter effect.
|
||||||
|
MaxLetterIndex += FMath::Max(TextLen, Segment.RunInfo.Name.IsEmpty() ? 0 : 1);
|
||||||
|
|
||||||
|
if (!bTextIsEmpty || !bRunIsEmpty)
|
||||||
|
{
|
||||||
|
bHasWrittenText = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bHasWrittenText)
|
||||||
|
{
|
||||||
|
CombinedTextHeight += View.TextHeight;
|
||||||
|
}
|
||||||
|
// Add check for an unnecessary newline after ever line even if there's nothing else to do, otherwise
|
||||||
|
// we end up inserting a newline after a simple single line of text
|
||||||
|
const bool bHasMoreText = v < (Views.Num() - 1);
|
||||||
|
if (bHasWrittenText && bHasMoreText)
|
||||||
|
{
|
||||||
|
Segments.Add(FTypewriterTextSegment{TEXT("\n")});
|
||||||
|
++NumberOfLines;
|
||||||
|
++MaxLetterIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout->SetWrappingWidth(0);
|
||||||
|
|
||||||
|
// Set the desired vertical size so that we're already the size we need to accommodate all lines
|
||||||
|
// Without this, a flexible box size will grow as lines are added
|
||||||
|
FVector2D Sz = GetMinimumDesiredSize();
|
||||||
|
Sz.Y = CombinedTextHeight;
|
||||||
|
SetMinimumDesiredSize(Sz);
|
||||||
|
|
||||||
|
LineText->SetText(LineText->GetText());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Segments.Add(FTypewriterTextSegment{CurrentLine.ToString()});
|
||||||
|
MaxLetterIndex = Segments[0].Text.Len();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
FString UTypewriterTextWidget::CalculateSegments()
|
||||||
|
{
|
||||||
|
FString Result = CachedSegmentText;
|
||||||
|
|
||||||
|
int32 Idx = CachedLetterIndex;
|
||||||
|
while (Idx <= CurrentLetterIndex && CurrentSegmentIndex < Segments.Num())
|
||||||
|
{
|
||||||
|
const FTypewriterTextSegment& Segment = Segments[CurrentSegmentIndex];
|
||||||
|
if (!Segment.RunInfo.Name.IsEmpty())
|
||||||
|
{
|
||||||
|
Result += FString::Printf(TEXT("<%s"), *Segment.RunInfo.Name);
|
||||||
|
|
||||||
|
if (Segment.RunInfo.MetaData.Num() > 0)
|
||||||
|
{
|
||||||
|
for (const TTuple<FString, FString>& MetaData : Segment.RunInfo.MetaData)
|
||||||
|
{
|
||||||
|
Result += FString::Printf(TEXT(" %s=\"%s\""), *MetaData.Key, *MetaData.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Segment.Text.IsEmpty())
|
||||||
|
{
|
||||||
|
Result += TEXT("/>");
|
||||||
|
++Idx; // This still takes up an index for the typewriter effect.
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Result += TEXT(">");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bIsSegmentComplete = true;
|
||||||
|
if (!Segment.Text.IsEmpty())
|
||||||
|
{
|
||||||
|
int32 LettersLeft = CurrentLetterIndex - Idx + 1;
|
||||||
|
bIsSegmentComplete = LettersLeft >= Segment.Text.Len();
|
||||||
|
LettersLeft = FMath::Min(LettersLeft, Segment.Text.Len());
|
||||||
|
Idx += LettersLeft;
|
||||||
|
|
||||||
|
Result += Segment.Text.Mid(0, LettersLeft);
|
||||||
|
|
||||||
|
if (!Segment.RunInfo.Name.IsEmpty())
|
||||||
|
{
|
||||||
|
Result += TEXT("</>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bIsSegmentComplete)
|
||||||
|
{
|
||||||
|
CachedLetterIndex = Idx;
|
||||||
|
CachedSegmentText = Result;
|
||||||
|
++CurrentSegmentIndex;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
//PRAGMA_ENABLE_OPTIMIZATION
|
131
Source/StevesUEHelpers/Public/StevesUI/TypewriterTextWidget.h
Normal file
131
Source/StevesUEHelpers/Public/StevesUI/TypewriterTextWidget.h
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// Original Copyright (c) Sam Bloomberg https://github.com/redxdev/UnrealRichTextDialogueBox (MIT License)
|
||||||
|
// Updates:
|
||||||
|
// 1. Fixed adding a spurious newline to single-line text
|
||||||
|
// 2. Expose line finished as event
|
||||||
|
// 3. Changed names of classes to indicate functionality better (typewriter not dialogue)
|
||||||
|
// 4. Update minimum desired size on calculate so that flexi boxes can start at the correct size
|
||||||
|
// instead of growing when the newline is added
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Blueprint/UserWidget.h"
|
||||||
|
#include "Components/RichTextBlock.h"
|
||||||
|
#include "Framework/Text/RichTextLayoutMarshaller.h"
|
||||||
|
#include "Framework/Text/SlateTextLayout.h"
|
||||||
|
#include "TypewriterTextWidget.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnTypewriterLineFinished, class UTypewriterTextWidget*, Widget);
|
||||||
|
/**
|
||||||
|
* A text block that exposes more information about text layout for typewriter widget.
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class URichTextBlockForTypewriter : public URichTextBlock
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
FORCEINLINE TSharedPtr<FSlateTextLayout> GetTextLayout() const
|
||||||
|
{
|
||||||
|
return TextLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
FORCEINLINE TSharedPtr<FRichTextLayoutMarshaller> GetTextMarshaller() const
|
||||||
|
{
|
||||||
|
return TextMarshaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual TSharedRef<SWidget> RebuildWidget() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
TSharedPtr<FSlateTextLayout> TextLayout;
|
||||||
|
TSharedPtr<FRichTextLayoutMarshaller> TextMarshaller;
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS(Blueprintable)
|
||||||
|
class STEVESUEHELPERS_API UTypewriterTextWidget : public UUserWidget
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UTypewriterTextWidget(const FObjectInitializer& ObjectInitializer);
|
||||||
|
|
||||||
|
/// Event called when a line has finished playing, whether on its own or when skipped to end
|
||||||
|
UPROPERTY(BlueprintAssignable)
|
||||||
|
FOnTypewriterLineFinished OnTypewriterLineFinished;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
|
||||||
|
URichTextBlockForTypewriter* LineText;
|
||||||
|
|
||||||
|
// The amount of time between printing individual letters (for the "typewriter" effect).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typewriter")
|
||||||
|
float LetterPlayTime = 0.025f;
|
||||||
|
|
||||||
|
// The amount of time to wait after finishing the line before actually marking it completed.
|
||||||
|
// This helps prevent accidentally progressing dialogue on short lines.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typewriter")
|
||||||
|
float EndHoldTime = 0.15f;
|
||||||
|
|
||||||
|
/// Set Text immediately
|
||||||
|
UFUNCTION(BlueprintCallable)
|
||||||
|
void SetText(const FText& InText);
|
||||||
|
|
||||||
|
/// Set Text immediately
|
||||||
|
UFUNCTION(BlueprintCallable)
|
||||||
|
FText GetText() const;
|
||||||
|
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Typewriter")
|
||||||
|
void PlayLine(const FText& InLine);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Typewriter")
|
||||||
|
void GetCurrentLine(FText& OutLine) const { OutLine = CurrentLine; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Typewriter")
|
||||||
|
bool HasFinishedPlayingLine() const { return bHasFinishedPlaying; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Typewriter")
|
||||||
|
void SkipToLineEnd();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
UFUNCTION(BlueprintImplementableEvent, Category = "Typewriter")
|
||||||
|
void OnPlayLetter();
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintImplementableEvent, Category = "Typewriter")
|
||||||
|
void OnLineFinishedPlaying();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void PlayNextLetter();
|
||||||
|
|
||||||
|
void CalculateWrappedString();
|
||||||
|
FString CalculateSegments();
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FText CurrentLine;
|
||||||
|
|
||||||
|
struct FTypewriterTextSegment
|
||||||
|
{
|
||||||
|
FString Text;
|
||||||
|
FRunInfo RunInfo;
|
||||||
|
};
|
||||||
|
TArray<FTypewriterTextSegment> Segments;
|
||||||
|
|
||||||
|
// The section of the text that's already been printed out and won't ever change.
|
||||||
|
// This lets us cache some of the work we've already done. We can't cache absolutely
|
||||||
|
// everything as the last few characters of a string may change if they're related to
|
||||||
|
// a named run that hasn't been completed yet.
|
||||||
|
FString CachedSegmentText;
|
||||||
|
int32 CachedLetterIndex = 0;
|
||||||
|
|
||||||
|
int32 CurrentSegmentIndex = 0;
|
||||||
|
int32 CurrentLetterIndex = 0;
|
||||||
|
int32 MaxLetterIndex = 0;
|
||||||
|
int32 NumberOfLines = 0;
|
||||||
|
float CombinedTextHeight = 0;
|
||||||
|
|
||||||
|
uint32 bHasFinishedPlaying : 1;
|
||||||
|
|
||||||
|
FTimerHandle LetterTimer;
|
||||||
|
};
|
38
doc/TypewriterText.md
Normal file
38
doc/TypewriterText.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Typewriter Rich Text Widget
|
||||||
|
|
||||||
|
This base User Widget provides you with a rich text box that plays text like
|
||||||
|
a typewriter, one character at a time, with a configurable speed. You can
|
||||||
|
skip to the end of the text if needed.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Notable features
|
||||||
|
|
||||||
|
* Pre-calculates line breaks so that the text doesn't start to play a word near
|
||||||
|
the end of the line then "jump down", it always knows where to start
|
||||||
|
* Pre-calculates the desired height of the text so that if embedded in a flexible
|
||||||
|
height container, the height is correct before anything is played
|
||||||
|
* Supports decorators like inline images
|
||||||
|
* Respects explicit line breaks in your text
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Due to a quirk of the API used for sizing, this isn't a straight `URichTextBlock`
|
||||||
|
subclass you can drop straight in. Instead, you need to:
|
||||||
|
|
||||||
|
1. Create a Widget Blueprint, subclassed from `UTypewriterTextWidget`
|
||||||
|
2. Place within it a `URichTextBlockForTypewriter`, named `LineText` (for binding)
|
||||||
|
3. Style the nested rich text box the way you want
|
||||||
|
4. Then, in your other Widget Blueprints, make use of this `UTypewriterTextWidget`
|
||||||
|
subclass in place of a rich text box
|
||||||
|
5. Call the `Play Line` and `Skip To Line End` functions instead of `Set Text`
|
||||||
|
to make use of the typewriter effect.
|
||||||
|
6. Listen to the `On Typewriter Line Finished` event to know when the typewriter
|
||||||
|
effect has finished, or the line has been skipped to the end.
|
||||||
|
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
This was derived from [Sam Bloomberg's
|
||||||
|
work](https://github.com/redxdev/UnrealRichTextDialogueBox) with some
|
||||||
|
enhancements / adjustments. Both works are released under the MIT license.
|
@ -24,6 +24,13 @@ Several custom widgets are supplied to assist with some common challenges:
|
|||||||
the input control which is bound to that input action right now.
|
the input control which is bound to that input action right now.
|
||||||
Again this dynamically switches as input method changes.
|
Again this dynamically switches as input method changes.
|
||||||
|
|
||||||
|
* [Typewriter Rich Text Widget](TypewriterText.md)
|
||||||
|
|
||||||
|
A widget that allows you to use the "typewriter effect" to display text
|
||||||
|
(displaying one character at a time). Pre-calculates line breaks so text
|
||||||
|
doesn't try to fit in then jump down, and pre-calculates desired height so
|
||||||
|
your flexible boxes are always the right size *before* the text is played.
|
||||||
|
|
||||||
* [Menu System](Menus.md)
|
* [Menu System](Menus.md)
|
||||||
|
|
||||||
A couple of classes to make it easy to create multi-level on-screen menus
|
A couple of classes to make it easy to create multi-level on-screen menus
|
||||||
|
Loading…
x
Reference in New Issue
Block a user