Aery的UE4 C++遊戲開發之旅(5)字元&字串

KillerAery發表於2021-02-07

TCHAR 字元


C++支援兩種字符集:即常見的ANSI編碼和寬字元的Unicode編碼,實際對應的字元型別分別是char和wchar_t,在不同平臺環境下,我們可能需要不同的字元型別。

TCHAR就是UE4通過對char和wchar_t的封裝,將其中的操作進行了統一,使程式具有可移植性。

使用TEXT()巨集包裹字串字面量

博主之前編碼規範的筆記中曾提到必須得用TEXT()巨集來包裹字串字面量,其原因就在於無包裹的字串字面量預設就是表示ANSI字元,字串字面量前面多個L就是表示寬字元,通過TEXT包裹則可以讓UE4自動選擇適合當前平臺環境的編碼(例如巨集可能展開成L"Hello World!"也可能展開成u“Hello World!”)

"Hello World!";             //ANSI字元
L"Hello World!";            //16位寬字元
TEXT("Hello World!");       //具有可移植性,在部分平臺是16位寬字元

//這樣我們可以用TCHAR*表示這種字串
const TCHAR* TcharString = TEXT("Hello World!");

轉換字元編碼

當我們呼叫UE4以外的API且必須得將TCHAR型別與char/wchar_t型別相互轉換時,就要使用UE4提供的轉換巨集:

// 引擎字串(TCHAR*) -> ANSI字串(char*)
TCHAR_TO_ANSI(TcharString);
// 引擎字串(TCHAR*) -> Unicode字串(wchar_t*)
TCHAR_TO_UTF8(TcharString);
// ANSI字串(char*) -> 引擎字串(TCHAR*)
ANSI_TO_TCHAR(CharString);
// Unicode字串(wchar_t*) -> 引擎字串(TCHAR*)
UTF8_TO_TCHAR(WChartString);

注意1:傳入的引數必須是一個字串(TCHAR*),因為引數無論是指標還是字元型別都會在巨集裡被強制型別轉換為指標,錯誤的型別轉換會導致執行時崩潰(編譯期無法檢測出該轉型錯誤)。典型的例子就是假如傳入的是 TCHAR 而非 TCHAR*,編譯後執行時會出現崩潰。

SomeAPI(TCHAR_TO_ANSI(TcharString));                    // OK
const char* SomePointer = TCHAR_TO_ANSI(TcharString);   // Bad!!!

注意2:只在給函式傳參時使用這個巨集轉換,千萬不要保留指向它們的指標。

如果字串相對較小,轉換器類物件的預分配陣列(presized array)直接可以容納所有字元,也就是記憶體都分配在棧中; 如果字串較長,則需要在堆中分配一個臨時緩衝區。轉換巨集實際上就是產生一個臨時的轉換器類物件,當該物件釋放時也會釋放其生成的字串。所以不要保留指向它們的指標,不然洩露給另一個作用域將會是巨大災難。

FString 字串


FString 是一種動態字串,實際上就類似於我們所熟悉的std::string型別,是我們平時編寫UE4 C++程式碼時最常需要用到的字串型別。

由於動態的特性,FString擁有以下特點:

  • 支援很多字串操作(例如轉換int32/float,字串拼接,查詢子字串,逆置)
  • 開銷比靜態(不可變)字串類(FName、FText)要更大

FString 剖析

FString 本質是構建在TArray<TCHAR> 之上,即字串的元素使用TCHAR型別而非char型別。

FString 使用

// 構造:通過字串字面量構造時應記得使用TEXT()巨集
FString MyFString = FString(TEXT("Hello"));
// 格式化方式建立
// 注:像C的printf函式那樣,使用格式化引數建立FString物件
FString MyFString = FString::Printf(TEXT("%s,%d"), *TestFString, 2333);

// 比較
// 注:前一種不忽略大小寫,後兩種忽略大小寫;使用Equals可以更加清晰表示是否忽略大小寫
if(MyFString.Equals(OtherFString, ESearchCase::CaseSensitive)){...}
if(MyFString.Equals(OtherFString, ESearchCase::IgnoreCase)){...}
if(MyFString == OtherFString){...}

