3D Widget Interaction Drag & Drop Freezes up the Game

Here’s the deal:

I have a game with 3D widgets that display an inventory. The items in the inventory need to be dragged and dropped so that the player can sort the inventory. See here:

I have a widget interaction component on the player controller. When the player left clicks, it starts the pressed pointer event, when they release left click, it starts the released event. See here:

However, when the player begins to drag an item for the first time after opening the engine, this appears:

LogOutputDevice: Error: === Handled ensure: ===
LogOutputDevice: Error: Ensure condition failed: Cursor  [File:D:\build\++UE5\Sync\Engine\Source\Runtime\Slate\Private\Framework\Application\SlateUser.cpp] [Line: 1053] 
LogOutputDevice: Error: Stack: 
LogOutputDevice: Error: [Callstack] 0x00007ff98fbace55 UnrealEditor-Slate.dll!FSlateUser::NotifyPointerMoveComplete() [D:\build\++UE5\Sync\Engine\Source\Runtime\Slate\Private\Framework\Application\SlateUser.cpp:1053]
LogOutputDevice: Error: [Callstack] 0x00007ff98fb00cff UnrealEditor-Slate.dll!FSlateApplication::RoutePointerMoveEvent() [D:\build\++UE5\Sync\Engine\Source\Runtime\Slate\Private\Framework\Application\SlateApplication.cpp:5690]
LogOutputDevice: Error: [Callstack] 0x00007ff97bc5f636 UnrealEditor-UMG.dll!UWidgetInteractionComponent::SimulatePointerMovement() [D:\build\++UE5\Sync\Engine\Source\Runtime\UMG\Private\Components\WidgetInteractionComponent.cpp:426]
LogOutputDevice: Error: [Callstack] 0x00007ff97f285311 UnrealEditor-Engine.dll!UE::Trace::Private::FLogScope::Enter<FMetadataAssetFields>() [D:\build\++UE5\Sync\Engine\Source\Runtime\Engine\Classes\GameFramework\Actor.h:4573]
LogOutputDevice: Error: [Callstack] 0x00007ff97f44c7d5 UnrealEditor-Engine.dll!FActorComponentTickFunction::ExecuteTick() [D:\build\++UE5\Sync\Engine\Source\Runtime\Engine\Private\Components\ActorComponent.cpp:1170]
LogOutputDevice: Error: [Callstack] 0x00007ff980e9b5fb UnrealEditor-Engine.dll!FTickFunctionTask::DoTask() [D:\build\++UE5\Sync\Engine\Source\Runtime\Engine\Private\TickTaskManager.cpp:278]
LogOutputDevice: Error: [Callstack] 0x00007ff980ebc5b5 UnrealEditor-Engine.dll!TGraphTask<FTickFunctionTask>::ExecuteTask() [D:\build\++UE5\Sync\Engine\Source\Runtime\Core\Public\Async\TaskGraphInterfaces.h:1235]
LogOutputDevice: Error: [Callstack] 0x00007ff98769b88a UnrealEditor-Core.dll!FNamedTaskThread::ProcessTasksNamedThread() [D:\build\++UE5\Sync\Engine\Source\Runtime\Core\Private\Async\TaskGraph.cpp:760]
LogOutputDevice: Error: [Callstack] 0x00007ff98769bd59 UnrealEditor-Core.dll!FNamedTaskThread::ProcessTasksUntilIdle() [D:\build\++UE5\Sync\Engine\Source\Runtime\Core\Private\Async\TaskGraph.cpp:662]
LogOutputDevice: Error: [Callstack] 0x00007ff980f24816 UnrealEditor-Engine.dll!FTickTaskSequencer::ReleaseTickGroup() [D:\build\++UE5\Sync\Engine\Source\Runtime\Engine\Private\TickTaskManager.cpp:573]
LogOutputDevice: Error: [Callstack] 0x00007ff980f3551b UnrealEditor-Engine.dll!FTickTaskManager::RunTickGroup() [D:\build\++UE5\Sync\Engine\Source\Runtime\Engine\Private\TickTaskManager.cpp:1583]
LogOutputDevice: Error: [Callstack] 0x00007ff97fdcef0f UnrealEditor-Engine.dll!UWorld::RunTickGroup() [D:\build\++UE5\Sync\Engine\Source\Runtime\Engine\Private\LevelTick.cpp:772]
LogOutputDevice: Error: [Callstack] 0x00007ff97fdde188 UnrealEditor-Engine.dll!UWorld::Tick() [D:\build\++UE5\Sync\Engine\Source\Runtime\Engine\Private\LevelTick.cpp:1514]
LogOutputDevice: Error: [Callstack] 0x00007ff985c0cffd UnrealEditor-UnrealEd.dll!UEditorEngine::Tick() [D:\build\++UE5\Sync\Engine\Source\Editor\UnrealEd\Private\EditorEngine.cpp:2015]
LogOutputDevice: Error: [Callstack] 0x00007ff98688f6b6 UnrealEditor-UnrealEd.dll!UUnrealEdEngine::Tick() [D:\build\++UE5\Sync\Engine\Source\Editor\UnrealEd\Private\UnrealEdEngine.cpp:550]
LogOutputDevice: Error: [Callstack] 0x00007ff68a8b8e0b UnrealEditor.exe!FEngineLoop::Tick() [D:\build\++UE5\Sync\Engine\Source\Runtime\Launch\Private\LaunchEngineLoop.cpp:5921]
LogOutputDevice: Error: [Callstack] 0x00007ff68a8de33c UnrealEditor.exe!GuardedMain() [D:\build\++UE5\Sync\Engine\Source\Runtime\Launch\Private\Launch.cpp:180]
LogOutputDevice: Error: [Callstack] 0x00007ff68a8de42a UnrealEditor.exe!GuardedMainWrapper() [D:\build\++UE5\Sync\Engine\Source\Runtime\Launch\Private\Windows\LaunchWindows.cpp:118]
LogOutputDevice: Error: [Callstack] 0x00007ff68a8e18a4 UnrealEditor.exe!LaunchWindowsStartup() [D:\build\++UE5\Sync\Engine\Source\Runtime\Launch\Private\Windows\LaunchWindows.cpp:258]
LogOutputDevice: Error: [Callstack] 0x00007ff68a8f70c4 UnrealEditor.exe!WinMain() [D:\build\++UE5\Sync\Engine\Source\Runtime\Launch\Private\Windows\LaunchWindows.cpp:298]
LogOutputDevice: Error: [Callstack] 0x00007ff68a8fa37a UnrealEditor.exe!__scrt_common_main_seh() [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288]
LogOutputDevice: Error: [Callstack] 0x00007ffaadbf7374 KERNEL32.DLL!UnknownFunction []
LogOutputDevice: Error: [Callstack] 0x00007ffaae2dcc91 ntdll.dll!UnknownFunction []

