feat: lines first iteration
This commit is contained in:
		
							
								
								
									
										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; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user