网络同步学习记录

news/2025/10/27 18:15:29/文章来源:https://www.cnblogs.com/xlh626/p/19169819

引擎选择

既然目标是DS架构那么,必须是源码版。需要从github上拉取源码并编译。
CS就像我的世界开放到局域网那种。服务端也是客户端。
DS就像腐竹开的服务器那样,专门的服务器,甚至是纯Linux的命令行环境,都没有图形界面。

Target.cs文件

DS架构的Target.cs结构

Source\MyProject\MyProjectServer.Target.cs   // 服务端
Source\MyProject\MyProjectEditor.Target.cs   // 编辑器
Source\MyProject\MyProjectClient.Target.cs   // 纯客户端(可选)

CS架构的Target.cs结构

MyProjectEditor.Target.cs 
MyProject.Target.cs

ServerTarget.cs

using UnrealBuildTool;  
using System.Collections.Generic;  public class MyProjectServerTarget : TargetRules  
{  public MyProjectServerTarget(TargetInfo Target) : base(Target)  {       Type = TargetType.Server;  DefaultBuildSettings = BuildSettingsVersion.V5;  IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_4;  ExtraModuleNames.Add("MyProject");  //千万不能改,必须和项目名一样。}
}

增加了新的Servertarget.cs意味着增加一个运行后的构建(编译加链接)目标,怎加target.cs后或需删除调binary和intermediate中间文件进行重新生成和构建

打包和构建

后面部分参考博客,这大哥写的博客很好,我照着做但是也遇到了些坑,于是我就在这里仅仅做一些补充。

我得声明源码版构建很慢很慢,我构建一个Editor和一个Server起码花了18个小时(我这破电脑),闲得蛋疼的时候找着了一本不错的书,推荐一下(大象无形:虚幻引擎程序涉及浅析(罗丁力),我只看了20%,因为翻了好多书才发现这本很不错,UBT的概念介绍的非常够用。而且罗老师还总结了不少杂七杂八的少用的库可以避免用原生C++造轮子)

  1. 地图:创建一个地图M_Client,这里是其关卡蓝图,用于输出当前关卡的名称,../attachment/Pasted image 20251021125543.png然后将地图复制俩个出来并命名成别的,我这里是M_Server和M_Transition。然后在UE项目中指定上去../attachment/Pasted image 20251021125940.png
  2. 打包客户端:首先选择Development Editor进行编译(第一次编译项目必须从ide中编译),编译完成后就可以打开此项目的UEEditor了。../attachment/Pasted image 20251021121902.png之后在里面打包到Windows(需要手动烘焙,因为源码版不会自动烘焙)。不出意外打包出来应该是这样的,有一个项目同名Folder存放资源和动态库之类的二进制文件,有一个EngineFolder存放对引擎的依赖../attachment/Pasted image 20251021123002.png
  3. 构建服务端:选择Development Server进行一次构建,因为不是打包,所以生成的文件都在项目工程路径的Binaries\Win64中。../attachment/Pasted image 20251021123709.png当然重要的只有这个exe文件,但是他无法直接打开,他的运算还需要借助资源,但是构建是没有资源的,所以把他放到打包出来的Package\Windows\项目名\Binaries\Win64下他就可以试用资源了。../attachment/Pasted image 20251021123953.png
  4. 因为没有图形化界面,直接打开还得到资源管理器查看,所以我这里用的cmd。E:\GameProject\UE5.4\DSLearn\Package\Windows\DSLearn\Binaries\Win64\DSLearnServer.exe -log 这里是我路径,-log用于输出信息,方便看Server地图中的输出。
  5. 这样log里面会反复输出M_Server,client运行起来则是M_Client.此时按住~键盘,open 服务端的ip,客户端就可以切换到服务端的地图,输出M_Server了。

尝试使用官方的同步

添加GameInstance如下。

//.h
public:UFUNCTION(BlueprintCallable)void ConnectDs(FString ip);//.cpp
void UMyGameInstance::ConnectDs(FString ip)
{UE_LOG(LogTemp, Warning, TEXT("连接到服务器: %s"), *ip);APlayerController* PC = GetWorld()->GetFirstPlayerController();if (PC){// 连接到服务器,服务器会自动把客户端带到 M_Server 地图FString ServerURL = ip + TEXT(":7777");UE_LOG(LogTemp, Warning, TEXT("连接URL: %s"), *ServerURL);PC->ClientTravel(ServerURL, ETravelType::TRAVEL_Absolute);}
}

因为UE的服务器默认在7777这里就写的7777。
../attachment/Pasted image 20251022003013.png
这段蓝图是一个控件的蓝图负责登录到输入的ip。把组件添加到客户端地图上就可以实现登录。
接下来修改服务端地图../attachment/Pasted image 20251022004102.png这里更换了地图,重点是得有PlayerStart点,和GameMode,并配置GameMode,这里是导入了第三人称的功能包用的是里面的GameMode。
我了个天调试了半天总算是登陆上去了,踩了几个坑,放在注意事项里了。
这种调试这玩意费劲,调通前完全不能理解,调通后就很明了了,就像调API的签名之类的一样。
../attachment/Pasted image 20251022005834.png

个人理解和注意事项

  1. 客户端需要拥有和客户端一样的资源,只是服务端不渲染,但是尺寸之类的信息都在资产里,所以得烘焙。所以编译出来的Server.exe最好放在打包出来的客户端里很舒服。
  2. 服务端的exe其实并不知道自己应该是什么地图,服务端只知道自己是服务端,服务端会查看引擎附带过去的配置资源决定自己启动时跑什么地图(见打包和构建)
  3. 服务端配置了Gamemode客户端不需要配置,因为Gamemode是地图级的,而GameInstance是游戏程序级的,既然登录到服务器就会切换地图,那客户端gamemode就不重要,所以Playercontroller在登陆上去也是如此。(大坑:GameInstance里实现了链接到服务器,所以GameInstance得去项目设置的地图里配置好)

配置自定义Actor的网络同步

同步一个Cube

  1. 修改Pawn的功能:../attachment/Pasted image 20251022175717.png配置生成一个cube,这里必须是事件,只有事件才能配置到服务器上运行../attachment/Pasted image 20251022175836.png针对这里,我的理解是,蓝图中函数的反射用掉了C++的引用传入,但是很多时候需要在原本的数据上做修改,所以引入了事件这一概念,我觉得事件就是给蓝图将引用传入映射为多引脚输出这一功能擦屁股的,本质也是函数。而RPC协议总归得调用函数,所以事件就成了这一媒介,我是这么理解的,肯定不准确,也肯定有坑,但是在我这算是能自圆其说了(我觉得独特的理解比精准的本质更能指点迷津)。
  2. 创建同步的Actor:我这里是用了一个Cube,核心是这段配置,固定关联,和复制,这段我没有啥看法。../attachment/Pasted image 20251022182033.png然后我还额外添加了物理模拟和重力,这个感觉可有可无。
  3. 打包,发现了这里的两个刚好对应Targetcs的名称,试了下确实能分别打服务包和客户端包,省的找那个exe了,至于那个Editor.Target.cs可能就是编辑器本身吧?../attachment/Pasted image 20251022182303.png

Actor权限

想Pawn添加。需要先增加TextRender组件。
在扣E SpawnCube的逻辑后面添加,目前暂时只有此事件能进行网络同步。
../attachment/Pasted image 20251022202521.png这里发现UE有提供调试联机游戏的功能,只要运行游戏选择Net模式中的Play As Listen Server就能以服务端的形式打开当前地图。

  • 确实服务端输出为:ROLE_Authority(服务器拥有所有Actor的控制权,即所有的Actor在服务器端的控制权都是ROLE_Authority)
  • 客户端上看自己的Pawn:ROLE_AutonomousProxy:客户端对本地Actor拥有这个控制权(不知道我的客户端上为啥是TEXT可能没渲染?)

实现一个Pawn

创建一个Actor,让它有些行为。../attachment/Pasted image 20251022211303.png
../attachment/Pasted image 20251022211257.png然后指定一套全新的gamemode,playercontroller和这个pawn。并取消弹簧臂对Pawn的控制旋转的继承。../attachment/Pasted image 20251022211929.png我这里似乎有点问题就是只有第一个Client能正常控制,其他Client似乎处于不能控制的状态,换用BP_ThirdPersonGameMode也是如此,似乎是因为碰撞原因,多加了几个PlayerStart并放远点确实可以正常控制了。可能PlayerStart和创建出来的Pawn间存在什么碰撞细节是我不知道的。总之感觉差不多就行了。

属性同步

  1. 如果客户端有某个属性需要同步到其他客户端,需注意:
    1. 需要将属性的Replication选定Replicated活RepNotify。
    2. 修改属性的代码运行在服务器。(估计是客户端发起更改的请求,服务端进行更改,并广播更改后的结果)
  2. UPROPERTY+Replicaiton标记=允许网络同步,Replication的属性枚举
    1. None:不允许网络同步
    2. Replication:属性允许网络同步
    3. RepNotify:属性允许网络同步,同时绑定一个回调函数,属性发生变化时回调,在蓝图中回调函数以OnRep_开头,属性名结尾。

简单同步

换了一个Pawn,一方面是这部分不需要了,之前的滚球pawn有点视角的问题,我挑了下camera视角没问题了但是移动方向还是有点问题,不咋看的明白这个滚球的运动是怎么算的。直接换一个Pawn(从第三人称模板复制过来的),反正这里主要冲学网络同步。
第一个是Pawn:../attachment/Pasted image 20251023115548.png还有就是../attachment/Pasted image 20251023115731.png
第二个是客户端Widget:../attachment/Pasted image 20251023115657.png
需要注意NotifyTimeValue得运行在服务器上,整体就是连上服务器后,可以扣F弹出客户端那个UI,UI里的可输入文本框提交后会在服务端调用NotifyTimeValue并传入输入的值。NotifyTimeValue会在服务端修改Time,修改完成后Time会下发到每个客户端进行同步(可网络同步且拥有客户但同步完成的回调,这个回调会执行SetTime)../attachment/Pasted image 20251023120254.png

复杂同步

这个Speek需要指定在服务器运行,我也遇到了博主遇到的问题,而且我使用multicast+server还是只能在所有客户端上同步,server的就没同步下来不过server端自己可以看到自己,client互相同步,不能理解啥原因。总之先过去再说吧。这段AddBeginPlay放在BeginPlay后执行。
../attachment/Pasted image 20251023164333.png
../attachment/Pasted image 20251023164604.png

C++网络同步

还有这篇博客很不错:博客点我
总算到C++了。创建一个C++类。这个老工程就扔了。这个新工程打算好好做一下。这是项目结构../attachment/Pasted image 20251025003456.png

基础的代码

一个用于调试的代码

MyTools中的GameDebug类

//.h
#pragma once  #include "CoreMinimal.h"  namespace Debug  
{  static void PrintMessage(const FString& Message,float ShowTime = 2.0f,FColor Color = FColor::MakeRandomColor(),int32 InKey = -1)  {       if (GEngine)  {          GEngine->AddOnScreenDebugMessage(InKey, ShowTime, Color, Message);  UE_LOG(LogTemp, Warning, TEXT("Message:%s"), *Message);  }    }}//.cpp,完全口的
// Fill out your copyright notice in the Description page of Project Settings.  
#include "GameDebug.h"

一个本地的Character

Actor中的MyCharacter类--头文件,本来打算用两个结构体来同步一些状态数据,后面突然一想到到时候网络同步每次都得发整个结构体可能会降低同步速度,这个Character能多段跳(不知道有啥用,反正最差也能设置为一段条,不过感觉可以让某些技能占用多段跳(暂时没接触过rpg动作和gas也不清楚这么做是不是合理行为,不过大不了就设置为1呗))

#pragma once  #include "CoreMinimal.h"  
#include "GameFramework/Character.h"          #include "MyCharacter.generated.h"  UCLASS()  
class MMODEMO_API AMyCharacter : public ACharacter   {  GENERATED_BODY()  public:  AMyCharacter();  protected:  virtual void BeginPlay() override;  public:  virtual void Tick(float DeltaTime) override;  virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;  public:  //Component  UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)  class USpringArmComponent* SpringArmComponent;  UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)  class UCameraComponent* CameraComponent;  //Config  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Config)  float WalkSpeed=300.f;  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Config)  float SprintSpeed=600.f;  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Config)  int32 MaxJumpTimes = 4;  UPROPERTY(EditAnywhere,BlueprintReadWrite,Category = Config)  float JumpZVelocity = 500.f;  //State  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = State)  int32 AvailableJumpTimes = MaxJumpTimes;  protected:  void MoveForward(float InputAxis);  void MoveRight(float InputAxis);  void StartSprint();  void StopSprint();  virtual void Jump() override;  virtual bool CanJumpInternal_Implementation() const override;  virtual void Landed(const FHitResult& Hit) override;  };

MyCharacter源文件,没啥说头,主要是

// Fill out your copyright notice in the Description page of Project Settings.  #include "MyCharacter.h"  
//组件  
#include <string>  #include "GameFramework/SpringArmComponent.h"  
#include "Camera/CameraComponent.h"  
#include "GameFramework/CharacterMovementComponent.h"  //开发用自己的工具
#if UE_BUILD_DEVELOPMENT ||UE_BUILD_DEBUG  
#include "../MyTools/GameDebug.h"  
#endif  // Sets default values  
AMyCharacter::AMyCharacter()  
{  // Set this pawn to call Tick() every frame.  You can turn this off to improve performance if you don't need it.  PrimaryActorTick.bCanEverTick = true;  SpringArmComponent = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));  CameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));  SpringArmComponent->SetupAttachment(RootComponent);  CameraComponent->SetupAttachment(SpringArmComponent,USpringArmComponent::SocketName);  GetMesh()->SetRelativeLocationAndRotation(FVector(0.0f, 0.0f, -90.0f), FQuat(FRotator(0.0f, -90.0f, 0.0f)));  SpringArmComponent->bUsePawnControlRotation = true;  //角色朝向移动方向,使用控制器的朝向(和bOrientRotationToMovement不能都为True,冲突覆盖),继承世界基础旋转  GetCharacterMovement()->bOrientRotationToMovement = true;  GetCharacterMovement()->bUseControllerDesiredRotation = true;  GetCharacterMovement()->bIgnoreBaseRotation = true;  }  // Called when the game starts or when spawned  
void AMyCharacter::BeginPlay()  
{  Super::BeginPlay();  //初始化配置  GetCharacterMovement()->MaxWalkSpeed = WalkSpeed;  GetCharacterMovement()->JumpZVelocity = JumpZVelocity;  
}  // Called every frame  
void AMyCharacter::Tick(float DeltaTime)  
{  Super::Tick(DeltaTime);  
}  // Called to bind functionality to input  
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)  
{  Super::SetupPlayerInputComponent(PlayerInputComponent);  //Axis  PlayerInputComponent->BindAxis("MoveForward",this,&AMyCharacter::MoveForward);  PlayerInputComponent->BindAxis("MoveRight",this,&AMyCharacter::MoveRight);  PlayerInputComponent->BindAxis("Turn",this,&AMyCharacter::AddControllerYawInput);  PlayerInputComponent->BindAxis("LookUp",this,&AMyCharacter::AddControllerPitchInput);  //Action  PlayerInputComponent->BindAction("Sprint",IE_Pressed,this,&AMyCharacter::StartSprint);  PlayerInputComponent->BindAction("Sprint",IE_Released,this,&AMyCharacter::StopSprint);  PlayerInputComponent->BindAction("Jump",IE_Pressed,this,&AMyCharacter::Jump);  
}  void AMyCharacter::MoveForward(float AxisValue)  
{  if((Controller != nullptr) &&(AxisValue  !=0.0f))  {       const FRotator Rotation = Controller->GetControlRotation();      
const FRotator YawRotation(0,Rotation.Yaw,0);  const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);  // AddMovementInput(GetActorForwardVector(),AxisValue);  AddMovementInput(Direction,AxisValue);  }}  void AMyCharacter::MoveRight(float AxisValue)  
{  if((Controller!=nullptr)&&(AxisValue !=0.0f))  {       const FRotator Rotation = Controller->GetControlRotation();  const FRotator YawRotation(0,Rotation.Yaw,0);  const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);  AddMovementInput(Direction,AxisValue);  }}  void AMyCharacter::StartSprint()  
{  if ((Controller != nullptr) && (GetCharacterMovement() != nullptr))  {       GetCharacterMovement()->MaxWalkSpeed = SprintSpeed;  }}  
void AMyCharacter::StopSprint()  
{  if ((Controller != nullptr) && (GetCharacterMovement() != nullptr))  {       GetCharacterMovement()->MaxWalkSpeed = WalkSpeed;  }}  void AMyCharacter::Jump()  
{  if ((Controller != nullptr)&& (GetCharacterMovement() != nullptr))  {       Super::Jump();  AvailableJumpTimes--;  }  
}  bool AMyCharacter::CanJumpInternal_Implementation() const  
{  if (AvailableJumpTimes>0)  {       return true;  }    return Super::CanJumpInternal_Implementation();  
}  void AMyCharacter::Landed(const FHitResult& Hit)  
{  Super::Landed(Hit);  AvailableJumpTimes=MaxJumpTimes;  
}

尝试--先看原作者的代码

首先过一遍原作者C++这部分的博客.
头文件

  1. 属性宏
    1. 在函数层面是UFUNCTION宏的3个参数,Server、Client、NetMulticast,变量曾是UPROPERTY的宏Replicated。
    2. 属性里的话Replicated应该就是普通的同步,ReplicatedUsing = OnRep_cubeCountTotal这个应该是蓝图中对应的RepNotify,OnRep_开头,属性名结尾(作者代码上属性名就是cubeCountTotal),不过似乎在C++中就可以任意指定函数名不需要按照这个死板的要求写函数名了。还是C++舒服,感觉UE官方为了让C++强兼蓝图真的做了太多,但是我个人还是想要一个文本化的脚本方式,不大喜欢蓝图。
  2. 方法宏
    1. 函数中Server标记的函数在客户端调用在服务器端执行,Client标记的函数在服务端调用,运行在客户端,NetMulticast在服务器调用,且在服务器和所有客户端上执行。这三种方式似乎都没有远程返回数据的说法,单项请求,想拿返回值估计完全靠Replicated
    2. Server,Client,NetMulticast需要搭配Reliable使用,Reliable标识可以丢包重发(内部有重排序重传和确认等实现,对上层进行封装,表现为即使送达数据且不丢包)。对应位Unrelible。
  3. RPC函数的实现
    1. 用Server,Client,NetMulticast标记后,函数名后需要加_Implementation后缀(当我没说,也可能时为了兼容蓝图整的)
  4. 默认AActor拥有网络同步的能力,UObject没有,若不重载GetLifetimeReplicatedProps方法,就算标识了RPC的标记宏,UObject也不能网络同步。
//ChatComponent.h  
#pragma once  
#include "Components/TextRenderComponent.h"  
#include "Blueprint/UserWidget.h"  
#include "CoreMinimal.h"  
#include "Components/ActorComponent.h"  
#include "ChatComponent.generated.h"  UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )  
class LSPTETRISCLIENT_API UChatComponent : public UActorComponent  
{  GENERATED_BODY()  public:   UChatComponent();  UPROPERTY(Replicated)  int cubeCount = 0;  UPROPERTY(ReplicatedUsing = OnRep_cubeCountTotal, EditAnywhere, BlueprintReadWrite)  int cubeCountTotal = 20;  UPROPERTY(EditAnywhere,BlueprintReadWrite)  FColor TextColor = FColor(0);  
private:  APlayerController* playerPtr;  UClass* Cube;  
protected:  virtual void BeginPlay() override;  public:   virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;  UFUNCTION(Server,Reliable)  void SpwanCube();  UFUNCTION(Client,Reliable,BlueprintCallable)  void SwitchTextRenderColor();  UFUNCTION(NetMulticast, Reliable)  void NetMulticastSetTextRenderColor(UTextRenderComponent* textRender);  virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;  UFUNCTION()  void OnRep_cubeCountTotal();  
};

源文件

  1. 难保以后我不用component来做同步,看看重载.
    1. DOREPLIFETIMEDOREPLIFETIME_CONDITION 这两个宏用在 GetLifetimeReplicatedProps用于给指定的类的某个字段的属性添加网络同步
    2. 这两个宏前者无条件注册,后者有第三个参数(条件)
  2. 构造函数通过SetIsReplicated来配置是否启动网络复制,不然所有标记属性都不能同步。
#include "Replication/ChatComponent.h"  
#include "InputCoreTypes.h"  
#include "Net/UnrealNetwork.h"  UChatComponent::UChatComponent()  
{  PrimaryComponentTick.bCanEverTick = true;  SetIsReplicated(true);  
}  void UChatComponent::BeginPlay()  
{  Super::BeginPlay();  playerPtr = GetWorld()->GetFirstPlayerController();  Cube = LoadClass<AActor>(NULL, TEXT("Blueprint'/Game/Map/Cube.Cube_C'"));  }  void UChatComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)  
{  Super::TickComponent(DeltaTime, TickType, ThisTickFunction);  if (playerPtr->IsInputKeyDown(EKeys::LeftMouseButton))  {       SpwanCube();  }    SwitchTextRenderColor();  
}  void UChatComponent::SwitchTextRenderColor_Implementation()  
{  if (GetWorld()->IsServer())  {       if (cubeCount >= cubeCountTotal)  {          cubeCount = 0;  TArray<UTextRenderComponent*> comps;  GetOwner()->GetComponents(comps);  if (comps.Num() != 1)  {             return;  }          UTextRenderComponent* textRender = comps[0];  NetMulticastSetTextRenderColor(textRender);  }    }}  void UChatComponent::SpwanCube_Implementation()  
{  if (Cube && GetWorld())  {       GetWorld()->SpawnActor<AActor>(Cube, GetOwner()->GetActorTransform());  cubeCount++;  }
}  void UChatComponent::NetMulticastSetTextRenderColor_Implementation(UTextRenderComponent* textRender)  
{  textRender->SetTextRenderColor(TextColor);  
}  void UChatComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const  
{  Super::GetLifetimeReplicatedProps(OutLifetimeProps);  DOREPLIFETIME(UChatComponent, cubeCount);  
}  void UChatComponent::OnRep_cubeCountTotal()  
{  if (cubeCountTotal >= 40)  {       FString msg = FString::FromInt(cubeCountTotal);  GEngine->AddOnScreenDebugMessage(-1, 5, FColor::Red, *msg);  }}

改良代码

在构造函数里指定bReplicates = true;随便整了个Playercontroller和Gamemode用于测试,效果就有了。看来CharacterMovement组件自带了网络同步,那Movement相关的底层调用肯定是同步了的,我的代码无非就根据一些判断算下速度什么的,应该不用,额也还是有必要添加这部分控制的,MMO总得防外挂吧?行动肯定得在服务端进行计算,不过作为demo我可懒得弄,以后再说,先把功能做了再说,这里打算做个额血条?
../attachment/Pasted image 20251025032507.png

一番没意义的折腾

出于不想用蓝图,所以控件打算用C++写,但是控件属于资源,只能用蓝图来写,哪怕逻辑放在C++上也还要单独在蓝图创建同名的变量和UI控件,那还不如纯蓝图呢。

控件

../attachment/Pasted image 20251025190746.png
希望我的审美还算可以,这里只有UI没有逻辑,对的,和Kimi进行一番探讨,我确定逻辑都用C++写在Actor里,这个控件在Actor里通过引用给Actor操作。嘛,这么说来的话,是资源也说的过去了。

然后因为我的控件时1928x512这个像素尺寸,所以肯定不能直接处理拿过去用。我这里选中整个画布,将他进行缩放。经过计算0.4倍数缩放就是772x205(这个比例很重要后面要用),这一块儿我要单独放在笔记整理内容里面,踩坑踩了好久起码两天,都卡在渲染上。
../attachment/Pasted image 20251026185103.png

涉及的代码

头文件
之前的代码完成了一个普通3段跳,这里添加一个UI。

public://ComponentUPROPERTY(VisibleAnywhere,BlueprintReadWrite,Category=Widget)  class UWidgetComponent* WidgetComponent;//Entity  UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")  TSubclassOf<UUserWidget> HealthBarWidgetClass;

源文件
之前计算出来的那个0.4的那个缩放出来的数值有用了,在SetDrawSize里配置WidgetComponent的画板的大小不可以低于这里的不然会有截断。(这里被AI骗了不少次,UEC++确实资源太少了,AI也不懂,最后自己拿着蓝图测试测出来的。)

AMyCharacter::AMyCharacter()
{//配置Actor状态栏  static ConstructorHelpers::FClassFinder<UUserWidget> Finder(TEXT("/Script/UMGEditor.WidgetBlueprint'/Game/MyAsset/UI/StateBar.StateBar_C'"));  if (HealthStatsBarClass.Succeeded())  {  //TODO:感觉这里可以优化,让UI永远面朝Actor。  HealthBarWidgetClass = Finder.Class;//载入UI资源到组件上 WidgetComponent->SetWidgetClass(HealthBarWidgetClass);WidgetComponent->SetWidgetSpace(EWidgetSpace::World);//让UI作为单独的部件和角色绑定(与Screen相对)  WidgetComponent->SetDrawAtDesiredSize(true);//启动强制裁切WidgetComponent的尺寸  WidgetComponent->SetDrawSize(FVector2D(1000.f,256.f));//指定WidgetComponent的尺  WidgetComponent->SetRelativeLocation(FVector(0.f, 0.f, 100.f)); // 角色头顶上方  WidgetComponent->SetPivot(FVector2D(0.5f, 0.5f)); // 设置中心点  WidgetComponent->SetVisibility(true);  }
}

这时候效果应该如这。
../attachment/Pasted image 20251026193002.png
我感觉挺好的,反正玩家不需要看到自己的血条,玩家自己的状态应该更详细点,整个自己的HUD。这里血条和ID还有等级都没有设置,接下来就要弄这个了。

头文件

  1. 状态类数据全部同步
  2. 重载函数GetLifetimeReplicatedProps
//State  
UPROPERTY(Replicated,EditAnywhere, BlueprintReadWrite, Category = State)  
int32 AvailableJumpTimes = MaxJumpTimes;  UPROPERTY(Replicated,EditAnywhere, BlueprintReadWrite, Category = Stats)  
float CurrentHealth = MaxHealth;  UPROPERTY(Replicated,EditAnywhere, BlueprintReadWrite, Category = State)  
FString PlayerName;  UPROPERTY(Replicated,EditAnywhere, BlueprintReadWrite, Category = State)  
int32 Level;virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;

源文件

  1. 引入头文件
  2. 注册同步的属性
#include "Net/UnrealNetwork.h"void AMyCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const  
{  Super::GetLifetimeReplicatedProps(OutLifetimeProps);  DOREPLIFETIME(AMyCharacter, AvailableJumpTimes);  DOREPLIFETIME(AMyCharacter, CurrentHealth);  DOREPLIFETIME(AMyCharacter, PlayerName);  DOREPLIFETIME(AMyCharacter, Level);  
}

效果展示

自己的血条扣下去就变红了,可能这个颜色不是很明显,但是以后再说吧。我觉得我有必要强调一下调试的代码。额蓝图。这是MyCharacter的派生蓝图类中的调试用蓝图,注意一定得是运行再服务器上,不然就只能看到服务器自己的actor血条变动,必须client请求扣血,服务端扣血完后向下同步并回调。
../attachment/Pasted image 20251027180618.png
../attachment/Pasted image 20251027180534.png

阶段代码

头文件,

  1. 增加了对UI内部组件的引用,避免频繁查哈希(Tools里)
  2. 增加同步血量的回调函数以及其调用的更新血条的方法。
  3. 命名对应控件名:../attachment/Pasted image 20251027174648.png
#pragma once  #include "CoreMinimal.h"  
#include "GameFramework/Character.h"          #include "MyCharacter.generated.h"  UCLASS()  
class MMODEMO_API AMyCharacter : public ACharacter   {  GENERATED_BODY()  public:  AMyCharacter();  protected:  virtual void BeginPlay() override;  public:  virtual void Tick(float DeltaTime) override;  virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;  virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;  public:  //Component  UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Camera)  class USpringArmComponent* SpringArmComponent;  UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Camera)  class UCameraComponent* CameraComponent;  UPROPERTY(VisibleAnywhere,BlueprintReadWrite,Category=Widget)  class UWidgetComponent* WidgetComponent;  //Entity  UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")  TSubclassOf<UUserWidget> HealthBarWidgetClass;  //Tools  UPROPERTY(EditDefaultsOnly, Category = "UI")  UUserWidget* HealthBar;  UPROPERTY()  class UProgressBar* HealthBar_Progress;  UPROPERTY()  class UTextBlock* HealthBar_LevelBlock;  UPROPERTY()  UTextBlock* HealthBar_CurrentHealthBlock;  UPROPERTY()  UTextBlock* HealthBar_MaxHealthBlock;  //BaseConfig  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Config)  float WalkSpeed=300.f;  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Config)  float SprintSpeed=600.f;  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Config)  int32 MaxJumpTimes = 4;  UPROPERTY(EditAnywhere,BlueprintReadWrite,Category = Config)  float JumpZVelocity = 500.f;  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stats)  float MaxHealth=100.0f;  //State  UPROPERTY(Replicated,EditAnywhere, BlueprintReadWrite, Category = State)  int32 AvailableJumpTimes = MaxJumpTimes;  UPROPERTY(ReplicatedUsing=OnRep_CurrentHealth,EditAnywhere, BlueprintReadWrite, Category = Stats)  float CurrentHealth = MaxHealth;  UPROPERTY(Replicated,EditAnywhere, BlueprintReadWrite, Category = State)  FString PlayerName;  UPROPERTY(Replicated,EditAnywhere, BlueprintReadWrite, Category = State)  int32 Level;  //Replicate--Callback  UFUNCTION()  void OnRep_CurrentHealth();  protected:  void MoveForward(float InputAxis);  void MoveRight(float InputAxis);  void StartSprint();  void StopSprint();  virtual void Jump() override;  virtual bool CanJumpInternal_Implementation() const override;  virtual void Landed(const FHitResult& Hit) override;  void UpdateHealthBar();  };

源文件
更改UI载入的逻辑(从构造方法迁移至Beginplay)

// Fill out your copyright notice in the Description page of Project Settings.  #include "MyCharacter.h"  
//组件  
#include "GameFramework/SpringArmComponent.h"  
#include "Camera/CameraComponent.h"  
#include "GameFramework/CharacterMovementComponent.h"  
#include "Components/WidgetComponent.h"  //控件  
#include "Components/ProgressBar.h"  
#include "Components/TextBlock.h"  
#include "Components/HorizontalBox.h"  //依赖  
#include "DSP/EventQuantizer.h"  
#include "Net/UnrealNetwork.h"  //自己的实现  //自己的工具  
#if UE_BUILD_DEVELOPMENT ||UE_BUILD_DEBUG  
#include "../MyTools/GameDebug.h"  
#endif  // Sets default values  
AMyCharacter::AMyCharacter()  
{  // Set this pawn to call Tick() every frame.  You can turn this off to improve performance if you don't need it.  PrimaryActorTick.bCanEverTick = true;  SpringArmComponent = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));  CameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));  WidgetComponent = CreateDefaultSubobject<UWidgetComponent>(TEXT("WidgetComp"));  SpringArmComponent->SetupAttachment(RootComponent);  CameraComponent->SetupAttachment(SpringArmComponent,USpringArmComponent::SocketName);  WidgetComponent->SetupAttachment(GetRootComponent());  //配置网格体初始朝向  GetMesh()->SetRelativeLocationAndRotation(FVector(0.0f, 0.0f, -90.0f), FQuat(FRotator(0.0f, -90.0f, 0.0f)));  //相机交给鼠标控制,角色朝向移动方向,使用控制器的朝向(和bOrientRotationToMovement不能都为True,冲突覆盖),继承世界基础旋转  SpringArmComponent->bUsePawnControlRotation = true;  GetCharacterMovement()->bOrientRotationToMovement = true;  GetCharacterMovement()->bUseControllerDesiredRotation = true;  GetCharacterMovement()->bIgnoreBaseRotation = true;  //网络同步  bReplicates = true;  //配置Actor状态栏控件的配  //TODO:感觉这里可以优化,让UI永远面朝Actor。但后面再说吧  static ConstructorHelpers::FClassFinder<UUserWidget> Finder(TEXT("/Script/UMGEditor.WidgetBlueprint'/Game/MyAsset/UI/StateBar.StateBar_C'"));  if(Finder.Succeeded())  {       HealthBarWidgetClass = Finder.Class;  // WidgetComponent->SetWidgetClass(HealthStatsBarClass.Class);//载入UI资源到组件上,一般避免在构造方法硬编码,此处写着用于在蓝图查看UI状态  WidgetComponent->SetWidgetSpace(EWidgetSpace::World);//让UI作为单独的部件和角色绑定(与Screen相对)  WidgetComponent->SetDrawAtDesiredSize(true);//启动强制裁切WidgetComponent的尺寸  WidgetComponent->SetDrawSize(FVector2D(1000.f,256.f));//指定WidgetComponent的尺  WidgetComponent->SetRelativeLocation(FVector(0.f, 0.f, 120.f)); // 角色头顶上方  WidgetComponent->SetPivot(FVector2D(0.5f, 0.5f)); // 设置中心点  WidgetComponent->SetVisibility(true);  }    }  // Called when the game starts or when spawned  
void AMyCharacter::BeginPlay()  
{  Super::BeginPlay();  //初始化配置  GetCharacterMovement()->MaxWalkSpeed = WalkSpeed;  GetCharacterMovement()->JumpZVelocity = JumpZVelocity;  //载入控件实例  if (WidgetComponent)  {       WidgetComponent->SetWidgetClass(HealthBarWidgetClass);  HealthBar = CreateWidget<UUserWidget>(GetWorld(),HealthBarWidgetClass);  WidgetComponent->SetWidget(HealthBar);  //获取一批引用工具  HealthBar_Progress = Cast<UProgressBar>(HealthBar->GetWidgetFromName(TEXT("HealthBar")));  HealthBar_LevelBlock = Cast<UTextBlock>(HealthBar->GetWidgetFromName((TEXT("LevelBlock"))));  HealthBar_CurrentHealthBlock = Cast<UTextBlock>(HealthBar->GetWidgetFromName(TEXT("CurrentHealthBlock")));  HealthBar_MaxHealthBlock = Cast<UTextBlock>(HealthBar->GetWidgetFromName(TEXT("MaxHealthBlock")));  }}  // Called every frame  
void AMyCharacter::Tick(float DeltaTime)  
{  Super::Tick(DeltaTime);  Debug::PrintMessage(FString::Printf(TEXT("%s:%f"),*GetName(),CurrentHealth),1.0);  
}  // Called to bind functionality to input  
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)  
{  Super::SetupPlayerInputComponent(PlayerInputComponent);  //Axis  PlayerInputComponent->BindAxis("MoveForward",this,&AMyCharacter::MoveForward);  PlayerInputComponent->BindAxis("MoveRight",this,&AMyCharacter::MoveRight);  PlayerInputComponent->BindAxis("Turn",this,&AMyCharacter::AddControllerYawInput);  PlayerInputComponent->BindAxis("LookUp",this,&AMyCharacter::AddControllerPitchInput);  //Action  PlayerInputComponent->BindAction("Sprint",IE_Pressed,this,&AMyCharacter::StartSprint);  PlayerInputComponent->BindAction("Sprint",IE_Released,this,&AMyCharacter::StopSprint);  PlayerInputComponent->BindAction("Jump",IE_Pressed,this,&AMyCharacter::Jump);  
}  void AMyCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const  
{  Super::GetLifetimeReplicatedProps(OutLifetimeProps);  DOREPLIFETIME(AMyCharacter, AvailableJumpTimes);  DOREPLIFETIME(AMyCharacter, CurrentHealth);  DOREPLIFETIME(AMyCharacter, PlayerName);  DOREPLIFETIME(AMyCharacter, Level);  
}  void AMyCharacter::OnRep_CurrentHealth()  
{  UpdateHealthBar();  
}  void AMyCharacter::MoveForward(float AxisValue)  
{  if((Controller != nullptr) &&(AxisValue  !=0.0f))  {       const FRotator Rotation = Controller->GetControlRotation();      
const FRotator YawRotation(0,Rotation.Yaw,0);  const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);  // AddMovementInput(GetActorForwardVector(),AxisValue);//会抽搐,可能存在Actor的朝向会被返回调整导致的  AddMovementInput(Direction,AxisValue);  }}  void AMyCharacter::MoveRight(float AxisValue)  
{  if((Controller!=nullptr)&&(AxisValue !=0.0f))  {       const FRotator Rotation = Controller->GetControlRotation();  const FRotator YawRotation(0,Rotation.Yaw,0);  const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);  AddMovementInput(Direction,AxisValue);  }}  void AMyCharacter::StartSprint()  
{  if ((Controller != nullptr) && (GetCharacterMovement() != nullptr))  {       GetCharacterMovement()->MaxWalkSpeed = SprintSpeed;  }}  
void AMyCharacter::StopSprint()  
{  if ((Controller != nullptr) && (GetCharacterMovement() != nullptr))  {       GetCharacterMovement()->MaxWalkSpeed = WalkSpeed;  }}  void AMyCharacter::Jump()  
{  if ((Controller != nullptr)&& (GetCharacterMovement() != nullptr))  {       Super::Jump();  AvailableJumpTimes--;  }  
}  bool AMyCharacter::CanJumpInternal_Implementation() const  
{  if (AvailableJumpTimes>0)  {       return true;  }    return Super::CanJumpInternal_Implementation();  
}  void AMyCharacter::Landed(const FHitResult& Hit)  
{  Super::Landed(Hit);  AvailableJumpTimes=MaxJumpTimes;  
}  void AMyCharacter::UpdateHealthBar()  
{  if (HealthBar)  {       if (HealthBar_CurrentHealthBlock != nullptr)HealthBar_CurrentHealthBlock->SetText(FText::AsNumber(CurrentHealth));  if (HealthBar_MaxHealthBlock!=nullptr)HealthBar_MaxHealthBlock->SetText(FText::AsNumber(MaxHealth));  if (HealthBar_Progress!=nullptr)  {          float Percentage = CurrentHealth/MaxHealth;  HealthBar_Progress->SetPercent(Percentage);  HealthBar_Progress->SetFillColorAndOpacity(FLinearColor(1.0f, Percentage, Percentage));  }  }    
}

好的,这个网络同步感觉差不多了,也写出来了个所以然,后面花点时间整理下UI的个人方案吧。
整体来说网络同步入门太简单了,就那么几个宏。唯一花时间的就是我不太熟UI的隐藏规则,我的UI是透明的刚开始没有配置缩小,但是又划定了显示,导致血条一直是个透明的,我一度怀疑人生。调试好久才找到原因。不过听说网络同步入门容易,但是精通很难。好了,这篇文章发网上了,抄了原作者不少,不过我也改了不少东西,应该不叫抄,叫借鉴。然后我还得贴上参考的帖子路径,嗯,就这俩。

  1. https://goulandis.github.io/2021/08/11/【UE5】UE5 Dedicated Server专用服务器与网络同步/#3-UFUNCTION-NetMulticast-Reliable
  2. https://cedric-neukirchen.net/docs/category/multiplayer-network-compendium/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/947983.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ZR 2025 NOIP 二十连测 Day 9

100 + 100 + 40 + 10 = 250, Rank 67/130.25noip二十连测day9 链接:link 题解:题目内 时间:4h (2025.10.27 14:00~18:00) 题目数:4 难度:A B C D\(\color{#F39C11} 橙\) \(\color{#52C41A} 绿\)*1000 *1600估分:…

1027随笔

今天开始要拍上课视频了。 我现在手上就只有三节课的视频,为什么呢——手机没内存了。 我寻思先把视频传过去吧,内容量太大无法传输。 额,压缩吧,10个g压到2.5个g。 然后关于专业的事-就把作业写了,也就不到100行…

阿斯顿

阿斯顿阶段 业务理解 数据理解 数据分析 模型训练 模型评价 模型部署任务 确定业务目标 原始数据收集 数据筛选 算法确定 评价结果 模型发布•背景 •数据描述 数据清洗 •算法选择 •评价模型产出 •发布说明•业务目…

交换机VOQ机制

virtual output queuing机制,是一种被广泛使用的内部调度机制,为了解决队头阻塞问题(head of line Blocking,hol blocking) 虚拟输出队列(Virtual Output Queuing, VOQ)是一种在高速交换机和路由器中广泛采用的…

ask_skill

如果目标是为防御做资产盘点或研究,应在获得所有者明确授权或在只扫描/管理你自身网络范围内进行;或者通过官方渠道(例如厂商、云/平台运营方或 ISP 与执法机构)请求协作或求助

最小树形图

给定一个有权有向图 \(G=\langle V,A\rangle,w:A\mapsto\mathbb{R}\) 和一个根 \(r\in G\),求以 \(r\) 为根的最小生成树,满足每条边都是父亲指向儿子(外向树)。暴力做法 不失一般性,我们可以简单的 \(O(|V|+|A|)…

网络安全资源大全:助你紧跟前沿威胁与防御技术

本文详细介绍了网络安全专家常用的各类资源,包括黑客新闻网站、技术博客、威胁情报平台、YouTube技术频道、社交媒体专家和网络安全播客,帮助读者构建完整的学习和情报获取体系。帮助我保持领先的网络安全资源 如何跟…

详细介绍:【Ubuntu 20.04升级python3.9后终端打不开的bug】

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

SVN 主分支合并之通过主分支合并子分支执行流程

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

鼾声识别芯片方案和睡眠产品的应用场景

相关数据显示,中国约有1.5亿人存在睡觉打鼾的情况。随着年龄的增睡觉打鼾人群的占比越高,30岁以上人群中约30%打鼾,40岁以上人群约为40%,而到了65岁以后,这一比例会达到约50%。改善打鼾的几种方式 调整睡眠姿势 通…

Win11 使用 QEMU 虚拟机运行 VC6 的可行性

有些学校或者老师仍然在要求学生必须使用 VC6 来学习 C 语言基础。但是现在的 Win11 已经逐渐开始停止支持 VC6 这个上古时期的 IDE 的运行。 有的时候不是要建议学生换用现代的 IDE 来学习,而是学校或者老师要求学生…

20232415 2025-2026-1 《网络与系统攻防技术》实验三实验报告

一、实验目的 1.利用msfvenom生成多种类型的文件及其多次编码版本 2.利用veil生成恶意代码 3.利用C语言结合shellcode生成恶意文件 4.利用upx压缩壳以及hyp加密壳技术实现免杀 5.开启杀软后回连实测 二、实验过程 接下…

2025年工程管理软件公司综合推荐榜:助力建筑行业数字化升级

随着建筑行业数字化进程加速、工程质量监管趋严及项目管理效率需求提升,工程管理软件已从大型建筑企业专属工具,逐步渗透至中小型建筑公司、监理单位、验房机构及各类工程项目现场。2025年,工程管理软件市场规模预计…

2025年离心式喷雾干燥机权威推荐榜单:小型喷雾干燥机/大型喷雾干燥机/离心喷雾干燥机源头厂家精选

随着制药、食品、化工等行业对粉体质量要求不断提升,离心式喷雾干燥机市场呈现专业化、智能化发展趋势。据行业数据统计,2025年中国离心喷雾干燥设备市场规模预计达42亿元,年增长率稳定在12%,其中智能化控制系统渗…

Win11 使用 Copy v86 在线网页运行 VC6 学习 C 语言的可行性

Win11 使用 Copy v86 在线网页运行 VC6 学习 C 语言的可行性 首先,程序员节快乐。有些学校或者老师仍然在要求学生必须使用 VC6 来学习 C 语言基础。但是现在的 Win11 已经逐渐开始停止支持 VC6 这个上古时期的 IDE 的…

现代c++编程体验2

##task1 #代码1 #include "2T.h"2 #include <iostream>3 #include<string>4 5 const std::string T::doc{"a simple class sample"};6 const int T::max_cnt = 999;7 8 int T::cnt =0…

人工智能中的线性代数核心知识(Linear Algebra for AI)- 微积分 - 何苦

人工智能中的线性代数核心知识(Linear Algebra for AI)- 微积分人工智能中的微积分核心知识(Calculus for AI) 微积分 1. 导数(Derivative)描述函数在某一点的瞬时变化率,是AI优化(如梯度下降)和模型参数求解…

Excel高性能异步导出完整方案!

前言 在大型电商系统中,数据导出是一个高频且重要的功能需求。 传统的同步导出方式在面对大数据量时往往会导致请求超时、内存溢出等问题,严重影响用户体验。 苏三商城项目创新性地设计并实现了一套完整的Excel异步导…

化繁为简:解密国标GB28181算法算力平台EasyGBS如何以兼容性与易用性赋能安防集成

化繁为简:解密国标GB28181算法算力平台EasyGBS如何以兼容性与易用性赋能安防集成引言:国标协议的“理想”与“现实” 国标GB28181协议作为中国安防领域的通用语言,其初衷是为了解决不同品牌、不同系统之间的互联互通…