资讯动态

UE5 C++多人FPS实战:网络同步与RPC核心机制详解

发布时间:2026/10/17 7:47:06 来源:尧图企业网站定制
做第一人称射击游戏时很多人会卡在同一个地方单机角色跑通以后一接入多人网络角色开始瞬移、开火不同步、伤害总是慢半拍。我之前用 UE5 系列做 C FPS 原型时也被网络复制和 RPC 反复折磨过。网上教程偏蓝图的居多C 工程又往往只给零散片段很难组装成一个能联机跑起来的整体。本文围绕 Unreal Engine 5.8 的 C 工程完整拆解一套第一人称多人射击游戏的搭建方案覆盖项目创建、角色移动、第一人称摄像机、命中检测、服务器权威验证、多人联机测试等关键环节并提供可直接复制的代码。标题里的“中配”可以理解为中文学习资源本文逻辑完全独立按自己的工程实现来写。如果你有 C 基础想系统理解 UE 网络同步和 FPS 核心机制这篇文章比较适合。如果你完全没接触过 UE建议先把编辑器基础操作过一遍再回来读效果会更好。1. 背景与核心概念1.1 第一人称多人射击游戏到底是什么第一人称射击游戏First-Person Shooter简称 FPS的核心玩法是玩家通过摄像机Camera观察世界使用输入设备控制角色移动和准星点击鼠标开火命中目标。它的核心体验来自“第一人称视角 射击手感 多人对抗”。从技术角度看一个 FPS 至少包含以下几块内容角色控制器处理移动、跳跃、观察、下蹲等输入。摄像机系统决定玩家看到什么射击射线从摄像机和准星方向发出。武器与开火射线检测Hitscan或投射物Projectile二选一。网络同步角色的坐标、旋转、动画、血量、开火事件在多台机器上保持一致。游戏规则GameMode 负责生成角色、判定胜负、管理比赛状态。单机 FPS 只需要关心前面四块中的前三块但一旦加入网络事情立刻变复杂。UE 解决这个问题的思路是“服务器权威”也就是让所有关键逻辑优先在服务器上执行再通过复制机制同步到客户端。1.2 UE5 多人联机的核心思路UE 的网络架构有几个基础概念必须提前说清楚。第一个是 Actor 复制。一个 Actor 如果开启了bReplicates true它就会在服务器和各客户端之间同步。默认情况下Actor 在服务器上创建然后复制到所有客户端。角色属于 Pawn由 PlayerController 控制每个客户端只控制自己那个 PlayerController 对应的 Pawn。第二个是所有权Owning Connection。每个客户端都有自己控制的 Pawn这个 Pawn 叫“本地控制的 Pawn”或 Autonomous Proxy。其他客户端看到的同一个角色则是 Simulated Proxy。听懂这两个词以后代码里判断“这是不是本地玩家”就很简单了。第三个是 RPCRemote Procedure Call。RPC 有三种类型Server RPC客户端调用在服务器上执行。Client RPC服务器调用在特定客户端上执行。Multicast RPC服务器调用在所有客户端和服务器本机执行。FPS 开火场景特别适合用 Server RPC客户端点击鼠标把射线起点和射线终点发给服务器服务器重新做一次命中检测再通过 Multicast RPC 把开火表现广播给所有客户端。第四个是属性复制。基础属性、血量、弹药等变量可以标记为Replicated当服务器上该变量变化时UE 会主动把新值同步给客户端。1.3 为什么选择 C 而不是纯蓝图UE5 允许蓝图和 C 混合开发。对于小型原型纯蓝图足够但第一人称多人射击游戏涉及大量网络逻辑、数据结构和性能敏感代码纯蓝图存在几个明显问题蓝图节点在复杂函数图中可读性差多人同步逻辑多了以后很难维护。射击命中检测、伤害计算、坐标预测等逻辑用 C 写更直接。多人项目需要频繁处理对象引用、数组、委托C 更可控。服务器部署和性能优化通常也依赖 C。本文的示例以 C 为主只在使用动画、音效等表现层时考虑蓝图。2. 环境准备与项目创建2.1 工具版本说明本文以 Unreal Engine 5.8 为例演示但核心配置思路在 UE5 整个系列都通用。不同小版本之间菜单名称和部分 API 可能有差异你需要按自己的实际版本调整。基础环境如下工具说明操作系统Windows 10 / 11 64 位开发引擎Unreal Engine 5.8编译器Visual Studio 2022勾选“使用 C 的游戏开发”构建工具UE 自带的 UnrealBuildTool显卡驱动建议更新到较新版本避免渲染异常UE5 对硬件有一定要求尤其是编译 C 工程时内存建议 16GB 以上。开发期间不需要渲染超高画质可以把全局光照和质量等级调低把编译和调试时间缩短。2.2 创建 C 工程打开 UE Editor在项目创建界面选择“游戏”分类中的“第三人称”模板。虽然我们要做第一人称但第三人称模板自带角色骨骼网格、动画蓝图和基础输入直接改会造成较少的工作量。关键选项注意项目名称建议使用英文例如FPSShooter。项目类型选择“C”。目标平台选择“桌面”。初学者内容包可以选上方便测试模型和贴图。项目创建完成后UE 会自动生成一个 C 工程包含FPSShooter模块的.uproject文件和Source目录。首次打开会提示编译等待完成即可。2.3 配置输入映射本文示例使用传统输入绑定Input Axis Mapping便于直接对应 C 代码。打开项目设置 - 输入 - Bindings添加如下“轴映射”轴名按键MoveForwardW 正方向S 负方向MoveRightA 负方向D 正方向Turn鼠标 X 轴LookUp鼠标 Y 轴Fire鼠标左键UE5.8 默认推荐使用增强输入Enhanced Input但传统 Input Component 仍然可用。本教程为了集中讲解网络和射击逻辑使用传统绑定后续章节会提到如何迁移到增强输入。3. 核心机制拆解3.1 Character、Pawn 和 PlayerController 的关系FPS 中角色不建议直接使用 Actor而是使用ACharacter或APawn。ACharacter 内置了UCharacterMovementComponent可以处理行走、跳跃、下蹲和网络平滑移动省去大量底层工作。三者关系可以这样理解PlayerController 是玩家和游戏世界的“控制接口”接收输入并控制 Pawn。Pawn 是可被控制 actor 的基类。Character 是带移动组件的 Pawn适合人形生物。在服务器端PlayerController 与玩家连接绑定。服务端生成一个 Character 后通过Possess让 PlayerController 控制它。客户端看到自己的角色是本地控制的其他玩家角色只是通过网络同步更新位置。3.2 第一人称摄像机的正确姿势第一人称射击游戏的摄像机不是挂在角色骨骼上而是直接作为角色组件位置放在头部附近。正确做法是在构造函数里创建一个UCameraComponent。把它挂接到GetCapsuleComponent()胶囊体上。设置相对位置例如(0, 0, 64)也就是眼睛高度。把bUsePawnControlRotation设置为true让摄像机跟随控制器的 Yaw 和 Pitch 旋转。如果你的角色直接使用 ACharacter 自带的网格体并且开启bUsePawnControlRotation会导致角色网格体也跟随摄像机旋转看起来像“人体模型扭来扭去”。所以还需要做一层隐藏处理自己的第一人称视图中应该看不到自己的完整身体只看到手臂和武器其他客户端看到完整身体。通常做法是给GetMesh()设置SetOwnerNoSee(true)自己看自己时隐藏身体另加一个只属于本人可见的手臂组件。手臂组件代码不复杂本文为了聚焦网络部分先只做摄像机隐藏手臂与武器模型可以在后续接入。3.3 属性复制与条件复制属性复制是 UE 网络同步的根本。在类声明中把需要同步的变量加上UPROPERTY(Replicated)然后在GetLifetimeReplicatedProps中注册即可。例如UPROPERTY(Replicated) float Ammo;注册写法void AFPSCharacter::GetLifetimeReplicatedProps(TArrayFLifetimeProperty OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(AFPSCharacter, Ammo); }不是所有变量都需要复制。控制器本地持有、只在本地使用的变量不要标记复制这样可以减少网络流量。对于血量、弹药、分数这类需要在所有客户端看到一致结果的变量才需要复制。3.4 RPC 在射击中的应用射击逻辑是典型的需要客户端请求服务器验证的场景。流程如下客户端按下鼠标输入函数被调用。客户端从摄像机位置计算射线起点和终点。客户端调用Server RPC把射线起点和终点传给服务器。服务器重新执行射线检测判断是否命中。如果命中服务器计算伤害并应用。服务器调用Multicast RPC广播开火表现让所有客户端播放音效、粒子、弹孔。客户端表现层收到后再展示命中特效。这种方案比较容易理解但不是延迟最优解。真实商业 FPS 会做“客户端预测 服务器回滚”即先本地判断命中并播放效果等服务器确认后再校正。入门阶段先掌握“服务器验证”这个安全模型即可。3.5 命中检测射线还是投射物第一人称射击游戏中“打枪”有两种实现方式。第一种是射线检测Hitscan。从摄像机位置沿视线方向打出射线立刻判断是否命中某个碰撞体。这种方式适合步枪、狙击枪等立即命中目标。延迟低性能好但视觉效果上没有“飞行中的子弹”。第二种是投射物Projectile。生成一个子弹 actor让它沿初始方向飞行在飞行过程中持续检测碰撞。适合火箭筒、手雷、投掷物等。表现力强但需要处理投射物的网络同步容易出现轨迹不一致。本文实战先实现射线检测因为网络同步逻辑更直观。投射物可以在你已经理解射线方案之后再去改造。4. 完整实战搭建一个可联机的 FPS4.1 创建项目结构在Source/FPSShooter目录下我们创建两个核心 C 类Source/FPSShooter/ |-- FPSCharacter.h |-- FPSCharacter.cpp |-- FPSGameMode.h |-- FPSGameMode.cpp在 UE 编辑器中右键点击Source/FPSShooter选择“新建 C 类”父类选择Character类名填写FPSCharacter生成后得到头文件和源文件。再用同样的方式创建GameModeBase子类类名填写FPSGameMode。4.2 编写 FPSGameModeGameMode 只在服务器上运行。它决定玩家进入游戏后生成什么 Pawn。在 FPSGameMode.h 中声明构造函数// 文件路径Source/FPSShooter/FPSGameMode.h #pragma once #include CoreMinimal.h #include GameFramework/GameModeBase.h #include FPSGameMode.generated.h UCLASS() class FPSSHOOTER_API AFPSGameMode : public AGameModeBase { GENERATED_BODY() public: AFPSGameMode(); };FPSGameMode.cpp 中把默认 Pawn 指定为我们的角色类// 文件路径Source/FPSShooter/FPSGameMode.cpp #include FPSGameMode.h #include FPSCharacter.h AFPSGameMode::AFPSGameMode() { DefaultPawnClass AFPSCharacter::StaticClass(); }这里有个知识点如果不设置 DefaultPawnClass玩家加入后默认生成的是空 Pawn没有移动和摄像机自然无法进行第一人称操作。接下来打开世界设置World Settings把 GameMode Override 设置为FPSGameMode或者直接修改项目设置中的默认 GameMode。编译后确保进入地图时生成的是 FPSCharacter。4.3 编写 FPSCharacter 头文件FPSCharacter 是本项目的核心类。它负责移动、视角、射击、网络同步和伤害处理。完整头文件如下// 文件路径Source/FPSShooter/FPSCharacter.h #pragma once #include CoreMinimal.h #include GameFramework/Character.h #include FPSCharacter.generated.h UCLASS() class FPSSHOOTER_API AFPSCharacter : public ACharacter { GENERATED_BODY() public: AFPSCharacter(); protected: // 第一人称摄像机 UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category Camera) class UCameraComponent* FirstPersonCamera; // 血量 UPROPERTY(ReplicatedUsing OnRep_Health, BlueprintReadOnly, Category Combat) float Health; // 弹药 UPROPERTY(Replicated, BlueprintReadOnly, Category Combat) int32 Ammo; // 最大射击距离 UPROPERTY(EditDefaultsOnly, Category Combat) float FireRange 10000.f; UFUNCTION() void OnRep_Health(); // 输入处理 void MoveForward(float Value); void MoveRight(float Value); void Turn(float Value); void LookUp(float Value); void StartFire(); // 根据本地控制者计算射线起点和终点 void GetCameraFireLine(FVector OutStart, FVector OutEnd) const; // 客户端调用服务器执行真实命中检测 UFUNCTION(Server, Reliable) void ServerFire(FVector_NetQuantize100 Start, FVector_NetQuantize100 End); // 服务器调用广播开火效果给所有客户端 UFUNCTION(NetMulticast, Reliable) void MulticastShowHit(FVector_NetQuantize100 TraceStart, FVector_NetQuantize100 TraceEnd, bool bHit, FVector_NetQuantize100 ImpactPoint); public: virtual void GetLifetimeReplicatedProps(TArrayFLifetimeProperty OutLifetimeProps) const override; virtual float TakeDamage(float DamageAmount, struct FDamageEvent const DamageEvent, AController* EventInstigator, AActor* DamageCauser) override; };这里使用了ReplicatedUsing OnRep_Health血量变化后客户端会调用OnRep_Health适合在 UI 上刷新血条。Ammo 使用普通复制客户端可以读取但不需要实时唤醒回调。4.4 编写 FPSCharacter.cpp构造函数与组件构造函数中创建摄像机并启用角色复制。// 文件路径Source/FPSShooter/FPSCharacter.cpp #include FPSCharacter.h #include Camera/CameraComponent.h #include Components/CapsuleComponent.h #include Components/SkeletalMeshComponent.h #include GameFramework/CharacterMovementComponent.h #include GameFramework/Controller.h #include Engine/World.h #include Engine/DamageEvents.h #include Kismet/GameplayStatics.h #include Net/UnrealNetwork.h #include DrawDebugHelpers.h AFPSCharacter::AFPSCharacter() { PrimaryActorTick.bCanEverTick true; // 创建第一人称摄像机挂在胶囊体上 FirstPersonCamera CreateDefaultSubobjectUCameraComponent(TEXT(FirstPersonCamera)); FirstPersonCamera-SetupAttachment(GetCapsuleComponent()); FirstPersonCamera-SetRelativeLocation(FVector(0.f, 0.f, 64.f)); FirstPersonCamera-bUsePawnControlRotation true; // 关键网络配置角色需要复制 bReplicates true; bAlwaysRelevant true; // 自己看不见自己的完整身体避免遮挡第一人称视角 GetMesh()-SetOwnerNoSee(true); // 初始属性 Health 100.f; Ammo 30; // 基础角色移动参数 GetCharacterMovement()-MaxWalkSpeed 600.f; GetCharacterMovement()-JumpZVelocity 420.f; }构造函数里注意bReplicates true没有这一行角色多人在线时会变成“本地角色不复制其他客户端看不到”。GetMesh()-SetOwnerNoSee(true)是简化方案真正的 FPS 项目通常会加一个单独可见的手臂网格体。4.5 编写 FPSCharacter.cpp移动与视角移动逻辑写在输入绑定函数里。void AFPSCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) { Super::SetupPlayerInputComponent(PlayerInputComponent); PlayerInputComponent-BindAxis(MoveForward, this, AFPSCharacter::MoveForward); PlayerInputComponent-BindAxis(MoveRight, this, AFPSCharacter::MoveRight); PlayerInputComponent-BindAxis(Turn, this, AFPSCharacter::Turn); PlayerInputComponent-BindAxis(LookUp, this, AFPSCharacter::LookUp); PlayerInputComponent-BindAction(Fire, IE_Pressed, this, AFPSCharacter::StartFire); } void AFPSCharacter::MoveForward(float Value) { if ((Controller ! nullptr) (Value ! 0.0f)) { FRotator YawRotation(0.f, Controller-GetControlRotation().Yaw, 0.f); FVector Direction FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X); AddMovementInput(Direction, Value); } } void AFPSCharacter::MoveRight(float Value) { if ((Controller ! nullptr) (Value ! 0.0f)) { FRotator YawRotation(0.f, Controller-GetControlRotation().Yaw, 0.f); FVector Direction FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y); AddMovementInput(Direction, Value); } } void AFPSCharacter::Turn(float Value) { AddControllerYawInput(Value); } void AFPSCharacter::LookUp(float Value) { AddControllerPitchInput(Value); }MoveForward和MoveRight获取控制器当前朝向中的 Yaw 旋转目的是让角色始终沿摄像机朝向的方向移动而不是沿世界坐标轴移动。这样玩家按 W 时角色会往准星对着的方向前进符合 FPS 操作直觉。Turn和LookUp直接使用引擎自带的输入累加函数驱动控制器旋转再通过bUsePawnControlRotation带动摄像机旋转。4.6 编写 FPSCharacter.cpp射击与命中射击是多人 FPS 最需要谨慎的部分。先写一个本地函数获取摄像机射线起点和终点。void AFPSCharacter::GetCameraFireLine(FVector OutStart, FVector OutEnd) const { if (!FirstPersonCamera) { OutStart GetActorLocation(); OutEnd GetActorLocation() GetActorForwardVector() * FireRange; return; } OutStart FirstPersonCamera-GetComponentLocation(); FVector Forward FirstPersonCamera-GetForwardVector(); OutEnd OutStart Forward * FireRange; }开火函数void AFPSCharacter::StartFire() { if (!IsLocallyControlled()) { return; } if (Ammo 0) { // 没有子弹时可以播放空枪音效本文略过 return; } FVector Start; FVector End; GetCameraFireLine(Start, End); // 减少弹药只改服务器变量客户端依靠复制同步 if (GetLocalRole() ROLE_Authority) { Ammo--; } // 无论本地角色是服务器还是客户端都通过 Server RPC 通知服务器验证 ServerFire(Start, End); }这里有一个容易踩坑的地方客户端不能直接修改复制属性否则本地改一个值等服务器同步时又会覆盖回旧值。所以只有在服务器上才让 Ammo 减少也就是if (GetLocalRole() ROLE_Authority)。客户端的信息通过 RPC 发给服务器服务器修改后统一同步。ServerFire的_Implementationvoid AFPSCharacter::ServerFire_Implementation(FVector_NetQuantize100 Start, FVector_NetQuantize100 End) { // 服务器再次执行命中检测这是权威判定 FHitResult Hit; FCollisionQueryParams QueryParams; QueryParams.AddIgnoredActor(this); bool bHit GetWorld()-LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, QueryParams); // 如果命中则应用伤害 if (bHit) { AActor* HitActor Hit.GetActor(); if (HitActor HitActor-CanBeDamaged()) { UGameplayStatics::ApplyDamage(HitActor, 30.f, GetController(), this, nullptr); } } // 广播效果给所有客户端 MulticastShowHit(Start, End, bHit, bHit ? Hit.ImpactPoint : End); }MulticastShowHit的_Implementationvoid AFPSCharacter::MulticastShowHit_Implementation( FVector_NetQuantize100 TraceStart, FVector_NetQuantize100 TraceEnd, bool bHit, FVector_NetQuantize100 ImpactPoint) { // 这是一个方便调试的实现实际项目应替换为音效、粒子、弹孔特效 DrawDebugLine(GetWorld(), TraceStart, TraceEnd, FColor::Red, false, 1.0f); if (bHit) { DrawDebugSphere(GetWorld(), ImpactPoint, 10.f, 12, FColor::Green, false, 1.0f); } }这里用 DrawDebug 代替特效是为了让代码不依赖任何美术资源。你可以在这一步替换成UGameplayStatics::SpawnEmitterAtLocation播放火箭粒子或弹着点特效。解释一个细节客户端本地没有直接命中表现需要等服务器 Multicast 回来后再显示所以会有一点网络延迟。入门项目能接受商业项目会在此基础上做本地预测。这也是为什么在StartFire里没有调用任何特效函数的原因。4.7 伤害处理与死亡逻辑为了让 ApplyDamage 真正起作用FPSCharacter 需要重写TakeDamage。服务器调用 ApplyDamage 后最终会走到目标对象的 TakeDamage 函数。float AFPSCharacter::TakeDamage(float DamageAmount, FDamageEvent const DamageEvent, AController* EventInstigator, AActor* DamageCauser) { float FinalDamage Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser); if (GetLocalRole() ROLE_Authority) { Health FMath::Clamp(Health - FinalDamage, 0.f, 100.f); if (Health 0.f) { // 简易死亡处理直接销毁角色 Destroy(); } } return FinalDamage; } void AFPSCharacter::OnRep_Health() { // 在这里更新 UI 血条或者播放受伤反馈 // UE 会自动在客户端调用此函数服务器本机不会调用 }这个实现的关键是伤害只在服务器上计算。客户端没有计算能力即使玩家修改本地内存也无法让服务器角色掉血。这符合 FPS 网络架构里的“服务器权威”原则。4.8 注册复制属性没有注册的 Replicated 变量不会同步这是最常见的问题。在 FPSCharacter.cpp 中实现void AFPSCharacter::GetLifetimeReplicatedProps(TArrayFLifetimeProperty OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(AFPSCharacter, Health); DOREPLIFETIME(AFPSCharacter, Ammo); }Health使用了ReplicatedUsing OnRep_Health当服务器向客户端复制新血量时客户端会调用OnRep_Health。你可以在里面触发 UI 委托刷新 HUD。4.9 配置 GameMode 并启动联机测试代码写完以后先编译通过。运行前的关键步骤是把默认 GameMode 设置为 FPSGameMode。打开项目设置 - 项目 - Maps Modes - Default GameMode选择FPSGameMode。或者在世界设置中覆盖。联机测试最简单的办法在编辑器中点击 Play 按钮旁边的下拉箭头。设置Number of Players 2。选择Play As Listen Server。点击 Play。主窗口会以 Listen Server 模式启动它会同时承担服务器和玩家 1 的渲染任务。第二个窗口作为客户端自动连接本机。两个窗口使用同一张地图互相能看到对方的 FPS 角色。如果你想手动测试局域网连接可以直接在服务器窗口控制台输入open 127.0.0.1客户端窗口输入同样的命令效果相同。4.10 验证同步是否正确运行以后可以观察两个点第一角色移动是否同步。玩家 1 按 W 前进时玩家 2 的窗口里应该看到同一角色平滑移动。如果出现瞬移需要检查CharacterMovement的网络优化是否正常多数情况下 UE 自带移动复制已经处理好了。第二开火命中是否同步。玩家 1 开火后两个窗口都应该看到红线和绿色命中球说明ServerFire和MulticastShowHit链路完整。如果只在自己窗口看到红色调试线另一个窗口没有问题通常出在 Multicast RPC 没有正确广播或者目标角色没有开启复制。5. 常见问题与排查思路FPS 网络项目最常见的报错和表现如下问题现象常见原因解决思路编译失败找不到头文件VS 缺少 C 游戏开发组件打开 VS Installer勾选“使用 C 的游戏开发”角色不动按 WASD 无反应输入映射没有配置或没有绑定到函数检查 Project Settings 里的 Input Axis摄像机不跟随鼠标摄像机没有设置 bUsePawnControlRotation在构造函数设置FirstPersonCamera-bUsePawnControlRotation true其他客户端看不到角色角色没有开启 bReplicates在构造函数设置bReplicates true开火后只有自己在本地看到特效Multicast RPC 没有触发或目标 Actor 未复制检查 ServerFire 和 MulticastShowHit 的 UFUNCTION 修饰符开火后服务器没有伤害射线起点错误或 IgnoredActor 设置错误确认射线是从摄像机而不是角色中心发出Ammo 客户端和服务器不一致客户端直接修改了复制属性只在服务器上修改 Ammo客户端通过 RPC 请求服务器扣减玩家死亡后角色不消失TakeDamage 没有被调用检查命中 Actor 是否继承 AActor且 CanBeDamaged 返回 true排查顺序建议这样先确认单机逻辑正常再开启多人测试。检查角色是否出现在所有客户端窗口。检查移动复制是否正常。检查 RPC 调用链客户端调用 ServerFire服务器调用 MulticastShowHit。检查命中检测使用的碰撞通道是否是 ECC_Visibility。检查复制属性是否在 GetLifetimeReplicatedProps 里注册。6. 最佳实践与工程建议6.1 拒绝信任客户端输入多人射击游戏最常见的安全漏洞就是信任客户端。如果玩家可以告诉服务器“我的子弹打中了任何人”那么外挂甚至不用修改引擎只要改几个本地变量就能无限刷伤害。正确模型是客户端负责表现和输入服务器负责权威计算。具体来说射线检测必须在服务器再次执行。伤害计算必须在服务器执行。血量、弹药等变量只在服务器修改。客户端请求射击时服务器可以做距离检测、射线检测、冷却检查。这样可以挡住大部分简单的作弊手段但无法完全杜绝透视和自动瞄准那是反作弊系统负责的领域不在本文范围。6.2 RPC 的频率与可靠性选择在上面的代码中射击 RPC 用了Reliable。可靠 RPC 确保消息到达但如果射速高达每秒 10 发每发都使用可靠 RPC会占用大量网络带宽并可能引起重传风暴。演进方向射速较高的武器可以改用Unreliable。给弹道数据加一个时间戳或序列号防止乱序导致命中错乱。高频移动数据用Unreliable命中确认这类关键事件用Reliable。对于刚入门的项目保持Reliable没有大问题但你要知道它的成本。6.3 从射线检测升级到投射物射线检测更适合狙击枪和步枪。如果你想做火箭筒、榴弹炮需要在服务器上生成投射物。基本思路AFPSCharacter::ServerFire 中生成一个子弹 Actor FActorSpawnParameters SpawnParams; SpawnParams.Owner this; SpawnParams.Instigator this; AFPSProjectile* Projectile GetWorld()-SpawnActorAFPSProjectile(ProjectileClass, MuzzleLocation, MuzzleRotation, SpawnParams); Projectile-InitVelocity(GetActorForwardVector());投射物本身设置bReplicates true在自己的 Tick 中做移动和碰撞检测。这样所有客户端都能看到同一个子弹飞行轨迹。但注意如果客户端预测做不好投射物轨迹会出现偏差所以投射物网络同步比射线更复杂。6.4 Enhanced Input 迁移UE5.8 默认推荐 Enhanced Input。传统绑定适合快速原型但大型项目应该使用 Input Actions 和 Input Mapping Context。增强输入的优势支持 Chord、Modifier、Trigger 等高级输入组合。更适合多人游戏中的动态按键提示。蓝图处理和 C 处理都更清晰。迁移时把代码里的BindAxis改为绑定 Input Action在 C 中创建UInputAction资产然后在APlayerController或 Pawn 的BeginPlay中调用UEnhancedInputLocalPlayerSubsystem::AddMappingContext。6.5 动画同步和视觉效果本文没有深入动画网络。实际 FPS 项目中敌人角色应该有第三人称动画同步自己的角色应有第一人称手臂动画。这是另一个大主题。动画同步通常使用UCharacterMovementComponent自带的速度和加速度数据驱动动画蓝图网络压缩由引擎内部处理。手臂动画则建议用单独的动画资产不参与网络复制因为它是表现层内容。7. 总结与学习路线本文从一个可运行的 C 工程出发实现了第一人称角色移动、视角控制、射线射击、服务器验证、伤害处理和多人联机测试。你现在应该掌握了四个关键能力知道 Character PlayerController 和 GameMode 如何配合。会区分 Server RPC 与 Multicast RPC 的使用场景。能自己写一套“客户端请求、服务器判定、广播表现”的射击链路。知道射线检测和投射物检测的差别与适用场景。接下来可以继续推进的方向有三个。第一个是武器系统。把StartFire抽成独立的 Weapon Actor支持换弹、后坐力、射速、多武器切换。这样射击逻辑不再写在角色类里项目会更容易维护。第二个是动画系统。给角色接入第三人称动画蓝图给手臂接入第一人称动画蓝图再配合 Actor 的复制属性同步奔跑、跳跃、举枪动作。第三个是上线级别的网络完善。引入延迟补偿、客户端预测、服务器回滚、反作弊检测等机制这些是真实商业 FPS 必须面对的问题但它们应该建立在今天这套基础模型之上。先不要急着堆功能建议把当前代码跑通两个窗口的联机多观察复制日志再一步步扩展。如果这篇文章对你有帮助可以先收藏备用后续我会继续拆解武器系统、动画同步和进阶网络同步方案。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