I know why this occurs. It’s an ensure from within the slate user code, SlateUser.cpp line 1053:

It states that the cursor is not valid, this makes sense as elsewhere in the code it states explicitly:

The cursor this user is in control of. Guaranteed to be valid for all real users, absence implies this is a virtual user.

See here, SlateUser.h line 208:
image

Which is true, the widget interaction component IS a virtual user. The problem is it freezes the engine and causes a GURANTEED hiccup every time the game is opened and the drag occurs for the first time.

So my question is, what do I do about it?

Since:

  • these are world space widget components
  • each with a quad very capable of responding to traces
  • and we’re using a mouse

There seems to be no need to get the widget interaction component involved, or, worse, to mess with slate. Trace against the component, move it. You could simply get hit under cursor by channel / object and be done with it.

Any reason why this approach is not feasible?


Here’s a crude example of what I mean:

  • I have extended a widget component and have aBoard actor create a grid of them:

This part is definitely different for you; here, included only for the sake of completeness.

  • the board moves the pieces (this should be done in the player controller, though):

  • the result:

Instead of snapping a vector to a fixed value, you’d snap it to the block behind it, ofc.
Could this approach work for you?

Hi, thank you for your response, I really appreciate it.

I’m currently trying to implement a solution based on your suggestion and I am just having a bit of trouble with the widget component. For some reason whenever I use a masked material for the world space UI I get this:

It’s this weird highly pixelated shadow cast onto the mesh behind and I have no idea how to fix it. Before I just used a translucent material but started running into issues with overlapping based on perspective. See here:


