Replication Hell - the gift that keeps on giving

Right about the time I started thinking I was getting the hang of Unreal and replication in general, I ran into an issue that I have not been able to overcome.

When spawning a new actor, I’m unable to get replication to the clients. I’ve spent a lot of time on this and just can’t figure it out.

PickupBase - this class is the base class for all future pickup classes in my game

RiflePickup - this class is a weapon pickup class derived from PickupBase

ShooterCPPGameMode - this is my Game Mode class and where I am spawning my pickups for now.

The spawning of my weapon pickups (RiflePickup) is working fine on the server, but it won’t replicate to the clients.

APickupBase.h


UENUM(BlueprintType)        
enum class EWeapon : uint8
{
    EW_AK47     UMETA(DisplayName = "AK-47"),
    EW_AR15     UMETA(DisplayName = "AR-15"),
    EW_SMG11    UMETA(DisplayName = "SMG-11"),
    EW_ASVAL    UMETA(DisplayName = "AS VAL"),
    EW_SUB        UMETA(DisplayName = "Sub")
};

UCLASS()
class SHOOTERCPP_API APickupBase : public AActor
{
    GENERATED_BODY()

public:    
    // Sets default values for this actor's properties
    APickupBase();

    virtual void Initialize(EWeapon weapon);


protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;    

    UPROPERTY(Replicated)
    UStaticMeshComponent* weaponMeshComponent;
    UPROPERTY(Replicated)
    UBoxComponent* boxComponent;    

public:    
    // Called every frame
    virtual void Tick(float DeltaTime) override;

private:    

};

APickupBase.cpp



#include "PickupBase.h"
#include "ConstructorHelpers.h"
#include "Components/BoxComponent.h"
#include "Components/StaticMeshComponent.h"
#include "UnrealNetwork.h"

// Sets default values
APickupBase::APickupBase()
{
    // 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;
    bReplicates = true;
    bReplicateMovement = true;

    // Every weapon in the game will have a box collider, static mesh and an actual type to spawn for use
    boxComponent = CreateDefaultSubobject<UBoxComponent>(TEXT("Box Comp"));
    boxComponent->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);

    weaponMeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Weapon Mesh Comp"));
    weaponMeshComponent->SetupAttachment(boxComponent);    

    boxComponent->SetWorldScale3D(FVector(1.f, 1.0f, 1.0f));
    boxComponent->SetBoxExtent(FVector(60.f, 32.f, 32.f));    
}

void APickupBase::Initialize(EWeapon weapon)
{
    // nothing to do here in the base class
}

// Called when the game starts or when spawned
void APickupBase::BeginPlay()
{
    Super::BeginPlay();
}

// Called every frame
void APickupBase::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
}

void APickupBase::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    DOREPLIFETIME(APickupBase, weaponMeshComponent);
    DOREPLIFETIME(APickupBase, boxComponent);
}

ARiflePickup.h (notice this is derived from APickupBase)


#include "CoreMinimal.h"
#include "PickupBase.h"
#include "RiflePickup.generated.h"

/**
 * 
 */
UCLASS()
class SHOOTERCPP_API ARiflePickup : public APickupBase
{
    GENERATED_BODY()

public:
    ARiflePickup();

    virtual void Initialize(EWeapon weapon) override;

protected:

    // Weapon meshes
    UStaticMesh* ak47Mesh;
    UStaticMesh* ar15Mesh;
    UStaticMesh* kaValMesh;
    UStaticMesh* smg11Mesh;
    UStaticMesh* ak47uMesh;

};

ARiflePickup.cpp (notice this is derived from APickupBase)


#include "RiflePickup.h"
#include "ConstructorHelpers.h"
#include "Components/BoxComponent.h"
#include "Components/StaticMeshComponent.h"
#include "UnrealNetwork.h"

ARiflePickup::ARiflePickup()
{
    bReplicates = true;
    bReplicateMovement = true;

    static ConstructorHelpers::FObjectFinder<UStaticMesh> ak47Asset(TEXT("/Game/FPS_Weapon_Bundle/Weapons/Meshes/Ka47/SM_KA47_X"));
    ak47Mesh = ak47Asset.Object;

    static ConstructorHelpers::FObjectFinder<UStaticMesh> ar15Asset(TEXT("/Game/FPS_Weapon_Bundle/Weapons/Meshes/AR4/SM_AR4_X"));
    ar15Mesh = ar15Asset.Object;

    static ConstructorHelpers::FObjectFinder<UStaticMesh> kaValAsset(TEXT("/Game/FPS_Weapon_Bundle/Weapons/Meshes/Ka_Val/SM_KA_Val_X"));
    kaValMesh = kaValAsset.Object;

    static ConstructorHelpers::FObjectFinder<UStaticMesh> smg11Asset(TEXT("/Game/FPS_Weapon_Bundle/Weapons/Meshes/SMG11/SM_SMG11_X"));
    smg11Mesh = smg11Asset.Object;

    static ConstructorHelpers::FObjectFinder<UStaticMesh> ak47uAsset(TEXT("/Game/FPS_Weapon_Bundle/Weapons/Meshes/KA74U/SM_KA74U_X"));
    ak47uMesh = ak47uAsset.Object;
}

