itch.io is community of indie game creators and players

Devlogs

Devlog 5: Drag and Drop

Fyolai Puzzle Game
A downloadable game for Windows

As a reminder for those who haven't read my previous devlogs, for this game I am following a UE4 jigsaw puzzle Blueprints tutorial and adapting it parts of it into C++ for practice. (Here's the tutorial below:)

Unreal Engine 4 Tutorial - Jigsaw Puzzle Part 5: Drag and Drop

In the tutorial, Ryan Laley  override the "OnMouseButtonDown" and "OnDragDetected" functions. I tried to override them in C++. I searched for information on how to implement these two functions, and I found these two forums:

How do I override OnMouseButton in C++

Creating Drag and Drop UI using C++

After reading them, this is the code I came up with the code below.

...and this code didn't work AT ALL.

PuzzlePiece.h

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "PyzzlePiece.generated.h"
class UJigsawPieceDragUI;
// We make the class abstract, as we don't want to create
// instances of this, instead we want to create instances
// of our UMG Blueprint subclass.
UCLASS(Abstract)
class PUZZLEGAME_API UPyzzlePiece : public UUserWidget
{
GENERATED_BODY()
public:
/* Image Widget*/
UPROPERTY(BlueprintReadWrite, Category = "Puzzle", meta = (BindWidget))
class UImage* PuzzlePieceImageWidget;
UPROPERTY(BlueprintReadWrite, Category = "Puzzle")
class UMaterialInterface* PuzzleMaterial;
//not meant to be edited in editor, created at runtime
UPROPERTY(BlueprintReadWrite, Category = "Puzzle")
class UMaterialInstanceDynamic* DynamicMaterial;
UPROPERTY(BlueprintReadWrite, Category = "Puzzle", meta = (ExposeOnSpawn = "true"))
int NumberOfPieces;
UPROPERTY(BlueprintReadWrite, Category = "Puzzle", meta = (ExposeOnSpawn = "true"))
FVector2D PuzzleCoordinate = FVector2D(2, 3);
UPROPERTY(BlueprintReadWrite, Category = "Puzzle", meta = (ExposeOnSpawn = "true"))
UTexture* JigsawImage;
/* Delegates/ Event Dispatchers*/
virtual FReply OnMouseButtonDown(FGeometry MyGeometry, const FPointerEvent& MouseEvent);
virtual FReply OnDragDetected(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent);
void CreateJigsawDragDrop(UJigsawPieceDragUI* JigsawPieceDragUI);
// virtual FReply NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent);
/* TSubClassOf Variables */
//this value will be set in blueprints
UPROPERTY(EditAnywhere, Category = "Puzzle")
TSubclassOf<class UUserWidget> JigsawPieceDragUIRef;
UPROPERTY(EditAnywhere, Category = "Puzzle")
TSubclassOf<class UDragDropOperation> JigsawDragDrop;
protected:
// Doing setup in the C++ constructor is not as useful as using NativeConstruct.
virtual void NativeConstruct() override;
UJigsawPieceDragUI* CreateJigsawPieceDragUI();
};

PuzzlePiece.cpp

