introduction to AI with XOR problem

Presented for the first time at 2018
Translated into Korean

VHPC Lab 은 글쓴이가 재학했던 학교의 IT 연구실입니다.

.

.

인공지능이란 무엇인가
자연적으로 정의되지 않은 지능
반대의 경우, 자연지능
인공지능은 주로 기계를 통해 구현되기 때문에, 기계지능이라고도 불림
지능을 가진다는 것은, 스스로 학습하고 추론하여 결정함을 의미

초기 인공지능
임의 알고리듬을 사용함
코드가 인공지능의 동작을 정의
하드웨어의 성격을 지님
동작을 수정하는 것이 (비교적) 어려움
제작자에 따라 인공지능 성능이 엇갈림
1980년대의 전문가 시스템이 대표적인 예시

머신러닝이란
인공지능 구현 방법론 중 하나
concept: 데이터가 인공지능의 동작을 정의
소프트웨어의 성격을 지님
동작을 수정하는 것이 (비교적) 쉬움
입력된 데이터와 학습전략에 따라 인공지능 성능이 엇갈림

뉴런
신경망을 구성
각 뉴런은 신호를 전파함
입력값이 threshold 보다 커질 경우
입출력 : 수상돌기 / 축색돌기 (즉, 신경접합부 ㅡsynapseㅡ)
“뇌를 모방해보면 어떨까”에 대한 영감 ㅡinspiringㅡ

퍼셉트론
뉴런을 모방함
각 퍼셉트론은 값을 전파함
조건을 만족할 때 계산을 수행함
입출력 : 단말
노드와 단말로 구성됨

인공 신경망이란
자연 신경망을 모방한 망 ㅡnetworkㅡ
다양한 계산들이 내포됨
대부분은 더하기와 곱하기
출력값을 수정하기 용이한 설계
가중치 변수만 조절하면 됨
이러한 구조가 학습 비용을 절감함

인공 신경망을 활용한 머신러닝
단일 퍼셉트론을 사용한 인공 신경망부터 시작되었음
여러 가지 이유로 효과적이지 않았음
부족한 컴퓨팅 파워
비효율적인 학습 전략
대규모 데이터셋의 부재

딥러닝이란
머신러닝의 일종으로서, Deep Neural Network (이하, D.N.N.) 를 사용함
(인공) 신경망이 매우 복잡하고 거대할 경우, 이를 D.N.N. 이라 부름
다양한 레이어 유형들이 제공되어 유연함

왜 딥러닝이 유용한가
여러 가지 문제점들이 해결됨
부족한 컴퓨팅 파워
비효율적인 학습 전략
대규모 데이터셋의 부재

초기 머신러닝과 딥러닝의 비교
2012년부터, 딥러닝 기반 인공지능이 I.L.S.V.R.C. 기반 인공지능을 이김
2016년부터, 해당 영역은 (딥러닝 기반) 인공지능이 장악해버린 것을 알 수 있음

더 자세히
초기 머신러닝
어떻게 결과를 도출할지 제작자가 결정
(예컨대, 자동차를 인식하기 위해 둥근 선이 바퀴의 특징이고 각진 선이 차체의 특징이라고 정의)
딥러닝
어떻게 결과를 도출할지조차 인공지능이 결정
(예컨대, 자동차를 인식하기 위한 특징이 무엇인지 알려주지 않음)
딥러닝 같은 방식이 (요즘 들어) 추천됨
물론 장단점은 있음

(생략)

.

Gradient Method 란
선형시스템을 수치계산으로 풀어내는 방법론
그 중에서도, Gradient Descent 를 다룰 것 (인공신경망의 가중치값을 조정하는 데에 사용되는 알고리듬)
다음 상황을 가정
곱하기 연산에 (x=2,y=3)(x = -2, y = 3) 두 가지 입력값이 제공됨
출력값을 0 에 가깝게 만들고 싶음
입력값만 수정할 수 있음

무작위 검색은 어떨까
계산할 때마다 무작위 값을 생성
하지만, 항상 운이 좋을 수는 없음
f (함수 출력값) 는 -6 보다 높아질 수도 있음
하지만 (높아진다 하더라도) 234,281,855 같은 큰 수여도 괜찮을까