// 返回是否存在子字串
// 注:引數ESearchCase(是否忽略大小寫)、ESearchDir(搜尋方向),預設引數為忽略大小寫,從前往後搜尋
if(MyFString.Contains(TEXT("ello"), ESearchCase::IgnoreCase, ESearchDir::FromStart){...}
// 返回找到的第一個子字串例項的索引,若未找到則返回INDEX_NONE
if(MyFString.Find(TEXT("ello"), ESearchCase::IgnoreCase, ESearchDir::FromStart, INDEX_NONE) != INDEX_NONE){...} 

// 用 + 或 += 運算子拼接字串
FString MyFString,A,B;
MyFString = A + B;
MyFString += A;

// FString -> TCHAR* (TCHAR*與FString基本都能自動隱式轉換)
const FString MyFString;
const TCHAR *TcharString = *MyFString;

// FString -> int32/float
const FString MyFString = TEXT("23333");
int32 MyStringtoInt = FCString::Atoi(*MyFString);
const FString TheString = TEXT("1234.12");
float MyStringtoFloat = FCString::Atof(*MyFString);

// int32/float -> FString
const FString MyFString = FString::FromInt(23333);
const FString MyFString = FString::SanitizeFloat(1234.12f);

// std::string -> FString 
std::string StdString = "Hello";
const FString FStringFromStdString(StdString.c_str());

// FString -> std::string
const  FString MyFString= TEXT("Hello");
std::string str(TCHAR_TO_UTF8(*MyFString));

// FName -> FString
FString MyFString = MyFName.ToString();

// FText -> FString
// 注:FText轉換為FString會丟失本地化資訊
FString MyFString = MyFText.ToString();

FName 字串


FName 是一種靜態(不可變)字串,主要被用來作為識別符號等不變的字串(例如:資源路徑/資原始檔型別/平臺標識/資料表格原始資料等...)

FName 的主要特點有:

  • 比較字串操作非常快
  • 即使多個相同的字串,也只在記憶體儲存一份副本,避免了冗餘的記憶體分配操作
  • 不區分大小寫

FName 剖析

FName 實際上就是一個索引編號,整個FName系統主要是通過雜湊表來實現的,代價是不允許對字串進行修改操作(靜態特性)。

FName 用字串構造時只進行一次字串雜湊對映,分配得到在雜湊表的索引編號。在此係統中,即使在多個地方宣告字串,只要其字串元素都一樣,那麼它在資料表中只有一份副本(雜湊對映到同一個索引編號)。通過這個索引編號,我們也可以在表中快速定位 FName 所代表的字串。

為了優化字串,在遊戲開發過程中,如果可以確定哪些字串是固定不變的資料且無需考慮文字國際化,應該儘可能對它們使用FName,只在必要的時候才將 FName 轉換為其他字串型別進行操作。

UE4的UObject的就是使用的FName來儲存物件名稱,在內容瀏覽器中為新資源命名時/變更動態材質例項中的引數/訪問骨骼網格體中的一塊骨骼時都會需要使用 FName

FName 使用

// 構造:記得TEXT()巨集
FName TestName = FName(TEXT("D:\UnrealEngine\Engine\Source\Runtime\CoreUObject\Public\UObject\UObjectBase.h"));

// 比較
// 它實際上並不比較每個字元,而是對比索引編號,可極大地節約CPU開銷
// == 運算子 用於對比兩個 FNames,返回 true 或 false
if(TestFName == OtherFName){...}
// FName::Compare 若等於 Other 將返回0;若小於/大於 Other 將返回小與/大於0的數
if(TestFName.Compare(OtherFName)==0){...}

// 搜尋名稱表
// 如果確定 FName 是否在表中(但不希望進行自動新增),可在建構函式中補充一個搜尋引數 FNAME_Find
// 若名稱不在名稱表中,FName 的索引將被設為 NAME_None
// 注:將不對指標進行null檢查,因為使用的是普通字串
if(FName(TEXT("pelvis"), FNAME_Find) != NAME_None){...}

// 檢查 FName 在特定使用情況下的有效性
// 注:執行轉換時,需注意可能包含對建立中的 FName 型別無效的字元(例如框內的字元:【\"' ,\n\r\t】)
if(MyFName.IsValidObjectName()){...}

// FString -> FName
// 注:FString轉換至FName時會丟失原始字串的大小寫資訊
FName MyFName = FName(*MyFString);

// FText -> FName
// 沒有直接的轉換方法,需要 FText -> FString -> FText

FText 字串


FText 是一種靜態字串,在UE4中主要負責處理文字本地化,從而顯示給不同語言的玩家。當你的遊戲需要支援不止一種語言時,就需要考慮文字本地化,遵循這個規則:當字串需要顯示(面向玩家)時,應當使用 FText

因此當 FString 字串需要顯示(面向玩家)時,應當轉換為 FText 型別再作顯示。

FText 的主要特點有:

  • 支援文字本地化
  • 提高文字渲染的效能
  • 較快的copy操作

FText 剖析

FText 核心實質是一個TSharedRef<ITextData>,即實際文字資料的智慧引用,這也使得 FText 的拷貝成本很低(只需拷貝指標)。

此外 FText 通過 flags 記錄一些屬性,這樣就可以利用 FTextSnapshot 工具來高效地檢測 FText 要顯示的內容是否發生改變(例如實時的語言文化切換),從而再立即編譯相應的字型。

FText 的不可變是指它的各語言文化等條件下的文字內容不會改變,但當前語言文化顯示的內容仍然可能會切換(切換語言)

FText 的設計符合UI效能優化的一個思想,讓UI更新儘可能基於通知而不是基於輪詢。這樣當內容發生改變時,UMG可以不必每幀主動檢測顯示字串內容的每個字元,而是檢測FTextSnapshot的flags即可。只有內容發生改變的時候,才將新字串內容的每個字元編譯成對應字型然後更新渲染字型

FText 使用

構造 FText 時,需要用如下2個巨集中的一個來包裹字串字面量:

  1. NSLOCTEXT( namespace , key , source )
    • namespace 名稱空間:一個工程中可以存在多個名稱空間,用於區分翻譯的不同用途(例如我們可以將要翻譯的原始碼區分為除錯和發行2個名稱空間)
    • key 上下文:區分在不同場景下相同的源文(例如同樣一句話“Fuck!”在兩種場合可能會翻譯成不同意思:“去你的!”、“真是見鬼了!”)
    • source 源文:需要翻譯的原始文字
FText constFTextHelloWorld = NSLOCTEXT("MyOtherNamespace","Scene1","Hello World!");
  1. LOCTEXT( key , source )
    • 需要使用 LOCTEXT 必須在原始檔頭定義 LOCTEXT_NAMESPACE 巨集,然後在需要在結尾處取消該巨集
    • 可以看作是 NSLOCTEXT 的一種簡便寫法,不用多次重複寫namespace
#define LOCTEXT_NAMESPACE "MyOtherNamespace"     // 定義 LOCTEXT 命令空間
FText constFTextGoodbyeWorld= LOCTEXT("Scene1","Goodbye World!");
//...
#undef LOCTEXT_NAMESPACE                         // 注意:必須取消巨集定義

UE4的本地化系統編輯器可以把所有的 FText 收集起來,然後就在編輯器中對目標Text進行不同語言的翻譯:

Aery的UE4 C++遊戲開發之旅(5)字元&字串

UE4編輯器具體的本地化功能操作可以參考 UE4製作多語言遊戲(本地化功能詳解)

// 數字/日期/時間變數 -> 當前文化(語言)下的FText文字
FText::AsNumber()
FText::AsPercent()
FText::AsCurrency()
FText::AsDate()
FText::AsTime()

// 格式化建立:排序引數
// 注:格式化引數都需是FText型別
// 佔位符是大括號,其標識格式引數的開頭和結尾,數值代表對應第x個已傳遞的引數
FText PlayerName;
FText MyFText = FText::Format(
       NSLOCTEXT("MyNamespace","ExampleScene", "Hello {0}!You have {1} Hp!"),
       PlayerName,
       FText::AsNumber(CurrentHealth)
       );

// 格式化建立:命名引數
// 佔位符是大括號,其標識格式引數的開頭和結尾,命名代表在傳入的 FFormatNamedArgs 集合中找到的引數名稱
FFormatNamedArguments Arguments;
Arguments.Add(TEXT("CurrentHealth"), FText::AsNumber(CurrentHealth));

FText MyFText = FText::Format(
       NSLOCTEXT("MyNamespace","ExampleScene", "You currently have {CurrentHealth} health left."),
       Arguments
);

// 比較
// FText 不支援過載運算子比較,但是提供多個函式以根據不同的比較規則進行比較(第二個引數ETextComparisonLevel決定要使用的比較規則)
// 返回bool
if(MyFText.EqualTo(OtherFText,ETextComparisonLevel::Default)){...}
// 實質還是呼叫EqualTo,只是第二個引數ETextComparisonLevel使用了IgnoreCase值(省略大小寫)
if(MyFText.EqualToCaseIgnored(OtherFText)){...}
// 返還0表示相等,而負值或正值分別表示比較結果的低於或高於
if(MyFText.CompareTo(OtherText,ETextComparisonLevel::Default)==0){...}
// 實質還是呼叫CompareTo,只是第二個引數ETextComparisonLevel使用了IgnoreCase值
if(MyFText.CompareToCaseIgnored(OtherText)){...}

// FName -> FText
FText MyFText = FText::FromName(MyFName);

// 建立非本地化的(即"語言不變")文字
// 例如:在UI中顯示一個玩家名字(即使不是同一文化的玩家,也應該看到他國文字的命名)
FText MyFText = FText::AsCultureInvariant(MyFString);

// FString -> FText
// 注:此效果等同於非編輯器版本中的 AsCultureInvariant。在編輯器版本中,此函式不會將文字標記為語言不變,也就是說若將其指定到已儲存資源中的 FText 屬性,其仍為可本地化狀態。
FText MyFText = FText::FromString(MyFString);

總結


  1. 一般情況,使用 FString 以支援複雜字串操作。
  2. 確定字串固定不變(這類字串往往起標識作用)時,使用 FName 可以提高效能。
  3. 當字串需要顯示給玩家時,使用 FText 以支援文字本地化和增強字型渲染效能。

參考


C++字元型別 char/wchar_t/char16_t/char32_t | Visual Studio 文件

虛幻引擎4 官方文件 | 字串

虛幻引擎4 官方文件 | Text Localization

UE4 C++基礎教程 - 字串和本地化

UE4製作多語言遊戲(本地化功能詳解)

UE4入門-常見基本資料型別-字串

系列其他文章:Aery的UE4 C++開發之旅系列文章

相關文章