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

Part 3: Map Customization

Kristopher C. Tobiasson
Everdevs Community

--

Tree vector created by macrovectorwww.freepik.com

Note 1: The Maps Sdk from Google is now deprecated ⚠️ . This is the third part of this guide. It is assumed that you have completed Part 1: Project Setup and Part 2: Map Scene Setup starting this tutorial.

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.

In the previous tutorial, we created a new map scene in Unity and populated it with some GameObjects. We learned that the Maps Service component is where the bulk of the magic happens in the SDK. As Google says, “The MapsService class serves as the entry point for interacting with the Maps SDK for Unity.”

Lastly, we created the MobileDeviceLocationFollower class which allows us to track the location of a mobile device in the real world and project its position onto the map of our game world. We also included a helper class, MobileDeviceEditorController, which enables us to mock the movement and vertical rotation of our MobileDevice object while we are in the Unity Editor. Then we did a little test run in front of the Sagrada Familia Cathedral in Barcelona, Spain.

Time to get styling!!

Styling the Map:

If you were paying attention in the previous tutorial, you would have noticed that when we loaded the map in the MobileDeviceLocationFollower, we passed ExampleDefaults.DefaultGameObjectOptions as the second parameter. That created our map’s regional styling for us using the default styles of the shared SDK examples. That’s great, but it’s likely that you’re going to want a bit more control over how your map looks. Let’s create a new script which will help us get our hands dirty with the design.

Open your scripts folder and do the following:

  • add a new subfolder called “MapStyles”.
  • Inside the MapStyles folder, add a new script called “MapFeaturesController”.
What the scripts folder should look like.

Open the newly created class and paste the following code: (Sorry, the code formatting is impossible to get perfect in Medium)