편미분이란
변수가 여러 개인 함수를 미분하는 방법
여러 개의 변수들 중 하나만을 다룸
나머지 변수들은 상수 취급
변수 개수에 상관 없이, 공식은 고정

Numerical Gradient 란
각 입력값에 대해 편미분을 적용하는 방법
(편미분 값을 계속해서 더함)
최적해 찾는 꽤 좋은 방법
하지만 연산비용이 높음
(단, 정밀한 조작 위해 step size 라는 배수 적용)

f(x,y)=xy f(x,y)x=f(x+h,y)f(x,y)h=xy+hyxyh=hyh=y f(x,y)y=f(x,y+h)f(x,y)h=xy+hxxyh=hxh=x f(x,y) = xy \\ \space \\ \frac{\partial f(x,y)}{\partial x} = \frac{f(x+h,y)-f(x,y)}{h} = \frac{xy + hy - xy}{h} = \frac{hy}{h} = y \\ \space \\ \frac{\partial f(x,y)}{\partial y} = \frac{f(x,y+h)-f(x,y)}{h} = \frac{xy + hx - xy}{h} = \frac{hx}{h} = x \\ \space \\

편미분 값은 출력값에 대한 (입력값이 가지는) 영향력을 의미
(우리가 해야할 것은) 단지 f 값이 0 을 넘어갈 때 멈추는 것
step size 값이 더 정밀하면 오류값도 적음

Analytic Gradient 란
Numerical Gradient 의 개선판
계산비용에서 보다 효율적임
미분값을 (매번 계산하지 않고) 고정된 값 사용

이전 예제에 적용해볼 경우
효율성 때문에, 모든 인공지능 프레임워크에서는 이러한 방법을 사용
아무리 더하기 빼기 곱하기 같은 간단한 계산뿐이라고 해도, 총량에서 수 배 십 수 배 차이가 날 수 있기 때문

다중연산을 어떻게 푸는가
일반적인 경우, 한 가지 연산만 사용하지 않음
그런데, 다중연산일 경우에는 각 연산들이 서로의 정보를 알 수 없음
(그럼에도 불구하고, 편미분값을 알아야 학습을 할 수 있을 텐데 이를 어떻게 하는가가 관건)
입력값으로 (x=2,y=5,z=4)(x = -2, y = 5, z = -4)
연산으로 q=x+y,f=qz=(x+y)zq = x + y, f = q \cdot z = (x + y) \cdot z 를 가정

(앞서 살펴봤던 내용과 마찬가지로) q 에 대한 편미분값은 z 이고, z 에 대한 편미분값은 q
x 에 대한 편미분값은 1 이고, y 에 대한 편미분값은 1
이렇게만 결론지으면 되는 걸까
아니다. 우리가 구한 것은 각 연산에서의 국소적 편미분값들이고, 우리가 원하는 것은 f 값에 대한 x y z 변수들의 편미분값이다
(당연하게도, 우리가 조절할 수 있는 값은 x y z 세 가지 변수뿐이기 때문)
즉, 추가로 f(q,z)x,f(q,z)y\frac{\partial f(q,z)}{\partial x}, \frac{\partial f(q,z)}{\partial y} 를 찾아야한다
(방금 f(q,z)z\frac{\partial f(q,z)}{\partial z} 는 찾았음)

역전파 ㅡBack Propagationㅡ 란
확인했다시피, 다중연산에서 입력값의 편미분값을 구하는 게 불가능해 보였음
예컨대, f 연산은 x 와 y 에 대해 알 수 있는 방법이 없음
역전파는 이러한 문제를 해결 가능
연쇄규칙 ㅡChain Ruleㅡ 을 사용하면, 연산 너머의 입력값들에 대한 편미분값을 획득 가능
(다시 한 번 예제 규칙을 정리하면 다음과 같음)

x=2,y=5,z=4x = -2, y = 5, z = -4 \\ q(x,y)=x+y, f(q,z)=qz=(x+y)zq(x, y) = x + y, \space f(q, z) = q \cdot z = (x + y) \cdot z

최종 f 값을 바꾸기 위한, x y z 미분값들을 모두 구할 수 있음
(최종 f 값이, 0 에 가까워져 가는 것 확인 가능)
당연하게도, 원한다면 step size 값을 음수 사용하는 것도 고려해볼 수 있음
주로, step size 는 0.01 같은 매우 작은 값이 사용됨
예시에서는, 극적인 변화 위해 0.1 로 사용하였음