// Fill out your copyright notice in the Description page of Project Settings.
#include "Widgets/PyzzlePiece.h"
#include "Components/Image.h"
#include "Kismet/KismetMaterialLibrary.h"
#include "Blueprint/WidgetBlueprintLibrary.h"
#include "JigsawPieceDragUI.h"
#include "JigsawDragDrop.h"
FReply UPyzzlePiece::OnMouseButtonDown(FGeometry MyGeometry, const FPointerEvent& MouseEvent)
{
FEventReply ReplyResult = UWidgetBlueprintLibrary::DetectDragIfPressed(MouseEvent, this, EKeys::LeftMouseButton);
//This function returns FReply. You use NativeReply to convert the FEventReply above to FReply.
if (GEngine)
{
FString Message = FString::Printf(TEXT("MouseButtonPressed"));
GEngine->AddOnScreenDebugMessage(1, 10.f, FColor::Green, Message);
}
return ReplyResult.NativeReply; 
}
FReply UPyzzlePiece::OnDragDetected(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
UJigsawPieceDragUI* JigsawPieceDragUI = CreateJigsawPieceDragUI();
CreateJigsawDragDrop(JigsawPieceDragUI);
return FReply::Unhandled();
}
void UPyzzlePiece::CreateJigsawDragDrop(UJigsawPieceDragUI* JigsawPieceDragUI)
{
if (JigsawDragDrop)
{
UDragDropOperation* JigsawDragDropInstance = UWidgetBlueprintLibrary::CreateDragDropOperation(JigsawDragDrop);
UJigsawDragDrop* CastJigsawDragDropInstance = Cast<UJigsawDragDrop>(JigsawDragDropInstance);
CastJigsawDragDropInstance->SetPieceCoordinate(PuzzleCoordinate);
CastJigsawDragDropInstance->SetPiece(this);
CastJigsawDragDropInstance->DefaultDragVisual = JigsawPieceDragUI;
if (GEngine && CastJigsawDragDropInstance)
{
FString Message = FString::Printf(TEXT("Jigsaw Drag Drop Created"));
GEngine->AddOnScreenDebugMessage(2, 10.f, FColor::Yellow, Message);
}
}
}
/*
FReply UPyzzlePiece::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
return FReply();
}
*/
void UPyzzlePiece::NativeConstruct()
{
if (PuzzleMaterial && NumberOfPieces && JigsawImage && PuzzlePieceImageWidget)
{
DynamicMaterial = UKismetMaterialLibrary::CreateDynamicMaterialInstance(this, PuzzleMaterial);
DynamicMaterial->SetScalarParameterValue(FName("NumberOfPieces"), NumberOfPieces);
FVector4 Value(PuzzleCoordinate.X, PuzzleCoordinate.Y, 0.f, 0.f);
DynamicMaterial->SetVectorParameterValue(FName("PuzzleCoordinate"), Value);
DynamicMaterial->SetTextureParameterValue(FName("JigsawImage"), JigsawImage);
PuzzlePieceImageWidget->SetBrushFromMaterial(DynamicMaterial);
//MyWidget = CreateDefaultSubobject<UWidgetComponent>("Widget");
//static ConstructorHelpers::FClassFinder<UUserWidget> hudWidgetObj(TEXT("/Game/HUD/SelectableActorHUD_Widget"));
}
}
UJigsawPieceDragUI* UPyzzlePiece::CreateJigsawPieceDragUI()
{
if (JigsawPieceDragUIRef)
{
// the function 'createwidget' only takes UUserWidget*, so I can't use UJigsawPieceDragUI*.
UUserWidget* JigsawPieceDragInstance = CreateWidget(this, JigsawPieceDragUIRef);
// I have to first use a UUserWidget* and then cast it to a UJigsawPieceDragUI* to set values on it
UJigsawPieceDragUI* CastJigsawPieceDragInstance = Cast<UJigsawPieceDragUI>(JigsawPieceDragInstance);
CastJigsawPieceDragInstance->SetJigsawImage(JigsawImage);
CastJigsawPieceDragInstance->SetPuzzleCoordinate(PuzzleCoordinate);
CastJigsawPieceDragInstance->SetNumberOfPieces(NumberOfPieces);
if (GEngine && CastJigsawPieceDragInstance)
{
FString Message = FString::Printf(TEXT("Jigsaw Piece Created"));
GEngine->AddOnScreenDebugMessage(3, 10.f, FColor::Red, Message);
}
return CastJigsawPieceDragInstance;
}
return nullptr;
}

As you can see, I inserted "AddOnScreenDebugMessage" debugs to figure out if my functions were even being called, and they were not. I couldn't quite figure out the problem, but eventually I did.

Now, this might seem obvious if you know a lot about UE5, but when I tried to override this function, I got an error:

virtual FReply OnMouseButtonDown(FGeometry MyGeometry, const FPointerEvent& MouseEvent);

This is because though "OnMouseButtonDown" has the same name as the blueprint version of the function, the equivalent C++ function is actually called "NativeOnMouseButtonDown". Anyhow, I tried to figure out how to use it, so I decided I had to try following this one C++ tutorial that I'd looked at earlier and taken parts from, but didn't completely copy. I created new classes and followed the new tutorial from beginning to end, and I finally felt like I could go back to this game's code and rewrite it.

At the end of the game development process, this is the code I had (and it works!):