The pixel shadows only seem to occur when the widget is in shadows.

Do you know why this masked material is casting this weird shadow?

Could you disable Lumen for now and see if it produces the same result. It looks as if the denoiser struggled to resolve but I might be completely wrong here.

So yeah, what I found is that this is something called “Shadow noise.” See here:

If I change the dynamic global illumination method, which can be found under rendering in the project settings:
CurrentIlluminationSetting

From Lumen to plugin:


PluginIlluminationSetting

The shadow noise goes away, see here:

But I wasn’t prepared to remove Lumen from my project so I just pulled the widget component closer to the mesh. See here:

However, this seems extremely weird. For future reference, are you aware of any methods to combat this issue?

Stop casting shadow if you do need it. GI is still being worked on, hopefully this will be fixed. Seems like an obvious omission, or might have been already reported.

I actually did try disabling shadows on the widget component, see here:

However, after your suggestion it made me think you may have also meant disable the shadows on the cube, see here:
CubeDisableShadow

And then I stopped casting shadows on all the meshes inside the actor but it still didn’t seem to work. See here:

It’s fine for this project to just pull the widget component in but it’s something to keep an eye out for future projects.

Also, does GI stand for “Global Illumination”?

Okay, so here is my final solution:

Big thanks @Everynone for the suggestion and pointing me in the right direction.

To give some context, I have an inventory system where:

  • Inventory components contain an Inventory UObject
  • Inventory UObjects contain an Inventory FFastArraySerializer
  • Inventory FFastArraySerializers contain a TArray of Inventory FFastArraySerializerItems
  • Inventory FFastArraySerializerItems contain an item instance and a TArray of Inventory IDs, however I only use 1 currently
  • Item instances contain Item data assets which do contain UI data like what image and brush to use
  • Inventory IDs contain an int32, the int32 is the location of the item inside the inventory

With this inventory system, it is possible to locate and grab an inventory item from the inventory component using an int32.

Part 1, UI CREATION:

Firstly, I created the main inventory UserWidget with a Uniform grid panel, see here:

Then I created a C++ base class for an Item slot user widget that contained an ID member variable and “InitIDFromObjectName” that sets the ID based off of the name of the object. See here: (Using common UI here)

UCLASS()
class ORGANISEMYBAGS_API UInventorySlotWidget : public UCommonActivatableWidget
{
public:

	GENERATED_BODY()

	// Implies that the the number is after an "_"
	UFUNCTION(BlueprintCallable)
	void InitIDFromObjectName();
	
protected:

	UPROPERTY(EditAnywhere, BlueprintGetter = GetID, BlueprintSetter = SetID)
	int32 ID = -1;

public:

	UFUNCTION(BlueprintGetter)
	FORCEINLINE int32 GetID() const { return ID; }
	UFUNCTION(BlueprintSetter)
	FORCEINLINE void SetID(int32 InID) { ID = InID; }
};

I used sub string functions from the Kismet string library to find an underscore in the name of the object, then, starting from the index after the underscore I try to convert the string, that is up to 6 characters, into a number, see here:

#include "Kismet/KismetStringLibrary.h"

void UInventorySlotWidget::InitIDFromObjectName()
{
	int32 StartIndex = UKismetStringLibrary::FindSubstring(GetName(), TEXT("_")) + 1;

	SetID(FCString::Atoi(*UKismetStringLibrary::GetSubstring(GetName(), StartIndex, 6)));
}

I then created a widget blueprint based of that C++ class, see here:

I populated the main inventory UserWidget’s Uniform grid panel with these item slots, see here:

Part 2, GRID WIDGET COMPONENT:

After all the UI was created I made a new C++ class based off of the Widget component, see here:

On this component I store a pointer to a panel widget, which in the case of the UI created earlier, means it will point to a Uniform Grid panel, see here:

	UPROPERTY()
	TObjectPtr<UPanelWidget> GridWidget = nullptr;

I also have a flag that is stored on the component that is set to true by a public function called “TriggerUpdateToInventory”, see here:

	UPROPERTY()
	uint8 bUpdateInventory : 1;
public:
	UFUNCTION(BlueprintCallable)
	void TriggerUpdateToInventory();