using Google.Maps;
using Google.Maps.Event;
using Google.Maps.Feature;
using Google.Maps.Feature.Style;
using Google.Maps.Unity.Intersections;
using UnityEngine;
using System.Collections.Generic;
namespace TestApp.MapFeatures
{
public class MapFeaturesController: MonoBehaviour
{
[SerializeField] private MapsService _mapsService;
[Header("Materials")] [SerializeField] private Material _intersectionMaterial;
[SerializeField] private Material _roadMaterial;
[SerializeField] private Material _roadBorderMaterial;
[SerializeField] private Material _waterMaterial;
[SerializeField] private Material _beachMaterial;
[SerializeField] private Material _grassMaterial;
[SerializeField] private Material _unspecifiedRegionMaterial;
[SerializeField] private Material _modeledStructureMaterial;
[SerializeField] private Material _defaultBuildingWallMaterial;
[SerializeField] private Material _defaultBuildingRoofMaterial;
private float _roadWidth = 4; [Header("Region Togglers")][SerializeField] private bool _showExtrudedStructures = true;
[SerializeField] private bool _showModeledStructures = true;
[SerializeField] private bool _showRegions = true;
[SerializeField] private bool _showSegments = true;
[SerializeField] private bool _showAreaWater = true;
[SerializeField] private bool _showLineWater = true;
[SerializeField] private bool _showIntersections = true;
public static GameObjectOptions DefaultGameObjectOptions; private RegionStyle _defaultRegionStyle;
private SegmentStyle _defaultSegmentStyle;
private AreaWaterStyle _defaultAreaWaterStyle;
private LineWaterStyle _defaultLineWaterStyle;
private ModeledStructureStyle _defaultModeledStructureStyle;
private ExtrudedStructureStyle _defaultExtrudedStructureStyle;
private void Awake()
{
if (_mapsService == null)
Debug.Log("Maps Service is required for this script to work.");
_defaultRegionStyle = new RegionStyle.Builder {
FillMaterial = _grassMaterial,
}.Build();
_defaultSegmentStyle = new SegmentStyle.Builder {
Material = _roadMaterial,
Width = _roadWidth,
BorderMaterial = _roadBorderMaterial,
BorderWidth = 0.6f
}.Build();
_defaultAreaWaterStyle = new AreaWaterStyle.Builder {
FillMaterial = _waterMaterial
}.Build();
_defaultLineWaterStyle = new LineWaterStyle.Builder {
Material = _waterMaterial
}.Build();
_defaultModeledStructureStyle = new ModeledStructureStyle.Builder {
Material = _modeledStructureMaterial
}.Build();
_defaultExtrudedStructureStyle = new ExtrudedStructureStyle.Builder {
WallMaterial = _defaultBuildingWallMaterial,
RoofMaterial = _defaultBuildingRoofMaterial
}.Build();
DefaultGameObjectOptions = new GameObjectOptions {
ExtrudedStructureStyle = _defaultExtrudedStructureStyle,
ModeledStructureStyle = _defaultModeledStructureStyle,
RegionStyle = _defaultRegionStyle,
AreaWaterStyle = _defaultAreaWaterStyle,
LineWaterStyle = _defaultLineWaterStyle,
SegmentStyle = _defaultSegmentStyle,
};
}
private void OnEnable()
{
if (_mapsService == null)
return;
_mapsService.Events.ExtrudedStructureEvents.WillCreate.AddListener(OnWillCreateExtrudedStructure);
_mapsService.Events.ModeledStructureEvents.WillCreate.AddListener(OnWillCreateModeledStructure);
_mapsService.Events.RegionEvents.WillCreate.AddListener(OnWillCreateRegion);
_mapsService.Events.SegmentEvents.WillCreate.AddListener(OnWillCreateSegment);
_mapsService.Events.AreaWaterEvents.WillCreate.AddListener(OnWillCreateAreaWater);
_mapsService.Events.LineWaterEvents.WillCreate.AddListener(OnWillCreateLineWater);
_mapsService.Events.IntersectionEvents.WillCreate.AddListener(OnWillCreateIntersection);
_mapsService.Events.RegionEvents.DidCreate.AddListener(OnRegionCreated);
_mapsService.Events.SegmentEvents.DidCreate.AddListener(OnSegmentCreated);
}
void OnDisable()
{
if (_mapsService == null)
return;
_mapsService.Events.ExtrudedStructureEvents.WillCreate.RemoveListener(OnWillCreateExtrudedStructure);
_mapsService.Events.ModeledStructureEvents.WillCreate.RemoveListener(OnWillCreateModeledStructure);
_mapsService.Events.RegionEvents.WillCreate.RemoveListener(OnWillCreateRegion);
_mapsService.Events.SegmentEvents.WillCreate.RemoveListener(OnWillCreateSegment);
_mapsService.Events.AreaWaterEvents.WillCreate.RemoveListener(OnWillCreateAreaWater);
_mapsService.Events.LineWaterEvents.WillCreate.RemoveListener(OnWillCreateLineWater);
_mapsService.Events.IntersectionEvents.WillCreate.RemoveListener(OnWillCreateIntersection);
_mapsService.Events.RegionEvents.DidCreate.RemoveListener(OnRegionCreated);
_mapsService.Events.SegmentEvents.DidCreate.RemoveListener(OnSegmentCreated);
}
void OnWillCreateExtrudedStructure(WillCreateExtrudedStructureArgs args)
{
args.Cancel = !_showExtrudedStructures;
}
void OnWillCreateModeledStructure(WillCreateModeledStructureArgs args)
{
args.Cancel = !_showModeledStructures;
}
void OnWillCreateRegion(WillCreateRegionArgs args)
{
if(_showRegions)
{
if(args.MapFeature.Metadata.Usage == RegionMetadata.UsageType.Beach)
{
args.Style = new RegionStyle.Builder(args.Style){
FillMaterial = _beachMaterial
}.Build();
}
else if(args.MapFeature.Metadata.Usage != RegionMetadata.UsageType.Park
|| args.MapFeature.Metadata.Usage != RegionMetadata.UsageType.Forest)
args.Style = new RegionStyle.Builder(args.Style){
FillMaterial = _unspecifiedRegionMaterial
}.Build();
}
else
args.Cancel = true;
}
void OnWillCreateSegment(WillCreateSegmentArgs args)
{
if(_showSegments && IsValidRoad(args.MapFeature))
{
if(!IsTraversableRoad(args.MapFeature))
args.Style = new SegmentStyle.Builder(args.Style) {
Material = _roadMaterial,
Width = _roadWidth * 0.5f,
BorderMaterial = _roadBorderMaterial,
BorderWidth = 0.3f
}.Build();
}
else
args.Cancel = true;
}
void OnWillCreateAreaWater(WillCreateAreaWaterArgs args)
{
args.Cancel = !_showAreaWater;
}
void OnWillCreateLineWater(WillCreateLineWaterArgs args)
{
args.Cancel = !_showLineWater;
}
void OnWillCreateIntersection(WillCreateIntersectionArgs args)
{
if(_showIntersections)
{
args.Style = new SegmentStyle.Builder(args.Style) {
IntersectionMaterial = _intersectionMaterial,
IntersectionArmLength = _roadWidth,
IntersectionJoinLength = _roadWidth * 4,
MaxIntersectionArmDistance = _roadWidth * 5,
Width = _roadWidth
}.Build();
List<IntersectionArm> arms = new List<IntersectionArm>(args.MapFeature.Shape.Arms); foreach (IntersectionArm arm in arms)
{
if (!IsTraversableRoad(arm.Segment))
{
arm.Cancel = true;
continue;
}
}
}
else
args.Cancel = true;
}
void OnRegionCreated(DidCreateRegionArgs args)
{
if (args.MapFeature.Metadata.Usage == RegionMetadata.UsageType.Park
|| args.MapFeature.Metadata.Usage == RegionMetadata.UsageType.Forest)
args.GameObject.tag = "Park";
}
void OnSegmentCreated(DidCreateSegmentArgs args)
{
if (args.MapFeature.Metadata.Usage != SegmentMetadata.UsageType.Highway
|| args.MapFeature.Metadata.Usage != SegmentMetadata.UsageType.ControlledAccessHighway
|| args.MapFeature.Metadata.Usage != SegmentMetadata.UsageType.Rail)
args.GameObject.tag = "Road";
}
public static bool IsTraversableRoad(Segment segment)
{
return segment.Metadata.Usage != SegmentMetadata.UsageType.Ferry
&& segment.Metadata.Usage != SegmentMetadata.UsageType.Footpath
&& segment.Metadata.Usage != SegmentMetadata.UsageType.Rail;
}
public static bool IsValidRoad(Segment segment)
{
return segment.Metadata.Usage != SegmentMetadata.UsageType.Ferry;
}
}
}

