图片由Joy Way提供
《STRIDE》的开发商Joy Way分享其如何实现VR跨平台多人游玩
Joy Way多人游戏主管Artem Tarasov
2022年11月16日
Joy Way
STRIDE
VR
技术博客
游戏
大家好,我是
Joy Way
的多人游戏主管Artem Tarasov。
Joy Way位于塞浦路斯,是一家开发和发行VR游戏的公司。从大约六年前到现在,我们开发了多个项目,涵盖PC和一众独立的VR平台,包括SteamVR、Meta Quest、PlayStation VR和Pico等。
跨平台游玩是我们的动作跑酷VR游戏《STRIDE》的一大特色,对团队来说,这款游戏的开发(尤其是它的多人游戏模式)充满了挑战性。我们必须应对多个平台、各种输入设备、基于物理学的运动、正确的命中判定以及其他任务。
这篇博客文章将介绍我们在为
《STRIDE》
开发多人游戏模式时,用来解决部分挑战的方案。
观看视频
你可以使用这篇博客文章作为指导,利用虚幻引擎开发跨平台游戏,并将其连接至自定义后端服务。对于同时使用
C++
和
蓝图
的虚幻程序员来说,这篇博客文章也非常有用。
编写特定于平台的代码
开发跨平台游戏时,每位开发者最先面临的挑战就是必须编写特定于平台的代码。
第一种(也是最常见的)情况就是你需要使用特定于平台的插件和模块。在虚幻引擎中,你可以针对每个单独的模块,将平台添加到白名单或黑名单中:
而对于插件,你可以使用BlacklistPlatforms、SupportedTargetPlatforms和WhitelistPlatforms(选择一个或多个参数)执行同样的操作。
将平台列入白名单或黑名单之后,为了使用模块作为C++的依赖项,你需要在PublicDependencyModuleNames和/或PrivateDependencyModuleNames中包含模块名称。之后的事情就比较重要了。
首先,你需要排除在某些平台上不受支持的模块。要实现这一点,最直接的方法就是在模块的.Build.cs文件中使用if/else语句。
例如:
其次,你需要使用特定于平台的编译标记。例如:
你可以在
这里
阅读关于C++预处理器指令的更多信息。
《STRIDE》中的多人地图
处理运行同一操作系统的多个平台
开发《STRIDE》时,我们遇到的一个问题是,有些目标VR平台(Pico和Meta Quest)运行的都是Android系统。这导致了一些问题,出于这个原因,我们无法使用虚幻平台提供的配置将Pico和Quest重叠的配置值区分开来。默认情况下,在代码中,你无法确定当前处于哪个平台。我们采用了下面的解决方案。
通过在虚幻构建工具中注入-ini参数,即可解决配置值的问题。你可以在你的CI/CD构建脚本中实现这一点,并将-ini配置传递到虚幻自动化工具中,从而覆盖配置。请以这种格式传递:
你可以在
这里
阅读关于配置注入的更多信息。此外还有一种方法,可以通过修改虚幻自动化工具,在打包的游戏中应用被注入的配置值。请在
这里
阅读更多信息。
接下来,你可以向UAT传递一个配置值,以确定当前所处的平台。
下一步是为Pico和Quest设备添加平台定义。请注意目标规则中GlobalDefinitions和模块规则中PublicDefinitions之间的区别。默认情况下,UAT会生成两套构建:一套供编辑器使用(用于在构建期间运行命令行),另一套供目标平台使用。GlobalDefinitions是整个目标的定义。这意味着,如果你将Pico/Quest平台的定义放到GlobalDefinitions中,那么(运行命令行所必需的)编辑器构建也会拥有非必要的平台定义。考虑到上述情况,你应该将平台定义放到模块规则的PublicDefinitions中。以下是具体做法:
现在,你可以使用熟悉的预处理器指令,在C++代码中确定当前平台,并使用BlueprintFunctionLibrary将平台检查公开给蓝图。
将游戏连接到自定义的后端服务
在博客文章的这一部分,我将介绍我们用来整合后端服务的方法。我们的后端包含多个HTTP API和一个WebSocket API。HTTP请求和WebSocket事件实现起来相当简单,所以我想重点介绍我们用来链接异步调用的方法。
《STRIDE》中跨平台枢纽的位置
最初,我们使用lambda函数对响应的回调实现API调用。然而,我们很快就得到了一大堆嵌套的调用,代码变得难以维护。所以,我们决定让每个请求成为单独的
ULBlueprintAsyncActionBase
。
在蓝图中使用UBlueprintAsyncActionBase自动生成的节点非常简单。
这里
是编程指南。
然而,我们仍有一个重要的问题要解决:在哪里调用这些节点?在某些情况下,你可以让调用成为游戏内的实体,这是一种很好的安置方法。但GameInstance层面的调用呢?我们的解决方案是为Worker实体使用一个扩展的UObject。
Worker是UObject的派生类,用于控制我们使用GameInstanceSubsystems的生命周期。你可以在
这里
阅读关于编程子系统的更多信息。你不一定要使用GameInstanceSubsystems。而且,使用LocalPlayerSubsystems可能会是一个更好的解决方案。
Worker使调用链的维护变得非常简单明了。
现在,我将揭开UObject扩展的神秘面纱,分享一些不易发现的技巧。
首先,你只需要扩展一个东西:通过WorldContext调用全局函数的GetWorld。以下代码示例重写了Worker GetWorld。
请注意对CDO的第一次检查。这将进一步说明问题。
下面是创建Worker的代码示例。
总结一下上面的内容。你应该继承UObject,重写GetWorld,然后继承你的C++ Worker,最后,在Worker生命周期的核心位置实例化正确的对象。下面的代码展示了如何从蓝图中获取一个类,以实例化正确的对象。
现在说说CDO。如你所见,我们在GameInstanceSubsystem的构造中指定了TSubclassOf变量。在这里,如果你不检查CDO,可能会遇到问题。在没有检查CDO的情况下,我们遇到了编辑器崩溃以及与资产系统相关的严重问题,所以你那里也可能会出现同样的情况。
将每个请求分开,并将请求链公开到蓝图图表中,有助于我们摆脱“面条式代码”的困境,使得与后端相关的代码更易于维护,从而加快迭代速度,减少错误。
如果你有兴趣进一步了解我们的游戏,或是想看看VR游戏的幕后开发工作,请
在Twitter上关注Joy Way
,并加入我们的
Discord服务器
。
立即获取虚幻引擎!
获取全球最开放、最先进的创作工具。
虚幻引擎包罗万象,并提供完整的源代码访问权限,开箱即用,诚意十足。
立即开始