使用 Unreal Engine 制作 FPS 游戏:从新建项目到 AI 敌人

使用 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 文档编写的,但没有在撰写本文的机器上编译和测试。列出的结果是步骤正确完成后应看到的效果。

示意图

从打开 Launcher 到进入 Unreal 编辑器的菜单路径
从打开 Launcher 到进入 Unreal 编辑器的菜单路径

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

**应看到的结果:** 敌人看见玩家时会走向玩家;玩家离开视野后,敌人会停止追击。

敌人的循环:视觉感知发现玩家,AI 控制器写入 Blackboard,行为树选择移动或射击
敌人的循环:视觉感知发现玩家,AI 控制器写入 Blackboard,行为树选择移动或射击

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。每一步都是点击菜单或按顺序粘贴代码。