UE5 C++游戏开发入门:从环境搭建到角色交互实战
1. 项目概述为什么选择UE5与C作为你的起点如果你正在寻找一份免费的Unreal Engine 5游戏开发教程并且希望从C脚本入手那么你来对地方了。我作为一个在游戏行业摸爬滚打多年的开发者深知学习路径的选择至关重要。UE5无疑是当下最炙手可热的游戏引擎其Nanite虚拟化微多边形几何体和Lumen全局光照系统带来的视觉革命让独立开发者也能创造出3A级别的画面。而C作为UE5的底层核心语言是解锁引擎全部潜力、实现高性能游戏逻辑和深度定制的钥匙。很多新手会纠结于蓝图Blueprints和C之间蓝图可视化、上手快适合原型设计和简单逻辑但当你需要处理复杂的算法、管理大规模数据、优化性能或者想要深入理解引擎架构时C是绕不开的坎。这份教程就是为你准备的——无论你是刚接触编程的游戏爱好者还是有一定C基础想转战游戏开发的程序员我将带你从零开始搭建环境理解UE5的C编程范式一步步构建出可运行的游戏功能模块。记住我们的目标不是“看会”而是“做会”。2. 环境准备搭建坚如磐石的C开发堡垒在兴奋地打开UE5编辑器之前一个稳定、高效的开发环境是成功的一半。这一步的坑最多也最容易被忽视很多人在这里就放弃了。我会带你避开所有雷区。2.1 安装Visual Studio 2022与必要的组件UE5对C编译环境有严格的要求。不要使用MinGW或其它编译器必须使用Microsoft Visual C (MSVC)。下载与安装前往微软官网下载Visual Studio 2022 Community版免费。运行安装程序。工作负载选择这是关键一步。在安装界面选择“使用C的桌面开发”工作负载。这还不够你需要点击这个工作负载右侧的“修改”或“安装详细信息”。勾选必备组件在弹出的列表中确保勾选以下组件MSVC v143 - VS 2022 C x64/x86 生成工具这是核心编译器。Windows 10/11 SDK最新的Windows SDK。C CMake 工具用于项目管理。对 v143 生成工具的 C Clang 编译工具可选但推荐Clang在某些情况下能提供更好的错误信息。C分析工具用于代码静态检查。安装位置建议将VS安装在默认路径或一个空间充足的SSD硬盘上因为后续的UE5引擎和项目会占用巨大空间。注意安装完成后你可能会遇到网络热词中提到的“Microsoft Visual C Redistributable”相关问题。这不是开发环境而是运行库。你的游戏打包后分发给玩家时需要包含相应的运行库。开发时只要VS安装正确就不会有问题。2.2 获取并安装Unreal Engine 5UE5的获取方式是通过Epic Games启动器。安装Epic Games启动器从Epic官网下载并安装。下载引擎在启动器的“虚幻引擎”标签页点击“库”然后点击“引擎版本”旁的“”号。选择最新的5.x版本例如5.3, 5.4。在“选项”中我强烈建议你勾选所有平台支持如Android、iOS即使你现在不做移动端开发这能确保所有必要的头文件和库都被安装避免未来奇怪的编译错误。耐心等待引擎大小超过30GB下载和安装需要很长时间请确保网络稳定和磁盘空间建议预留100GB以上。2.3 配置你的代码编辑器VS Code还是Visual Studio这是一个常见的抉择。Visual Studio是“官方钦定”的IDE与UE5集成度最高调试体验无缝。而VS Code更轻量、快速深受喜欢定制化的开发者喜爱。对于纯新手我强烈建议使用Visual Studio 2022。它能自动识别并加载UE5项目的解决方案.sln文件IntelliSense代码补全和调试器开箱即用省去大量配置麻烦。你可以在UE5编辑器中直接点击“工具”-“打开Visual Studio”来无缝切换。如果你有一定经验喜欢轻量级可以选择VS Code。但这需要额外配置安装C/C扩展ms-vscode.cpptools。为UE5项目生成compile_commands.json文件通过UE5内置的UnrealBuildTool参数或使用第三方工具并配置C/C扩展指向它才能获得准确的代码补全。调试配置更为复杂。鉴于本教程面向从零开始的学习者后续所有步骤和截图都将基于Visual Studio 2022进行。这能让你把精力集中在学习C和UE5本身而不是折腾工具链。3. 创建你的第一个UE5 C项目环境就绪让我们开始创造。打开Epic Games启动器切换到“虚幻引擎”标签点击右上角的“启动”按钮。3.1 项目模板选择与核心设置选择游戏模板在项目浏览器中选择“游戏”类别然后选择“空白”模板。这是最干净的起点没有预设的蓝图或内容干扰我们学习C。项目设置项目默认设置选择“C”而不是“蓝图”。这是最关键的一步这会告诉UE5为我们生成初始的C项目框架。质量预设选择“最大”我们要体验UE5的全部画质。初学者内容包取消勾选。我们从头开始需要什么加什么保持项目纯净。目标平台桌面。光线追踪可选如果你的显卡支持RTX系列可以勾选。命名与路径给你的项目起一个响亮的名字比如MyFirstCPPGame。路径不要有中文或特殊字符最好在SSD硬盘上。点击“创建”UE5将开始生成项目文件并自动打开Visual Studio 2022。第一次打开会触发编译编译着色器、编译代码这可能需要几分钟到十几分钟取决于你的电脑性能。3.2 初识项目结构C开发者的视角项目创建完成后让我们用文件资源管理器而不是UE5的内容浏览器看一下目录结构这对C开发者至关重要MyFirstCPPGame/ ├── Source/ │ ├── MyFirstCPPGame/ # 你的游戏模块 │ │ ├── MyFirstCPPGame.Build.cs # 模块构建规则文件 │ │ ├── MyFirstCPPGame.cpp # 模块入口实现文件 │ │ ├── MyFirstCPPGame.h # 模块入口头文件 │ │ ├── MyFirstCPPGameGameModeBase.h/.cpp # 游戏模式类 │ │ └── ... (其他自动生成的类) │ └── MyFirstCPPGame.Target.cs # 打包目标游戏客户端 │ └── MyFirstCPPGameEditor.Target.cs # 编辑器目标 ├── Content/ # 资源目录蓝图、材质、模型等 ├── Config/ # 配置文件 └── MyFirstCPPGame.uproject # 项目描述文件.Build.cs文件定义了模块的依赖关系比如你如果需要用到AI模块、网络模块就需要在这里添加PublicDependencyModuleNames.AddRange。GameModeBase类控制游戏的规则比如默认Pawn玩家角色、HUD、玩家控制器等。它是游戏世界的“导演”。.uproject文件右键选择“Generate Visual Studio project files”可以在你增删C类后重新生成VS的解决方案文件。4. UE5 C编程核心概念与第一个Actor类UE5的C不是标准的C它是一套建立在C之上的、由宏Macros和反射系统驱动的框架。理解这几个核心概念是你从普通C程序员转变为UE5开发者的关键。4.1 UCLASS, UPROPERTY, UFUNCTION反射系统的基石UE5通过一套特定的宏将C类、变量和函数暴露给编辑器、蓝图系统和网络复制。UCLASS()标记一个类使其被UE5的反射系统识别。这意味着这个类可以在编辑器中放置、可以被蓝图继承、可以序列化。// MyActor.h #pragma once #include CoreMinimal.h #include GameFramework/Actor.h #include MyActor.generated.h // 必须包含由UHT生成。 UCLASS() class MYFIRSTCPPGAME_API AMyActor : public AActor // 注意Actor类前缀通常为A { GENERATED_BODY() public: AMyActor(); // 构造函数 };UPROPERTY()标记一个成员变量。通过其参数你可以定义变量在编辑器中的显示方式EditAnywhere、是否可被蓝图读写BlueprintReadWrite、是否参与网络复制Replicated等。UPROPERTY(EditAnywhere, BlueprintReadWrite, CategoryMy Actor) float Health; UPROPERTY(VisibleAnywhere, BlueprintReadOnly, CategoryMy Actor) UStaticMeshComponent* MeshComponent;UFUNCTION()标记一个成员函数。同样可以定义其是否可被蓝图调用BlueprintCallable、是否可被覆盖BlueprintImplementableEvent等。UFUNCTION(BlueprintCallable, CategoryMy Actor) void TakeDamage(float DamageAmount);4.2 创建并编写你的第一个C Actor让我们在游戏中创建一个可以交互的物体。在编辑器中添加C类在UE5编辑器的内容浏览器中右键 - 新建C类。选择“Actor”作为父类命名为MyFirstActor。编辑头文件.h打开生成的MyFirstActor.h我们添加一个网格组件和一个可编辑的速度属性。#pragma once #include CoreMinimal.h #include GameFramework/Actor.h #include MyFirstActor.generated.h UCLASS() class MYFIRSTCPPGAME_API AMyFirstActor : public AActor { GENERATED_BODY() public: AMyFirstActor(); protected: virtual void BeginPlay() override; virtual void Tick(float DeltaTime) override; // 声明一个静态网格组件 UPROPERTY(VisibleAnywhere, BlueprintReadOnly, CategoryComponents) UStaticMeshComponent* MeshComp; // 一个可在编辑器中编辑的速度变量 UPROPERTY(EditAnywhere, CategoryMovement) float RotationSpeed; private: // 内部使用的旋转角度 float CurrentRotation; };编辑源文件.cpp打开MyFirstActor.cpp实现构造函数、BeginPlay和Tick函数。#include MyFirstActor.h AMyFirstActor::AMyFirstActor() { PrimaryActorTick.bCanEverTick true; // 允许每帧调用Tick // 创建并附加根组件虽然不是必须但是好习惯 RootComponent CreateDefaultSubobjectUSceneComponent(TEXT(RootComponent)); // 创建静态网格组件并附加到根组件 MeshComp CreateDefaultSubobjectUStaticMeshComponent(TEXT(MeshComp)); MeshComp-SetupAttachment(RootComponent); // 设置默认值 RotationSpeed 45.0f; // 默认每秒旋转45度 CurrentRotation 0.0f; } void AMyFirstActor::BeginPlay() { Super::BeginPlay(); // 游戏开始时调用一次可以在这里进行初始化 UE_LOG(LogTemp, Warning, TEXT(MyFirstActor %s has begun play!), *GetName()); } void AMyFirstActor::Tick(float DeltaTime) { Super::Tick(DeltaTime); // 每帧调用DeltaTime是上一帧到这一帧的时间秒 CurrentRotation RotationSpeed * DeltaTime; FRotator NewRotation FRotator(0.0f, CurrentRotation, 0.0f); SetActorRotation(NewRotation); }编译与测试在Visual Studio中按CtrlShiftB编译解决方案或F7。编译成功后回到UE5编辑器它会自动重新加载模块。在内容浏览器中你可以找到你的MyFirstActor类。将其拖拽到场景视口中。运行游戏PIE你会看到这个Actor在持续旋转。在细节面板中你可以实时修改RotationSpeed属性观察旋转速度的变化。实操心得CreateDefaultSubobject只能在构造函数中调用用于创建组件并设置默认属性。BeginPlay是Actor被放入世界并准备开始游戏逻辑时调用而Tick是每帧更新。对于不需要每帧更新的逻辑应避免使用Tick以提升性能。可以通过设置PrimaryActorTick.bCanEverTick false来关闭它。5. 深入核心玩家输入、碰撞与游戏逻辑一个会旋转的方块只是个开始。真正的游戏需要交互。接下来我们实现一个由玩家控制移动的角色并为其添加碰撞检测。5.1 创建可控制的角色CharacterCharacter是自带移动组件CharacterMovementComponent和胶囊体碰撞的Pawn非常适合作为玩家角色。新建C类选择“Character”作为父类命名为MyPlayerCharacter。设置输入映射Input Mapping Context这是UE5增强输入系统Enhanced Input System的一部分比旧的输入系统更强大、灵活。在内容浏览器中右键 - 输入 - 输入映射上下文Input Mapping Context命名为IMC_Default。双击打开点击“添加映射”创建一个“IA_Move”动作。为这个动作添加两个输入W键正向和S键负向用于前后移动。再创建一个“IA_Look”动作绑定鼠标X轴和Y轴。再创建一个“IA_Jump”动作绑定空格键。在角色类中绑定输入编辑MyPlayerCharacter.h和.cpp。// MyPlayerCharacter.h #include InputActionValue.h // 需要包含 // ... 其他include UCLASS() class MYFIRSTCPPGAME_API AMyPlayerCharacter : public ACharacter { // ... GENERATED_BODY 等 public: AMyPlayerCharacter(); protected: virtual void BeginPlay() override; virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override; // 输入相关的属性 UPROPERTY(EditAnywhere, BlueprintReadOnly, CategoryInput) class UInputMappingContext* DefaultMappingContext; UPROPERTY(EditAnywhere, BlueprintReadOnly, CategoryInput) class UInputAction* MoveAction; UPROPERTY(EditAnywhere, BlueprintReadOnly, CategoryInput) class UInputAction* LookAction; UPROPERTY(EditAnywhere, BlueprintReadOnly, CategoryInput) class UInputAction* JumpAction; // 输入处理函数 void Move(const FInputActionValue Value); void Look(const FInputActionValue Value); };// MyPlayerCharacter.cpp #include MyPlayerCharacter.h #include EnhancedInputComponent.h #include EnhancedInputSubsystems.h #include GameFramework/CharacterMovementComponent.h AMyPlayerCharacter::AMyPlayerCharacter() { // 设置角色属性... bUseControllerRotationPitch false; bUseControllerRotationYaw false; bUseControllerRotationRoll false; GetCharacterMovement()-bOrientRotationToMovement true; // 角色朝向移动方向 GetCharacterMovement()-RotationRate FRotator(0.0f, 500.0f, 0.0f); // 旋转速率 } void AMyPlayerCharacter::BeginPlay() { Super::BeginPlay(); // 获取玩家控制器并为其添加输入映射 if (APlayerController* PlayerController CastAPlayerController(Controller)) { if (UEnhancedInputLocalPlayerSubsystem* Subsystem ULocalPlayer::GetSubsystemUEnhancedInputLocalPlayerSubsystem(PlayerController-GetLocalPlayer())) { if (DefaultMappingContext) { Subsystem-AddMappingContext(DefaultMappingContext, 0); } } } } void AMyPlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) { // 使用EnhancedInputComponent绑定输入 if (UEnhancedInputComponent* EnhancedInputComponent CastUEnhancedInputComponent(PlayerInputComponent)) { if (MoveAction) EnhancedInputComponent-BindAction(MoveAction, ETriggerEvent::Triggered, this, AMyPlayerCharacter::Move); if (LookAction) EnhancedInputComponent-BindAction(LookAction, ETriggerEvent::Triggered, this, AMyPlayerCharacter::Look); if (JumpAction) EnhancedInputComponent-BindAction(JumpAction, ETriggerEvent::Started, this, ACharacter::Jump); } } void AMyPlayerCharacter::Move(const FInputActionValue Value) { FVector2D MovementVector Value.GetFVector2D(); if (Controller ! nullptr) { // 获取控制器的前向和右向向量 const FRotator Rotation Controller-GetControlRotation(); const FRotator YawRotation(0, Rotation.Yaw, 0); const FVector ForwardDirection FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X); const FVector RightDirection FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y); // 应用移动 AddMovementInput(ForwardDirection, MovementVector.Y); AddMovementInput(RightDirection, MovementVector.X); } } void AMyPlayerCharacter::Look(const FInputActionValue Value) { FVector2D LookAxisVector Value.GetFVector2D(); if (Controller ! nullptr) { AddControllerYawInput(LookAxisVector.X); AddControllerPitchInput(LookAxisVector.Y); } }配置蓝图子类并测试编译后在内容浏览器中基于MyPlayerCharacter创建一个蓝图类如BP_MyPlayerCharacter。在蓝图的细节面板中将我们之前创建的IMC_Default、IA_Move等资源分别赋值给对应的属性。最后在世界的“游戏模式重载”中将默认Pawn类设置为你的BP_MyPlayerCharacter。运行游戏你就可以用WASD移动、鼠标环视、空格跳跃了。5.2 实现碰撞与伤害交互让我们让之前创建的MyFirstActor对玩家造成伤害。为MyFirstActor添加碰撞事件修改MyFirstActor.h和.cpp。// MyFirstActor.h 添加声明 UFUNCTION() void OnMeshBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult SweepResult); UPROPERTY(EditAnywhere, CategoryDamage) float DamageAmount;// MyFirstActor.cpp 构造函数中绑定事件并设置碰撞 AMyFirstActor::AMyFirstActor() { // ... 之前的代码 MeshComp-SetCollisionProfileName(TEXT(OverlapAllDynamic)); // 设置碰撞预设 MeshComp-OnComponentBeginOverlap.AddDynamic(this, AMyFirstActor::OnMeshBeginOverlap); // 绑定重叠事件 DamageAmount 10.0f; } void AMyFirstActor::OnMeshBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult SweepResult) { // 检查重叠的Actor是否是我们的玩家角色 AMyPlayerCharacter* Player CastAMyPlayerCharacter(OtherActor); if (Player) { // 这里应该调用一个通用的伤害接口为了简单我们直接打印 UE_LOG(LogTemp, Warning, TEXT(%s took %f damage from %s!), *OtherActor-GetName(), DamageAmount, *GetName()); // 在实际项目中你可能会调用 Player-ApplyDamage(DamageAmount, ...); } }在玩家角色中实现伤害接口为了更规范我们可以让玩家角色实现一个伤害接口。创建一个新的C接口类在添加类时选择“Unreal Interface”命名为Damageable。然后在玩家角色中实现它。这涉及到UE5的接口系统是构建松耦合游戏逻辑的推荐做法。注意事项碰撞处理是性能敏感区域。确保碰撞预设Collision Preset设置合理避免使用过于复杂的碰撞几何体。对于频繁发生的碰撞如子弹可以考虑使用OnComponentHit阻挡而非OnComponentBeginOverlap重叠或者使用射线检测Line Trace来获得更精确的控制。6. 调试、优化与打包发布代码写完了但工作还没结束。如何高效地调试和优化最终将你的作品打包分享是最后也是最重要的一步。6.1 UE5 C调试技巧使用Visual Studio调试器这是最强大的工具。在VS中设置启动项目为MyFirstCPPGameEditor你的项目名Editor然后按F5启动调试。你可以在C代码中设置断点当游戏运行到断点时执行会暂停你可以查看所有变量的值、调用堆栈。使用UE_LOG进行日志输出如上文示例UE_LOG是打印信息到“输出日志”窗口和日志文件中的好方法。它有多个日志级别Log, Warning, Error。合理使用日志可以帮助你追踪程序流程。使用ensure和check宏用于断言。check(条件)如果条件为假在开发版本中会触发崩溃并指出错误位置用于捕捉绝不应该发生的错误。ensure(条件)如果条件为假会记录一次错误在屏幕上显示黄色警告但程序继续运行。适合用于检查那些可能出错但不至于崩溃的情况。使用编辑器的“调试”功能在编辑器中运行游戏时你可以查看“世界大纲视图”中Actor的实时属性使用“调试”菜单下的各种工具。6.2 性能优化初步认知对于初学者先关注几个最容易出问题的地方Tick的滥用这是性能杀手第一名。问问自己这个逻辑真的需要每帧都执行吗能不能用定时器FTimerHandle能不能用事件驱动对于不活动的Actor可以关闭Tick。动态内存分配在Tick或频繁调用的函数中避免使用new/delete或TArray::Add如果可能导致扩容。使用对象池Object Pooling来复用对象例如子弹、特效。蓝图与C的调用开销从C调用蓝图函数或从蓝图调用C函数都有一定的开销。对于每帧调用的高频函数尽量纯C实现。使用性能分析工具UE5内置了强大的性能分析器~键打开控制台输入stat unit查看帧时间stat scenerendering查看渲染状态。学习使用Unreal Insights进行深度分析。6.3 打包你的游戏当你完成了一个可玩的版本就可以打包分享了。项目设置检查在“编辑”-“项目设置”中检查“地图和模式”设置正确的默认地图和游戏模式。打包配置点击“平台”-“Windows”-“打包项目设置”选择目标目录。执行打包点击“平台”-“Windows”-“打包项目”。UE5将开始编译所有内容包括着色器并生成一个独立的可执行文件目录。这个过程很耗时并且可能会遇到各种缺失资源或编译错误需要根据日志逐一解决。测试打包版本务必在打包后的目录中运行游戏确保所有功能在脱离编辑器环境后依然正常。7. 常见问题与排查技巧实录在学习和开发过程中你一定会遇到各种错误和奇怪的现象。这里记录了一些最常见的问题和解决方法。问题现象可能原因排查与解决思路编译失败报错“无法打开源文件…”或“未定义的标识符”1. 头文件包含路径错误。2. 模块依赖未在.Build.cs文件中添加。3. 生成的Visual Studio项目文件过时。1. 检查#include路径是否正确UE5头文件通常用#include “组件名/文件名.h”格式。2. 打开项目名.Build.cs在PublicDependencyModuleNames中添加缺失的模块名如AIModule,UMG。3. 右键.uproject文件选择“Generate Visual Studio project files”然后重新编译。编辑器能运行但打包失败1. 某些资源只在编辑器中引用未正确打包。2. C代码调用了编辑器专用模块的API。3. 插件未启用或配置错误。1. 检查所有在C中通过构造函数FObjectFinder或运行时动态加载的资源路径是否正确资源是否存在。2. 确保打包配置中包含了所有用到的插件。检查代码将#if WITH_EDITOR宏包裹的编辑器专用代码移出或做条件编译。3. 在“编辑”-“插件”中确保所有用到的插件已启用并且支持打包的运行时Runtime模式。游戏运行时崩溃无明确错误1. 空指针访问最常见。2. 数组越界。3. 多线程访问冲突。1. 在VS中启用调试查看崩溃时的调用堆栈。重点关注崩溃前最后执行的你的代码。2. 大量使用ensure和check来提前暴露问题。例如在解引用指针前check(MyPtr ! nullptr)。3. 使用UE5的“崩溃报告器”查看生成的崩溃dump文件。蓝图无法找到或编译C中创建的变量/函数1.UPROPERTY或UFUNCTION宏的BlueprintReadWrite/BlueprintCallable等标记未设置或设置错误。2. 头文件修改后未重新编译/生成项目。3. 变量/函数不是public访问权限。1. 仔细检查宏参数。BlueprintReadOnly表示蓝图只读BlueprintReadWrite表示可读写。2. 修改头文件后必须在VS中编译C代码然后UE4编辑器会热重载。有时需要完全关闭编辑器再打开。3. 确保暴露给蓝图的成员在类的public:部分。输入绑定无效角色无反应1. 输入映射上下文Input Mapping Context未添加到玩家的子系统。2. 输入动作Input Action未正确绑定到增强输入组件。3. 玩家控制器Player Controller未正确设置或获取失败。1. 在角色的BeginPlay中确保成功获取到UEnhancedInputLocalPlayerSubsystem并添加了映射上下文。2. 在SetupPlayerInputComponent中确认EnhancedInputComponent转换成功并且BindAction调用无误。3. 在BeginPlay中打印或调试Controller和PlayerController是否有效。碰撞事件不触发1. 碰撞预设Collision Preset设置不当双方无法产生重叠Overlap或阻挡Block。2. 碰撞组件未正确生成或附加。3. 事件绑定代码未执行例如未在构造函数中绑定。1. 在编辑器中查看两个Actor的静态网格组件的“碰撞预设”确保至少一方能检测到另一方例如一个设为OverlapAll另一个设为BlockAll。2. 在C构造函数中确认CreateDefaultSubobject创建了碰撞组件并且SetupAttachment到了根组件。3. 确认OnComponentBeginOverlap.AddDynamic这行代码被执行了可以在其后加个日志输出。我个人在实际操作中的体会是学习UE5 C最大的障碍不是语言本身而是理解其庞大的框架和特有的编程模式。不要试图一次性掌握所有内容。从一个具体的小目标开始比如“让一个方块旋转”、“让角色跳跃”遇到问题就针对性地去查文档、搜社区、看源码。多使用UE_LOG来跟踪程序流善用调试器。最重要的是保持耐心动手去写去试错。每一个你亲手解决掉的编译错误和运行时Bug都会让你对引擎的理解更深一层。这个免费教程只是一个起点UE5的海洋广阔无垠祝你航行愉快。