SteamWrecked is a survival exploration game developed during our third game project at Futuregames Stockholm. The game takes place in a deserted land filled with natural hazards and autonomous mechanical lifeforms left behind by its previous inhabitants. After crash-landing, the player must gather resources and blueprints to survive, upgrade their equipment and airship, and uncover the truth behind the fall of the civilization that once inhabited the land.

UCLASS(BlueprintType)
class PROJECTSANDWALKER_API UInventoryItem : public UDataAsset
{
GENERATED_BODY()
//
// Properties
//
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
FString ItemName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
FString ItemDescription;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
UTexture2D* ItemIcon;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties",
meta=(Bitmask, BitmaskEnum = "EInventoryItemType"))
int32 InventoryItemType;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
FIntPoint Dimension;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
FIntPoint ItemPivot;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
TArray<FIntPoint> BlocksOffsetFromPivot;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
TArray<FExtraData> ExtraData;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
TSubclassOf<AWorldItem> WorldItemClass;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
bool bIsRecipe;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
UTexture2D* CraftedItemIcon;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
TArray<FCraftingMaterialEntry> Material;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
UInventoryItem* CraftedItem;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
bool bIsTool;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties", meta = (EditCondition = "bIsTool"))
TSubclassOf<AToolBase> ToolClass;
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (AllowPrivateAccess = true), Category="Inventory Item | Properties")
FVector2D IconSize;
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (AllowPrivateAccess = true), Category="Inventory Item | Properties")
FVector2D IconPosition;
#pragma region EDITOR
#if WITH_EDITORONLY_DATA
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (AllowPrivateAccess = true), Category="Inventory Item | Editor")
FIntPoint TopLeft;
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (AllowPrivateAccess = true), Category="Inventory Item | Editor")
FIntPoint BottomRight;
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (AllowPrivateAccess = true), Category="Inventory Item | Editor")
UBlueprint* BlueprintAsset;
#endif
#if WITH_EDITOR
UFUNCTION(BlueprintCallable)
void SetupInstance(const FString& Name, const FString& Description, UTexture2D* Icon, const FVector2D& Size,
const FVector2D& Position, const FIntPoint& Pivot, const TArray<FIntPoint>& Blocks,
const int32& Type, const TArray<FCraftingMaterialEntry>& CraftingIngredients);
UFUNCTION(BlueprintCallable)
void SetDimension(FIntPoint NewTopLeft, FIntPoint NewBottomRight);
#endif
#pragma endregion
};UCLASS(Blueprintable, BlueprintType)
class PROJECTSANDWALKER_API AWorldItem : public AActor
{
GENERATED_BODY()
public:
AWorldItem();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory|Item")
UInventoryItem* InventoryItem;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inventory|Item")
FGuid Guid;
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
};Our game uses a Tetris-style grid inventory, where each item can occupy a different number and arrangement of tiles. While item icons and occupied tiles could be configured manually through numerical values, the process was time-consuming and difficult to visualize. To streamline content creation, I developed a custom Item Editor with two main features:
// Sets default values
ASteamPunkShip::ASteamPunkShip()
{
BoxComp = CreateDefaultSubobject<UBoxComponent>(TEXT("Box"));
SetRootComponent(BoxComp);
Generator = CreateDefaultSubobject<UChildActorComponent>("Generator");
Generator->SetupAttachment(RootComponent);
CameraSpringArm = CreateDefaultSubobject<USpringArmComponent>("Spring Arm");
CameraSpringArm->SetupAttachment(RootComponent);
ShipCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("ActualCamera"));
ShipCamera->SetupAttachment(CameraSpringArm, USpringArmComponent::SocketName);
// Set this pawn to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
SetActorRotation(GetActorRotation() + FRotator(0, 90, 0));
}
void ASteamPunkShip::SetAdditionalVelocity(float InAdditionalVelocity)
{
AdditionalVelocity = InAdditionalVelocity;
}
void ASteamPunkShip::AddAdditionalVelocity(float InAdditionalVelocity)
{
AdditionalVelocity += InAdditionalVelocity;
}
float ASteamPunkShip::GetAdditionalVelocity()
{
return AdditionalVelocity;
}
void ASteamPunkShip::SetAdditionalVector(FVector& InAdditionalVector)
{
AdditionalVector = InAdditionalVector;
}
void ASteamPunkShip::ResetAdditionalVector()
{
AdditionalVector = FVector(0, 0, 0);
}
void ASteamPunkShip::ShipTakeDamage(float Amount, FVector ImpulseDirection)
{
OnShipDamagedDelegate.Broadcast();
ImpulseForce = ImpulseDirection * DamageImpactForce;
float NewAmount = Health - Amount;
Health = FMath::Max(NewAmount, 0);
if (Health == 0)
{
IsFunctional = false;
OnStateChangedDelegate.Broadcast(IsFunctional);
}
}
void ASteamPunkShip::SetHealth(float Amount)
{
Health = FMath::Min(Amount, MaxHealth);
if (Health > 0)
{
IsFunctional = true;
OnStateChangedDelegate.Broadcast(IsFunctional);
}
}
float ASteamPunkShip::GetHealth()
{
return Health;
}void ASteamPunkShip::Move_Implementation(const FVector& Value)
{
if (Fuel <= 0 || Accelaration == 0 || !IsFunctional)
{
SteerDirection = 0;
ForwardAccel = 0;
IsAccelerating = false;
return;
}
if (Controller != nullptr)
{
SteerDirection = Value.X;
ForwardAccel = Value.Y;
if (Value.Y != 0)
{
IsAccelerating = true;
}
if (Value.X != 0)
{
IsAccelerating = true;
}
}
}
void ASteamPunkShip::MoveStop_Implementation()
{
IsAccelerating = false;
ForwardAccel = 0;
SteerDirection = 0;
}
void ASteamPunkShip::Look_Implementation(const FVector& Value)
{
if (Controller != nullptr)
{
// add yaw and pitch input to controller
AddControllerYawInput(Value.X * RotationMult);
AddControllerRollInput(Value.Y * RotationMult);
}
}
void ASteamPunkShip::JumpTrigger_Implementation()
{
if (Fuel <= 0 || Accelaration == 0 || !IsFunctional)
{
AccelUp = false;
IsAccelerating = false;
return;
}
AccelUp = true;
IsAccelerating = true;
}
void ASteamPunkShip::JumpStop_Implementation()
{
AccelUp = false;
IsAccelerating = false;
}
void ASteamPunkShip::Crouch_Implementation()
{
if (Accelaration == 0 || !IsFunctional)
{
return;
}
AccelDown = true;
}
void ASteamPunkShip::CrouchStop_Implementation()
{
AccelDown = false;
}
void ASteamPunkShip::Interact_Implementation()
{
if (AController* DriverController = GetController())
{
Driver->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
Driver->SetActorHiddenInGame(false);
Driver->SetActorLocation(CameraSpringArm->GetComponentLocation());
DriverController->Possess(Driver);
Driver = nullptr;
}
UE_LOG(LogTemp, Display, TEXT("Interact"));
}void ASteamPunkShip::UpdateImpulse(const float& DeltaTime)
{
if (ImpulseForce.Size() <= 0)
return;
SetActorLocation(GetActorLocation() + (ImpulseForce * DeltaTime), true);
FVector NewForce = (ImpulseForce - (ImpulseForce.GetSafeNormal() * ImpulseReduceRate * DeltaTime));
if (NewForce.Dot(ImpulseForce) < 0)
ImpulseForce = FVector(0, 0, 0);
else
ImpulseForce = NewForce;
}
void ASteamPunkShip::UpdateVerticalMovement(const float& DeltaTime)
{
if (AccelUp)
VerticalVelocity = FMath::Min(VerticalVelocity + (UpwardAccelaration * DeltaTime), MaxUpwardVelocity);
if (AccelDown)
VerticalVelocity = FMath::Max(VerticalVelocity + (DownwardAccelaration * DeltaTime), MaxDownwardVelocity);
if (VerticalVelocity == 0)
return;
if (VerticalVelocity > 0 && GetActorLocation().Z > HeightLimit)
{
VerticalVelocity = FMath::Max(VerticalVelocity - SuppressionForce * DeltaTime, 0);
}
SetActorLocation(GetActorLocation() + (GetActorUpVector() * VerticalVelocity * DeltaTime), true);
if (VerticalVelocity > 0)
{
VerticalVelocity = (VerticalVelocity + DownwardDrag * DeltaTime < 0)
? 0
: VerticalVelocity + DownwardDrag * DeltaTime;
}
else
{
VerticalVelocity = (VerticalVelocity + UpwardDrag * DeltaTime > 0)
? 0
: VerticalVelocity + UpwardDrag * DeltaTime;
}
}
void ASteamPunkShip::UpdateSteering(const float& DeltaTime)
{
if (SteerDirection != 0)
SteeringSpeed += DeltaTime * SteeringPower * SteerDirection;
if (SteeringSpeed == 0)
return;
if (SteeringSpeed > 0)
{
SteeringSpeed = (SteeringSpeed - SteeringDrag * DeltaTime <= 0)
? 0
: SteeringSpeed - SteeringDrag * DeltaTime;
}
else
{
SteeringSpeed = (SteeringSpeed + SteeringDrag * DeltaTime >= 0)
? 0
: SteeringSpeed + SteeringDrag * DeltaTime;
}
SetActorRotation(GetActorRotation() + FRotator(0, SteeringSpeed * DeltaTime, 0));
}
void ASteamPunkShip::UpdateMovement(const float& DeltaTime)
{
if (ForwardAccel != 0)
{
if (ForwardAccel < 0)
{
if (Velocity > 0)
{
Velocity -= Accelaration * DeltaTime;
}
Velocity = FMath::Max(0, Velocity);
}
else if (Velocity < MaxVelocity)
{
Velocity += Accelaration * DeltaTime ;
}
}
FVector Movement = FVector::ZeroVector;
if (Velocity == 0)
return;
Movement += GetActorRightVector() * Velocity * DeltaTime;
Movement += AdditionalVector * AdditionalVelocity * DeltaTime;
SetActorLocation(GetActorLocation() + Movement, true);
Velocity = (Velocity + Drag * DeltaTime <= 0) ? 0 : Velocity + Drag * DeltaTime;
}
// Called every frame
void ASteamPunkShip::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
UpdateSteering(DeltaTime);
UpdateVerticalMovement(DeltaTime);
UpdateMovement(DeltaTime);
UpdateImpulse(DeltaTime);
if (PotentialFuel <= 0)
return;
if (IsAccelerating)
{
Fuel -= DeltaTime;
//Fuel = FMath::Max(Fuel, 0);
PotentialFuel -= DeltaTime;
PotentialFuel = FMath::Max(PotentialFuel, 0);
for (int i = 0; i < PropellerRibbonNiagaraSystems.Num(); i++)
{
if (!PropellerRibbonNiagaraSystems[i]->IsActive())
{
PropellerRibbonNiagaraSystems[i]->Activate();
}
}
}
else
{
for (int i = 0; i < PropellerRibbonNiagaraSystems.Num(); i++)
{
if (PropellerRibbonNiagaraSystems[i]->IsActive())
{
PropellerRibbonNiagaraSystems[i]->Deactivate();
}
}
}
if (Fuel <= 0)
{
if (UInventoryComponent* Inventory = Generator->GetChildActor()->GetComponentByClass<UInventoryComponent>())
{
TArray<FInventoryItemPosition> Items = Inventory->GetAllItems();
FInventoryItemPosition* ItemPos = &Items[0];
if (!ItemPos)
{
return;
}
if (ItemPos->Item->InventoryItemType & static_cast<int>(EInventoryItemType::Burnable))
{
for (auto& ExtraData : ItemPos->Item->ExtraData)
{
if (ExtraData.Type != 6)
{
continue;
}
Inventory->TryRemoveItem(ItemPos->Item);
if (!Inventory->GetAllItems().IsEmpty())
Fuel = ExtraData.Value + Fuel;
if (UInventoryManager* InventoryManager = Inventory->InventoryManager)
{
InventoryManager->UpdateProgressBar(Inventory, PotentialFuel / MaxFuel);
}
break;
}
}
Inventory->RefreshUIElements();
}
}
}UINTERFACE(MinimalAPI, Blueprintable)
class UMountableBase : public UInterface
{
GENERATED_BODY()
};
class PROJECTSANDWALKER_API IMountableBase
{
GENERATED_BODY()
public:
// Sets default values for this pawn's properties
IMountableBase();
UFUNCTION(BlueprintNativeEvent, Category = "Input")
void PrimaryAction();
virtual void PrimaryAction_Implementation();
UFUNCTION(BlueprintNativeEvent, Category = "Input")
void PrimaryActionEnd();
virtual void PrimaryActionEnd_Implementation();
UFUNCTION(BlueprintNativeEvent, Category = "Input")
void SecondaryAction();
virtual void SecondaryAction_Implementation();
UFUNCTION(BlueprintNativeEvent, Category = "Input")
void Move(const FVector& Value);
virtual void Move_Implementation(const FVector& Value);
UFUNCTION(BlueprintNativeEvent, Category = "Input")
void MoveStop();
virtual void MoveStop_Implementation();
UFUNCTION(BlueprintNativeEvent, Category = "Input")
void Look(const FVector& Value);
virtual void Look_Implementation(const FVector& Value);
UFUNCTION(BlueprintNativeEvent, Category = "Input")
void RunStart();
virtual void RunStart_Implementation();
UFUNCTION(BlueprintNativeEvent, Category = "Input")
void RunStop();
virtual void RunStop_Implementation();
UFUNCTION(BlueprintNativeEvent, Category = "Input")
void JumpTrigger();
virtual void JumpTrigger_Implementation();
UFUNCTION(BlueprintNativeEvent, Category = "Input")
void JumpStart();
virtual void JumpStart_Implementation();
UFUNCTION(BlueprintNativeEvent, Category = "Input")
void JumpStop();
virtual void JumpStop_Implementation();
UFUNCTION(BlueprintNativeEvent, Category = "Input")
void Crouch();
virtual void Crouch_Implementation();
UFUNCTION(BlueprintNativeEvent, Category = "Input")
void CrouchStop();
virtual void CrouchStop_Implementation();
UFUNCTION(BlueprintNativeEvent, Category = "Input")
void Interact();
virtual void Interact_Implementation();
UFUNCTION(BlueprintNativeEvent, Category = "Input")
void ToggleInventory();
virtual void ToggleInventory_Implementation();
};void AMainController::BeginPlay()
{
Super::BeginPlay();
//Add Input Mapping Context
if (AMainController* PlayerController = Cast<AMainController>(this))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<
UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
{
Subsystem->AddMappingContext(PlayerMappingContext, 0);
}
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(InputComponent))
{
// Actions
EnhancedInputComponent->BindAction(PrimaryActionAction, ETriggerEvent::Triggered, this,
&AMainController::PrimaryAction);
EnhancedInputComponent->BindAction(PrimaryActionAction, ETriggerEvent::Completed, this,
&AMainController::PrimaryActionEnd);
EnhancedInputComponent->BindAction(SecondaryActionAction, ETriggerEvent::Triggered, this,
&AMainController::SecondaryAction);
//Movement
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AMainController::Move);
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Completed, this, &AMainController::MoveStop);
//Looking
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AMainController::Look);
//Running
EnhancedInputComponent->BindAction(RunningAction, ETriggerEvent::Started, this, &AMainController::RunStart);
EnhancedInputComponent->BindAction(RunningAction, ETriggerEvent::Completed, this,
&AMainController::RunStop);
//Jumping
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Triggered, this,
&AMainController::JumpTrigger);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &AMainController::JumpStart);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &AMainController::JumpStop);
//Crouch
EnhancedInputComponent->BindAction(CrouchAction, ETriggerEvent::Triggered, this, &AMainController::Crouch);
EnhancedInputComponent->BindAction(CrouchAction, ETriggerEvent::Completed, this, &AMainController::CrouchStop);
//Interact
EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this,
&AMainController::Interact);
//Inventory
EnhancedInputComponent->BindAction(InventoryAction, ETriggerEvent::Started, this,
&AMainController::ToggleInventory);
}
}
}
void AMainController::OnPossess(APawn* inPawn)
{
Super::OnPossess(inPawn);
if (!inPawn->GetClass()->ImplementsInterface(UMountableBase::StaticClass()))
{
bImplementsInterface = false;
return;
}
bImplementsInterface = true;
}
void AMainController::PrimaryAction()
{
if (!bImplementsInterface)
{
return;
}
IMountableBase::Execute_PrimaryAction(GetPawn());
}
void AMainController::PrimaryActionEnd()
{
if (!bImplementsInterface)
{
return;
}
IMountableBase::Execute_PrimaryActionEnd(GetPawn());
}
void AMainController::SecondaryAction()
{
if (!bImplementsInterface)
{
return;
}
IMountableBase::Execute_SecondaryAction(GetPawn());
}
void AMainController::Move(const FInputActionValue& Value)
{
if (!bImplementsInterface)
{
return;
}
IMountableBase::Execute_Move(GetPawn(), Value.Get<FVector>());
}
void AMainController::MoveStop()
{
if (!bImplementsInterface)
{
return;
}
IMountableBase::Execute_MoveStop(GetPawn());
}
void AMainController::Look(const FInputActionValue& Value)
{
if (!bImplementsInterface)
{
return;
}
IMountableBase::Execute_Look(GetPawn(), Value.Get<FVector>());
}
void AMainController::RunStart()
{
if (!bImplementsInterface)
{
return;
}
IMountableBase::Execute_RunStart(GetPawn());
}
void AMainController::RunStop()
{
if (!bImplementsInterface)
{
return;
}
IMountableBase::Execute_RunStop(GetPawn());
}
void AMainController::JumpTrigger()
{
if (!bImplementsInterface)
{
return;
}
IMountableBase::Execute_JumpTrigger(GetPawn());
}
void AMainController::JumpStart()
{
if (!bImplementsInterface)
{
return;
}
IMountableBase::Execute_JumpStart(GetPawn());
}
void AMainController::JumpStop()
{
if (!bImplementsInterface)
{
return;
}
IMountableBase::Execute_JumpStop(GetPawn());
}
void AMainController::Crouch()
{
if (!bImplementsInterface)
{
return;
}
IMountableBase::Execute_Crouch(GetPawn());
}
void AMainController::CrouchStop()
{
if (!bImplementsInterface)
{
return;
}
IMountableBase::Execute_CrouchStop(GetPawn());
}
void AMainController::Interact()
{
if (!bImplementsInterface)
{
return;
}
IMountableBase::Execute_Interact(GetPawn());
}
void AMainController::ToggleInventory()
{
if (!bImplementsInterface || bIsDead)
{
return;
}
IMountableBase::Execute_ToggleInventory(GetPawn());
}
void AMainController::SetIsDead(bool b)
{
bIsDead = b;
}
Midway through the project, we encountered several architectural issues with the inventory system. Inventory logic was distributed across multiple UI widgets, making bugs difficult to trace and the system increasingly difficult to maintain. Additionally, the InventoryComponent stored direct references to WorldItem objects, which were destroyed when picked up. This resulted in invalid references and unpredictable behavior, including inventory wipes and crashes.
To address these issues, I proposed two major changes:
InventoryComponent and UI widgets. This reduced dependencies between individual components and made the flow of inventory operations easier to trace and debug.WorldItem with a mapping between the item's DataAsset and its grid position. This allowed the inventory to retain the information it needed without requiring the corresponding WorldItem to remain alive.void UItemWidget::NativeOnMouseEnter(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
Super::NativeOnMouseEnter(InGeometry, InMouseEvent);
for (UBorder* BorderWidget : BorderWidgets)
{
if (BorderWidget)
{
BorderWidget->SetBrushColor(BackgroundHoverColor);
}
}
OnMouseEnterDelegate.Execute(this, InventoryItemPosition.Item);
}
void UItemWidget::NativeOnMouseLeave(const FPointerEvent& InMouseEvent)
{
Super::NativeOnMouseLeave(InMouseEvent);
for (UBorder* BorderWidget : BorderWidgets)
{
if (BorderWidget)
{
BorderWidget->SetBrushColor(BackgroundColor);
}
}
OnMouseLeaveDelegate.Execute();
}
void UItemWidget::NativeOnDragDetected(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent,
UDragDropOperation*& OutOperation)
{
if (!bCanRemoveItem)
{
Debug::LogWarning(TEXT("Item is not droppable, cannot start drag operation."), true, 2);
// Ensure focus is cleared so the grid doesn't keep keyboard focus after a blocked drag
UWidgetBlueprintLibrary::SetFocusToGameViewport();
return;
}
for (UBorder* BorderWidget : BorderWidgets)
{
if (BorderWidget)
{
BorderWidget->SetBrushColor(BackgroundPressedColor);
}
}
UDragDropOperation* DragOperation = NewObject<UDragDropOperation>();
// Create drag info object
UDragInfo* DragInfo = NewObject<UDragInfo>();
DragInfo->ItemGridPosition = InventoryItemPosition;
// --- Calculate grab offset ---
// Get mouse position relative to this widget
FVector2D LocalMousePos = InGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition());
int32 LocalTileX = FMath::FloorToInt(LocalMousePos.X / TileSize);
int32 LocalTileY = FMath::FloorToInt(LocalMousePos.Y / TileSize);
DragInfo->GrabOffset = FIntPoint(LocalTileX, LocalTileY);
// Start the drag with the current rotation of this instance
DragInfo->Rotation = InventoryItemPosition.Rotation;
// Set drag operation properties
DragOperation->Payload = DragInfo;
DragOperation->DefaultDragVisual = this;
DragOperation->Pivot = EDragPivot::MouseDown;
OutOperation = DragOperation;
Super::NativeOnDragDetected(InGeometry, InMouseEvent, OutOperation);
OnItemDragDetectedDelegate.Execute(this, InventoryItemPosition);
}
FReply UItemWidget::NativeOnMouseButtonUp(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
if (InMouseEvent.IsShiftDown())
{
OnMouseButtonUpDelegate.Execute(this, InventoryItemPosition, true);
return FReply::Handled();
}
OnMouseButtonUpDelegate.Execute(this, InventoryItemPosition, false);
return FReply::Handled();
}
FReply UItemWidget::NativeOnMouseButtonDoubleClick(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
OnMouseButtonDoubleClickDelegate.Execute(this, InventoryItemPosition);
return FReply::Handled();
}// This function is called when the drag operation is detected to be stopped outside the widget
bool UInventoryGridWidget::NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent,
UDragDropOperation* InOperation)
{
if (InOperation->Payload)
{
UDragInfo* DragData = Cast<UDragInfo>(InOperation->Payload);
if (!DragData)
{
return false;
}
UInventoryItem* DraggedItem = DragData->ItemGridPosition.Item;
FInventoryItemPosition ItemPosition;
ItemPosition.Item = DraggedItem;
ItemPosition.Position = DraggedItemTopLeftTile;
// Ensure rotation is reduced/consistent
ItemPosition.Rotation = InventoryRotationUtils::ReduceRotationForItem(DraggedItem, DragData->Rotation);
Super::NativeOnDrop(InGeometry, InDragDropEvent, InOperation);
OnItemDropped.Execute(this, ItemPosition);
bDrawDropLocation = false;
// Remove keyboard focus so TAB can close inventory again
UWidgetBlueprintLibrary::SetFocusToGameViewport();
return true;
}
return false;
}bool UInventoryWidget::NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent,
UDragDropOperation* InOperation)
{
if (InOperation->Payload)
{
UDragInfo* DragData = Cast<UDragInfo>(InOperation->Payload);
if (!DragData)
{
return false;
}
UInventoryItem* DraggedItem = DragData->ItemGridPosition.Item;
Super::NativeOnDrop(InGeometry, InDragDropEvent, InOperation);
OnItemDropped.Execute(this, DraggedItem);
return true;
}
return false;
}void UInventoryManager::BeginPlay()
{
// Set player reference
Player = GetOwner<AMainCharacterClass>();
// Set player controller reference
if (AController* Controller = Player->GetController())
{
//This might end up with nullptr even if there is a controller. and this might never happend if a player doesn't have a controller
PlayerController = Cast<APlayerController>(Controller);
}
//Create inventory widget
if (!InventoryWidgetClass)
{
Debug::LogWarning("InventoryWidgetClass is not set in InventoryManager. Please set it in the editor or code.",
true, 2);
return;
}
if (!PlayerController)
{
Debug::LogWarning("Tried to create inventory widget while not having a valid player controller line 89 on Inventory manager. cpp",
true, 5);
return;
}
InventoryWidget = CreateWidget<UInventoryWidget>(PlayerController, InventoryWidgetClass);
InventoryWidget->OnItemDropped.BindDynamic(this, &UInventoryManager::OnItemDroppedFromInventory);
InventoryWidget->SetVisibility(ESlateVisibility::Hidden);
InventoryWidget->AddToViewport();
}
void UInventoryManager::OpenComplimentaryInventory(UInventoryComponent* ComplimentaryInventory)
{
if (!ComplimentaryInventory)
{
UE_LOG(LogTemp, Warning, TEXT("Invalid inventory component for opening complimentary inventory."));
return;
}
ComplimentaryInventory->InventoryManager = this;
if (InventoryWidget->IsVisible())
{
// if we only have 1 inventory, we should still open the complimentary inventory
if (LinkageList.Num() == 1)
{
AddInventoryGridWidget(ComplimentaryInventory);
return;
}
ToggleInventory();
return;
}
ToggleInventory();
AddInventoryGridWidget(ComplimentaryInventory);
}
UInventoryGridWidget* UInventoryManager::AddInventoryGridWidget(UInventoryComponent* InventoryComponent)
{
if (!InventoryGridWidgetClass || !InventoryWidget)
{
Debug::LogWarning("InventoryGridWidgetClass or InventoryWidget is not set in InventoryManager. Please check your setup.",true, 2);
return nullptr;
}
if (!InventoryComponent)
{
Debug::LogWarning("InventoryComponent is null. Cannot add inventory grid.", true, 2);
return nullptr;
}
// Create the grid widget
UInventoryGridWidget* GridWidget = CreateWidget<UInventoryGridWidget>(InventoryWidget, InventoryGridWidgetClass);
if (!GridWidget)
{
Debug::LogWarning("Failed to create InventoryGridWidget. Please check your InventoryGridWidgetClass.", true, 2);
return nullptr;
}
GridWidget->OnItemDropped.BindDynamic(this, &UInventoryManager::OnItemDroppedFromGrid);
GridWidget->TileSize = InventoryComponent->TileSize;
// Find the linkage list entry for the component, if none, make a new one
FInventoryWidgetLink* LinkEntry = GetOrCreateLinkForComponent(InventoryComponent);
LinkEntry->SetGridWidget(GridWidget);
// Add to our array
InventoryWidget->GridWidgets.Add(GridWidget);
ConfigureGridWidget(InventoryComponent, GridWidget);
RepositionAllGrids();
RefreshItemWidgets(InventoryComponent);
Player->OnInventoryOpened.Broadcast(InventoryComponent);
return GridWidget;
}
void UInventoryManager::RefreshItemWidgets(UInventoryComponent* InventoryComponent)
{
if (!InventoryComponent)
{
Debug::LogWarning("RefreshInventoryWidgets called with null InventoryComponent", true, 2);
return;
}
if (!InventoryWidget)
{
Debug::LogWarning("InventoryWidget is not set in InventoryManager. Please check your setup.", true, 2);
return;
}
// Find the grid widget associated with this inventory component
FInventoryWidgetLink* LinkEntry = GetOrCreateLinkForComponent(InventoryComponent);
UInventoryGridWidget* GridWidget = LinkEntry ? LinkEntry->GridWidget : nullptr;
if (!GridWidget)
{
//Debug::LogWarning("No grid widget found for the provided inventory component: " + LinkEntry->InventoryComponent->InventoryName, true, 2);
return;
}
ClearItemWidgets(GridWidget);
TArray<UItemWidget*> UpdatedWidgets;
for (const auto& Pair : InventoryComponent->GetAllItems())
{
FItemGridPosition ItemPos;
ItemPos.Item = Pair.Item;
ItemPos.Position = Pair.Position;
FInventoryItemPosition ItemPosition = { ItemPos.Item, ItemPos.Position };
{
const uint8 StoredRot = InventoryComponent->GetRotationAt(ItemPos.Position);
ItemPosition.Rotation = InventoryRotationUtils::ReduceRotationForItem(ItemPos.Item, StoredRot);
}
const FIntPoint MinOff = InventoryRotationUtils::GetMinRotatedOffset(ItemPos.Item, ItemPosition.Rotation);
const FVector2D TopLeft(
(ItemPos.Position.X + MinOff.X) * InventoryComponent->TileSize,
(ItemPos.Position.Y + MinOff.Y) * InventoryComponent->TileSize
);
const bool bIsRecipe = InventoryComponent->IsCraftingStation();
UItemWidget* TypedItemWidget = CreateItemWidget(GridWidget, ItemPosition, bIsRecipe, InventoryComponent->TileSize);
if (TypedItemWidget)
{
if (UPanelSlot* NewPanelSlot = GridWidget->GetGridCanvasPanel()->AddChild(TypedItemWidget))
{
if (UCanvasPanelSlot* CanvasSlot = Cast<UCanvasPanelSlot>(NewPanelSlot))
{
// Size explicitly based on rotated dimensions so layout/hit-test match the visual
const int32 Dx = FMath::Max(1, ItemPos.Item->Dimension.X);
const int32 Dy = FMath::Max(1, ItemPos.Item->Dimension.Y);
const bool bOdd = (ItemPosition.Rotation % 2) == 1;
const FVector2D WidgetSize(
(bOdd ? Dy : Dx) * InventoryComponent->TileSize,
(bOdd ? Dx : Dy) * InventoryComponent->TileSize
);
CanvasSlot->SetAutoSize(false);
CanvasSlot->SetSize(WidgetSize);
CanvasSlot->SetPosition(TopLeft);
CanvasSlot->SetZOrder(10);
}
}
UpdatedWidgets.Add(TypedItemWidget);
}
}
// Save widgets
LinkEntry = GetOrCreateLinkForComponent(InventoryComponent);
if (LinkEntry)
{
LinkEntry->SetItemWidgets(UpdatedWidgets);
}
}
UItemWidget* UInventoryManager::CreateItemWidget(UInventoryGridWidget* GridWidget, FInventoryItemPosition ItemPosition,
bool bIsRecipe, float TileSize)
{
UItemWidget* TypedItemWidget = CreateWidget<UItemWidget>(GridWidget, ItemWidgetClass);
if (!TypedItemWidget)
{
return nullptr;
}
UInventoryComponent* InventoryComponent = GetComponentWithGridWidget(GridWidget);
TypedItemWidget->bIsRecipe = bIsRecipe;
TypedItemWidget->bCanRemoveItem = InventoryComponent->bCanRemoveItems;
// Uncomment this to disable dragging the item widget
//TypedItemWidget->bIsDroppable = InventoryComponent->bCanRemoveItems;
TypedItemWidget->Setup(&ItemPosition, TileSize, InventoryComponent);
TypedItemWidget->OnMouseEnterDelegate.BindDynamic(this, &UInventoryManager::OnItemEntered);
TypedItemWidget->OnMouseLeaveDelegate.BindDynamic(this, &UInventoryManager::OnItemExited);
TypedItemWidget->OnItemDragDetectedDelegate.BindDynamic(this, &UInventoryManager::OnItemDragDetected);
TypedItemWidget->OnMouseButtonUpDelegate.BindDynamic(this, &UInventoryManager::OnItemClicked);
TypedItemWidget->OnMouseButtonDoubleClickDelegate.BindDynamic(this, &UInventoryManager::OnItemDoubleClicked);
return TypedItemWidget;
}
void UInventoryManager::OnItemEntered(UItemWidget* ItemWidget, UInventoryItem* Item)
{
// Process for when the cusor enter item widget area
}
void UInventoryManager::OnItemExited()
{
// Process for when the cursor exit item widget area
}
void UInventoryManager::OnItemDragDetected(UItemWidget* ItemWidget, FInventoryItemPosition InventoryItemPosition)
{
Process for when an drag operation is detected
}
void UInventoryManager::OnItemClicked(UItemWidget* ItemWidget, FInventoryItemPosition InventoryItemPosition,
bool bIsShiftClicked)
{
// Process for when an item widget is clicked
}
void UInventoryManager::OnItemDoubleClicked(UItemWidget* ItemWidget, FInventoryItemPosition InventoryItemPosition)
{
// Process for when an item widget is double clicked
}
void UInventoryManager::OnItemDroppedFromGrid(UInventoryGridWidget* GridWidget,
FInventoryItemPosition InventoryItemPosition)
{
// Process for when drag operation ended with the dragged item being dropped on a grid widget
}
void UInventoryManager::OnItemDroppedFromInventory(UInventoryWidget* InInventoryWidget, UInventoryItem* InventoryItem)
{
// Process for when drag operation ended with the dragged item being dropped on the inventory widget
}