void UGridWidgetComponent::TriggerUpdateToInventory()
{
	bUpdateInventory = true;
}

Then In order to update the UI I have an “UpdateToInventory” function that takes in an inventory component, see here:

	UFUNCTION(BlueprintCallable)
	void UpdateToInventory(UBaseInventoryComponent* InInventoryComponent);

The way this function works is that it gets the world location of item slots inside the inventory UI and either creates or moves widget components to those locations.

It then also adds box components to those locations so that later they can be traced against to grab the widget at that location. After there has been a valid attempt to update the UI, the update inventory flag is set to false.

Massive help from @Caracole_1 with this post in getting the locations of elements inside a widget being drawn by a widget component. The only note is do not use relative scale, world scale is the way to go because like in my case the widget component may be a child of another component.

Here are the member variables used in the function and the function definition itself:

	UPROPERTY(EditAnywhere)
	TSubclassOf<UUserWidget> GridItemClass = nullptr;

	UPROPERTY(EditAnywhere)
	FVector2D GridItemDrawSize = FVector2D(220.f, 220.f);

	UPROPERTY(EditAnywhere)
	FVector GridItemBoxExtents = FVector(20.f, 120.f, 120.f);

	UPROPERTY(EditAnywhere)
	uint8 bDebugGridItemBoxComponents : 1;

	UPROPERTY()
	TMap<int32, UWidgetComponent*> AddedWidgetComponents;

	UPROPERTY()
	TMap<int32, FTransform> IDTransforms;

	UPROPERTY()
	TMap<UPrimitiveComponent*, int32> GridItemSlotColliders;
