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;
}