
1. 项目概述与核心价值最近在啃《UE5_C多人TPS完整教程》的第60节讲的是抛射物武器。说实话这一节的内容密度相当高它不仅仅是教你如何让子弹飞出去而是把一套完整的、适用于多人联机环境的远程武器框架给搭了起来。对于想从蓝图转向C或者想深入理解UE5网络同步机制的朋友来说这绝对是个宝藏章节。很多人在做射击游戏时处理抛射物比如火箭弹、榴弹、魔法飞弹会遇到一堆坑客户端预测不准、服务器回滚难受、伤害计算时机诡异、特效不同步等等。P60这节课就是从根儿上用C构建一个稳健的ProjectileWeapon类及其配套的抛射物Projectile类系统地解决这些问题。它清晰地划分了武器逻辑装填、发射冷却和抛射物逻辑移动、碰撞、伤害应用并且严格遵循了UE5的“服务器权威”原则是构建任何复杂远程武器系统的绝佳起点。无论你是独立开发者还是团队中的程序吃透这一套设计模式都能让你在开发联机射击游戏时少走很多弯路。2. 抛射物武器系统核心设计思路拆解2.1 武器与抛射物的职责分离这是本教程设计中最精妙的一点。初学者很容易把发射逻辑和飞行物逻辑混在一起写在武器类里但这会带来严重的耦合问题不利于扩展和维护。P60采用了清晰的分离策略武器AProjectileWeapon的职责逻辑触发响应玩家的输入如鼠标点击在满足条件弹药充足、不在冷却期时请求发射。网络同步在服务器端执行权威的发射逻辑并通过RPC远程过程调用通知所有客户端进行视觉表现。资源管理管理弹药数量、装填时间、发射速率等“武器属性”。生成请求计算发射的起始位置和方向然后请求生成抛射物Actor。抛射物AProjectile的职责移动模拟负责自身的飞行轨迹可以是直线、抛物线受重力影响或更复杂的运动。碰撞检测检测与场景或其他角色的碰撞并触发相应事件。伤害应用在碰撞发生时计算并应用伤害。关键点伤害计算必须在服务器端进行以防止客户端作弊。视觉与音频表现管理飞行轨迹特效粒子、撞击特效、音效等。这部分需要在客户端进行同步播放。这种分离使得Weapon和Projectile都可以独立开发和迭代。例如你可以轻松地更换不同的抛射物类型火箭弹换成手榴弹而无需修改武器类的核心发射逻辑。2.2 网络同步架构服务器权威与客户端预测在多人游戏中网络延迟是无法避免的。P60的实现严格遵循了UE5的服务器权威模型并辅以客户端预测来提升操作手感。服务器权威Server Authority所有关键逻辑弹药消耗、是否命中、伤害计算的最终决定权都在服务器。武器类中的Fire函数其核心执行体是在服务器上运行的。客户端只是发起一个请求ServerFireRPC。抛射物的生成尤其是带有伤害逻辑的生成也由服务器执行。客户端生成的抛射物通常是“预测性”或“纯视觉”的副本。客户端预测Client-side Prediction为了不让玩家感觉到延迟当玩家按下开火键时客户端会立即本地模拟一次发射效果播放本地动画、生成一个视觉抛射物、消耗本地UI显示的弹药。这个预测是“乐观”的。如果服务器后来拒绝了这次开火比如弹药其实已耗尽服务器需要进行“校正”Reconciliation例如通过RPC通知客户端撤销这次开火的视觉效果或恢复弹药显示。对于抛射物一种常见的预测方案是客户端生成一个bReplicatesfalse的抛射物只负责飞行和视觉特效但不处理伤害。服务器生成真正的抛射物同步下来后客户端的预测抛射物可以被销毁或与之合并。RPC远程过程调用的运用ServerFire()一个ServerRPC由客户端调用仅在服务器上执行。用于传递权威的开火指令。MulticastFire()一个MulticastRPC由服务器调用在所有客户端包括服务器本身上执行。用于同步开火的视觉和听觉效果如枪口闪光、音效确保所有玩家看到的是一致的表现。2.3 抛射物运动与碰撞的考量抛射物的移动通常使用UE的移动组件如ProjectileMovementComponent。P60中需要深入理解其参数InitialSpeed和MaxSpeed控制飞行速度。bRotationFollowsVelocity让抛射物始终朝向飞行方向这对于火箭弹等模型很重要。ProjectileGravityScale重力缩放。0为无重力直线飞行1为正常重力抛物线这是实现不同弹道步枪子弹 vs 榴弹的关键。Bounciness如果启用物理模拟定义抛射物的弹性。碰撞需要精细设置碰撞预设Collision Preset通常为Projectile。碰撞事件需要生成Hit事件阻挡时和Overlap事件重叠时用于范围伤害检测。在BeginPlay中通过SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics)等函数启用。模拟生成Simulation Generates Hit Events对于由移动组件驱动的抛射物必须勾选此选项才能触发OnHit事件。3. 核心类实现细节与C代码解析3.1 AProjectileWeapon 类设计与实现AProjectileWeapon类继承自一个基础的AWeapon类教程中应已实现它增加了抛射物特有的逻辑。关键属性头文件ProjectileWeapon.hUCLASS() class YOURGAME_API AProjectileWeapon : public AWeapon { GENERATED_BODY() public: AProjectileWeapon(); protected: // 服务器端处理开火的权威函数 virtual void Fire(const FVector HitTarget) override; // 要生成的抛射物类 UPROPERTY(EditAnywhere, Category Weapon Properties) TSubclassOfclass AProjectile ProjectileClass; // 抛射物生成位置通常是一个场景组件附着在枪口骨骼上 UPROPERTY(VisibleAnywhere, Category Components) USceneComponent* ProjectileSpawnPoint; };关键函数实现源文件ProjectileWeapon.cppFire函数是核心它在服务器上执行void AProjectileWeapon::Fire(const FVector HitTarget) { // 调用父类Fire执行通用逻辑如弹药检查、冷却判断 Super::Fire(HitTarget); // 确保在服务器上且拥有Pawn APawn* InstigatorPawn CastAPawn(GetOwner()); if (!HasAuthority() || !InstigatorPawn) return; // 计算发射变换位置和旋转 // ProjectileSpawnPoint 是附着在武器模型枪口的组件 FVector SpawnLocation ProjectileSpawnPoint-GetComponentLocation(); // 计算朝向目标点的旋转。注意HitTarget是世界空间中的点。 // 一种简单方式用目标点减去生成点得到方向向量然后转化为旋转。 FRotator SpawnRotation (HitTarget - SpawnLocation).Rotation(); // 更健壮的做法可以加入一些随机散布Spread // SpawnRotation.Yaw FMath::RandRange(-SpreadAngle, SpreadAngle); // SpawnRotation.Pitch FMath::RandRange(-SpreadAngle, SpreadAngle); // 生成抛射物的参数 FActorSpawnParameters SpawnParams; SpawnParams.Owner GetOwner(); // 抛射物的Owner设为武器持有者 SpawnParams.Instigator InstigatorPawn; // Instigator是实际造成伤害的Pawn SpawnParams.SpawnCollisionHandlingOverride ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButDontSpawnIfColliding; // 在服务器上权威地生成抛射物 if (ProjectileClass) { GetWorld()-SpawnActorAProjectile( ProjectileClass, SpawnLocation, SpawnRotation, SpawnParams ); } }注意HitTarget参数通常来自玩家的瞄准系统如摄像机中心射线检测到的点。在多人游戏中这个目标点需要从客户端通过RPC传递给服务器服务器不能完全信任客户端传来的位置通常会结合服务器端的数据进行验证或重新计算但作为基础教程这里直接使用。3.2 AProjectile 类设计与实现AProjectile类负责抛射物出生后的所有行为。关键属性与组件头文件Projectile.hUCLASS() class YOURGAME_API AProjectile : public AActor { GENERATED_BODY() public: AProjectile(); virtual void Tick(float DeltaTime) override; protected: virtual void BeginPlay() override; virtual void Destroyed() override; // 用于销毁时播放特效 UFUNCTION() virtual void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult Hit); // 移动组件 UPROPERTY(VisibleAnywhere, Category Components) UProjectileMovementComponent* ProjectileMovement; // 碰撞组件如球体 UPROPERTY(VisibleAnywhere, Category Components) USphereComponent* CollisionSphere; // 静态网格体可选用于视觉表现 UPROPERTY(VisibleAnywhere, Category Components) UStaticMeshComponent* ProjectileMesh; // 特效 UPROPERTY(EditAnywhere, Category Combat) UParticleSystem* ImpactParticles; UPROPERTY(EditAnywhere, Category Combat) USoundCue* ImpactSound; // 伤害类型与数值 UPROPERTY(EditAnywhere, Category Combat) TSubclassOfUDamageType DamageType; UPROPERTY(EditAnywhere, Category Combat) float Damage 20.0f; };初始化与碰撞设置源文件Projectile.cppAProjectile::AProjectile() { PrimaryActorTick.bCanEverTick true; bReplicates true; // 关键允许复制到各个客户端 // 创建根组件和碰撞组件 CollisionSphere CreateDefaultSubobjectUSphereComponent(TEXT(CollisionSphere)); SetRootComponent(CollisionSphere); CollisionSphere-SetCollisionObjectType(ECC_WorldDynamic); CollisionSphere-SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); CollisionSphere-SetCollisionResponseToAllChannels(ECR_Ignore); CollisionSphere-SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Block); CollisionSphere-SetCollisionResponseToChannel(ECC_WorldDynamic, ECR_Block); CollisionSphere-SetCollisionResponseToChannel(ECC_Pawn, ECR_Block); // 对Pawn阻挡 CollisionSphere-SetSphereRadius(10.0f); // 创建网格体组件并附着 ProjectileMesh CreateDefaultSubobjectUStaticMeshComponent(TEXT(ProjectileMesh)); ProjectileMesh-SetupAttachment(RootComponent); ProjectileMesh-SetCollisionEnabled(ECollisionEnabled::NoCollision); // 网格体本身不参与碰撞 // 创建并配置抛射物移动组件 ProjectileMovement CreateDefaultSubobjectUProjectileMovementComponent(TEXT(ProjectileMovement)); ProjectileMovement-UpdatedComponent RootComponent; ProjectileMovement-InitialSpeed 3000.0f; ProjectileMovement-MaxSpeed 3000.0f; ProjectileMovement-ProjectileGravityScale 0.0f; // 无重力直线飞行 ProjectileMovement-bRotationFollowsVelocity true; ProjectileMovement-bShouldBounce false; } void AProjectile::BeginPlay() { Super::BeginPlay(); // 绑定碰撞事件 if (CollisionSphere HasAuthority()) // 通常只在服务器端绑定伤害碰撞 { CollisionSphere-OnComponentHit.AddDynamic(this, AProjectile::OnHit); } }碰撞处理与伤害应用void AProjectile::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult Hit) { // 1. 获取抛射物的Owner即开火的玩家控制器或Pawn AController* InstigatorController nullptr; if (GetOwner()) { APawn* OwnerPawn CastAPawn(GetOwner()); if (OwnerPawn) { InstigatorController OwnerPawn-GetController(); } } // 2. 应用伤害仅在服务器执行 if (OtherActor OtherActor ! this OtherActor ! GetOwner() HasAuthority()) { // 使用UE内置的伤害系统 UGameplayStatics::ApplyDamage( OtherActor, // 被伤害的目标 Damage, // 伤害值 InstigatorController, // 伤害来源的控制器 this, // 伤害来源抛射物自身 DamageType // 伤害类型 ); } // 3. 播放本地特效所有机器都播放但服务器需要复制给客户端 if (ImpactParticles) { UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ImpactParticles, GetActorLocation(), GetActorRotation()); } if (ImpactSound) { UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation()); } // 4. 销毁抛射物 Destroy(); } void AProjectile::Destroyed() { // 在Actor被销毁前也可以在这里播放特效作为备份 // 这对于因生命周期结束而非碰撞销毁的情况很有用 if (ImpactParticles) { UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ImpactParticles, GetActorLocation(), GetActorRotation()); } Super::Destroyed(); }4. 网络同步与RPC的实战部署理论讲完了我们来看如何将上述C类与网络同步结合起来。关键在于武器类的开火流程。在AProjectileWeapon或其父类AWeapon中通常会有这样的调用链客户端输入玩家按下鼠标触发InputAction绑定的事件。客户端本地调用在玩家控制的Pawn中调用武器接口的StartFire或直接调用Fire。但注意武器的关键逻辑如弹药检查应在服务器进行。发起服务器RPC在武器或持有武器的角色中定义一个ServerFireRPC。// 在角色或武器类中 UFUNCTION(Server, Reliable) void ServerFire(const FVector_NetQuantize TraceHitTarget);客户端在确认输入后调用ServerFire(TraceHitTarget)将瞄准点经过压缩的向量发送给服务器。服务器执行权威逻辑服务器收到ServerFire后执行我们上面写的AProjectileWeapon::Fire函数。这里会进行真正的弹药消耗判断并生成权威的抛射物Actor。由于抛射物bReplicatestrue它会被自动同步到所有客户端。多播视觉反馈在服务器的Fire函数中生成抛射物后可以调用一个MulticastFireRPC。UFUNCTION(NetMulticast, Unreliable) // 视觉反馈可以用不可靠传输 void MulticastFire();这个RPC在所有客户端执行用于播放枪口特效、后坐力动画、音效等。注意抛射物本身的生成和移动已经通过Actor复制同步了所以MulticastFire通常只用于武器本体的附加效果。抛射物的网络角色抛射物在服务器上生成设置为bReplicates true。它的移动由ProjectileMovementComponent驱动该组件默认支持网络同步bReplicatestrue时其位置、旋转、速度等属性会被复制。碰撞检测和伤害应用只在服务器端进行通过HasAuthority()判断。视觉和音效在OnHit或Destroyed中通过UGameplayStatics函数播放这些函数会在所有机器上本地执行如果是从服务器RPC调用的路径触发或者通过多播RPC触发。5. 常见问题、调试技巧与性能优化5.1 抛射物看不到或不同步检查复制设置确保AProjectile的bReplicates属性在构造函数中设为true。在编辑器中选中生成的抛射物蓝图查看“复制”选项是否勾选。检查移动组件确保ProjectileMovementComponent的UpdatedComponent正确设置为碰撞组件。网络角色在游戏运行时使用showdebug命令如showdebug net查看Actor的网络状态。抛射物在客户端应为“模拟代理Simulated Proxy”。生成位置确认ProjectileSpawnPoint组件的位置正确附着在武器骨骼上并且在世界空间中的位置计算正确。可以在Fire函数中添加调试绘制DrawDebugSphere来可视化生成点。5.2 伤害无法应用或应用多次权限检查务必在OnHit函数中检查HasAuthority()确保伤害逻辑只在服务器执行。Owner 和 Instigator正确设置抛射物的Owner和Instigator。ApplyDamage函数需要InstigatorController来正确归属伤害来源。通常Owner是武器持有者PawnInstigator也是该Pawn。碰撞过滤检查抛射物的碰撞预设Collision Preset和响应通道。确保它能阻挡Block或重叠Overlap你想要伤害的对象类型如Pawn。同时也要检查被击中对象的碰撞设置是否接收来自抛射物的碰撞事件。重复触发OnHit事件可能在单次碰撞中被多次调用例如与复杂网格体碰撞。可以通过设置一个bHitAlreadyProcessed布尔变量来防止重复处理伤害和特效。5.3 客户端预测与校正预测抛射物为了极致的手感可以在客户端Fire时也本地生成一个bReplicatesfalse的预测抛射物。当服务器的权威抛射物同步下来后将预测抛射物销毁或将其位置/旋转与权威抛射物对齐。这需要更复杂的逻辑来管理预测ID和服务器校正。特效同步撞击特效和音效最好在OnHit中使用MulticastRPC 触发而不是依赖Destroyed。因为Destroyed在客户端被调用的时机可能因网络延迟而滞后导致特效播放位置不准。可以在服务器OnHit中调用Multicast_PlayImpactEffects(HitLocation, HitNormal)。5.4 性能优化对象池Object Pooling对于高频发射的抛射物如机枪子弹频繁的SpawnActor和Destroy会产生垃圾回收GC开销。可以实现一个抛射物对象池复用已销毁的抛射物对象。网络更新频率对于高速移动的抛射物默认的网络更新速率可能不够导致运动不平滑。可以在抛射物上调整NetUpdateFrequency和MinNetUpdateFrequency属性或者考虑使用ProjectileMovementComponent的bInterpMovement和bInterpRotation进行插值。简化复制如果抛射物没有复杂的动态变化可以考虑只复制初始位置、速度和生命期由客户端完全根据物理公式模拟其轨迹确定性模拟这能极大减少网络流量。但这要求所有客户端的模拟结果必须与服务器完全一致实现难度较高。5.5 扩展功能思路抛物线弹道调整ProjectileMovementComponent-ProjectileGravityScale大于0并合理设置初始速度和发射角度。制导导弹在抛射物的Tick函数中每帧根据目标位置重新计算ProjectileMovementComponent-Velocity的方向。范围伤害Area Damage在OnHit中使用UGameplayStatics::ApplyRadialDamage代替ApplyDamage。需要传入中心点、伤害半径、伤害值等参数。抛射物穿透在碰撞检测中不立即销毁抛射物而是减少其伤害或穿透次数并继续飞行。抛射物事件可以定义OnProjectileSpawned,OnProjectileHit,OnProjectileDestroyed等多播委托Multicast Delegates让武器或其他系统能够订阅这些事件实现更解耦的逻辑如武器根据命中播放不同音效。