在之前的文章里,我们已经深入剖析了 C++ 的核心:内存管理、指针、多态和编译模型。这些都是你从 C# 领域跨越到 C++ 的必经之路。今天,我们将把目光投向更广阔的宏观层面,从现代 C++ 的语言特性开始,然后详细对比两个引擎在代码组织上的差异。

掌握这些,你就不仅仅是能用 C++ 写代码,而是能真正理解并驾驭 Unreal 庞大而精妙的工程体系了。


引言:拥抱现代 C++,告别繁琐

如果你对 C++ 的印象还停留在 C++98 时代,认为它语法陈旧、开发繁琐,那这篇文章会彻底刷新你的认知。自 C++11 以来,C++ 委员会每隔三年发布一个新版本,不断引入强大的新特性,让 C++ 开发变得更加现代化、简洁和高效。

这些新特性旨在解决 C++ 老版本中的痛点,让开发者能用更少的代码做更多的事情。它们在 Unreal 引擎中被广泛使用,是每个开发者都应该掌握的利器。


现代 C++ 特性:更简洁,更强大

auto 类型推导

在旧版 C++ 中,你必须显式声明变量的类型,这在处理复杂类型时会变得非常冗长。

C++

std::map<FString, TArray<int32>>::iterator It = MyMap.begin();

现代 C++ 的 auto 关键字可以自动推导出变量的类型,让代码变得更简洁、更易读。

C++

// 编译器会自动推导出 It 的类型
auto It = MyMap.begin();

Lambda 表达式

在 C# 中,我们习惯于使用匿名函数来处理事件回调或简化委托。C++11 引入的 Lambda 表达式提供了类似的功能,它让你可以在代码中快速创建临时的、没有名字的函数对象。这在处理异步任务、事件绑定或简化算法时非常有用。

C++

TArray<int32> Numbers = {1, 2, 3, 4, 5};

// 使用 Lambda 表达式遍历并打印每个元素
Algo::ForEach(Numbers, [](int32 Num)
{
    UE_LOG(LogTemp, Warning, TEXT("Number: %d"), Num);
});

智能指针

我们在第二篇文章中已经提到了智能指针,这里再次强调它的重要性。std::unique_ptrstd::shared_ptr 等智能指针是现代 C++ 内存管理的核心,它们通过 RAII 机制,确保堆内存能被自动释放,从根本上解决了手动内存管理带来的风险。

在 Unreal 中,你应该优先使用引擎提供的 TUniquePtrTSharedPtr,它们与 Unreal 的内存分配器和系统集成得更好。