이제 퍼셉트론 배우기 위한 배경지식은 준비되었음 (3장 내용)

2장의 내용은 소스코드로도 확인 및 직접 실행해볼 수 있음
https://github.com/BaeMinCheon/introduction-to-ai/tree/master/Chapter02

.

퍼셉트론이란
계산에 필요한 가중치 변수들을 보유하는 노드
인공신경망에서의 노드 하나하나를 지칭하는 용어이기도 함
뉴런처럼, 활성화함수를 가짐
활성화함수는 (자신에게 연결된 다른 퍼셉트론에게) 어떻게 값이 전달될 지 결정하는 역할 수행
(곱하기, 더하기, 활성화함수 순서는 예시일 뿐. 경우에 따라 다른 구조를 가질 수 있음)

input:x1+x2+...input: x_1 + x_2 + ... \\ output:f(x1w1+x2w2+...)(f is for activate function)output: f(x_1 \cdot w_1 + x_2 \cdot w_2 + ...) \quad (f \space is \space for \space activate \space function) \\

단일 퍼셉트론이 할 수 있는 것
단일 퍼셉트론은 최대 2가지 종류의 데이터를 분류 가능
1가지 종류 데이터 분류일 경우, Linear Regression (다음 슬라이드에서 다룰 것)
2가지 종류 데이터 분류일 경우, Binary Classification (또는, Linear Classification)
다시 말해, 단일 퍼셉트론만으로 2가지 초과 데이터를 분류 불가능
이것을 해결하기 위해, 다중 퍼셉트론을 추후 이야기할 것임

Linear Regression 이란
(데이터를 가로질러) 비용 최소화 선을 그릴 수 있는 알고리듬
여기에서의 비용이란, 오차들의 합산
예측 모델을 얻는 방법으로도 볼 수 있음
Gradient Descent 의 좋은 활용 예시이기도 함

퍼셉트론으로 간단한 예제 구현
입출력값이 각각 1, 2, 3 으로 주어졌을 때 퍼셉트론이 이들을 제일 적은 비용으로 가로지를 수 있도록 학습시켜보자
참고로, bias 라고 하여 가중치를 하나 더 조절할 수 있는 placeholder 를 사용하곤 함 (경험/관습적으로 유용하기 때문)
즉, 다음과 같은 입출력값이 예상되고 각 출력값은 1, 2, 3 에 가까워져야함

input:1,2,3input: 1, 2, 3 \\ output:w1+w2,2w1+w2,3w1+w2<=>w1+b,2w1+b,3w1+boutput: w_1 + w_2, 2w_1 + w_2, 3w_1 + w_2 <=> w_1 + b, 2w_1 + b, 3w_1 + b \\

이 퍼셉트론을 어떻게 학습시키는가
모든 가중치 값들은 1 로 설정하자
매 계산마다 비용 값을 계산해야할 것
그리고 Gradient Descent 통해 그 비용을 줄여나갈 것
가중치 값을 변경함으로써 이를 실현할 것

어떻게 Gradient Descent 를 적용하나
우리는 비용을 줄이고 싶은 것이지, 출력값 자체를 0 으로 만들고 싶은 게 아니다
따라서, 앞서 G.D. 예시들처럼 다룰 경우 결과값이 0 으로만 만들어지고 말 것
그러므로, 결과값으로부터 비용 계산하는 함수 만들 필요 있음
이를 Error Function 이라 부름
(Error Function 붙인 채로 학습을 진행하고,) 학습 완료 후 Error Function 을 제거한 뒤, 모델 그 자체만 사용하면 되는 방식

(확률과 통계에서의 분산 공식과 유사하며, 의미 또한 그와 동일함)
매 학습마다 모든 데이터를 사용해야함
Error Function 식 자체가 모든 데이터를 참조하기 때문 (sum from 1 to N)
다시 말해, Error Function 은 모든 데이터가 처리되고 나서 수행될 것임
(개별 데이터에 휘둘리지 않고,) 일관성 있게 가중치 변수를 조절할 수 있게 될 것
이전 예시에서는 우리가 직접 수행했던 부분임
(편의상 Error Function 을 편미분한 값을 GDx 라고 하자) 이를 사용하면 역전파도 할 수 있으니, 가중치 변수 x 를 조정할 수 있을 것
(아래 내용은 GDx 구하는 과정)

