Compare commits

...

3 Commits

Author SHA1 Message Date
c8d8b6b802 Conflict resolve 2026-05-17 11:25:21 +02:00
f800e78f14 kontrola simulace na telefonu 2026-04-26 11:40:58 +02:00
e29581cc21 opravene kabely (je jen 1 scena) 2026-04-26 10:30:47 +02:00
28 changed files with 807 additions and 17978 deletions

View File

@@ -1,4 +1,4 @@
using UnityEngine;
using UnityEngine;
using GeoSus.Client;
using System;
using System.Collections;
@@ -16,6 +16,292 @@ namespace Subsystems
Running,
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 Position ToLocal(this Position position, Position center)
@@ -51,34 +337,283 @@ namespace Subsystems
private GameObject _player;
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 float _speed = 0.00001f;
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)
{
_gameClient = gameClient;
_player = player;
_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()
{
var state = _gameClient?.CurrentLobbyState;
if (state == null || state.Phase != GamePhase.Playing)
return;
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
_currentPosition = _gameClient.CurrentLobbyState.MapData.Center;
_mapCenter = _gameClient.CurrentLobbyState.MapData.Center;
_currentPosition = state.MapData.Center;
_mapCenter = state.MapData.Center;
_lastSentPosition = _currentPosition;
}
if (!SuppressWasd)
TestPlayerPosition();
else
TrySendCurrentPosition(); // keep-alive only
}
else
{
@@ -93,54 +628,84 @@ namespace Subsystems
}
else if (_GPSState == GPSState.Running)
{
try
EnsureMapCenter();
TrySendCurrentPosition();
}
else
{
if (_currentPosition != _lastSentPosition)
Debug.Log("GPS failed, trying again...");
if (_gpsRetryCount < _maxGpsRetries)
{
_gameClient.UpdatePosition(_currentPosition);
_lastSentPosition = _currentPosition;
_player.transform.position = _currentPosition.ToLocalVector3(_mapCenter);
_player.transform.rotation = Quaternion.Euler(0, (float)CalculateHeading(_lastSentPosition.ToLocalVector3(_mapCenter), _currentPosition.ToLocalVector3(_mapCenter)), 0);
_gpsRetryCount++;
_GPSState = GPSState.Uninitialized;
}
else
{
Debug.LogWarning("GPS unavailable after max retries. Using last known position.");
// Keep _GPSState = Failed so we stop retrying
}
}
}
}
catch (Exception ex)
{
Debug.Log(ex);
Debug.LogWarning($"[Input] positionCheck failed: {ex.Message}");
}
}
else
private void EnsureMapCenter()
{
Debug.Log("GPS failed, trying again...");)
_GPSState = GPSState.Uninitialized;
if (_mapCenter.Lat != 0 || _mapCenter.Lon != 0)
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()
{
double x = Input.GetAxis("Horizontal");
double y = Input.GetAxis("Vertical");
Debug.Log($"Input: {x}, {y}");
_currentPosition = new Position( _lastSentPosition.Lat + y * _speed, _lastSentPosition.Lon + x * _speed);
Debug.Log($"Current Position: {_currentPosition.Lat}, {_currentPosition.Lon}");
var localCurrent = _currentPosition.ToLocalVector3(_mapCenter);
Debug.Log($"Local Current Position: {localCurrent}");
var heading = CalculateHeading(_lastSentPosition.ToLocalVector3(_mapCenter), localCurrent);
if (heading != null)
{
Debug.Log($"Heading: {heading}");
if (_player != null)
_player.transform.rotation = Quaternion.Euler(0, (float)heading, 0);
}
if (_player != null)
_player.transform.position = localCurrent;
try
{
if (_currentPosition != _lastSentPosition)
{
_gameClient.UpdatePosition(_currentPosition);
_lastSentPosition = _currentPosition;
}
TrySendCurrentPosition();
}
catch
{
@@ -150,105 +715,212 @@ namespace Subsystems
}
private double? CalculateHeading(Vector3 first, Vector3 second)
{
double? heading = null;
if ((first - second).magnitude == 0)
{
return null;
}
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
{
if ((first - second).magnitude < 0.0001f) return null;
float dx = second.x - first.x;
float dz = second.z - first.z;
float heading = Mathf.Atan2(dx, dz) * Mathf.Rad2Deg;
if (heading < 0) heading += 360f;
return heading;
}
}
IEnumerator InitiallizeGPS()
{
_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)
{
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 updateDistanceInMeters = 10f;
float desiredAccuracyInMeters = 5f;
float updateDistanceInMeters = 1f;
Input.location.Start(desiredAccuracyInMeters, updateDistanceInMeters);
// Waits until the location service initializes
int maxWait = 20;
int maxWait = _gpsInitTimeoutSeconds;
while (Input.location.status == LocationServiceStatus.Initializing && maxWait > 0)
{
yield return new WaitForSeconds(1);
maxWait--;
}
// If the service didn't initialize in 20 seconds this cancels location service use.
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;
Debug.LogError("Timed out");
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)
{
Debug.LogError("Unable to determine device location");
_lastGpsError = "Unity Input.location reported Failed status";
Debug.LogError("[GPS] " + _lastGpsError);
_GPSState = GPSState.Failed;
yield break;
}
else
{
// If the connection succeeded, this retrieves the device's current location and displays it in the Console window.
_currentPosition = new Position(Input.location.lastData.latitude, Input.location.lastData.longitude);
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);
_GPSState = GPSState.Running;
_gpsRetryCount = 0;
_coroutineHost.StartCoroutine(GPSService());
}
// Stops the location service if there is no need to query location updates continuously.
yield return _coroutineHost.StartCoroutine(GPSService());
#if UNITY_ANDROID && !UNITY_EDITOR
/// <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);
}
}
}
}

