Migration from Perforce into Git in Windows

Overview

Sometimes, you have to switch the version control system for some reason. In this post, I will cover how to migrate Perforce stream into Git repository. I have confirmed that the method in this post works only in Windows, but you might be able to accomplish the same result with a similar way.

Prerequisites

First of all, you should have Git and Perforce installed. Any latest version would be okay. Plus, you should be able to use their commands through the command prompt. For instance, the commands below should be working:

1
2
3
4
5
6
7
8
9
10
11
> p4 -V
Perforce - The Fast Software Configuration Management System.
Copyright 1995-2023 Perforce Software. All rights reserved.
This product includes software developed by the OpenSSL Project
for use in the OpenSSL Toolkit (http://www.openssl.org/)
Version of OpenSSL Libraries: OpenSSL 1.1.1u 30 May 2023
See 'p4 help [ -l ] legal' for additional license information on
these licenses and others.
Extensions/scripting support built-in.
Parallel sync threading built-in.
Rev. P4/NTX64/2023.1/2468153 (2023/07/24).
1
2
> git -v
git version 2.42.0.windows.2

Second, you should have Python installed. The version after 2.7 would be okay. (eg. 2.8 or 3.5) Plus, you should be able to use its commands through the command prompt. For instance, the commands below should be working:

1
2
> python -V
Python 3.11.6

The last one, you have to change your system locale settings if you had written the description of changelist with non-ascii codes. You can enable the option Beta: Use Unicode UTF-8 for worldwide language support from the depth of Control Panel/All Control Panel Items/Region/Administrative/Change system locale....

Unless the option enabled, the commit message from migration result can be seen as untranslatable if the description was written with non-ascii codes.

With the option enabled, the commit message would be migrated properly just like the image below. So, check your descriptions in Perforce and change the system locale settings.

Migration

Type the command of format python <path of git-p4> clone //<depot>/<stream>/<directory>@all in prompt. For instance, I can type the command like this:

1
> python "C:\Program Files\Git\mingw64\libexec\git-core\git-p4" clone //HellLady/mainline/HellLady@all

Then, all changelists from the //<depot>/<stream>/<directory> will be migrated into a Git repository.

Postscript

That is all about the migration. 😂 So simple, but it was hard to know because the official document does not cover the usage in Windows. Anyway, I hope this would be helpful for you. Check the official document for more details if you also need other commands. Good luck. 🤞

Conversion from UA into GA4

Overview

Google noticed that the support for Universal Analytics will be ended in 2023/06/30. Check this document for more details.

Therefore, I had to migrate my UA settings into GA4. Here is a solution for Hexo blog, which is the framework I am using for this blog.

Google Tag

For activating Google Analytics, Google provides you a tag named as “Google Tag”. First of all, you should find out what tag should be installed. You can find out this at Google Analytics 4’s page.

Click the button Admin.

Click the button Account Access Management.

Click the button Data Streams.

Click the right arrow at your data stream.

Click the right arrow at the option Configure tag settings.

Click the button Installation instructions.

Check out the code that you should include manually. This is the Google Tag for analytics.

Theme Config

Most of themes for Hexo have the config file for Google Analytics. You can find it by just searching “google_analytics” with text.

Especially, google-analytics.ejs file would be containing the Google Tag.


The tag is inserted at the front of page of every post in your blog, so you can check that with View page source. Thus, you should replace the Google Tag with new one. Copy the new one we have prepared and paste it to the google-analytics.ejs file. Here are the commits I used for that.

Result

After the setup, the data stream will be constructed. But, it would take some time…about 1 day or 2 days ? So just keep calm and wait for that.

When it constructed successfully, you can see the result DATA FLOWING just like above at GA4 / Admin / Account Access Management / Setup Assistant.

The promotion for online lecture of UnrealEngine




Hello, this is Ross Bae, a game programmer.
I had a great opportunity to open a class with Coloso, a platform specializing in online classes.
The class is <FPS게임 개발로 한 번에 입문하는 언리얼 엔진>. And it is supported only in Korean yet.

Currently, UnrealEngine is widely used to the extent that it is used in world-famous games such as Battlegrounds, Fortnite, and Valorant. However, I have seen many people who feel hopeless due to the lack of systematically organized materials and lectures compared to their popularity, so I prepared this class.

In this class, I created a lecture by designing a curriculum so that you can learn the basic knowledge of the UnrealEngine by making FPS games with me, and cover from blueprints to scripting using C++.

You can learn the basic contents of game development using UnrealEngine as well as the knowledge and skills necessary to study UnrealEngine on your own, so it would be a good lecture for those who are interested in UnrealEngine.

It is not easy to get through the world of UnrealEngine using only Blueprint, so if you know how to handle C++ at all, it will be a great help. That is why I would like to deal with C++ in this lecture. However, you don’t have to be afraid of streotypes about C++ because we provide training materials that would be helpful to C++ beginners.

For your information, you can take the course at a significant discount for the current Early Bird period. Therefore, if you are interested, please check the attached link below.


안녕하세요, 게임 프로그래머 배민천입니다
이번에 제가 좋은 기회로 온라인 클래스 전문 플랫폼 콜로소와 함께
<FPS게임 개발로 한 번에 입문하는 언리얼 엔진>
클래스를 열게 되었습니다

현재 언리얼엔진은 배틀그라운드, 포트나이트, 발로란트 등
세계적으로 저명한 게임에 쓰일 정도로, 널리 사용되고 있습니다
하지만, 그 유명세에 비해 체계적으로 정리된 자료나 강의가 부족해
막막함을 느끼는 분들을 많이 봐왔기 때문에, 이번 강의를 준비하게 되었습니다

이번 클래스에서 저와 함께 FPS게임을
직접 만들어보면서 언리얼엔진의 기본지식들을 익힐 수 있으며
블루프린트부터 C++ 을 활용한 스크립팅까지 커버할 수 있도록
커리큘럼을 설계하여 강의를 제작했습니다

언리얼엔진을 활용한 게임개발에 기본적인 내용들은 물론
언리얼엔진을 스스로 공부하는 데에 필요한 지식과 기술들을
배우실 수 있으므로, 평소에 언리얼엔진에
관심이 있던 분이시라면 좋은 강의가 될 것입니다

블루프린트만으로는 언리얼엔진의 세상을 헤쳐나가기 쉽지 않기에
C++ 을 조금이라도 다룰 줄 안다면, 큰 도움이 될 것이기
때문에 이번 강의에서 C++ 을 다루고자 합니다
하지만, C++ 입문자를 고려하여 교육자료를 별도로 만들어
제공하므로 C++ 에 대한 선입견 때문에 겁먹지 않아도 됩니다

참고로, 현재 얼리버드(예약구매) 기간으로 크게 할인된 금액으로 수강할 수 있습니다
따라서, 관심 있는 분은 첨부된 링크를 통해 자세한 내용을 확인해주세요


https://bit.ly/3iMuyYb

Retargeting animations in UnrealEngine 5

Environment
UnrealEngine version: 5.0.3
Windows 11 Pro build: 22621.521

Overview

It is common that an animation asset is binding at a certain skeleton asset. So you might have experience that you could not utilize the skeleton A’s animation at the skeleton B’s animation. Because an animation data is made of trace of bones. Therefore, an animation asset would not be compatible when you attempt to apply it to the different skeleton asset.

Think about two skeletons, A and B. The skeleton A has a bone for head, but the skeleton B does not. An animation asset for the skeleton A would not be compatible with the skeleton B, because the skeleton B does not have a bone for head. Though, you might want to apply it, at least parts of animation without head. Fortunately, UnrealEngine provides you to reuse the animation assets by retargeting bones, even if the number of bones or position of bones are different. That is the “Retargeting animations”.

Preparation

I will explain you while showing an example. First, install the project Lyra. You can purchase the project in the UnrealEngine marketplace and install it into your local system. After that, purchase Animation Starter Pack, too. Both of them are free.

Add Animation Starter Pack into the project Lyra you installed. Now, open it.

You can see the folder AnimStarterPack at the below of Content, and there are several animation assets fit for SK_Mannequin.

Open an animation asset, and you can see the list of animations available in the current skeleton.

Switch to the tab for skeleton, and you can see the skeleton asset with the hierarchy of bones. Okay, we have checked the asset Animation Starter Pack. Jump to the next, the project Lyra.

In the Lyra, there are one skeleton asset, but two skeleton meshes; SKM_Manny and SKM_Quinn. Each of them for male and female appearance.

The name of skeleton asset is the same with the asset Animation Starter Pack with SK_Mannequin. From now on, I will name the skeleton for Lyra as UE5 skeleton, and the skeleton for Animation Starter Pack as UE4 skeleton.

Also, you can find animation assets for UE5 skeleton at Content/Characters/Heroes/Mannequin/Animations/Actions. All we have to do is, retargeting animations from ue4 skeleton into ue5 skeleton, and retargeting animations from ue5 skeleton into ue4 skeleton.

Create a folder RetargetedAnimations at Content/AnimStarterPack. We will save the retargeted animations and so on here. First, you should create IK Rigs for each skeleton mesh.



Name them as IKRigUE4 and IKRigUE5.


They might look like this. Huh, it is time to setup the IK Rig.

Setup IK Rig

The IK Rig is used to define many properties, especially for retargeting animations. First of all, you should choose the root of retargeting. It is recommended to choose a pelvis in most cases. (Especially, when it is a human form.) Right click the pelvis and select the Set Retarget Root. Then, the text (Retarget Root) is displayed by the pelvis. After that, UnrealEngine will retarget the animations from the root, pelvis. Do it on both of IK Rig assets.


Get back to the content browser, create a IK Retargeter. You should choose a source IK Rig to create a IK Retargeter. Choose the IKRigUE4, and name this as UE4_TO_UE5. Open it up.

The source IK Rig is the IKRigUE4. Therefore, you should assign IKRigUE5 at the Target IKRig Asset.

Now you can see both of them in the viewport. Try to play an animation from the asset browser.

Then, the target one will not be animating properly. Just like the video. As you have set the pelvis as a retarget root, it looks like only the pelvis is synchronized, while others are not. The problem is, the hierarchy of bones is different between two skeletons.

For instance, the UE4 skeleton has 3 bones for spine; spine_01, spine_02, and spine_03.

The UE5 skeleton has 5 bones for spine; spine_01, spine_02, spine_03, spine_04, and spine_05. UnrealEngine cannot retarget animations because the number and position of bones are different between two skeletons. So, you should specify how to match the bones, and you can do it with a chain.

Setup Chain

The IK Rig asset has a panel IK Retargeting beside a panel Asset Browser. You can specify some chains here, and it chains a part of bones as a group. UnrealEngine matches the group of same name when you attempt to retarget animations. Let me show you an example. Add new chain, and name it as leg_left. We are going to group bones for the left leg.

Check the bones. After the pelvis, left leg starts at thigh_l and ends at ball_l. So, set the Start Bone and End Bone of chain leg_left. You have set the chain for left leg in UE4 skeleton. Next, you should set the chain in UE5 skeleton, too.

Create a chain leg_left. Check the bones for left leg. Set the Start Bone and End Bone. Then, we are good to go.

Back to the retargeter asset, click the panel Chain Mapping. And click the button Auto-Map Chains. The Auto-Map Chains will match the chains of similar name. You can also match the chains of different name, but you should do it manually in that case.

Try to play some animations. You can notice there is a change. Yes, as you can see in the video, the left leg is synchronized. All you have to do is, create chains and match them.

I recommend you to create the chains; leg_right, spine, arm_left, arm_right, head. Here are the chain settings I used for UE4 skeleton.

Chain Name Start Bone End Bone
leg_left thigh_l ball_l
leg_right thigh_r ball_r
spine spine_01 spine_03
arm_left clavicle_l hand_l
arm_right clavicle_l hand_r
head neck_01 head

Unfortunately, UnrealEngine does not support to copy the chain settings yet. So, you should write the same settings in UE5 skeleton.

Back to the retargeter asset, again. Click the button Auto-Map Chains, and try to play some animations. It looks like the video. Does it look like perfect ? No, focus on their hands. You shoud care about fingers, too. (Even for toes if the animation covers them 🤣)

I will show you an example for index finger, rest of fingers are your work.

After the work for all fingers, it should look like this video.

Edit Pose

Sometimes, you might want to retarget animations but two skeleton are different each other. Suppose you have a skeleton of pose A, and a skeleton of pose T.

In this situation, the retargeted animations look weird even you set the chains well. Just like the video. Oh…it is like the necromorph in the Dead Space…😱 It happens due to the pose, the two skeletons are different on the pose. You should edit one’s pose so that they have the same pose.

Here, I will edit the skeleton of pose T. Let me edit the pose as A. First, click the button Edit Pose.

We have returned to the base pose. Now you can select bones of the target IK Rig.

I recommend you to change the property Target Actor Offset if you need. You can check the rotation more precisely when it is set by 0.


I have editted the pose by this settings;

  • Rotating the upper arm by -60 degrees on the axis Y.
  • Rotating the lower arm by +40 degrees on the axis Z.

Do this settings on two arms.

Now it seems okay. Then, set a proper value to Target Actor Offset. Click the button Edit Pose to leave the edit mode.

You would see the result just like the video. Quite better than before. But, there is onething you should remember about the feature Edit Pose. It is that, you cannot rotate bones not in any chain.

Suppose an IK Rig asset does not have a chain for right leg. As you can see in the screenshot, there is only a chain for left leg. Go to the retargeter asset.

You cannot see the section for right leg, even you have entered the edit mode. So, it is crucial that creating necessary chains before you attempt to edit pose in the retargeter asset.

Export

Get back to the UE4 & UE5 skeletons. You could play animations for UE4 skeleton via the Asset Browser in the retargeter asset UE4_TO_UE5. Plus, you can export selected animations to create animation assets for UE5 skeleton, which is the target skeleton.


Export animations at Content/AnimStarterPack/RetargetedAnimations.

When you open it up, you can see the asset is using the skeletal mesh for UE5 skeleton. Great. It is simple that retargeting animations in opposite direction; UE5 -> UE4.

  • Create a retargeter asset based on IKRigUE5.
  • Set the Target IKRig Asset as IKRigUE4.
  • Click the button Auto-Map Chains.
  • Select animations you want to export.
  • Export them, profit !

You can see it is using the skeletal mesh for UE4 skeleton. 😎

How the RichTextBlock works in UnrealEngine (part.1)

Environment
UnrealEngine branch: 5.0
Visual Studio 2022 version: 17.2.6
Windows 11 Pro build: 22000.795

Overview

We have learned about the TextBlock in UnrealEngine at the previous post. As we saw, the TextBlock provides the function to split a long text into multiple lines. But, it was only for the text, combinations of character.

Sometimes, we want to put something that is not a character in the middle of text. For example, you may want to put an image for key icon into the text that describes character’s skill. Maybe, you want to highlight a part of the text by coloring it. Furthermore, you could want to put a “widget” in the middle of text. The widget would interact with the player’s action so that they can have better experience of the user interface.

An option description in PUBG Xbox.

UnrealEngine has a solution for that, the RichTextBlock widget. You can put an image or anything else in the middle of text. Plus, it also supports auto-wrapping just like at the TextBlock. Now you know why its name is the “Rich”TextBlock. Then, let us check out how the RichTextBlock widget is implemented and how it works.

An example

Already there is a tutorial in the document, but I will show you an another example including how to make your custom RichTextBlock decorator. Suppose you want to display the text like the screenshot below.

The size of SizeBox is (512, 512). The text used in RichTextBlock is here:

1
Test <Emphasis> Test </> <somewidget id="Ferris_02"/> Test <somewidget id="Ferris_01"/> Test aaa aaa aaa aaa aaa aaa aaa aaa <img id="Ferris_01"/> aaa <somewidget id="Ferris_02"/> aaa

As we can see, the images are put in the middle of text. The RichTextBlock parses the input text and decorates the text with your configurations. Without some configurations, the tags such as <Emphasis> and <somewidget> would be displayed as a plain text. Yes, you should do some configurations for using the RichTextBlock.

I have already set some properties, TextStyleSet and DecoratorClasses.

RichTextStyle

The TextStyleSet is used for decorating a text just like a markup. You can specify a font, size, color, and so on with it. I made two data rows in the data table, and that is why some of text was displayed with green color. Check the screenshot below.


The RichTextBlock decorates rest of text if you make a Default row. That is why the text not embraced with tags was displayed with white color.

Without the TextStyleSet, the RichTextBlock cannot display the text properly. You can make a data table containing RichTextStyleRow with the instructions.

1
2
3
4
1. Right click on contents browser.
2. find the item `Data Table` at the category `Miscellaneous`, and click it.
3. The dialog `Pick Row Structure` pops up.
4. Click the drop down, and select `RichTextStyleRow`.

Now, you can manipulate the data table. But, you should be careful that the name of data row is the same with the name of tag in the RichTextBlock.

RichImage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/** Simple struct for rich text styles */
USTRUCT(Blueprintable, BlueprintType)
struct UMG_API FRichImageRow : public FTableRowBase
{
GENERATED_USTRUCT_BODY()

public:

UPROPERTY(EditAnywhere, Category = Appearance)
FSlateBrush Brush;
};

/**
* Allows you to setup an image decorator that can be configured
* to map certain keys to certain images. We recommend you subclass this
* as a blueprint to configure the instance.
*
* Understands the format <img id="NameOfBrushInTable"></>
*/
UCLASS(Abstract, Blueprintable)
class UMG_API URichTextBlockImageDecorator : public URichTextBlockDecorator

UnrealEngine provides a decorator class, URichTextBlockImageDecorator. It helps you add an image widget in the middle of text.

Without it, the RichTextBlock cannot create an image from the tag img. You can make a data table containing RichImageRow with the instructions.

1
2
3
4
1. Right click on contents browser.
2. find the item `Data Table` at the category `Miscellaneous`, and click it.
3. The dialog `Pick Row Structure` pops up.
4. Click the drop down, and select `RichImageRow`.

Now, you can manipulate the data table. Also, you should be careful that the name of data row is the same with the name of tag in the RichTextBlock as I mentioned at the RichTextStyle. So, remember it because this mechanism will work on other cases (Decorators using their own data table) too.

However, you need one step more to apply the data table.

1
2
3
4
5
6
7
8
9
// Engine/Source/Runtime/UMG/Public/Components/RichTextBlock.h

/** */
UPROPERTY(EditAnywhere, Category=Appearance, meta=(RequiredAssetDataTags = "RowStructure=RichTextStyleRow"))
TObjectPtr<class UDataTable> TextStyleSet;

/** */
UPROPERTY(EditAnywhere, Category=Appearance)
TArray<TSubclassOf<URichTextBlockDecorator>> DecoratorClasses;

The TextStyleSet needs only a data table, but the DecoratorClasses takes a class inherits URichTextBlockDecorator. That is why URichTextBlockImageDecorator inherits that.



So, you should create a blueprint class inherits URichTextBlockImageDecorator because the class URichTextBlockImageDecorator has the UCLASS keyword Abstract. And, assign it into the DecoratorClasses at the RichTextBlock widget. The blueprint class should reference the data table for images.

Custom decorator

I have written a custom decorator for this example, the URichTextBlockSomeWidgetDecorator. As you can see in the example, it displays a combination of image and text. First of all, the code for this class is here.

And the followings are the major changes.

1
2
3
4
5
6
7
8
9
public class TestRichTextBlock : ModuleRules
{
public TestRichTextBlock(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "UMG", "Slate", "SlateCore" });
}
}

