DynamicMapsService.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. using Google.Maps.Coord;
  2. using System;
  3. using System.Collections;
  4. using UnityEngine;
  5. using UnityEngine.Events;
  6. namespace Google.Maps.Examples.Shared {
  7. /// <summary>
  8. /// Script for keeping keeping the <see cref="Camera.main"/>'s viewport loaded at all times.
  9. /// </summary>
  10. /// <remarks>
  11. /// By default loads Melbourne, Australia. If a new latitude/longitude is set in Inspector (before
  12. /// pressing start), will load new location instead.
  13. /// </remarks>
  14. [RequireComponent(typeof(MapsService))]
  15. public sealed class DynamicMapsService : MonoBehaviour {
  16. ///< summary>Interval in seconds at which unseen geometry is detected and unloaded.</summary>
  17. private const float UnloadUnseenDelay = 5f;
  18. [Tooltip("LatLng to load (must be set before hitting play).")]
  19. public LatLng LatLng = new LatLng(-37.8110057, 144.9601189);
  20. [Tooltip(
  21. "Maximum distance to render to (prevents loading massive amount of geometry if looking" +
  22. "up at the horizon).")]
  23. public float MaxDistance = 1000f;
  24. [Tooltip(
  25. "The ground plane. We keep this centered underneath Camera.main, so as we move around " +
  26. "the game world the ground plane stays always underneath us. As such the Material " +
  27. "applied to this ground plane should either be untextured, or textured using worldspace " +
  28. "coordinates (as opposed to local uv coordinates), so that we cannot actually see the " +
  29. "ground plane moving around the world, creating the illusion that there is always ground " +
  30. "beneath us.")]
  31. public GameObject Ground;
  32. [Tooltip("Invoked when the map is initialized and has started loading.")]
  33. public UnityEvent OnMapLoadStarted = new UnityEvent();
  34. [Header("Read Only"), Tooltip("Is geometry currently being loaded?")]
  35. public bool Loading;
  36. /// <summary>The <see cref="GameObjectOptions"/> to use when rendering loaded
  37. /// geometry.</summary> <remarks> This value must be overriden before this script's <see
  38. /// cref="Start"/> function is called in order to render loaded geometry with a different set of
  39. /// <see cref="GameObjectOptions"/>. If no change is made, <see
  40. /// cref="ExampleDefaults.DefaultGameObjectOptions"/> will be used instead.
  41. /// </remarks>
  42. public GameObjectOptions RenderingStyles;
  43. /// <summary>Required <see cref="MapsService"/> component.</summary>
  44. /// <remarks>
  45. /// This component is auto-found on first access (so this component can be accessed by an
  46. /// external script at any time without a null-reference exception).
  47. /// </remarks>
  48. public MapsService MapsService {
  49. get {
  50. return _MapsService ?? (_MapsService = GetComponent<MapsService>());
  51. }
  52. }
  53. /// <summary>
  54. /// Required <see cref="MapsService"/> component.
  55. /// </summary>
  56. private MapsService _MapsService;
  57. /// <summary>
  58. /// Position of <see cref="Camera.main"/> last frame. We use this to see if the view has moved
  59. /// this frame.
  60. /// </summary>
  61. private Vector3 CameraPosition;
  62. /// <summary>
  63. /// Euler angles of <see cref="Camera.main"/> last frame. We use this to see if the view has
  64. /// rotated this frame.
  65. /// </summary>
  66. private Quaternion CameraRotation;
  67. /// <summary>
  68. /// Do we need to restart coroutines when the component is next enabled?
  69. /// </summary>
  70. private bool RestartCoroutinesOnEnable;
  71. /// <summary>
  72. /// Handle to coroutine used to remove unneeded areas of the map.
  73. /// </summary>
  74. private Coroutine UnloadUnseenCoroutine;
  75. /// <summary>
  76. /// Setup this script if have not done so already.
  77. /// </summary>
  78. private void Start() {
  79. // Verify all required parameters are defined and correctly setup, skipping any further setup
  80. // if any parameter is missing or invalid.
  81. if (!VerifyParameters()) {
  82. // Disable this script to prevent error spamming (where Update will producing one or more
  83. // errors every frame because one or more parameters are undefined).
  84. enabled = false;
  85. return;
  86. }
  87. // Move the Ground plane to be directly underneath the main Camera. We do this again whenever
  88. // the main Camera moves.
  89. ReCenterGround();
  90. // Set real-world location to load. Note that the MapsService variable is auto-found on first
  91. // access.
  92. MapsService.InitFloatingOrigin(LatLng);
  93. // Make sure we have a set of GameObjectOptions to render loaded geometry with, using defaults
  94. // if no specific set of options has been given. This allows a different set of options to be
  95. // used, e.g. with Road Borders enabled, provided these new options are set into this
  96. // parameter before this Start function is called.
  97. if (RenderingStyles == null) {
  98. RenderingStyles = ExampleDefaults.DefaultGameObjectOptions;
  99. }
  100. // Connect to Maps Service error event so we can be informed if an error occurs while trying
  101. // to load tiles. However, if this GameObject also contains an Error Handling Component, then
  102. // we skip handling errors here, leaving it to the Error Handling Component instead.
  103. if (GetComponent<ErrorHandling>() == null) {
  104. MapsService.Events.MapEvents.LoadError.AddListener(args => {
  105. if (args.Retry) {
  106. Debug.LogWarning(args);
  107. } else {
  108. Debug.LogError(args);
  109. }
  110. });
  111. }
  112. // Revert loading flag to false whenever loading finishes (this flag is set to true whenever
  113. // loading starts, and so it remain true until the currently requested geometry has finished
  114. // loading).
  115. MapsService.Events.MapEvents.Loaded.AddListener(args => Loading = false);
  116. // Load the current viewport.
  117. RefreshView();
  118. // Now load map around the camera.
  119. MapsService.MakeMapLoadRegion()
  120. .AddCircle(new Vector3(CameraPosition.x, 0f, CameraPosition.z), MaxDistance)
  121. .Load(RenderingStyles);
  122. StartCoroutines();
  123. if (OnMapLoadStarted != null) {
  124. OnMapLoadStarted.Invoke();
  125. }
  126. }
  127. /// <summary>
  128. /// Start any coroutines needed by this component.
  129. /// </summary>
  130. private void StartCoroutines() {
  131. // Run a coroutine to clean up unseen objects.
  132. UnloadUnseenCoroutine = StartCoroutine(UnloadUnseen());
  133. }
  134. /// <summary>
  135. /// Handle Unity OnDisable event.
  136. /// </summary>
  137. private void OnDisable() {
  138. if (UnloadUnseenCoroutine != null) {
  139. StopCoroutine(UnloadUnseenCoroutine);
  140. UnloadUnseenCoroutine = null;
  141. RestartCoroutinesOnEnable = true;
  142. }
  143. }
  144. /// <summary>
  145. /// Handle Unity OnEnable event.
  146. /// </summary>
  147. private void OnEnable() {
  148. if (RestartCoroutinesOnEnable) {
  149. StartCoroutines();
  150. RestartCoroutinesOnEnable = false;
  151. }
  152. }
  153. /// <summary>
  154. /// Check if the main Camera has moved or rotated each frame, recentering the ground-plane and
  155. /// refreshing the viewed area as required.
  156. /// </summary>
  157. private void Update() {
  158. // If the main Camera has moved this frame, we re-center the ground-plane underneath it, and
  159. // refresh the part of the world the moved main Camera now sees.
  160. if (Camera.main.transform.position != CameraPosition) {
  161. ReCenterGround();
  162. RefreshView();
  163. return;
  164. }
  165. // If the main Camera has not moved, but has rotated this frame, we refresh the part of the
  166. // world the rotated main Camera now sees.
  167. if (Camera.main.transform.rotation != CameraRotation) {
  168. RefreshView();
  169. }
  170. }
  171. /// <summary>
  172. /// Recenter the Floating Origin based on a given <see cref="Camera"/>'s position.
  173. /// <para>
  174. /// This allows the world to be periodically recentered back to the origin, which avoids
  175. /// geometry being created with increasingly large floating point coordinates, ultimately
  176. /// resulting in floating point rounding errors.
  177. /// </para></summary>
  178. /// <param name="camera">
  179. /// <see cref="Camera"/> to use to recenter world.
  180. /// <para>
  181. /// This <see cref="Camera"/> will be moved until it is over the origin (0f, 0f, 0f). At the
  182. /// same time all geometry created by the <see cref="MapsService"/> will be moved the same
  183. /// amount. The end result is that the world is recentered over the origin, with the change
  184. /// being unnoticeable to the player.
  185. /// </para></param>
  186. internal Vector3 RecenterWorld(Camera camera) {
  187. // The Camera's current position is given to the MoveFloatingOrigin function, along with the
  188. // Camera itself, so that the world and the Camera can all be moved until the Camera is over
  189. // the origin again. Note that the MoveFloatingOrigin function automatically moves all loaded
  190. // geometry, so the only extra geometry that needs moving is the given camera (given as the
  191. // second, optional parameter of this function).
  192. return MapsService.MoveFloatingOrigin(camera.transform.position, new[] { camera.gameObject });
  193. }
  194. /// <summary>
  195. /// Move the ground plane directly underneath the <see cref="Camera.main"/>.
  196. /// </summary>
  197. private void ReCenterGround() {
  198. // Store the position of the main Camera, so we can check next frame if the main Camera has
  199. // moved, and thus if the ground plane needs to be recentered.
  200. CameraPosition = Camera.main.transform.position;
  201. Ground.transform.position = new Vector3(CameraPosition.x, 0f, CameraPosition.z);
  202. }
  203. /// <summary>
  204. /// Reload the world, making sure the area <see cref="Camera.main"/> can see it loaded.
  205. /// </summary>
  206. private void RefreshView() {
  207. // Store the rotation of the camera, so we can check next frame if the main Camera has
  208. // rotated, and thus if the visible world should be refreshed again.
  209. CameraRotation = Camera.main.transform.rotation;
  210. float height = Camera.main.transform.position.y;
  211. // Flag that we are now loading geometry.
  212. Loading = true;
  213. // Load the visible map region. The range is increased based on the height of the camera
  214. // to ensure we have a circle of radius MaxDistance on the ground.
  215. float maxDistance = (float) Math.Sqrt(Math.Pow(height, 2) + Math.Pow(MaxDistance, 2));
  216. MapsService.MakeMapLoadRegion()
  217. .AddViewport(Camera.main, maxDistance)
  218. .Load(RenderingStyles);
  219. }
  220. /// <summary>Periodically remove unneeded areas of the map.</summary>
  221. private IEnumerator UnloadUnseen() {
  222. while (true) {
  223. // Unload map regions that are not in viewport, and are outside a radius around the camera.
  224. // This is to avoid unloading geometry that may be reloaded again very shortly (as it is
  225. // right on the edge of the view).
  226. MapsService.MakeMapLoadRegion()
  227. .AddViewport(Camera.main, MaxDistance)
  228. .AddCircle(new Vector3(CameraPosition.x, 0f, CameraPosition.z), MaxDistance)
  229. .UnloadOutside();
  230. // Wait for a preset interval before seeing if new geometry needs to be unloaded.
  231. yield return new WaitForSeconds(UnloadUnseenDelay);
  232. }
  233. }
  234. /// <summary>
  235. /// Verify that all required parameters have been correctly defined, returning false if not.
  236. /// </summary>
  237. private bool VerifyParameters() {
  238. // TODO(b/149056787): Standardize parameter verification across scripts.
  239. // Verify that a Ground plane has been given.
  240. if (Ground == null) {
  241. Debug.LogError(ExampleErrors.MissingParameter(this, Ground, "Ground"));
  242. return false;
  243. }
  244. // Verify that there is a Camera.main in the scene (i.e. a Camera that is tagged:
  245. // "MainCamera").
  246. if (Camera.main == null) {
  247. Debug.LogError(ExampleErrors.NullMainCamera(this));
  248. return false;
  249. }
  250. // If have reached this point then we have verified all required parameters.
  251. return true;
  252. }
  253. }
  254. }