View File

@@ -5,7 +5,6 @@ using System.Collections.Generic;
using System.Globalization;
using UnityEditor;
using UnityEngine;
using UnityEngine.Localization.Pseudo;
using UnityEngine.UI;

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 6b37670de43269e4f984694475e75510
guid: 2230bf768ecb84610af77bea6cdd7074
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 2651bd22cf5764e4eb358f11641edca2
guid: 7142a58f80866aba3ae4ffeb79d5f67c
NativeFormatImporter:
externalObjects: {}
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

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 8356cb86749a3674299ae725d58c8012
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: a007c35d1a63dc3418a3afe3c7407450
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 8b4a331c3bc0f6445a0797f99cc38604
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 87c5ee97c7d1da345978bf431113faeb
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 17fc65d1a95feae42b8815a07ea2ffef
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 3f2ca619b4f5384489cf03c1dd13a701
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -193,13 +193,13 @@ Transform:
m_GameObject: {fileID: 210602716}
serializedVersion: 2
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_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1140131532}
- {fileID: 2102485167}
m_Father: {fileID: 1814274513}
m_Father: {fileID: 1790534262}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &331729628
GameObject:
@@ -1570,9 +1570,10 @@ RectTransform:
m_GameObject: {fileID: 1790534261}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
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_Children: []
m_Children:
- {fileID: 210602717}
m_Father: {fileID: 143293625}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
@@ -1650,9 +1651,8 @@ RectTransform:
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 143293625}
- {fileID: 210602717}
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_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}

View File

@@ -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:

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 58a0d1d70570e1340b7bde7edda96e9a
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: bd359125026cdc144a1f35a5c8607b45

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: d0a79df83e583b147b8f08cd4a8a96a8

View File

@@ -1,5 +1,6 @@
fileFormatVersion: 2
guid: 38f8393f8cca8f24bb21ca2ab3a28acc
guid: b9c8f9e5bbf063b4fb1152966129a495
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:

View File

@@ -1,5 +1,6 @@
fileFormatVersion: 2
guid: ae701dd46572ae44faa1c08754c677a1
guid: 0f6331bfef5b3a00cb6f84141e60f9c4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:

View File

@@ -1,5 +1,6 @@
fileFormatVersion: 2
guid: fbd0ba17ed8002141b6b82c678cd3baf
guid: 43d195e24415843378eb1b53e9e1da18
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData: