feat: lines first iteration
This commit is contained in:
BIN
Content/UI/WBP_SkillTree.uasset
(Stored with Git LFS)
BIN
Content/UI/WBP_SkillTree.uasset
(Stored with Git LFS)
Binary file not shown.
299
Source/UTAD_UI/SkillTree/SkillTreeConnectionsWidget.cpp
Normal file
299
Source/UTAD_UI/SkillTree/SkillTreeConnectionsWidget.cpp
Normal 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;
|
||||||
|
}
|
||||||
106
Source/UTAD_UI/SkillTree/SkillTreeConnectionsWidget.h
Normal file
106
Source/UTAD_UI/SkillTree/SkillTreeConnectionsWidget.h
Normal 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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user