void UGridWidgetComponent::UpdateToInventory(UBaseInventoryComponent* InInventoryComponent)
{
	// Check if slate application is initialised
	if (FSlateApplication::IsInitialized())
	{
		// Check that both the inventory component and grid widget are valid
		if (IsValid(InInventoryComponent) && IsValid(GridWidget))
		{
			// If they are then we want to get the Grid widget's geometry
			const FGeometry& GridGeometry = GridWidget->GetTickSpaceGeometry();

			// and a default geometry to compare it to
			FGeometry DefaultGeometry = FGeometry();

			// If the grid widget geometry's local size is the same as geometry that doesn't have data then we are properly rendering
			// the grid widget yet and therefore shouldn't enter this code
			if (USlateBlueprintLibrary::GetLocalSize(GridGeometry) != USlateBlueprintLibrary::GetLocalSize(DefaultGeometry))
			{
				// If we pass that check get the widget component's draw size
				const FVector2D& CachedDrawSize = GetDrawSize();

				// Move it into an FVector instead of an FVector2D
				FVector DrawSize3D = FVector(0.f, CachedDrawSize.X, CachedDrawSize.Y);

				// Multiply it by this component's world scale
				FVector ScaledDrawSize3D = DrawSize3D * GetComponentScale();

				// Then shift it to half (not actually sure why this is done but it works)
				FVector ShiftedDrawSize3D = ScaledDrawSize3D * 0.5f;

				// Finally, find the top left location of the widget this component is drawing from the current location shifted by the shifted draw size
				FVector TopLeftWidgetWorldLocation = GetComponentLocation() + (GetUpVector() * ShiftedDrawSize3D.Z) + (GetRightVector() * ShiftedDrawSize3D.Y);

				// Loop through all the slots inside of the grid widget
				for (const UPanelSlot* Slot : GridWidget->GetSlots())
				{
					// If the slot is valid
					if (IsValid(Slot))
					{
						// Check that it is an inventory slot
						UInventorySlotWidget* InventorySlot = Cast<UInventorySlotWidget>(Slot->Content);

						if (IsValid(InventorySlot))
						{
							// If it is make sure it's ID is set by calling the Init ID
							InventorySlot->InitIDFromObjectName();

							// Then get this ID
							const int32 CurrentID = InventorySlot->GetID();

							// and use it to find an item inside of the inventory component
							FInventoryItem FoundItem;
							InInventoryComponent->GetItemFromID(FoundItem, CurrentID);

							// Declare a widget and box component
							UWidgetComponent* CurrentWidgetComponent = nullptr;
							UBoxComponent* CurrentBoxComponent = nullptr;

							// and try to find if a widget component already exists at the current ID
							UWidgetComponent** WidgetPtr = AddedWidgetComponents.Find(CurrentID);

							if (WidgetPtr != nullptr)
							{
								// If it does set current widget component to it
								CurrentWidgetComponent = *WidgetPtr;
							}
							else
							{
								// but if it doesn't then create a new widget component with the ID number in the name
								FName CurrentWidgetComponentName = FName(*FString::Printf(TEXT("GridItemWidgetComponent_%i"), CurrentID));
								CurrentWidgetComponent = NewObject<UWidgetComponent>(this, CurrentWidgetComponentName);


								if (IsValid(CurrentWidgetComponent))
								{
									// Then register and set it up if it was successfully created
									CurrentWidgetComponent->RegisterComponent();
									CurrentWidgetComponent->AttachToComponent(this, FAttachmentTransformRules::KeepRelativeTransform);
									CurrentWidgetComponent->CreationMethod = EComponentCreationMethod::Instance;
									CurrentWidgetComponent->SetMaterial(0, GetMaterial(0));
									CurrentWidgetComponent->SetDrawSize(GridItemDrawSize);
									CurrentWidgetComponent->SetCastShadow(CastShadow);
									CurrentWidgetComponent->SetAffectDistanceFieldLighting(bAffectDistanceFieldLighting);
									CurrentWidgetComponent->SetAffectDynamicIndirectLighting(bAffectDynamicIndirectLighting);
								}

								// and add it to the added widget components map
								AddedWidgetComponents.Add(CurrentID, CurrentWidgetComponent);

								// Then because there wasn't a widget here a box component must also be created
								FName CurrentBoxComponentName = FName(*FString::Printf(TEXT("BoxComponent_%i"), CurrentID));
								CurrentBoxComponent = NewObject<UBoxComponent>(this, CurrentBoxComponentName);

								if (IsValid(CurrentBoxComponent))
								{
									// regiter and set it up if created successfully 
									CurrentBoxComponent->RegisterComponent();
									CurrentBoxComponent->AttachToComponent(this, FAttachmentTransformRules::KeepRelativeTransform);
									CurrentBoxComponent->CreationMethod = EComponentCreationMethod::Instance;
									CurrentBoxComponent->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
									CurrentBoxComponent->SetCollisionResponseToChannel(ECollisionChannel::ECC_Visibility, ECollisionResponse::ECR_Block);
									CurrentBoxComponent->SetBoxExtent(GridItemBoxExtents);
									CurrentBoxComponent->SetHiddenInGame(!bDebugGridItemBoxComponents);
								}

								// and add it to the grid item colliders
								GridItemSlotColliders.Add(CurrentBoxComponent, CurrentID);
							}

							// Now there should be a widget component
							if (IsValid(CurrentWidgetComponent))
							{
								// and the inventory slot should have a cached widget
								TSharedPtr<SWidget> SafeWidgetSlot = InventorySlot->GetCachedWidget();

								if (SafeWidgetSlot.IsValid())
								{
									// which if both are true, get the inventory slot's geometry
									const FGeometry& Geometry = InventorySlot->GetTickSpaceGeometry();

									// then get the local size
									FVector2D LocalSize = USlateBlueprintLibrary::GetLocalSize(Geometry);

									// half it
									FVector2D HalfLocalSize = LocalSize / 2.f;

									// and turn it to an absolute size
									FVector2D AbsoluteSize = USlateBlueprintLibrary::LocalToAbsolute(Geometry, HalfLocalSize);

									// scale the absolute size by the component world scale and multiply that by negative 1
									FVector ScaledAbsoluate = FVector(0.f, AbsoluteSize.X, AbsoluteSize.Y) * GetComponentScale() * -1;

									// Finally, get the world location of the item slot widget from this widget component's widget top left location and the scaled size of 
									// the item slot widget
									FVector WidgetWorldLocation = TopLeftWidgetWorldLocation + (GetRightVector() * ScaledAbsoluate.Y) + (GetUpVector() * ScaledAbsoluate.Z);

									// Then set the current widget component's location to the item slot widget's location but shift it forward a little bit
									CurrentWidgetComponent->SetWorldLocation(WidgetWorldLocation + (GetForwardVector() * 1.f));

									// Try and get the Transform at this ID
									FTransform& IDTransform = IDTransforms.FindOrAdd(CurrentID);

									// and set it to the current widget component's relative transform
									IDTransform = CurrentWidgetComponent->GetRelativeTransform();

									// Then just set the current box component's location to the item slot's locaton
									if (IsValid(CurrentBoxComponent))
									{
										CurrentBoxComponent->SetWorldLocation(WidgetWorldLocation);
									}
								}

								// If there was an item at the ID
								if (IsValid(FoundItem.ItemInstance))
								{
									// Check whether there is a widget to use and if there isn't make one
									if (!IsValid(CurrentWidgetComponent->GetWidget()))
									{
										CurrentWidgetComponent->SetWidgetClass(GridItemClass);
									}

									// Check that the item at the ID has a data asset
									if (IsValid(FoundItem.ItemInstance->GetDataAsset()))
									{
										// Check the widget again and that it has a widget tree
										if (IsValid(CurrentWidgetComponent->GetWidget()) && IsValid(CurrentWidgetComponent->GetWidget()->WidgetTree))
										{
											// declare an array of found widgets
											TArray<UWidget*> FoundWidgets;

											// fill out the array with the results from getting all widgets within the free
											CurrentWidgetComponent->GetWidget()->WidgetTree->GetAllWidgets(FoundWidgets);

											for (UWidget* FoundWidget : FoundWidgets)
											{
												// Then try to find an image in the tree and update the image with data from the data asset
												if (UImage* Image = Cast<UImage>(FoundWidget))
												{
													Image->SetBrushFromAsset(FoundItem.ItemInstance->GetDataAsset()->BrushAsset);
													Image->SetBrushFromTexture(FoundItem.ItemInstance->GetDataAsset()->ItemImage);
												}
											}
										}

									}


								}
								else
								{
									// If there wasn't an item remove the widget
									CurrentWidgetComponent->SetWidgetClass(nullptr);
								}
							}
						}
					}
				}

				// Then as long there was geometry with size the inventory update can 
				// be considered complete and set the flag to false
				bUpdateInventory = false;
			}
		}
	}
}

