斯坦福CS193u-使用虚幻引擎和C++的游戏开发[7]-存档与主菜单/UI优化

admin 2个月前 (02-14) 科技知识 45 0

  这门课程的所有录像将在一周内发布,可以在讲师的主页 等待消息。保存和加载游戏

  UE4中有专门应对存档功能的SaveGame类,作为游戏存档类,在UGameplayStatics库中有多种读/写SaveGame类的方式,包括SaveGameToSlot()AsyncSaveGameToSlot()LoadGameFromSlot()AsyncLoadGameFromSlot()DoesSaveGameExist()CreateSaveGameObject()

  我们可以用这些函数来保存SaveGame到硬盘或者从硬盘加载SaveGame。SaveGame是序列化的二进制文件,在PC平台上以.sav为后缀名,而在其他平台上可能为其他格式(例如Playstation上的存档)。

  首先使用CreateSaveGameObject(参数为我们的SaveGame类的StaticClass)函数来创建一个SaveGame的实例,然后写入游戏需要保存的数据,再使用SaveGameToSlot或者Async异步版来保存到硬盘即可。在实际游戏代码中,我们一般会在GameMode的InitGame函数中使用DoesSaveGameExist检测当前的存档位置是否有存档,如果没有就调用CreateSaveGameObject创建并SaveGameToSlot保存在这个存档位置,有存档则直接用LoadGameFromSlot加载。

  对于UE4中不同种类的数据,我们有不同的保存方法。最简单的是玩家状态PlayerState中的数据,例如我们之前制作的得分。我们只需要在我们的SaveGame类中声明同样类型的变量,然后在PlayerState类中从存档里读取自己的PlayerState就可以了:

  SPlayerState.h

  SPlayerState.cpp

  在创建好SaveGame实例/拿到存档位上的存档文件之后,对所有的玩家调用SavePlayerState即可保存得分变量。

  接下来,我们让存储的数据更加复杂:我们在存档中存储场景中移动的Actor的位置。为此,我们需要对世界中所有物体进行迭代,根据接口/类/GameplayTag等信息寻找到对应的物体,然后保存名称+变换信息。为此,我们声明了一个存储相关信息的结构体,将序列化后的信息存储在其中:

  并且我们以数组的形式来存储这些变量:

  在UE4中将变量序列化可以使用一个专用的序列化工具:FObjectAndNameAsStringProxyArchive(将Object和名称存储为String)来将这些信息转换为字节流,并且使用FMemoryWriter来将字节流存储在数组中。What is the best way to handle Saving/Loading an Array of Objects?

  同时,我们可以将Actor中声明的变量标记为UPROPERTY(SaveGame),这样在Actor序列化时,如果FObjectAndNameAsStringProxyArchive.ArIsSaveGame被标记为true,他就会自动被保存。

  我们保存游戏的逻辑就变成了这样:

  SlotName是以FString存储的存档槽名称,而CurrentSaveGame声明为我们的SaveGame类USSaveGame的指针。

  为了更好地找到世界中所有我们关心的Actor,我们使用了之前响应交互的接口来判别这个Actor会不会影响游戏的Gameplay。同时为了让不同的Actor在读入存档时初始化自己对应的数据,我们在接口中声明了函数处理加载逻辑

  现在,我们暂时存储我们已经进行网络复制的宝箱,这样我们可以在读取他的信息之后直接去通知所有客户端对读取到的宝箱开启状态进行复制:

  对于加载,就是把保存的过程反向走一遍,只不过我们将写入的FMemoryWriter修改为读取的FMemoryReader :

  由于每一局开始的玩家可能不一样,所以我们不能在LoadGame这个读取整个游戏世界状态的函数中去读取PlayerState,而是应该在每个玩家连接进游戏时读取他对应的信息。当然我们现在还没有对每个玩家赋予独特的ID,但是接入STEAM API或者之类的服务后我们就可以拿到这个ID信息:

  编写完保存和读取的逻辑后,我们重写GameModeBase的InitGame函数即可在开始游戏时读取保存文件:

  我们注意到,InitGame带有参数MapName,即加载的地图/关卡名称,但我们的存档目前还没有对每一个Actor记录对应的关卡名称(因为我们暂时还没有其他关卡)。

  为了让我们看到保存游戏的结果,我们制作一个类似黑魂中篝火的物体,当我们与他交互时保存游戏,并且如果有游戏被保存,在加载时这个篝火是BONFIRE LIT的状态:只要存档文件并没有删除,这个篝火在每次进入的时候都会是燃起的状态

  在编辑器模式下,存档文件默认保存在Saved\SaveGames文件夹下。

  1.PlayerState得分复制

  PlayerState.h

  PlayerState.cpp

  SPlayerController.h

  PlayerState与PlayerController对应,我们要在这里设置PlayerState的复制

  SPlayerController.cpp

  迁移创建HUD函数的位置

  SGameModeBase.cpp

  2.可拾取物的复制

  SPowerupActor.h

  .cpp

  3.怒气属性复制

  AttributeSet.h

  .cpp

  加分项:AI发现玩家的提示复制

  对于一个完整的游戏来说,主菜单部分的功能和制作是非常必要的。主菜单UI一般被放在一个单独的关卡中,这样做不仅可以提升游戏启动时的加载速度,也可以搭建主菜单的单独场景来让主菜单的表现更加好看。

  既然主菜单需要好看,那么这一讲当中我们主要关注UI的制作,简述UMG UI中样式设置的参数。我们计划在主菜单的UI上使用相同样式风格的按钮。样式设置

  对于基础的可调整边距等样式信息,UMG UI的调整方式和其他的前端网页代码没有什么不同。按钮的图像样式也有普通/Hover/Pressed/Disabled四种状态可以分别选择。值得一提的是,在UMG UI中,所有可以选择图像进行外观调整的选项都可以使用图片纹理或者自定义的材质(虽然材质会更加消耗性能),只需要在材质制作时将材质域选择为用户界面即可。

  对于直接使用纹理图像显示的UI,我们可以使用“边缘”选项来调整UI拉伸九宫格的位置。同时对于导入为UI纹理的图片,我们应该将压缩设置和纹理组设置为UI,并且关闭MipMap的生成,以便引擎在打包时更好地管理资源。

  UI的音效绑定在UMG编辑器中也有对应的选项位置。而UI的触发事件则更多地被用在处理游戏性逻辑和UI间交互的逻辑上。

  而在UI上显示的文本,默认会加入本地化,这时他会生成一个在本地化语言包中的引用键值,我们可以通过本地化表给为他指定应该使用的文本。

  如果我们不需要本地化内容,则可以关闭这个选项以节省空间。字体

  字体可以调整的选项非常丰富,虚幻引擎文档对于它有专门的介绍章节。

  在制作好UI的样式之后,我们为UI组件添加一个事件分发器(Dispatcher)并且让按钮的点击事件触发这个分发器以在UI之间/UI和游戏之间通信。

  我们使用三个按钮拼接出最基本的联机游戏主菜单:开始游戏,加入游戏以及退出游戏:

  并且在每个按钮的点击事件上绑定对应的逻辑。

  我们为打开关卡加上了?listen选项,这样关卡将以聆听服务器模式运行,其他玩家可以通过IP加入这个游戏。这里我们将输入文本框的TEXT先转换为FString再转换为FName,成为搜索关卡/主机IP的可匹配字符串。

  在主菜单需要编写的唯一的游戏性逻辑可能就是场景的切换了。在虚幻中,场景切换的逻辑一般由蓝图来控制(包括类似主菜单UI直接触发的加载关卡和大世界场景的level streaming),因为场景的加载一般不涉及太多的数学计算或者循环。

  对于游戏的屏幕适配,需要在项目设置的DPI缩放中调整。UI界面右上角的选项只是快速将画布调整至相应的设备大小以预览在对应设备分辨率下UI的外观。

  除了主界面的菜单,我们还需要游戏内呼出的菜单:

  为了绑定游戏内菜单,并且让这个菜单能够在单机游戏时暂停游戏,我们需要将其绑定到PlayerController上:

  SPlayerController.h

  .cpp

  使用SetInputMode(FInputModeGameOnly())和SetInputMode(FInputModeUIOnly());来快速切换鼠标指针的显示状态。

  绑定暂停菜单的逻辑。

  接下来,我们制作一些动态的UI控件。我们将在之前制作的血条上加上目标受到的Effect状态影响,并且用动态的材质作为显示的图像。

  我们希望这个图像能表示出Effect的剩余时间以及种类,所以我们先来为ActionEffect制作一个方便的,暴露给蓝图(因为最后使用的UI都是蓝图类)的接口来查询当前的剩余时间,并且声明一个表示当前效果的图标的引用:

  SAction.h

  技能开始/施放的时间被标记为可复制,这样我们就不需要多余的MultiCast函数来通知各个客户端技能的开始时间了。

  使用服务器的时间并且将它复制到所有的客户端上。

  SActionEffect.h

  .cpp

  因为我们显示的信息与游戏性有关,所以我们需要尽可能准确的时间。我们使用GetServerWorldTimeSeconds从GameStateBase来获得准确的服务器时间。

  接下来需要制作的是显示在UI上的材质,这个材质需要将技能的图标作为自己的基础颜色纹理。但是从UI找到正在施放的技能的引用并不是很方便,所以我们在技能组件中声明多播委托来通过事件的方式与UI交互。

  SActionComponent.h

  当技能被执行时,调用多播委托的广播来通知UI进行改变。

  能够从多播委托的擦描述中拿到正在执行的技能的指针后,我们就可以开始着手制作材质。首先我们要将材质的材质域设置为UI以在UI中使用。接下来我们拿到技能的图标,将他作为不透明度的蒙版,然后再通过拿到的技能的剩余时间和总时间算出填色的部分UV坐标。材质只根据Alpha来控制红色和白色的比例与游戏的交互被放在UI的Tick中,并且设置为仅客户端Tick以减少服务器开销

  由于我们可能身上同时存在多个效果,所以我们需要一个存放多个图标的容器,由它来接受技能执行的信息并且创建和销毁图标。

  为了预览它的效果,我们在这里放了三个标志占位,在它的构造函数中需要把这些占位标志清除。

  由于这个UI需要与血条绑定,那么他的初始化逻辑应该由血条组件来控制,所以我们在血条出现时为他初始化绑定的ActionComponent变量。

  在为效果制定好对应的图标后,我们就可以在血条上看见效果的状态和剩余时间了。同时我们为了看到自己的效果,在Character的BeginPlay函数中为自己也添加一个血条。

斯坦福CS193u-使用虚幻引擎和C++的游戏开发[7]-存档与主菜单/UI优化

斯坦福CS193u-使用虚幻引擎和C++的游戏开发[7]-存档与主菜单/UI优化

相关推荐

网友评论

  • (*)

最新评论