Modifying Environment on Server is Not Replicating to Clients even with NetMulticast

I’m building a co-op game and I’m trying to ensure that there are some tasks in the game that are achievable together. One of those tasks is lighting a fire. However, I’m having problems replicating the fire to the clients.

When a character goes to use an item. The particle effect doesn’t spawn on his screen or any of the other clients.

However, it is spawning on what I’m assuming is the server since it’s the main window in the Unreal Engine editor.

//ShooterCharacter.h

UFUNCTION(reliable, server, WithValidation)
void UseItem(); 

//ShooterCharacter.cpp

void AShooterCharacter::UseItem_Implementation()
{
	if (Role < ROLE_Authority)
	{
		return;
	}

	//Code removed that gets the overlapping actors and loops through them

		AUsableItem* const TestPickup = Cast<AUsableItem>(CollectedActors[iCollected]);
	        if (TestPickup && !TestPickup->isActive)
		{
			TestPickup->isActive = true;
			TestPickup->UseTheItem();
		}
	}
}

//HiddenFire.h

class SHOOTERGAME_API AHiddenFire : public AUsableItem
.
.
.

public:
  virtual void UseTheItem() override;

  UPROPERTY(EditDefaultsOnly, Category = "Particle System")
  UParticleSystem* FireSpawnFX;

//HiddenFire.cpp

void AHiddenFire::UseTheItem()
{
      //This code also does not work. Validated its not just particle effects. 
		//FVector NewLocation = this->GetActorLocation() + FVector(0, 0, 300.0f);
		//this->SetActorLocation(NewLocation);

		UGameplayStatics::SpawnEmitterAtLocation(this, FireSpawnFX, GetActorLocation(), GetActorRotation());
	}
}

I have also tried multicast. I set the UseItem to Netmulticast and then removed the if (Role < ROLE_Authority) code from the UseItem_Implementation and it worked on just the client that called it. If I left the if (Role < ROLE_Authority) in there, it was never called at all and spawned on no clients.

Any help would be GREATLY appreciated. Even pointing me to a particular location in an example project would be helpful. Thank you for your time.

You’ll need to make sure that it’s only clients trying to SpawnEmitterAtLocation, as the server doesn’t care about anything graphical.

Secondly, you’ll need to make sure that the actor is relicated, so all the clients have a copy of it.

Personally, I would setup a repnotify to show that the AHiddenItem has been “used”. The absolute most simple way to do it would be to have a bool replicated variable. Make sure it is the server setting this as true (or false) and then in the OnRep_ function, use the SpawnEmitterAtLocation.

You also have the case of if the hosted game is as a ListenServer, where that would also need to use SpawnEmitterAtLocation, but only in the case of the hosting “client” / listen server. You will only need to do this if you plan on having players hosting games and not only using dedicated servers.

Here is some code to begin with:

Make sure you have “OnlineSubsystem” in your MYPROJECT.Build.cs, PublicDependencyModuleNames;

[h]

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "HiddenItem.generated.h"

UCLASS()
class PLAYGROUND_API AHiddenItem : public AActor
{
	GENERATED_BODY()
	
public:	
	AHiddenItem();
	virtual void BeginPlay() override;
	virtual void Tick(float DeltaTime) override;

    // Replicated as notify so clients get event when toggled
    UPROPERTY(BlueprintReadOnly, ReplicatedUsing=OnRep_Unlocked, Category = "Hidden Actor")
    bool bUnlocked;

    // The particle system to spawn 
    UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Hidden Actor")
    class UParticleSystem *UnlockedSystemTemplate;
    
    // The collider used to know when a character has come close to the hidden actor
    UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "Hidden Actor")
    class UBoxComponent *CollisionBox;
    
    // Scene component to use as a spawn location instead of relying on GetActorLocation( ) + Offset
    UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "Hidden Actor")
    class USceneComponent *SpawnLocation;   
    