Then I override the “UpdateWidget” function so that the Grid widget gets set and triggers an update to inventory if the flag has been set to true. See here:

	virtual void UpdateWidget() override;
void UGridWidgetComponent::UpdateWidget()
{
	Super::UpdateWidget();

	if (IsValid(this) && IsValid(GetWidget()))
	{
		GridWidget = GetWidget()->WidgetTree->FindWidget<UPanelWidget>(GridName);
	}
	
	if (bUpdateInventory)
	{
		UpdateToInventory(GetOwner()->FindComponentByClass<UBaseInventoryComponent>());
	}
}

Then to tie this all together, inside the bag actor I trigger an inventory update on begin play and bind to my inventory component’s “InventoryUpdateDelegate” so that every time it updates it triggers an update to the UI, see here:

This displays the items as so:

But it doesn’t have any interaction. In order to do that I created a “GrabGridItemWidgetComponent” function that takes in a primitive component, finds that primitive inside of the “GridItemSlotColliders” TMap and uses the associated ID on the “AddedWidgetComponents” TMap to find a widget component to return. See here:

	// The in component is the one hit which is used to find associated widget component
	UFUNCTION(BlueprintCallable)
	UWidgetComponent* GrabGridItemWidgetComponent(UPrimitiveComponent* InCollisionComponent);
UWidgetComponent* UGridWidgetComponent::GrabGridItemWidgetComponent(UPrimitiveComponent* InCollisionComponent)
{
	int32* IDPtr = GridItemSlotColliders.Find(InCollisionComponent);

	if (IDPtr == nullptr) return nullptr;

	int32 FoundID = *IDPtr;

	UWidgetComponent** WidgetComponentPtr = AddedWidgetComponents.Find(FoundID);

	if (WidgetComponentPtr == nullptr) return nullptr;

	return *WidgetComponentPtr;
}

This widget component can then be moved around and manipulated but in order to place it back onto another grid widget component I created the “PlaceGridItemWidgetComponent” function.

