Replication
客户端与服务端同步数据和procedure call的过程
Client-Server Model
Note: 数据在服务器和客户端之间传输,无法在客户端之间直传
- 服务器负责管理游戏状态 // 行为树只在服务器运行
- 客户端从服务器接收数据(Packets)并更新状态
Actor Replication
Actor将采用以下两种方式更新:
- Property updates - 自动把更新的变量从服务器发送到客户端
- 例子: 玩家血量降低,数据从服务器发往所有的客户端
- RPCs (Remote Procedure Calls) - 其他客户端执行了某个函数
- 例子: 玩家左键攻击,从客户端发RPC到服务器
- 例子: 玩家左键攻击,从客户端发RPC到服务器
Property Replication
变量从服务器同步到客户端
RPC
- Server RPC - 客户端Call,请求服务器执行函数
- Client RPC - 被服务器Call,去某个特定客户端上执行
- NetMulticast RPC - 被服务器Call,所有客户端上执行
Steps to Implement Multiplayer
- 游玩现有的游戏并观察客户端和服务器之间game state的不同
- 将Actor或Component标记为Replicated
- 将需要同步的变量标记为Replicated
- 在代码间添加RPCs
Tips
- Function Specifier
- Reliable - 保证最终到达. 请求会被重发直到收到ack
- Unreliable - 不保证到达
- Variable Specifier
- Replicated - 会被ClientRPC同步
- ReplicatedUsing = “OnRep_” - RepNotify当变量发生改变时的回调函数
- 使用该通知时还需在服务器手动叫一次RepNotify
- 使用该通知时还需在服务器手动叫一次RepNotify
项目代码
GitHub: https://github.com/yufeige4/ActionRoguelike
- 将交互组件修复为支持多人游戏
// GInteractionComponent.h
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class ACTIONROGUELIKE_API UGInteractionComponent : public UActorComponent
{
GENERATED_BODY()
protected:
UPROPERTY(EditDefaultsOnly, Category = "Trace")
float TraceDistance;
UPROPERTY(EditDefaultsOnly, Category = "Trace")
float TraceRadius;
UPROPERTY(EditDefaultsOnly, Category = "Trace")
TEnumAsByte<ECollisionChannel> CollisionChannel;
UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category = "Interact")
AActor* FocusedActor;
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf<UGUserWidget_World> HintWidgetClass;
UPROPERTY()
UGUserWidget_World* HintWidgetInstance;
public:
// Sets default values for this component's properties
UGInteractionComponent();
void PrimaryInteract();
protected:
UFUNCTION(Server, Unreliable)
void ServerInteract(AActor* InFocusedActor);
// Called when the game starts
virtual void BeginPlay() override;
void FindBestInteractable();
public:
// Called every frame
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
};
// GInteractionComponent.cpp
static TAutoConsoleVariable<bool> CVarInteractionDrawDebug(TEXT("ARPG.InteractDrawDebug"),false,TEXT("toggle whether draw debug info for InteractionComp"),ECVF_Cheat);
// Sets default values for this component's properties
UGInteractionComponent::UGInteractionComponent()
{
// Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features
// off to improve performance if you don't need them.
PrimaryComponentTick.bCanEverTick = true;
TraceDistance = 500.0f;
TraceRadius = 30.0f;
CollisionChannel = ECC_WorldDynamic;
// ...
}
// Called when the game starts
void UGInteractionComponent::BeginPlay()
{
Super::BeginPlay();
// ...
}
void UGInteractionComponent::FindBestInteractable()
{
FCollisionObjectQueryParams ObjectQueryParams;
ObjectQueryParams.AddObjectTypesToQuery(CollisionChannel);
FVector End;
AActor* MyOwner = GetOwner();
FVector CameraLocation;
FRotator CameraRotation;
AGCharacter* MyCharacter = Cast<AGCharacter>(MyOwner);
MyCharacter->GetCameraViewPoint(CameraLocation,CameraRotation);
End = CameraLocation + (CameraRotation.Vector()*TraceDistance);
FCollisionShape Shape;
Shape.SetSphere(TraceRadius);
TArray<FHitResult> Hits;
bool bBlockingHit = GetWorld()->SweepMultiByObjectType(Hits,CameraLocation,End,FQuat::Identity,ObjectQueryParams,Shape);
// Debug color
FColor LineColor = bBlockingHit ? FColor::Green : FColor::Red;
bool bDebugDraw = CVarInteractionDrawDebug.GetValueOnGameThread();
// 清空之前保存的Focus并重新搜索
FocusedActor = nullptr;
for(FHitResult Hit : Hits)
{
AActor* HitActor = Hit.GetActor();
if(HitActor!=nullptr)
{
// 如果该Actor实现了这个接口
if(HitActor->Implements<UGGameplayInterface>())
{
FocusedActor = HitActor;
break;
}
}
// Debug Purpose
if(bDebugDraw)
{
DrawDebugSphere(GetWorld(),Hit.ImpactPoint,TraceRadius,32,LineColor,false,2.0f);
}
}
if(FocusedActor)
{
// 若当前未实例化过并且widget类被指定
if(!HintWidgetInstance && ensure(HintWidgetClass))
{
HintWidgetInstance = CreateWidget<UGUserWidget_World>(GetWorld(),HintWidgetClass);
}
if(HintWidgetInstance)
{
// attach到相应的actor上, 如果未在视口中则添加到视口
HintWidgetInstance->AttachedActor = FocusedActor;
if(!HintWidgetInstance->IsInViewport())
{
HintWidgetInstance->AddToViewport();
}
}
}else
{
// 交互物超出范围, 提示消失
if(HintWidgetInstance && HintWidgetInstance->IsInViewport())
{
HintWidgetInstance->RemoveFromParent();
}
}
// Debug purpose
if(bDebugDraw)
{
DrawDebugLine(GetWorld(),CameraLocation,End,LineColor,false,2.0f,0,2.0f);
}
}
// Called every fame
void UGInteractionComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
auto MyPawn = Cast<APawn>(GetOwner());
if(MyPawn->IsLocallyControlled())
{
FindBestInteractable();
}
}
void UGInteractionComponent::PrimaryInteract()
{
ServerInteract(FocusedActor);
}
void UGInteractionComponent::ServerInteract_Implementation(AActor* InFocusedActor)
{
// refactored, move tracing logic into tick
if(InFocusedActor==nullptr)
{
if(CVarInteractionDrawDebug.GetValueOnGameThread())
{
GEngine->AddOnScreenDebugMessage(-1,1.0f,FColor::Red,"No FocusedActor to Interact");
}
return;
}
APawn* MyPawn = Cast<APawn>(GetOwner());
// 调用接口
IGGameplayInterface::Execute_Interact(InFocusedActor,MyPawn);
}
- 箱子实现了简单的服务器客户端同步
// AGItemChest.h
UCLASS()
class ACTIONROGUELIKE_API AGItemChest : public AActor, public IGGameplayInterface
{
GENERATED_BODY()
protected:
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* BaseMesh;
UPROPERTY(VisibleAnywhere,BlueprintReadOnly)
UStaticMeshComponent* LidMesh;
UPROPERTY(ReplicatedUsing = "OnRep_LidOpened") // RepNotify
bool bLidOpened;
public:
UPROPERTY(EditAnywhere)
float TargetRoll;
void Interact_Implementation(APawn* InstigatorPawn);
// Sets default values for this actor's properties
AGItemChest();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
UFUNCTION() // Need to mark UFUNCTION for Unreal
void OnRep_LidOpened();
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
};
// AGItemChest.cpp
AGItemChest::AGItemChest()
{
// 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;
BaseMesh = CreateDefaultSubobject<UStaticMeshComponent>("BaseMesh");
RootComponent = BaseMesh;
LidMesh = CreateDefaultSubobject<UStaticMeshComponent>("LidMesh");
LidMesh->SetupAttachment(BaseMesh);
TargetRoll = -110.0f;
SetReplicates(true);
}
// Called when the game starts or when spawned
void AGItemChest::BeginPlay()
{
Super::BeginPlay();
}
void AGItemChest::OnRep_LidOpened()
{
float CurrPitch = bLidOpened ? TargetRoll : 0.0f;
LidMesh->SetRelativeRotation(FRotator(0,0,CurrPitch));
}
// Called every frame
void AGItemChest::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void AGItemChest::Interact_Implementation(APawn* InstigatorPawn)
{
bLidOpened = !bLidOpened;
OnRep_LidOpened();
}
// key function generated by UHT for replication rules
void AGItemChest::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 所有客户端同步bLidOpened变量
DOREPLIFETIME(AGItemChest,bLidOpened);
}