You must add the modules at your Build.cs: UMG, Slate, and SlateCore.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool FRichInlineSomeWidget::Supports(const FTextRunParseResults& RunParseResult, const FString& Text) const
{
bool Result = false;
const bool IsContainId = RunParseResult.MetaData.Contains(TEXT("id"));
const bool IsNameSomeWidget = RunParseResult.Name == TEXT("somewidget");
if (IsContainId && IsNameSomeWidget)
{
const FTextRange& IdRange = RunParseResult.MetaData[TEXT("id")];
const FString TagId = Text.Mid(IdRange.BeginIndex, IdRange.EndIndex - IdRange.BeginIndex);
const bool bWarnIfMissing = false;
Result = Decorator->FindSomeWidgetRow(*TagId, bWarnIfMissing) != nullptr;
}
return Result;
}

I have changed the tag that my decorator supports. img -> somewidget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void SRichInlineSomeWidget::Construct(const FArguments& InArgs, const FRichSomeWidgetRow* Row, const FTextBlockStyle& TextStyle, TOptional<int32> Width, TOptional<int32> Height, EStretch::Type Stretch)
{
const FSlateBrush* InBrush = &(Row->Brush);
check(InBrush)
const FText InText = Row->Text;
const TSharedRef<FSlateFontMeasure> FontMeasure = FSlateApplication::Get().GetRenderer()->GetFontMeasureService();
const float MaxHeight = FontMeasure->GetMaxCharacterHeight(TextStyle.Font, 1.0f);
float IconHeight = FMath::Max(MaxHeight, InBrush->ImageSize.Y);
if (Height.IsSet())
{
IconHeight = Height.GetValue();
}
float IconWidth = IconHeight;
if (Width.IsSet())
{
IconWidth = Width.GetValue();
}
ChildSlot
[
SNew(SBox)
[
SNew(SHorizontalBox)

+SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
[
SNew(SImage)
.Image(InBrush)
]

+SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(InText)
]
]
];
}

Used the max value for IconHeight because I wanted to display the image properly. Plus, the decorator has a TextBlock for descripting an image. In the example, a text Ferris_01 or Ferris_02 is located on the right of Ferris’ image.

So, you can create a custom decorator like this. Rest works are just similar with RichImage, creating some blueprint classes (decorator and data table) and assigning each other. Let your decorator have awesome functions :)

Preview of Part #2

At this part, we have seen how to use the RichTextBlock and how to make a custom decorator.

  • Only for a text, you should create a data table and assign it.
  • For other content, you should create a custom decorator and assign it. But, UnrealEngine has already made a default decorator for an image, URichTextBlockImageDecorator.
  • When creating a custom decorator, you should know them below:
    • Specify an unique tag name. There should be no confliction.
    • Design a widget layout with Slate. You can reference many examples from engine codes, just find all references of ChildSlot.
    • Create a SWidget version of your widget if you want to put your widget into the custom decorator. As you can see, the SNew accepts only the class inherits SWidget. In most of cases, it is okay to inherit the class SLeafWidget.

At next part, we would find out how does the RichTextBlock wrap its contents. It will be interesting because the RichTextBlock can have an image as a content.

How the text wrap works in UnrealEngine

Environment
UnrealEngine branch: 5.0
Visual Studio 2022 version: 17.1.1
Windows 11 Pro build: 22000.556

Overview

A TextBlock has an option AutoWrapText and the option makes the TextBlock can wrap its text. Thanks to the option, we can display a text without concerning about breaking lines. For general cases of text, even the option works within very short time, almost 1 tick. How does it possible ? What is the implementation of that option ? Let us find out it in this post.

The TextBlock upper has the option turned on. Contrary, the TextBlock lower has the option turned off.

Where is the code

1
2
3
4
5
// TextWidgetTypes.h

/** True if we're wrapping text automatically based on the computed horizontal space for this widget. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Wrapping)
uint8 AutoWrapText:1;

The option is loacted in the class UTextLayoutWidget. We can see the option as the class UTextBlock inherites UTextLayoutWidget. Unfortunately, the variable is not directly used for wrapping text, but used for saving the value of option.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void UTextBlock::SynchronizeProperties()
{
Super::SynchronizeProperties();

TAttribute<FText> TextBinding = GetDisplayText();
TAttribute<FSlateColor> ColorAndOpacityBinding = PROPERTY_BINDING(FSlateColor, ColorAndOpacity);
TAttribute<FLinearColor> ShadowColorAndOpacityBinding = PROPERTY_BINDING(FLinearColor, ShadowColorAndOpacity);

if ( MyTextBlock.IsValid() )
{
MyTextBlock->SetText( TextBinding );
MyTextBlock->SetFont( Font );
MyTextBlock->SetStrikeBrush( &StrikeBrush );
MyTextBlock->SetColorAndOpacity( ColorAndOpacityBinding );
MyTextBlock->SetShadowOffset( ShadowOffset );
MyTextBlock->SetShadowColorAndOpacity( ShadowColorAndOpacityBinding );
MyTextBlock->SetMinDesiredWidth( MinDesiredWidth );
MyTextBlock->SetTransformPolicy( TextTransformPolicy );
MyTextBlock->SetOverflowPolicy(TextOverflowPolicy);

Super::SynchronizeTextLayoutProperties( *MyTextBlock );
}
}

When you turn on or turn off the option AutoWrapText, widget’s SynchronizeProperties() would be called. By the code Super::SynchronizeTextLayoutProperties(*MyTextBlock); executed, Parent’s SynchronizeProperties(TWidgetType&) is called.

1
2
3
4
5
6
7
8
9
10
11
12
13
/** Synchronize the properties with the given widget. A template as the Slate widgets conform to the same API, but don't derive from a common base. */
template <typename TWidgetType>
void SynchronizeTextLayoutProperties(TWidgetType& InWidget)
{
ShapedTextOptions.SynchronizeShapedTextProperties(InWidget);

InWidget.SetJustification(Justification);
InWidget.SetAutoWrapText(!!AutoWrapText);
InWidget.SetWrapTextAt(WrapTextAt != 0 ? WrapTextAt : TAttribute<float>());
InWidget.SetWrappingPolicy(WrappingPolicy);
InWidget.SetMargin(Margin);
InWidget.SetLineHeightPercentage(LineHeightPercentage);
}

In this function, InWidget is our TextBlock. And it would call the function SetAutoWrapText(bool) for updating the option.

1
2
3
4
5
6
7
8
void UTextBlock::SetAutoWrapText(bool InAutoWrapText)
{
AutoWrapText = InAutoWrapText;
if(MyTextBlock.IsValid())
{
MyTextBlock->SetAutoWrapText(InAutoWrapText);
}
}

Good. The parameter InAutoWrapText updates the variable AutoWrapText and MyTextBlock. The variable MyTextBlock is TSharedPtr<STextBlock>. Now, the time to jump to STextBlock.

1
2
3
4
void STextBlock::SetAutoWrapText(TAttribute<bool> InAutoWrapText)
{
AutoWrapText.Assign(*this, MoveTemp(InAutoWrapText), 0.f);
}

Here, in STextBlock the variable AutoWrapText holds the value of option. The function Assign() just saves the value its inside. The value of AutoWrapText is used in two positions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// STextBlock.cpp
FVector2D STextBlock::ComputeDesiredSize(float LayoutScaleMultiplier) const
{
SCOPE_CYCLE_COUNTER(Stat_SlateTextBlockCDS);

if (bSimpleTextMode)
{
const FVector2D LocalShadowOffset = GetShadowOffset();

const float LocalOutlineSize = (float)(GetFont().OutlineSettings.OutlineSize);

// Account for the outline width impacting both size of the text by multiplying by 2
// Outline size in Y is accounted for in MaxHeight calculation in Measure()
const FVector2D ComputedOutlineSize(LocalOutlineSize * 2.f, LocalOutlineSize);
const FVector2D TextSize = FSlateApplication::Get().GetRenderer()->GetFontMeasureService()->Measure(BoundText.Get(), GetFont()) + ComputedOutlineSize + LocalShadowOffset;

CachedSimpleDesiredSize = FVector2f(FMath::Max(MinDesiredWidth.Get(), TextSize.X), TextSize.Y);
return FVector2D(CachedSimpleDesiredSize.GetValue());
}
else
{
// ComputeDesiredSize will also update the text layout cache if required
const FVector2D TextSize = TextLayoutCache->ComputeDesiredSize(
FSlateTextBlockLayout::FWidgetDesiredSizeArgs(
BoundText.Get(),
HighlightText.Get(),
WrapTextAt.Get(),
AutoWrapText.Get(),
WrappingPolicy.Get(),
GetTransformPolicyImpl(),
Margin.Get(),
LineHeightPercentage.Get(),
Justification.Get()),
LayoutScaleMultiplier, GetComputedTextStyle());

return FVector2D(FMath::Max(MinDesiredWidth.Get(), TextSize.X), TextSize.Y);
}
}