It takes in a context actor used by the inventory system, a primitive that was hit and was created as part of the grid widget component and the widget component that is currently being held and should have all the information about where it came from.

It uses all the inputs to move the item represented by the widget component to the location represented by the primitive and then returns the widget component to it’s original location. This is done because it’s easier then swapping the location of 2 widget components. See here:

	UFUNCTION(BlueprintCallable)
	bool PlaceGridItemWidgetComponent(AActor* ContextActor, UPrimitiveComponent* InCollisionComponent, UWidgetComponent* InWidgetComponent);
bool UGridWidgetComponent::PlaceGridItemWidgetComponent(AActor* ContextActor, UPrimitiveComponent* InCollisionComponent, UWidgetComponent* InWidgetComponent)
{
	// Check that both are valid
	if (!IsValid(InCollisionComponent) || !IsValid(InWidgetComponent)) return false;

	// If they are get a pointer to ID of the collider that was hit
	int32* InventoryIDPtr1 = GridItemSlotColliders.Find(InCollisionComponent);

	// If we didn't find one, the return
	if (InventoryIDPtr1 == nullptr) return false;

	// Check that the widget component's parent is a grid
	UGridWidgetComponent* OtherGridWidgetParent = Cast<UGridWidgetComponent>(InWidgetComponent->GetAttachParent());

	// If it is not then return
	if (!IsValid(OtherGridWidgetParent)) return false;

	// return the widget component back to it's original location
	OtherGridWidgetParent->ResetGridItemWidgetTransform(InWidgetComponent);

	// Get the owning actor of the widget
	AActor* WidgetOwner = InWidgetComponent->GetOwner();

	// If it is not valid then return
	if (!IsValid(WidgetOwner)) return false;

	// Use that actor and get it's inventory component
	UBaseInventoryComponent* OtherInventoryComponent = WidgetOwner->FindComponentByClass<UBaseInventoryComponent>();

	// if it is not valid then return
	if (!IsValid(OtherInventoryComponent)) return false;

	// Get this owner
	AActor* ThisOwner = GetOwner();

	// Check if it's valid
	if (!IsValid(ThisOwner)) return false;

	// Then get this owner's inventory component
	UBaseInventoryComponent* InventoryComponent = ThisOwner->FindComponentByClass<UBaseInventoryComponent>();

	// return false if not valid
	if (!IsValid(InventoryComponent)) return false;

	// Get a ptr to the id in the other grid widget associated with the widget component
	const int32* InventoryIDPtr2 = OtherGridWidgetParent->AddedWidgetComponents.FindKey(InWidgetComponent);

	// Check if its a nullptr
	if (InventoryIDPtr2 == nullptr) return false;

	FInventoryItem OtherItem;
	
	// Then get the item from the other inventory using the ID gotten from the widget component
	OtherInventoryComponent->GetItemFromID(OtherItem, *InventoryIDPtr2);

	// Then move the item from the other inventory into this one
	bool bSuccess = InventoryComponent->MoveItem(OtherInventoryComponent, OtherItem.ItemInstance, *InventoryIDPtr1, UOrganiseMyBagsStatics::GetNetworkContextFromActor(ContextActor));

	return bSuccess;
}

Because the UI is asking the inventory to make a change it will trigger an inventory update and automatically update this widget component and represent accurate information.

This function also uses another function called “ResetGridItemWidgetTransform” which simply resets the location of the widget component to where it came from, see here:

	UFUNCTION(BlueprintCallable)
	void ResetGridItemWidgetTransform(UWidgetComponent* InGridItemWidgetComponent);
void UGridWidgetComponent::ResetGridItemWidgetTransform(UWidgetComponent* InGridItemWidgetComponent)
{
	if (!IsValid(InGridItemWidgetComponent)) return;

	const int32* IDPtr = AddedWidgetComponents.FindKey(InGridItemWidgetComponent);

	if (IDPtr != nullptr)
	{
		int32 FoundID = *IDPtr;

		InGridItemWidgetComponent->SetRelativeTransform(IDTransforms[FoundID]);
	}
}

Part 3, PLAYER CONTROLLER:

