文章目录
装备的合成
资源管理器CAssetManager
中添加新的函数用来获取合成物品
cpp
/**
* 获取指定物品能合成的所有结果物品
* @param Item - 作为材料的物品
* @return 指向结果物品集合的指针(找不到返回nullptr)
*/
const FItemCollection* GetCombinationForItem(const UPDA_ShopItem* Item) const;
/**
* 获取合成指定物品所需的所有材料
* @param Item - 目标物品
* @return 指向材料物品集合的指针(找不到返回nullptr)
*/
const FItemCollection* GetIngredientForItem(const UPDA_ShopItem* Item) const;
cpp
const FItemCollection* UCAssetManager::GetCombinationForItem(const UPDA_ShopItem* Item) const
{
// 在合成映射表中查找材料对应的合成结果
return CombinationMap.Find(Item);
}
const FItemCollection* UCAssetManager::GetIngredientForItem(const UPDA_ShopItem* Item) const
{
// 在材料映射表中查找物品所需的材料
return IngredientMap.Find(Item);
}
到库存组件InventoryComponent
中调用并合成物品
cpp
public:
/**
* 查找合成指定物品所需的材料
* @param Item 目标物品
* @param OutIngredients 输出找到的材料
* @param IngredientToIgnore 需要忽略的材料列表
* @return 是否找到全部材料
*/
bool FindIngredientForItem(const UPDA_ShopItem* Item, TArray<UInventoryItem*>& OutIngredients, const TArray<const UPDA_ShopItem*>& IngredientToIgnore = TArray<const UPDA_ShopItem*>{});
// 尝试获取与商店物品对应的库存物品
UInventoryItem* TryGetItemForShopItem(const UPDA_ShopItem* Item) const;
/*********************************************************/
/* Server RPCs */
/*********************************************************/
/** 尝试物品合成 */
bool TryItemCombination(const UPDA_ShopItem* NewItem);
购买物品的时候调用尝试合成
cpp
bool UInventoryComponent::FindIngredientForItem(const UPDA_ShopItem* Item, TArray<UInventoryItem*>& OutIngredients,
const TArray<const UPDA_ShopItem*>& IngredientToIgnore)
{
// 获取物品合成所需材料
const FItemCollection* Ingredients = UCAssetManager::Get().GetIngredientForItem(Item);
if (!Ingredients) return false;
bool bAllFound = true;
// 遍历材料列表
for (const UPDA_ShopItem* Ingredient : Ingredients->GetItems())
{
// 跳过忽略的素材
if (IngredientToIgnore.Contains(Ingredient))
continue;
// 查找背包中是否有该物品
UInventoryItem* FoundItem = TryGetItemForShopItem(Ingredient);
if (!FoundItem)
{
// 缺一个就是找不到全部,返回false
bAllFound = false;
break;
}
// 背包中找到的物品
OutIngredients.Add(FoundItem);
}
return bAllFound;
}
UInventoryItem* UInventoryComponent::TryGetItemForShopItem(const UPDA_ShopItem* Item) const
{
if (!Item) return nullptr;
// 遍历背包 寻找指定商品
for (const TPair<FInventoryItemHandle, UInventoryItem*>& ItemHandlePair : InventoryMap)
{
// if (ItemHandlePair.Value && ItemHandlePair.Value->IsForItem(Item))
if (ItemHandlePair.Value && ItemHandlePair.Value->GetShopItem() == Item)
{
return ItemHandlePair.Value;
}
}
return nullptr;
}
bool UInventoryComponent::TryItemCombination(const UPDA_ShopItem* NewItem)
{
// 仅服务器调用
if (!GetOwner()->HasAuthority()) return false;
// 获取可合成的物品
const FItemCollection* CombinationItems = UCAssetManager::Get().GetCombinationForItem(NewItem);
if (!CombinationItems) return false;
// 遍历所有的可合成物品
for (const UPDA_ShopItem* CombinationItem : CombinationItems->GetItems())
{
TArray<UInventoryItem*> Ingredients;
// 查找合成所需材料
if (!FindIngredientForItem(CombinationItem, Ingredients, TArray<const UPDA_ShopItem*>{NewItem}))
continue;
// 移除材料
for (UInventoryItem* Ingredient : Ingredients)
{
RemoveItem(Ingredient);
}
// 合成物品
GrantItem(CombinationItem);
return true;
}
return false;
}
合成树的绘制
创建线条UI
SplineWidget
完整线条UISplineWidget
代码
cpp
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "SplineWidget.generated.h"
/**
* 自定义样条线控件,用于在两个UI控件之间绘制贝塞尔曲线连接线
* 常用于技能树、节点图、流程图等UI连接线绘制
*/
UCLASS()
class CRUNCH_API USplineWidget : public UUserWidget
{
GENERATED_BODY()
public:
/**
* 初始化样条线连接参数
* @param InStartWidget 起始控件
* @param InEndWidget 目标控件
* @param InStartPortLocalCoord 起始点在起始控件内的局部坐标(相对于控件左上角)
* @param InEndPortLocalCoord 目标点在目标控件内的局部坐标
* @param InStartPortDirection 起始点切线方向(控制曲线形状)
* @param InEndPortDirection 目标点切线方向(控制曲线形状)
*/
void SetupSpline(
const UUserWidget* InStartWidget,
const UUserWidget* InEndWidget,
const FVector2D& InStartPortLocalCoord,
const FVector2D& InEndPortLocalCoord,
const FVector2D& InStartPortDirection,
const FVector2D& InEndPortDirection
);
/**
* 设置样条线显示样式
* @param InColor 线条颜色
* @param InThickness 线条粗细(像素)
*/
void SetSplineStyle(const FLinearColor& InColor, float InThickness);
private:
// 重写原生绘制函数
virtual int32 NativePaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override;
// 编辑器测试用起点位置(开发调试使用)
UPROPERTY(EditAnywhere, Category = "Spline")
FVector2D TestStartPos;
// 编辑器测试用终点位置(开发调试使用)
UPROPERTY(EditAnywhere, Category = "Spline")
FVector2D TestEndPos = FVector2D{100.f, 100.f};
// 样条线颜色
UPROPERTY(EditAnywhere, Category = "Spline")
FLinearColor Color = FLinearColor::White;
// 样条线粗细(像素单位)
UPROPERTY(EditAnywhere, Category = "Spline")
float Thickness = 3.f;
// 起始控件引用
UPROPERTY()
const UUserWidget* StartWidget;
// 目标控件引用
UPROPERTY()
const UUserWidget* EndWidget;
// 起始点在起始控件内的局部坐标
FVector2D StartPortLocalCoord;
// 目标点在目标控件内的局部坐标
FVector2D EndPortLocalCoord;
// 起始点切线方向(控制曲线起始斜率)
UPROPERTY(EditAnywhere, Category = "Spline")
FVector2D StartPortDirection;
// 目标点切线方向(控制曲线结束斜率)
UPROPERTY(EditAnywhere, Category = "Spline")
FVector2D EndPortDirection;
};
cpp
#include "SplineWidget.h"
void USplineWidget::SetupSpline(const UUserWidget* InStartWidget, const UUserWidget* InEndWidget,
const FVector2D& InStartPortLocalCoord, const FVector2D& InEndPortLocalCoord, const FVector2D& InStartPortDirection,
const FVector2D& InEndPortDirection)
{
// 设置连接的起始控件和目标控件
StartWidget = InStartWidget;
EndWidget = InEndWidget;
// 设置连接点在控件内的局部坐标(相对于控件左上角)
StartPortLocalCoord = InStartPortLocalCoord;
EndPortLocalCoord = InEndPortLocalCoord;
// 设置样条线在起点和终点的切线方向(控制曲线形状)
StartPortDirection = InStartPortDirection;
EndPortDirection = InEndPortDirection;
}
void USplineWidget::SetSplineStyle(const FLinearColor& InColor, float InThickness)
{
// 设置样条线的视觉样式
Color = InColor; // 线条颜色
Thickness = InThickness; // 线条粗细(像素)
}
int32 USplineWidget::NativePaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry,
const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId,
const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
// 调用父类的绘制方法(确保基础UI元素被正确绘制)
LayerId = Super::NativePaint(Args, AllottedGeometry, MyCullingRect,
OutDrawElements, LayerId, InWidgetStyle, bParentEnabled);
// 初始化起点和终点位置(默认使用编辑器测试位置)
FVector2D StartPos = TestStartPos;
FVector2D EndPos = TestEndPos;
// 如果已设置起始控件和目标控件,则计算实际位置
if (StartWidget && EndWidget)
{
// 将起始点局部坐标转换为当前几何空间的绝对位置
StartPos = StartWidget->GetCachedGeometry().GetLocalPositionAtCoordinates(StartPortLocalCoord);
// 将终点局部坐标转换为当前几何空间的绝对位置
EndPos = EndWidget->GetCachedGeometry().GetLocalPositionAtCoordinates(EndPortLocalCoord);
}
// 使用Slate绘制API创建贝塞尔曲线样条线
FSlateDrawElement::MakeSpline(
OutDrawElements, // 绘制元素列表
++LayerId, // 递增图层ID确保在顶层绘制
AllottedGeometry.ToPaintGeometry(), // 当前控件的几何信息
StartPos, // 曲线起点位置
StartPortDirection, // 起点切线方向(控制曲线起始斜率)
EndPos, // 曲线终点位置
EndPortDirection, // 终点切线方向(控制曲线结束斜率)
Thickness, // 线条粗细
ESlateDrawEffect::None, // 无特殊绘制效果
Color // 线条颜色
);
// 返回最终使用的图层ID
return LayerId;
}
创建树节点接口
创建一个接口
TreeNodeInterface
cpp
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "TreeNodeInterface.generated.h"
// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UTreeNodeInterface : public UInterface
{
GENERATED_BODY()
};
/**
* 树节点接口,用于实现树形数据结构中的节点功能
* 适用于技能树、科技树、组织结构图等需要节点关系的系统
*/
class ITreeNodeInterface
{
GENERATED_BODY()
// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
/**
* 获取该节点关联的UI控件
* @return 返回节点对应的用户控件实例
*/
virtual UUserWidget* GetWidget() const = 0;
/**
* 获取当前节点的所有输入节点(父节点/前置节点)
* @return 输入节点接口指针的数组
*/
virtual TArray<const ITreeNodeInterface*> GetInputs() const = 0;
/**
* 获取当前节点的所有输出节点(子节点/后置节点)
* @return 输出节点接口指针的数组
*/
virtual TArray<const ITreeNodeInterface*> GetOutputs() const = 0;
/**
* 获取节点关联的数据对象
* @return 节点关联的UObject数据对象(如技能配置、科技配置等)
*/
virtual const UObject* GetItemObject() const = 0;
};
让商店物品继承树节点接口,并实现接口的函数
cpp
UCLASS()
class UShopItemWidget : public UItemWidget,
public IUserObjectListEntry,
public ITreeNodeInterface
{
GENERATED_BODY()
public:
//~ Begin ITreeNodeInterface 接口实现
// 获取当前控件实例(树节点接口要求)
virtual UUserWidget* GetWidget() const override;
// 获取输入节点(合成树中的材料项)
virtual TArray<const ITreeNodeInterface*> GetInputs() const override;
// 获取输出节点(合成树中的产出项)
virtual TArray<const ITreeNodeInterface*> GetOutputs() const override;
// 获取关联的数据对象(商店物品资产)
virtual const UObject* GetItemObject() const override;
//~ End ITreeNodeInterface 接口实现
private:
// 从另一个商店物品控件复制数据(用于列表项重用)
void CopyFromOther(const UShopItemWidget* OtherWidget);
// 使用商店物品数据初始化控件
void InitWithShopItem(const UPDA_ShopItem* NewShopItem);
/**
* 将商店物品数组转换为树节点接口数组
* @param Items 商店物品数组
* @return 对应的树节点接口数组
*/
TArray<const ITreeNodeInterface*> ItemsToInterfaces(const TArray<const UPDA_ShopItem*>& Items) const;
// 父级ListView控件(此物品所属的列表视图)
UPROPERTY()
TObjectPtr<const class UListView> ParentListView;
};
cpp
UUserWidget* UShopItemWidget::GetWidget() const
{
// 创建一样的商店物品UI
UShopItemWidget* Copy = CreateWidget<UShopItemWidget>(GetOwningPlayer(), GetClass());
// 复制商店物品UI数据
Copy->CopyFromOther(this);
return Copy;
}
TArray<const ITreeNodeInterface*> UShopItemWidget::GetInputs() const
{
// 获取可合成该物品的材料列表
const FItemCollection* Collection = UCAssetManager::Get().GetCombinationForItem(GetShopItem());
if (Collection)
{
// 将物品转换为树节点
return ItemsToInterfaces(Collection->GetItems());
}
return TArray<const ITreeNodeInterface*>{};
}
TArray<const ITreeNodeInterface*> UShopItemWidget::GetOutputs() const
{
// 获取合成该物品所需的材料列表
const FItemCollection* Collection = UCAssetManager::Get().GetIngredientForItem(GetShopItem());
if (Collection)
{
// 将物品转换为树节点
return ItemsToInterfaces(Collection->GetItems());
}
return TArray<const ITreeNodeInterface*>{};
}
const UObject* UShopItemWidget::GetItemObject() const
{
return ShopItem;
}
void UShopItemWidget::NativeOnListItemObjectSet(UObject* ListItemObject)
{
IUserObjectListEntry::NativeOnListItemObjectSet(ListItemObject);
// ShopItem = Cast<UPDA_ShopItem>(ListItemObject);
// if (!ShopItem) return;
//
// SetIcon(ShopItem->GetIcon());
//
// SetToolTipWidget(ShopItem);
// 使用新数据初始化控件
InitWithShopItem(Cast<UPDA_ShopItem>(ListItemObject));
// 获取所属的ListView控件
ParentListView = Cast<UListView>(IUserListEntry::GetOwningListView());
}
void UShopItemWidget::CopyFromOther(const UShopItemWidget* OtherWidget)
{
// 复制委托绑定
OnItemPurchaseIssued = OtherWidget->OnItemPurchaseIssued;
OnShopItemClicked = OtherWidget->OnShopItemClicked;
// 复制父列表
ParentListView = OtherWidget->ParentListView;
// 使用商店物品数据初始化界面
InitWithShopItem(OtherWidget->GetShopItem());
}
void UShopItemWidget::InitWithShopItem(const UPDA_ShopItem* NewShopItem)
{
// 设置商店物品数据
ShopItem = NewShopItem;
if (!ShopItem) return;
// 设置图标
SetIcon(ShopItem->GetIcon());
// 创建并设置提示信息
SetToolTipWidget(ShopItem);
}
TArray<const ITreeNodeInterface*> UShopItemWidget::ItemsToInterfaces(const TArray<const UPDA_ShopItem*>& Items) const
{
// 创建用来存储的树节点的数组
TArray<const ITreeNodeInterface*> RetInterfaces;
if (!ParentListView) return RetInterfaces;
// 遍历物品数组,把它们从 ListView 中转换为 Widget(然后作为树节点返回)
for (const UPDA_ShopItem* Item : Items)
{
// 从ParentListView列表视图中获取与Item数据项关联的UShopItemWidget控件
// ParentListView存储的就是商店的那个框框
const UShopItemWidget* ItemWidget = ParentListView->GetEntryWidgetFromItem<UShopItemWidget>(Item);
if (ItemWidget)
{
// 将对应的树节点添加到数组中
RetInterfaces.Add(ItemWidget);
}
}
return RetInterfaces;
}
ParentListView
就是获取到商店UI的ShopItemList
表
合成树的绘制
继承UserWidget
创建ItemTreeWidget
合成树的节点遍历类似于二叉树的前序遍历顺序(点击前往观看前序遍历),这边使用的是多叉树的前序遍历。
完整ItemTreeWidget
代码
cpp
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "ItemTreeWidget.generated.h"
class UCanvasPanelSlot;
class ITreeNodeInterface;
class UCanvasPanel;
/**
* @class UItemTreeWidget
* @brief 合成树的绘制
*/
UCLASS()
class CRUNCH_API UItemTreeWidget : public UUserWidget
{
GENERATED_BODY()
public:
/**
* 从指定的树节点接口开始,绘制整棵树。
* @param NodeInterface - 树的"根节点",实现了 ITreeNodeInterface 的对象(通常代表一个 ShopItem)。
*/
void DrawFromNode(const ITreeNodeInterface* NodeInterface);
private:
/**
* 递归绘制一支"上游"或"下游"分支。
* @param bUpperStream - true 表示绘制"上游"(输入源),false 表示"下游"(输出目标)。
* @param StartingNodeInterface - 当前分支的起始节点接口。
* @param StartingNodeWidget - 当前节点的UI控件,用于连线起点或终点。
* @param StartingNodeSlot - 当前节点的画布中的插槽。
* @param StartingNodeDepth - 深度(层级),用于垂直位置计算。
* @param NextLeafXPosition - 引用参数,记录绘制下一个叶子节点时应该使用的水平偏移。
* @param OutStreamSlots - 所有节点槽的集合,后续做居中调整。
*/
void DrawStream(
bool bUpperStream,
const ITreeNodeInterface* StartingNodeInterface,
UUserWidget* StartingNodeWidget,
UCanvasPanelSlot* StartingNodeSlot,
int32 StartingNodeDepth,
float& NextLeafXPosition,
TArray<UCanvasPanelSlot*>& OutStreamSlots
);
/** 清空当前画布上的所有节点和连线 */
void ClearTree();
/**
* 根据一个节点接口创建对应的 Widget 并添加到 CanvasPanel,
* 同时返回该 Widget 在 CanvasPanel 上的 Slot 以便后续定位。
* @param Node - 要显示的数据节点接口。
* @param OutCanvasSlot - 输出参数,生成的 CanvasPanelSlot。
* @return 新创建的 UUserWidget*,或 nullptr。
*/
UUserWidget* CreateWidgetForNode(const ITreeNodeInterface* Node, UCanvasPanelSlot*& OutCanvasSlot);
/**
* 创建一条从 "From" Widget 到 "To" Widget 的连线(SplineWidget)。
* @param From - 起始节点 Widget。
* @param To - 目标节点 Widget。
*/
void CreateConnection(const UUserWidget* From, UUserWidget* To);
// 画布根节点
UPROPERTY(meta = (BindWidget))
TObjectPtr<UCanvasPanel> RootPanel;
/** 用来记录当前树的中心物品,避免重复绘制同一个树 */
UPROPERTY()
TObjectPtr<const UObject> CurrentCenterItem;
/** 节点尺寸(宽高),可在编辑器中调整 */
UPROPERTY(EditDefaultsOnly, Category = "Tree")
FVector2D NodeSize = FVector2D{ 60.f };
/** 节点之间的水平和垂直间隔 */
UPROPERTY(EditDefaultsOnly, Category = "Tree")
FVector2D NodeGap = FVector2D{ 16.f, 30.f};
/** 连线颜色 */
UPROPERTY(EditDefaultsOnly, Category = "Tree")
FLinearColor ConnectionColor = FLinearColor{0.8f, 0.8f, 0.8f, 1.f};
/** 连线粗细 */
UPROPERTY(EditDefaultsOnly, Category = "Tree")
float ConnectionThickness = 3.f;
/** 连线起点相对于节点大小的本地坐标(百分比) */
UPROPERTY(EditDefaultsOnly, Category = "Tree")
FVector2D SourcePortLocalPos = FVector2D{ 0.5f, 0.9f };
/** 连线终点相对于节点大小的本地坐标(百分比) */
UPROPERTY(EditDefaultsOnly, Category = "Tree")
FVector2D DestinationPortLocalPos = FVector2D{ 0.5f, 0.1f };
/** 起点连线方向(角度) */
UPROPERTY(EditDefaultsOnly, Category = "Tree")
FVector2D SourcePortDirection = FVector2D{ 0.f, 90.f };
/** 终点连线方向(角度) */
UPROPERTY(EditDefaultsOnly, Category = "Tree")
FVector2D DestinationPortDirection = FVector2D{ 0.f, 90.f };
};
cpp
#include "ItemTreeWidget.h"
#include "SplineWidget.h"
#include "TreeNodeInterface.h"
#include "Components/CanvasPanel.h"
#include "Components/CanvasPanelSlot.h"
void UItemTreeWidget::DrawFromNode(const ITreeNodeInterface* NodeInterface)
{
if (!NodeInterface) return;
// 避免重复绘制:检查是否与当前中心节点相同
if (CurrentCenterItem == NodeInterface->GetItemObject()) return;
// 清空画布
ClearTree();
// 记录中心物品
CurrentCenterItem = NodeInterface->GetItemObject();
// 用于计算叶子节点的水平位置
float NextLeafXPos = 0.0f;
UCanvasPanelSlot* CenterWidgetPanelSlot = nullptr;
// 创建中心节点的UI
UUserWidget* CenterWidget = CreateWidgetForNode(NodeInterface, CenterWidgetPanelSlot);
// 分别绘制下游节点和上游节点
TArray<UCanvasPanelSlot*> LowerStreamSlots, UpperStreamSlots;
// 绘制下游
DrawStream(
false, // bUpperStream = false (下游)
NodeInterface, // 起始节点接口
CenterWidget, // 中心节点控件
CenterWidgetPanelSlot, // 中心节点位置槽
0, // 起始深度
NextLeafXPos, // 叶节点位置计数器
LowerStreamSlots // 输出:下游节点槽集合
);
// 计算总宽度,然后将其居中
float LowerStreamXMax = NextLeafXPos - NodeSize.X - NodeGap.X;
// 居中偏移量
float LowerMoveAmt = 0.f - LowerStreamXMax / 2.0f;
// 应用下游分支偏移(水平居中)
for (UCanvasPanelSlot* StreamSlot : LowerStreamSlots)
{
StreamSlot->SetPosition(StreamSlot->GetPosition() + FVector2D(LowerMoveAmt, 0.f));
}
// 刷新水平位置
NextLeafXPos = 0.f;
// 向上绘制
DrawStream(
true, // bUpperStream = true (上游)
NodeInterface, // 起始节点接口
CenterWidget, // 中心节点控件
CenterWidgetPanelSlot, // 中心节点位置槽
0, // 起始深度
NextLeafXPos, // 叶节点位置计数器
UpperStreamSlots // 输出:上游节点槽集合
);
// 计算上游分支最大X位置
float UpperStreamXMax = NextLeafXPos - NodeSize.X - NodeGap.X;
// 计算上游分支居中偏移量
float UpperMoveAmt = 0.f - UpperStreamXMax / 2.0f;
// 应用上游分支偏移(水平居中)
for (UCanvasPanelSlot* StreamSlot : UpperStreamSlots)
{
StreamSlot->SetPosition(StreamSlot->GetPosition() + FVector2D{UpperMoveAmt, 0.f});
}
// 将中心节点置于坐标系原点 (0,0)
CenterWidgetPanelSlot->SetPosition(FVector2D::Zero());
}
void UItemTreeWidget::DrawStream(bool bUpperStream, const ITreeNodeInterface* StartingNodeInterface,
UUserWidget* StartingNodeWidget, UCanvasPanelSlot* StartingNodeSlot, int32 StartingNodeDepth,
float& NextLeafXPosition, TArray<UCanvasPanelSlot*>& OutStreamSlots)
{
// 节点的生成方式类似于多叉树遍历的前序遍历:采用深度优先策略来遍历,一路走到黑
// 如果bUpperStream为True 获取合成的目标,将向上递归(生成树往上画),否则获取合成的材料物品,向下进行递归(生成树往下画)
TArray<const ITreeNodeInterface*> NextTreeNode = bUpperStream ? StartingNodeInterface->GetInputs() : StartingNodeInterface->GetOutputs();
// 计算当前节点的垂直位置 = (节点的高度 + 节点之间的垂直间隔) * 当前的深度 * (向上连接的话乘负数,向下连接乘正数)
float StartingNodeYPos = (NodeSize.Y + NodeGap.Y) * StartingNodeDepth * (bUpperStream ? -1 : 1);
// 递归的退出条件(遇到叶子节点)
if (NextTreeNode.Num() == 0)
{
// 设置节点的位置
StartingNodeSlot->SetPosition(FVector2D{NextLeafXPosition, StartingNodeYPos});
// 更新下一个叶节点的位置:向右移动(节点的宽度 + 水平间距)(用于将父类节点或者是将后面遍历到的兄弟节点往右边移动)
NextLeafXPosition += NodeSize.X + NodeGap.X;
return;
}
// 累加子节点水平位置
float NextNodeXPosSum = 0;
// 遍历所有子节点
for (const ITreeNodeInterface* NextTreeNodeInterface : NextTreeNode)
{
// 子节点的画布插槽
UCanvasPanelSlot* NextWidgetSlot;
// 创建子节点UI
UUserWidget* NextWidget = CreateWidgetForNode(NextTreeNodeInterface, NextWidgetSlot);
OutStreamSlots.Add(NextWidgetSlot); // 添加节点槽
// 创建线条连接两个UI
if (bUpperStream)
{
// 如果向上连接的话,由下一个节点(NextWidget)连接到当前节点(StartingNodeWidget)
CreateConnection(NextWidget, StartingNodeWidget);
}else
{
// 向下连接,当前节点指向下一个节点
CreateConnection(StartingNodeWidget, NextWidget);
}
// 深度优先遍历: 处理子节点的子树
DrawStream(
bUpperStream, // 保持分支方向
NextTreeNodeInterface, // 子节点接口
NextWidget, // 子节点控件
NextWidgetSlot, // 子节点位置槽
StartingNodeDepth + 1, // 深度 + 1
NextLeafXPosition, // 传递叶节点位置(引用修改)
OutStreamSlots // 继续收集节点槽
);
// 累加子节点水平位置(累加的时候,该节点已经经过了循环外面的位置计算了,所以这里的值是有的)
NextNodeXPosSum += NextWidgetSlot->GetPosition().X;
}
// ===== 位置计算 =====
// 计算当前节点的水平位置 = 所有子节点位置的平均值
float AvgXNodePos = NextNodeXPosSum / NextTreeNode.Num();
// 设置当前节点位置(水平居中于子节点, 垂直按深度排列)
StartingNodeSlot->SetPosition(FVector2D{AvgXNodePos, StartingNodeYPos});
}
void UItemTreeWidget::ClearTree()
{
// 清空画布
RootPanel->ClearChildren();
}
UUserWidget* UItemTreeWidget::CreateWidgetForNode(const ITreeNodeInterface* Node, UCanvasPanelSlot*& OutCanvasSlot)
{
if (!Node) return nullptr;
// 创建节点的Widget
UUserWidget* NodeWidget = Node->GetWidget();
// 将生成的控件添加到Canvas根面板中
// AddChildToCanvas返回值为画布插槽指针,用于调整布局参数
OutCanvasSlot = RootPanel->AddChildToCanvas(NodeWidget);
if (OutCanvasSlot)
{
// 指定它的大小、锚点在中心、对齐方式也居中、层级为 1
OutCanvasSlot->SetSize(NodeSize);
OutCanvasSlot->SetAnchors(FAnchors(0.5f));
OutCanvasSlot->SetAlignment(FVector2D(0.5f));
OutCanvasSlot->SetZOrder(1);
}
return NodeWidget;
}
void UItemTreeWidget::CreateConnection(const UUserWidget* From, UUserWidget* To)
{
if (!From || !To) return;
// 创建线条UI
USplineWidget* Connection = CreateWidget<USplineWidget>(GetOwningPlayer());
// 创建画布插槽,并将线条UI添加到画布插槽中,再添加到画布中
UCanvasPanelSlot* ConnectionSlot = RootPanel->AddChildToCanvas(Connection);
if (ConnectionSlot)
{
// 设置连线底层,确保在节点下面
ConnectionSlot->SetAnchors(FAnchors{0.f});
ConnectionSlot->SetAlignment(FVector2D{0.f});
ConnectionSlot->SetPosition(FVector2D::Zero());
ConnectionSlot->SetZOrder(0);
}
// 设置线条的起点、终点、本地偏移和方向
Connection->SetupSpline(
From, To,
SourcePortLocalPos, DestinationPortLocalPos,
SourcePortDirection, DestinationPortDirection
);
// 设置颜色和粗细
Connection->SetSplineStyle(ConnectionColor, ConnectionThickness);
}
将合成树添加到商店中
cpp
// 物品合成树控件
UPROPERTY(meta=(BindWidget))
TObjectPtr<UItemTreeWidget> CombinationTree;
// 显示指定物品的合成树
void ShowItemCombination(const UShopItemWidget* ItemWidget);
cpp
void UShopWidget::ShopItemWidgetGenerated(UUserWidget& NewWidget)
{
// 转换为商店物品控件
UShopItemWidget* ItemWidget = Cast<UShopItemWidget>(&NewWidget);
if (ItemWidget)
{
// 绑定购买事件到库存系统
if (OwnerInventoryComponent)
{
ItemWidget->OnItemPurchaseIssued.AddUObject(
OwnerInventoryComponent,
&UInventoryComponent::TryPurchase);
}
// 绑定选择事件(鼠标左键)到合成树显示
ItemWidget->OnShopItemClicked.AddUObject(
this,
&UShopWidget::ShowItemCombination);
// 添加到物品映射表
ItemsMap.Add(ItemWidget->GetShopItem(), ItemWidget);;
}
}
void UShopWidget::ShowItemCombination(const UShopItemWidget* ItemWidget)
{
if (CombinationTree)
{
// 绘制合成树,以传入物品为Root
CombinationTree->DrawFromNode(ItemWidget);
}
}
创建合成树的蓝图
放入一个画布面板再用尺寸框包裹住,设置一个大小
用水平框包裹住原本的包裹着商店的垂直框,并将创建好的合成树画布放进来并改名
物品的子节点都在这里配置(自由随意搭配,这里也都是乱搞的,感觉奇奇怪怪的)
暂且先告一段落,明天我应该就能把库存组件做完了
完整商店物品ShopItemWidget
代码
cpp
#pragma once
#include "CoreMinimal.h"
#include "TreeNodeInterface.h"
#include "Blueprint/IUserObjectListEntry.h"
#include "Inventory/PDA_ShopItem.h"
#include "UI/Common/ItemWidget.h"
#include "ShopItemWidget.generated.h"
class UShopItemWidget;
// 声明委托:当物品购买请求发出时触发(参数:商店物品数据资产)
DECLARE_MULTICAST_DELEGATE_OneParam(FOnItemPurchaseIssused, const UPDA_ShopItem*);
// 声明委托:当商店物品被选中时触发(参数:商店物品控件实例)
DECLARE_MULTICAST_DELEGATE_OneParam(FOnShopItemSelected, const UShopItemWidget*);
/**
* 商店物品UI控件
* 功能:
* - 继承基础物品控件功能
* - 实现列表项接口(用于ListView)
* - 实现树节点接口(用于合成树展示)
* - 处理购买和选择事件
* 使用场景:商店界面、合成系统界面
*/
UCLASS()
class UShopItemWidget : public UItemWidget,
public IUserObjectListEntry,
public ITreeNodeInterface
{
GENERATED_BODY()
public:
// 委托:物品购买请求发出时广播
FOnItemPurchaseIssused OnItemPurchaseIssued;
// 委托:物品被点击选择时广播
FOnShopItemSelected OnShopItemClicked;
//~ Begin ITreeNodeInterface 接口实现
// 获取当前控件实例(树节点接口要求)
virtual UUserWidget* GetWidget() const override;
// 获取输入节点(合成树中的材料项)
virtual TArray<const ITreeNodeInterface*> GetInputs() const override;
// 获取输出节点(合成树中的产出项)
virtual TArray<const ITreeNodeInterface*> GetOutputs() const override;
// 获取关联的数据对象(商店物品资产)
virtual const UObject* GetItemObject() const override;
//~ End ITreeNodeInterface 接口实现
//~ Begin IUserObjectListEntry 接口实现
// 当列表项绑定数据对象时调用(通常为UPA_ShopItem实例)
virtual void NativeOnListItemObjectSet(UObject* ListItemObject) override;
//~ End IUserObjectListEntry 接口实现
// 获取当前绑定的商店物品数据
FORCEINLINE const UPDA_ShopItem* GetShopItem() const { return ShopItem; }
private:
// 从另一个商店物品控件复制数据(用于列表项重用)
void CopyFromOther(const UShopItemWidget* OtherWidget);
// 使用商店物品数据初始化控件
void InitWithShopItem(const UPDA_ShopItem* NewShopItem);
/**
* 将商店物品数组转换为树节点接口数组
* @param Items 商店物品数组
* @return 对应的树节点接口数组
*/
TArray<const ITreeNodeInterface*> ItemsToInterfaces(const TArray<const UPDA_ShopItem*>& Items) const;
// 父级ListView控件(此物品所属的列表视图)
UPROPERTY()
TObjectPtr<const class UListView> ParentListView;
// 当前绑定的商店物品数据资产
UPROPERTY()
TObjectPtr<const UPDA_ShopItem> ShopItem;
//~ Begin UItemWidget 重写
// 右键点击处理:触发购买委托
virtual void RightButtonClicked() override;
// 左键点击处理:触发选择委托
virtual void LeftButtonClicked() override;
//~ End UItemWidget 重写
};
cpp
#include "ShopItemWidget.h"
#include "Components/ListView.h"
#include "Framework/CAssetManager.h"
UUserWidget* UShopItemWidget::GetWidget() const
{
// 创建一样的商店物品UI
UShopItemWidget* Copy = CreateWidget<UShopItemWidget>(GetOwningPlayer(), GetClass());
// 复制商店物品UI数据
Copy->CopyFromOther(this);
return Copy;
}
TArray<const ITreeNodeInterface*> UShopItemWidget::GetInputs() const
{
// 获取可合成该物品的材料列表
const FItemCollection* Collection = UCAssetManager::Get().GetCombinationForItem(GetShopItem());
// 如果找到了材料,就把它们转换为树节点返回
if (Collection)
{
return ItemsToInterfaces(Collection->GetItems());
}
return TArray<const ITreeNodeInterface*>{};
}
TArray<const ITreeNodeInterface*> UShopItemWidget::GetOutputs() const
{
// 获取合成该物品所需的材料列表
const FItemCollection* Collection = UCAssetManager::Get().GetIngredientForItem(GetShopItem());
// 转换为树节点(例如合成树中的"箭头"方向)
if (Collection)
{
return ItemsToInterfaces(Collection->GetItems());
}
return TArray<const ITreeNodeInterface*>{};
}
const UObject* UShopItemWidget::GetItemObject() const
{
return ShopItem;
}
void UShopItemWidget::NativeOnListItemObjectSet(UObject* ListItemObject)
{
IUserObjectListEntry::NativeOnListItemObjectSet(ListItemObject);
// ShopItem = Cast<UPDA_ShopItem>(ListItemObject);
// if (!ShopItem) return;
//
// SetIcon(ShopItem->GetIcon());
//
// SetToolTipWidget(ShopItem);
// 使用新数据初始化控件
InitWithShopItem(Cast<UPDA_ShopItem>(ListItemObject));
// 获取所属的ListView控件
ParentListView = Cast<UListView>(IUserListEntry::GetOwningListView());
}
void UShopItemWidget::CopyFromOther(const UShopItemWidget* OtherWidget)
{
// 拷贝事件绑定(点击购买、点击选中)
OnItemPurchaseIssued = OtherWidget->OnItemPurchaseIssued;
OnShopItemClicked = OtherWidget->OnShopItemClicked;
// 复制父列表
ParentListView = OtherWidget->ParentListView;
// 使用商店物品数据初始化界面
InitWithShopItem(OtherWidget->GetShopItem());
}
void UShopItemWidget::InitWithShopItem(const UPDA_ShopItem* NewShopItem)
{
// 设置商店物品数据
ShopItem = NewShopItem;
if (!ShopItem) return;
// 设置图标
SetIcon(ShopItem->GetIcon());
// 创建并设置提示信息
SetToolTipWidget(ShopItem);
}
TArray<const ITreeNodeInterface*> UShopItemWidget::ItemsToInterfaces(const TArray<const UPDA_ShopItem*>& Items) const
{
// 创建用来存储的树节点的数组
TArray<const ITreeNodeInterface*> RetInterfaces;
if (!ParentListView) return RetInterfaces;
// 遍历物品数组,把它们从 ListView 中转换为 Widget(然后作为树节点返回)
for (const UPDA_ShopItem* Item : Items)
{
// 从ParentListView列表视图中获取与Item数据项关联的UShopItemWidget控件
// ParentListView存储的就是商店的那个框框
const UShopItemWidget* ItemWidget = ParentListView->GetEntryWidgetFromItem<UShopItemWidget>(Item);
if (ItemWidget)
{
// 将对应的树节点添加到数组中
RetInterfaces.Add(ItemWidget);
}
}
return RetInterfaces;
}
void UShopItemWidget::RightButtonClicked()
{
// 购买
OnItemPurchaseIssued.Broadcast(GetShopItem());
}
void UShopItemWidget::LeftButtonClicked()
{
// 选中
OnShopItemClicked.Broadcast(this);
}
完整商店UIShopWidget
代码
cpp
#pragma once
#include "CoreMinimal.h"
#include "ShopItemWidget.h"
#include "Blueprint/UserWidget.h"
#include "Components/TileView.h"
#include "Inventory/InventoryComponent.h"
#include "Inventory/PDA_ShopItem.h"
#include "ShopWidget.generated.h"
class UItemTreeWidget;
/**
* 商店界面主控件
* 功能:
* - 显示可购买的商品列表
* - 展示物品的合成关系树
* - 管理商店物品加载和显示
* - 处理物品选择和合成展示
*/
UCLASS()
class CRUNCH_API UShopWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
private:
// 商店物品列表
UPROPERTY(meta = (BindWidget))
TObjectPtr<UTileView> ShopItemList;
// 物品合成树控件
UPROPERTY(meta=(BindWidget))
TObjectPtr<UItemTreeWidget> CombinationTree;
// 加载商店物品
void LoadShopItems();
// 商店物品加载完成
void ShopItemLoadFinished();
// 商店物品生成
void ShopItemWidgetGenerated(UUserWidget& NewWidget);
// 商店物品到控件的映射表
// 用途:快速查找物品对应的控件实例
UPROPERTY()
TMap<const UPDA_ShopItem*, const UShopItemWidget*> ItemsMap;
// 库存组件:获取玩家的库存
UPROPERTY()
TObjectPtr<UInventoryComponent> OwnerInventoryComponent;
// 显示指定物品的合成树
void ShowItemCombination(const UShopItemWidget* ItemWidget);
};
cpp
#include "ShopWidget.h"
#include "ItemTreeWidget.h"
#include "Framework/CAssetManager.h"
void UShopWidget::NativeConstruct()
{
Super::NativeConstruct();
// 设置可聚焦
SetIsFocusable(true);
// 加载物品
LoadShopItems();
// 绑定列表项生成事件
ShopItemList->OnEntryWidgetGenerated().AddUObject(this, &UShopWidget::ShopItemWidgetGenerated);
// 获取玩家的库存组件并存储起来
if (APawn* OwnerPawn = GetOwningPlayerPawn())
{
OwnerInventoryComponent = OwnerPawn->GetComponentByClass<UInventoryComponent>();
}
}
void UShopWidget::LoadShopItems()
{
// 调用资产管理器的异步加载方法
// 加载完成后触发 ShopItemLoadFinished 回调
UCAssetManager::Get().LoadShopItems(
FStreamableDelegate::CreateUObject(this, &UShopWidget::ShopItemLoadFinished)
);
}
void UShopWidget::ShopItemLoadFinished()
{
// 获取所有已加载的商店物品
TArray<const UPDA_ShopItem*> ShopItems;
if (UCAssetManager::Get().GetLoadedShopItems(ShopItems))
{
// 添加商店物品
for (const UPDA_ShopItem* ShopItem : ShopItems)
{
ShopItemList->AddItem(const_cast<UPDA_ShopItem*>(ShopItem));
}
}
}
void UShopWidget::ShopItemWidgetGenerated(UUserWidget& NewWidget)
{
// 转换为商店物品控件
UShopItemWidget* ItemWidget = Cast<UShopItemWidget>(&NewWidget);
if (ItemWidget)
{
// 绑定购买事件到库存系统
if (OwnerInventoryComponent)
{
ItemWidget->OnItemPurchaseIssued.AddUObject(
OwnerInventoryComponent,
&UInventoryComponent::TryPurchase);
}
// 绑定选择事件(鼠标左键)到合成树显示
ItemWidget->OnShopItemClicked.AddUObject(
this,
&UShopWidget::ShowItemCombination);
// 添加到物品映射表
ItemsMap.Add(ItemWidget->GetShopItem(), ItemWidget);;
}
}
void UShopWidget::ShowItemCombination(const UShopItemWidget* ItemWidget)
{
if (CombinationTree)
{
// 绘制合成树,以传入物品为Root
CombinationTree->DrawFromNode(ItemWidget);
}
}