Need Help ): Replicated Inventory and Chests

Hey guys,

i am at a point where i don’t know what to do anymore. I want to create an Inventory for each Player in a Multiplayer Game that works seamlessly with Chests and other things.
So, after talking to someone on skype, i tried to do this with an ActorComponent that i give to every Actor which needs some kind of an Inventory.
For now, the PlayerState and the Chest got one.

PlayerState.h



#pragma once

#include "GameFramework/PlayerState.h"
#include "UnknownProjectPlayerState.generated.h"



/**
 * 
 */
UCLASS()
class UNKNOWNPROJECT_API AUnknownProjectPlayerState : public APlayerState
{
	GENERATED_BODY()
	
	/// Variables ///
public:

	UPROPERTY(Replicated, BlueprintReadOnly, VisibleAnywhere, Category = "Inventory")
	class UInventoryComponent* Inventory;

	/// Functions ///

public:

	/// Constructors ///
	AUnknownProjectPlayerState(const FObjectInitializer& ObjectInitializer);

	/// Function Override ///

	virtual void BeginPlay() override;	

        /// Network Function to Replicate Variables ///
	void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const;
};

PlayerState.cpp



#include "UnknownProject.h"
#include "UnknownProjectPlayerState.h"

AUnknownProjectPlayerState::AUnknownProjectPlayerState(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	bReplicates = true;	

	Inventory = CreateDefaultSubobject<UInventoryComponent>(FName("Inventory"));
	Inventory->SetIsReplicated(true);
}

/// Function Override ///
void AUnknownProjectPlayerState::BeginPlay()
{
	Super::BeginPlay();
	
}

/// Network Function to Replicate Variables ///
void AUnknownProjectPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	DOREPLIFETIME(AUnknownProjectPlayerState, Inventory)
}

And the ChestCode looks similar, but let’s only focus on the PlayerState, because this is already not working like i want.

Inside of my UInventoryComponent (code below), i have a replicated Array of Items (that gets replicated using an OnRep function), the inventory size, a delegate that should be bind to a function
inside the widget to update it and 2 functions that should help editing the Array for the beginning. Both of these functions have a normal and a Client->Server version.

Let’s only concentrate on the AddItemToInventory function. It is called from a linetrace that hits an item. Then it searches for other stacks in the inventory, or adds
the item to an empty place. Since the client calls it, i call the server version if the OwnerRole is not authority. The Server then adds the Item to the Array
and now the OnRep should get called + the Delegate. But the Delegate is only working on Client 1 and Client 2 is just doing nothing.

The function it self works, since i printed the arrays. And if i update the Widget by hand, i see my items. But the delegate is not working.

I could remove the “return” after the server call and move it to the end of the function, to have a Client version of the array for displaying and then updating it later
from serverside, but that doesn’t solve the problem of the delegate not firing. If my client cheats and changes the inventory, the server would fix it but the widget won’t
get updated correctly. I could also create some kind of a C++ version of a widget, but why should i if i can bind a delegate to the update event.

Or is this not working in Network Classes? Is the bind getting overridden or something like that?

I am also not happy with the “OwnerOnly” flag. This InventoryComponent should work on a chest without changing anything.
The Inventory should be visible for all clients looking into the chest. So it should be replicated to everyone.

I also created a reference of the InventoryComponent on my Widgets that gets passed when the widget is created. So i can pass the component to the second function.
Although this somewhat seems to work, the component is not getting replicated correctly. I also tried to replicated the InventoryComponent Variable in the PlayerState,
but i still had problems getting the Array in my Widgets. Accessing the Array directly from the PlayerState gives me the correct array, but passing the Component from the PlayerState
to a widget and getting the Array gives me an empty array -.-

InventoryComponent.h



#pragma once

