Painting

视频教程已同步发布至B站:https://www.bilibili.com/video/BV1oY411r7mY

画作

首先完成常规的项目创建操作,将多余的物体全部删掉,然后新建复制默认的游戏模式和VRPawn,并将默认地图另存为PaintingMap

image-20230221224853160

画点连线

创建一个Actor,命名为BP_Stroke,并在这个Stroke下创建两个“实例化静态网格体组件”

image-20230221225723725

导入Cylinder和Sphere(拖入即可),然后创建一个M_Stroke材质备用

注意混合模式要选择半透明,这能保证在笔画堆叠的情况下不会出现闪烁的效果

image-20230221232949816

image-20230221232928904

接着在StrokeMeshes中选择Cylinder静态网格体和M_stroke材质;在JointMeshes中选择Sphere静态网格体和M_stroke材质。

image-20230221233339942

创建PreviousCursorLocation和ControlPoints变量,以及Update函数

image-20230221233312769

在PaintingPawn中调用Update函数,实现按下Trigger生成连续小球的功能

image-20230221233430348

将Joint反转变换位置收缩为GetNextJointTransform函数

image-20230222084026910

创建一个GetNextSegmentTransform函数,用于计算每一帧的点之间的长度和旋转

image-20230222084157015

在Update中更新StrokeMeshes和JointMeshes,即可实现画点连线

image-20230222084236297

为了存档,需要将每次的位置记录在Control Points中

image-20230222231619588

接着改造环境,进入封闭的场景,发现初始的第一个点始终在原点,解决方案如下

image-20230222084948992

保存画作

在PaintingPawn中创建一个Painting变量,用于保存本画作的名字

image-20230222223136863

创建一个Stroke结构,用于存放ControlPoints数组

image-20230222233531154

创建一个SaveGame类,命名为BP_PainterSave

image-20230222223208673

创建一个Strokes变量,类型为Stroke结构的数组

接着再创建Save和Load两个函数,用于存储数据

image-20230222233636500

image-20230222225723680

在PaintingPawn中初始化游戏存档对象

image-20230222225819441

随后创建Save和Load两个操作映射,用于发起存档和读档操作

image-20230222225901501

实现存档操作

image-20230222233735182

当按下对应操作按钮的时候,就会把我们当前的画作存储到本地,效果如下

image-20230222230009441

读取存档

创建一个DestoryWorld函数,在读档之前先清空

image-20230222233812574

创建一个Load函数,用于加载存档

image-20230222233911874

其中SpawnAndDeserialStrokes函数,用于解压读档后的数据

image-20230222233933834

最后在PaintingPawn中调用Load函数即可完成读档

image-20230222234011522

画册

UI画板

地图配置

创建一个MainMenu地图,并创建与之对应的UIGameMode和UIPawn

UIGameMode直接复制上一个PaintingGameMode即可

UIPawn继承自VRPawn

在MainMenu中做好如下配置

image-20230223222012662

WBP_PaintingGrid

创建一个控件蓝图,继承自用户控件,命名为WBP_PaintingGrid

创建一个画布面板,注意锚点设定在中间,以及位置、尺寸、对齐和填充的设置

image-20230223215511136

尺寸框注意需要填充对齐,并将行和列都设置为对应的索引

image-20230223222214194

BP_PaintingPicker

创建一个Actor蓝图,命名为BP_PaintingPicker

创建一个样条Spline和一个控件组件PaintingGrid

Spline的配置细节如下

image-20230223222743275

切换到视口的右视图,将PaintingGrid的位置移动到样条的端点,移动之后,PaintingGrid的细节配置如下

image-20230223223019102

UIPawn

在UIPawn中新增一个控件交互组件,用于VR与控件蓝图之间的交互

image-20230223221132876

做好如下配置,然后在时间图表中补充这段蓝图,用于将手柄交互映射为鼠标交互

image-20230223223155150

地图配置

新创建的地图是空的,需要加上用户起始点以及上面创建的BP_PaintingPicker

image-20230223223452842

然后运行蓝图,你就会发现面前有一个面板,并且可以进行简单的交互

操作台

创建一个Img文件夹,将图像素材都导进去

创建一个控件蓝图,继承自用户控件,命名为WBP_ActionBar

创建两个按钮,分别是删除和新增

image-20230223230928968

image-20230223231100137

image-20230223231041087

image-20230223231111376

在BP_PaintingPicker中再创建一个样条,然后在对应的位置把这个ActionBar加进去

image-20230223231537103

image-20230223231559666

接着再运行VR,会看到下面有两个按钮,

image-20230223231638333

新增画作

对画作的主要操作都在BP_PaintingPicker中完成,

我们需要将ActionBar和PaintingPicker绑定起来

打开ActionBar的图表,创建函数,存储PaintingPicker的引用

image-20230224224205474

然后打开PaintingPicker,做一些初始化操作,关联ActionBar,同时存储PaintingGrid的面板

image-20230224224310233

