#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 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(Property)) { const void* ValuePtr = Property->ContainerPtrToValuePtr(Widget); OutSkillID = NameProperty->GetPropertyValue(ValuePtr); return true; } } } return false; } void USkillTreeConnectionsWidget::FindWidgetsOfClass( UWidget* Root, TSubclassOf WidgetClass, TArray& OutWidgets) const { if (!Root || !WidgetClass) return; // Check if this widget is a UserWidget of the desired class if (UUserWidget* UserWidget = Cast(Root)) { if (UserWidget->IsA(WidgetClass)) { OutWidgets.Add(UserWidget); } } // If this is a panel widget, recursively search its children if (UPanelWidget* PanelWidget = Cast(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(Root)) { if (UserWidget->WidgetTree) { TArray AllWidgets; UserWidget->WidgetTree->GetAllWidgets(AllWidgets); for (UWidget* Widget : AllWidgets) { if (Widget != Root) // Avoid infinite recursion { FindWidgetsOfClass(Widget, WidgetClass, OutWidgets); } } } } } void USkillTreeConnectionsWidget::BuildNodeInfoList( TArray& 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(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 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 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 LinePoints; LinePoints.Add(PrereqLocalPos); LinePoints.Add(DependentLocalPos); FSlateDrawElement::MakeLines(OutDrawElements, MaxLayerId + 1, AllottedGeometry.ToPaintGeometry(), LinePoints, ESlateDrawEffect::None, LineColor, true, // bAntialias LineThickness); } } return MaxLayerId + 1; }