PuzzlePiece.h

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "PyzzlePiece.generated.h"
class UJigsawPieceDragUI;
// We make the class abstract, as we don't want to create
// instances of this, instead we want to create instances
// of our UMG Blueprint subclass.
UCLASS(Abstract)
class PUZZLEGAME_API UPyzzlePiece : public UUserWidget
{
GENERATED_BODY()
public:
/* Image Widget*/
UPROPERTY(BlueprintReadWrite, Category = "Puzzle", meta = (BindWidget))
class UImage* PuzzlePieceImageWidget;
UPROPERTY(BlueprintReadWrite, Category = "Puzzle")
class UMaterialInterface* PuzzleMaterial;
//not meant to be edited in editor, created at runtime
UPROPERTY(BlueprintReadWrite, Category = "Puzzle")
class UMaterialInstanceDynamic* DynamicMaterial;
UPROPERTY(BlueprintReadWrite, Category = "Puzzle", meta = (ExposeOnSpawn = "true"))
int NumberOfPieces;
UPROPERTY(BlueprintReadWrite, Category = "Puzzle", meta = (ExposeOnSpawn = "true"))
FVector2D PuzzleCoordinate = FVector2D(2, 3);
UPROPERTY(BlueprintReadWrite, Category = "Puzzle", meta = (ExposeOnSpawn = "true"))
UTexture* JigsawImage;
/* Mouse Functions */
virtual FReply NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
virtual void NativeOnDragDetected(const FGeometry& MyGeometry, const FPointerEvent& InMouseEvent, UDragDropOperation*& OutOperation) override;
//virtual void NativeOnDragLeave(const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation) override;
/* TSubClassOf Variables */
//this value will be set in blueprints
UPROPERTY(EditAnywhere, Category = "Puzzle")
TSubclassOf<class UUserWidget> JigsawPieceDragUIRef;
UPROPERTY(EditAnywhere, Category = "Puzzle")
TSubclassOf<class UDragDropOperation> JigsawDragDrop;
protected:
// Doing setup in the C++ constructor is not as useful as using NativeConstruct.
virtual void NativeConstruct() override;
/* My Functions */
void CreateJigsawDragDrop(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent, UDragDropOperation*& OutOperation, UJigsawPieceDragUI* WidgetReference);
UJigsawPieceDragUI* CreateJigsawPieceDragUI();
};

PuzzlePiece.cpp