注意PaintingGrid要将变量打开

image-20230224224417301

创建一个用户控件,命名为WBP_PaintingCard

image-20230224224532371

在WBP_PaintingCard的图表中新增一个Painting变量,用于存储这个画作的名字

image-20230224224611965

在PaintingPicker中创建AddPainting函数

image-20230224224651120

其中GetTimeStamp是获取时间戳,保证每个画作的名字不一样

image-20230224224745362

AddCard是新增画作卡片

image-20230224224846636

最后绑定ActionBar中的添加事件即可

image-20230224224926138

删除画作

本节要实现的功能是,点击操作台上的删除按钮,所有画作都会出现红框,然后点击对应的画作就可以删除

首先打开PaintingCard控件,创建一个PaintingPicker变量,用于绑定它两之间的关系

image-20230224232500419

然后在PaintingPicker的AddCard逻辑里加上这部分的初始化逻辑

image-20230224232548213

创建一个删除画作的函数,叫DeletePainting

核心思路是先从Paintings字符串数组里找到要删除的那个Painting,然后清空所有面板,接着根据新的Painting数组再创建所有卡片

image-20230224232805587

其中InitCards的逻辑如下

image-20230224232833644

接着为了控制删除模式和正常模式,我们需要创建一个ChangeDeleteMode函数,并创建好相关的变量(DeleteButtonStyle、NormalButtonStyle、DeleteMode)

DeleteButtonStyle是删除状态下的卡片样式

NormalButtonStyle是正常状态下的卡片样式

DeleteMode是一个布尔值,用于控制状态

image-20230224232957890

SetCardButtonStyle的详细逻辑如下

image-20230224233148894

控制好这两种模式之后,再创建一个CardClick函数,用于实现删除模式下,点击卡片就删除的效果

image-20230224233348811

最后在PaintingCard中调用该函数即可

image-20230224233421754

保存画板

创建一个SaveGame蓝图,命名为BP_MenuSave

创建Save和Load两个函数

image-20230225114910274

image-20230225114943309

在PaintingPicker初始化的时候,完成BP_MenuSave的初始化,并读取相应存档

image-20230225115705592

在新增画作或者删除画作的时候,也需要做存档,所以先创建一个存档函数,然后在相应的时候调用

image-20230225115801650

image-20230225115820210

image-20230225115839062

完成之后,整个画板就会自动保存和读档了

image-20230225115918629

优化

数据联通

创建一个游戏实例,命名为PaintingGameInstance

image-20230225122302402

在这个游戏实例中创建一个CurrentPainting变量

image-20230225122344868

在项目设置中应用这个游戏实例

image-20230225122417999

在用户点击卡片的时候,我们就将游戏实例里的CurrentPainting设置为对应卡片的Painting,并打开PaintingMap

image-20230225122805717

在初始化PaintingPawn的时候,根据CurrentPainting来读档就可以了

image-20230225123220341

最后补充一个返回画册的操作

image-20230225134358233

image-20230225134341293

画作封面

目前从画册里看,所有画作都是白色封面,我们需要做个截图来提醒用户每幅画的大致内容

先创建一个渲染目标,命名为RT_PainterSpectator

image-20230225123359069

然后配置它的细节,注意别填错

image-20230225123539563

接着打开PaintingPawn,创建一个场景捕获2D

image-20230225123622977

image-20230225123749054

在用户存档的时候,就顺便截个图

image-20230225124100798

为了防止在截图的时候,画面被头显挡住,我们可以关掉头显的可视

image-20230225125732568

(测试一下,存档之后会看到你的截图)

image-20230225124504463

接着回到PaintingPicker,我们需要读取图片,然后展示在画册上

创建一个DefaultImg变量,当用户新创建画作,或者画作截图为空的时候,就读取这个默认图片

image-20230225125249398

创建SetImg函数,实现给卡片填充封面的能力

image-20230225125209770

在初始化所有卡片的时候,补充上填充封面的功能

image-20230225125438707

在用户新增画作的时候,也需要补上默认封面

image-20230225125508806

至此所有的画册就都有封面了

笔画删除

我们在绘画的时候发现还无法删除笔画,这里就补上相关功能

首先在PainterPawn里面创建一个Painter静态网格体,用于指示准确的笔尖触点

image-20230225130329283

image-20230225130412694

为了区分删除模式和正常模式,我们想让Painter在不同的模式下显示出不用的颜色

首先在PaintingPawn初始化的时候创建动态材质实例

image-20230225131102981

然后按下右手Grab键的时候,设置不同的模式和颜色(注意DeleteMode变量需要提前创建)

image-20230225131136109

接着检测Painter和笔画之间的碰撞关系,如果在删除模式下碰到了,就直接删除该笔画

为了让碰撞顺利发生,我们还需要设置Painter和BP_stroke的碰撞预设

image-20230225131354819

image-20230225131425691

至此,整个VR绘画就基本完成了!

image-20230225133104922