// Callstack
UnrealEditor-Slate.dll!STextBlock::ComputeDesiredSize(float LayoutScaleMultiplier) Line 300 C++
UnrealEditor-SlateCore.dll!SWidget::CacheDesiredSize(float InLayoutScaleMultiplier) Line 936 C++
UnrealEditor-SlateCore.dll!SWidget::Prepass_Internal(float InLayoutScaleMultiplier) Line 1714 C++
[Inline Frame] UnrealEditor-SlateCore.dll!SWidget::Prepass_ChildLoop::__l2::<lambda_a0677895c4614612fd5b4c5f4771eae9>::operator()(SWidget &) Line 1751 C++
UnrealEditor-SlateCore.dll!FChildren::ForEachWidget<<lambda_a0677895c4614612fd5b4c5f4771eae9>>(SWidget::Prepass_ChildLoop::__l2::<lambda_a0677895c4614612fd5b4c5f4771eae9> Pred) Line 67 C++
[Inline Frame] UnrealEditor-SlateCore.dll!SWidget::Prepass_ChildLoop(float) Line 1721 C++
UnrealEditor-SlateCore.dll!SWidget::Prepass_Internal(float InLayoutScaleMultiplier) Line 1708 C++
...
UnrealEditor-SlateCore.dll!SWidget::Prepass_Internal(float InLayoutScaleMultiplier) Line 1708 C++
[Inline Frame] UnrealEditor-SlateCore.dll!SWidget::Prepass_ChildLoop::__l2::<lambda_a0677895c4614612fd5b4c5f4771eae9>::operator()(SWidget &) Line 1751 C++
UnrealEditor-SlateCore.dll!FChildren::ForEachWidget<<lambda_a0677895c4614612fd5b4c5f4771eae9>>(SWidget::Prepass_ChildLoop::__l2::<lambda_a0677895c4614612fd5b4c5f4771eae9> Pred) Line 67 C++
[Inline Frame] UnrealEditor-SlateCore.dll!SWidget::Prepass_ChildLoop(float) Line 1721 C++
UnrealEditor-SlateCore.dll!SWidget::Prepass_Internal(float InLayoutScaleMultiplier) Line 1708 C++
UnrealEditor-SlateCore.dll!SWidget::SlatePrepass(float InLayoutScaleMultiplier) Line 690 C++
UnrealEditor-Slate.dll!PrepassWindowAndChildren(TSharedRef<SWindow,1> WindowToPrepass) Line 1197 C++
UnrealEditor-Slate.dll!FSlateApplication::DrawPrepass(TSharedPtr<SWindow,1> DrawOnlyThisWindow) Line 1249 C++
UnrealEditor-Slate.dll!FSlateApplication::PrivateDrawWindows(TSharedPtr<SWindow,1> DrawOnlyThisWindow) Line 1294 C++
UnrealEditor-Slate.dll!FSlateApplication::DrawWindows() Line 1060 C++
UnrealEditor-Slate.dll!FSlateApplication::TickAndDrawWidgets(float DeltaTime) Line 1625 C++
UnrealEditor-Slate.dll!FSlateApplication::Tick(ESlateTickType TickType) Line 1482 C++
UnrealEditor.exe!FEngineLoop::Tick() Line 5325 C++
[Inline Frame] UnrealEditor.exe!EngineTick() Line 62 C++
UnrealEditor.exe!GuardedMain(const wchar_t * CmdLine) Line 183 C++
UnrealEditor.exe!GuardedMainWrapper(const wchar_t * CmdLine) Line 147 C++
UnrealEditor.exe!LaunchWindowsStartup(HINSTANCE__ * hInInstance, HINSTANCE__ * hPrevInstance, char * __formal, int nCmdShow, const wchar_t * CmdLine) Line 283 C++
UnrealEditor.exe!WinMain(HINSTANCE__ * hInInstance, HINSTANCE__ * hPrevInstance, char * pCmdLine, int nCmdShow) Line 330 C++
[External Code]

First, an execution flow by Prepass.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// STextBlock.cpp

int32 STextBlock::OnPaint( const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled ) const
{
SCOPE_CYCLE_COUNTER(Stat_SlateTextBlockOnPaint);

if (bSimpleTextMode)
{
// Draw the optional shadow
const FLinearColor LocalShadowColorAndOpacity = GetShadowColorAndOpacity();
const FVector2D LocalShadowOffset = GetShadowOffset();
const bool ShouldDropShadow = LocalShadowColorAndOpacity.A > 0.f && LocalShadowOffset.SizeSquared() > 0.f;

const bool bShouldBeEnabled = ShouldBeEnabled(bParentEnabled);

const FText& LocalText = BoundText.Get();
FSlateFontInfo LocalFont = GetFont();

if (ShouldDropShadow)
{
const int32 OutlineSize = LocalFont.OutlineSettings.OutlineSize;
if (!LocalFont.OutlineSettings.bApplyOutlineToDropShadows)
{
LocalFont.OutlineSettings.OutlineSize = 0;
}

FSlateDrawElement::MakeText(
OutDrawElements,
LayerId,
AllottedGeometry.ToOffsetPaintGeometry(LocalShadowOffset),
LocalText,
LocalFont,
bShouldBeEnabled ? ESlateDrawEffect::None : ESlateDrawEffect::DisabledEffect,
InWidgetStyle.GetColorAndOpacityTint() * LocalShadowColorAndOpacity
);

// Restore outline size for main text
LocalFont.OutlineSettings.OutlineSize = OutlineSize;

// actual text should appear above the shadow
++LayerId;
}

// Draw the text itself
FSlateDrawElement::MakeText(
OutDrawElements,
LayerId,
AllottedGeometry.ToPaintGeometry(),
LocalText,
LocalFont,
bShouldBeEnabled ? ESlateDrawEffect::None : ESlateDrawEffect::DisabledEffect,
InWidgetStyle.GetColorAndOpacityTint() * GetColorAndOpacity().GetColor(InWidgetStyle)
);
}
else
{
const FVector2D LastDesiredSize = TextLayoutCache->GetDesiredSize();

// OnPaint will also update the text layout cache if required
LayerId = TextLayoutCache->OnPaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, ShouldBeEnabled(bParentEnabled));

const FVector2D NewDesiredSize = TextLayoutCache->GetDesiredSize();

// HACK: Due to the nature of wrapping and layout, we may have been arranged in a different box than what we were cached with. Which
// might update wrapping, so make sure we always set the desired size to the current size of the text layout, which may have changed
// during paint.
const bool bCanWrap = WrapTextAt.Get() > 0 || AutoWrapText.Get();

if (bCanWrap && !NewDesiredSize.Equals(LastDesiredSize))
{
const_cast<STextBlock*>(this)->Invalidate(EInvalidateWidgetReason::Layout);
}
}

return LayerId;
}

