Dialogue: Subtitles
Some of the main logic for presenting subtitles to each client. It is written in the exp language created by John Haggerty.
Exp Cheatsheet:
- aslongas - keeps the current scope alive "as long as" the expression remains true
- whenever - Runs the following statement repeatedly, once each time the expression is true, ending the statement when it becomes false, and waiting for it to become true again when the statement ends
- meantime - Whenever may be paired with "meantime" to provide a statement that will be run when the expression is false
- thread - Exp "threads" are asynchronous function calls. They are akin to coroutines in some other languages: they take turns executing and so are not truly asynchronous.
- localthread - A localthread is a thread that is guaranteed to end when the scope it is defined in is exited. You can customize the scope it is attached to with the @ symbol.
- cleanup - After a cleanup block is passed during execution, the statement (or block of statements) after cleanup is guaranteed to run when the scope is exited.
// lets users set their option for how many subtitles to appear shared watchobj< int > SubtitleDisplayMode = RegisterConVar( "subtitlesEnabled", 2, "0 - no subtitles, 1 - all subtitles, 2 - no battle chatter pain efforts" ) // the actual widget for the subtitle UCLASS() shared struct USubtitleWidget : UUserWidget { UPROPERTY(meta=(BindWidget)) UBorder? SubtitleBorder UPROPERTY(meta=(BindWidget)) URichTextBlock? SubtitleRichText // subtitles support different styling for the speaker name and dialogue int Id bool IsMainQueue watchable bool SubtitleFinished = false // TODO: Unreal doesn't let you Get() this? float CurrentRenderScale } shared bool fn APlayer::ShouldAddSubtitle( AActor speaker, string subtitleText, bool isForcedRadio, ESubtitleType subtitleType ) { if ( !hudWidget || !isControlledLocally ) return false if ( SubtitleDisplayMode == 0 ) return false if ( SubtitleDisplayMode == 2 && subtitleType == ESubtitleType.BattleChatterEfforts ) return false USubtitlesBoxWidget? subtitleBox = hudWidget!.SubtitlesBox assert( subtitleBox ) if ( subtitleText.size <= 0 ) return false if ( isForcedRadio || subtitleType == ESubtitleType.QueuedMain ) return true if ( subtitleBox.CountActiveSubtitles >= MAX_ACTIVE_SUBTITLES ) return false // now do expensive check for unimportant dialogue float distSqr = DistanceSqr( speaker.GetActorLocation(), GetActorLocation() ) if ( distSqr >= sqr(DISTANCE_SPEAKER_TOO_FAR_FOR_SUBTITLE) ) return false return true } // Create and hide an individual subtitle shared maywait fn APlayer::AddSubtitleWidget( FGameplayTag speakerTag, FText subtitleText, bool isForcedRadio, ESubtitleType subtitleType, int dialogueId ) { aslongas @fn inPlay && isControlledLocally && hudWidget != null // this is the container widget for all subtitles with a gray background USubtitlesBoxWidget? subtitleBox = hudWidget!.SubtitlesBox assert( subtitleBox ) // create subtitle widget UWidget? newWidget = subtitleBox.ConstructWidget( subtitleBox.SubtitleBlueprint.Get(), NAME_None ) assert( newWidget is USubtitleWidget ) newWidget.Id = dialogueId newWidget.IsMainQueue = subtitleType == ESubtitleType.QueuedMain // define speaker styling/name string speakerStyle if ( IsSpeakerTagPlayer(speakerTag) || IsSpeakerTagHero(speakerTag) ) speakerStyle = SPEAKER_STYLE_FRIENDLY else if ( IsSpeakerTagVillain(speakerTag) ) speakerStyle = SPEAKER_STYLE_VILLAIN else speakerStyle = SPEAKER_STYLE_DEFAULT string speakerName foreach ( tag, subtitleName in SPEAKER_NAME_MAP ) { if ( UGameplayTagLibrary_MatchesTag(tag, speakerTag, true) ) { speakerName = subtitleName break } } else assert( false, "Couldn't find subtitle name based on speaker tag." ) if ( isForcedRadio ) speakerName = "{speakerName} [{SUBTITLE_NAME_RADIO}]" // apply subtitle text string fullSubtitle = "<{speakerStyle}>{speakerName}:</> {subtitleText}" newWidget.SubtitleRichText!.SetText( fullSubtitle ) // widget settings subtitleBox.SubtitleVerticalBox!.AddChildToVerticalBox( newWidget ) newWidget.SubtitleBorder!.SetDesiredSizeScale( <1.0, 0.0> ) newWidget.SubtitleRichText!.SetRenderOpacity( 0.0 ) newWidget.CurrentRenderScale = 0.0 newWidget.SetRenderTransformPivot( <0.5, 1.0> ) // So it scales from the bottom center UVerticalBoxSlot? slot = Cast<[ UVerticalBoxSlot ]>( newWidget.Slot ) assert( slot ) slot.SetHorizontalAlignment( EHorizontalAlignment.HAlign_Fill ) slot.SetVerticalAlignment( EVerticalAlignment.VAlign_Fill ) // add to the array subtitleBox.SubtitleArray.push( newWidget ) subtitleBox.CountActiveSubtitles++ cleanup { if ( inPlay && isControlledLocally && hudWidget != null ) { subtitleBox.CountActiveSubtitles-- newWidget.SubtitleFinished = true thread subtitleBox.SubtitleOutro( this, newWidget ) // fade out, then vertically shrink to 0 } } // wait for box to fade in aslongas !newWidget.SubtitleFinished waitfor subtitleBox.IsReady if ( newWidget.SubtitleFinished ) return // intro scale newWidget.SubtitleScaleTo( DURATION_SUBTITLE_SCALE, 0.0, 1.0 ) // intro fade in newWidget.SubtitleRichText!.FadeTo( DURATION_SUBTITLE_FADE, 0.0, 1.0 ) wait DURATION_MIN_SUBTITLE_DISPLAY // prevents abrupt removal if (dialogueId < 0) return waitfor newWidget.SubtitleFinished }