Unreal Multiplayer Swapping Material at runtime

I am very new to Unreal, so apologies if I’m making a simple mistake.

I’ve been working on a small multiplayer prototype, where simply a character will run across a pickup, and a bool will be changed on an associated Component, to either true or false, and then that component will swap that characters mesh material to a different material. In the below code, I’ve changed this to simply be triggered by the character itself, through button press.

I’ve tried as many ways as I could find online to do this, and only one is working, but it’s not great. This working method is checking which material is on the character mesh, rather than using the bool (which doesn’t seem to work). Could someone show me what I’m doing wrong for this not to work, and let me know what I need to do to get this working? I also get the feeling that I’m doing this the hard way, so any direction for how to better program multiplayer with Unreal would also be appreciated.

I’ve removed parts of the code which are not related to my question, so to simplify what you can see.

PlayerCharacter.h:

#pragma once    

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "PlayerCharacter.generated.h"

class UStaticMeshComponent;

UCLASS()
class Game_API PlayerCharacter : public ACharacter
{
    GENERATED_BODY()

public:
    APlayerCharacter();

public:
    UPROPERTY(EditAnywhere)
    class USkeletalMeshComponent* CharacterMesh;

protected:
    virtual void BeginPlay() override;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
    class UPlayerShieldComponent* ShieldComp;
    
public:
    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

    UFUNCTION()
    void ToggleShield();

    UFUNCTION(Server, Reliable, WithValidation)
    void Server_ToggleShield();
};

PlayerCharacter.cpp:

#include "Character/PlayerCharacter.h"
#include "GameFramework/Controller.h"
#include "Components/StaticMeshComponent.h"
#include "Components/PlayerShieldComponent.h"
#include "Net/UnrealNetwork.h"

APlayerCharacter::APlayerCharacter()
{
    PrimaryActorTick.bCanEverTick = true;
    CharacterMesh = Cast<USkeletalMeshComponent>(GetDefaultSubobjectByName("CharacterMesh0"));
    ShieldComp = CreateDefaultSubobject<UPlayerShieldComponent>(TEXT("ShieldComp"));
}

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

void APlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);
    PlayerInputComponent->BindAction("Interact", IE_Pressed, this, &APlayerCharacter::ToggleShield);
}

void APlayerCharacter::ToggleShield()
{
    if (GetLocalRole() == ROLE_Authority)
    {
        if (ShieldComp)
        {
            ShieldComp->DoToggleShield();
        }
    }
    else
    {
        Server_ToggleShield();
    }
}

bool APlayerCharacter::Server_ToggleShield_Validate()
{
    return true;
}

void APlayerCharacter::Server_ToggleShield_Implementation()
{
    if (UPlayerShieldComponent* sc = Cast<UPlayerShieldComponent>(GetDefaultSubobjectByName("ShieldComp")))
    {
        sc->DoToggleShield();
    }
}

PlayerShieldComponent.h

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Character/PlayerCharacter.h"
#include "PlayerShieldComponent.generated.h"

UCLASS(ClassGroup = (Game), meta = (BlueprintSpawnableComponent))
class Game_API UPlayerShieldComponent : public UActorComponent
{
    GENERATED_BODY()

public:
    UPlayerShieldComponent();

    UPROPERTY(Replicated, BlueprintReadOnly)
    bool bShieldIsOn;
protected:
    virtual void BeginPlay() override;

    UFUNCTION(NetMulticast, Reliable, WithValidation)
    void Multi_ToggleShield();

public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Materials)
    class UMaterialInstance* OffMaterial;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Materials)
    class UMaterialInstance* OnMaterial;

    void DoToggleShield();
};

PlayerShieldComponent.cpp

#include "Components/PlayerShieldComponent.h"
#include "Net/UnrealNetwork.h"
#include "Components/StaticMeshComponent.h"
#include "Character/PlayerCharacter.h"

// Sets default values for this component's properties
UPlayerShieldComponent::UPlayerShieldComponent()
{
    PrimaryComponentTick.bCanEverTick = false;

    OnMaterial = CreateDefaultSubobject<UMaterialInstance>(TEXT("On Material"));
    OffMaterial = CreateDefaultSubobject<UMaterialInstance>(TEXT("Off Material"));
        
    bShieldIsOn = false;
}

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

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

    DOREPLIFETIME(UPlayerShieldComponent, bShieldIsOn);
}

