天气API推荐:精准获取气象数据的首选
使用虚幻引擎提供的谷歌地图API等Web API(仅限蓝图)
随着Cesium for Unreal推出来自谷歌地图平台的3D瓦片(3D Tiles),所有人都将可以访问来自世界各地覆盖区域内的摄影测量资产。这就非常神奇,因为现在你可以通过虚幻引擎提供的真实感,在全球各地驾车驰骋和自由飞翔。但是数字孪生不仅涉及3D多边形,它还需要根据请求提供语义数据。因此,我们想尝试利用谷歌提供的其他API,在这种3D可视化基础上添加更多服务。
介绍
概念形成随着Cesium for Unreal推出来自谷歌地图平台的3D瓦片,所有人都将可以访问来自世界各地覆盖区域内的摄影测量资产。这就非常神奇,因为现在你可以通过虚幻引擎提供的真实感,在全球各地驾车驰骋和自由飞翔。但是数字孪生不仅仅涉及3D多边形,它还需要根据请求提供语义数据。因此,我们想尝试利用谷歌提供的其他API,在这种3D可视化基础上添加更多服务。本教程涉及多个基本概念,但不在此详细阐释。蓝图、游戏对象、地理参考、UMG控件。
在阅读下文前,请先了解这些概念。本文并非逐步再现概念验证流程的详细指南,但会介绍在虚幻引擎中处理异步设计的一些最佳实践。在本教程中,我们不会粘贴任何蓝图片段以呈现更好的视觉效果,但是BP_PlacesAPI_Helper蓝图将在本教程最后“按原样”提供。这意味着它“不受支持”,只是为了快速验证相关概念,生产就绪版本需要更多的验证步骤。
什么是Web API?
Web API是一个可扩展的框架,用于构建基于HTTP的服务,这些服务可以在不同平台(例如Web端、Windows、移动端等)上的不同应用程序中访问。
遵循REST架构约束的Web服务API被称为RESTful API。基于HTTP的RESTful API由以下几个方面定义:
被用作起点(有时叫做端点或入点)的一个或多个资源的URI
所有可能的资源表示的编码。这意味着服务器会使用资源的表示进行响应(现在通常是HTML、XML或JSON文档)。
可能的状态转换以及它们可能发生的位置
REST API来源:https://en.wikipedia.org/wiki/Representational_state_transfer
总的说来,要使用REST API,就需要向服务器发出HTTP请求,服务器会按已知的格式给我们返回答案,比如JSON或XML。
虚幻引擎拥有两个非常实用的插件:
Http Blueprint – 向服务器发送HTTP。
JSON Blueprint Utilities – 解析和解码JSON对象。
这些插件在C++和蓝图中公开了它们的功能,不用编写代码就可以查询这些API!
初始设置
设置谷歌云(Google Cloud)服务
要使用REST API,通常需要个人API密钥(Personal API Key)。
这个密钥会识别请求的来源,以便在出现与服务相关的费用时,根据你的使用情况收取费用。
使用这些服务必须有谷歌地图API密钥。参阅此处了解关于密钥创建的更多信息。
对本实验而言,我们在项目密钥(Project Key)中添加了以下API:
- 地图瓦片API(Map Tile API) – 使用Cesium for Unreal插件加载3D瓦片内容
- 地图地点API(Maps Places API) – 查询地点信息
- 地图海拔API(Maps Elevation API) – 了解某一位置的海拔数据
发送请求时要小心,因为有可能产生相关成本。请避免在循环中发送请求,因为这样有可能超过每秒查询数的限制,还有可能造成成本浪费。为此,设置API密钥限制和使用警报非常重要。
创建虚幻项目
新建一个虚幻引擎5.1项目,在模拟类别(Simulation Category)中选择空白项目模板,仅使用蓝图。
打开编辑器之后,启用以下插件
- Cesium for Unreal(可从虚幻商城获取)。
- Http Blueprint(它会自动向Json Blueprint Utilities添加一个依赖项)
在模拟模板中启用SunSky和GeoReferencing插件。
然后,你要将Photorealistic 3D Tiles添加到关卡中。
Cesium在本教程中记录了该过程。遵循其中描述的步骤,但是有以下例外情形:
- 不需要使用Cesium Sunky,模拟模板已经设置好虚幻引擎SunSky。
- 不要使用Cesium动态Pawn(Cesium Dynamic Pawn)。我们使用GeoReferencing插件提供的BP_RoundPlanetPawn。
确保为以下内容设置相同的地理参考点:
- Cesium地理参考(Cesium Georeference)
- 虚幻引擎地理参考系统(UE Georeferencing System)
- SunSky
这一步是必须要做的,因为三个插件各自独立运行,我们不想在它们之间创建依赖关系。
确保所有地理参考对象都同步
玩家出生点在巴黎凯旋门附近
下面我们就来使用API
创建需要的资产
设置基本游戏对象
确保:
- 关卡中有一个玩家出生点
- 有可以使用的自定义游戏模式设置
- 你自己的玩家控制器(我们使用修改过的BP_SimPlayerController副本)
- Georeferencing插件中有可用的BP_RoundPlanetPawn在项目设置中设置以下游戏模式和初始地图。
设计顾虑
在这个演示项目中,我们实现了3个主要功能
- 自动完成位置搜索并飞到该位置
- 查询附近的兴趣点(餐馆、酒吧),生成地理标记(GeoMarker)
- 点击地理标记获取详细信息。
3个主要功能
为了实现这些功能,我们创建了以下对象:
- BP_PlaceAPI_Helper:负责调用谷歌地图API的蓝图Actor
- BP_Place:存储地点(位置)属性的纯蓝图数据对象
- UMG_GeoQueries:主应用程序UMG控件
- BP_GeoMarker:作为地点视觉表示的蓝图Actor
- BP_MarkerManager:用于管理地理标记的蓝图Actor
使用REST API要注意的主要问题是,发送请求是一个非阻塞调用。调用者将在之后收到请求结果,我们不希望在等待请求结果的过程中阻塞程序。因此,我们将使用大量的异步编程模式,利用事件分发器。
自定义事件VS函数
在使用Http发布请求节点(Http Post Request Node)发布请求时,你可以在右上角看到一个小时钟。这意味着该节点将执行异步调用。事件图表将在这里暂停执行(除非在请求处理(Request Processing)引脚连接其他内容)。收到结果后,它将通过“请求成功(Request Was Successful)”或“请求不成功(Request Was Not Successful)”执行引脚恢复执行。
发布请求是一个异步调用!
这会带来了一个重要的设计约束。函数中不能包含任何带异步调用的蓝图节点。只能在事件图表中使用。因此,你必须在一个事件图表中、在一个自定义事件中发送请求。
但是如果在事件图表中实现所有内容,整个图表很快就会变得非常混乱。因此,我们仍然使用函数将同步操作(例如构建请求和处理应答)组合在一起,然后将这些调用堆叠在自定义事件下。
谁知道谁?在对象之间创建引用的正确方法!
另一种常见的重大设计缺陷在于用户界面和底层对象的绑定方式。
在应用程序编程中,我们有不同的设计模式,包括文档/视图、模型-视图-控制器(MVC)、模型-视图-视图模型(MVVM)。
无需输入太多细节,表示层(UI)知道它所显示的对象,但对象本身必须对表示层不可知。
从数据对象(如BP_PlacesAPI_Helper对象)调用控件更新是非法的!
建议让BP_PlacesAPI_Helper在数据可用或发生更改时触发事件。因此,我们要创建“事件分发器”,其他应用程序对象,比如UMG控件或玩家控制器将订阅事件分发器!
所有可能的请求都将遵循类似下面的模式:
请求的剖析
注意,搜索附近地点功能有一个小技巧:为了限制服务器的负载,请求只返回前20个结果。
要想获取更多结果,需要重新发起相同的查询,但这次有一个特殊选项,就是和第一个查询结果一起收到的token。这样就可以得到后续的20个地点,以及再往后的20个地点,最多60个地点。
为了处理这种特殊情况,我们有两个针对附近地点的事件,一个用于获取前20个地点,另一个用于获取后续的20个地点。
聚焦BP_PlacesAPI_Helper Actor
根据上面阐述的设计,BP_PlacesAPI_Helper将包含下列成员:
- 在事件图表中:主入口点函数
- 自动完成地点寻找(Find Place Autocomplete)
- In:部分地址,Out:预测的BP_Places数组
- 完成时,事件分发器“PredictedPlacesAvailable”
- 查询地点详情(Query Place Details)
- In:BP_Place, GetAltitude(可选)。Out:更新输入BP_Place
- 完成时,事件分发器“Place Details Received”
- 搜寻附近地点(Search Nearby Places)
- In:类型字符串、半径,Out:附近BP_Places数组
- 每当收到一批地点时,事件分发器“Nearby Places Received”
- 作为函数
- 格式化请求URL的函数
- 处理JSON结果的函数
- 效用函数
- 净化URL(用%20替换空格,用%2C替换逗号)
- 检查JSON响应中的错误
BP_PlacesAPI_Helper函数
BP_PlacesAPI_Helper变量和事件分发器
格式化请求
API文档中有关于格式化请求的解释。
例如,从地点自动完成API(Places Autocomplete API)文档中,我们了解到URL必须按这种方式格式化,可能的其他输入参数要用“¶m=value”隔开。
URL格式
URL示例
大多数时候,我们只是附加字符串,最后是我们的API密钥。
查找海拔请求
发送请求
Post Request需要一个URL和说明这是一个JSON请求的请求头。
如果需要添加其他可选参数,可以提供一个可选的请求体。
注意,我们为实际请求发送统计添加了计数器,用于监控实际发送的请求数量。
发送请求(注意异步调用)
处理结果
处理结果是整个过程中最枯燥的部分。
你将收到一个采用通用编码方式编码的JSON对象。
你可以请求一个字段值,你将得到:
- 一个带值的字符串,如果是“叶节点(Leaf Node)”。
- 又一个JSON对象,如果是“子节点(Child Node)”。
- 其他JSON对象的数组,在循环中处理。
所有这些情况都由“获取字段(Get Field)”蓝图节点处理。
你要知道结果的格式,以便处理结果。好在它会被记录下来。
专业意见:在尝试迭代解析之前,最好先在浏览器中输入请求URL来运行查询。如果JSON对象以字符串形式返回,会难以读取。
但是有些在线JSON查看器,比如这个,你可以将这个字符串粘贴到这个查看器中,它会创建一个更容易读取的节点层级!
原始JSON响应字符串
在线查看器中显示的相同JSON字符串
这种表示方式更便于在层级结构中导航,也更容易了解获取字段节点是需要返回一个字符串,还是另一个JSON对象或一个JSON对象数组。
在创建获取字段节点时,数值(Value)引脚默认显示灰色。它会根据你连接的引脚发生改变。因此,在连接引脚之前,必须先创建包含目标引脚的节点。
始终将数值引脚从目标绑定到源,以获得正确的类型!
由于JSON的结构一目了然,并且知道获取字段的规则,解码响应就变得非常简单,比如下面的“查找地点自动完成(Find Place Autocomplete)”。
注意,在生产中,建议在处理过程中采用更好的错误处理流程。
JSON文件处理示例
处理过程的最后一步是将结果通知给感兴趣的对象。
我们事先不知道谁会对结果感兴趣。可能是显示地点细节的UI,然后由玩家控制器决定是否让Pawn飞到这个位置。
为此,我们添加了一个事件分发器,其中可能包含将随事件传递的输入,订阅该事件的其他对象将接收该输入。
在BP_PlacesAPI_Helper中,我们在处理完结果之后触发事件分发器
在UMG控件中,我们订阅了这个事件,并相应地重新构建了建议列表。
用户界面
除了ListView系统之外,UMG控件并没有什么特别之处。
在UMG中,使用控件填充垂直框是一个非常常见的操作。但是根据元素的数量,具体过程可能比较复杂或者比较缓慢。ListView是一个虚拟视图,可以帮助我们处理列表中的大量对象,具有很大的灵活性。
- 为列表的每一行创建一个特殊控件。
- ListView将专门用于这个控件。
- 在向列表中添加元素时,该元素会被传输到行控件用于显示。
为了整理这个列表,我们需要:
- 创建一个UMG用户控件(UMG User Widget)作为行控件。(这里是UMG_PlaceWidgetEntry)
- 在本POC中只是个简单的文本框,但它可以包含一个图标。
- 在“图表(Graph)”面板中,点击“类设置(Class Settings)”,在细节面板中找到“接口(Interfaces)”类别,将“用户对象列表条目(User Object List Entry)”添加到已实现接口列表中。
- 右键单击“列表项目对象集上(OnListItemObjectSet)”,选择实现事件。
- 在蓝图图表中,将这个对象转换为需要显示的实际类型,然后用它的属性来更新UI。
行控件(Row Widget)需要实现“用户对象列表条目”接口。
需要实现“列表项目对象集上”事件。
然后,在主控件上添加一个列表视图控件(ListView Widget),将条目控件类(Entry Widget Class)设为刚才创建的类。每当向列表中添加一个项目,就会创建相应的行控件,显示底层对象。
将点击项目(Item Clicked)事件绑定到一个自定义事件处理器上,这样处理就结束了。
定义与每一行相关的控件类(Widget Class),绑定点击项目事件。
事件处理器负责填充列表,对点击做出响应。
地理标记及其管理器
地理标记蓝图由一个简单的缩放立方体、一个用于显示大头钉的自发光材质和一个2D文本控件构成。
大头钉稍微有点特殊,我们将根据距离对它进行缩放,以呈现单像素线条的效果。
在我们的例子中,如果使用3D瓦片,在下载瓦片时,几何体将随着时间的推移不断改进。为了确保始终正确对齐,2s定时器会将控件重新放在地平面上。
按距离缩放大头钉网格体
限制在地平面上(使用海拔高度偏移)
确保控件组件(Widget Component)使用你的自定义控件,
并在“屏幕”模式下按“期望的大小”呈现。
为了实现交互性,2D控件包含一个带鼠标键按下绑定的换行边界,
它会发送一个“Clicked”事件分发器。(可以是一个透明的按钮)
在处理多个这样的对象时,一种明智的做法是创建一个专门的对象来管理这些对象。在这种情况下,标记管理器(Marker Manager)非常简单,它只处理一组标记类型,但可以发展到处理不同的标记层,带可见性开关……
创建一个BP_MarkerManager Actor,一个生成地理标记(SpawnGeoMarkers)事件。在这个事件中,我们要:
- 移除所有现有标记,
- 获取样式信息(见下文)
- 计算变换(向上向量不是Z+,我们在圆形的地球上)
- 生成标记
- 设置属性
- 绑定事件“Clicked”,该事件将发送标记与BP_Place
删除标记时,一定要取消绑定事件“Clicked”!
生成标记1/2
生成标记2/2
样式
在启动这样的应用程序时,我们事先不知道会给标记使用哪种颜色(Colors)或图标(Icons)。随着时间的推移,我们可能还会添加其他类型的标记。如果开始对这些值进行硬编码,之后的编辑过程将会十分痛苦。
要解决这个问题,数据表是一个非常好的选择。
- 利用颜色和图标属性创建一个简单的S_MarkerStyle。
- 基于这一结构创建数据表,并为你希望提供支持的标记类型添加行。
- 将这个数据表设为BP_MarkerManager的属性(Property)。
- 在生成标记时,管理器将使用这些数据来选择样式。通过这种方式,你可以逐渐改进自己的样式,甚至为最终用户提供不同的自定义样式!
基于样式内容构建数据表
玩家控制器处理高级逻辑
现在,MarkerManager和PlaceAPI Helper可以在事情发生时发送事件,接下来我们要处理逻辑。
这是在玩家控制器中完成的,它会生成UI并获取对MarkerManager和PlaceAPI Helper的引用。
绑定地点相关事件
绑定标记相关事件
总结
通过Http蓝图和JSON蓝图实用程序,在虚幻引擎中使用Web API变得越来越容易。
借助这些模块,所有人都可以使用这些API,不需要编写代码。
由于请求的异步性,采用事件驱动型设计非常重要,但是通过事件分发器和将回调绑定到这些事件上,一切就简单多了。
这个简短的概念验证过程是在一天内完成的,还有很多地方需要改进。但我们的目标是说明查询外部服务其实非常容易,而这也是数字孪生应用程序的关键一环!
希望大家喜欢!
文章转自微信公众号@虚幻引擎