使用 Unreal Engine 制作 FPS 游戏:从新建项目到 AI 敌人
本文带你用 Unreal Engine 5 制作第一人称射击游戏(FPS)。从 Epic Games Launcher 的第一次点击菜单开始,一直到做出能看见玩家并追击的敌人。每一部分都是可以逐步跟做的步骤,并说明代码文件应放在哪里。图示可以帮助你看清整体流程。
需要准备的东西
需要 Windows 10 或 11 电脑、支持 DirectX 12 的显卡、至少 100 GB 可用磁盘空间,以及一个免费的 Epic Games 账号。若要编译 C++ 代码,请安装包含“使用 C++ 的游戏开发”工作负载的 Visual Studio 2022。若只使用 Blueprint,可以不使用 C++ 开始。注意:本文的代码是依据 Unreal Engine 5 文档编写的,但没有在撰写本文的机器上编译和测试。列出的结果是步骤正确完成后应看到的效果。
示意图
1. 安装并创建 FPS 项目
先通过 Epic Games Launcher 安装 Unreal Engine,然后从现成的模板创建项目。
1. 打开 Epic Games Launcher,使用 Epic Games 账号登录
2. 在左侧菜单中选择 Unreal Engine,并打开 Library 标签
3. 点击 + Add Version,选择最新的 Unreal Engine 5 版本,然后点击 Install
4. 安装完成后,点击该版本的 Launch
5. 在 Unreal Project Browser 左侧选择 Games
6. 选择 First Person 模板
7. 在 Project Defaults 中,要写代码就选 C++,想先用可视化方式就选 Blueprint
8. 勾选 Starter Content,以获得基础模型和材质
9. 为项目命名,例如 FpsDemo,然后点击 Create
**应看到的结果:** 编辑器打开,里面有示例关卡和第一人称角色。按 Play 后可以走动并转动镜头。
2. 项目结构与脚本的位置
了解项目各部分的作用。C++ 代码放在 Source 文件夹,Blueprint 和模型则在 Content 中。
**注:** 所有 C++ 脚本都在 Source/FpsDemo/ 中。Blueprint 在编辑器中创建和编辑。不要手动修改 Content/ 中的文件。
**代码: project-tree.txt**
FpsDemo/
├── FpsDemo.uproject ไฟล์โปรเจกต์ ดับเบิลคลิกเพื่อเปิดในเอดิเตอร์
├── Config/ ค่าตั้งต้นของเกม เช่น ปุ่มควบคุม
├── Content/ ไฟล์ของเกมที่สร้างในเอดิเตอร์ เช่น Blueprint โมเดล ฉาก เสียง
├── Source/
│ ├── FpsDemo/ โค้ด C++ ของเกม เขียนเป็นไฟล์ .h และ .cpp
│ │ └── FpsDemo.Build.cs ระบุว่าโมดูลนี้ใช้ไลบรารีอะไร เช่น AIModule
│ ├── FpsDemo.Target.cs
│ └── FpsDemoEditor.Target.cs
└── Binaries/, Intermediate/, Saved/ ไฟล์ที่ UE สร้างเอง ไม่ต้องแก้
3. 将 3D 模型导入项目
大多数模型以 FBX 文件保存。可以从 Fab 或其他你有使用权限的来源获取,并在游戏中使用前确认许可条款。
1. 在底部的 Content Browser 中右键点击目标文件夹,例如 Content/Models
2. 选择 Import to /Game/Models,然后选择 .fbx 文件
3. 在 FBX Import Options 中,如果模型有骨骼就选 Skeletal Mesh,如果是固定物体就选 Static Mesh
4. 勾选 Import Materials and Textures,然后点击 Import
5. 把模型从 Content Browser 拖入关卡进行测试
**应看到的结果:** 模型出现在 Content Browser 中,拖入关卡后会出现在场景里。
4. 使用 Enhanced Input 设置操作
Unreal Engine 5 使用 Enhanced Input 系统把按键和动作分开,因此不改代码也能更换按键。
1. 在 Content Browser 中打开模板自带的 Input 文件夹
2. 打开 IA_Fire,确认它是用于射击按键的 Digital (bool) Input Action
3. 打开 IMC_Default,确认鼠标左键已映射到 IA_Fire
4. 如需更换按键,在 IMC_Default 中修改映射并点击 Save
5. 按 Play,确认左键点击会触发射击
**应看到的结果:** 每次按下绑定的按键,角色的射击函数都会被调用。
5. 用射线检测射击
大多数 FPS 枪械会从摄像机向前发出一条看不见的直线,并检查击中了什么。本例调用 LineTraceSingleByChannel,并对被击中的对象造成伤害。
1. 打开 Source/FpsDemo/FpsCharacter.h,按示例添加 Range 和 Damage 变量以及 Fire 的声明
2. 打开 Source/FpsDemo/FpsCharacter.cpp,添加 Fire 函数
3. 在 FpsCharacter.cpp 中找到模板生成的 OnFire 函数,把生成子弹的代码换成调用 Fire()
4. 按 Ctrl+Alt+F11 或在编辑器中点击 Compile 来编译代码
**代码: FpsCharacter_Fire.cpp**
// ไฟล์: Source/FpsDemo/FpsCharacter.h (เพิ่มในคลาสตัวละครผู้เล่นที่ template สร้างให้)
// UPROPERTY(EditAnywhere, Category = "Combat")
// float Range = 10000.f;
//
// UPROPERTY(EditAnywhere, Category = "Combat")
// float Damage = 25.f;
//
// void Fire();
//
// ไฟล์: Source/FpsDemo/FpsCharacter.cpp (ฟังก์ชัน Fire ด้านล่าง)
#include "FpsCharacter.h"
#include "Camera/CameraComponent.h"
#include "Engine/World.h"
#include "Kismet/GameplayStatics.h"
void AFpsCharacter::Fire()
{
if (!FirstPersonCameraComponent)
{
return;
}
// จุดเริ่มคือกล้อง และจุดสิ้นสุดคือระยะ Range ไปทางที่กล้องมอง
const FVector Start = FirstPersonCameraComponent->GetComponentLocation();
const FVector End = Start + FirstPersonCameraComponent->GetForwardVector() * Range;
FHitResult Hit;
FCollisionQueryParams Params;
Params.AddIgnoredActor(this); // ไม่ให้เส้นยิงชนตัวเอง
if (GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, Params))
{
if (AActor* Target = Hit.GetActor())
{
UGameplayStatics::ApplyDamage(Target, Damage, GetController(), this, UDamageType::StaticClass());
}
}
}
**应看到的结果:** 左键点击瞄准的对象会造成伤害。射击时 Output Log 中没有错误,即表示成功。
6. 敌人受伤并被消灭
从 Character 创建敌人类。它有生命值,通过 TakeDamage 受到伤害,生命值为零时被移除。
1. 在编辑器中选择 File > New C++ Class,父类选 Character,命名为 EnemyCharacter
2. 打开 EnemyCharacter.h,按示例添加声明
3. 把 EnemyCharacter.cpp 中的代码放入文件
4. 编译后,基于 EnemyCharacter 创建 Blueprint 并放入关卡
**代码: EnemyCharacter.cpp**
// ไฟล์: Source/FpsDemo/EnemyCharacter.h
// UCLASS()
// class FPSDEMO_API AEnemyCharacter : public ACharacter
// {
// GENERATED_BODY()
// public:
// AEnemyCharacter();
// virtual float TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent,
// AController* EventInstigator, AActor* DamageCauser) override;
// protected:
// UPROPERTY(EditAnywhere, Category = "Combat")
// float Health = 100.f;
// };
//
// ไฟล์: Source/FpsDemo/EnemyCharacter.cpp
#include "EnemyCharacter.h"
#include "EnemyAIController.h"
AEnemyCharacter::AEnemyCharacter()
{
// ให้ AI Controller ของศัตรูเข้าควบคุมอัตโนมัติ เมื่อวางในฉากหรือสร้างขึ้นมา
AIControllerClass = AEnemyAIController::StaticClass();
AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
}
float AEnemyCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent,
AController* EventInstigator, AActor* DamageCauser)
{
const float Applied = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
Health -= Applied;
if (Health <= 0.f)
{
Destroy(); // เมื่อพลังชีวิตหมด ศัตรูหายไปจากฉาก
}
return Applied;
}
**应看到的结果:** 把敌人打到生命值为零时,它会从关卡中消失。
7. 添加关卡、道具和 NavMesh
敌人需要在地面上行走,所以关卡需要 NavMesh,它是可行走区域的地图。也要添加一些物体作为掩体。
1. 从你有使用权限的来源查找道具,例如 Fab,然后选择 Add to Project 或下载后导入
2. 把物体从 Content Browser 拖入关卡,并用 Move 和 Scale 工具调整位置和大小
3. 在 Place Actors 中搜索 Nav Mesh Bounds Volume,并把它拖入关卡
4. 调整 Volume 的大小,使其覆盖可行走的区域
5. 在编辑器中按 P 键显示 NavMesh
**应看到的结果:** 在 P 视图中,可行走的地面显示为绿色,物体下方的区域没有绿色。
8. 检测玩家的 AI 控制器
AI 控制器是敌人的“大脑”。它使用 AI Perception 判断是否看见玩家,并把结果写入 Blackboard。
1. 从 AIController 创建 C++ 类,命名为 EnemyAIController
2. 打开 FpsDemo.Build.cs,在 PublicDependencyModuleNames 中加入 "AIModule"
3. 把 .h 和 .cpp 代码放入项目并编译
4. EnemyCharacter 的构造函数指定了 AI 控制器,因此放置或生成敌人时控制器会自动接管
**代码: EnemyAIController.cpp**
// ไฟล์: Source/FpsDemo/EnemyAIController.h
// UCLASS()
// class FPSDEMO_API AEnemyAIController : public AAIController
// {
// GENERATED_BODY()
// public:
// AEnemyAIController();
// protected:
// virtual void BeginPlay() override;
// UFUNCTION()
// void OnTargetPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus);
// UPROPERTY(VisibleAnywhere)
// UAIPerceptionComponent* Perception;
// UPROPERTY()
// UAISenseConfig_Sight* Sight;
// UPROPERTY(EditAnywhere, Category = "AI")
// UBehaviorTree* BehaviorTreeAsset;
// };
//
// ไฟล์: Source/FpsDemo/EnemyAIController.cpp
// ต้องเพิ่ม "AIModule" ใน PublicDependencyModuleNames ของ FpsDemo.Build.cs ก่อน
#include "EnemyAIController.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Perception/AIPerceptionComponent.h"
#include "Perception/AISenseConfig_Sight.h"
#include "Perception/AIPerceptionTypes.h"
AEnemyAIController::AEnemyAIController()
{
Perception = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("Perception"));
SetPerceptionComponent(*Perception);
Sight = CreateDefaultSubobject<UAISenseConfig_Sight>(TEXT("Sight"));
Sight->SightRadius = 1500.f; // มองเห็นได้ไกลเท่านี้
Sight->LoseSightRadius = 1800.f; // ต้องไกลเกินนี้จึงจะเสียการมองเห็น
Sight->PeripheralVisionAngleDegrees = 90.f;
Sight->DetectionByAffiliation.bDetectEnemies = true;
Sight->DetectionByAffiliation.bDetectNeutrals = true;
Sight->DetectionByAffiliation.bDetectFriendlies = true;
Perception->ConfigureSense(*Sight);
Perception->SetDominantSense(Sight->GetSenseImplementation());
Perception->OnTargetPerceptionUpdated.AddDynamic(this, &AEnemyAIController::OnTargetPerceptionUpdated);
}
void AEnemyAIController::BeginPlay()
{
Super::BeginPlay();
if (BehaviorTreeAsset)
{
RunBehaviorTree(BehaviorTreeAsset);
}
}
void AEnemyAIController::OnTargetPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus)
{
// เห็นผู้เล่นเมื่อใด ให้เขียนลงช่อง TargetActor ใน Blackboard ที่ Behavior Tree ใช้อยู่
if (UBlackboardComponent* BB = GetBlackboardComponent())
{
BB->SetValueAsObject(TEXT("TargetActor"), Stimulus.WasSuccessfullySensed() ? Actor : nullptr);
}
}
**应看到的结果:** 玩家进入敌人的视野距离和角度后,Blackboard 中的 TargetActor 键会保存玩家。
9. 行为树与 Blackboard
行为树是决定行动的图。例如:如果有目标,就走过去并射击;否则巡逻。Blackboard 保存行为树用来做决定的数据。
1. 在 Content Browser 中右键,选择 Artificial Intelligence > Blackboard,命名为 BB_Enemy
2. 打开 BB_Enemy,添加名为 TargetActor、类型为 Object 的键
3. 再次右键,选择 Artificial Intelligence > Behavior Tree,命名为 BT_Enemy
4. 打开 BT_Enemy,把 Blackboard Asset 设为 BB_Enemy
5. 在根节点放一个 Selector,再放一个带 Blackboard 条件的 Sequence,检查 TargetActor 已设置,然后接一个使用 TargetActor 的 MoveTo 任务
6. 在敌人的 Blueprint 详情中,把 AI Controller 的 Behavior Tree 设为 BT_Enemy
**应看到的结果:** 敌人看见玩家时会走向玩家;玩家离开视野后,敌人会停止追击。
10. 构建与测试
检查所有部分能否协同工作,并解决常见问题。
1. 按 Compile 或 Ctrl+Alt+F11 编译 C++ 代码
2. 在编辑器中按 Play,走向敌人
3. 射击敌人直到其消失,检查伤害是否与你设定的数值一致
4. 打开 Window > Developer Tools > Output Log,查看警告和错误
**应看到的结果:** 游戏可以运行。敌人看见玩家时会转向玩家,被击中后生命值减少,生命值为零时消失。
下一步
接下来可以添加枪声、计分系统、开始菜单和敌人动画。主要参考资料是 Unreal Engine Learning Portal,以及 dev.epicgames.com/documentation 上的 Unreal Engine 5 文档。
总结
从 Epic Games Launcher 的模板创建 FPS 项目,导入模型,用射线检测编写射击和敌人伤害,设置 AI 控制器来检测玩家,并将其连接到行为树和 Blackboard。每一步都是点击菜单或按顺序粘贴代码。