#include "Components/ActorComponent.h"
#include "InventoryComponent.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FUpdateWidgetInventory);

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class UNKNOWNPROJECT_API UInventoryComponent : public UActorComponent
{
	GENERATED_BODY()

	/// Variables ///

public:

	// Inventory Properties //

	UPROPERTY(ReplicatedUsing = OnRep_Inventory, BlueprintReadWrite, EditAnywhere, Category = "Inventory")
		TArray<class ABase_Item*> Inventory;

	UPROPERTY(Replicated, BlueprintReadWrite, EditAnywhere, Category = "Inventory")
		int32 InventorySize;

        UPROPERTY(ReplicatedUsing = OnRep_Inventory)
		bool bUpdate;

	// Delegates //

	UPROPERTY(BlueprintAssignable, Category = "Inventory")
		FUpdateWidgetInventory UpdateInventoryDelegate;

	/// Functions ///

public:	
	// Sets default values for this component's properties
	UInventoryComponent();

	// Called when the game starts
	virtual void InitializeComponent() override;
	
	// Called every frame
	virtual void TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction ) override;


	/// Network Function to Replicate Variables ///
	void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const;

	UFUNCTION()
		void OnRep_Inventory();

	UFUNCTION(BlueprintCallable, Category = "Inventory")
		void AddItemToInventory(class ABase_Item* _Item);
	
	UFUNCTION(reliable, server, WithValidation)
		void Server_AddItemToInventory(class ABase_Item* _Item);
		bool Server_AddItemToInventory_Validate(class ABase_Item* _Item);
		void Server_AddItemToInventory_Implementation(class ABase_Item*);

	UFUNCTION(BlueprintCallable, Category = "Inventory")
		void MoveItem(int32 _TargetSlot, UInventoryComponent* _SourceInvCom, int32 _SourceSlot);

	UFUNCTION(reliable, server, WithValidation)
		void Server_MoveItem(int32 _TargetSlot, UInventoryComponent* _SourceInvCom, int32 _SourceSlot);
		bool Server_MoveItem_Validate(int32 _TargetSlot, UInventoryComponent* _SourceInvCom, int32 _SourceSlot);
		void Server_MoveItem_Implementation(int32, UInventoryComponent*, int32);
};


InventoryComponent.cpp



#include "UnknownProject.h"
#include "InventoryComponent.h"


// Sets default values for this component's properties
UInventoryComponent::UInventoryComponent()
{
	// 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.
	bWantsInitializeComponent = true;
	PrimaryComponentTick.bCanEverTick = true;

	// ...
	bReplicates = true;
}


// Called when the game starts
void UInventoryComponent::InitializeComponent()
{
	Super::InitializeComponent();

	Inventory.Init(nullptr, InventorySize);

	for (int32 i = 0; i < InventorySize; i++)
	{
		Inventory* = nullptr;
	}
	
}


// Called every frame
void UInventoryComponent::TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction )
{
	Super::TickComponent( DeltaTime, TickType, ThisTickFunction );

	// ...
}

/// Network Function to Replicate Variables ///
void UInventoryComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	DOREPLIFETIME(UInventoryComponent, Inventory);
	DOREPLIFETIME(UInventoryComponent, bUpdate);
}

/// Network Function to change Inventory ///

// OnRepNotify when the Inventory gets changed

void UInventoryComponent::OnRep_Inventory()
{
	if (GetOwnerRole() < ROLE_Authority)
	{
		GEngine->AddOnScreenDebugMessage(-1, 15.f, FColor::Green, TEXT("Delegate RepCall"));
		UpdateInventoryDelegate.Broadcast();
	}
}

// Add Item to the Inventory