void ARiflePickup::Initialize(EWeapon weapon)
{
    FVector meshOffset;
    UStaticMesh* weaponMesh;

    switch (weapon)
    {
    case EWeapon::EW_AK47:
    {
        weaponMesh = ak47Mesh;
        meshOffset = FVector(-15.f, 0.f, 0.f);
        break;
    }
    case EWeapon::EW_AR15:
    {
        weaponMesh = ar15Mesh;
        meshOffset = FVector(-15.f, 0.f, 0.f);
        break;
    }
    case EWeapon::EW_ASVAL:
    {
        weaponMesh = kaValMesh;
        meshOffset = FVector(-15.f, 0.f, 0.f);
        break;
    }
    case EWeapon::EW_SMG11:
    {
        weaponMesh = smg11Mesh;
        meshOffset = FVector(-15.f, 0.f, 0.f);
        break;
    }
    case EWeapon::EW_SUB:
    {
        weaponMesh = ak47uMesh;
        meshOffset = FVector(-15.f, 0.f, 0.f);
        break;
    }
    default:
    {
        weaponMesh = ak47Mesh;
        meshOffset = FVector(-15.f, 0.f, 0.f);
        break;
    }

    }
    if (weaponMeshComponent)
    {
        weaponMeshComponent->SetStaticMesh(weaponMesh);
        weaponMeshComponent->SetRelativeLocation(FVector(ForceInitToZero));
        weaponMeshComponent->SetRelativeLocation(meshOffset);
        weaponMeshComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision);
    }
}

AShooterCPPGameMode.h


#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "PickupBase.h"

#include "ShooterCPPGameMode.generated.h"


UCLASS(minimalapi)
class AShooterCPPGameMode : public AGameModeBase
{
    GENERATED_BODY()

public:
    AShooterCPPGameMode();
    virtual void BeginPlay() override;

protected:

    UFUNCTION(Server, Reliable, WithValidation)
    void SpawnWeaponToWorld(EWeapon weapon, FVector location, FRotator rotation);
};

AShooterCPPGameMode.cpp


#include "ShooterCPPGameMode.h"
#include "ShooterCPPCharacter.h"
#include "UObject/ConstructorHelpers.h"
#include "PickupBase.h"
#include "RiflePickup.h"
#include "PickupBase.h"
#include "Engine/World.h"



AShooterCPPGameMode::AShooterCPPGameMode()
{
    // set default pawn class to our Blueprinted character
    static ConstructorHelpers::FClassFinder<APawn> PlayerPawnBPClass(TEXT("/Game/ThirdPersonCPP/Blueprints/ThirdPersonCharacter"));
    if (PlayerPawnBPClass.Class != NULL)
    {
        DefaultPawnClass = PlayerPawnBPClass.Class;
    }
}

void AShooterCPPGameMode::BeginPlay()
{
    Super::BeginPlay();

    UWorld* world = GetWorld();

    if (world)
    {
        FVector location = FVector(-1000.f, -1000.f, 200.f);
        FRotator rotation = FRotator::ZeroRotator;

        // Spawning a single weapon to the works works on the server, but is not displayed on the client

        SpawnWeaponToWorld(EWeapon::EW_AK47, location, rotation);
        ARiflePickup* temp =  (ARiflePickup*) GetWorld()->SpawnActor<APickupBase>(ARiflePickup::StaticClass(), location, rotation);
        temp->Initialize(EWeapon::EW_AK47);

    }
}

bool AShooterCPPGameMode::SpawnWeaponToWorld_Validate(EWeapon weapon, FVector location, FRotator rotation)
{
    return true;    
}

void AShooterCPPGameMode::SpawnWeaponToWorld_Implementation(EWeapon weapon, FVector location, FRotator rotation)
{
    ARiflePickup* temp = (ARiflePickup*)GetWorld()->SpawnActor<APickupBase>(ARiflePickup::StaticClass(), location, rotation);
    bool test = temp->HasAuthority();
    temp->Initialize(EWeapon::EW_AK47);
}

