前段时间有在Unity中添加定位功能的需求,先后尝试了三种方法,在此记录一下。
Unity原生方法
Unity提供了LocationService,LocationInfo和LocationServiceStatus三个API来实现定位功能。
LocationService
类用于控制定位服务和获取定位数据,LocationService
类的实例可通过Input.location
获取。
LocationService提供了三个属性和两个方法:
- isEnabledByUser:设备是否开启了定位
- lastData:上一次定位的数据
- status:定位服务的状态
- Start:开始定位,可以通过参数指定定位精度和定位信息更新距离(即相对于上次位置移动了多少米后才更新定位数据)。如果定位精度设置较高,获取不到满足精度的数据,服务状态会一直处于
Initializing
- Stop:停止定位
LocationServiceStatus
枚举用于表明定位服务的状态,共有Initializing
, Running
, Stopped
, Failed
四种状态。当LocationService
的status为Running
时,才能通过lastData
获取到定位数据。
LocationInfo
结构存储了位置的经纬度、高度、时间戳和精度信息。需要注意Unity获取到的经纬度坐标系为WGS84坐标系,国内应用使用的坐标系一般为GCJ02坐标系,百度使用的是BD09坐标系。如果要与国内其他地理信息服务结合使用则要进行坐标系的转换。一开始我用Unity定位得到的经纬度数据去高德的坐标拾取器中查询位置,发现与真实位置有较大偏差,还以为是Unity获取的数据不准确,因此才又尝试了腾讯和高德的方法,后面才知道实际是因为Unity和高德使用的坐标系不同导致的。
使用示例
在场景中创建两个按钮和一个文本,新建一个测试脚本,给按钮绑定点击事件即可。
using System; using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; public class LocationTest : MonoBehaviour { public TextMeshProUGUI LocationInfo; public IEnumerator StartLocation() { // 检查设备是否开启定位 if (!Input.location.isEnabledByUser) { LocationInfo.text = "Location is not enabled by user"; yield break; } // 开始定位 Input.location.Start(); // 设置定位精度为5m // Input.location.Start(5); // 设置定位精度为5m,每移动20m更新一次位置 // Input.location.Start(5, 20); // 开始定位后并不会立即获取到定位数据,需等待定位服务初始化完成 int maxWait = 20; while (Input.location.status == LocationServiceStatus.Initializing && maxWait > 0) { yield return new WaitForSeconds(1); maxWait--; } if (maxWait <= 0) { LocationInfo.text = "Timed out"; yield break; } if (Input.location.status == LocationServiceStatus.Failed) { LocationInfo.text = "Unable to determine device location"; } else { // 获取定位数据 LocationInfo.text = $"{Input.location.lastData.timestamp}: {Input.location.lastData.longitude},{Input.location.lastData.latitude} {Input.location.lastData.altitude}"; } } /// <summary> /// 开始定位按钮点击事件 /// </summary> public void StartLocationButton() { StartCoroutine(StartLocation()); } /// <summary> /// 停止定位按钮点击事件 /// </summary> public void StopLocation() { Input.location.Stop(); LocationInfo.text = "Location service stopped"; } }
腾讯定位SDK
国内三家主要的地图厂商中只有腾讯提供了Unity定位SDK 。首先前往控制台申请Key。创建完应用后,添加一个Key。
下载SDK后,根据发布平台将DLL4Android
或DLL4Ios
中的TencentLocationSDK.dll
文件放到工程的Assets/Plugins目录下,并将TencentLocationSDK.unitypackage导入到工程中。可根据需要选择平台和架构。
下面先解决发布时会遇到的一些错误。
- x86_64架构的库文件配置有误,发布时会报错,需要将CPU选项从
ARMv7
改为X86_64
- 删除Plugins/Android目录下的res文件夹
- 在AndroidManifest.xml的Activity标签中添加
android:exported="true"
<activity android:name="com.tencent.tencentlocation.MainActivity" android:configChanges="orientation|keyboardHidden|screenSize" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
还是简单创建一个有文本和两个按钮的场景,将dll文件中的TencentLocationService脚本挂载到一个物体上,并填写自己的API Key。
在TencentLocationService脚本挂载的物体上新建一个脚本,内容如下。腾讯定位SDK基本与Unity原生的方法类似,可以通过两种方式获取定位数据,第一种是像原生方法一样直接访问tencentLocationService.lastData
,第二种是通过OnLocationUpdate回调获取。下面使用了第二种回调的方法,只要在挂载了TencentLocationService.cs
的物体上的任意脚本中实现ILocationListener
接口就可以从OnLocationUpdate
回调中实时获取到定位信息。
//与TencentLocationService挂载在同一物体上的脚本实现ILocationListener接口后可以通过回调获取定位结果 public class LocationTest : MonoBehaviour, ILocationListener { public TextMeshProUGUI infoText; public TencentLocationService locationService; void Awake() { // 同意隐私协议 TencentLocationService.SetAgreePrivacy(true); } /// <summary> /// 开始定位 /// </summary> public void StartLocation() { if(locationService.status == TencentLocationServiceStatus.Ready || locationService.status == TencentLocationServiceStatus.Stopped) { locationService.Start(); } } void Update() { switch (locationService.status) { case TencentLocationServiceStatus.Initializing: infoText.text = "Location Service Initializing"; break; case TencentLocationServiceStatus.Ready: infoText.text = "Location Service Ready"; break; case TencentLocationServiceStatus.Failed: infoText.text = $"Location Service Failed. ErrorCode: {locationService.errorCode}"; break; } } /// <summary> /// 停止定位 /// </summary> public void StopLocation() { locationService.Stop(); infoText.text = "Location Service Stopped"; } /// <summary> /// 定位信息更新回调 /// </summary> /// <param name="locInfo">定位信息</param> public void OnLocationUpdate(string locInfo) { // 解析定位信息 TencentLocationInfo info = Util.ParseLocationInfo(locInfo); infoText.text = $"{info.timestamp}: {info.latitude}, {info.longitude}, {info.altitude}"; } }
因为项目中要用到另外一个插件,一直搞不定腾讯定位和另一个插件的AndroidManifest.xml文件合并的问题,所以又尝试了使用高德的定位SDK。
高德定位SDK
高德并没有直接提供Unity上的SDK,所以只能在Unity中调用jar包来实现功能,相较于前两种方式复杂一些。
首先到控制台申请API Key,SHA1码的获取可参考常见问题,包名需要和Unity中的配置一致。Unity中包名在Project Settings-Player设置。
下载SDK,解压后将jar包放到Assets\Plugins\Android目录下,然后在Player Setting-Publish Settings中开启Custom Main Manifest。
这会在Assets\Plugins\Android目录中生成AndroidManifest.xml
文件,参考下面代码修改清单文件。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.unity3d.player" xmlns:tools="http://schemas.android.com/tools"> <!--配置权限--> <!--用于进行网络定位--> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"></uses-permission> <!--用于访问GPS定位--> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"></uses-permission> <!--用于获取运营商信息,用于支持提供运营商信息相关的接口--> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"></uses-permission> <!--用于访问wifi网络信息,wifi信息会用于进行网络定位--> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"></uses-permission> <!--用于获取wifi的获取权限,wifi信息会用来进行网络定位--> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"></uses-permission> <!--用于访问网络,网络定位需要上网--> <uses-permission android:name="android.permission.INTERNET"></uses-permission> <!--用于写入缓存数据到扩展存储卡--> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission> <!--用于申请调用A-GPS模块--> <uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS"></uses-permission> <!--如果设置了target >= 28 如果需要启动后台定位则必须声明这个权限--> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <!--如果您的应用需要后台定位权限,且有可能运行在Android Q设备上,并且设置了target>28,必须增加这个权限声明--> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/> <application> <activity android:name="com.unity3d.player.UnityPlayerActivity" android:theme="@style/UnityThemeSelector"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <meta-data android:name="unityplayer.UnityActivity" android:value="true" /> <!--设置API Key--> <meta-data android:name="com.amap.api.v2.apikey" android:value="你的Key"/> </activity> <!--声明定位服务--> <service android:name="com.amap.api.location.APSService"></service> </application> </manifest>
依然是两个按钮一个文本的布局,创建如下两个脚本。代码中用到了AndroidJavaClass, AndroidJavaObject和AndroidJavaProxy来与高德定位jar包代码进行交互。通过AndroidJavaProxy
用C#实现了Java接口,在C#脚本中处理定位回调的逻辑。下面只是最简单的示例,可以参考官方文档对定位进一步配置。
using System; using TMPro; using UnityEngine; public class LocationTest : MonoBehaviour { AndroidJavaClass unityPlayerClass; AndroidJavaObject currentAcitivity; AndroidJavaObject locationClient; AndroidJavaClass locationClientClass; AndroidJavaObject locationOption; LocationListener locationListener; public TextMeshProUGUI InfoText; public void StartLocation() { // 通过UnityPlayer类获取当前Activity unityPlayerClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); currentAcitivity = unityPlayerClass.GetStatic<AndroidJavaObject>("currentActivity"); // 同意授权政策和隐私协议 locationClientClass = new AndroidJavaClass("com.amap.api.location.AMapLocationClient"); locationClientClass.CallStatic("updatePrivacyAgree", currentAcitivity, true); locationClientClass.CallStatic("updatePrivacyShow", currentAcitivity, true, true); // 创建定位客户端 locationClient = new AndroidJavaObject("com.amap.api.location.AMapLocationClient", currentAcitivity); locationOption = new AndroidJavaObject("com.amap.api.location.AMapLocationClientOption"); // 此处可以对定位参数进行设置,参考 https://lbs.amap.com/api/android-location-sdk/guide/android-location/getlocation#configure locationClient.Call("setLocationOption", locationOption); locationListener = new LocationListener(); // 注册定位监听 locationListener.OnLocationChangedEvent += OnLocationChanged; // 设置定位监听 locationClient.Call("setLocationListener", locationListener); locationClient.Call("startLocation"); } public void StopLocation() { locationClient?.Call("stopLocation"); locationClient?.Call("onDestroy"); InfoText.text = "Location Stopped"; } /// <summary> /// 定位回调 /// </summary> /// <param name="location">定位信息,参考<a href="https://amappc.cn-hangzhou.oss-pub.aliyun-inc.com/lbs/static/unzip/Android_Location_Doc/index.html">官方文档</a></param> /// public void OnLocationChanged(AndroidJavaObject location) { if(location != null) { // 成功获取定位信息 if (location.Call<int>("getErrorCode") == 0) { try { InfoText.text = $"{location.Call<long>("getTime")}: {location.Call<double>("getLongitude")}, {location.Call<double>("getLatitude")}"; } catch (Exception e) { Debug.LogError(e); } } else { InfoText.text = $"Location Error:{location.Call<int>("getErrorCode")} {location.Call<string>("getErrorInfo")}"; } } } }
using UnityEngine; /// <summary> /// 实现高德的AMapLocationListener接口,可在Unity脚本中实现回调的具体逻辑 /// </summary> public class LocationListener : AndroidJavaProxy { public delegate void LocationChangedDelegate(AndroidJavaObject location); public event LocationChangedDelegate OnLocationChangedEvent; public LocationListener() : base("com.amap.api.location.AMapLocationListener") { } public void onLocationChanged(AndroidJavaObject location) { OnLocationChangedEvent?.Invoke(location); } }
高德定位获取到的信息相比于前两种方法要更丰富些,除了经纬度还可以获取到城市、地址、方位角等信息。
高德方案参考:Unity接入高德定位sdk简单三步无需与安卓工程交互_高德地图 unity sdk-CSDN博客
总结
- Unity原生定位:经纬度信息为WGS84坐标系,和国内其他服务一起使用需要转换坐标系
- 腾讯定位:经纬度信息为GCJ02坐标系,和其他插件同时使用可能会有冲突
- 高德定位:经纬度信息为GCJ02坐标系,定位信息丰富,涉及到Unity和Java代码的交互,相对复杂些
项目源码:UnityLocationDemo, TencentLocationDemo, AmapLocationDemo
文章评论