feat: lines first iteration

This commit is contained in:
2025-10-12 20:30:38 +02:00
parent 149bbb562c
commit c4d2334fb7
3 changed files with 407 additions and 2 deletions

View File

@@ -0,0 +1,299 @@
#include "SkillTreeConnectionsWidget.h"
#include "SkillTreeComponent.h"
#include "Blueprint/WidgetTree.h"
#include "Components/PanelWidget.h"
#include "Rendering/DrawElements.h"
#include "Blueprint/WidgetLayoutLibrary.h"
USkillTreeConnectionsWidget::USkillTreeConnectionsWidget(
const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
bNeedsRedraw = true;
}
void USkillTreeConnectionsWidget::NativeConstruct()
{
Super::NativeConstruct();
// Bind to skill tree component events for automatic refresh
if (SkillTreeComponent)
{
// Remove any existing bindings first to prevent double-binding
SkillTreeComponent->OnSkillStateChanged.RemoveDynamic(
this, &USkillTreeConnectionsWidget::OnSkillStateChangedHandler);
SkillTreeComponent->OnSkillSelectionChanged.RemoveDynamic(
this, &USkillTreeConnectionsWidget::OnSkillSelectionChangedHandler);
SkillTreeComponent->OnSkillPurchased.RemoveDynamic(
this, &USkillTreeConnectionsWidget::OnSkillPurchasedHandler);
// Now bind
SkillTreeComponent->OnSkillStateChanged.AddDynamic(
this, &USkillTreeConnectionsWidget::OnSkillStateChangedHandler);
SkillTreeComponent->OnSkillSelectionChanged.AddDynamic(
this, &USkillTreeConnectionsWidget::OnSkillSelectionChangedHandler);
SkillTreeComponent->OnSkillPurchased.AddDynamic(
this, &USkillTreeConnectionsWidget::OnSkillPurchasedHandler);
}
}
void USkillTreeConnectionsWidget::NativeDestruct()
{
// Unbind from events to prevent memory leaks
if (SkillTreeComponent)
{
SkillTreeComponent->OnSkillStateChanged.RemoveDynamic(
this, &USkillTreeConnectionsWidget::OnSkillStateChangedHandler);
SkillTreeComponent->OnSkillSelectionChanged.RemoveDynamic(
this, &USkillTreeConnectionsWidget::OnSkillSelectionChangedHandler);
SkillTreeComponent->OnSkillPurchased.RemoveDynamic(
this, &USkillTreeConnectionsWidget::OnSkillPurchasedHandler);
}
Super::NativeDestruct();
}
void USkillTreeConnectionsWidget::RefreshConnections()
{
bNeedsRedraw = true;
// Force widget to repaint
if (IsValid(this))
{
Invalidate(EInvalidateWidgetReason::Paint);
}
}
void USkillTreeConnectionsWidget::OnSkillStateChangedHandler(FName SkillID)
{
RefreshConnections();
}
void USkillTreeConnectionsWidget::OnSkillSelectionChangedHandler(
int32 TotalCost)
{
RefreshConnections();
}
void USkillTreeConnectionsWidget::OnSkillPurchasedHandler(FName SkillID)
{
RefreshConnections();
}
bool USkillTreeConnectionsWidget::GetSkillIDFromWidget(UUserWidget* Widget,
FName& OutSkillID) const
{
if (!Widget)
return false;
// Try to find a property named "SkillID" on the widget
for (TFieldIterator<FProperty> PropIt(Widget->GetClass()); PropIt; ++PropIt)
{
FProperty* Property = *PropIt;
if (Property && Property->GetFName() == FName(TEXT("SkillID")))
{
// Check if it's a FName property
if (FNameProperty* NameProperty =
CastField<FNameProperty>(Property))
{
const void* ValuePtr =
Property->ContainerPtrToValuePtr<void>(Widget);
OutSkillID = NameProperty->GetPropertyValue(ValuePtr);
return true;
}
}
}
return false;
}
void USkillTreeConnectionsWidget::FindWidgetsOfClass(
UWidget* Root, TSubclassOf<UUserWidget> WidgetClass,
TArray<UUserWidget*>& OutWidgets) const
{
if (!Root || !WidgetClass)
return;
// Check if this widget is a UserWidget of the desired class
if (UUserWidget* UserWidget = Cast<UUserWidget>(Root))
{
if (UserWidget->IsA(WidgetClass))
{
OutWidgets.Add(UserWidget);
}
}
// If this is a panel widget, recursively search its children
if (UPanelWidget* PanelWidget = Cast<UPanelWidget>(Root))
{
for (int32 i = 0; i < PanelWidget->GetChildrenCount(); ++i)
{
UWidget* Child = PanelWidget->GetChildAt(i);
FindWidgetsOfClass(Child, WidgetClass, OutWidgets);
}
}
// If this is a UserWidget, search its widget tree
if (UUserWidget* UserWidget = Cast<UUserWidget>(Root))
{
if (UserWidget->WidgetTree)
{
TArray<UWidget*> AllWidgets;
UserWidget->WidgetTree->GetAllWidgets(AllWidgets);
for (UWidget* Widget : AllWidgets)
{
if (Widget != Root) // Avoid infinite recursion
{
FindWidgetsOfClass(Widget, WidgetClass, OutWidgets);
}
}
}
}
}
void USkillTreeConnectionsWidget::BuildNodeInfoList(
TArray<FSkillNodeInfo>& OutNodeInfo) const
{
OutNodeInfo.Empty();
if (!SkillTreeComponent || !SkillNodeWidgetClass)
return;
// Determine root widget to search from
UWidget* RootWidget = SkillTreeWidget;
if (!RootWidget)
{
// If no parent widget is set, try to get the outer widget
RootWidget = Cast<UWidget>(GetOuter());
if (!RootWidget)
{
// As last resort, try to get root widget from widget tree
if (WidgetTree)
{
RootWidget = WidgetTree->RootWidget;
}
}
}
if (!RootWidget)
{
UE_LOG(
LogTemp, Error,
TEXT(
"SkillTreeConnectionsWidget: No valid root widget found to search for skill nodes"));
return;
}
// Find all skill node widgets recursively
TArray<UUserWidget*> SkillNodeWidgets;
FindWidgetsOfClass(RootWidget, SkillNodeWidgetClass, SkillNodeWidgets);
// Build info for each skill node
for (UUserWidget* SkillNodeWidget : SkillNodeWidgets)
{
if (!SkillNodeWidget)
continue;
// Try to get the SkillID from the widget
FName SkillID;
if (!GetSkillIDFromWidget(SkillNodeWidget, SkillID))
continue;
// Get the skill node data from the component
FSkillNodeRuntime NodeData;
if (!SkillTreeComponent->GetSkillNodeData(SkillID, NodeData))
continue;
// Get the widget's geometry to determine its position
const FGeometry& Geometry = SkillNodeWidget->GetCachedGeometry();
FVector2D LocalSize = Geometry.GetLocalSize();
FVector2D AbsolutePosition = Geometry.GetAbsolutePosition();
// Calculate center position (in absolute/screen space)
FVector2D CenterPosition = AbsolutePosition + (LocalSize * 0.5f);
// Build the info struct
FSkillNodeInfo NodeInfo;
NodeInfo.SkillID = SkillID;
NodeInfo.Position = CenterPosition;
NodeInfo.State = NodeData.CurrentState;
NodeInfo.Prerequisites = NodeData.NodeData.Prerequisites;
OutNodeInfo.Add(NodeInfo);
}
}
FLinearColor
USkillTreeConnectionsWidget::GetLineColorForState(ESkillNodeState State) const
{
switch (State)
{
case ESkillNodeState::Locked:
return LockedColor;
case ESkillNodeState::Available:
return AvailableColor;
case ESkillNodeState::Selected:
return SelectedColor;
case ESkillNodeState::Purchased:
return PurchasedColor;
default:
return LockedColor;
}
}
int32 USkillTreeConnectionsWidget::NativePaint(
const FPaintArgs& Args, const FGeometry& AllottedGeometry,
const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements,
int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
int32 MaxLayerId = Super::NativePaint(Args, AllottedGeometry, MyCullingRect,
OutDrawElements, LayerId,
InWidgetStyle, bParentEnabled);
if (!SkillTreeComponent || !SkillNodeWidgetClass)
return MaxLayerId;
// Build node information
TArray<FSkillNodeInfo> NodeInfoList;
BuildNodeInfoList(NodeInfoList);
// Get this widget's absolute position to convert coordinates
FVector2D ConnectionsWidgetAbsolutePosition = AllottedGeometry.GetAbsolutePosition();
// Draw lines for each node's prerequisites
for (const FSkillNodeInfo& DependentNode : NodeInfoList)
{
// Get the color for this node's connections
FLinearColor LineColor = GetLineColorForState(DependentNode.State);
// Draw lines to each prerequisite
for (const FName& PrerequisiteID : DependentNode.Prerequisites)
{
// Find the prerequisite node in our list
const FSkillNodeInfo* PrereqNode = NodeInfoList.FindByPredicate(
[&PrerequisiteID](const FSkillNodeInfo& Info)
{ return Info.SkillID == PrerequisiteID; });
if (!PrereqNode)
continue;
// Convert absolute positions to local coordinates relative to this widget
FVector2D PrereqLocalPos = PrereqNode->Position - ConnectionsWidgetAbsolutePosition;
FVector2D DependentLocalPos = DependentNode.Position - ConnectionsWidgetAbsolutePosition;
// Draw line from prerequisite to dependent
TArray<FVector2D> LinePoints;
LinePoints.Add(PrereqLocalPos);
LinePoints.Add(DependentLocalPos);
FSlateDrawElement::MakeLines(OutDrawElements, MaxLayerId + 1,
AllottedGeometry.ToPaintGeometry(),
LinePoints, ESlateDrawEffect::None,
LineColor,
true, // bAntialias
LineThickness);
}
}
return MaxLayerId + 1;
}