So, in my GameMode class, I’m spawning a single pickup. It works perfectly on the server, but will not propigate to the client.

Can someone please tell me what I’m doing wrong?

Thanks in advance!

APickupItem::APickupItem(const FObjectInitializer & ObjectInitializer)
: Super(ObjectInitializer)
{
Mesh = ObjectInitializer.CreateDefaultSubobject<USkeletalMeshComponent>(this, TEXT(“Mesh”));
RootComponent = Mesh;
Mesh->SetCollisionProfileName(FName(TEXT(“PhysicsActor”)));
Mesh->SetCollisionResponseToAllChannels(ECR_Ignore);
Mesh->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Block);
Mesh->SetCollisionResponseToChannel(ECC_GameTraceChannel1, ECR_Block);
Mesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
Mesh->CanCharacterStepUpOn = ECanBeCharacterBase::ECB_No;
Mesh->bEnablePhysicsOnDedicatedServer = true;
Mesh->SetSimulatePhysics(true);
Mesh->CanCharacterStepUpOn = ECanBeCharacterBase::ECB_No;

WeaponComponent = ObjectInitializer.CreateOptionalDefaultSubobject&lt;UPickupComponent&gt;(this, TEXT("New_weapon"));

WeapontComponent->SetupAttachment(Mesh, FName(TEXT(“Gun_Root”)));

bReplicates = true;
bAlwaysRelevant = true;
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.TickInterval = 0.5f;

What I have on my pickup item. It replicates on both client and server. Maybe replicating location also

void APickupItem::GetLifetimeReplicatedProps(TArray<FLifetimeProperty> & OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);

DOREPLIFETIME(APickupItem, Instigator);
DOREPLIFETIME(APickupItem, ReplicatedLocation);

You need to add

SetRemoteRoleForBackwardsCompat(ROLE_SimulatedProxy);

To your constructor otherwise the default RemoteRole will be ROLE_None

You don’t seem to call the superclass. Might be something?

Thanks for all the suggestions. I’ve now tried them all but still don’t have it working. The base constructor was automatically being called because it was the default constructor. However, I verified this was not the issue. I tried forcing the role in each constructor, but that did not make any difference.

I’m running out of ideas. I’ll probably remove the class hierarchy out of the equation and just create one class for testing purposes. Other than that, I have no idea.

I’m stumped.

Is the root component ever set?

I don’t see it in your code, though @thadkinsjr posted it in theirs.

Without a valid root component on an actor that replicates, you would need to make sure bAlwaysRelevant is true.
This is what the GameStateBase and similar actors do.

A root component is also important to ensure the actors is replicated to the correct location.
Otherwise, it will always end up at the origin of the world (0,0,0).

You may notice that in various native C++ classes, a dummy transform component is created and set in the constructor.

e.g. Controller.cpp