// Fill out your copyright notice in the Description page of Project Settings.
#include "Widgets/PyzzlePiece.h"
#include "Components/Image.h"
#include "Kismet/KismetMaterialLibrary.h"
#include "Blueprint/WidgetBlueprintLibrary.h"
#include "JigsawPieceDragUI.h"
#include "JigsawDragDrop.h"
FReply UPyzzlePiece::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
FEventReply ReplyResult = UWidgetBlueprintLibrary::DetectDragIfPressed(InMouseEvent, this, EKeys::LeftMouseButton);
//This function returns FReply. You use NativeReply to convert the FEventReply above to FReply.
/*
if (GEngine)
{
FString Message = FString::Printf(TEXT("MouseButtonPressed"));
GEngine->AddOnScreenDebugMessage(1, 10.f, FColor::Green, Message);
}
*/
return ReplyResult.NativeReply;
}
void UPyzzlePiece::NativeOnDragDetected(const FGeometry& MyGeometry, const FPointerEvent& InMouseEvent, UDragDropOperation*& OutOperation)
{
Super::NativeOnDragDetected(MyGeometry, InMouseEvent, OutOperation);
UJigsawPieceDragUI* JigsawPieceDragUI = CreateJigsawPieceDragUI();
CreateJigsawDragDrop(MyGeometry, InMouseEvent, OutOperation, JigsawPieceDragUI);
//this part is from Jigsaw Puzzle tutorial part 5
SetVisibility(ESlateVisibility::Hidden);
}
/*  I removed this function because with it it was impossible to get the piece to snap back to 
its original spot if the player didn't place the piece in the correct slot
void UPyzzlePiece::NativeOnDragLeave(const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation)
{
RemoveFromParent();
}
*/
void UPyzzlePiece::CreateJigsawDragDrop(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent, UDragDropOperation*& OutOperation, UJigsawPieceDragUI* WidgetReference)
{
if (JigsawDragDrop)
{
UDragDropOperation* JigsawDragDropInstance = UWidgetBlueprintLibrary::CreateDragDropOperation(JigsawDragDrop);
UJigsawDragDrop* CastJigsawDragDropInstance = Cast<UJigsawDragDrop>(JigsawDragDropInstance);
CastJigsawDragDropInstance->SetPieceCoordinate(PuzzleCoordinate);
//the line below is replacing this line in the drag drop tutorial:
// CastJigsawDragDropInstance->WidgetReference = this;
CastJigsawDragDropInstance->SetPiece(this);
this->SetVisibility(ESlateVisibility::HitTestInvisible);
CastJigsawDragDropInstance->DragOffset = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition());
CastJigsawDragDropInstance->DefaultDragVisual = WidgetReference;
CastJigsawDragDropInstance->Pivot = EDragPivot::MouseDown;
OutOperation = CastJigsawDragDropInstance;
/*
if (GEngine && CastJigsawDragDropInstance)
{
FString Message = FString::Printf(TEXT("Jigsaw Drag Drop Created"));
GEngine->AddOnScreenDebugMessage(2, 10.f, FColor::Yellow, Message);
}
*/
}
}
void UPyzzlePiece::NativeConstruct()
{
if (PuzzleMaterial && NumberOfPieces && JigsawImage && PuzzlePieceImageWidget)
{
DynamicMaterial = UKismetMaterialLibrary::CreateDynamicMaterialInstance(this, PuzzleMaterial);
DynamicMaterial->SetScalarParameterValue(FName("NumberOfPieces"), NumberOfPieces);
FVector4 Value(PuzzleCoordinate.X, PuzzleCoordinate.Y, 0.f, 0.f);
DynamicMaterial->SetVectorParameterValue(FName("PuzzleCoordinate"), Value);
DynamicMaterial->SetTextureParameterValue(FName("JigsawImage"), JigsawImage);
PuzzlePieceImageWidget->SetBrushFromMaterial(DynamicMaterial);
}
}
UJigsawPieceDragUI* UPyzzlePiece::CreateJigsawPieceDragUI()
{
if (JigsawPieceDragUIRef)
{
/*
if (GEngine)
{
FString Message = FString::Printf(TEXT("CreateJigsawPieceDragUI()"));
GEngine->AddOnScreenDebugMessage(3, 10.f, FColor::Yellow, Message);
}
*/
//UDragDropOperation* JigsawDragDropInstance = UWidgetBlueprintLibrary::CreateDragDropOperation(JigsawDragDrop);
//UJigsawDragDrop* CastJigsawDragDropInstance = Cast<UJigsawDragDrop>(JigsawDragDropInstance);
// the function 'createwidget' only takes UUserWidget*, so I can't use UJigsawPieceDragUI*.
UUserWidget* JigsawPieceDragInstance = CreateWidget(this, JigsawPieceDragUIRef);
// I have to first use a UUserWidget* and then cast it to a UJigsawPieceDragUI* to set values on it
UJigsawPieceDragUI* CastJigsawPieceDragUI = Cast<UJigsawPieceDragUI>(JigsawPieceDragInstance);
CastJigsawPieceDragUI->SetJigsawImage(JigsawImage);
CastJigsawPieceDragUI->SetPuzzleCoordinate(PuzzleCoordinate);
CastJigsawPieceDragUI->SetNumberOfPieces(NumberOfPieces);
/*
if (GEngine && CastJigsawPieceDragUI)
{
FString Message = FString::Printf(TEXT("Jigsaw Piece Created"));
GEngine->AddOnScreenDebugMessage(4, 10.f, FColor::Red, Message);
}
*/
return CastJigsawPieceDragUI;
}
return nullptr;
}

JigsawSlot.h

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "JigsawSlot.generated.h"
//delegates must be created outside fo the class declaration
//delegate type declarations must start with F
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FPieceCorrectlyPlacedDelegate);
// We make the class abstract, as we don't want to create
// instances of this, instead we want to create instances
// of our UMG Blueprint subclass.
UCLASS(Abstract)
class PUZZLEGAME_API UJigsawSlot : public UUserWidget
{
GENERATED_BODY()
public:
/* Widgets*/
UPROPERTY(BlueprintReadWrite, Category = "Puzzle", meta = (BindWidget))
class USizeBox* SizeBox;
UPROPERTY(BlueprintReadWrite, Category = "Puzzle", meta = (BindWidget))
class UImage* Image;
/* Variables*/
UPROPERTY(BlueprintReadWrite, Category = "Puzzle")
class UMaterialInterface* PuzzleMaterial;
UPROPERTY(BlueprintReadWrite, Category = "Puzzle", meta = (ExposeOnSpawn = "true"))
FVector2D SlotCoordinate = FVector2D(0, 0);
UPROPERTY(BlueprintReadWrite, Category = "Puzzle", meta = (ExposeOnSpawn = "true"))
UTexture* JigsawImage;
UPROPERTY(BlueprintReadWrite, Category = "Puzzle", meta = (ExposeOnSpawn = "true"))
int NumberOfPieces;
UPROPERTY(BlueprintReadWrite, Category = "Puzzle")
class UMaterialInstanceDynamic* DynamicMaterial;
/* Delegates/ Event Dispatchers*/
UPROPERTY(BlueprintAssignable, Category = "Delegate")
FPieceCorrectlyPlacedDelegate OnPieceCorrectlyPlaced;
UFUNCTION(BlueprintImplementableEvent)
void PieceCorrectlyPlaced();
protected:
// Doing setup in the C++ constructor is not as useful as using NativeConstruct.
virtual void NativeConstruct() override;
virtual bool NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation) override;
void AssignSlotCoordinateTexture();
};

