using Google.Maps.Coord;
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Events;
namespace Google.Maps.Examples.Shared {
///
/// Script for keeping keeping the 's viewport loaded at all times.
///
///
/// By default loads Melbourne, Australia. If a new latitude/longitude is set in Inspector (before
/// pressing start), will load new location instead.
///
[RequireComponent(typeof(MapsService))]
public sealed class DynamicMapsService : MonoBehaviour {
///< summary>Interval in seconds at which unseen geometry is detected and unloaded.
private const float UnloadUnseenDelay = 5f;
[Tooltip("LatLng to load (must be set before hitting play).")]
public LatLng LatLng = new LatLng(-37.8110057, 144.9601189);
[Tooltip(
"Maximum distance to render to (prevents loading massive amount of geometry if looking" +
"up at the horizon).")]
public float MaxDistance = 1000f;
[Tooltip(
"The ground plane. We keep this centered underneath Camera.main, so as we move around " +
"the game world the ground plane stays always underneath us. As such the Material " +
"applied to this ground plane should either be untextured, or textured using worldspace " +
"coordinates (as opposed to local uv coordinates), so that we cannot actually see the " +
"ground plane moving around the world, creating the illusion that there is always ground " +
"beneath us.")]
public GameObject Ground;
[Tooltip("Invoked when the map is initialized and has started loading.")]
public UnityEvent OnMapLoadStarted = new UnityEvent();
[Header("Read Only"), Tooltip("Is geometry currently being loaded?")]
public bool Loading;
/// The to use when rendering loaded
/// geometry. This value must be overriden before this script's function is called in order to render loaded geometry with a different set of
/// . If no change is made, will be used instead.
///
public GameObjectOptions RenderingStyles;
/// Required component.
///
/// This component is auto-found on first access (so this component can be accessed by an
/// external script at any time without a null-reference exception).
///
public MapsService MapsService {
get {
return _MapsService ?? (_MapsService = GetComponent());
}
}
///
/// Required component.
///
private MapsService _MapsService;
///
/// Position of last frame. We use this to see if the view has moved
/// this frame.
///
private Vector3 CameraPosition;
///
/// Euler angles of last frame. We use this to see if the view has
/// rotated this frame.
///
private Quaternion CameraRotation;
///
/// Do we need to restart coroutines when the component is next enabled?
///
private bool RestartCoroutinesOnEnable;
///
/// Handle to coroutine used to remove unneeded areas of the map.
///
private Coroutine UnloadUnseenCoroutine;
///
/// Setup this script if have not done so already.
///
private void Start() {
// Verify all required parameters are defined and correctly setup, skipping any further setup
// if any parameter is missing or invalid.
if (!VerifyParameters()) {
// Disable this script to prevent error spamming (where Update will producing one or more
// errors every frame because one or more parameters are undefined).
enabled = false;
return;
}
// Move the Ground plane to be directly underneath the main Camera. We do this again whenever
// the main Camera moves.
ReCenterGround();
// Set real-world location to load. Note that the MapsService variable is auto-found on first
// access.
MapsService.InitFloatingOrigin(LatLng);
// Make sure we have a set of GameObjectOptions to render loaded geometry with, using defaults
// if no specific set of options has been given. This allows a different set of options to be
// used, e.g. with Road Borders enabled, provided these new options are set into this
// parameter before this Start function is called.
if (RenderingStyles == null) {
RenderingStyles = ExampleDefaults.DefaultGameObjectOptions;
}
// Connect to Maps Service error event so we can be informed if an error occurs while trying
// to load tiles. However, if this GameObject also contains an Error Handling Component, then
// we skip handling errors here, leaving it to the Error Handling Component instead.
if (GetComponent() == null) {
MapsService.Events.MapEvents.LoadError.AddListener(args => {
if (args.Retry) {
Debug.LogWarning(args);
} else {
Debug.LogError(args);
}
});
}
// Revert loading flag to false whenever loading finishes (this flag is set to true whenever
// loading starts, and so it remain true until the currently requested geometry has finished
// loading).
MapsService.Events.MapEvents.Loaded.AddListener(args => Loading = false);
// Load the current viewport.
RefreshView();
// Now load map around the camera.
MapsService.MakeMapLoadRegion()
.AddCircle(new Vector3(CameraPosition.x, 0f, CameraPosition.z), MaxDistance)
.Load(RenderingStyles);
StartCoroutines();
if (OnMapLoadStarted != null) {
OnMapLoadStarted.Invoke();
}
}
///
/// Start any coroutines needed by this component.
///
private void StartCoroutines() {
// Run a coroutine to clean up unseen objects.
UnloadUnseenCoroutine = StartCoroutine(UnloadUnseen());
}
///
/// Handle Unity OnDisable event.
///
private void OnDisable() {
if (UnloadUnseenCoroutine != null) {
StopCoroutine(UnloadUnseenCoroutine);
UnloadUnseenCoroutine = null;
RestartCoroutinesOnEnable = true;
}
}
///
/// Handle Unity OnEnable event.
///
private void OnEnable() {
if (RestartCoroutinesOnEnable) {
StartCoroutines();
RestartCoroutinesOnEnable = false;
}
}
///
/// Check if the main Camera has moved or rotated each frame, recentering the ground-plane and
/// refreshing the viewed area as required.
///
private void Update() {
// If the main Camera has moved this frame, we re-center the ground-plane underneath it, and
// refresh the part of the world the moved main Camera now sees.
if (Camera.main.transform.position != CameraPosition) {
ReCenterGround();
RefreshView();
return;
}
// If the main Camera has not moved, but has rotated this frame, we refresh the part of the
// world the rotated main Camera now sees.
if (Camera.main.transform.rotation != CameraRotation) {
RefreshView();
}
}
///
/// Recenter the Floating Origin based on a given 's position.
///
/// This allows the world to be periodically recentered back to the origin, which avoids
/// geometry being created with increasingly large floating point coordinates, ultimately
/// resulting in floating point rounding errors.
///
///
/// to use to recenter world.
///
/// This will be moved until it is over the origin (0f, 0f, 0f). At the
/// same time all geometry created by the will be moved the same
/// amount. The end result is that the world is recentered over the origin, with the change
/// being unnoticeable to the player.
///
internal Vector3 RecenterWorld(Camera camera) {
// The Camera's current position is given to the MoveFloatingOrigin function, along with the
// Camera itself, so that the world and the Camera can all be moved until the Camera is over
// the origin again. Note that the MoveFloatingOrigin function automatically moves all loaded
// geometry, so the only extra geometry that needs moving is the given camera (given as the
// second, optional parameter of this function).
return MapsService.MoveFloatingOrigin(camera.transform.position, new[] { camera.gameObject });
}
///
/// Move the ground plane directly underneath the .
///
private void ReCenterGround() {
// Store the position of the main Camera, so we can check next frame if the main Camera has
// moved, and thus if the ground plane needs to be recentered.
CameraPosition = Camera.main.transform.position;
Ground.transform.position = new Vector3(CameraPosition.x, 0f, CameraPosition.z);
}
///
/// Reload the world, making sure the area can see it loaded.
///
private void RefreshView() {
// Store the rotation of the camera, so we can check next frame if the main Camera has
// rotated, and thus if the visible world should be refreshed again.
CameraRotation = Camera.main.transform.rotation;
float height = Camera.main.transform.position.y;
// Flag that we are now loading geometry.
Loading = true;
// Load the visible map region. The range is increased based on the height of the camera
// to ensure we have a circle of radius MaxDistance on the ground.
float maxDistance = (float) Math.Sqrt(Math.Pow(height, 2) + Math.Pow(MaxDistance, 2));
MapsService.MakeMapLoadRegion()
.AddViewport(Camera.main, maxDistance)
.Load(RenderingStyles);
}
/// Periodically remove unneeded areas of the map.
private IEnumerator UnloadUnseen() {
while (true) {
// Unload map regions that are not in viewport, and are outside a radius around the camera.
// This is to avoid unloading geometry that may be reloaded again very shortly (as it is
// right on the edge of the view).
MapsService.MakeMapLoadRegion()
.AddViewport(Camera.main, MaxDistance)
.AddCircle(new Vector3(CameraPosition.x, 0f, CameraPosition.z), MaxDistance)
.UnloadOutside();
// Wait for a preset interval before seeing if new geometry needs to be unloaded.
yield return new WaitForSeconds(UnloadUnseenDelay);
}
}
///
/// Verify that all required parameters have been correctly defined, returning false if not.
///
private bool VerifyParameters() {
// TODO(b/149056787): Standardize parameter verification across scripts.
// Verify that a Ground plane has been given.
if (Ground == null) {
Debug.LogError(ExampleErrors.MissingParameter(this, Ground, "Ground"));
return false;
}
// Verify that there is a Camera.main in the scene (i.e. a Camera that is tagged:
// "MainCamera").
if (Camera.main == null) {
Debug.LogError(ExampleErrors.NullMainCamera(this));
return false;
}
// If have reached this point then we have verified all required parameters.
return true;
}
}
}