A Guide to Using Google’s New Maps SDK for Unity 3D - Part 2

Part 2: Map Scene Setup

Kristopher C. Tobiasson
Everdevs Community

--

Map vector created by freepikwww.freepik.com

Note 1: The Maps Sdk from Google is now deprecated ⚠️. This is the second part of this guide. To go through the initial setup of the Google Maps SDK for Unity, please refer to “Part 1: Project Setup” here.

Note 2: This tutorial contains C# code snippets. In order to get the most out of this tutorial, it is recommended that you have, at least, basic knowledge in C# and Unity 3D development.

Although the project setup from the previous tutorial was fairly basic and perhaps even boring, it was a necessary obstacle to overcome. Without first getting an API key, google wouldn’t have served us any map data at all. In fact, we would have had numerous errors in the console had we not provided the API key to the “Maps Service” component before pressing play. The API key is how we identify ourselves with google so they know who to charge when we start getting roughly 20,000+ daily active users per month. So you can understand why they wouldn’t just blindly serve us data without first knowing who we are.

Moving on…

Create the Map Scene and Populate It With Objects:

Now that we have the Maps SDK playing nice with Unity, it’s time we go ahead and setup a custom scene. In the “Project” panel of Unity, open up the “Scenes” folder at the root of the Assets directory and rename “Sample Scene” to “Map”.

Create custom Map scene

In the “Hierarchy” panel, do the following:

  • Create 2 empty GameObjects and name one “GameWorld” and name the other “MapManager”.
  • Set the MainCamera and the DirectionalLight as children of the GameWorld object.
  • Create 2 more empty GameObjects and set them as children of the GameWorld object. Name these 2 new objects “Ground” and “MobileDevice”.

Your Hierarchy should now look like the image below.

Start populating the hierarchy

Select the MainCamera and change the Far field of the Clipping Planes to 5000. Don’t worry, we are not going to be loading 5 km of map data, we do this to ensure that the ground plane will not be clipped in the distance. Depending on your camera angle, a tightly clipped ground plane tends to be visually unpleasant if you get my drift.

Change Clipping Planes Far to 5000

In the Assets folder of your Project, create a new folder named “Materials”. Within the Materials folder, create a new material named “GroundPlane”. Select the “BaseMap Color” shader in the dropdown menu of the newly created material (the shader path is Google/Maps/Shaders/BaseMap Color).

Create GroundPlane material

Select the Ground object in the hierarchy and do the following:

  • Add a MeshFilter: Select a regular plane as the target mesh.
  • Add a MeshRenderer: Select the GroundPlane material we created as the target material.
  • Add a SortingGroup: Sorting Layer set to Default and Order in Layer set to -32768 (this is so as the ground plane does not render over top of any of the dynamically loaded objects of the map since the MapsService Min Basemap Sorting Order is -32767 by default).
  • Set the Transform Scale: (1000, 1, 1000)

The inspector of the Ground object should now look like the image below.

Setup the Ground

Select the MapManager object in the hierarchy and do the following:

  • Add a DeviceCountryProvider.
  • Add a MapsService: Include your API key in the API key field and drag the DeviceCountryProvider to the Country Provider field. Under Map Feature Options, open Road Lattice and select Enable Road Lattice and then select Enable Intersections above. Leave the default values on the rest of the component.

The MapsService Component:

The MapsService component is where most of the magic happens in the SDK. You can find an introduction to the component in the documentation here, but I’ll go ahead and give a little explanation on some properties worth noting:

  • API Key: We already know about this one. This is where we paste our API key that we got in the previous tutorial and it allows us to identify ourselves with google.
  • Zoom Level: Pretty self explanatory here. The zoom level of the map needs to be chosen before you run the game. You cannot change the zoom level of a map dynamically at runtime. To achieve multiple zoom levels, you would have to create multiple MapsService instances, each with a different zoom level. However, there are a couple built in components (MapLoader and MixedZoom) which can help in that department (check Assets/GoogleMaps/Examples/04_Advanced/MixedZoom scene for an example).
  • Country Provider: Let’s just leave this to the DeviceCountryProvider component. It does exactly as you would expect and there is no need to recreate the wheel.
  • Map Preview Options: These settings allow you to preview the map at edit time. Be careful though, you could unintentionally make changes to your project with this setting as the warning when you try to enable it suggests. However, it can make design workflow a bit easier at times.
  • Map Feature Options: For enabling all the necessary map features your scene needs. My features of choice are Modeled Structures, Intersections and Road Lattice. Modeled Structures (Statue of Liberty, Eiffel Tower, etc.) and Intersections are great for giving users a sense of realism while they use your app. Note that you must enable Road Lattice in order to use Intersections.
  • Events: The events which are fired by the SDK when loading the map and its features. This page of the documentation covers events well. For GameObject creation, WillCreate events are fired right after a MapFeature is created, but before the corresponding GameObject is created. DidCreate events are fired after the GameObject has been created and is a part of the scene.