View File

@@ -0,0 +1,106 @@
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "SkillTreeTypes.h"
#include "SkillTreeConnectionsWidget.generated.h"
class USkillTreeComponent;
class UUserWidget;
class UWidget;
/**
* Widget that automatically draws connection lines between skill tree nodes
* Place this widget in your skill tree UI and it will handle all line rendering
*/
UCLASS()
class UTAD_UI_API USkillTreeConnectionsWidget : public UUserWidget
{
GENERATED_BODY()
public:
USkillTreeConnectionsWidget(const FObjectInitializer& ObjectInitializer);
// Parent widget to search for skill nodes (can be the skill tree widget
// itself) If not set, will search from the outer widget
UPROPERTY(EditAnywhere, BlueprintReadWrite,
Category = "Skill Tree Connections")
UUserWidget* SkillTreeWidget;
// Reference to the skill tree component (set this in Blueprint)
UPROPERTY(EditAnywhere, BlueprintReadWrite,
Category = "Skill Tree Connections")
USkillTreeComponent* SkillTreeComponent;
// Widget class for skill nodes
UPROPERTY(EditAnywhere, BlueprintReadWrite,
Category = "Skill Tree Connections")
TSubclassOf<UUserWidget> SkillNodeWidgetClass;
// Line thickness in pixels
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Line Appearance",
meta = (ClampMin = "0.5", ClampMax = "10.0"))
float LineThickness = 2.0f;
// Colors for different states
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Line Colors")
FLinearColor LockedColor = FLinearColor(0.3f, 0.3f, 0.3f, 1.0f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Line Colors")
FLinearColor AvailableColor = FLinearColor(1.0f, 1.0f, 1.0f, 1.0f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Line Colors")
FLinearColor SelectedColor = FLinearColor(1.0f, 0.84f, 0.0f, 1.0f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Line Colors")
FLinearColor PurchasedColor = FLinearColor(0.0f, 1.0f, 0.0f, 1.0f);
// Call this to refresh the lines (useful when skill states change)
UFUNCTION(BlueprintCallable, Category = "Skill Tree Connections")
void RefreshConnections();
protected:
virtual int32 NativePaint(const FPaintArgs& Args,
const FGeometry& AllottedGeometry,
const FSlateRect& MyCullingRect,
FSlateWindowElementList& OutDrawElements,
int32 LayerId, const FWidgetStyle& InWidgetStyle,
bool bParentEnabled) const override;
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
private:
// Event handlers for skill tree component events
UFUNCTION()
void OnSkillStateChangedHandler(FName SkillID);
UFUNCTION()
void OnSkillSelectionChangedHandler(int32 TotalCost);
UFUNCTION()
void OnSkillPurchasedHandler(FName SkillID);
// Helper struct to store node information for drawing
struct FSkillNodeInfo
{
FName SkillID;
FVector2D Position;
ESkillNodeState State;
TArray<FName> Prerequisites;
};
// Build the list of nodes and their info
void BuildNodeInfoList(TArray<FSkillNodeInfo>& OutNodeInfo) const;
// Recursively find all widgets of a specific class in the widget hierarchy
void FindWidgetsOfClass(UWidget* Root, TSubclassOf<UUserWidget> WidgetClass,
TArray<UUserWidget*>& OutWidgets) const;
// Get the color for a line based on the dependent node's state
FLinearColor GetLineColorForState(ESkillNodeState State) const;
// Get SkillID from a skill node widget (looks for "SkillID" property)
bool GetSkillIDFromWidget(UUserWidget* Widget, FName& OutSkillID) const;
// Cache for forcing redraws
mutable bool bNeedsRedraw = true;
};