// Callstack
UnrealEditor-Slate.dll!STextBlock::OnPaint(const FPaintArgs & Args, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 255 C++
UnrealEditor-SlateCore.dll!SWidget::Paint(const FPaintArgs & Args, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 1543 C++
UnrealEditor-SlateCore.dll!SPanel::PaintArrangedChildren(const FPaintArgs & Args, const FArrangedChildren & ArrangedChildren, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 31 C++
UnrealEditor-SlateCore.dll!SPanel::OnPaint(const FPaintArgs & Args, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 12 C++
UnrealEditor-SlateCore.dll!SWidget::Paint(const FPaintArgs & Args, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 1543 C++
...
UnrealEditor-SlateCore.dll!SPanel::PaintArrangedChildren(const FPaintArgs & Args, const FArrangedChildren & ArrangedChildren, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 31 C++
UnrealEditor-SlateCore.dll!SPanel::OnPaint(const FPaintArgs & Args, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 12 C++
UnrealEditor-SlateCore.dll!SWidget::Paint(const FPaintArgs & Args, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 1543 C++
UnrealEditor-SlateCore.dll!SOverlay::OnPaint(const FPaintArgs & Args, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 200 C++
UnrealEditor-SlateCore.dll!SWidget::Paint(const FPaintArgs & Args, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 1543 C++
UnrealEditor-SlateCore.dll!SCompoundWidget::OnPaint(const FPaintArgs & Args, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 46 C++
UnrealEditor-SlateCore.dll!SWidget::Paint(const FPaintArgs & Args, const FGeometry & AllottedGeometry, const FSlateRect & MyCullingRect, FSlateWindowElementList & OutDrawElements, int LayerId, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 1543 C++
UnrealEditor-SlateCore.dll!SWindow::PaintSlowPath(const FSlateInvalidationContext & Context) Line 2073 C++
UnrealEditor-SlateCore.dll!FSlateInvalidationRoot::PaintInvalidationRoot(const FSlateInvalidationContext & Context) Line 399 C++
UnrealEditor-SlateCore.dll!SWindow::PaintWindow(double CurrentTime, float DeltaTime, FSlateWindowElementList & OutDrawElements, const FWidgetStyle & InWidgetStyle, bool bParentEnabled) Line 2105 C++
UnrealEditor-Slate.dll!FSlateApplication::DrawWindowAndChildren(const TSharedRef<SWindow,1> & WindowToDraw, FDrawWindowArgs & DrawWindowArgs) Line 1106 C++
UnrealEditor-Slate.dll!FSlateApplication::PrivateDrawWindows(TSharedPtr<SWindow,1> DrawOnlyThisWindow) Line 1338 C++
UnrealEditor-Slate.dll!FSlateApplication::DrawWindows() Line 1060 C++
UnrealEditor-Slate.dll!FSlateApplication::TickAndDrawWidgets(float DeltaTime) Line 1625 C++
UnrealEditor-Slate.dll!FSlateApplication::Tick(ESlateTickType TickType) Line 1482 C++
UnrealEditor.exe!FEngineLoop::Tick() Line 5325 C++
[Inline Frame] UnrealEditor.exe!EngineTick() Line 62 C++
UnrealEditor.exe!GuardedMain(const wchar_t * CmdLine) Line 183 C++
UnrealEditor.exe!GuardedMainWrapper(const wchar_t * CmdLine) Line 147 C++
UnrealEditor.exe!LaunchWindowsStartup(HINSTANCE__ * hInInstance, HINSTANCE__ * hPrevInstance, char * __formal, int nCmdShow, const wchar_t * CmdLine) Line 283 C++
UnrealEditor.exe!WinMain(HINSTANCE__ * hInInstance, HINSTANCE__ * hPrevInstance, char * pCmdLine, int nCmdShow) Line 330 C++
[External Code]

Second, an execution flow by Paint.

The flows are branched at FSlateApplication::PrivateDrawWindows(). In the function, DrawPrepass() is called at line #1292, and DrawWindowAndChildren() is called at line #1338. Respectively, Prepass and Paint. Engine just invalidate the widget in Paint flow, so we only need to look into Prepass flow.

Calculating a length of text wrap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
FVector2D FSlateTextBlockLayout::ComputeDesiredSize(const FWidgetDesiredSizeArgs& InWidgetArgs, const float InScale, const FTextBlockStyle& InTextStyle)
{
// Cache the wrapping rules so that we can recompute the wrap at width in paint.
CachedWrapTextAt = InWidgetArgs.WrapTextAt;
bCachedAutoWrapText = InWidgetArgs.AutoWrapText;

const ETextTransformPolicy PreviousTransformPolicy = TextLayout->GetTransformPolicy();

// Set the text layout information
TextLayout->SetScale(InScale);
TextLayout->SetWrappingWidth(CalculateWrappingWidth());
TextLayout->SetWrappingPolicy(InWidgetArgs.WrappingPolicy);
TextLayout->SetTransformPolicy(InWidgetArgs.TransformPolicy);
TextLayout->SetMargin(InWidgetArgs.Margin);
TextLayout->SetJustification(InWidgetArgs.Justification);
TextLayout->SetLineHeightPercentage(InWidgetArgs.LineHeightPercentage);

// Has the transform policy changed? If so we need a full refresh as that is destructive to the model text
if (PreviousTransformPolicy != TextLayout->GetTransformPolicy())
{
Marshaller->MakeDirty();
}

// Has the style used for this text block changed?
if (!IsStyleUpToDate(InTextStyle))
{
TextLayout->SetDefaultTextStyle(InTextStyle);
Marshaller->MakeDirty(); // will regenerate the text using the new default style
}

{
bool bRequiresTextUpdate = false;
const FText& TextToSet = InWidgetArgs.Text;
if (!TextLastUpdate.IdenticalTo(TextToSet))
{
// The pointer used by the bound text has changed, however the text may still be the same - check that now
if (!TextLastUpdate.IsDisplayStringEqualTo(TextToSet))
{
// The source text has changed, so update the internal text
bRequiresTextUpdate = true;
}

// Update this even if the text is lexically identical, as it will update the pointer compared by IdenticalTo for the next Tick
TextLastUpdate = FTextSnapshot(TextToSet);
}

if (bRequiresTextUpdate || Marshaller->IsDirty())
{
UpdateTextLayout(TextToSet);
}
}

{
const FText& HighlightTextToSet = InWidgetArgs.HighlightText;
if (!HighlightTextLastUpdate.IdenticalTo(HighlightTextToSet))
{
// The pointer used by the bound text has changed, however the text may still be the same - check that now
if (!HighlightTextLastUpdate.IsDisplayStringEqualTo(HighlightTextToSet))
{
UpdateTextHighlights(HighlightTextToSet);
}

// Update this even if the text is lexically identical, as it will update the pointer compared by IdenticalTo for the next Tick
HighlightTextLastUpdate = FTextSnapshot(HighlightTextToSet);
}
}

// We need to update our size if the text layout has become dirty
TextLayout->UpdateIfNeeded();

return TextLayout->GetSize();
}

The function FSlateTextBlockLayout::ComputeDesiredSize() is called during Prepass flow. Here, bCachedAutoWrapText caches the value of InWidgetArgs.AutoWrapText. This will be used at CalculateWrappingWidth() later.

1
2
3
4
5
6
7
8
9
10
11
12
float FSlateTextBlockLayout::CalculateWrappingWidth() const
{
// Text wrapping can either be used defined (WrapTextAt), automatic (bAutoWrapText and CachedSize),
// or a mixture of both. Take whichever has the smallest value (>1)
float WrappingWidth = CachedWrapTextAt;
if (bCachedAutoWrapText && CachedSize.X >= 1.0f)
{
WrappingWidth = (WrappingWidth >= 1.0f) ? FMath::Min(WrappingWidth, CachedSize.X) : CachedSize.X;
}

return FMath::Max(0.0f, WrappingWidth);
}

The CachedWrapTextAt will be the same with the value set by option WrapTextAt in editor. And, the CachedSize depends on the size of panel where the TextBlock resides in. In the example we are using, the variables would have a value like below:

  • CachedWrapTextAt = 0
  • CachedSize.X = 100

Because the width of SizeBox is 100 and we set the option WrapTextAt as 0. The function determines the length of wrapping, but it is not for the logic about how to divide texts or how to break lines. So, look back on FSlateTextBlockLayout::ComputeDesiredSize().

UpdateLayout when it is dirty

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// SlateTextBlockLayout.cpp

...
// We need to update our size if the text layout has become dirty
TextLayout->UpdateIfNeeded();
...

// TextLayout.cpp

void FTextLayout::UpdateIfNeeded()
{
if (CachedLayoutGeneration != GSlateLayoutGeneration)
{
CachedLayoutGeneration = GSlateLayoutGeneration;
DirtyFlags |= ETextLayoutDirtyState::Layout;
DirtyAllLineModels(ELineModelDirtyState::All);
}

const bool bHasChangedLayout = !!(DirtyFlags & ETextLayoutDirtyState::Layout);
const bool bHasChangedHighlights = !!(DirtyFlags & ETextLayoutDirtyState::Highlights);

if ( bHasChangedLayout )
{
// if something has changed then create a new View
UpdateLayout();
}

// If the layout has changed, we always need to update the highlights
if ( bHasChangedLayout || bHasChangedHighlights)
{
UpdateHighlights();
}
}

In the function, there is some code to call FTextLayout::UpdateIfNeeded(). Oh, the UpdateLayout() looks like the one we wanted. The code will be executed when bHasChangedLayout is true, and the value is usually set by SetWrappingWidth().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void FTextLayout::SetWrappingWidth( float Value )
{
const bool WasWrapping = WrappingWidth > 0.0f;
const bool IsWrapping = Value > 0.0f;

if ( WrappingWidth != Value )
{
WrappingWidth = Value;
DirtyFlags |= ETextLayoutDirtyState::Layout;

if ( WasWrapping != IsWrapping )
{
// Changing from wrapping/not-wrapping will affect the wrapping information for *all lines*
// Clear out the entire cache so it gets regenerated on the text call to FlowLayout
DirtyAllLineModels(ELineModelDirtyState::WrappingInformation);
}
}
}

Suppose you switch the option AutoWrapText from false into true. Here, DirtyFlags will flag the ETextLayoutDirtyState::Layout, which is 1. Therefore, !!(DirtyFlags & ETextLayoutDirtyState::Layout) turns into 1. The bHasChangedLayout becomes 1, too.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void FTextLayout::UpdateLayout()
{
SCOPE_CYCLE_COUNTER(STAT_SlateTextLayout);

ClearView();
BeginLayout();

FlowLayout();
JustifyLayout();
MarginLayout();

EndLayout();

DirtyFlags &= ~ETextLayoutDirtyState::Layout;
}

The ClearView() and BeginLayout() are not important in this post. Plus, they do not something important either.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void FTextLayout::FlowLayout()
{
const float WrappingDrawWidth = GetWrappingDrawWidth();

TArray< TSharedRef< ILayoutBlock > > SoftLine;
for (int32 LineModelIndex = 0; LineModelIndex < LineModels.Num(); LineModelIndex++)
{
FLineModel& LineModel = LineModels[ LineModelIndex ];
CalculateLineTextDirection(LineModel);
FlushLineTextShapingCache(LineModel);
CreateLineWrappingCache(LineModel);

FlowLineLayout(LineModelIndex, WrappingDrawWidth, SoftLine);
}
}

In the FlowLayout(), the code that calls CreateLineWrappingCache() is a point since the CreateLineWrappingCache() creates data for wrapping text.

Break lines (1/3); Separating text into slices

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void FTextLayout::CreateLineWrappingCache(FLineModel& LineModel)
{
if (!(LineModel.DirtyFlags & ELineModelDirtyState::WrappingInformation))
{
return;
}

LineModel.BreakCandidates.Empty();
LineModel.DirtyFlags &= ~ELineModelDirtyState::WrappingInformation;

for (int32 RunIndex = 0; RunIndex < LineModel.Runs.Num(); RunIndex++)
{
LineModel.Runs[RunIndex].ClearCache();
}

const bool IsWrapping = WrappingWidth > 0.0f;
if (!IsWrapping)
{
return;
}

// If we've not yet been provided with a custom line break iterator, then just use the default one
if (!LineBreakIterator.IsValid())
{
LineBreakIterator = FBreakIterator::CreateLineBreakIterator();
}

LineBreakIterator->SetStringRef(&LineModel.Text.Get());

int32 PreviousBreak = 0;
int32 CurrentBreak = 0;
int32 CurrentRunIndex = 0;

while( ( CurrentBreak = LineBreakIterator->MoveToNext() ) != INDEX_NONE )
{
LineModel.BreakCandidates.Add( CreateBreakCandidate(/*OUT*/CurrentRunIndex, LineModel, PreviousBreak, CurrentBreak) );
PreviousBreak = CurrentBreak;
}

LineBreakIterator->ClearString();
}

In this function, we found some variables that have a name of LineBreak. Let us check what the line break iterator does.

1
2
3
4
TSharedRef<IBreakIterator> FBreakIterator::CreateLineBreakIterator()
{
return MakeShareable(new FICULineBreakIterator());
}

The LinBreakIterator is a line break iterator using the implementation of ICU(International Components for Unicode)’s break iterator. The break iterator does a job of finding a location of boundaries in text. Visit here for more details. To summarize, the break iterator can find where each word ends. For example, we have a text of Text Block Test and the break iterator can find locations just like this Text (HERE)Block (HERE)Test(HERE). So, let us see how it works.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int32 FICULineBreakIterator::MoveToNextImpl()
{
TSharedRef<icu::BreakIterator> LineBrkIt = GetInternalLineBreakIterator();
FICUTextCharacterIterator& CharIt = static_cast<FICUTextCharacterIterator&>(LineBrkIt->getText());

int32 InternalPosition = CharIt.SourceIndexToInternalIndex(CurrentPosition);

// For Hangul using per-word wrapping, we walk forward to the last Hangul character in the word and use that as the starting point for the
// line-break iterator, as this will correctly handle the remaining Geumchik wrapping rules, without also causing per-syllable wrapping
if (GetHangulTextWrappingMethod() == EHangulTextWrappingMethod::PerWord)
{
CharIt.setIndex32(InternalPosition);

if (IsHangul(CharIt.current32()))
{
// Walk to the end of the Hangul characters
while (CharIt.hasNext() && IsHangul(CharIt.next32()))
{
InternalPosition = CharIt.getIndex();
}
}
}

InternalPosition = LineBrkIt->following(InternalPosition);
CurrentPosition = CharIt.InternalIndexToSourceIndex(InternalPosition);

return CurrentPosition;
}

The MoveToNext() calls the MoveToNextImpl(). And, the MoveToNextImpl() change the InternalPosition, which is used for finding a location in text.

1
2
3
4
5
6
7
8
9
10
11
// UnrealEngine/Engine/Source/ThirdParty/ICU/icu4c-64_1/include/unicode/brkiter.h

/**
* Advance the iterator to the first boundary following the specified offset.
* The value returned is always greater than the offset or
* the value BreakIterator.DONE
* @param offset the offset to begin scanning.
* @return The first boundary after the specified offset.
* @stable ICU 2.0
*/
virtual int32_t following(int32_t offset) = 0;

The InternalPosition is passed into following and it is the code of ICU library.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[index] 0123456789...
[array] Text Block Test

[flow]
PreviousBreak = 0, CurrentBreak = 0
MoveToNext()
PreviousBreak = 0, CurrentBreak = 5
CreateBreakCandidate()
PreviousBreak = 5, CurrentBreak = 5
MoveToNext()
PreviousBreak = 5, CurrentBreak = 11
CreateBreakCandidate()
PreviousBreak = 11, CurrentBreak = 11
...

In our test text, the flow looks like above.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct FBreakCandidate
{
/** Range inclusive of trailing whitespace, as used to visually display and interact with the text */
FTextRange ActualRange;
/** Range exclusive of trailing whitespace, as used to perform wrapping on a word boundary */
FTextRange TrimmedRange;
/** Measured size inclusive of trailing whitespace, as used to visually display and interact with the text */
FVector2D ActualSize;
/** Measured width exclusive of trailing whitespace, as used to perform wrapping on a word boundary */
float TrimmedWidth;
/** If this break candidate has trailing whitespace, this is the width of the first character of the trailing whitespace */
float FirstTrailingWhitespaceCharWidth;

int16 MaxAboveBaseline;
int16 MaxBelowBaseline;

int8 Kerning;

#if TEXT_LAYOUT_DEBUG
FString DebugSlice;
#endif
};

A FBreakCandidate will be inserted into BreakCandidates each iteration. It seems the FBreakCandidate knows the size of word (or a part of text). What happened in CreateBreakCandidate() ? How could they know the actual size of text ?

Break lines (2/3); Measuring size of each slice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
FTextLayout::FBreakCandidate FTextLayout::CreateBreakCandidate( int32& OutRunIndex, FLineModel& Line, int32 PreviousBreak, int32 CurrentBreak )
{
...
// We need to consider the Runs when detecting and measuring the text lengths of Lines because
// the font style used makes a difference.
const int32 FirstRunIndexChecked = OutRunIndex;
for (; OutRunIndex < Line.Runs.Num(); OutRunIndex++)
{
FRunModel& Run = Line.Runs[ OutRunIndex ];
const FTextRange Range = Run.GetTextRange();

FVector2D SliceSize;
FVector2D SliceSizeWithoutTrailingWhitespace;
int32 StopIndex = PreviousBreak;

WhitespaceStopIndex = StopIndex = FMath::Min( Range.EndIndex, CurrentBreak );
int32 BeginIndex = FMath::Max( PreviousBreak, Range.BeginIndex );

while( WhitespaceStopIndex > BeginIndex && FText::IsWhitespace( (*Line.Text)[ WhitespaceStopIndex - 1 ] ) )
{
--WhitespaceStopIndex;
}

if ( BeginIndex == StopIndex )
{
// This slice is empty, no need to adjust anything
SliceSize = SliceSizeWithoutTrailingWhitespace = FVector2D::ZeroVector;
}
else if ( BeginIndex == WhitespaceStopIndex )
{
// This slice contains only whitespace, no need to adjust SliceSizeWithoutTrailingWhitespace
SliceSize = Run.Measure( BeginIndex, StopIndex, Scale, RunTextContext );
SliceSizeWithoutTrailingWhitespace = FVector2D::ZeroVector;
}
else if ( WhitespaceStopIndex != StopIndex )
{
// This slice contains trailing whitespace, measure the text size, then add on the whitespace size
SliceSize = SliceSizeWithoutTrailingWhitespace = Run.Measure( BeginIndex, WhitespaceStopIndex, Scale, RunTextContext );
const float WhitespaceWidth = Run.Measure( WhitespaceStopIndex, StopIndex, Scale, RunTextContext ).X;
SliceSize.X += WhitespaceWidth;

// We also need to measure the width of the first piece of trailing whitespace
if ( WhitespaceStopIndex + 1 == StopIndex )
{
// Only have one piece of whitespace
FirstTrailingWhitespaceCharWidth = WhitespaceWidth;
}
else
{
// Deliberately use the run version of Measure as we don't want the run model to cache this measurement since it may be out of order and break the binary search
FirstTrailingWhitespaceCharWidth = Run.GetRun()->Measure( WhitespaceStopIndex, WhitespaceStopIndex + 1, Scale, RunTextContext ).X;
}
}
else
{
// This slice contains no whitespace, both sizes are the same and can use the same measurement
SliceSize = SliceSizeWithoutTrailingWhitespace = Run.Measure( BeginIndex, StopIndex, Scale, RunTextContext );
}
...
}

The CreateBreakCandidate() function is quite big size, about 200 lines. But the core of function is to calculate a size of slice. Do you remember the variable CurrentBreak that indicates where each slice ends ? Here, the function make a slice according to CurrentBreak and trim it. Trimming happens in while statement, which decreases the WhitespaceStopIndex until it indicates an end of last word.

The WhitespaceStopIndex would be 4 in our test text. That is because the index of first whitespace is 4 in Text Block Test. Eventually, we will enter the function Measure() as the slice is not empty. The only case that Measure() not called is when BeginIndex == StopIndex is true, in other words CurrentBreak == 0.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// TextLayout.cpp
FVector2D FTextLayout::FRunModel::Measure(int32 BeginIndex, int32 EndIndex, float InScale, const FRunTextContext& InTextContext)
{
FVector2D Size = Run->Measure(BeginIndex, EndIndex, InScale, InTextContext);

MeasuredRanges.Add( FTextRange( BeginIndex, EndIndex ) );
MeasuredRangeSizes.Add(Size);

return Size;
}

// SlateTextRun.cpp
FVector2D FSlateTextRun::Measure( int32 BeginIndex, int32 EndIndex, float Scale, const FRunTextContext& TextContext ) const
{
const FVector2D ShadowOffsetToApply((EndIndex == Range.EndIndex) ? FMath::Abs(Style.ShadowOffset.X * Scale) : 0.0f, FMath::Abs(Style.ShadowOffset.Y * Scale));

// Offset the measured shaped text by the outline since the outline was not factored into the size of the text
// Need to add the outline offsetting to the beginning and the end because it surrounds both sides.
const float ScaledOutlineSize = Style.Font.OutlineSettings.OutlineSize * Scale;
const FVector2D OutlineSizeToApply((BeginIndex == Range.BeginIndex ? ScaledOutlineSize : 0) + (EndIndex == Range.EndIndex ? ScaledOutlineSize : 0), ScaledOutlineSize);

if (EndIndex - BeginIndex == 0)
{
return FVector2D(0, GetMaxHeight(Scale)) + ShadowOffsetToApply + OutlineSizeToApply;
}

// Use the full text range (rather than the run range) so that text that spans runs will still be shaped correctly
return ShapedTextCacheUtil::MeasureShapedText(TextContext.ShapedTextCache, FCachedShapedTextKey(FTextRange(0, Text->Len()), Scale, TextContext, Style.Font), FTextRange(BeginIndex, EndIndex), **Text) + ShadowOffsetToApply + OutlineSizeToApply;
}

We will get a FVector2D from FSlateTextRun::Measure(), which is the size of slice. The code Run->Measure() is the same with calling ShapedTextCacheUtil::MeasureShapedText() when you are using a TextBlock. Calculating shadow offset is not important in this post, so we need to focus on ShapedTextCacheUtil::MeasureShapedText().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ShapedTextFwd.h
typedef TSharedRef<const FShapedGlyphSequence> FShapedGlyphSequenceRef;

// ShapedTextCache.cpp
FVector2D ShapedTextCacheUtil::MeasureShapedText(const FShapedTextCacheRef& InShapedTextCache, const FCachedShapedTextKey& InRunKey, const FTextRange& InMeasureRange, const TCHAR* InText)
{
// Get the shaped text for the entire run and try and take a sub-measurement from it - this can help minimize the amount of text shaping that needs to be done when measuring text
FShapedGlyphSequenceRef ShapedText = InShapedTextCache->FindOrAddShapedText(InRunKey, InText);

TOptional<int32> MeasuredWidth = ShapedText->GetMeasuredWidth(InMeasureRange.BeginIndex, InMeasureRange.EndIndex);
if (!MeasuredWidth.IsSet())
{
FCachedShapedTextKey MeasureKey = InRunKey;
MeasureKey.TextRange = InMeasureRange;

// Couldn't measure the sub-range, try and measure from a shape of the specified range
ShapedText = InShapedTextCache->FindOrAddShapedText(MeasureKey, InText);
MeasuredWidth = ShapedText->GetMeasuredWidth();
}

check(MeasuredWidth.IsSet());
return FVector2D(MeasuredWidth.GetValue(), ShapedText->GetMaxTextHeight());
}

As you can see, the FShapedGlyphSequenceRef is a shared reference of FShapedGlyphSequence. Then, what the hell is FShapedGlyphSequence ? And what it does ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
FShapedGlyphSequenceRef FShapedTextCache::FindOrAddShapedText(const FCachedShapedTextKey& InKey, const TCHAR* InText)
{
FShapedGlyphSequencePtr ShapedText = FindShapedText(InKey);

if (!ShapedText.IsValid())
{
ShapedText = AddShapedText(InKey, InText);
}

return ShapedText.ToSharedRef();
}

FShapedGlyphSequencePtr FShapedTextCache::FindShapedText(const FCachedShapedTextKey& InKey) const
{
FShapedGlyphSequencePtr ShapedText = CachedShapedText.FindRef(InKey);

if (ShapedText.IsValid() && !ShapedText->IsDirty())
{
return ShapedText;
}

return nullptr;
}

FShapedGlyphSequenceRef FShapedTextCache::AddShapedText(const FCachedShapedTextKey& InKey, FShapedGlyphSequenceRef InShapedText)
{
CachedShapedText.Add(InKey, InShapedText);
return InShapedText;
}

First, engine tries to find if there is already existing one. If not, creates new one and insert it into the cache.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// FontCache.h
/** Information for rendering a shaped text sequence */
class SLATECORE_API FShapedGlyphSequence
{
...
/** Array of glyphs in this sequence. This data will be ordered so that you can iterate and draw left-to-right, which means it will be backwards for right-to-left languages */
TArray<FShapedGlyphEntry> GlyphsToRender;
...

/** Information for rendering one glyph in a shaped text sequence */
struct FShapedGlyphEntry
{
...
/** The index of this glyph from the source text. The source indices may skip characters if the sequence contains ligatures, additionally, some characters produce multiple glyphs leading to duplicate source indices */
int32 SourceIndex = 0;
/** The amount to advance in X before drawing the next glyph in the sequence */
int16 XAdvance = 0;
...

// FontCache.cpp
FShapedGlyphSequence::FShapedGlyphSequence(TArray<FShapedGlyphEntry> InGlyphsToRender, const int16 InTextBaseline, const uint16 InMaxTextHeight, const UObject* InFontMaterial, const FFontOutlineSettings& InOutlineSettings, const FSourceTextRange& InSourceTextRange)
: GlyphsToRender(MoveTemp(InGlyphsToRender))
, TextBaseline(InTextBaseline)
, MaxTextHeight(InMaxTextHeight)
, FontMaterial(InFontMaterial)
, OutlineSettings(InOutlineSettings)
, SequenceWidth(0)
, GlyphFontFaces()
, SourceIndicesToGlyphData(InSourceTextRange)
{
const int32 NumGlyphsToRender = GlyphsToRender.Num();
for (int32 CurrentGlyphIndex = 0; CurrentGlyphIndex < NumGlyphsToRender; ++CurrentGlyphIndex)
{
const FShapedGlyphEntry& CurrentGlyph = GlyphsToRender[CurrentGlyphIndex];

// Track unique font faces
if (CurrentGlyph.FontFaceData->FontFace.IsValid())
{
GlyphFontFaces.AddUnique(CurrentGlyph.FontFaceData->FontFace);
}

// Update the measured width
SequenceWidth += CurrentGlyph.XAdvance;
...

The FShapedGlyphSequence has a TArray of FShapedGlyphEntry. And the FShapedGlyphEntry has several properties such as SourceIndex and XAdvance. Looks like the FShapedGlyphEntry has properties responding each character in text, and the FShapedGlyphSequence has properties responding whole text. The properties are for how to render the text appropriately. So here, we can regard the term Glyph as one single character.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// SlateTextShaper.cpp
void FSlateTextShaper::PerformKerningOnlyTextShaping(const TCHAR* InText, const int32 InTextStart, const int32 InTextLen, const FSlateFontInfo& InFontInfo, const float InFontScale, TArray<FShapedGlyphEntry>& OutGlyphsToRender) const
{
...
for (int32 SequenceCharIndex = 0; SequenceCharIndex < KerningOnlyTextSequenceEntry.TextLength; ++SequenceCharIndex)
{
const int32 CurrentCharIndex = KerningOnlyTextSequenceEntry.TextStartIndex + SequenceCharIndex;
const TCHAR CurrentChar = InText[CurrentCharIndex];

if (!InsertSubstituteGlyphs(InText, CurrentCharIndex, ShapedGlyphFaceData, AdvanceCache, OutGlyphsToRender, LetterSpacingScaled))
{
uint32 GlyphIndex = FT_Get_Char_Index(KerningOnlyTextSequenceEntry.FaceAndMemory->GetFace(), CurrentChar);

// If the given font can't render that character (as the fallback font may be missing), try again with the fallback character
if (CurrentChar != 0 && GlyphIndex == 0)
{
GlyphIndex = FT_Get_Char_Index(KerningOnlyTextSequenceEntry.FaceAndMemory->GetFace(), SlateFontRendererUtils::InvalidSubChar);
}

int16 XAdvance = 0;
{
FT_Fixed CachedAdvanceData = 0;
if (AdvanceCache->FindOrCache(GlyphIndex, CachedAdvanceData))
{
XAdvance = FreeTypeUtils::Convert26Dot6ToRoundedPixel<int16>((CachedAdvanceData + (1<<9)) >> 10);
}
}

const int32 CurrentGlyphEntryIndex = OutGlyphsToRender.AddDefaulted();
FShapedGlyphEntry& ShapedGlyphEntry = OutGlyphsToRender[CurrentGlyphEntryIndex];
ShapedGlyphEntry.FontFaceData = ShapedGlyphFaceData;
ShapedGlyphEntry.GlyphIndex = GlyphIndex;
ShapedGlyphEntry.SourceIndex = CurrentCharIndex;
ShapedGlyphEntry.XAdvance = XAdvance;
...

Usually, the XAdvande is determined at FSlateTextShaper::PerformKerningOnlyTextShaping(). Engine uses the FreeType library for getting a estimated size of character when it rendered. The GlyphIndex is calculated based on font and character value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
bool FFreeTypeAdvanceCache::FindOrCache(const uint32 InGlyphIndex, FT_Fixed& OutCachedAdvance)
{
// Try and find the advance from the cache...
{
const FT_Fixed* FoundCachedAdvance = AdvanceMap.Find(InGlyphIndex);
if (FoundCachedAdvance)
{
OutCachedAdvance = *FoundCachedAdvance;
return true;
}
}

FreeTypeUtils::ApplySizeAndScale(Face, FontSize, FontScale);

// No cached data, go ahead and add an entry for it...
const FT_Error Error = FT_Get_Advance(Face, InGlyphIndex, LoadFlags, &OutCachedAdvance);
if (Error == 0)
{
if (!FT_IS_SCALABLE(Face) && FT_HAS_FIXED_SIZES(Face))
{
// Fixed size fonts don't support scaling, but we calculated the scale to use for the glyph in ApplySizeAndScale
OutCachedAdvance = FT_MulFix(OutCachedAdvance, ((LoadFlags & FT_LOAD_VERTICAL_LAYOUT) ? Face->size->metrics.y_scale : Face->size->metrics.x_scale));
}

AdvanceMap.Add(InGlyphIndex, OutCachedAdvance);
return true;
}

return false;
}

The code AdvanceCache->FindOrCache(GlyphIndex, CachedAdvanceData) finds at cache, but it creates new one and cache it if could not find. The FT_Get_Advance() returns the result with parameter &OutCachedAdvance. We can get a size of single character through the function because the value GlyphIndex includes information of font and character value.

In our test text Text Block Test, the result is like below:

Index Character GlyphIndex XAdvance
0 T 55 18
1 e 72 17
2 x 91 16
3 t 87 11
4 3 8
5 B 37 20
6 l 79 9
7 o 82 18
8 c 70 17
9 k 78 17
10 3 8
11 T 55 18
12 e 72 17
13 s 86 17
14 t 87 11

You can see that the same character has the same XAdvance value. For example, The character T has 55 of GlyphIndex and 18 of XAdvance. Go back to the ShapedTextCacheUtil::MeasureShapedText(), that is why the MeasuredWidth has a value of 220 ≒ 222 = 18 + 17 + ... + 17 + 11. The difference 2 occurs by the kerning.

The final width may differ a little bit because some combination of characters need a kerning. For example, though e and k have the same XAdvance value 17, a combination Te has a small size than a combination Tk. Because in the combination Te, e can stick to T closer than k in Tk. In other words, a character T can have XAdvance of 17 in the combinations such as Ta/Tc/Td, and so on. Otherwise such as Tb/Tf/Th, it can have XAdvance of 18.

Break lines (3/3); Creating lines with wrapping

Go back to the FTextLayout::CreateLineWrappingCache(), now we can wrap text according to size (exactly, width) of each slice. All slices are stored at the container BreakCandidates. In our test text Text Block Test, the result is like below:

BreakCandidates ActualRange TrimmedRange
0 Text [0, 5) Text [0, 4)
1 Block [5, 11) Block [5, 10)
2 Test [11, 15) Test [11, 15)

[0, 5) is equal to [0, 4]

Do you remember there is a code calls FTextLayout::FlowLineLayout() in FTextLayout::FlowLayout() ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void FTextLayout::FlowLineLayout(const int32 LineModelIndex, const float WrappingDrawWidth, TArray<TSharedRef<ILayoutBlock>>& SoftLine)
{
...
float CurrentWidth = 0.0f;
for (int32 BreakIndex = 0; BreakIndex < LineModel.BreakCandidates.Num(); BreakIndex++)
{
const FBreakCandidate& Break = LineModel.BreakCandidates[ BreakIndex ];

const bool IsLastBreak = BreakIndex + 1 == LineModel.BreakCandidates.Num();
const bool IsFirstBreakOnSoftLine = CurrentWidth == 0.0f;
const int8 Kerning = ( IsFirstBreakOnSoftLine ) ? Break.Kerning : 0;
const bool BreakDoesFit = CurrentWidth + Break.ActualSize.X + Kerning <= WrappingDrawWidth;
const bool BreakWithoutTrailingWhitespaceDoesFit = CurrentWidth + Break.TrimmedWidth + Kerning <= WrappingDrawWidth;
...

Here, we accumulate a width of each BreakCandidate on CurrentWidth. And wrapping text occurs whenever CurrentWidth almost reaches to WrappingDrawWidth.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
else if ( !BreakDoesFit || IsLastBreak )
{
const bool IsFirstBreak = BreakIndex == 0;

const FBreakCandidate& FinalBreakOnSoftLine = ( !IsFirstBreak && !IsFirstBreakOnSoftLine && !BreakWithoutTrailingWhitespaceDoesFit ) ? LineModel.BreakCandidates[ --BreakIndex ] : Break;

// We want the wrapped line width to contain the first piece of trailing whitespace for a line, however we only do this if we have trailing whitespace
// otherwise very long non-breaking words can cause the wrapped line width to expand beyond the desired wrap width
float WrappedLineWidth = CurrentWidth;
if ( BreakWithoutTrailingWhitespaceDoesFit )
{
// This break has trailing whitespace
WrappedLineWidth += ( FinalBreakOnSoftLine.TrimmedWidth + FinalBreakOnSoftLine.FirstTrailingWhitespaceCharWidth );
}
else
{
// This break is longer than the wrapping point, so make sure and clamp the line size to the given wrapping width
WrappedLineWidth += FinalBreakOnSoftLine.ActualSize.X;
WrappedLineWidth = FMath::Min(WrappedLineWidth, WrappingDrawWidth);
}

// We want wrapped lines to ignore any trailing whitespace when justifying
// If FinalBreakOnSoftLine isn't the current Break, then the size of FinalBreakOnSoftLine (including its trailing whitespace) will have already
// been added to CurrentWidth, so we need to remove that again before adding the trimmed width (which is the width we should justify with)
// We should not attempt to adjust the last break on a soft-line as that might have explicit trailing whitespace
TOptional<float> JustifiedLineWidth;
if ( &FinalBreakOnSoftLine != &LineModel.BreakCandidates.Last() )
{
JustifiedLineWidth = CurrentWidth - (&FinalBreakOnSoftLine == &Break ? 0.0f : FinalBreakOnSoftLine.ActualSize.X) + FinalBreakOnSoftLine.TrimmedWidth;
}

CreateLineViewBlocks( LineModelIndex, FinalBreakOnSoftLine.ActualRange.EndIndex, WrappedLineWidth, JustifiedLineWidth, /*OUT*/CurrentRunIndex, /*OUT*/CurrentRendererIndex, /*OUT*/PreviousBlockEnd, SoftLine );

if ( CurrentRunIndex < LineModel.Runs.Num() && FinalBreakOnSoftLine.ActualRange.EndIndex == LineModel.Runs[ CurrentRunIndex ].GetTextRange().EndIndex )
{
++CurrentRunIndex;
}

PreviousBlockEnd = FinalBreakOnSoftLine.ActualRange.EndIndex;

CurrentWidth = 0.0f;
SoftLine.Reset();
}

Usually, when wrapping text needed, the codes above would be executed. FinalBreakOnSoftLine indicates the BreakCandidate that needs a new line after itself. In our test text Text Block Test, Text could be assigned.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void FTextLayout::CreateLineViewBlocks( int32 LineModelIndex, const int32 StopIndex, const float WrappedLineWidth, const TOptional<float>& JustificationWidth, int32& OutRunIndex, int32& OutRendererIndex, int32& OutPreviousBlockEnd, TArray< TSharedRef< ILayoutBlock > >& OutSoftLine )
{
...
// Add the new block
{
FBlockDefinition BlockDefine;
BlockDefine.ActualRange = FTextRange(BlockBeginIndex, BlockStopIndex);
BlockDefine.Renderer = BlockRenderer;

OutSoftLine.Add( Run.CreateBlock( BlockDefine, Scale, FLayoutBlockTextContext(RunTextContext, BlockTextDirection) ) );
OutPreviousBlockEnd = BlockStopIndex;

// Update the soft line bounds based on this new block (needed within this loop due to bi-directional text, as the extents of the line array are not always the start and end of the range)
const FTextRange& BlockRange = OutSoftLine.Last()->GetTextRange();
SoftLineRange.BeginIndex = FMath::Min(SoftLineRange.BeginIndex, BlockRange.BeginIndex);
SoftLineRange.EndIndex = FMath::Max(SoftLineRange.EndIndex, BlockRange.EndIndex);
}
...
FTextLayout::FLineView LineView;
LineView.Offset = CurrentOffset;
LineView.Size = LineSize;
LineView.TextHeight = UnscaleLineHeight;
LineView.JustificationWidth = JustificationWidth.Get(LineView.Size.X);
LineView.Range = SoftLineRange;
LineView.TextBaseDirection = LineModel.TextBaseDirection;
LineView.ModelIndex = LineModelIndex;
LineView.Blocks.Append( OutSoftLine );

LineViews.Add( LineView );
...

The function FTextLayout::CreateLineViewBlocks() creates new FTextLayout::FLineView and adds it into initialized LineViews. We already cleared the LineViews at the function FTextLayout::ClearView(). In our test txt, after all process, the LineViews will have the value like below:

LineViews Range
0 [0, 5)
1 [5, 11)
2 [11, 15)

Finally, we found that the result of wrapping text. All of prerequisites are for splitting a text. Now we understand how the text can be wrapped in UnrealEngine.

Wrap-up

Text wrapping in UnrealEngine can be divided into 3 major steps.

  1. Separating a text into slices
    Find where each word ends using ICU library.
    Separate text into slices based on the indices.

  2. Measuring size of each slice
    Estimate size of rendered character using FreeType library.
    Apply several modifications such as kerning, shadow, and so on.

  3. Creating lines with wrapping
    Add width until it reaches the wrapping width.
    When it reaches, create new line.

How to rename your project in UnrealEngine

Environment
UnrealEngine branch: 5.0
Visual Studio 2022 version: 17.0.4
Windows 11 Pro build: 22000.493

Overview

Sometimes, you might need to rename your project in some reasons.

  • Just you may be bored with that name.
  • To unify the name same with your team name.
  • Due to change on design of game…etc.

Unfortunately, in those situations, UnrealEngine does not provide any feature to rename your project.
So, in this post, we gonna find out how to rename your project manually.

Prerequisites

Suppose we have a project created from template Third Person with options above.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[ProjectRoot]/Source/SomeProjectA/SomeProjectACharacter.h

UCLASS(config=Game)
class ASomeProjectACharacter : public ACharacter
{
GENERATED_BODY()

/** Camera boom positioning the camera behind the character */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
class USpringArmComponent* CameraBoom;

...

public:
/** Returns CameraBoom subobject **/
FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
/** Returns FollowCamera subobject **/
FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }

UFUCTION(BlueprintPure)
const FString GetSomeString() const { return TEXT("SomeString"); }
};

After the project created, make a simple function GetSomeString() in the character class, which returns some string. We will try to migrate the function for example later in this post. Now, build editor with the combo Development Editor + Win64 and run it.

You can find a character blueprint created from template in the Content/ThirdPersonCPP/Blueprints/ThirdPersonCharacter. Furthermore, the blueprint has a parent class as the cpp class SomeProjectACharacter. It means that the blueprint can use the function we have just made.

I think it would be proper to print that string when character spawned. Make some blueprint nodes for printing that string. Now, compile and save it.

Check it works out. You should be able to see that string SomeString through the screen. Great.

1
2
3
4
5
6
7
8
.../SomeProjectA> git log
commit 32688a379adc5fad30ae7ba9765816684c62d05e (HEAD -> master)
Author: MinCheon Bae <baemincheon@gmail.com>

Initial commit
.../SomeProjectA> git status
On branch master
nothing to commit, working tree clean

I setup the project directory as git repository to clarify what is changed. You do not have to follow this, it is optional. But, you should prepare a gitignore fits in UnrealEngine if you want to follow this. (For example, https://github.com/github/gitignore/blob/main/UnrealEngine.gitignore)

We are all prepared, and let us change the name of project from SomeProjectA into OtherProjectB.

Step #1; Clean-up

First of all, we should remove some files. Some files and folders are generated by other files, so we do not have to care about that files would be generated later. Thus, we would better remove those files or folders listed below:

  • .vs/
  • Binaries/
  • DerivedDataCache/
  • Intermediate/
  • Saved/
  • [ProjectName].sln

You can check if the files or folders are generated. Remove them and generate VisualStudio project files. Then, files and folders related to VisualStudio would be generated. And you can build your project from VisualStudio project. After all, you will see the files and folders listed above are restored.

Plus, that is why gitignore for UnrealEngine contains those files or folders. We do not need them to be version-controlled.

Step #2; Change contents of files

Now it is time to rename the project. There are some files usually contain the name of project in its contents. Therefore, we should change that part of contents. In this goal, we will manipulate the files like…

  • all of files in Config/
    • .ini files such as DefaultEngine.ini
  • all of files in Source/
  • [ProjectName].uproject

Maybe there some files contain the name of project in the folder Content/. Such as an absolute path of media file, and a blueprint class inherites a cpp class whose name contains the name of project. However, basically the files in Content/ are binary type. So, manipulating its contents as text might not ensure a result we expect. In worst case, the manipulation could break some references between blueprints. That is why we handle only the files of text type in this step.

By the way, I recommend you to use notepad++ when manipulating multiple text files, and I will use that in this post. It is open source and provides powerful features. The tool supports Windows and you can install it for ease. You do not have to install it, but I will show an example based on the tool.

Open the notepad++ and drag your project folder from file explorer into notepad++. Now notepad++ would show the folder as list view at the left sidebar.

Right click on Config/ and select Find in Files.... Then, a dialog for search would appear.

Click the tab Find in Files and type SomeProjectA and OtherProjectB respectively at Find what and Replace with. After that, click Replace in Files.

Repeat the steps on Source/ folder.

Open the .uproject file and Replace in similar manner.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.../SomeProjectA> git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: Config/DefaultEngine.ini
modified: SomeProjectA.uproject
modified: Source/SomeProjectA.Target.cs
modified: Source/SomeProjectA/SomeProjectA.Build.cs
modified: Source/SomeProjectA/SomeProjectA.cpp
modified: Source/SomeProjectA/SomeProjectACharacter.cpp
modified: Source/SomeProjectA/SomeProjectACharacter.h
modified: Source/SomeProjectA/SomeProjectAGameMode.cpp
modified: Source/SomeProjectA/SomeProjectAGameMode.h
modified: Source/SomeProjectAEditor.Target.cs

We can see some files changed. Now the contents of file get ready.

Step #3; Change name of files

We have changed the contents of files. Next, let us change the name of files. This also works whole project without Content/ folder with the same reason I mentioned.

Open a powershell prompt and type the command like below:

1
%ProjectRoot% > Get-ChildItem -Recurse -Path Config/* | Rename-Item -NewName { $_.Name.replace("SomeProjectA","OtherProjectB") }

This will change the name of all files in Config/ folder.

1
%ProjectRoot% > Get-ChildItem -Recurse -Path Source/* | Rename-Item -NewName { $_.Name.replace("SomeProjectA","OtherProjectB") }

Apply this on Source/ folder, too. After that, change the name of .uproject file and [ProjectRoot] folder manually.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.../OtherProjectB> git status
On branch master
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: Config/DefaultEngine.ini
deleted: SomeProjectA.uproject
deleted: Source/SomeProjectA.Target.cs
deleted: Source/SomeProjectA/SomeProjectA.Build.cs
deleted: Source/SomeProjectA/SomeProjectA.cpp
deleted: Source/SomeProjectA/SomeProjectA.h
deleted: Source/SomeProjectA/SomeProjectACharacter.cpp
deleted: Source/SomeProjectA/SomeProjectACharacter.h
deleted: Source/SomeProjectA/SomeProjectAGameMode.cpp
deleted: Source/SomeProjectA/SomeProjectAGameMode.h
deleted: Source/SomeProjectAEditor.Target.cs

Untracked files:
(use "git add <file>..." to include in what will be committed)
OtherProjectB.uproject
Source/OtherProjectB.Target.cs
Source/OtherProjectB/
Source/OtherProjectBEditor.Target.cs

Check the result with git status again.

Step #4; Redirect blueprints

Generating VisualStudio project files okay. Building editor on VisualStudio project okay. But, there is one last task to do.

We skipped blueprint files in previous steps. But, some blueprint assets could try to use old cpp classes or codes. Therefore, you will see the dialog while opening the editor. The CDO has been broken.

Some of blueprint classes lost their parent cpp class or get broken. Especially, the blueprint class ThirdPersonCharacter was disconnected with its parent, old cpp class SomeProjectACharacter. We need to fix it.

1
2
3
4
5
6
7
// DefaultEngine.ini

[/Script/Engine.Engine]
+ActiveGameNameRedirects=(OldGameName="SomeProjectA",NewGameName="/Script/OtherProjectB")
+ActiveGameNameRedirects=(OldGameName="/Script/SomeProjectA",NewGameName="/Script/OtherProjectB")
+ActiveClassRedirects=(OldClassName="SomeProjectAGameMode",NewClassName="OtherProjectBGameMode")
+ActiveClassRedirects=(OldClassName="SomeProjectACharacter",NewClassName="OtherProjectBCharacter")

Fortunately, UnrealEngine provides redirecting blueprint classes. You can set the redirection settings in DefaultEngine.ini. I have set the settings like above, and engine will redirect SomeProjectA things into OtherProjectB things. You should create more settings if you need. Because the example settings are from template and your project may have more classes whose CDO broken.

After setting up the redirection, remove Binaries/ + DerivedDataCache/ + Saved/ folders. And repeat build the editor.

Finally we meet again ! The string SomeString was the text we prepared. We have done renaming a project and restoring whole project.

How to create or remove CPP class in UnrealEngine

Environment
UnrealEngine branch: 5.0
Visual Studio 2022 version: 17.0.4
Windows 11 Pro build: 22000.376

Overview

Developing your UnrealEngine project with only the blueprint is not easy because the blueprint has some limitations on functionalities than the native, CPP. For instance, in blueprint you can access the source code tagged by BlueprintCallable, BlueprintType, BlueprintReadOnly, or those series. But, in CPP you can access all of the source code as possible and even you can modify the source code of engine. In other words, using only blueprint is like using a part of UnrealEngine. So eventually, you would want to create CPP class for more functionalities. This post covers that topic; how to create CPP class in UnrealEngine.

Plus, not only creating something but removing something is important. I will tell you how to remove CPP class in UnrealEngine, too. Let us create a project from ThirdPerson template with the options below. I named it as Unreal_5_0.

Creating CPP class; method #1

Open the Content Drawer and click All/C++ Classes folder. After the steps, you can see the option New C++ Class... when you click the Add button. Click it.

In this dialog, you can select a parent of new CPP class. Common Classes tab contains the most commonly used classes, so you should switch to All Classes tab and find an appropriate class if needed.

I chose the class UserWidget as a parent of new CPP class. Click the button Next>.

In this dialog, you can name the new CPP class and save it with some options. I will left the name as default, My[ParentClassName]. The combobox beside name is for selecting a module to include this class. Our project created from ThirdPerson template starts with only one module whose name is the same with project, in this case Unreal_5_0.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// GameProjectUtils.h

/** Where is this class located within the Source folder? */
enum class EClassLocation : uint8
{
/** The class is going to a user defined location (outside of the Public, Private, or Classes) folder for this module */
UserDefined,

/** The class is going to the Public folder for this module */
Public,

/** The class is going to the Private folder for this module */
Private,

/** The class is going to the Classes folder for this module */
Classes,
};

The radio button Class Type is for selecting a location of new CPP class. The enum value is UserDefined in default, but it would be forced to Public or Private when you select one of the radio buttons.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// SNewClassDialog.cpp

void SNewClassDialog::OnClassLocationChanged(GameProjectUtils::EClassLocation InLocation)
{
const FString AbsoluteClassPath = FPaths::ConvertRelativePathToFull(NewClassPath) / ""; // Ensure trailing /

GameProjectUtils::EClassLocation TmpClassLocation = GameProjectUtils::EClassLocation::UserDefined;
GameProjectUtils::GetClassLocation(AbsoluteClassPath, *SelectedModuleInfo, TmpClassLocation);

const FString RootPath = SelectedModuleInfo->ModuleSourcePath;
const FString PublicPath = RootPath / "Public" / ""; // Ensure trailing /
const FString PrivatePath = RootPath / "Private" / ""; // Ensure trailing /

// Update the class path to be rooted to the Public or Private folder based on InVisibility
switch (InLocation)
{
case GameProjectUtils::EClassLocation::Public:
if (AbsoluteClassPath.StartsWith(PrivatePath))
{
NewClassPath = AbsoluteClassPath.Replace(*PrivatePath, *PublicPath);
}
else if (AbsoluteClassPath.StartsWith(RootPath))
{
NewClassPath = AbsoluteClassPath.Replace(*RootPath, *PublicPath);
}
else
{
NewClassPath = PublicPath;
}
break;

case GameProjectUtils::EClassLocation::Private:
if (AbsoluteClassPath.StartsWith(PublicPath))
{
NewClassPath = AbsoluteClassPath.Replace(*PublicPath, *PrivatePath);
}
else if (AbsoluteClassPath.StartsWith(RootPath))
{
NewClassPath = AbsoluteClassPath.Replace(*RootPath, *PrivatePath);
}
else
{
NewClassPath = PrivatePath;
}
break;

default:
break;
}

// Will update ClassVisibility correctly
UpdateInputValidity();
}

With these codes, the radio buttons just change the location of new CPP class. The new CPP class would be included in Public folder when you clicked a radio button Public, vice versa. This setting makes some differences especially onto accessibility.

1
2
3
4
5
6
7
8
9
10
11
// GameProjectUtils.cpp
// GameProjectUtils::GenerateClassHeaderFile()

if ( GetClassLocation(NewHeaderFileName, ModuleInfo, ClassPathLocation) )
{
// If this class isn't Private, make sure and include the API macro so it can be linked within other modules
if ( ClassPathLocation != EClassLocation::Private )
{
ModuleAPIMacro = ModuleInfo.ModuleName.ToUpper() + "_API "; // include a trailing space for the template formatting
}
}
1
2
3
// Definitions.Unreal_5_0.h

#define UNREAL_5_0_API DLLEXPORT

Only the class of location for Private cannot have the macro [ModuleName]_API. And the macro is defined as DLLEXPORT. The attribute is used to export codes in MSVC, visit here for more details.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// MyUserWidget.h

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "MyUserWidget.generated.h"

/**
*
*/
UCLASS()
class UNREAL_5_0_API UMyUserWidget : public UUserWidget
{
GENERATED_BODY()

};

Of course, my new CPP class MyUserWidget has the macro [ModuleName]_API because I had not chosen any radio button. It was left as UserDefined and UserDefined is usually treated like Public. Then, the new CPP class would not have the macro if you clicked Private at the dialog.

Click Create Class. Engine will create intermediate files, generate project files, and build source codes.

After that, the new CPP class is ready for you.

FYI, remove [ProjectRoot]/Binaries folder and build again if you meet a dialog like above while opening the editor.

Creating CPP class; method #2

At the method #1, you must wait for a moment while engine does a process; creating intermediate files, generating project files, and build source codes. The process of creating new CPP class is not expensive when your project is small enough, but every project gets bigger and bigger as time goes on. When it comes to the point, you would want create multiple new CPP classes and wait for only one moment. At that time, the method #2 will be able to save you.

The method #2 for creating new CPP class is quite simple; do it yourself what engine did for you. Let me explain step by step. Suppose you want to create new CPP class inherits UserWidget class.

Open your VisualStudio project. Find an location to add your new CPP class at Solution Explorer. I will add a class at Unreal_5_0 folder. Select Add/New Item.... at the option.

Select Header File and name the file. I will name the file as SomeUserWidget.h. And click the button Browse... to locate the file. I will locate the file as the same location in Solution Explorer, [ProjectRoot]/Source/Unreal_5_0.

After click the button Add, you can find the new file at both file explorer and Solution Explorer in VisualStudio IDE. Repeat previous steps for creating a cpp file.

Then you have two files for creating new CPP class.

But they have no contents, in other words, empty. So what ? Let us fill the contents manually. The cpp file is very simple as it has only an include statement, #include "[HeaderName]". Problem is the header file. Usually, a generated header file from a class inherits UObject (or child of UObject) has a format like below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Copyright notice

#pragma once

#include "CoreMinimal.h"
#include "[ParentClassHeaderFile]"
#include "[ThisClass].generated.h"

/** Comment for documentation
*
*/
UCLASS()
class ([ModuleName]_API) U[ThisClass] : public U[ParentClass]
{
GENERATED_BODY()
};

For instance, we had created a class MyUserWidget. The header file MyUserWidget.h has the contents like below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "MyUserWidget.generated.h"

/**
*
*/
UCLASS()
class UNREAL_5_0_API UMyUserWidget : public UUserWidget
{
GENERATED_BODY()
};

FYI, the part [ModuleName]_API is optional as I explained at the method #1.

Then, we can write down some codes for SomeUserWidget. They look like above. Alright, now we should generate intermediate files and project files. And then build the source codes.

For this, close your VisualStudio IDE. Right click the uproject file and select Generate Visual Studio project files.

Open your VisualStudio project after generation ends. And build the editor. The engine will generate intermediate files such as generated.h and gen.cpp. For more details about generating intermediate files, visit this post.

Now build ended. Let us open the editor. We can see new class SomeUserWidget well.

Removing CPP class

As you can see, you cannot select Delete at the option about CPP class in editor. Then, how we can remove a class when we do not need it ? It is quite simple, but you cannot do it in editor.

Close your editor and remove files for the class you want to remove. I will remove the files for the class SomeUserWidget.

And then generate project files via uproject file. Plus, you must remove [ProjectRoot]/Binaries folder.

Open your VisualStudio project and build editor.

Now you can see the class SomeUserWidget disappeared.

Still the intermediate files could be remained. Remove [ProjectRoot]/Intermediate folder and repeat the steps.

Retrospection 2021

Two years from beginning a blog

What a monumental, it has been 2 years ago when I posted the first article on this blog; The article about UnrealEngine build target. I have usually written a post each month, mostly about UnrealEngine. The topic of post is chosen by my interest. For instance, things that I want to know, what I want to check, or just a record for memo.

An example fits in first rule is the post about UnrealEngine macro generation. At that time, I was curious how UnrealEngine uses a macro for implementing its framework. Another one for second rule is the post about growth of std::vector in cpp. At that time, I already knew there is difference between GCC and MSVC on growth size of std::vector. But, I had also wanted to check where the difference comes from. Last one for third rule is the post about how to setup Perforce server. Starting a small game project, I had to setup Perforce server for my team. However, finding an easy and good manual for this was so hard that I decided to post the process of setup Perforce server. For myself and anyone.

Plus, almost every resource about UnrealEngine in Youtube or Google is targeting for the blueprint user, not the cpp user. So it was so hard to use a feature in cpp source code with the resources. Even they usually do not explain how the code works, just explain about how to use it. In this aspect, I am trying to explain the source code of engine for making it transparent. Furthermore, I am trying to put an example on every explanation. Because I have been tired of resources explained by only some words; Beginners cannot understand them, even follow the process without a precise description. Writing as simple as possible, demonstrating as many as possible, these policies would be maintained as long as possible.

I collect data via Google Analytics plugged in this blog. Top 10 pages most visited (until now) are below:

  1. /2020/02/09/unreal-widget-coordinate-system/
  2. /2021/04/07/difference-between-build-cs-and-target-cs/
  3. /2020/10/25/unreal-input-system-via-gamepad/
  4. /2020/03/14/unreal-unique-pointer/
  5. /2019/08/06/what-is-unreal-build-target/
  6. /2019/08/11/custom-unreal-engine-build/
  7. /2019/08/16/differences-of-unreal-build-targets/
  8. /2020/01/28/unreal-blueprint-practical-use/
  9. /2021/03/01/unreal-engine-natvis/
  10. /2020/01/17/unreal-fname-anatomy/

It seems that a post about widget, input, or build is more popular than others. For helping UnrealEngine newbies, I would better consider writing a post about those topics in next year. Looking back my experiences on studying UnrealEngine, I was also struggling for those topics.

Career with UnrealEngine

On working at PUBG Studio for 2 years, I have learned many things. (Even though some of them are not my part) Managing infra structure for large scale project, working remote in efficient way, developing and testing on console platforms, and so on. But most of all, various experiences of developing with UnrealEngine. Thanks to talented coworkers’ help, I could have done my tasks.

Reading and understanding the engine code is essential to development of industry level, but my skill was not sufficient to do that. So, in first year, I was busy to learn about topics mentioned in conversations. It was enough to take up my time. Repeated working at office and studying at home. After that, in second year, I became to understand most of what coworkers said. I started to read more engine code deeper and deeper. It is the time when I could say that “I know UnrealEngine…a bit ?”. Now, entering third year, I have plenty of skills enough to advice coworkers. Especially, many of coworkers ask me as I have experiences of console platforms. (But I still think I should learn more about console platforms)

I have done main tasks such as optimizing contents, fixing bugs and crashes, and making development environment better. Most of all, I want to say about only the first thing. Optimizing contents was the most challenging task. You may know the common sense in program optimization;

When you increase a speed, an available memory would be decreased. On the other hand, when you increase an available memory, a speed would be decreased.

Due to the structure of memory in computer, loading data from auxiliary storage is quite expensive operation. (In comparison to main storage) Anyone who wants the program to run faster would sacrifice the memory of program for loading data as much as possible. Particularly, among the programs, the size of data in game is bigger than others. That is why games try to load data before it is needed. Because any gamer does not want to see the freezed screen whenever picking up an item.

Right…in this condition, optimizing contents should not be easy one. What more worse is, new contents come in the game every update. In other words, an available memory could be decreased as the total size of data increased. So, it is hard to maintain a speed of program without sacrificing an available memory. Even more and more users are using the SSD thesedays, we have to support the old-gen consoles such as XboxOne and Playstation4, which are the devices uses HDD in default.

Additionally, loading data asynchronously is not always a best solution. Because, eventually it also consumes the resource of computer, and results a hitch or freeze. Yeap, so we struggled to manage an available memory of our game almost every update. Mainly, we have rarely chosen to skip some data loaded before entering ingame, instead we have chosen to optimize assets or codes to reduce its size so that the total size of data get reduced. Profiling our game and finding assets or codes taking up significant times, and analyzing what the assets or codes are wrong, and optimizing them…with magical spell. (I mean, the way how to optimize contents varies every time so it is like MAGIC, which is not easy to explain)

What I want to do in future would be developing games with newest features of UnrealEngine. But it would be hard without engine migration as we are using 4.16 version of UnrealEngine. Additionally, it is also hard to use the most fascinating features; Nanite and Lumen, as we are supporting old-gen consoles. Maybe I keep going my tasks as before, but at least, gonna to learn the newest features of UnrealEngine. Maybe, sometime I can use them. Or, I would post with contents of them. ;)

Move to new house

In the middle of 2021, I had moved to new house as mentioned at this post. New house is bigger than old house, and I had to buy some furnitures. (Basic furnitures such as bed, purifier, and so on are provided in old house) Therefore, I had searched several items including closet, purifier, bed, desk, and so on. Especially, I had a difficulty for buying devices related to home network. Because I never had a need to buy these tools or devices. Even when I was in Army as Signal Corps, those things were supplies. (I did not choose what to buy)

Now I have a living room. Trying to put a sofa, table, or something fits in living room, I decided to buy UHD TV. No, not only UHD resolution…also 120Hz framerate should be great. That is a maximum range of next-gen console’s signal output. Yeap, I bought a next-gen console, too. Xbox Series X (XSX). Finally it looks like below:

The TV is the product of SAMSUNG, 50QN90A. It supports resolution and framerate upto UHD/120Hz, and has 50 inches display. I am satisfied with this product in overall aspects, what a nice TV. Only one thing disappointed is, the OS of TV is TIZEN, which is SAMSUNG’s proprietary OS. This OS does not have plenty of applications yet. For example, if you want to watch Twitch ? There is no twitch application in TIZEN, so you have to watch Twitch via web browser. Very inconvenient. Or you should check this.

After living at new house for about 6 months, I think it was a good choice. More space gets me more comfortable, and makes it possible to stay home more than before. Still we are living with the various, so I wanted to stay home without some reason. Besides, I am also working from home, so more space is suitable for me. :)

Thought about next-gen console

I cannot help saying this, I bought the next-gen console to play Battlefield 2042. Yeap…sadly, the game has totally been ruined. Huh…I might write some post for that sometime. As a fan of Battlefield series, it is so sad that people compare Cyberpunk 2077 and Battlefield 2042. Even worse, it is not easy to find any point that Battlefield 2077 is better than Cyberpunk 2077. Crap. Anyway, that is why I decided to buy a next-gen console.

Though a start of experience was not good, the console itself was amazing. Recently, UnrealEngine has released The Matrix Awakens: An Unreal Engine 5 Experience, which is kind of tech demo that shows what UnrealEngine 5 can do. The game runs at UHD/30Hz with DynamicResolution on XSX, but it looks like REAL footage. At least, it seems this tech would become popular on movie industry. Already many of short films, part of films, or commercials use UnrealEngine, but currently they are using UnrealEngine for capturing a video. In other words, they are not realtime rendering. In the view, the tech demo was so impressed. “Wow, is it possible in realtime rendering ?”

Absolutely, this is not possible only with improvement of processor performance. In comparison of playing video, playing game needs more various resources not only processor resource. Playing video only needs loading next frame and next frame…until the video ends. (Displaying is none of business here) But, playing game needs loading assets should be displayed, executing codes may update the world or actors, integrating everything into a projection…in SINGLE ONE frametime.

CPU takes charge of executing codes, and GPU takes charge of integrating into a projection. Then, who takes charge of loading assets ? In general, CPU and memory works for it. CPU tries to read data from memory and blocks itself until the read done, but it takes some time if memory bandwidth is low. Low bandwidth means that you are try to load data from auxiliary storage, and it must not be always faster than loading data from main storage (ex: RAM). Even the problem gets worse when the size of data bigger and bigger. Thus, people have tried several techniques; A compression skill is sometimes applied to minimize the size of data. Just loading a compressed data and decompress it into the original data. Of course, this skill needs an extra resource from CPU, which could trigger a hitch while playing a game. Next, a asset duplication skill is common one in games supporting for device using HDD. The skill is for reducing the seek time of HDD, because the seek time of HDD is too slow to load data in realtime. This skill also has a drawback that the size of game package gonna be VERY large. Horrible.

Thankfully, both Microsoft and Sony seem to have recognized these kind of issues. Specially, Microsoft. They made an exclusive processor for decompressing data to minimize any side effect of data compression skill. Like Sony did, Microsoft customized their NVMe SSD. Furthermore, Microsoft gave game developers more flexibity atm manpulating storage by providing DirectStorage API. Finally, Sampler Feedback Streaming (SFS) could be built on these basement skills. SFS will optimize game’s memory usage and file I/O operations. Then, Microsoft names all the skills as an architecture Xbox Velocity Architecture. Previously, I did not believe their words because it looks like hype. But I do know that is an amazing advance in game development. Turning back to UnrealEngine, EpicGames did great job so far with the new features.

Plans on next year

Except for writing a blog post each month, I have considered some plans to do.

Plan #1. Making a game.

Recently, I started to develop a game. About 3 months ago, my ex-coworker had suggested an Action RPG game project, and I accepted that. I do not know when the game is released, but some milestones should be completed in next year. In 2022, there may be a playable build. Developing this game, I should develop many features or design critical systems. With these kind of experiences, I retrospect when I was a student. It is the same that struggling on every issue, but the result would be better than when I was a student.

Plan #2. Starting to develop a game engine.

Working as a game programmer for 2 years, I have thought that “How about making my own game engine ?”. It would be fun that developing my own game engine, proud if many games are made of it, satisfied that I could do this. This kind of thought would be similar many of programmers have a dream that making their own programming language, operating system, database system, or etc. The game engine I will develop would be lightweight, but Rust gonna be used. I want to use Rust as many as possible in developing it. Because managing the side effect of using CPP would not be easy in a small project.

Plan #3. Publishing console application or game.

Actually, I have already subscribed the Xbox developer plan. It was cheaper than I expected, $19 USD only one time. (Maybe I would work for Playstation sometime…but I want to focus on Xbox now) I want to utilize this Xbox developer license. How or what to make ? Let us think about. Case first, Playing games in Xbox, I will note them if I experience some inconveniences. It would be great project if there would be something to make users convenient. Second case, making a tech demo just like The Matrix Awakens would be fun. It could be a kind of inspiration to some developers. Well…I may not make any application or game if I am busy for other works. It is on the back burner. XD

Conversion from BP-only project into BP+CPP project

Environment
UnrealEngine branch: 5.0
Visual Studio 2022 version: 17.0.1
Windows 11 Pro build: 22000.318

Overview

UnrealEngine provides you two options to build your project and you can choose one of them. The options are BLUEPRINT and C++ as you can see at the screenshot above. Selecting left one means that, “I gonna develop my project using only blueprint”. Otherwise, selecting right one means, “I want to use both blueprint and cpp on my project”.

By the way, what is different between them ? How can we convert BP only project into BP+CPP project ? Let us go over.

Comparison

After creation, you can see the directory if selected the BP-Only. In this project BPOnly, you only can execute UnrealEngine editor and write blueprints. Even if you make source code files and place them into appropriate position, your project does not compile the source code. Let us find out “why not working” by the difference between BP only project and BP+CPP project.

After creation with C++ selection. The BPCPP project supports both blueprint and cpp like its name. You can see the difference on number of files, BPOnly is 6 while BPCPP is 10. Files that exist only in BPCPP are here.

Name of file/folder Description
.vs Containing VisualStudio related files. Mostly, cached data for optimization.
Binaries Containing output files of this project. Currently, this project’s UnrealEditor library exists.
Source Containing some simple source code files. Plus, BuildRule and TargetRule exist in this folder.
<ProjectName>.sln Just like uproject file, it defines required version of VisualStudio, dependency of the project, and so on.

The only Source folder is not generated one. The Binaries folder is generated when you build the project with a certain target such as WindowsClient, WindowsServer, and Editor. The files related to VisualStudio are generated when you attempt to make project files. Also, UnrealEngine refers the Source folder while generating project files.

So, is that all ? No, actually there is one more thing different. Check the uproject file and you can find some difference. The contents of uproject file looks like similar, but BPCPP‘s one has a Modules property. The name of module is the same with project name, BPCPP.

In summary, there are some differences between BP only project and BP+CPP project. (Except for generated files)

  • Existence of Source folder
  • Property Modules in uproject file

Where these differences come from ?

Template

We have learned about templates used in UnrealEngine at the post. What found was that making new project from a template is equal to copying the template project and replacing placeholders. Right, then it would be similar to that. Find the template project for BP only project and BP+CPP project.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
TMap<FName, TArray<TSharedPtr<FTemplateItem>> > SProjectDialog::FindTemplateProjects()
{
// Clear the list out first - or we could end up with duplicates
TMap<FName, TArray<TSharedPtr<FTemplateItem>>> Templates;

// Now discover and all data driven templates
TArray<FString> TemplateRootFolders;

// @todo rocket make template folder locations extensible.
TemplateRootFolders.Add(FPaths::RootDir() + TEXT("Templates"));

// Add the Enterprise templates
TemplateRootFolders.Add(FPaths::EnterpriseDir() + TEXT("Templates"));

// Allow plugins to define templates
TArray<TSharedRef<IPlugin>> Plugins = IPluginManager::Get().GetEnabledPlugins();
for (const TSharedRef<IPlugin>& Plugin : Plugins)
{
FString PluginDirectory = Plugin->GetBaseDir();
if (!PluginDirectory.IsEmpty())
{
const FString PluginTemplatesDirectory = FPaths::Combine(*PluginDirectory, TEXT("Templates"));

if (IFileManager::Get().DirectoryExists(*PluginTemplatesDirectory))
{
TemplateRootFolders.Add(PluginTemplatesDirectory);
}
}
}
...

As you see, UnrealEngine finds template files from the path; Root/Templates/.

There are many folders for each template, and now we found. The TP_Blank and TP_BlankBP. The templates contain a uproject file, which is used for making new uproject file while creating new project using template.

The BPOnly.uproject was created based on TP_BlankBP.uproject. You can check that at the FProjectDescriptor::Write() function.



1
2
3
4
5
6
7
8
9
10
11
12
void FModuleDescriptor::WriteArray(TJsonWriter<>& Writer, const TCHAR* ArrayName, const TArray<FModuleDescriptor>& Modules)
{
if (Modules.Num() > 0)
{
Writer.WriteArrayStart(ArrayName);
for(const FModuleDescriptor& Module : Modules)
{
Module.Write(Writer);
}
Writer.WriteArrayEnd();
}
}

Why the Modules property not copied ? Look at the FModuleDescriptor::WriteArray(). UnrealEngine does not write that property when it is empty.

1
2
3
4
5
6
7
8
9
10
11
12
bool GameProjectUtils::SetEngineAssociationForForeignProject(const FString& ProjectFileName, FText& OutFailReason)
{
if(FUProjectDictionary(FPaths::RootDir()).IsForeignProject(ProjectFileName))
{
if(!FDesktopPlatformModule::Get()->SetEngineIdentifierForProject(ProjectFileName, FDesktopPlatformModule::Get()->GetCurrentEngineIdentifier()))
{
OutFailReason = LOCTEXT("FailedToSetEngineIdentifier", "Couldn't set engine identifier for project");
return false;
}
}
return true;
}

Why the EngineAssociation property not filled ? That property is filled later at the FDesktopPlatformBase::SetEngineIdentifierForProject() function.

Of course, the BPCPP.uproject was created based on TP_Blank.uproject. In this case, whole contents of file copied. And, the EngineAssociation would be overwritten.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// Retarget any files that were chosen to have parts of their names replaced here
FString DestBaseFilename = FPaths::GetBaseFilename(SrcFileSubpath);
const FString FileExtension = FPaths::GetExtension(SrcFileSubpath);
for ( const FTemplateReplacement& Replacement : TemplateDefs->FilenameReplacements )
{
if ( Replacement.Extensions.Contains( FileExtension ) )
{
// This file matched a filename replacement extension, apply it now
FString LastDestBaseFilename = DestBaseFilename;
DestBaseFilename = DestBaseFilename.Replace(*Replacement.From, *Replacement.To, Replacement.bCaseSensitive ? ESearchCase::CaseSensitive : ESearchCase::IgnoreCase);

if (LastDestBaseFilename != DestBaseFilename)
{
UE_LOG(LogGameProjectGeneration, Verbose, TEXT("'%s': Renaming to '%s/%s' as it matched file rename ('%s'->'%s')"), *SrcFilename, *DestFileSubpathWithoutFilename, *DestBaseFilename, *Replacement.From, *Replacement.To);
}
}
}
...
// Open all files with the specified extensions and replace text
for ( const FString& FileToFix : FilesThatNeedContentsReplaced )
{
InnerSlowTask.EnterProgressFrame();

bool bSuccessfullyProcessed = false;

FString FileContents;
if ( FFileHelper::LoadFileToString(FileContents, *FileToFix) )
{
for ( const FTemplateReplacement& Replacement : TemplateDefs->ReplacementsInFiles )
{
if ( Replacement.Extensions.Contains( FPaths::GetExtension(FileToFix) ) )
{
FileContents = FileContents.Replace(*Replacement.From, *Replacement.To, Replacement.bCaseSensitive ? ESearchCase::CaseSensitive : ESearchCase::IgnoreCase);
}
}

if ( FFileHelper::SaveStringToFile(FileContents, *FileToFix) )
{
bSuccessfullyProcessed = true;
}
}

if ( !bSuccessfullyProcessed )
{
FFormatNamedArguments Args;
Args.Add( TEXT("FileToFix"), FText::FromString( FileToFix ) );
OutFailReason = FText::Format( LOCTEXT("FailedToFixUpFile", "Failed to process file \"{FileToFix}\"."), Args );
return TOptional<FGuid>();
}
}

The name of folders and content of files are replaced by the codes above. In this post, from TP_Blank into BPCPP. (Or, from TP_BlankBP into BPOnly)

Module

We have confirmed that the difference between BPOnly and BPCPP is about a module system, which are Modules property in uproject and Source folder containing code files. Thus, it would be possible converting blueprint only project into blueprint with cpp project by making some changes. In other words, we should make a new module.

Though a good wiki page for this exists, I will show you an example based on TP_BlankBP template.

#1. Prepare a project created with TP_BlankBP. In this post, I use the BPOnly project.

#2. Make a folder Source at project directory, and make a folder <ModuleName> in the Source directory.

Name the module as you want, but it is recommended to set by project name. (Because this module is the first module of project) Just to show that any name is okay, I set the module name as Robb, which is different with project name.


#3. Copy some files from the template TP_Blank. Replace their names and contents.

I had copied all of files in Source folder of TP_Blank template. For using the template files in this project, I replaced filenames and contents. (In this case, I need to replace the text TP_Blank into Robb)

#4. Generate VisualStudio project files and open VisualStudio project file.


#5. Build the project and open UnrealEngine editor. Profit !

Wrap-Up

It is not common case that creating a project with blueprint only option, but we are able to convert blueprint only project into blueprint with cpp project. We have checked what happens while creating our project using template, what is different between TP_Blank and TP_BlankBP, and how to add cpp module at blueprint only project. As we seen earlier in this post, the conversion we did is the same work of what UnrealEngine does.

When we make an initial module, the name of module does not have to be the same with project name. But, it is recommended to set by project name with convention and several reasons. For example, I had made a module Robb at the project BPOnly. I tried to package the project and got the result like below. Some of files have the name as Robb, but others have the name as BPOnly. Kind of disharmony on naming could be problem when accessing files with name.