The other properties can generally be left alone unless you have a specific use case which calls for modifying them.

The MapManager inspector should currently look like the following image:

MapManager inspector

Tracking a Mobile Device on the Map:

Tracking a mobile device and projecting its position on the map sounds a bit daunting at first if you’ve never done it before, but the Maps SDK has some handy functions which can help smooth out the process.

Do the following in the Project panel:

  • Create a new folder in the Assets directory of your project and name it “Scripts”.
  • Create a new folder inside the Scripts folder and name it “MobileDevice”.
  • Create 2 new scripts inside the MobileDevice folder and name one of them “MobileDeviceEditorController”. Name the second script “MobileDeviceLocationFollower”.
MobileDevice scripts

Open the MobileDeviceEditorController and paste the following code:

using UnityEngine;namespace TestApp.MobileDevice
{
public class MobileDeviceEditorController : MonoBehaviour
{
float _speed = 20.0f;

void Update()
{
float _step = Time.deltaTime * _speed;

if(Input.GetKey(KeyCode.UpArrow))
transform.Translate(transform.forward * _step, Space.World);
if(Input.GetKey(KeyCode.DownArrow))
transform.Translate(-transform.forward * _step, Space.World );
if(Input.GetKey(KeyCode.LeftArrow))
transform.Rotate( 0, -3, 0 );
if(Input.GetKey(KeyCode.RightArrow))
transform.Rotate( 0, 3, 0 );
}
}
}

Those of you who are unhappy that I’m not using comments, I am very sorry, but I hate comments as they usually just distract me, look messy and are unnecessary if you use descriptive naming conventions.

Although this is a fairly basic script, here is a quick run down of what is happening and why we would use it. As the name suggests, this is for mocking the position and vertical rotation of the MobileDevice in the editor. As shown in the code, the up arrow key and the down arrow key move the transform this script is attached to forward and backwards respectively. The left and right arrow keys rotate the transform clockwise and counterclockwise respectively.

Open the MobileDeviceLocationFollower and paste the following code:

using System.Collections;
using System.Globalization;
using UnityEngine;
using Google.Maps;
using Google.Maps.Coord;
#if UNITY_ANDROID
using UnityEngine.Android;
#endif
namespace TestApp.MobileDevice
{
public class MobileDeviceLocationFollower : MonoBehaviour
{
[SerializeField] private MapsService _mapsService;
[SerializeField] private LatLng _latLng;
private bool _isPaused = false; private Vector3 _pausedPosition;
private Vector3 _north;
private void Start()
{
GetPermissions();
StartCoroutine(Follow());
}

private IEnumerator Follow()
{
#if UNITY_IOS
Input.location.Start();
#endif
#if UNITY_EDITOR
gameObject.AddComponent<MobileDeviceEditorController>();
Debug.Log( "Mocking location in editor. Origin starting at MapService Location from Map Preview Options" );
#else
while (!Input.location.isEnabledByUser)
{
Debug.Log("Location services not enabled..");
yield return new WaitForSeconds(1f);
}
Debug.Log("Location services enabled.");
#endif
#if !UNITY_IOS
Input.location.Start();
#endif
#if !UNITY_EDITOR
Input.compass.enabled = true;
while (true)
{
if (Input.location.status == LocationServiceStatus.Initializing)
{
yield return new WaitForSeconds(1f);
}
else if (Input.location.status == LocationServiceStatus.Failed)
{
Debug.LogError("Location Services failed to start.");
yield break;
}
else if (Input.location.status == LocationServiceStatus.Running)
{
break;
}
}
_latLng = new LatLng(Input.location.lastData.latitude, Input.location.lastData.longitude);
#endif
yield return new WaitWhile(() => _mapsService == null); _mapsService.Events.MapEvents.Loaded.AddListener(OnMapLoaded);

_mapsService.InitFloatingOrigin(_latLng);
_mapsService.LoadMap(
new Bounds(Vector3.zero, new Vector3(1000, 1, 1000)),
Google.Maps.Examples.Shared.ExampleDefaults.DefaultGameObjectOptions
);
}

private void OnMapLoaded(Google.Maps.Event.MapLoadedArgs _args)
{
LatLng _ll = GetLatLng();
LatLng _ll2 = new LatLng( _ll.Lat + 0.0005, _ll.Lng );
_north = GetPosition(_ll2) - GetPosition(_ll);
CheckIfAtWorldOrigin();
#if !UNITY_EDITOR
StartCoroutine(UpdateDevice());
#endif
}
private IEnumerator UpdateDevice()
{
while(true)
{
Vector3 _newForward = Quaternion.AngleAxis(Input.compass.trueHeading, Vector3.up) * _north;
transform.rotation = Quaternion.LookRotation(_newForward, Vector3.up);
transform.position = Position();
yield return new WaitForEndOfFrame();
}
}
private void CheckIfAtWorldOrigin()
{
if( DistanceFromOther(new LatLng(0.0, 0.0)) > 5 )
{
//The user does not have location settings on.
//This could mean that the location permission is
//granted to the app, but the general location services is off.
//What should we do??
}
}
public float DistanceFromOther(LatLng _latLong)
{
return Vector3.Distance(
GetPosition(_latLong),
Position()
);
}
public float DistanceFromOther(string _latLongStr)
{
string[] _latLongArr = _latLongStr.Split(',');
return DistanceFromOther(
new LatLng(
double.Parse(_latLongArr[0].Trim(), new CultureInfo("en-US")),
double.Parse(_latLongArr[1].Trim(), new CultureInfo("en-US"))
)
);
}
public LatLng GetLatLng()
{
LatLng _latLng;
#if UNITY_EDITOR
_latLng = _mapsService.Projection.FromVector3ToLatLng(transform.position);
#else
_latLng = new LatLng(
Input.location.lastData.latitude,
Input.location.lastData.longitude
);
#endif
return _latLng;
}
private Vector3 GetPosition(LatLng _latLng)
{
return _mapsService.Projection.FromLatLngToVector3(_latLng);
}
public Vector3 Position()
{
return GetPosition(GetLatLng());
}
private void OnApplicationPause( bool _pauseStatus )
{
if( !_isPaused && _pauseStatus )
{
_isPaused = true;
_pausedPosition = transform.localPosition;
}
else if( _isPaused && !_pauseStatus )
{
StartCoroutine( CheckLocationVarianceDuringPause() );
_isPaused = false;
}
}
private IEnumerator CheckLocationVarianceDuringPause()
{
yield return null;
if( Vector3.Distance( _pausedPosition, Position() ) > 10 )
{
//The GPS offset is more than 10 meters from the paused position,
//this likely happened because the user paused and came back after
//moving the said distance
}
}
private void GetPermissions()
{
#if UNITY_ANDROID
if(!Permission.HasUserAuthorizedPermission(Permission.FineLocation))
Permission.RequestUserPermission(Permission.FineLocation);
#endif
}
}
}

Ok, you got me, I’m using comments, but just to show where temporary code should go and to give an explanation about what is going on in some cases because the script is a bit longer.

The MobileDeviceLocationFollower class above is used get the users device location and translate it into a Vector3 position in our game world in Unity. I’ve added a few helper functions inside the class which are helpful for most use cases. Here is a rundown of how the class works:

  • In the Start() method we call GetPermissions(); which tests to see if the user has location services enabled for the app and if the location services are not enabled, we request for the user to allow the app to access their location. This is just valid for Android devices since iOS automatically asks the user for us if the location services are not enabled when we call Input.location.Start();
  • After getting permission from Android users, we start the Follow() coroutine where we immediately call Input.location.Start(); if our user has an iOS device to make sure we get the permission we need before continuing and also to startup the GPS on iOS.
  • If we are working in the Unity Editor then we add the MobileDeviceEditorController gameObject.AddComponent<MobileDeviceEditorController>(); , otherwise, we wait until location services have been enabled.
  • Now we startup the GPS on Android and then we enable the compass so we know which way the device is heading Input.compass.enabled = true;
  • We then wait for GPS to start running if(Input.location.status == LocationServiceStatus.Running) or for it to fail if(Input.location.status == LocationServiceStatus.Failed) , but let’s try to be positive here.
  • Depending on what you have going on in your scene, the Maps Service can be null, so we wait until it isn’t null yield return new WaitWhile(() => _mapsService == null); before we add a listener to the map loaded event _mapsService.Events.MapEvents.Loaded.AddListener(OnMapLoaded);
  • The InitFloatingOrigin call, _mapsService.InitFloatingOrigin(_latLng); , tells Maps Service to initiate from a particular latitude/longitude real world position and the origin of the game world in Unity will take on this position. I set mine to Sagrada Familia in Barcelona, Spain in the inspector 41.404209, 2.175686.
  • The LoadMap call, _mapsService.LoadMap() , is self explanatory. I just passed the bounds as new Bounds(Vector3.zero, new Vector3(1000, 1, 1000)) so we have 1km * 1km of map and then I passed the DefaultGameObjectOptions of the SDK, Google.Maps.Examples.Shared.ExampleDefaults.DefaultGameObjectOptions to get some ready made styles in the scene.
  • When OnMapLoaded() finally gets called, we get our current latitude/longitude and then we get the north direction as a Vector3. We then call CheckIfAtWorldOrigin() ( this checks against the real world origin ). Finally, we start the UpdateDevice() coroutine if we are not in the Unity Editor.
  • The UpdateDevice() coroutine just keeps updating the MobileDevice game object position and rotation to match that of the device in the real world until the scene is unloaded.
  • The CheckIfAtWorldOrigin() method is a helper script that I included because the real world origin is off the west coast of Africa in the middle of the Atlantic ocean and it is highly unlikely that your user is there. If the user is registering world origin coordinates, it means that, although they may have the location services enabled for the app, the global location service settings for the phone my be off. You can decide what to do in this case.
  • The DistanceFromOther() methods are helpers for getting the distance as a float from the LatLong/string passed in as a parameter to the current LatLong of the MobileDevice.
  • The GetLatLng() method is self explanatory. It gets the LatLng of the user device unless we are in the Unity Editor, in which case it get it from converting the Vector3 position into a LatLng.
  • GetPosition() and Position() both return the Vector3 position of the MobileDevice.
  • We include OnApplicationPause() in the script so as to know how far the user has travelled while the game was paused, which can be helpful in some use cases where we need to adjust things depending on the game state prior to pausing the game.
  • Finally we have CheckLocationVarianceDuringPause() which allows us to act if the user moved too much during the pause game for example.

Now select the MobileDevice in the hierarchy and do the following:

  • add a MobileDeviceLocationFollower: Drag the MapManager object into the Maps Service field and add Sagrada Familia’s coordinates (41.404209, 2.175686) in the Lat Lng slot.
  • Add a MeshFilter: Set the Mesh target to a “Cube”.
  • Add a MeshRenderer: Use the Default-Diffuse material.

The MobileDevice inspector should now look like the following image:

MobileDevice Inspector

Now you can make the MainCamera a child of the MobileDevice and position it a short distance behind the MobileDevice and cross your fingers while you press play…

What do you know, it worked!! Or it should have if you were following along attentively. I bet you never thought you would be playing around with a cube in-front of the Sagrada Familia today!! Gaudí would be proud…

Sagrada Familia and our Cube

In the next tutorial of this guide, I will walk through how to customize map styles and modify the Map Features when the respective Maps Service events are fired.

Ready to head over to the next tutorial? Part3: Map Customization

I hope you found this tutorial helpful. Feel free to leave feedback in the comments below.

--

--

Kristopher C. Tobiasson
Everdevs Community

Professional Freelance Unity 3D / Full Stack NodeJS Web Developer | Founder of Everdevs.com | Diehard Dad