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