void UInventoryComponent::AddItemToInventory(class ABase_Item* _Item)
{
	if (GetOwnerRole() < ROLE_Authority)
	{
		Server_AddItemToInventory(_Item);
		return;
	}

	// Bool to keep track of destroyed item
	bool bWasDestroyed = false;

	/*
	Make sure our Item is still Valid. If yes, check if we have a specific Slot that we want to fill.
	*/
	if (_Item)
	{
		/*
		Search for an Item of that Type to Stack it
		*/
		if (_Item->GetCanBeStacked())
		{
			for (int32 i = 0; i < InventorySize; i++)
			{
				if (Inventory.IsValidIndex(i) && Inventory*)
				{
					// If we found an Item that is the same
					if (Inventory*->GetClass() == _Item->GetClass())
					{
						if (Inventory*->Amount < Inventory*->GetMaxStacks())
						{
							int32 Difference = Inventory*->Amount + _Item->Amount - Inventory*->GetMaxStacks();
							/*
							When the difference is equal to 0, the Stacks hits exactly the MaxStacks and we
							destroy the _Item we want to add.
							When the difference is greater than 0, we have more Items then we can Stack, so
							we just fill the Stack to max and leave the rest on the Item. We search for a
							new Item to place the Rest on.
							When the difference is less than 0, we can add the new amount on the old and destroy
							the _Item.
							*/
							if (Difference == 0)
							{
								Inventory*->Amount = Inventory*->GetMaxStacks();

								bWasDestroyed = true;
								_Item->Destroy();
                                                                bUpdate = !bUpdate;
								break;
							}
							else if (Difference > 0)
							{
								Inventory*->Amount = Inventory*->GetMaxStacks();
								_Item->Amount = Difference;
                                                                bUpdate = !bUpdate;
							}
							else
							{
								Inventory*->Amount = Inventory*->Amount + _Item->Amount;

								bWasDestroyed = true;
								_Item->Destroy();
                                                                bUpdate = !bUpdate;
								break;
							}
						}
					}
				}
			}
		}

		/*
		If no existing Item was found, we will search for an empty spot and add the
		item there.
		*/
		if (_Item && !bWasDestroyed)
		{
			for (int32 i = 0; i < InventorySize; i++)
			{
				// We found an empty spot, put the Item here and set the correct ItemSlot to use later in UMG
				if (Inventory.IsValidIndex(i) && !Inventory*)
				{
					Inventory* = _Item;
					_Item->SetCurrentSlot(i);

					_Item->bIsVisible = false;

					// Make sure that the client updates the visibility right away
					// Also the Server calls this, because he won't get the RepNotify call
					_Item->MakeItemInvisible();
                                        bUpdate = !bUpdate;
					break;
				}
				if (i == InventorySize - 1)
				{
					GEngine->AddOnScreenDebugMessage(-1, 15.f, FColor::Green, TEXT("Inventory full!"));
					// Inventory full
				}
			}
		}
	}
	if (GetOwnerRole() < ROLE_Authority)
	{
		GEngine->AddOnScreenDebugMessage(-1, 15.f, FColor::Green, TEXT("Delegate ClientCall"));
		UpdateInventoryDelegate.Broadcast();
	}
}

bool UInventoryComponent::Server_AddItemToInventory_Validate(class ABase_Item* _Item)
{
	return true;
}
void UInventoryComponent::Server_AddItemToInventory_Implementation(class ABase_Item* _Item)
{
	if (GetOwnerRole() == ROLE_Authority)
	{
		GEngine->AddOnScreenDebugMessage(-1, 15.f, FColor::Green, TEXT("ServerCall"));
		AddItemToInventory(_Item);
	}
}

// Move Item in one or two Inventories
void UInventoryComponent::MoveItem(int32 _TargetSlot, UInventoryComponent* _SourceInvCom, int32 _SourceSlot)
{
	if (GetOwnerRole() < ROLE_Authority)
	{
		Server_MoveItem(_TargetSlot, _SourceInvCom, _SourceSlot);
	}
	if (!_SourceInvCom || !_SourceInvCom->Inventory[_SourceSlot])
	{
		GEngine->AddOnScreenDebugMessage(-1, 10.f, FColor::Red, TEXT("InvCom or Slot not valid!"));
		return;
	}
	if (this == _SourceInvCom && _TargetSlot == _SourceSlot)
	{
		return;
	}

	// Empty Slot? Just fill the Item and remove it from the other Inventory
	if (!Inventory[_TargetSlot])
	{
		Inventory[_TargetSlot] = _SourceInvCom->Inventory[_SourceSlot];
		_SourceInvCom->Inventory[_SourceSlot]->SetCurrentSlot(_TargetSlot);
		_SourceInvCom->Inventory[_SourceSlot] = nullptr;
			
	}
	else
	{
		if (Inventory[_TargetSlot]->GetClass() != _SourceInvCom->Inventory[_SourceSlot]->GetClass())
		{
			ABase_Item* Temp_Item = Inventory[_TargetSlot];
			Inventory[_TargetSlot] = _SourceInvCom->Inventory[_SourceSlot];
			_SourceInvCom->Inventory[_SourceSlot] = Temp_Item;

			Inventory[_TargetSlot]->SetCurrentSlot(_TargetSlot);
			_SourceInvCom->Inventory[_SourceSlot]->SetCurrentSlot(_SourceSlot);
		}
		else
		{
			if (Inventory[_TargetSlot]->GetMaxStacks() >= (Inventory[_TargetSlot]->Amount + _SourceInvCom->Inventory[_SourceSlot]->Amount))
			{
				Inventory[_TargetSlot]->Amount += _SourceInvCom->Inventory[_SourceSlot]->Amount;

				_SourceInvCom->Inventory[_SourceSlot]->Destroy();
				_SourceInvCom->Inventory[_SourceSlot] = nullptr;
			}
			else
			{
				int32 Difference = (Inventory[_TargetSlot]->Amount + _SourceInvCom->Inventory[_SourceSlot]->Amount) - Inventory[_TargetSlot]->GetMaxStacks();

				Inventory[_TargetSlot]->Amount = Inventory[_TargetSlot]->GetMaxStacks();

				_SourceInvCom->Inventory[_SourceSlot]->Amount -= Difference;
			}
		}
	}
	if (GetOwnerRole() < ROLE_Authority)
	{
		UpdateInventoryDelegate.Broadcast();
		_SourceInvCom->UpdateInventoryDelegate.Broadcast();
	}
}

