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