Although the class is a bit long, it is actually just scratching the surface of what can be done as far as styling and manipulating Map Features. Let’s take a look at how this class works:

  • We first declare all of our materials which will serve to give life to our map.
  • We supply a _roadWidth so we can control the width of our roads.
  • Then we create regional toggles/bools for each of the visible Map Features in our scene so we can switch them on/off. This is helpful when we just want to do some design work with just one Map Feature for example.
  • We declare our new static custom GameObjectOptions variable and the default styles which will make it up.
  • In the Awake() function, we build all of our default styles and pass them to the GameObjectOptions() constructor. So now our static DefaultGameObjectOptions variable can be used when calling theLoadMap() function of the Maps Service class.
  • In the OnEnable() function, we add our listeners to the appropriate WillCreate and DidCreate events.
  • In the OnDisable() function, we make sure to clean up after ourselves by removing the event listeners so we don’t get any memory leaks.
  • For the WillCreate listener functions of Extruded Structures, Modeled Structures, Area Water and Line Water, we merely just enable or disable the features according to the values of the respective booleans because we are happy with the default styles we applied in the Awake() function above.
  • Since our default style for Regions is _grassMaterial we need to build separate styles for the _beachMaterial and _unspecifiedRegionMaterial if we want to be able to distinguish between the regions on our map. So our OnWillCreateRegion() listener tests the MapFeature.Metadata.Usage of the WillCreateRegionArgs variable passed in by the event to see if it is a beach or unspecified region.
  • As we just saw with the Regions, our default Segment is one that represents a regular road, but a Segment can be a path, road, ferry route, etc.. So we want to filter through each Segment to make sure we don’t show the road style on a ferry route and also to change the width of paths to get a distinction from the default road.
  • For Intersections, we build our default style in the OnWillCreateIntersection() function and remove any arms of the Intersection which are projecting towards paths or nothing so as not to have any awkward looking Intersection arms on the map.
  • In the DidCreate listeners for Segments and Regions (OnRegionCreated() and OnSegmentCreated() ) we add tags to the instantiated GameObjects. I just put this tag assignment in the code as an example so you would know how to access the created GameObjects for further manipulation.
  • Finally we have our 2 static helper functions ( IsTraversableRoad() and IsValidRoad()) for finding out if we have valid (not ferry routes) and traversable (not ferry, footpath or rail routes) roads.

