前言:
笔者目前在校本科大三,目标方向是人工智能、计算机视觉。上一个OpenCV学习笔记专栏已完结,在学习完OpenCV后,我继续学习C++,并用纯C++做UE4项目的方式继续提升自己的水平。
梁迪老师的水平非常高,他的课程本来也无需笔记:课程本身即为最好的笔记。但由于我天赋有限,还是边看边记,以防遗忘——知识点太多,步骤太繁杂了。在学习过程中,我也偶有思考,思索为什么某个方法老师要这样做。所以,一是为了记录,二是为了分享,才有了这个专栏。
内容方面,由于我在开启这个专栏时,此项目已经做完很多了。所以,前期的一些大篇幅叙述的知识,可能在后期应用中一带而过。以及,前期的一些知识,后期会重新剖析,并加上我的个人理解。
另外,若有学术交流/学业交流意愿,可以邮件联系1246210283@qq.com,希望一齐进步。
本篇学习内容:
47.Widget获取位置和大小
48.背包系统
47.Widget获取位置和大小
FGeometry类下的参数解释:
Position
获取相对于父类的位置
AbsolutePosition
获取在整个电脑屏幕中的位置
Size
获取这个Widget的大小
获取一个Widget A相对于某个Widget B的位置:(下面的A和B是对应的FGeometry)
B.AbsoluteToLocal(A.AbsolutePosition);
在鼠标事件中获取鼠标相对于Widget的位置:
MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition());
获取DPI缩放:
UWidgetLayoutLibrary::GetViewportScale(GEngine->GameViewport);
获取鼠标在游戏窗口的位置:
#include "Engine/GameEngine.h"
#include "Engine/Engine.h"
//获取鼠标在游戏屏幕里的位置,这个位置没有经过DPI缩放
FVector2D MousePos;
if(GEngine) GEngine->GameViewport->GetMousePosition(MousePos);
//如果想要获取经过DPI缩放的位置,要获取DPIScaler
MousePos = MousePos/DPIScaler;
48.背包系统
背包系统有6个文件:ContainerBaseWidget、ContainerInputWidget、ContainerNormalWidget、ContainerOutputWidget、ContainerShortcutWidget、PackageWidget.其中,Input、Normal、Output、Shortcut类都继承自ContainerBaseWidget.
(1)枚举类准备
//背包容器类型
namespace EContainerType
{
enum Type
{
Output,//合成表输出:背包合成台里的输出
Input,//合成表输入:背包合成台里的输入
Normal,//普通容器:背包中间那一堆,背包空间
Shortcut,//快捷栏容器:背包下面的快捷栏容器
};
}
(2)WorkIndex与子类传入给父类
父类Base中定义了一个参数SLATE_ATTRIBUTE(int,WorkIndex)
,并且有一个工厂方法static TSharedPtr<SSlAiContainerBaseWidget> CreateContainer(EContainerType::Type NeedType, int WorkID);
如果想让子类创建时传入的这个WorkID传给父类,就需要在子类里创建一个一样的参数。随后在子类的构造函数中:
SSlAiContainerBaseWidget::Construct(
SSlAiContainerBaseWidget::FArguments()
.WorkIndex(InArgs._WorkIndex) //在子类的构造函数中,通过这种方式,把构建子类时传递的WorkIndex传递给了父类
);
随后在工厂方法中:
TSharedPtr<SSlAiContainerBaseWidget> SSlAiContainerBaseWidget::CreateContainer(EContainerType::Type NeedType, int WorkID)
{
TSharedPtr<SSlAiContainerBaseWidget> ResultContainer;
switch (NeedType)
{
case EContainerType::Output:
SAssignNew(ResultContainer, SSlAiContainerOutputWidget).WorkIndex(WorkID);
break;
...
}
}
这样,就可以把WorkIndex传给它的父类,并且对父类下的WorkIndex进行赋值。
(3)写PackageWidget结构
背包:根组件为SOverlay(因为背包是全屏幕填充的),根组件下创建SBox,SBox下创建SOverlay,再创建数个Slot,分别放背景图、底部快捷栏、背包主体、合成表输入框、合成表输出框、合成小箭头。
显然地,需要准备表格控件:快捷栏表格、背包表格、合成表表格和输出容器。
//表格控件:快捷栏表格、背包表格、合成表表格
TSharedPtr<class SUniformGridPanel> ShortcutGrid;
TSharedPtr<SUniformGridPanel> PackageGrid;
TSharedPtr<SUniformGridPanel> CompoundGrid;
//输出容器
TSharedPtr<class SBorder> OutputBorder;
随后在cpp文件中写好构造函数。
(4)写初始化背包函数,这个函数作为委托给GameMode调用
(5)鼠标位置获取
在PackageWidget创建变量标定鼠标位置,再定义一个SLATE_ATTRIBUTE(float, UIScaler)
来获取DPI缩放。由于背包UI是在GameHUD中实例化的,所以在GameHUD创建背包UI时将自己的UIScaler传给背包UI。
初始化鼠标位置后,重写Tick函数,每帧获取鼠标位置。
(6)鼠标选中背包容器
鼠标移动到背包容器上时,容器变色。
在PackageManager中准备方法:
public:
//更新悬停的容器颜色
void UpdateHoverd(FVector2D MousePos, FGeometry PackGeo);
private:
//获取鼠标指向的容器
TSharedPtr<SSlAiContainerBaseWidget> LocateContainer(FVector2D MousePos, FGeometry PackGeo);
这些方法写好之后,在PackageWidget的Tick函数中逐帧调用。
(7)背包拖拽
重写Onpaint
函数和OnMouseButtonDown
函数。
其中,Onpaint绘制物品和物品数量。OnMouseButtonDown调用PackageManager的LeftOption和RightOption。
随后在ContainerBaseWidget中准备:
//物品序号
int ObjectIndex;
//物品数量
int ObjectNum;
//获取容器的物品ID和数量
int GetIndex() const;
int GetNum() const;
//左键点击操作
virtual void LeftOperate(int InputID, int InputNum, int&OutputID, int& OutputNum);
//右键点击操作
virtual void RightOperate(int InputID, int InputNum, int&OutputID, int& OutputNum);
//获取物品是否可以叠加
bool MultiplyAble(int ObjectID);
须知,我们需要实现哪些功能:(Normal容器下的规则)
Normal下的规则写在了虚函数LeftOperate、RightOperate下,其余的合成台等,可以对其进行重写规则。
鼠标左键选取背包某一格上物品,会拿起全部物品;右键选取背包上某一格物品,会拿起一半物品;
鼠标拿起物品时,左键或右键点击背包上某一格,如果此格上有物品,则会交换;如果此格上没有物品,左键会放下全部物品;如果此格上没有物品,右键会放下一个物品;
以此规则来写LeftOperate、RightOperate函数。
写完之后,可以写PackageManager的LeftOption和RightOption函数了。
(8)丢弃物品、绑定快捷栏
在ContainerBaseWidget中定义委托:
//合成输入委托
DECLARE_DELEGATE(FCompoundInput)
//合成提取委托,参数是物品序号,物品数量
DECLARE_DELEGATE_TwoParams(FCompoundOutput,int,int)
//丢弃物品委托,参数是物品序号,物品数量
DECLARE_DELEGATE_TwoParams(FThrowObject,int,int)
//背包快捷栏更新引起游戏变化委托,参数分别是快捷栏序号、更新的物品ID、更新物品数量
DECLARE_DELEGATE_ThreeParams(FPackShortChange,int,int,int)
然后重写ContainerOutputWidget的LeftOperate
、RightOperate
函数
再重写ContainerInputWidget的ResetContainerPara
函数
再重写ContainerShortcutWidget的ResetContainerPara
函数
void SSlAiContainerShortcutWidget::ResetContainerPara(int ObjectID, int Num)
{
bool IsChanged = false;
if (ObjectIndex != ObjectID || ObjectNum != Num) IsChanged = true;
//调用父类事件 放在中间是因为父类事件会对ObjectIndex和ObjectNum进行修改
SSlAiContainerBaseWidget::ResetContainerPara(ObjectID, Num);
//如果有改变,执行快捷栏修改更新委托,传出快捷栏序号以及新的物品ID
if (IsChanged) PackShortChange.ExecuteIfBound(WorkIndex.Get(), ObjectID, Num);
}
这些重写的函数都会用到上面定义的委托。下面来绑定委托。
委托绑定的方法在PackageManager下,然后在SlAiPackageManager::InsertContainer
函数下对委托进行绑定。
另外这四个委托绑定的方法中,ThrowObject和PackShortChange里面额外调用PackageManager创建的2个委托变量,绑定在背包初始化函数。在PackageManager的LeftOption中,丢弃物品直接为ThrowObject函数(即ContainerBase中委托绑定的函数),且这个函数中调用PackageManager创建的委托。
在PackageManager中另创建2个委托变量的好处:不用在PackageManager中引入PlayerState等文件。
(9)合成台实现
在JsonHandle中读取合成表数据,(自然要准备一个合成表结构体)
实现PackageManager的CompoundOutput
和CompoundInput
函数
(10)添加物品到背包
快捷栏优先级最高,然后是背包主体。另外,如果有一个物品背包里有但是快捷栏没有,那么就添加到背包。
主要实现Private的SlAiPackageManager::SearchFreeSpace方法,然后创建一个SlAiPackageManager::SearchFreeSpace给外部访问,再创建一个AddObject给PlayerCharacter调用
在PlayerCharacter中写好相应调用函数后,在FlobObject、PlayerContainer下补全之前的代码。
(11)吃东西更新背包快捷栏
PackageManager下bool EatUpEvent(int ShortcutID);然后PlayerCharacter下void EatUpEvent();调用Package下的函数。然后PlayerAnim调用PlayerCharacter下函数。