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入门-常见基本数据类型-字符串

系列其他文章:Aery的UE4 C++开发之旅系列文章

posted @ 2021-02-07 16:04  KillerAery  阅读(4757)  评论(2编辑  收藏  举报