As I said above, this script is just scratching the surface of how you can interact with the Map Features. You can expand on the DidCreate event listeners to apply parapets and borders on your buildings. You can change out the buildings for your own prefabs or add a squashing functionality that makes the buildings slowly go to the ground when your avatar gets close to them to ensure clear line of view from the camera. You could add an animated water effect. The list of what is possible goes on forever, but there are a lot of examples of what can be done in the SDK.

Ok, now do the following:

  • Select your MapManager GameObject in the hierarchy that we created in the previous tutorial and add the MapFeaturesController class to it.
  • Thereafter, open your list of Tags and add “Road” and “Park” to the list.
  • Open your Materials folder and create 6 new materials with BaseMap Color shaders (Google/Maps/Shaders/BaseMap Color) and name them “Beach”, “Park”, “Road”, “RoadBorder”, “Water” and “UnspecifiedRegion”. Create another 3 new materials with Standard shaders and name them “Roof”, “Wall” and “ModeledStructure”. Create 1 more material with an Intersection shader (Google/Maps/Shaders/Intersection) and name it, you guessed it, “Intersection”. Adjust the colours of the new materials to your liking, but leave the Intersection material white.
  • Select the Intersection material so you can see its inspector. Go to Assets/GoogleMaps/Examples/04_Advanced/Intersections/Textures and drag the intersection-main-asphalt texture to the Albedo (RGB) slot of the material and drag the intersection-decal to the Arm Decal (RGB) slot of the material.
  • Open the MobileDeviceLocationFollower class and change the second parameter of the _mapsService.LoadMap() from Google.Maps.Examples.Shared.ExampleDefaults.DefaultGameObjectOptions to MapFeatures.MapFeaturesController.DefaultGameObjectOptions .
  • Now add all of the newly created materials to the respective slot on the MapFeaturesContoller component and add the Maps Service component to the Maps Service slot of the MapFeaturesController.
MapFeatures inspector and new Materials

If you press play now, you should see your map come to life with the materials you set…

Congratulations, you have come quite a ways and you should now understand how to get an how to get an API key, setup the Maps SDK to play nice with Unity, track the GPS of a device and project it’s position on the map in the game world, style your map and manipulate Map Feature GameObjects when they are created at runtime.

One important subject that I have not touched on in this guide is to update the FloatingPointOrigin. It is a very important concept, but it is thoroughly covered here.

I hope you found this tutorial and guide helpful. If you would like me to create a tutorial which goes into further detail about something we have already touched on or another part of the SDK feel free to let me know in the comments below. Feedback is also very welcome as I would love to know what you think.

--

--

Kristopher C. Tobiasson
Everdevs Community

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