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

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進行不同語言的翻譯:

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入門-常見基本數據類型-字符串