In the C++ base class for the player controller there is a member variable pointer to a scene component called “HeldComponent.” It is used as part of this drag and drop system to maintain a reference to what is being dragged and casted to a primitive so that it can be used to specify what component to ignore when tracing from the mouse. See here:

	// Component currently being held as part of the drag and drop
	UPROPERTY(BlueprintGetter = GetHeldComponent, BlueprintSetter = SetHeldComponent)
	TObjectPtr<USceneComponent> HeldComponent = nullptr;

	UPROPERTY()
	TObjectPtr<UPrimitiveComponent> HeldComponentPrimitive = nullptr;
public:
	
	UFUNCTION(BlueprintGetter)
	FORCEINLINE USceneComponent* GetHeldComponent() const { return HeldComponent; }
	UFUNCTION(BlueprintSetter)
	FORCEINLINE void SetHeldComponent(USceneComponent* InHeldComponent) { HeldComponent = InHeldComponent; HeldComponentPrimitive = Cast<UPrimitiveComponent>(InHeldComponent); }

    UWorld* World = GetWorld();

    if (IsValid(World))
    {
        FCollisionQueryParams Params;
        Params.AddIgnoredComponent(HeldComponentPrimitive);

        World->LineTraceSingleByChannel(
            InputLocationData.CurrentHitResult,
            InputLocationData.CurrentWorldMouseLocation,
            InputLocationData.CurrentWorldMouseLocation + InputLocationData.CurrentWorldMouseDirection * InputTraceLength,
            ECollisionChannel::ECC_Visibility, Params);
    }

I have an event that gets called when the player left clicks which sends through a struct of data that can contain information about a line trace when the input starts, is triggered and ends as well as mouse position on screen and in world space. See here:

// Position data collected from input
USTRUCT(BlueprintType, Blueprintable)
struct ORGANISEMYBAGS_API FInputPositionData
{
public:

	GENERATED_BODY()

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
	FVector StartWorldMouseLocation = FVector::ZeroVector;
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
	FVector StartWorldMouseDirection = FVector::ZeroVector;

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
	FVector2D StartScreenLocation = FVector2D::ZeroVector;

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
	FHitResult StartHitResult = FHitResult();

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
	FVector CurrentWorldMouseLocation = FVector::ZeroVector;
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
	FVector CurrentWorldMouseDirection = FVector::ZeroVector;

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
	FVector2D CurrentScreenLocation = FVector2D::ZeroVector;

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
	FHitResult CurrentHitResult = FHitResult();

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
	FVector EndWorldMouseLocation = FVector::ZeroVector;
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
	FVector EndWorldMouseDirection = FVector::ZeroVector;

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
	FVector2D EndScreenLocation = FVector2D::ZeroVector;

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
	FHitResult EndHitResult = FHitResult();

	// Screen Data
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
	float ViewportScale = 0.f;

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
	FVector2D ScreenLocation = FVector2D::ZeroVector;

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
	FVector2D ScreenSize = FVector2D::ZeroVector;

	void Reset()
	{
		StartWorldMouseLocation = FVector::ZeroVector;
		StartWorldMouseDirection = FVector::ZeroVector;

		StartScreenLocation = FVector2D::ZeroVector;

		StartHitResult = FHitResult();

		CurrentWorldMouseLocation = FVector::ZeroVector;
		CurrentWorldMouseDirection = FVector::ZeroVector;

		CurrentScreenLocation = FVector2D::ZeroVector;

		CurrentHitResult = FHitResult();

		EndWorldMouseLocation = FVector::ZeroVector;
		EndWorldMouseDirection = FVector::ZeroVector;

		EndScreenLocation = FVector2D::ZeroVector;

		EndHitResult = FHitResult();

		ViewportScale = 0.f;
		ScreenLocation = FVector2D::ZeroVector;
		ScreenSize = FVector2D::ZeroVector;
	}
};

When the first event gets called it only contains starting line trace data which is used to try and grab the widget component from the grid widget and store it in held component pointer as seen here:

When the trigger event is called I just update the held component’s location to the mouse’s world location shifted by some arbitrary value, as seen here:

Finally, when the last event is triggered, just see if the mouse ended on an appropriate grid widget location.


Here is the final result:

Before implementing this solution I would like to recommend a critical analysis of it because there are certain components of the solution I’m not sure are best practice. If there are any mistakes or clarifications needed please let me know!