bool UInventoryComponent::Server_MoveItem_Validate(int32 _TargetSlot, UInventoryComponent* _SourceInvCom, int32 _SourceSlot)
{
	return true;
}
void UInventoryComponent::Server_MoveItem_Implementation(int32 _TargetSlot, UInventoryComponent* _SourceInvCom, int32 _SourceSlot)
{
	if (GetOwnerRole() == ROLE_Authority)
	{
		MoveItem(_TargetSlot, _SourceInvCom, _SourceSlot);
	}
}

I can’t find the specific documentation right now, but TArray modifications simply do not trigger the ReplicatedUsing function.

One simple way around this is to create an extra replicated variable (like an int32) with the same OnRep function. You modify that variable whenever you modify the array server-side to enforce the OnRep being called on remote clients. For example, use an int32 and increment it each time. (Bool is not preferred since after two changes it is back at its default value, causing the OnRep not to be called on a client that joins later in the game if the number of array changes is even). I got this from one of Rama’s wiki pages, so credits to him for this tip.

They do not trigger at all, or only sometimes? Because i definitely get the OnRep call on the clients. Not always, but most of the time.

Not at all afaik, is the first client a remote client or the server?

Both are clients. I use a dedicated server. I will test a bool for the OnRep. Gimme a sec.

EDIT: Nope, it’s freaking buggy. Second client picks up items, i see the “OnRep” debug message, Widget is still empty.
Client 1 grabs an item, client 2 updates his widget. I restart the game, pick up an item with client 2, Widget updates.
I restart again, client 2 isn’t working again. This is so random, i have no idea where this comes from -.-

I feel your frustrations. :smiley: So if you try the workaround by making an replicated int32 that has the OnRep callback instead of the array, and increment that int whenever you change the array server-side, does that make the result more as expected?

Another quick note, shouldn’t the UInventoryComponent UPROPERTY in your player state be replicated as well?

I made a bool that i change every time. The OnRep gets called, but not the Delegate in it. It feels like the Delegate is wrong somehow o.o

I had the UInventoryComponent also replicated, but had no change. I mean, normally i wouldn’t need to set any of the Variables to OwnerOnly.
The Client to Server call happens on the Client version of the PlayerState of the Inventory owning client. So it only changes its Inventory.
The Widget uses only the PlayerController0 PlayerState, so the Inventory should match.

It makes 0! sense that the Inventory is so mixed up when replicating this to everyone. I mean, if i set the Array to replicate to everyone,
i suddenly have the array of the first client on the second one. Although they never get the array of another PlayerState than the one they own.
I can understand, that the OnRep fires the Delegate on the PlayerState that is replicated on the other Client, so OnRep calls the Update Widget
not only on the first, but also on the second client. But this should only result on an update of the widget and they normaly should stay the same.
Even if it is not necessary to update the other clients when one changes its inventory, i can’t understand why they are sharing the array.

;_; This is so **** frustrating. I kinda feel like making an actor for the Inventory and not a Component. But this doesn’t solve the Delegate doing ****…

Where do you bind the delegate? Is it in the widget, in your PlayerState blueprint, or?

Does the delegate behavior change per restart or even within the same session? If its per restart, it could be an order of initialization issue. You could, just for the sake of testing, try putting a Delay node before you find and bind to the delegate to make sure that the component is (net) initialized on the remote clients. Just for testing purposes though!

Just to update. Right now i’m replicating the Component Variable in my PlayerState too and changed the Inventory Array replication, as well as the bool replication from “OwnerOnly” to a normal replicate without conditions.

I bind the Delegate inside of my widget in the construct. My PlayerState creates the MainUI. The MainUI creates the Inventory. The Inventory binds the Delegate to an Update function.
Calling the update function with a key press, updates my widget correctly. The OnRep is fired correctly. Twice at the moment, because the replication of the array happens on all instances (which is ok for testing now).
The Array of the Client 2 isn’t updating. The array of client 1 seems to update. So the Delegate gets called there. The OnRep call from the Client 1, that also gets called on Client 2 is also not working for the Client 2.

Adding a delay right in front my the bind makes no difference. I also stepped back from passing the InventoryComponent from the PlayerState down to the Inventory Widget and just get the InventoryComponent from Controller->PlayerState inside the Widget. Although i need to have the Inventory Component being passed to the widget, because i need to use this for moving items to the Chest -.-

PS: I will go to bed now. Thanks for your time. I will wait if anyone has an idea by tomorrow. In the meantime, i will try to do the same with with an actor that i spawn instead of an Component.
Maybe the component makes things buggy? i have no idea -.-

I find it strange that client 1 and 2 show different behavior, while you’re using a dedicated server so they are the same network case. What can make the difference? On client 2 when you do the key press to update your widget, it also doesn’t update correctly?

Since the PlayerState creates the inventory widget, that rules out the possibility that you try to bind before the PlayerState is initialized. Are you sure only the local player’s PlayerStates creates the MainUI? Since PlayerStates are replicated to all clients.

Try printing the contents of your inventory manually, for example on key press. Are the inventory contents correct on client 1 and 2?

I guess the objective is to narrow it down to:

  • whether the data isn’t replicated correctly, or
  • whether the binding isn’t done correctly (maybe binding to the wrong inventory), or
  • whether the widget is just displaying incorrectly but the data is okay

Key press updates the inventory correctly.
I already print the arrays, they are ok.
I only let the clients create the inventory with an authority switch.

I happended to see in the debug graph, that the delegate in the client 2 inventory sometimes gets called by client 1. although the widget doesn’t update. It might really be the delegate, but what am i doing wrong about it? I already had a 5 second delay before binding it so the playertate was ok. I also binded the delegate in inventory and, for testing, in the playerstate. Nothing changes.

Is it possible to create a c++ function that i override in blueprint which gets called when i call the c++ version?

EDIT: Ok, instead of binding it, i just called it directly:

http://puu.sh/iojTD/93f8561ae6.png

First test seemed to work though.

Ok, now it getscalled “sometimes”. -.-

I will create an actor for my inventory, instead of a component. Lets see if this solves something.

I like the idea of having an inventory component though, like you said to make it easier to have the same functionality on chests.

Yeah, creating an actor for this seems more and more fishy. I guess i will need to start from scratch and try the component again -.-

EDIT: So, just the data seems to be correct. I stripped of everything that i don’t need. Removed the Delegate etc and printed the things inside the
array on key press. They are correct.

EDIT2: Ok, the problem with the Component Reference in my Widget seems to be that it is not a real reference, but more like a function parameter. I don’t know
if this i wanted, but i made a “workaround” by passing the AActor, using this inventory and getting the component from him:

http://puu.sh/iov2k/6cc2bfa1a0.png

Now i need to fix the delegate problem.

EDIT3: Ok, i seem to have it working now.

I have no idea why. Instead of binding it, i only got the red node that gets called. Didn’t even no that exists.