error=12Ni=1N(outputidatai)2error = \frac{1}{2N} \displaystyle\sum_{i=1}^{N}(output_i - data_i)^2 \\ 1xerror=errorx=1x(12Ni=1N(outputidatai)2)\to \frac{1}{\partial x} \cdot error = \frac{\partial error}{\partial x} = \frac{1}{\partial x} (\frac{1}{2N} \displaystyle\sum_{i=1}^{N}(output_i - data_i)^2) \\ =1x(12Ni=1N(outputi22outputidatai+datai2))= \frac{1}{\partial x} (\frac{1}{2N} \displaystyle\sum_{i=1}^{N}(output_i^2 - 2 \cdot output_i \cdot data_i + data_i^2)) \\ =12Ni=1N(2outputioutputix2dataioutputix+0outputix)= \frac{1}{2N} \displaystyle\sum_{i=1}^{N}(2 \cdot output_i \cdot \frac{\partial output_i}{\partial x} - 2 \cdot data_i \cdot \frac{\partial output_i}{\partial x} + 0 \cdot \frac{\partial output_i}{\partial x}) \\ =12N2i=1N(outputioutputixdataioutputix)= \frac{1}{2N} \cdot 2 \displaystyle\sum_{i=1}^{N}(output_i \cdot \frac{\partial output_i}{\partial x} - data_i \cdot \frac{\partial output_i}{\partial x}) \\ =1Ni=1N(outputidatai)outputix= \frac{1}{N} \displaystyle\sum_{i=1}^{N}(output_i - data_i) \frac{\partial output_i}{\partial x} \\

가중치 수정 방향을 (+ 또는 -) 어떻게 결정하는가
지금까지 알아본 내용들 기반으로, G.D. 방법론으로 결과값을 0 에 가깝게 만들 수 있음
여기에 Error Function 을 추가하면, 우리가 원하는 값을 나오게 만들 수도 있을 것임
Error Function 을 거치는 과정에서, 제곱 연산 때문에 무조건 양수 값이 나올 것
따라서, 우리는 Error Function 값이 0 에 가까워지도록 줄이기만 (minus) 하면 됨. 어차피 음수 값이 나올 수 없기 때문
(사진은 제곱 함수의 특성 설명 예시일 뿐. 이번 예시의 Error Function 을 그래프로 그린 것이 아님)

퍼셉트론의 출력값 g 에 대한, 가중치들의 편미분값은 앞서 봐왔던 방식대로 구할 수 있음

g(x)=w1x+w2=wx+bg(x) = w_1 x + w_2 = w x + b 에서, 당연하게도 서로에게 곱해지는 값이 곧 편미분임

(w 입장에서는 x, b 입장에서는 1)

gw=x=outputw,gb=1=outputb\frac{\partial g}{\partial w} = x = \frac{\partial output}{\partial w}, \quad \frac{\partial g}{\partial b} = 1 = \frac{\partial output}{\partial b} 일 때,
g(1)=2,g(2)=3,g(3)=4g(1) = 2, \quad g(2) = 3, \quad g(3) = 4 이므로 GDw 및 GDb 는 다음과 같이 구해질 수 있음

GDw=1Ni=1N(outputidatai)outputiwGD_w = \frac{1}{N} \displaystyle\sum_{i=1}^{N}(output_i - data_i) \frac{\partial output_i}{\partial w} \\ =13i=13(outputidatai)outputiw= \frac{1}{3} \displaystyle\sum_{i=1}^{3}(output_i - data_i) \frac{\partial output_i}{\partial w} \\ =13((21)1+(32)2+(43)3)=13(1+2+3)=2= \frac{1}{3} ((2 - 1) \cdot 1 + (3 - 2) \cdot 2 + (4 - 3) \cdot 3) = \frac{1}{3} (1 + 2 + 3) = 2 \\ GDb=1Ni=1N(outputidatai)outputibGD_b = \frac{1}{N} \displaystyle\sum_{i=1}^{N}(output_i - data_i) \frac{\partial output_i}{\partial b} \\ =13i=13(outputidatai)outputib= \frac{1}{3} \displaystyle\sum_{i=1}^{3}(output_i - data_i) \frac{\partial output_i}{\partial b} \\ =13((21)1+(32)1+(43)1)=13(1+1+1)=1= \frac{1}{3} ((2 - 1) \cdot 1 + (3 - 2) \cdot 1 + (4 - 3) \cdot 1) = \frac{1}{3} (1 + 1 + 1) = 1 \\

여기에서, 가중치 값들에 대해 GDw 및 GDb 를 step size 만큼 조정된 값으로 빼주기만 해도, 다음 번 Error Function 값이 떨어질 것임

2번째 입력과 3번째 입력을 순서대로 확인해보자
g(x) 값이 점점 y = x 꼴에 맞춰가는 걸 알 수 있음

G.D. 같은 알고리듬이 잘 동작하는 예시를 살펴봤는데, 어디까지나 잘 준비된 예시일 뿐 실제로는 문제 발생하기 쉬운 것이 인공지능 학습임
우리가 살펴봤던 step size = 0.1 예시의 경우, 가중치 값이 조절되면서 비용(error) 수치도 줄어드는 걸 알 수 있었음
하지만 반대로 step size = 1 로 설정했을 경우, 비용 수치가 줄기는 커녕 늘어나는 현상을 볼 수 있음
(e11e^{11} 같이 무지막지하게 큰 수까지 올라가버림)
따라서, 적절한 초기 가중치 값과 step size 수치 설정이 중요함. 이는 여러 차례 시도하며 경험해보는 게 일반적

직전 슬라이드에서 보았던, 비용 수치가 오히려 증가하는 경우가 곧 Overshooting
하지만, step size 가 지나치게 작을 경우 국소적인 해 ㅡsolutionㅡ 에서 벗어나지 못할 수 있음
(국소적인 해에서 벗어나고자 가중치 값을 수정해보지만, 비용 수치가 증가하니 다시 되돌아가려는 동작을 해서 도루묵이 됨)

Difference between copy and cat in combining files

Overview

There are some commands for combining files such as copy and cat. Respectively, copy is a command for Windows prompt and cat is a command for Unix prompt. You can check the specifications at official documentations below:

Comparison #1

Suppose we have two text files, a.txt and b.txt. Each of them has simple contents.

We gotta combine them into one text file, c.txt.

In Windows, it would be like this:

1
2
3
4
5
6
7
8
C:\Users\qmffk\Downloads>copy a.txt + b.txt c.txt
a.txt
b.txt
1 file(s) copied.

C:\Users\qmffk\Downloads>type c.txt
hello, a !hello, b !
C:\Users\qmffk\Downloads>

In Linux, it would be like this:

1
2
3
thanang@ROSS-DESKTOP:/mnt/d/test$ cat a.txt b.txt > c.txt
thanang@ROSS-DESKTOP:/mnt/d/test$ cat c.txt
hello, a !hello, b !thanang@ROSS-DESKTOP:/mnt/d/test$

Both look like the same, but there is a difference between two c.txt.

Using WinMerge, we can see the additional character 1A (in hex) at Windows’ c.txt. In short, the character 1A (26 in decimal) is appended for indicating an EOF (= End Of File). This is why this is mentioned in the documentation. (For more information about the character 1A / 032 / SUB / Substitute, visit here)

1
2
3
...You can copy an ASCII text file that uses an end-of-file character (CTRL+Z) to indicate the end of the file...

...To copy a file called memo.doc to letter.doc in the current drive and ensure that an end-of-file character (CTRL+Z) is at the end of the copied file...

So, how do we prevent from appending the EOF character ? This is also mentioned in the documentation.

1
The effect of /b depends on its position in the command–line string: - If /b follows source, the copy command copies the entire file, including any end-of-file character (CTRL+Z). - If /b follows destination, the copy command doesn't add an end-of-file character (CTRL+Z).

Comparison #2

Do combine a.txt and b.txt into c.txt again.

In Windows, it would be like this:

1
2
3
4
5
6
7
8
C:\Users\qmffk\Downloads>copy /b a.txt + b.txt c.txt
a.txt
b.txt
1 file(s) copied.

C:\Users\qmffk\Downloads>type c.txt
hello, a !hello, b !
C:\Users\qmffk\Downloads>

In Linux, it would be the same with before.

Using WinMerge, we can see they are the same.

So, you should plus the flag /b in copy commandline for experiencing the same result as cat in Unix.

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.