Unreal 的日志系统(UE_LOG

Unreal 引擎没有直接使用 C++ 标准库的 iostream(例如 std::cout),而是提供了自己的一套日志宏 UE_LOG。这个宏是你的调试和信息输出的首选工具。

UE_LOG 提供了结构化的日志系统,你可以为日志指定不同的类别(Category)和详细级别(Verbosity),并在编辑器中进行筛选。

C++

// 类别(LogTemp)、详细级别(Warning)、日志消息
UE_LOG(LogTemp, Warning, TEXT("Player %s just picked up an item."), *PlayerName);


与 C# 特性对比:解决相同问题的不同哲学

LINQ vs. 命令式编程

在 C# 中,LINQ (Language-Integrated Query) 是一种声明式的数据查询语法,它让你可以像写 SQL 语句一样,以一种非常直观的方式操作集合。

C#

// C# 的 LINQ 示例
var activePlayers = players.Where(p => p.IsActive).ToList();

Unreal C++ 没有内置 LINQ 这样的特性。通常,你需要使用传统的命令式编程来遍历容器和筛选数据。不过,Unreal 的 Algo 库(比如 Algo::Filter)提供了一些函数式编程的便利,可以帮助你写出更简洁的代码。

async/await vs. 异步 API

C# 的 async/await 关键字是处理异步编程的革命性工具。它让异步代码看起来和同步代码一样简单,极大地简化了复杂的异步任务链。

Unreal C++ 没有直接等效的 async/await 语法。在 Unreal 中,异步任务通常通过以下方式管理:

  • Gameplay Tasks:Unreal 游戏框架的一部分,用于处理游戏性相关的异步操作,比如技能冷却、等待动画完成等。

  • Async 函数:Unreal 提供了许多异步 API,例如 FAsyncTaskAsync<>() 等,它们通常需要你手动管理任务的生命周期和回调。

  • 蓝图(Blueprint):在蓝图中,你可以非常直观地通过节点来创建和管理异步任务。

这种差异再次体现了两种语言在设计哲学上的不同:C# 倾向于在语言层面提供高级抽象,而 Unreal C++ 则倾向于提供底层的工具和框架,由开发者手动组合以达到最佳性能。


模块组织与代码结构:从 Assembly 到 Modules

C# 的模块组织

在 Unity 中,默认情况下,所有的脚本都会被编译到一个名为 Assembly-CSharp.dll 的程序集中。这在项目初期很方便,但随着代码量的增长,每次修改一个小文件都会导致整个程序集被重新编译,从而大大延长编译时间。

为了解决这个问题,Unity 引入了 Assembly Definition 文件(.asmdef。通过创建 .asmdef 文件,你可以将代码库划分为多个独立的、相互依赖的程序集。这样,当你修改一个程序集中的代码时,Unity 只会重新编译这个程序集以及依赖它的程序集,从而大幅缩短编译时间,实现更快的迭代。

Unreal 的模块与插件

Unreal 引擎从一开始就采用了一种更宏观、更精细的代码组织方式:模块(Modules)和插件(Plugins)

  • 模块(Modules):Unreal 的代码库被划分为数百个模块,比如 CoreEngineGameplay 等。每个模块都有一个独立的 Build.cs 文件,用于定义其编译规则、依赖项和公开的 API。

    • PublicDependencyModuleNames:声明模块所需的公共依赖。

    • PrivateDependencyModuleNames:声明模块所需的私有依赖。

    • AddEngineThirdPartyPrivateStaticDependencies:声明第三方库依赖。

    这种严格的模块化设计,使得 Unreal 的编译系统 UBT 能够精准地进行增量编译。当你修改 Gameplay 模块中的一个文件时,UBT 只会重新编译这个模块,而不是整个引擎。

  • 插件(Plugins):插件是模块的更高一级封装,它可以包含一个或多个模块,以及相关的资源文件。插件是**可热插拔(Hot-Pluggable)**的,你可以很容易地在不同的项目之间共享和复用功能。一个功能完整的插件,比如一套角色控制器,可以轻松地从一个项目迁移到另一个,极大地提升了开发效率。


核心总结:理解并驾驭 Unreal 的工程体系

通过这篇文章,你应该对 Unreal 引擎的代码组织方式有了更深的认识。

  • 语言特性:现代 C++ 的特性让开发变得更愉快,你应该拥抱 auto、Lambda 表达式和智能指针。

  • 设计哲学:C++ 和 C# 在解决异步、数据操作等问题上,有着不同的哲学。C# 依赖于高级语言特性,而 Unreal C++ 则依赖于底层的工具和框架。

  • 代码组织:Unreal 的模块与插件系统提供了比 Unity .asmdef 更强大、更细粒度的代码组织方式,这是它能够管理一个如此庞大代码库的关键。

理解这些,你就不仅仅是在写 C++ 代码,而是在融入一个复杂的、工业级的工程体系

到这里,我们的四篇系列文章就圆满结束了。从最基本的语法到宏观的代码组织,我们走过了一段不短的旅程。希望这些内容能为你从 Unity C# 开发者到 Unreal C++ 开发者的转型提供坚实的基础。

如果你还有任何想深入探讨的主题,或者在实际开发中遇到了任何挑战,随时可以提出来。我一直都在。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