《UE5_C++多人TPS完整教程》学习笔记59 ——《P60 投射物武器(Projectile Weapons)》
本文为B站系列教学视频 《UE5_C++多人TPS完整教程》 —— 《P60 投射物武器(Projectile Weapons)》 的学习笔记,该系列教学视频为计算机工程师、程序员、游戏开发者、作家(Engineer, Programmer, Game Developer, Author) Stephen Ulibarri 发布在 Udemy 上的课程 《Unreal Engine 5 C++ Multiplayer Shooter》 的中文字幕翻译版,UP主(也是译者)为 游戏引擎能吃么。
文章目录
- P60 投射物武器(Projectile Weapons)
- 60.1 创建投射物武器类
- 60.2 Summary
P60 投射物武器(Projectile Weapons)
本节课我们将介绍弹道武器,并创建我们自己的弹道武器类;除此之外,我们还将创建一个投射物(弹体)类,它的实例将由弹道武器类的实例生成。
60.1 创建投射物武器类
-
在枪战游戏中,武器根据命中判定机制(Hit Detection)可分为两种:
- 投射物武器(Projectile weapons):这类武器发射时会生成实际的弹体,弹体拥有自己的飞行速度,弹体在空中飞行一段时间后,才能到达目标位置从而命中目标。当我们希望弹体能够进行抛射并最终落向地面时,弹体需要具有重力这一属性;当我们希望弹体飞行时间尽可能长,以直线的弹道射向目标时,弹体需要忽略或没有重力这一属性,例如火箭筒(Rocket launcher)的弹体。这类武器通过弹体的弹道判断是否命中物体,若击中物体则会产生命中事件,我们根据命中事件确定弹体产生爆炸粒子和命中物体的位置,由此可以对命中物体施加伤害事件。通常为了展示弹体飞行的效果,我们使用追踪粒子系统(Tracer particles)标记弹道(Leave a trail)。
- 即时命中武器(Hitscan weapons):这类武器通常不会生成实际的弹体,无需考虑弹体下坠等物理延迟(指哪打哪立刻就到哪,Instant hit),它通过射线追踪(Line trace)即时判定是否命中目标,出于这个原因,我们通常使用光束粒子(Beam particles)系统来显示命中轨迹。
总的来说,上述两种武器都是实施枪战的合法方式(Legitimate ways to implement gunfire),在游戏中根据实际情况和需求进行运用,例如在近距离(Close ranges)交火时,我们可能需要快速开火(Rapid firing),这时候就需要即时命中武器;而在远距离交火(fire over long ranges)时,可能需要投射物武器,但在一些情况下,即时命中武器中的狙击步枪也可以进行远距离打击。
命中判定机制(Hit Detection)是FPS游戏中最核心的技术之一,直接影响射击手感、公平性和反作弊能力。下面我将详细讲解命中判定机制的原理、分类、延迟补偿、典型实现、代码示例和实际开发建议,适用于C/S强同步架构。
一、命中判定的基本分类
- Hitscan(即时射线判定)
代表武器:步枪、手枪、狙击枪等
原理:开火瞬间从枪口/摄像机发出一条射线,检测是否与目标碰撞
特点:无弹道延迟,命中即反馈 - Projectile(弹体/弹道判定)
代表武器:火箭筒、榴弹、投掷物等
原理:发射一个有速度和轨迹的物体,实时模拟其运动,碰撞时判定命中
特点:有飞行时间和抛物线,需考虑重力、风等因素
二、命中判定的关键环节
- 客户端表现
客户端本地先做一次命中检测,立即给玩家反馈(如击中音效、粒子、动画),提升手感。
但最终命中结果以服务器为准,客户端结果仅作表现。 - 服务器权威判定
服务器收到射击请求后,基于权威状态(包括延迟补偿)重新判定是否命中。
服务器判定结果才会影响伤害、击杀等核心逻辑。 - 双重校验与反作弊
客户端命中结果仅作表现,服务器独立判定,防止自瞄、穿墙等作弊。
服务器可采用简化碰撞体(如胶囊体)做二次校验,防止模型漏洞。
三、延迟补偿(Lag Compensation)
- 问题
网络延迟下,玩家A在本地看到的B的位置,和服务器上B的实际位置可能不同。
如果直接用服务器当前状态判定,玩家会觉得“明明打中了却没判中”。 - 解决方案
服务器保存最近几秒的所有玩家状态快照(如每帧保存5秒内的状态)。
当收到射击请求时,根据客户端射击时间戳,回滚所有目标玩家到当时的位置,再做命中判定。
这样,命中判定基于“射手看到的世界”,保证公平。
四、典型实现流程 - Hitscan命中判定
- 客户端
- 开火时本地做一次射线检测,立即表现击中效果。
- 发送射击请求(含射击时间戳、射线起点、方向等)到服务器。
- 服务器
- 根据射击时间戳,回滚目标玩家到当时的位置。
- 用权威数据做射线检测,判定是否命中。
- 结果同步给所有客户端。
- Projectile命中判定
- 客户端
- 本地生成弹体,做弹道模拟,表现飞行和命中。
- 发送发射请求(含时间戳、初速度、方向等)到服务器。
- 服务器
- 生成权威弹体,按物理规则模拟飞行。
- 每帧检测弹体与目标的碰撞,判定命中。
- 结果同步给所有客户端。
—— CSDN 《命中判定机制(Hit Detection)》
-
在虚幻引擎中 “创建原生自 Weapon 的 C++ 类”(Create C++ class derived from Weapon)“
ProjectileWeapon
”(投射物武器 C++ 类) ,创建 Actor C++ 类 “Projectile
”(投射物弹体 C++ 类)。接着,在 Visual Studio 中打开 “Projectile.h
”,为投射物弹体声明盒体碰撞组件 “CollisionBox
”,然后在 “Projectile.cpp
” 的构造函数 “AProjectile()
” 中基于盒体碰撞组件创建对象,将其设置为根组件,然后设置碰撞。
/*** ProjectileWeapon.h ***/// Fill out your copyright notice in the Description page of Project Settings.#pragma once#include "CoreMinimal.h"/* P60 投射物武器(Projectile Weapons)*/ #include "Weapon.h" // 原来自动生成的代码是 #include "Weapon/Weapon.h",这里需要把 "Weapon/" 去掉,否则找不到文件 "Weapon.h" /* P60 投射物武器(Projectile Weapons)*/#include "ProjectileWeapon.generated.h"/** * */ UCLASS() class BLASTER_API AProjectileWeapon : public AWeapon {GENERATED_BODY()};
/*** ProjectileWeapon.cpp ***/// Fill out your copyright notice in the Description page of Project Settings./* P60 投射物武器(Projectile Weapons)*/ #include "ProjectileWeapon.h" // 原来自动生成的代码是 #include "Weapon/ProjectileWeapon.h",这里需要把 "Weapon/" 去掉,否则找不到文件 "ProjectileWeapon.h" /* P60 投射物武器(Projectile Weapons)*/
-
创建 Actor C++ 类 “
Projectile
”(投射物弹体 C++ 类),在 “Projectile.h
”,为投射物弹体声明盒体碰撞组件 “CollisionBox
”;接着,在 “Projectile.cpp
” 的构造函数 “AProjectile()
” 中基于盒体碰撞组件创建对象,将其设置为根组件;随后,设置其碰撞类型为动态物体 “WorldDynamic
”,物理碰撞启用类型为 “QueryAndPhysics
”;在设置与碰撞通道发生碰撞时的响应方式时,先暂时关闭所有通道的碰撞检测和碰撞响应,然后再开启与可视物体以及静态物体发生碰撞的响应为阻挡。// Fill out your copyright notice in the Description page of Project Settings.#pragma once#include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "Projectile.generated.h"UCLASS() class BLASTER_API AProjectile : public AActor {GENERATED_BODY()public: // Sets default values for this actor's propertiesAProjectile();protected:// Called when the game starts or when spawnedvirtual void BeginPlay() override;public: // Called every framevirtual void Tick(float DeltaTime) override;/* P60 投射物武器(Projectile Weapons)*/ private:UPROPERTY(EditAnywhere)class UBoxComponent* CollisionBox; /* P60 投射物武器(Projectile Weapons)*/};
/*** Projectile.cpp ***/ // Fill out your copyright notice in the Description page of Project Settings./* P60 投射物武器(Projectile Weapons)*/ #include "Projectile.h" // 原来自动生成的代码是 #include "Weapon/Projectile.h",这里需要把 "Weapon/" 去掉,否则找不到文件 "Projectile.h" #include "Components/BoxComponent.h" /* P60 投射物武器(Projectile Weapons)*/// Sets default values AProjectile::AProjectile() {// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.PrimaryActorTick.bCanEverTick = true;/* P60 投射物武器(Projectile Weapons)*/CollisionBox = CreateDefaultSubobject<UBoxComponent>(TEXT("CollisionBox")); // 基于盒体组件类创建对象SetRootComponent(CollisionBox); // 设置根组件为 CollisionBox// 指定碰撞检测的对象类型CollisionBox->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic); // 设定自己的碰撞类型为 WorldDynamic,用于动态物体的碰撞,通常是玩家角色、移动的物体等。CollisionBox->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); // 设定自己的物理碰撞启用类型为 QueryAndPhysics,可以用于空间查询(射线投射、扫描、覆盖)和模拟(刚体、约束)。// 设置与碰撞通道发生碰撞时的响应方式CollisionBox->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore); // 先暂时关闭所有通道的碰撞检测和碰撞响应,然后再开启需要的通道CollisionBox->SetCollisionResponseToChannel(ECollisionChannel::ECC_Visibility, ECollisionResponse::ECR_Block); // 与可视物体发生碰撞的响应为阻挡,在碰撞时会停止移动或反弹。CollisionBox->SetCollisionResponseToChannel(ECollisionChannel::ECC_WorldStatic, ECollisionResponse::ECR_Block); // 与静态物体发生碰撞的响应为阻挡,在碰撞时会停止移动或反弹。/* P60 投射物武器(Projectile Weapons)*/ }// Called when the game starts or when spawned void AProjectile::BeginPlay() {Super::BeginPlay();}// Called every frame void AProjectile::Tick(float DeltaTime) {Super::Tick(DeltaTime);}
-
ECC (Collision Channel)
ECC 是 Collision Channel 的缩写,表示碰撞通道。它用于定义不同类型的碰撞对象,以便在碰撞检测中进行分类。每个碰撞对象都有一个对应的碰撞通道,在运行时可以根据这个通道来检测和响应碰撞。
在 Unreal Engine 中,ECC 定义了碰撞的种类,常见的碰撞通道包括:ECC_Visibility
: 用于可见物体的碰撞,通常是摄像机视野中的物体。ECC_WorldStatic
: 用于静态物体的碰撞,例如建筑、地面等。ECC_WorldDynamic
: 用于动态物体的碰撞,通常是玩家角色、移动的物体等。ECC_Pawn
: 用于与玩家角色(Pawn)相关的碰撞。ECC_PhysicsBody
: 用于物理物体的碰撞。ECC_Vehicle
: 用于与车辆相关的碰撞。ECC_Destructible
: 用于可破坏物体的碰撞。ECC_GameTraceChannel1
到ECC_GameTraceChannel8
: 自定义的碰撞通道,开发者可以根据需要定义和使用。
-
ECR (Collision Response)
ECR 是 Collision Response 的缩写,表示碰撞响应类型。它定义了物体在发生碰撞时应该如何响应。在 Unreal Engine 中,碰撞响应有多个类型,可以设置为以下几种:ECR_Ignore
: 忽略碰撞,不做任何响应。ECR_Overlap
: 发生重叠但不产生物理反应,即物体不会被推开。通常用于触发事件或碰撞检测。ECR_Block
: 发生阻挡,物体在碰撞时会停止移动或反弹。ECR_Custom
: 自定义响应,允许开发者实现特定的碰撞处理逻辑。
—— CSDN 《ECC和ECR》
物理碰撞启用类型
NoCollision
:不会在物理引擎中创建任何表示。不能用于空间查询(射线投射、扫描、覆盖)或模拟(刚体、约束)。这将提供最佳的性能(尤其是对于移动的对象)。
QueryOnly
:仅用于空间查询(射线投射、扫描、覆盖)。不能用于模拟(刚体、约束)。这对于角色移动和不需要物理模拟的事物非常有用。通过将数据保持在模拟树之外,可以提高性能。
PhysicsOnly
:仅用于物理模拟(刚体、约束)。不能用于空间查询(射线投射、扫描、覆盖)。这对于角色上不需要每个骨骼检测的摇摆部分非常有用。通过将数据保持在查询树之外,可以提高性能。
QueryAndPhysics
:可以用于空间查询(射线投射、扫描、覆盖)和模拟(刚体、约束)。
—— 知乎《UE物理检测中的类型》
-
60.2 Summary
本节课我们开始设计投射物武器,为游戏添加更丰富的武器类型。我们了解到,根据命中判定机制,枪战游戏主要分为两种武器,第一种是投射物武器,它发射实际弹体,具有飞行时间和弹道特性,常用追踪粒子标记其飞行轨迹;第二种是即使命中武器,它不发射实际的弹体,通过射线追踪即时判定命中,常用光束粒子显示其命中轨迹。于是,我们在虚幻引擎中创建了继承自现有的 “Weapon
” C++ 类的 “ProjectileWeapon
”,它将作为投射物武器的基类,后续将实现发射弹体的功能;同时,还创建了投射物武器的弹体类 “Projectile
” 类,它继承自 Actor C++ 类,我们为它配置了盒体碰撞组件(CollisionBox)作为根组件,设置它的碰撞类型为WorldDynamic(动态物体),并启用 “QueryAndPhysics
” 碰撞检测,在关闭它对所有通道的碰撞检测后,仅对可见物体 “Visibility
” 和静态物体 “WorldStatic
” 通道启用阻挡响应。