Compare commits
3 Commits
VerticalBi
...
c8d8b6b802
| Author | SHA1 | Date | |
|---|---|---|---|
| c8d8b6b802 | |||
| f800e78f14 | |||
| e29581cc21 |
@@ -1,4 +1,4 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using GeoSus.Client;
|
using GeoSus.Client;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
@@ -16,6 +16,292 @@ namespace Subsystems
|
|||||||
Running,
|
Running,
|
||||||
Failed
|
Failed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Position source backend. Selectable at runtime via the GPS overlay
|
||||||
|
/// "Source" button so the user can recover when one path misbehaves on
|
||||||
|
/// their phone:
|
||||||
|
/// Auto - JNI: subscribe to gps + network, pick most recent fix.
|
||||||
|
/// GpsOnly - JNI: subscribe to gps only (network's frequent indoor
|
||||||
|
/// fixes don't drown out the slower-but-precise gps fix).
|
||||||
|
/// NetworkOnly - JNI: subscribe to network only (cell tower / WiFi).
|
||||||
|
/// Useful indoors when no satellite lock is possible.
|
||||||
|
/// UnityInput - Unity's Input.location wrapper. Verified to hang on
|
||||||
|
/// Mi 9T / A20e (which is why JNI exists), but works on
|
||||||
|
/// newer Android where the JNI streaming-callbacks path
|
||||||
|
/// silently doesn't fire (MIUI/HyperOS battery saver,
|
||||||
|
/// approximate-vs-precise permission split, minDistance
|
||||||
|
/// gating on stationary phones).
|
||||||
|
/// EditorWasd - WASD-driven simulated position. Available regardless
|
||||||
|
/// of testMode flag so desktop builds and editor sessions
|
||||||
|
/// can navigate the map without real GPS.
|
||||||
|
/// </summary>
|
||||||
|
public enum PositionSource
|
||||||
|
{
|
||||||
|
Auto,
|
||||||
|
GpsOnly,
|
||||||
|
NetworkOnly,
|
||||||
|
UnityInput,
|
||||||
|
EditorWasd,
|
||||||
|
}
|
||||||
|
|
||||||
|
#if UNITY_ANDROID && !UNITY_EDITOR
|
||||||
|
/// <summary>
|
||||||
|
/// Bridges android.location.LocationListener to managed code. The method
|
||||||
|
/// names here must match Java's LocationListener interface exactly so
|
||||||
|
/// AndroidJavaProxy's reflection dispatcher can find them.
|
||||||
|
/// </summary>
|
||||||
|
internal class AndroidLocationProxy : AndroidJavaProxy
|
||||||
|
{
|
||||||
|
public AndroidLocationProvider Owner { get; set; }
|
||||||
|
public AndroidLocationProxy() : base("android.location.LocationListener") { }
|
||||||
|
|
||||||
|
// Called by Android each time a new fix arrives from the registered provider.
|
||||||
|
public void onLocationChanged(AndroidJavaObject location)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (location == null) return;
|
||||||
|
double lat = location.Call<double>("getLatitude");
|
||||||
|
double lon = location.Call<double>("getLongitude");
|
||||||
|
long t = location.Call<long>("getTime");
|
||||||
|
string provider = "";
|
||||||
|
try { provider = location.Call<string>("getProvider"); } catch { }
|
||||||
|
// Streaming callbacks are LIVE (never cached). The cached path
|
||||||
|
// calls UpdateLocation directly with isCached=true.
|
||||||
|
Owner?.UpdateLocation(lat, lon, t, provider, isCached: false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[GPS-JNI] onLocationChanged failed: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required by the LocationListener interface even if we don't use them.
|
||||||
|
// Missing methods cause java.lang.AbstractMethodError at runtime.
|
||||||
|
public void onStatusChanged(string provider, int status, AndroidJavaObject extras) { }
|
||||||
|
public void onProviderEnabled(string provider) { }
|
||||||
|
public void onProviderDisabled(string provider) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Direct wrapper around android.location.LocationManager via JNI, used as
|
||||||
|
/// a replacement for Unity's Input.location on Android when the user picks
|
||||||
|
/// Auto/GpsOnly/NetworkOnly. Subscribed providers are configurable so the
|
||||||
|
/// position-source picker can rewire live without restart.
|
||||||
|
/// </summary>
|
||||||
|
internal class AndroidLocationProvider
|
||||||
|
{
|
||||||
|
private AndroidJavaObject _activity;
|
||||||
|
private AndroidJavaObject _locationManager;
|
||||||
|
private AndroidLocationProxy _gpsListener;
|
||||||
|
private AndroidLocationProxy _networkListener;
|
||||||
|
private double _lat, _lon;
|
||||||
|
private long _lastTimeMillis;
|
||||||
|
private long _lastLiveTimeMillis; // Time of most recent NON-cached fix.
|
||||||
|
private bool _hasFix;
|
||||||
|
private bool _hasLiveFix; // True once any streaming callback fired.
|
||||||
|
private string _activeProvider = "";
|
||||||
|
|
||||||
|
// Captured at Initialize() so the diagnostic can report
|
||||||
|
// "GPS provider DISABLED, only network enabled" etc.
|
||||||
|
private bool _gpsProviderEnabled;
|
||||||
|
private bool _networkProviderEnabled;
|
||||||
|
private bool _gpsLastKnownExists;
|
||||||
|
private bool _networkLastKnownExists;
|
||||||
|
private string _enabledProvidersList = "";
|
||||||
|
|
||||||
|
// Subscription scope - set in Initialize, used in Shutdown to know
|
||||||
|
// which listeners we registered.
|
||||||
|
private bool _subscribedGps;
|
||||||
|
private bool _subscribedNetwork;
|
||||||
|
|
||||||
|
public bool HasFix => _hasFix;
|
||||||
|
public bool HasLiveFix => _hasLiveFix;
|
||||||
|
public long LastLiveTimeMillis => _lastLiveTimeMillis;
|
||||||
|
public long LastTimeMillis => _lastTimeMillis;
|
||||||
|
public double Lat => _lat;
|
||||||
|
public double Lon => _lon;
|
||||||
|
public string ActiveProvider => _activeProvider;
|
||||||
|
public bool GpsProviderEnabled => _gpsProviderEnabled;
|
||||||
|
public bool NetworkProviderEnabled => _networkProviderEnabled;
|
||||||
|
public bool GpsLastKnownExists => _gpsLastKnownExists;
|
||||||
|
public bool NetworkLastKnownExists => _networkLastKnownExists;
|
||||||
|
public string EnabledProvidersList => _enabledProvidersList;
|
||||||
|
public bool SubscribedGps => _subscribedGps;
|
||||||
|
public bool SubscribedNetwork => _subscribedNetwork;
|
||||||
|
|
||||||
|
public bool Initialize(out string error, bool useGps, bool useNetwork)
|
||||||
|
{
|
||||||
|
error = "";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
|
||||||
|
{
|
||||||
|
_activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
|
||||||
|
}
|
||||||
|
if (_activity == null) { error = "no current activity"; return false; }
|
||||||
|
|
||||||
|
_locationManager = _activity.Call<AndroidJavaObject>("getSystemService", "location");
|
||||||
|
if (_locationManager == null) { error = "getSystemService(\"location\") returned null"; return false; }
|
||||||
|
|
||||||
|
// Capture provider enable state up front so the diagnostic
|
||||||
|
// can distinguish "provider disabled at OS level" from
|
||||||
|
// "provider enabled but produced no fix yet".
|
||||||
|
_gpsProviderEnabled = SafeIsProviderEnabled("gps");
|
||||||
|
_networkProviderEnabled = SafeIsProviderEnabled("network");
|
||||||
|
_enabledProvidersList = SafeGetEnabledProviders();
|
||||||
|
|
||||||
|
Debug.Log($"[GPS-JNI] init useGps={useGps} useNetwork={useNetwork} gps enabled={_gpsProviderEnabled} network enabled={_networkProviderEnabled} all enabled=[{_enabledProvidersList}]");
|
||||||
|
|
||||||
|
// Try cached last-known fixes from the providers we're about
|
||||||
|
// to subscribe to. If the OS already knows where we are
|
||||||
|
// (e.g. from another app that recently used GPS), we get a
|
||||||
|
// fix at zero cost and zero wait time. Tagged isCached so
|
||||||
|
// the diagnostic can mark them and we know we still need
|
||||||
|
// to wait for a streaming callback.
|
||||||
|
if (useNetwork) TryLastKnown("network", out _networkLastKnownExists);
|
||||||
|
if (useGps) TryLastKnown("gps", out _gpsLastKnownExists);
|
||||||
|
|
||||||
|
_subscribedGps = useGps;
|
||||||
|
_subscribedNetwork = useNetwork;
|
||||||
|
|
||||||
|
if (useGps) _gpsListener = new AndroidLocationProxy { Owner = this };
|
||||||
|
if (useNetwork) _networkListener = new AndroidLocationProxy { Owner = this };
|
||||||
|
|
||||||
|
// requestLocationUpdates must be called on a thread with a
|
||||||
|
// Looper. Use the Activity's UI thread, which always has one.
|
||||||
|
// minTime=1000ms, minDistance=0f - we want updates on every
|
||||||
|
// fix the OS produces. Previously this was 1f which gated
|
||||||
|
// out updates from a stationary phone (MIUI/newer Android
|
||||||
|
// are stricter about this and that's the suspected cause of
|
||||||
|
// "via gps (cached)" sticking forever).
|
||||||
|
_activity.Call("runOnUiThread", new AndroidJavaRunnable(() =>
|
||||||
|
{
|
||||||
|
if (useGps)
|
||||||
|
{
|
||||||
|
try { _locationManager.Call("requestLocationUpdates", "gps", 1000L, 0f, _gpsListener); }
|
||||||
|
catch (Exception ex) { Debug.LogWarning("[GPS-JNI] gps subscribe failed: " + ex.Message); }
|
||||||
|
}
|
||||||
|
if (useNetwork)
|
||||||
|
{
|
||||||
|
try { _locationManager.Call("requestLocationUpdates", "network", 1000L, 0f, _networkListener); }
|
||||||
|
catch (Exception ex) { Debug.LogWarning("[GPS-JNI] network subscribe failed: " + ex.Message); }
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
error = "JNI init exception: " + ex.Message;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TryLastKnown(string provider, out bool nonNullReturned)
|
||||||
|
{
|
||||||
|
nonNullReturned = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var loc = _locationManager.Call<AndroidJavaObject>("getLastKnownLocation", provider);
|
||||||
|
if (loc != null)
|
||||||
|
{
|
||||||
|
nonNullReturned = true;
|
||||||
|
double lat = loc.Call<double>("getLatitude");
|
||||||
|
double lon = loc.Call<double>("getLongitude");
|
||||||
|
long t = loc.Call<long>("getTime");
|
||||||
|
UpdateLocation(lat, lon, t, provider, isCached: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[GPS-JNI] getLastKnownLocation({provider}) failed: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SafeIsProviderEnabled(string provider)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _locationManager.Call<bool>("isProviderEnabled", provider);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[GPS-JNI] isProviderEnabled({provider}) failed: " + ex.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a comma-separated list of currently-enabled providers via
|
||||||
|
// LocationManager.getProviders(true). We iterate the returned
|
||||||
|
// java.util.List by index because AndroidJavaObject does not
|
||||||
|
// implement IEnumerable.
|
||||||
|
string SafeGetEnabledProviders()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var list = _locationManager.Call<AndroidJavaObject>("getProviders", true);
|
||||||
|
if (list == null) return "";
|
||||||
|
int size = list.Call<int>("size");
|
||||||
|
var parts = new System.Text.StringBuilder();
|
||||||
|
for (int i = 0; i < size; i++)
|
||||||
|
{
|
||||||
|
var name = list.Call<string>("get", i);
|
||||||
|
if (i > 0) parts.Append(",");
|
||||||
|
parts.Append(name);
|
||||||
|
}
|
||||||
|
return parts.ToString();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[GPS-JNI] getProviders failed: " + ex.Message);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateLocation(double lat, double lon, long timeMillis, string provider, bool isCached)
|
||||||
|
{
|
||||||
|
// Ignore older fixes if a newer one is already in hand. This lets
|
||||||
|
// both gps + network listeners feed us without ping-ponging
|
||||||
|
// between stale and fresh data.
|
||||||
|
if (timeMillis < _lastTimeMillis) return;
|
||||||
|
_lat = lat;
|
||||||
|
_lon = lon;
|
||||||
|
_lastTimeMillis = timeMillis;
|
||||||
|
// Active-provider name carries cached/live state in the diagnostic
|
||||||
|
// banner so the user can see at a glance whether streaming has
|
||||||
|
// kicked in or we're still on the initial cached snapshot.
|
||||||
|
_activeProvider = (provider ?? "") + (isCached ? " (cached)" : "");
|
||||||
|
_hasFix = true;
|
||||||
|
if (!isCached)
|
||||||
|
{
|
||||||
|
_hasLiveFix = true;
|
||||||
|
_lastLiveTimeMillis = timeMillis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Shutdown()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_locationManager != null)
|
||||||
|
{
|
||||||
|
if (_gpsListener != null) _locationManager.Call("removeUpdates", _gpsListener);
|
||||||
|
if (_networkListener != null) _locationManager.Call("removeUpdates", _networkListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[GPS-JNI] Shutdown failed: " + ex.Message);
|
||||||
|
}
|
||||||
|
_gpsListener = null;
|
||||||
|
_networkListener = null;
|
||||||
|
_locationManager = null;
|
||||||
|
_activity = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
public static class PositonExtensions
|
public static class PositonExtensions
|
||||||
{
|
{
|
||||||
public static Position ToLocal(this Position position, Position center)
|
public static Position ToLocal(this Position position, Position center)
|
||||||
@@ -51,34 +337,283 @@ namespace Subsystems
|
|||||||
private GameObject _player;
|
private GameObject _player;
|
||||||
private bool _testMode;
|
private bool _testMode;
|
||||||
|
|
||||||
|
// PlayerPrefs key for the user's chosen position source. Persists
|
||||||
|
// across app restarts so a user who flipped to UnityInput because
|
||||||
|
// their phone hated the JNI path doesn't have to flip again every
|
||||||
|
// launch.
|
||||||
|
private const string PrefsSourceKey = "PositionSource_v1";
|
||||||
|
private PositionSource _currentSource = PositionSource.Auto;
|
||||||
|
|
||||||
|
// When the multi-client editor test mode picks a non-host bot as
|
||||||
|
// active, we need the host's WASD path to NOT also move. Set true
|
||||||
|
// by GameManager when active slot != 0.
|
||||||
|
public bool SuppressWasd = false;
|
||||||
|
|
||||||
private GPSState _GPSState = GPSState.Uninitialized;
|
private GPSState _GPSState = GPSState.Uninitialized;
|
||||||
private float _speed = 0.00001f;
|
private float _speed = 0.00001f;
|
||||||
private Position _mapCenter;
|
private Position _mapCenter;
|
||||||
private CoroutineHost _coroutineHost = new CoroutineHost();
|
private CoroutineHost _coroutineHost;
|
||||||
|
|
||||||
|
private int _gpsRetryCount = 0;
|
||||||
|
private const int _maxGpsRetries = 5;
|
||||||
|
private float _lastPositionSendTime;
|
||||||
|
private const float _positionKeepAliveSeconds = 1.0f;
|
||||||
|
|
||||||
|
// Diagnostic state. We capture *why* GPS init failed so the UI can
|
||||||
|
// surface it to the user without requiring logcat. Older Android
|
||||||
|
// phones (Mi 9T, A20e) hit silent failure modes that are impossible
|
||||||
|
// to distinguish from "still warming up" without this.
|
||||||
|
private string _lastGpsError = "";
|
||||||
|
private float _gpsInitStartTime = -1f;
|
||||||
|
// Bump from the original 20s. Cold-start GPS on older Android can
|
||||||
|
// easily exceed 20s indoors or under cloud cover - by the time the
|
||||||
|
// user notices nothing is happening, we've already given up.
|
||||||
|
private const int _gpsInitTimeoutSeconds = 60;
|
||||||
|
|
||||||
|
#if UNITY_ANDROID && !UNITY_EDITOR
|
||||||
|
// JNI-backed location provider, used for Auto/GpsOnly/NetworkOnly.
|
||||||
|
// UnityInput uses Input.location instead and leaves this null.
|
||||||
|
private AndroidLocationProvider _androidProvider;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// <summary>Last known GPS position (for CreateLobby centre point)</summary>
|
||||||
|
public Position? LastKnownPosition => _currentPosition.Lat != 0 || _currentPosition.Lon != 0 ? _currentPosition : (Position?)null;
|
||||||
|
|
||||||
|
/// <summary>Current GPS state machine value (debug/diagnostic).</summary>
|
||||||
|
public string GpsStateName => _GPSState.ToString();
|
||||||
|
|
||||||
|
/// <summary>Last GPS error reason captured during init (empty if none).</summary>
|
||||||
|
public string LastGpsError => _lastGpsError ?? "";
|
||||||
|
|
||||||
|
/// <summary>Retry count out of max (debug/diagnostic).</summary>
|
||||||
|
public string GpsRetryProgress => $"{_gpsRetryCount}/{_maxGpsRetries}";
|
||||||
|
|
||||||
|
/// <summary>Currently selected position source (for UI cycle button).</summary>
|
||||||
|
public PositionSource CurrentSource => _currentSource;
|
||||||
|
|
||||||
|
/// <summary>Display name for the current source (for UI label).</summary>
|
||||||
|
public string CurrentSourceName
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
switch (_currentSource)
|
||||||
|
{
|
||||||
|
case PositionSource.Auto: return "Auto (GPS+Net)";
|
||||||
|
case PositionSource.GpsOnly: return "GPS only";
|
||||||
|
case PositionSource.NetworkOnly: return "Network only";
|
||||||
|
case PositionSource.UnityInput: return "Unity Input";
|
||||||
|
case PositionSource.EditorWasd: return "WASD";
|
||||||
|
default: return _currentSource.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Human-readable one-line GPS status for on-screen overlay. Designed
|
||||||
|
/// to be visible without ADB so users can self-diagnose permission
|
||||||
|
/// vs. timeout vs. device-disabled vs. running-but-no-fix-yet.
|
||||||
|
/// </summary>
|
||||||
|
public string GpsDiagnostic
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_currentSource == PositionSource.EditorWasd)
|
||||||
|
{
|
||||||
|
if (_currentPosition.Lat == 0 && _currentPosition.Lon == 0)
|
||||||
|
return "WASD: waiting for map center";
|
||||||
|
return $"WASD lat={_currentPosition.Lat:F5} lon={_currentPosition.Lon:F5}";
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (_GPSState)
|
||||||
|
{
|
||||||
|
case GPSState.Uninitialized:
|
||||||
|
return "Uninitialized (will start on first lobby action)";
|
||||||
|
case GPSState.Initializing:
|
||||||
|
{
|
||||||
|
float elapsed = _gpsInitStartTime >= 0 ? Time.time - _gpsInitStartTime : 0;
|
||||||
|
string providers = "";
|
||||||
|
#if UNITY_ANDROID && !UNITY_EDITOR
|
||||||
|
if (_androidProvider != null && !string.IsNullOrEmpty(_androidProvider.EnabledProvidersList))
|
||||||
|
providers = $" providers=[{_androidProvider.EnabledProvidersList}]";
|
||||||
|
#endif
|
||||||
|
return $"Initializing ({elapsed:F1}s / max {_gpsInitTimeoutSeconds}s){providers}";
|
||||||
|
}
|
||||||
|
case GPSState.Running:
|
||||||
|
{
|
||||||
|
string suffix = "";
|
||||||
|
#if UNITY_ANDROID && !UNITY_EDITOR
|
||||||
|
if (_androidProvider != null)
|
||||||
|
{
|
||||||
|
string p = _androidProvider.ActiveProvider;
|
||||||
|
if (!string.IsNullOrEmpty(p)) suffix = " via " + p;
|
||||||
|
// Show how stale the most recent fix is (ms-level
|
||||||
|
// resolution) so "stuck on cached" is obvious at
|
||||||
|
// a glance: "via gps (cached) [no live, 47s old]".
|
||||||
|
if (!_androidProvider.HasLiveFix)
|
||||||
|
{
|
||||||
|
long now = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
|
||||||
|
long ageMs = now - _androidProvider.LastTimeMillis;
|
||||||
|
if (_androidProvider.LastTimeMillis > 0 && ageMs > 0)
|
||||||
|
suffix += $" [no live, {ageMs / 1000}s old]";
|
||||||
|
else
|
||||||
|
suffix += " [no live]";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
long now = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
|
||||||
|
long ageMs = now - _androidProvider.LastLiveTimeMillis;
|
||||||
|
if (ageMs > 5000) suffix += $" [live {ageMs / 1000}s old]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (_currentPosition.Lat == 0 && _currentPosition.Lon == 0)
|
||||||
|
return "Running but no fix yet (waiting for satellites)" + suffix;
|
||||||
|
return $"Running lat={_currentPosition.Lat:F5} lon={_currentPosition.Lon:F5}" + suffix;
|
||||||
|
}
|
||||||
|
case GPSState.Failed:
|
||||||
|
return $"Failed: {(_lastGpsError ?? "unknown")} (retries {GpsRetryProgress})";
|
||||||
|
default:
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public GameManager_Input(GameClient gameClient, GameObject player, bool testMode)
|
public GameManager_Input(GameClient gameClient, GameObject player, bool testMode)
|
||||||
{
|
{
|
||||||
_gameClient = gameClient;
|
_gameClient = gameClient;
|
||||||
_player = player;
|
_player = player;
|
||||||
_testMode = testMode;
|
_testMode = testMode;
|
||||||
|
// CoroutineHost needs a MonoBehaviour on a real GameObject
|
||||||
|
var hostGO = new UnityEngine.GameObject("_CoroutineHost");
|
||||||
|
UnityEngine.Object.DontDestroyOnLoad(hostGO);
|
||||||
|
_coroutineHost = hostGO.AddComponent<CoroutineHost>();
|
||||||
|
|
||||||
|
// Restore the user's last picked source. Default depends on
|
||||||
|
// platform: editor defaults to EditorWasd (no GPS hardware in
|
||||||
|
// editor anyway); device defaults to Auto.
|
||||||
|
string saved = PlayerPrefs.GetString(PrefsSourceKey, "");
|
||||||
|
if (!string.IsNullOrEmpty(saved) && Enum.TryParse(saved, out PositionSource parsed))
|
||||||
|
{
|
||||||
|
_currentSource = parsed;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
_currentSource = PositionSource.EditorWasd;
|
||||||
|
#else
|
||||||
|
_currentSource = PositionSource.Auto;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy testMode flag forces EditorWasd. New code paths should
|
||||||
|
// use SwitchPositionSource(EditorWasd) instead, but we keep the
|
||||||
|
// old behavior for backward compatibility with the inspector flag.
|
||||||
|
if (_testMode) _currentSource = PositionSource.EditorWasd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Called from OnSceneLoaded when Client.unity loads so the
|
||||||
|
/// Player capsule (which lives in Client.unity) can be wired at runtime.</summary>
|
||||||
|
public void SetPlayerObject(GameObject player) { _player = player; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Switch the active position source backend live. Tears down the
|
||||||
|
/// current backend's listeners (JNI proxies, Input.location), resets
|
||||||
|
/// the state machine, and kicks off init for the new source. Persists
|
||||||
|
/// the choice to PlayerPrefs.
|
||||||
|
/// </summary>
|
||||||
|
public void SwitchPositionSource(PositionSource newSource)
|
||||||
|
{
|
||||||
|
if (_currentSource == newSource) return;
|
||||||
|
Debug.Log($"[GPS] SwitchPositionSource {_currentSource} -> {newSource}");
|
||||||
|
|
||||||
|
// Tear down whatever's running.
|
||||||
|
ShutdownCurrentBackend();
|
||||||
|
|
||||||
|
_currentSource = newSource;
|
||||||
|
PlayerPrefs.SetString(PrefsSourceKey, newSource.ToString());
|
||||||
|
PlayerPrefs.Save();
|
||||||
|
|
||||||
|
_GPSState = GPSState.Uninitialized;
|
||||||
|
_gpsRetryCount = 0;
|
||||||
|
_lastGpsError = "";
|
||||||
|
_gpsInitStartTime = -1f;
|
||||||
|
// Don't clear _currentPosition - the user has presumably been
|
||||||
|
// playing somewhere. Map markers/avatar position can stay until
|
||||||
|
// the next fix arrives from the new source.
|
||||||
|
|
||||||
|
EnsureGPSStarted();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Cycle through the available sources for tap-to-cycle UI.</summary>
|
||||||
|
public void CycleNextPositionSource()
|
||||||
|
{
|
||||||
|
var values = (PositionSource[])Enum.GetValues(typeof(PositionSource));
|
||||||
|
int idx = Array.IndexOf(values, _currentSource);
|
||||||
|
var next = values[(idx + 1) % values.Length];
|
||||||
|
SwitchPositionSource(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShutdownCurrentBackend()
|
||||||
|
{
|
||||||
|
#if UNITY_ANDROID && !UNITY_EDITOR
|
||||||
|
if (_androidProvider != null)
|
||||||
|
{
|
||||||
|
_androidProvider.Shutdown();
|
||||||
|
_androidProvider = null;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
// Stop Unity Input.location too, in case it was running.
|
||||||
|
try { Input.location.Stop(); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kick off GPS initialization if it hasn't started yet. Safe to call
|
||||||
|
/// repeatedly. Hosts must call this from the lobby setup screen so
|
||||||
|
/// that by the time they click "Create Lobby" we have a real GPS
|
||||||
|
/// fix to use as the play-area center, instead of falling back to
|
||||||
|
/// the hardcoded coordinates.
|
||||||
|
/// </summary>
|
||||||
|
public void EnsureGPSStarted()
|
||||||
|
{
|
||||||
|
if (_currentSource == PositionSource.EditorWasd) return;
|
||||||
|
if (_coroutineHost == null) return;
|
||||||
|
// Allow tapping "Create Lobby" again (or any caller of this
|
||||||
|
// method) to retry from Failed up to _maxGpsRetries times.
|
||||||
|
if (_GPSState == GPSState.Uninitialized)
|
||||||
|
{
|
||||||
|
_coroutineHost.StartCoroutine(InitiallizeGPS());
|
||||||
|
}
|
||||||
|
else if (_GPSState == GPSState.Failed && _gpsRetryCount < _maxGpsRetries)
|
||||||
|
{
|
||||||
|
_gpsRetryCount++;
|
||||||
|
_coroutineHost.StartCoroutine(InitiallizeGPS());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
public void positionCheck()
|
public void positionCheck()
|
||||||
{
|
{
|
||||||
|
var state = _gameClient?.CurrentLobbyState;
|
||||||
|
if (state == null || state.Phase != GamePhase.Playing)
|
||||||
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_gameClient.CurrentLobbyState.Phase == GamePhase.Playing)
|
if (_currentSource == PositionSource.EditorWasd)
|
||||||
{
|
{
|
||||||
if (_testMode)
|
if (_currentPosition == new Position(0, 0))
|
||||||
{
|
{
|
||||||
|
if (state.MapData == null)
|
||||||
|
return;
|
||||||
|
|
||||||
if (_currentPosition == null || _currentPosition == new Position(0, 0))
|
|
||||||
{
|
|
||||||
//Init blok
|
//Init blok
|
||||||
_currentPosition = _gameClient.CurrentLobbyState.MapData.Center;
|
_currentPosition = state.MapData.Center;
|
||||||
_mapCenter = _gameClient.CurrentLobbyState.MapData.Center;
|
_mapCenter = state.MapData.Center;
|
||||||
_lastSentPosition = _currentPosition;
|
_lastSentPosition = _currentPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!SuppressWasd)
|
||||||
TestPlayerPosition();
|
TestPlayerPosition();
|
||||||
|
else
|
||||||
|
TrySendCurrentPosition(); // keep-alive only
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -93,54 +628,84 @@ namespace Subsystems
|
|||||||
}
|
}
|
||||||
else if (_GPSState == GPSState.Running)
|
else if (_GPSState == GPSState.Running)
|
||||||
{
|
{
|
||||||
try
|
EnsureMapCenter();
|
||||||
|
TrySendCurrentPosition();
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
if (_currentPosition != _lastSentPosition)
|
Debug.Log("GPS failed, trying again...");
|
||||||
|
if (_gpsRetryCount < _maxGpsRetries)
|
||||||
{
|
{
|
||||||
_gameClient.UpdatePosition(_currentPosition);
|
_gpsRetryCount++;
|
||||||
_lastSentPosition = _currentPosition;
|
_GPSState = GPSState.Uninitialized;
|
||||||
_player.transform.position = _currentPosition.ToLocalVector3(_mapCenter);
|
}
|
||||||
_player.transform.rotation = Quaternion.Euler(0, (float)CalculateHeading(_lastSentPosition.ToLocalVector3(_mapCenter), _currentPosition.ToLocalVector3(_mapCenter)), 0);
|
else
|
||||||
|
{
|
||||||
|
Debug.LogWarning("GPS unavailable after max retries. Using last known position.");
|
||||||
|
// Keep _GPSState = Failed so we stop retrying
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Debug.Log(ex);
|
Debug.LogWarning($"[Input] positionCheck failed: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
private void EnsureMapCenter()
|
||||||
{
|
{
|
||||||
Debug.Log("GPS failed, trying again...");)
|
if (_mapCenter.Lat != 0 || _mapCenter.Lon != 0)
|
||||||
_GPSState = GPSState.Uninitialized;
|
return;
|
||||||
|
|
||||||
|
var md = _gameClient?.CurrentLobbyState?.MapData;
|
||||||
|
if (md != null)
|
||||||
|
_mapCenter = md.Center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void TrySendCurrentPosition()
|
||||||
|
{
|
||||||
|
bool moved = _currentPosition != _lastSentPosition;
|
||||||
|
bool keepAliveDue = (Time.time - _lastPositionSendTime) >= _positionKeepAliveSeconds;
|
||||||
|
if (!moved && !keepAliveDue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var previous = _lastSentPosition;
|
||||||
|
_gameClient.UpdatePosition(_currentPosition);
|
||||||
|
_lastSentPosition = _currentPosition;
|
||||||
|
_lastPositionSendTime = Time.time;
|
||||||
|
|
||||||
|
if (_player == null || (_mapCenter.Lat == 0 && _mapCenter.Lon == 0))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var localCurrent = _currentPosition.ToLocalVector3(_mapCenter);
|
||||||
|
_player.transform.position = localCurrent;
|
||||||
|
|
||||||
|
if (previous.Lat == 0 && previous.Lon == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var heading = CalculateHeading(previous.ToLocalVector3(_mapCenter), localCurrent);
|
||||||
|
if (heading.HasValue)
|
||||||
|
_player.transform.rotation = Quaternion.Euler(0, (float)heading.Value, 0);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (NullReferenceException ex) { Debug.Log(ex); }
|
|
||||||
}
|
|
||||||
private void TestPlayerPosition()
|
private void TestPlayerPosition()
|
||||||
{
|
{
|
||||||
double x = Input.GetAxis("Horizontal");
|
double x = Input.GetAxis("Horizontal");
|
||||||
double y = Input.GetAxis("Vertical");
|
double y = Input.GetAxis("Vertical");
|
||||||
Debug.Log($"Input: {x}, {y}");
|
|
||||||
_currentPosition = new Position( _lastSentPosition.Lat + y * _speed, _lastSentPosition.Lon + x * _speed);
|
_currentPosition = new Position( _lastSentPosition.Lat + y * _speed, _lastSentPosition.Lon + x * _speed);
|
||||||
Debug.Log($"Current Position: {_currentPosition.Lat}, {_currentPosition.Lon}");
|
|
||||||
var localCurrent = _currentPosition.ToLocalVector3(_mapCenter);
|
var localCurrent = _currentPosition.ToLocalVector3(_mapCenter);
|
||||||
Debug.Log($"Local Current Position: {localCurrent}");
|
|
||||||
var heading = CalculateHeading(_lastSentPosition.ToLocalVector3(_mapCenter), localCurrent);
|
var heading = CalculateHeading(_lastSentPosition.ToLocalVector3(_mapCenter), localCurrent);
|
||||||
if (heading != null)
|
if (heading != null)
|
||||||
{
|
{
|
||||||
Debug.Log($"Heading: {heading}");
|
if (_player != null)
|
||||||
_player.transform.rotation = Quaternion.Euler(0, (float)heading, 0);
|
_player.transform.rotation = Quaternion.Euler(0, (float)heading, 0);
|
||||||
}
|
}
|
||||||
|
if (_player != null)
|
||||||
_player.transform.position = localCurrent;
|
_player.transform.position = localCurrent;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_currentPosition != _lastSentPosition)
|
TrySendCurrentPosition();
|
||||||
{
|
|
||||||
_gameClient.UpdatePosition(_currentPosition);
|
|
||||||
_lastSentPosition = _currentPosition;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -150,105 +715,212 @@ namespace Subsystems
|
|||||||
}
|
}
|
||||||
private double? CalculateHeading(Vector3 first, Vector3 second)
|
private double? CalculateHeading(Vector3 first, Vector3 second)
|
||||||
{
|
{
|
||||||
double? heading = null;
|
if ((first - second).magnitude < 0.0001f) return null;
|
||||||
if ((first - second).magnitude == 0)
|
float dx = second.x - first.x;
|
||||||
{
|
float dz = second.z - first.z;
|
||||||
return null;
|
float heading = Mathf.Atan2(dx, dz) * Mathf.Rad2Deg;
|
||||||
}
|
if (heading < 0) heading += 360f;
|
||||||
else if (first.x == second.x && first.z < second.z)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
else if (first.x == second.x && first.z > second.z)
|
|
||||||
{
|
|
||||||
return 180;
|
|
||||||
}
|
|
||||||
else if (first.x > second.x && first.z == second.z)
|
|
||||||
{
|
|
||||||
return 270;
|
|
||||||
}
|
|
||||||
else if (first.x < second.x && first.z == second.z)
|
|
||||||
{
|
|
||||||
return 90;
|
|
||||||
}
|
|
||||||
else if (first.x < second.x && first.z < second.z)
|
|
||||||
{
|
|
||||||
heading = Math.Asin((second.z - first.z) / first.DistanceTo(second));
|
|
||||||
return (heading * 180) / Math.PI;
|
|
||||||
}
|
|
||||||
else if (first.x < second.x && first.z > second.z)
|
|
||||||
{
|
|
||||||
heading = Math.Asin((second.z - first.z) / first.DistanceTo(second));
|
|
||||||
return (heading * 180) / Math.PI + 180;
|
|
||||||
}
|
|
||||||
else if (first.x > second.x && first.z < second.z)
|
|
||||||
{
|
|
||||||
heading = Math.Asin((second.z - first.z) / first.DistanceTo(second));
|
|
||||||
return (heading * 180) / Math.PI - 90;
|
|
||||||
}
|
|
||||||
else if (first.x > second.x && first.z > second.z)
|
|
||||||
{
|
|
||||||
heading = Math.Asin((second.z - first.z) / first.DistanceTo(second));
|
|
||||||
return (heading * 180) / Math.PI - 90;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return heading;
|
return heading;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
IEnumerator InitiallizeGPS()
|
IEnumerator InitiallizeGPS()
|
||||||
{
|
{
|
||||||
_GPSState = GPSState.Initializing;
|
_GPSState = GPSState.Initializing;
|
||||||
|
_gpsInitStartTime = Time.time;
|
||||||
|
_lastGpsError = "";
|
||||||
|
|
||||||
|
#if UNITY_ANDROID
|
||||||
|
// Request fine location permission if not already granted.
|
||||||
|
// On Android 12+ a "precise" toggle exists separately from coarse,
|
||||||
|
// but Unity's FineLocation request covers both for our purposes.
|
||||||
|
if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.FineLocation))
|
||||||
|
{
|
||||||
|
UnityEngine.Android.Permission.RequestUserPermission(UnityEngine.Android.Permission.FineLocation);
|
||||||
|
// Wait up to 10 seconds for user to respond to the permission dialog
|
||||||
|
float waited = 0f;
|
||||||
|
while (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.FineLocation) && waited < 10f)
|
||||||
|
{
|
||||||
|
yield return new WaitForSeconds(0.5f);
|
||||||
|
waited += 0.5f;
|
||||||
|
}
|
||||||
|
if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.FineLocation))
|
||||||
|
{
|
||||||
|
_lastGpsError = "Permission denied (fine location)";
|
||||||
|
Debug.LogError("[GPS] " + _lastGpsError);
|
||||||
|
_GPSState = GPSState.Failed;
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if UNITY_ANDROID && !UNITY_EDITOR
|
||||||
|
// Choose subscription scope based on selected source. UnityInput
|
||||||
|
// skips JNI entirely and falls through to the Input.location path
|
||||||
|
// below (the same path iOS / editor use).
|
||||||
|
if (_currentSource == PositionSource.Auto ||
|
||||||
|
_currentSource == PositionSource.GpsOnly ||
|
||||||
|
_currentSource == PositionSource.NetworkOnly)
|
||||||
|
{
|
||||||
|
bool useGps = (_currentSource != PositionSource.NetworkOnly);
|
||||||
|
bool useNetwork = (_currentSource != PositionSource.GpsOnly);
|
||||||
|
|
||||||
|
if (_androidProvider != null)
|
||||||
|
{
|
||||||
|
_androidProvider.Shutdown();
|
||||||
|
_androidProvider = null;
|
||||||
|
}
|
||||||
|
_androidProvider = new AndroidLocationProvider();
|
||||||
|
if (!_androidProvider.Initialize(out var initError, useGps, useNetwork))
|
||||||
|
{
|
||||||
|
_lastGpsError = "Native LocationManager failed: " + initError;
|
||||||
|
Debug.LogError("[GPS] " + _lastGpsError);
|
||||||
|
_androidProvider = null;
|
||||||
|
_GPSState = GPSState.Failed;
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fast-fail if neither subscribed provider is enabled at OS
|
||||||
|
// level. Waiting 60s for fixes from disabled providers is
|
||||||
|
// pointless - tell the user immediately what's wrong.
|
||||||
|
bool anyUsableEnabled =
|
||||||
|
(useGps && _androidProvider.GpsProviderEnabled) ||
|
||||||
|
(useNetwork && _androidProvider.NetworkProviderEnabled);
|
||||||
|
if (!anyUsableEnabled)
|
||||||
|
{
|
||||||
|
string which = useGps && useNetwork ? "gps + network"
|
||||||
|
: useGps ? "gps"
|
||||||
|
: "network";
|
||||||
|
_lastGpsError = $"{which} provider DISABLED at OS level. Open Settings > Location and switch it ON. Or tap [Source] to try a different backend.";
|
||||||
|
Debug.LogError("[GPS] " + _lastGpsError);
|
||||||
|
_androidProvider.Shutdown();
|
||||||
|
_androidProvider = null;
|
||||||
|
_GPSState = GPSState.Failed;
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the first fix (cached or live).
|
||||||
|
int maxWaitJni = _gpsInitTimeoutSeconds;
|
||||||
|
while (!_androidProvider.HasFix && maxWaitJni > 0)
|
||||||
|
{
|
||||||
|
yield return new WaitForSeconds(1);
|
||||||
|
maxWaitJni--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_androidProvider.HasFix)
|
||||||
|
{
|
||||||
|
string enabled = _androidProvider.EnabledProvidersList ?? "";
|
||||||
|
string gpsState = _androidProvider.GpsProviderEnabled ? "ON" : "OFF";
|
||||||
|
string netState = _androidProvider.NetworkProviderEnabled ? "ON" : "OFF";
|
||||||
|
string lastKnown = $"lastKnown[gps={(_androidProvider.GpsLastKnownExists ? "yes" : "no")}, net={(_androidProvider.NetworkLastKnownExists ? "yes" : "no")}]";
|
||||||
|
|
||||||
|
_lastGpsError = $"Timeout {_gpsInitTimeoutSeconds}s on {_currentSource}. enabled=[{enabled}] gps={gpsState} net={netState} {lastKnown}. Try [Source] cycle to switch backends.";
|
||||||
|
Debug.LogError("[GPS] " + _lastGpsError);
|
||||||
|
_androidProvider.Shutdown();
|
||||||
|
_androidProvider = null;
|
||||||
|
_GPSState = GPSState.Failed;
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentPosition = new Position(_androidProvider.Lat, _androidProvider.Lon);
|
||||||
|
_GPSState = GPSState.Running;
|
||||||
|
_gpsRetryCount = 0;
|
||||||
|
_coroutineHost.StartCoroutine(AndroidGPSService());
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// _currentSource == UnityInput on Android: fall through to the
|
||||||
|
// Input.location path below. This is the recovery path for
|
||||||
|
// newer Android phones where JNI's streaming-callbacks don't
|
||||||
|
// fire (MIUI/HyperOS background restrictions, approximate-vs-
|
||||||
|
// precise permission, minDistance gating on stationary phones).
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// iOS / editor / non-Android / Android-with-UnityInput-source:
|
||||||
|
// use Unity's Input.location.
|
||||||
if (!Input.location.isEnabledByUser)
|
if (!Input.location.isEnabledByUser)
|
||||||
{
|
{
|
||||||
Debug.LogError("Location not enabled on device or app does not have permission to access location");
|
_lastGpsError = "Location services not enabled by user";
|
||||||
|
Debug.LogError("[GPS] " + _lastGpsError);
|
||||||
|
_GPSState = GPSState.Failed;
|
||||||
|
yield break;
|
||||||
}
|
}
|
||||||
// Starts the location service.
|
|
||||||
|
|
||||||
float desiredAccuracyInMeters = 10f;
|
float desiredAccuracyInMeters = 5f;
|
||||||
float updateDistanceInMeters = 10f;
|
float updateDistanceInMeters = 1f;
|
||||||
|
|
||||||
Input.location.Start(desiredAccuracyInMeters, updateDistanceInMeters);
|
Input.location.Start(desiredAccuracyInMeters, updateDistanceInMeters);
|
||||||
|
|
||||||
// Waits until the location service initializes
|
int maxWait = _gpsInitTimeoutSeconds;
|
||||||
int maxWait = 20;
|
|
||||||
while (Input.location.status == LocationServiceStatus.Initializing && maxWait > 0)
|
while (Input.location.status == LocationServiceStatus.Initializing && maxWait > 0)
|
||||||
{
|
{
|
||||||
yield return new WaitForSeconds(1);
|
yield return new WaitForSeconds(1);
|
||||||
maxWait--;
|
maxWait--;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the service didn't initialize in 20 seconds this cancels location service use.
|
|
||||||
if (maxWait < 1)
|
if (maxWait < 1)
|
||||||
{
|
{
|
||||||
|
_lastGpsError = $"Timed out after {_gpsInitTimeoutSeconds}s waiting for first fix (try moving outdoors, or tap [Source] to try a different backend)";
|
||||||
|
Debug.LogError("[GPS] " + _lastGpsError);
|
||||||
_GPSState = GPSState.Failed;
|
_GPSState = GPSState.Failed;
|
||||||
Debug.LogError("Timed out");
|
|
||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
_GPSState = GPSState.Running;
|
|
||||||
yield return _coroutineHost.StartCoroutine(GPSService());
|
|
||||||
}
|
|
||||||
IEnumerator GPSService()
|
|
||||||
{
|
|
||||||
// Check if the user has location service enabled.
|
|
||||||
|
|
||||||
|
|
||||||
// If the connection failed this cancels location service use.
|
|
||||||
if (Input.location.status == LocationServiceStatus.Failed)
|
if (Input.location.status == LocationServiceStatus.Failed)
|
||||||
{
|
{
|
||||||
Debug.LogError("Unable to determine device location");
|
_lastGpsError = "Unity Input.location reported Failed status";
|
||||||
|
Debug.LogError("[GPS] " + _lastGpsError);
|
||||||
|
_GPSState = GPSState.Failed;
|
||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
_GPSState = GPSState.Running;
|
||||||
// If the connection succeeded, this retrieves the device's current location and displays it in the Console window.
|
_gpsRetryCount = 0;
|
||||||
_currentPosition = new Position(Input.location.lastData.latitude, Input.location.lastData.longitude);
|
_coroutineHost.StartCoroutine(GPSService());
|
||||||
Debug.Log("Location: " + Input.location.lastData.latitude + " " + Input.location.lastData.longitude + " " + Input.location.lastData.altitude + " " + Input.location.lastData.horizontalAccuracy + " " + Input.location.lastData.timestamp);
|
|
||||||
yield return new WaitForSeconds(5f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stops the location service if there is no need to query location updates continuously.
|
#if UNITY_ANDROID && !UNITY_EDITOR
|
||||||
yield return _coroutineHost.StartCoroutine(GPSService());
|
/// <summary>
|
||||||
|
/// Mirrors the JNI provider's most recent fix into _currentPosition
|
||||||
|
/// every 0.5s so the rest of the game (which polls _currentPosition
|
||||||
|
/// indirectly via LastKnownPosition / TrySendCurrentPosition) keeps
|
||||||
|
/// working unchanged. Replaces GPSService on Android.
|
||||||
|
/// </summary>
|
||||||
|
IEnumerator AndroidGPSService()
|
||||||
|
{
|
||||||
|
while (_GPSState == GPSState.Running && _androidProvider != null)
|
||||||
|
{
|
||||||
|
if (_androidProvider.HasFix)
|
||||||
|
{
|
||||||
|
_currentPosition = new Position(_androidProvider.Lat, _androidProvider.Lon);
|
||||||
|
}
|
||||||
|
yield return new WaitForSeconds(0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop ended (state != Running or provider disposed). Clean up
|
||||||
|
// listeners so we don't leak across retries.
|
||||||
|
if (_androidProvider != null)
|
||||||
|
{
|
||||||
|
_androidProvider.Shutdown();
|
||||||
|
_androidProvider = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
IEnumerator GPSService()
|
||||||
|
{
|
||||||
|
while (_GPSState == GPSState.Running)
|
||||||
|
{
|
||||||
|
if (Input.location.status == LocationServiceStatus.Failed)
|
||||||
|
{
|
||||||
|
_lastGpsError = "Location service died after init (provider stopped)";
|
||||||
|
Debug.LogError("[GPS] " + _lastGpsError);
|
||||||
|
_GPSState = GPSState.Failed;
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep current GPS position fresh; sending is throttled in positionCheck().
|
||||||
|
var data = Input.location.lastData;
|
||||||
|
_currentPosition = new Position(data.latitude, data.longitude);
|
||||||
|
yield return new WaitForSeconds(0.5f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,6 @@ using System.Collections.Generic;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.Localization.Pseudo;
|
|
||||||
using UnityEngine.UI;
|
using UnityEngine.UI;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 6b37670de43269e4f984694475e75510
|
guid: 2230bf768ecb84610af77bea6cdd7074
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 2651bd22cf5764e4eb358f11641edca2
|
guid: 7142a58f80866aba3ae4ffeb79d5f67c
|
||||||
NativeFormatImporter:
|
NativeFormatImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
mainObjectFileID: 0
|
mainObjectFileID: 0
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 8356cb86749a3674299ae725d58c8012
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: a007c35d1a63dc3418a3afe3c7407450
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 8b4a331c3bc0f6445a0797f99cc38604
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 87c5ee97c7d1da345978bf431113faeb
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 17fc65d1a95feae42b8815a07ea2ffef
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 3f2ca619b4f5384489cf03c1dd13a701
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -193,13 +193,13 @@ Transform:
|
|||||||
m_GameObject: {fileID: 210602716}
|
m_GameObject: {fileID: 210602716}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||||
m_LocalPosition: {x: -55.235718, y: 368.19592, z: -781.7374}
|
m_LocalPosition: {x: -55.235718, y: 368.19595, z: -781.7374}
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children:
|
m_Children:
|
||||||
- {fileID: 1140131532}
|
- {fileID: 1140131532}
|
||||||
- {fileID: 2102485167}
|
- {fileID: 2102485167}
|
||||||
m_Father: {fileID: 1814274513}
|
m_Father: {fileID: 1790534262}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1 &331729628
|
--- !u!1 &331729628
|
||||||
GameObject:
|
GameObject:
|
||||||
@@ -1570,9 +1570,10 @@ RectTransform:
|
|||||||
m_GameObject: {fileID: 1790534261}
|
m_GameObject: {fileID: 1790534261}
|
||||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||||
m_LocalPosition: {x: 0, y: 0, z: 781.2808}
|
m_LocalPosition: {x: 0, y: 0, z: 781.2808}
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
m_LocalScale: {x: 2.92736, y: 2.5490422, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children:
|
||||||
|
- {fileID: 210602717}
|
||||||
m_Father: {fileID: 143293625}
|
m_Father: {fileID: 143293625}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
m_AnchorMin: {x: 0, y: 0}
|
m_AnchorMin: {x: 0, y: 0}
|
||||||
@@ -1650,9 +1651,8 @@ RectTransform:
|
|||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children:
|
m_Children:
|
||||||
- {fileID: 143293625}
|
- {fileID: 143293625}
|
||||||
- {fileID: 210602717}
|
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 34.899}
|
||||||
m_AnchorMin: {x: 0, y: 0}
|
m_AnchorMin: {x: 0, y: 0}
|
||||||
m_AnchorMax: {x: 0, y: 0}
|
m_AnchorMax: {x: 0, y: 0}
|
||||||
m_AnchoredPosition: {x: 0, y: 0}
|
m_AnchoredPosition: {x: 0, y: 0}
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: f39c83cede024654b8b01f3ed649efc8
|
|
||||||
TextureImporter:
|
|
||||||
internalIDToNameTable: []
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 13
|
|
||||||
mipmaps:
|
|
||||||
mipMapMode: 0
|
|
||||||
enableMipMap: 1
|
|
||||||
sRGBTexture: 1
|
|
||||||
linearTexture: 0
|
|
||||||
fadeOut: 0
|
|
||||||
borderMipMap: 0
|
|
||||||
mipMapsPreserveCoverage: 0
|
|
||||||
alphaTestReferenceValue: 0.5
|
|
||||||
mipMapFadeDistanceStart: 1
|
|
||||||
mipMapFadeDistanceEnd: 3
|
|
||||||
bumpmap:
|
|
||||||
convertToNormalMap: 0
|
|
||||||
externalNormalMap: 0
|
|
||||||
heightScale: 0.25
|
|
||||||
normalMapFilter: 0
|
|
||||||
flipGreenChannel: 0
|
|
||||||
isReadable: 0
|
|
||||||
streamingMipmaps: 0
|
|
||||||
streamingMipmapsPriority: 0
|
|
||||||
vTOnly: 0
|
|
||||||
ignoreMipmapLimit: 0
|
|
||||||
grayScaleToAlpha: 0
|
|
||||||
generateCubemap: 6
|
|
||||||
cubemapConvolution: 0
|
|
||||||
seamlessCubemap: 0
|
|
||||||
textureFormat: 1
|
|
||||||
maxTextureSize: 2048
|
|
||||||
textureSettings:
|
|
||||||
serializedVersion: 2
|
|
||||||
filterMode: 1
|
|
||||||
aniso: 1
|
|
||||||
mipBias: 0
|
|
||||||
wrapU: 0
|
|
||||||
wrapV: 0
|
|
||||||
wrapW: 0
|
|
||||||
nPOTScale: 1
|
|
||||||
lightmap: 0
|
|
||||||
compressionQuality: 50
|
|
||||||
spriteMode: 0
|
|
||||||
spriteExtrude: 1
|
|
||||||
spriteMeshType: 1
|
|
||||||
alignment: 0
|
|
||||||
spritePivot: {x: 0.5, y: 0.5}
|
|
||||||
spritePixelsToUnits: 100
|
|
||||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
|
||||||
spriteGenerateFallbackPhysicsShape: 1
|
|
||||||
alphaUsage: 1
|
|
||||||
alphaIsTransparency: 0
|
|
||||||
spriteTessellationDetail: -1
|
|
||||||
textureType: 0
|
|
||||||
textureShape: 1
|
|
||||||
singleChannelComponent: 0
|
|
||||||
flipbookRows: 1
|
|
||||||
flipbookColumns: 1
|
|
||||||
maxTextureSizeSet: 0
|
|
||||||
compressionQualitySet: 0
|
|
||||||
textureFormatSet: 0
|
|
||||||
ignorePngGamma: 0
|
|
||||||
applyGammaDecoding: 0
|
|
||||||
swizzle: 50462976
|
|
||||||
cookieLightType: 0
|
|
||||||
platformSettings:
|
|
||||||
- serializedVersion: 4
|
|
||||||
buildTarget: DefaultTexturePlatform
|
|
||||||
maxTextureSize: 2048
|
|
||||||
resizeAlgorithm: 0
|
|
||||||
textureFormat: -1
|
|
||||||
textureCompression: 1
|
|
||||||
compressionQuality: 50
|
|
||||||
crunchedCompression: 0
|
|
||||||
allowsAlphaSplitting: 0
|
|
||||||
overridden: 0
|
|
||||||
ignorePlatformSupport: 0
|
|
||||||
androidETC2FallbackOverride: 0
|
|
||||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
|
||||||
- serializedVersion: 4
|
|
||||||
buildTarget: Standalone
|
|
||||||
maxTextureSize: 2048
|
|
||||||
resizeAlgorithm: 0
|
|
||||||
textureFormat: -1
|
|
||||||
textureCompression: 1
|
|
||||||
compressionQuality: 50
|
|
||||||
crunchedCompression: 0
|
|
||||||
allowsAlphaSplitting: 0
|
|
||||||
overridden: 0
|
|
||||||
ignorePlatformSupport: 0
|
|
||||||
androidETC2FallbackOverride: 0
|
|
||||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
|
||||||
- serializedVersion: 4
|
|
||||||
buildTarget: Android
|
|
||||||
maxTextureSize: 2048
|
|
||||||
resizeAlgorithm: 0
|
|
||||||
textureFormat: -1
|
|
||||||
textureCompression: 1
|
|
||||||
compressionQuality: 50
|
|
||||||
crunchedCompression: 0
|
|
||||||
allowsAlphaSplitting: 0
|
|
||||||
overridden: 0
|
|
||||||
ignorePlatformSupport: 0
|
|
||||||
androidETC2FallbackOverride: 0
|
|
||||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
|
||||||
- serializedVersion: 4
|
|
||||||
buildTarget: WebGL
|
|
||||||
maxTextureSize: 2048
|
|
||||||
resizeAlgorithm: 0
|
|
||||||
textureFormat: -1
|
|
||||||
textureCompression: 1
|
|
||||||
compressionQuality: 50
|
|
||||||
crunchedCompression: 0
|
|
||||||
allowsAlphaSplitting: 0
|
|
||||||
overridden: 0
|
|
||||||
ignorePlatformSupport: 0
|
|
||||||
androidETC2FallbackOverride: 0
|
|
||||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
|
||||||
spriteSheet:
|
|
||||||
serializedVersion: 2
|
|
||||||
sprites: []
|
|
||||||
outline: []
|
|
||||||
customData:
|
|
||||||
physicsShape: []
|
|
||||||
bones: []
|
|
||||||
spriteID:
|
|
||||||
internalID: 0
|
|
||||||
vertices: []
|
|
||||||
indices:
|
|
||||||
edges: []
|
|
||||||
weights: []
|
|
||||||
secondaryTextures: []
|
|
||||||
spriteCustomMetadata:
|
|
||||||
entries: []
|
|
||||||
nameFileIdTable: {}
|
|
||||||
mipmapLimitGroupName:
|
|
||||||
pSDRemoveMatte: 0
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 58a0d1d70570e1340b7bde7edda96e9a
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: bd359125026cdc144a1f35a5c8607b45
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: d0a79df83e583b147b8f08cd4a8a96a8
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 38f8393f8cca8f24bb21ca2ab3a28acc
|
guid: b9c8f9e5bbf063b4fb1152966129a495
|
||||||
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
userData:
|
userData:
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: ae701dd46572ae44faa1c08754c677a1
|
guid: 0f6331bfef5b3a00cb6f84141e60f9c4
|
||||||
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
userData:
|
userData:
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: fbd0ba17ed8002141b6b82c678cd3baf
|
guid: 43d195e24415843378eb1b53e9e1da18
|
||||||
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
userData:
|
userData:
|
||||||
Reference in New Issue
Block a user