JigsawSlot.cpp

// Fill out your copyright notice in the Description page of Project Settings.
#include "JigsawSlot.h"
#include "Components/Image.h"
#include "Components/SizeBox.h"
#include "Blueprint/WidgetTree.h"
#include "JigsawDragDrop.h"
#include "Kismet/KismetMaterialLibrary.h"
#include "Components/SizeBoxSlot.h"
#include "Widgets/PyzzlePiece.h"
void UJigsawSlot::NativeConstruct()
{
/* The code below was causing problems with the widget's visibility at runtime, so I just commented it out.
* It didn't affect anything in the blueprint tbh.
SizeBox = WidgetTree->ConstructWidget<USizeBox>(USizeBox::StaticClass(), TEXT("RootWidget"));
WidgetTree->RootWidget = SizeBox;
SizeBox->AddChild(Image);
*/
OnPieceCorrectlyPlaced.AddDynamic(this, &UJigsawSlot::PieceCorrectlyPlaced);
}
bool UJigsawSlot::NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation)
{
Super::NativeOnDrop(InGeometry, InDragDropEvent, InOperation);
//creating an object of our custom DragDropOperation class
UJigsawDragDrop* DragDropResult = Cast<UJigsawDragDrop>(InOperation);
if (!IsValid(DragDropResult))
{
UE_LOG(LogTemp, Warning, TEXT("Cast returned null."))
return false;
}
//checking if the player is dropping the puzzle piece into the correct slot
if (DragDropResult->PieceCoordinate == SlotCoordinate)
{
AssignSlotCoordinateTexture();
DragDropResult->Piece->RemoveFromParent();
if (OnPieceCorrectlyPlaced.IsBound())
{
OnPieceCorrectlyPlaced.Broadcast();
/*
if (GEngine)
{
FString Message = FString::Printf(TEXT("OnPieceCorrectlyPlaced Is Bound"));
GEngine->AddOnScreenDebugMessage(10, 30.f, FColor::White, Message);
}
*/
}
return true;
}
return false;
}
void UJigsawSlot::AssignSlotCoordinateTexture()
{
DynamicMaterial = UKismetMaterialLibrary::CreateDynamicMaterialInstance(this, PuzzleMaterial);
DynamicMaterial->SetScalarParameterValue(FName("NumberOfPieces"), NumberOfPieces);
FVector4 Value(SlotCoordinate.X, SlotCoordinate.Y, 0.f, 0.f);
DynamicMaterial->SetVectorParameterValue(FName("PuzzleCoordinate"), Value);
DynamicMaterial->SetTextureParameterValue(FName("JigsawImage"), JigsawImage);
Image->SetBrushFromMaterial(DynamicMaterial);
//This is equivalent of blueprint node 'slot as size box slot'
USizeBoxSlot* SizeBoxSlot = CastChecked<USizeBoxSlot>(SizeBox->GetSlots()[0]); 
SizeBoxSlot->SetPadding(FMargin(0, 0, 0, 0));
Image->SetColorAndOpacity(FLinearColor::White);
   
/*
if (GEngine && SizeBoxSlot)
{
FString Message = FString::Printf(TEXT("SizeBoxSlot Exists"));
GEngine->AddOnScreenDebugMessage(5, 10.f, FColor::White, Message);
}
*/
}

Well, that's all from me! Join me soon for a Post-Mortem.

Download Fyolai Puzzle Game
Leave a comment