AController::AController(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
{
...
    TransformComponent = CreateDefaultSubobject<USceneComponent>(TEXT("TransformComponent0"));
    RootComponent = TransformComponent;

Hi Kris,

I modified my base class to set the RootComponent:


APickupBase::APickupBase()
{
    // 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;
    bReplicates = true;
    bReplicateMovement = true;

    RootComponent = boxComponent;

    SetRemoteRoleForBackwardsCompat(ROLE_SimulatedProxy);

Unfortunately it made no difference. Any other ideas?

How about adding SetReplicates(true) in the PickupBase constructor



 // Sets default values
APickupBase::APickupBase()
{    
     //Whatever there is      
      SetReplicates(true);    
}


If you see Actor class, there is one routine entirely dedicated for setting the replication environment. If that doesn’t work, try browsing the UT Code https://github.com/EpicGames/UnrealT…e/UTPickup.cpp. Good luck!

Yeah, I tried SetReplicates(true) in my base and derived classes and it made no difference.

One thing I just noticed that is once all my pickups are spawned and visible by the server, if I Eject the camera from the player, all the assets in scene disappear (visibly). I’m guessing this is because the ejected camera just becomes a new client and can only see what is being replicated? Thought I would mention it in case it’s a clue.

Try taking the UTPickup class instead of your class APickupBase and inheriting that for testing purpose. That is the next natural step I can think of. If you still can’t work it out, the problem is something else, because rest assured, I have played UT online (windows) and I certainly have picked the pickups!

A few pointers (heh):

  • Your ‘Initialize’ function will not run on the Client, and it is terrible design to set weapon properties like this anyway. You should create blueprint sub-classes of the pickup, and set the default meshes there. You then spawn the subclasses, not class-default objects.
  • Having a UFUNCTION marked with ‘Server’ and ‘Reliable’ in the Game Mode class makes no sense. The GameMode only exists on the Server so clients can’t call it anyway, and the server is running that code locally. Just use a normal function to spawn the weapon. You ONLY spawn the actor on the Server.
  • C-Style casts are dangerous and unneccesary. You’re already casting the weapon with SpawnActor so the additional (ARiflePickup*) before it isn’t needed.
  • You do not need to replicate the actor components - they are part of the class-default object and the client knows what components an actor has. Marking components as replicated propertied in GetLifetimeReplicatedProps makes no sense and isn’t needed.
  • You’re spawning an ARiflePickup::StaticClass() - so you’re spawning the most basic C++ version of the actor with no meshes, or visible properties to speak of. How do you know where it is on the Client -OR- Server if you can’t see it? The Initialize() function isn’t going to run client side.
  • Setting the RemoteRole will do absolutely nothing.
  • You’re spawning Two pickups on the Server in BeginPlay() - one from the Server function call, another immediatelly after it.
  • Instigator and ReplicatedLocation also aren’t needed - Instigator replicates anyway as part of the Super call, and ReplicatedLocation is pointless since ReplicatedMovement will override it anyway.

It’s clear to me why you can’t see the actor on the client - there’s nothing to see because the Initialize() function won’t run there how is the client supposed to know to run that, or what weapon class it is? Create blueprint sub-classes of the class and spawn those instead, and get rid of that Initialize() awfulness.

EDIT: Bullet-point formatting on this forum is awful.

3 Likes

@TheJamsh I just implemented ever single bullet-point you listed and everything is working perfectly now. I guess I was trying to make it too complicated by doing EVERYTHING in C++. I now see the value in creating the final derived class as a blueprint and set the default mesh (and other vars) within the BP - then just spawn when needed. Seems simple now!

I also got rid of my Initialize() function that you were so fond of :slight_smile: since each weapon type now has its own blueprint to spawn - removed the need to set defaults like I was trying to do.

Thanks for the detailed response - you got me on the right track and I learned a lot while doing it.

Do you mind sharing the right code!

No problem. It will take me a bit to get it all together, but I’ll get it posted a bit later today.

Here is the working and much simplified code. A few things of note:

The RiflePickup class is basically empty now. All the default settings (collider and mesh) are still in the base class and I have no special features written yet for the rifle specifically. What is not shown below are 4 blueprint classes - one for each weapon in my game. This is where I set the default mesh and collider. In the GameMode class, I then spawn each BP and since the base class is marked for replication, it just works.

PickupBase.h


UCLASS()
class SHOOTERCPP_API APickupBase : public AActor
{
    GENERATED_BODY()

public:    
    // Sets default values for this actor's properties
    APickupBase();

    // the item to be spawned from this pickup
    UPROPERTY(EditDefaultsOnly, Category = WeaponToSpawn)
    TSubclassOf<AActualBase> weaponToSpawn;    

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;    

    UPROPERTY(BlueprintReadWrite, VisibleAnywhere)
    UStaticMeshComponent* weaponMeshComponent;

    UPROPERTY(BlueprintReadWrite, VisibleAnywhere)
    UBoxComponent* boxComponent;    

public:    
    // Called every frame
    virtual void Tick(float DeltaTime) override;

private:    

};

PickupBase.cpp


APickupBase::APickupBase()
{
    // 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;
    bReplicates = true;    
    bReplicateMovement = true;    

    // Every pickup in the game will have a box collider, static mesh and an actual type to spawn after pickup (set in header)
    boxComponent = CreateDefaultSubobject<UBoxComponent>(TEXT("Box Comp"));
    boxComponent->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);

    weaponMeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Weapon Mesh Comp"));
    weaponMeshComponent->SetupAttachment(boxComponent);    

    boxComponent->SetWorldScale3D(FVector(1.f, 1.0f, 1.0f));
    boxComponent->SetBoxExtent(FVector(60.f, 32.f, 32.f));        
}

// Called when the game starts or when spawned
void APickupBase::BeginPlay()
{
    Super::BeginPlay();
}

// Called every frame
void APickupBase::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
}

RiflePickup.h


UCLASS()
class SHOOTERCPP_API ARiflePickup : public APickupBase
{
    GENERATED_BODY()

public:
    ARiflePickup();


protected:    

};

RiflePickup.cpp


ARiflePickup::ARiflePickup()
{
    bReplicates = true;
    bReplicateMovement = true;        
}

ShooterCPPGameMode.h


UCLASS(minimalapi)
class AShooterCPPGameMode : public AGameModeBase
{
    GENERATED_BODY()

public:
    AShooterCPPGameMode();
    virtual void BeginPlay() override;

protected:

    TSubclassOf<ARiflePickup> weaponAK47;
    TSubclassOf<ARiflePickup> weaponAR15;
    TSubclassOf<ARiflePickup> weaponSMG11;
    TSubclassOf<ARiflePickup> weaponSUB;
};

ShooterCPPGameMode.cpp


AShooterCPPGameMode::AShooterCPPGameMode()
{
    // set default pawn class to our Blueprinted character
    static ConstructorHelpers::FClassFinder<APawn> PlayerPawnBPClass(TEXT("/Game/ThirdPersonCPP/Blueprints/ThirdPersonCharacter"));
    if (PlayerPawnBPClass.Class != NULL)
    {
        DefaultPawnClass = PlayerPawnBPClass.Class;        
    }    

    static ConstructorHelpers::FClassFinder<ARiflePickup> AK47(TEXT("/Game/ThirdPersonCPP/Blueprints/WeaponPickup/Rifle_Pickup_AK47"));
    static ConstructorHelpers::FClassFinder<ARiflePickup> AR15(TEXT("/Game/ThirdPersonCPP/Blueprints/WeaponPickup/Rifle_Pickup_AR15"));
    static ConstructorHelpers::FClassFinder<ARiflePickup> SMG11(TEXT("/Game/ThirdPersonCPP/Blueprints/WeaponPickup/Rifle_Pickup_SMG11"));
    static ConstructorHelpers::FClassFinder<ARiflePickup> SUB(TEXT("/Game/ThirdPersonCPP/Blueprints/WeaponPickup/Rifle_Pickup_SUB"));    

    weaponAK47 = AK47.Class;
    weaponAR15 = AR15.Class;
    weaponSMG11 = SMG11.Class;
    weaponSUB = SUB.Class;    
}

void AShooterCPPGameMode::BeginPlay()
{
    Super::BeginPlay();

    if (GetWorld())
    {
        FVector location = FVector(-1000.f, -1000.f, 200.f);
        FRotator rotation = FRotator::ZeroRotator;        

        ARiflePickup* temp = GetWorld()->SpawnActor<ARiflePickup>(weaponAK47, location, rotation);

        location += FVector(0.0f, 200.f, 0.f);
        ARiflePickup* temp2 = GetWorld()->SpawnActor<ARiflePickup>(weaponAR15, location, rotation);

        location += FVector(0.0f, 200.f, 0.f);
        ARiflePickup* temp3 = GetWorld()->SpawnActor<ARiflePickup>(weaponSMG11, location, rotation);

        location += FVector(0.0f, 200.f, 0.f);
        ARiflePickup* temp4 = GetWorld()->SpawnActor<ARiflePickup>(weaponSUB, location, rotation);        

    }
}

Is this really necessary or you are just playing safe? Shouldn’t you be calling super thingy!

Furthermore, you don’t need to set the replication properties client side actually.

This is not needed and I have since removed it. As long as it’s set in the base class it should work.

As for calling the Base/Master/Super class, I don’t need to explicitly call it since I’m using the default constructor. If I add a new constructor, or add parameters to the existing constructor, I would then need to explicitly call the super.

I don’t understand? I’m setting this here so the derived blueprint is already set by default. Is that what you are asking or am I missing something?

No worries, hope it helps :slight_smile: If in doubt, study the content examples Epic have for C++ - they’re quite good (if a little dated now!)

I would use the Epic’s routine SetReplicates(true) which has a special check for the Authority role. That way this setting doesn’t get applied to the clients!

I mean I trust


void AActor::SetReplicates(bool bInReplicates)
{
    if (Role == ROLE_Authority)
    {
        // Only call into net driver if we actually changed
        if (bReplicates != bInReplicates)
        {
            // Update our settings before calling into net driver
            RemoteRole = bInReplicates ? ROLE_SimulatedProxy : ROLE_None;
            bReplicates = bInReplicates;

            // This actor should already be in the Network Actors List if it was already replicating.
            if (bReplicates)
            {
                // GetWorld will return nullptr on CDO, FYI
                if (UWorld* MyWorld = GetWorld())        
                {
                    MyWorld->AddNetworkActor(this);
                    ForcePropertyCompare();
                }
            }

        }
    }
    else
    {
        UE_LOG(LogActor, Warning, TEXT("SetReplicates called on actor '%s' that is not valid for having its role modified."), *GetName());
    }
}

and not reinventing the wheel (giving joys of OOP)!