private:
    
    // ParticleSystemComponent to keep reference of what SpawnEmitterAtLocation created
    UPROPERTY( )
    class UParticleSystemComponent *UnlockedSystemComponent;
    
    // RepNotify function to know if bUnlocked has been set by server
    UFUNCTION( )
    void OnRep_Unlocked( );
    
    // Function to call to create particle system
    UFUNCTION( )
    void ToggleUnlockedSystem( );
	
    // Overlap bind for CollisionBox
    UFUNCTION( )
    void OnBeginOvlerlap(UPrimitiveComponent *OverlappedComponent, 
                         AActor *OtherActor, 
                         UPrimitiveComponent *OtherComp, 
                         int32 OtherBodyIndex, 
                         bool bFromSweep, 
                         const FHitResult &SweepResult);
	
};

[.cpp]

#include "HiddenItem.h"

// Includes for all the types we are using
#include "UnrealNetwork.h"
#include "Components/BoxComponent.h"
#include "Particles/ParticleSystem.h"
#include "GameFramework/Character.h"
#include "Particles/ParticleSystemComponent.h"
#include "Kismet/GameplayStatics.h"
#include "Components/SceneComponent.h"

AHiddenItem::AHiddenItem()
{
    // Probably not needed but default
	PrimaryActorTick.bCanEverTick = true;

    // Create box and bind overlap / set default size and set as root
    CollisionBox = CreateDefaultSubobject<UBoxComponent>(TEXT("Box Collider"));
    CollisionBox->SetBoxExtent(FVector(128.f, 128.f, 128.f));
    CollisionBox->OnComponentBeginOverlap.AddDynamic(this, &AHiddenItem::OnBeginOvlerlap);
    SetRootComponent(CollisionBox);
    
    // Child component to use as location to create particle system
    SpawnLocation = CreateDefaultSubobject<USceneComponent>(TEXT("Spawn Location"));
    SpawnLocation->SetupAttachment(CollisionBox);
    
    // Replicated so other clients get copy
    bReplicates = true;
}

void AHiddenItem::BeginPlay()
{
	Super::BeginPlay();
	
}

void AHiddenItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

void AHiddenItem::GetLifetimeReplicatedProps(TArray<FLifetimeProperty> &OutLifetimeProps) const
{ 
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    
    // set bUnlocked to replicate
    DOREPLIFETIME(AHiddenItem, bUnlocked); 
}

void AHiddenItem::OnRep_Unlocked( )
{
    ToggleUnlockedSystem( );
}

void AHiddenItem::ToggleUnlockedSystem( )
{
    UWorld *World = GetWorld( );
    
    // Check for world
    if(World)
    {
        // If bUnlocked got set to true
        if(bUnlocked)
        {
            UnlockedSystemComponent = UGameplayStatics::SpawnEmitterAtLocation(World, UnlockedSystemTemplate,
                                                                               SpawnLocation->GetComponentLocation( ),
                                                                               SpawnLocation->GetComponentRotation( ),
                                                                               true);
        }
        else
        {
            // If bUnlocked got set to false
            if(UnlockedSystemComponent)
            {
                UnlockedSystemComponent->Deactivate( );
                UnlockedSystemComponent = nullptr;
            }
        }
    }   
}

void AHiddenItem::OnBeginOvlerlap(UPrimitiveComponent *OverlappedComponent, 
                                  AActor *OtherActor, 
                                  UPrimitiveComponent *OtherComp, 
                                  int32 OtherBodyIndex, 
                                  bool bFromSweep, 
                                  const FHitResult &SweepResult)
{
    // Overlap only on authority
    if(Role == ROLE_Authority)
    {
        ACharacter *OverlappingCharacter = Cast<ACharacter>(OtherActor);
        
        // Check if character overlapping
        if(OverlappingCharacter)
        {
            // If not set true to bUnlocked
            if(!bUnlocked)
            {
                // Set true
                bUnlocked = true;   
                
                // Handle listen server case
                if(GetNetMode() == ENetMode::NM_ListenServer)
                {
                    ToggleUnlockedSystem( );  
                }
            }
        }
    }
}

Thank you very much. This was extremely helpful. I was able to get it working with a few tweaks.