void UPlayerShieldComponent::DoToggleShield()
{
    if (GetOwnerRole() == ROLE_Authority)
    {
        bShieldIsOn = !bShieldIsOn;
        Multi_ToggleShield();   // From the server.
    }
}

void UPlayerShieldComponent::Multi_ToggleShield_Implementation()
{
    if (APlayerCharacter* character = Cast<APlayerCharacter>(GetOwner()))
    {
        if (USkeletalMeshComponent* cm = Cast<USkeletalMeshComponent>(character->GetDefaultSubobjectByName("CharacterMesh0")))
        {
            GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, character->GetFName().ToString());

            if (cm->GetMaterial(0) == OnMaterial)
            {
                cm->SetMaterial(0, OffMaterial);
            }
            else
            {
                cm->SetMaterial(0, OnMaterial);
            }
        }
    }
}

bool UPlayerShieldComponent::Multi_ToggleShield_Validate()
{
    return true;
}

I am assuming you want to Turn on/off Shield depending on value of bool.
I wouldn’t use multicast to change shield material.
I would use OnRep function to change shield material whenever on client Bool changes.

in header

UPROPERTY(ReplicatedUsing=OnRep_ShieldIsOn, BlueprintReadOnly)
     bool bShieldIsOn;

UFUNCTION()
void OnRep_ShieldIsOn()

in cpp

void UPlayerShieldComponent::OnRep_ShieldIsOn()
{
         this->ChangeShieldMaterial(bShieldIsOn);
}

 void UPlayerShieldComponent::DoToggleShield()
 {
     if (GetOwnerRole() == ROLE_Authority)
     {
         bShieldIsOn = !bShieldIsOn;
         OnRep_ShieldIsOn() // OnRep only fired on client automatically so we will call it on server manually
     }
 }


void UPlayerShieldComponent::ChangeShieldMaterial(bShieldIsOn)
 {
     if (APlayerCharacter* character = Cast<APlayerCharacter>(GetOwner()))
     {
         if (USkeletalMeshComponent* cm = Cast<USkeletalMeshComponent>(character->GetDefaultSubobjectByName("CharacterMesh0")))
         {
             GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, character->GetFName().ToString());
 
             if (!bShieldIsOn)
             {
                 cm->SetMaterial(0, OffMaterial);
             }
             else
             {
                 cm->SetMaterial(0, OnMaterial);
             }
         }
     }
 }

I hope it helps

Thank you for your reply. I actually tried this approach previously, but didn’t realise it was working with the bool, which is good! The only thing is, it doesn’t swap the material using this set up? I have two Clients running on start up, and if either pick up a shield, the function is running (and I see the Debug message), but the material doesn’t change. I’m wondering why that might be?

I’m using UMaterialInstance for both OnMaterial and OffMaterial. They have references associated, and I can see them/ set them in the editor. Do I need to replicate the materials? I was considering they should be somewhere else anyway (so that all characters are pointing at the same references?).

334019-materials.png

OnMaterial = CreateDefaultSubobject(TEXT(“On Material”));
OffMaterial = CreateDefaultSubobject(TEXT(“Off Material”));

you dont need this in constructor

this must be overriding your material pointers.

That was the issue! :smiley: The component wasn’t marked to replicate, such a silly mistake. Thank you!! :slight_smile:

Thank you for your help so far, I have really appreciated it.

I removed the lines in the constructor, but still not changing. I added some debug calls, to check the material name when it was attached, and even though the material appears to be correct, it doesn’t show visually.

334100-shield-material.png

void UPlayerShieldComponent::ChangeShieldMaterial(bool shieldIsOn)
{
	if (APlayerCharacter* character = Cast<APlayerCharacter>(GetOwner()))
	{
		if (USkeletalMeshComponent* cm = Cast<USkeletalMeshComponent>(character->GetDefaultSubobjectByName("CharacterMesh0")))
		{
			GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Blue, character->GetFName().ToString());

			if (!bShieldIsOn)
			{				
				cm->SetMaterial(0, OffMaterial);
				GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, "OFF");
			}
			else
			{
				cm->SetMaterial(0, OnMaterial);
				GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Green, "ON");
			}
			GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Yellow, cm->GetMaterial(0)->GetFName().ToString());
		}
	}
}

I wonder, do I need to be passing a reference to the character who should have their material changed? As, each Client would need to change that character’s material, right? Is weird that the collecting Client isn’t showing it though?

Is your component and character marked as replicated?
Does on rep is firing properly on client?

It happens :p.