Tutorial 03 – How to add objects to the…
In this tutorial I’ll show you how easy it is to add your gameobjects to specific latitude/longitude coordinates. I tried to make this kind of operations as close as possible to any other maps sdk that you may already know (iOS Mapkit, Google maps) but I assure that it’s really easy even if GO Map is your first map sdk.
In this tutorial we’re going to make from scratch a script to download features from Google Places.
The script is already contained in GO Map asset but in this tutorial you’ll understand how it works and how to make a similar script to retrieve another POI dataset from your API.
This tutorial will be a lot useful to you to understand how GOMap events works and how you can use them to your purposes.
Why Google Places?
There are many ways to get a dataset of objects that can be displayed on a map, whether if it’s for a location based game or a mobile app. You could use a free online API, build your own one on your server, or just have a local json file with a list of features in it.
But in the end it’s always the same story, you would have a way to query data based on latitude/longitude coordinates and just parse the response to a list of geolocalized objects that you show on the map.
GO Map will ease the process as much as possible but you still have to code a bit, if you want to use your own API.
So in the end: Google Places is just one of the possible apis. I have decided to use it because of its worldwide scale and for the large amount of features you can get from it.
So let’s start from a simple basic scene with GO Map, Avatar, and Location Manager.
You can copy the one used in first tutorial.
Then create a new Game Object and name it GOPlaces. Attach to it a new component named GOPlaces_new and open it in monodevelop.
1 2 3 4 5 6 7 8 9 10 11 12 13 | using GoShared; using System.Linq; namespace GoMap { public class GOPlaces_new : MonoBehaviour { } } |
Let’s add some public attributes that we will use later like a link to GOMap and a box to enter your Google API key (make one here if you don’t have it already).
Add also a string with the base “nearby search” API url, that’s the one we’re gonna use.
1 2 3 4 5 | public GOMap goMap; public string googleAPIkey; string nearbySearchUrl = "https://maps.googleapis.com/maps/api/place/nearbysearch/json?"; |
GO Map events
We are going to make light to how events can be used in unity instead of update loops and other stuff to invoke our methods at the right time.
GO Map offers a handful of useful events that you can use in your own scripts, in this case the OnTileLoad is the right one.
OnTileLoad is triggered every time a map is created, and a GOTile object is passed in input.
This is the Awake() method of our script in which we register the method “OnTileLoad” of our class to the OnTileLoad event of goMap.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // Use this for initialization void Awake () { goMap.OnTileLoad.AddListener ((GOTile) = > { OnLoadTile (GOTile); }); } void OnLoadTile (GOTile tile) { } |
Google Places – Nearby Search API
The nearby search API works receiving in input a center (latitude, longitude), a radius (in meters) and a type of POI we want to receive back.
Beside the “type” attribute we have to get the other two from the tile so that our request will give back only the features that are contained in the map tile.
As you can see from the drawing a tile is rectangular but the Google Places request works with a circular area. That means we have to choose if we want to take the big circle or the smaller one. In the first case we risk to have some features duplicate in various tiles and in the second we risk to miss out some features in the tile corners.
We are going to request for the bigger circle and then try to filter out the features outside the tile bounds.
1 2 3 | Coordinates tileCenter = tile.goTile.tileCenter; float radius = tile.goTile.diagonalLenght / 2; |
Let’s also add the “type” public string at the top of your script so we have everything for our request.
Enter the word “Cafe” in the inspector.
IEnumerator, Coroutine and the actual reqeuest
We have now to perform the request to the Google Places API, and this is kinda easy using the Unity WWW API. Just format the url like Google wants and we’re done.
Then we just deserialize the response and we’re (almost) ready to add our places to the map.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | void OnLoadTile (GOTile tile) { StartCoroutine (NearbySearch(tile)); } IEnumerator NearbySearch (GOTile tile) { //Center of the map tile Coordinates tileCenter = tile.goTile.tileCenter; //radius of the request, equals the tile diagonal /2 float radius = tile.goTile.diagonalLenght / 2; //The complete nearby search url, api key is added at the end string url = nearbySearchUrl + "location="+tile.goTile.tileCenter.latitude+","+tile.goTile.tileCenter.longitude+"&radius="+tile.goTile.diagonalLenght/2+"&type="+type+"&key="+googleAPIkey; //Perform the request var www = new WWW(url); yield return www; //Check for errors if (string.IsNullOrEmpty (www.error)) { string response = www.text; //Deserialize the json response IDictionary deserializedResponse = (IDictionary)Json.Deserialize (response); Debug.Log(string.Format("[GO Places] Tile center: {0} - Request Url {1} - response {2}",tileCenter.toLatLongString(),url,response)); } } |
The request looks like this (add your own API key):
https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=48.8737487792969,2.28790283203125&radius=284.3542&type=cafe&key=ADD_YOUR_API_KEY
Open it in your browser to see the response in json that we are going to parse next.
Basically we have a list of features under the key “results” and for each feature we need just the coordinates and the name, but there’s a lot more if you want dig deeper.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | { "geometry" : { "location" : { "lat" : 48.875352, "lng" : 2.2867476 }, "viewport" : { "northeast" : { "lat" : 48.8767249802915, "lng" : 2.288019280291502 }, "southwest" : { "lat" : 48.8740270197085, "lng" : 2.285321319708498 } } }, "icon" : "https://maps.gstatic.com/mapfiles/place_api/icons/shopping-71.png", "id" : "724b1e420963c6c7949ded2b6cd357214895cad2", "name" : "Maison Nicoulet", "opening_hours" : { "open_now" : true, "weekday_text" : [] }, "photos" : [ { "height" : 1365, "html_attributions" : [ "\u003ca href="https://maps.google.com/maps/contrib/111804204529431767083/photos"\u003eMaison Nicoulet\u003c/a\u003e" ], "photo_reference" : "CmRaAAAAiaHCTo06DwL2nPFTS1EuQR8eNODuLvrxJcD81hXMFUa_kMYXJwuiTC4yJkSm-ojxiMG92Ldeyo8IDP4soksM5LzqzQNP7ngQj-X_qVxhU5YW9Joj6MwvCJyvSF3FqkYYEhBFYMJRIg61Kbw1hK9NwO51GhR9l3fVONyKZ2XunXaPgZFnGhtX8w", "width" : 2048 } ], "place_id" : "ChIJSePmA_Nv5kcRHqg4486X_aU", "rating" : 5, "reference" : "CmRSAAAA7jAP9TB_u99rZqt75FGCGRhsZXI5bcX5Pd1gdI41oHrP02qW8wfNdYS2Dkgg48j0hOZZBNLt84M9lx-PdrUb_teU_q4dJ0iYQYctlcejna32_4kgYjgpp5rZoiCczQuWEhCPUk6RxYh8J0XbFIdlpFckGhRF0sghwn6b-LnQJ_G_8pG08F5d3g", "scope" : "GOOGLE", "types" : [ "cafe", "food", "store", "point_of_interest", "establishment" ], "vicinity" : "32 Rue Duret, Paris" } |
Let’s add a public GameObject attribute with the prefab we are going to instantiate for each one of the POIs.
1 | public GameObject prefab; |
Get the list of places from the response, create a container for the places and set it as a GOTile child. In this way when the tile is destroyed by GOMap it will destroy also all the places.
1 2 3 4 5 | IList results = (IList)deserializedResponse ["results"]; GameObject placesContainer = new GameObject ("Places"); placesContainer.transform.SetParent (tile.transform); |
That’s how we loop through each place data and spawn our Game Objects.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | foreach (IDictionary result in results) { string placeID = (string)result["place_id"]; string name = (string)result["name"]; IDictionary location = (IDictionary)((IDictionary)result ["geometry"])["location"]; double lat = (double)location ["lat"]; double lng = (double)location ["lng"]; //Create a new coordinate object, with the desired lat lon Coordinates coordinates = new Coordinates (lat, lng,0); //Instantiate your game object GameObject place = GameObject.Instantiate (prefab); //Convert coordinates to position Vector3 position = coordinates.convertCoordinateToVector(place.transform.position.y); //Set the position to object place.transform.localPosition = position; //the parent place.transform.SetParent (placesContainer.transform); //and the name place.name = (name != null && name.Length >0)? name:placeID; } |
At this point everything works fine and if you run the scene it will look like this
But I really want to dwell for a moment on the Game Object Instantiate and position part of this code.
1 2 3 4 5 6 7 | Coordinates coordinates = new Coordinates (lat, lng,0); GameObject place = GameObject.Instantiate (prefab); Vector3 position = coordinates.convertCoordinateToVector(place.transform.position.y); place.transform.localPosition = position; |
This four lines are just everything you will ever need to place whatever GameObject onto GOMap.
Whether it’s a little monster, a tree, a flying dragon or the position of another player this is the fastest way to spawn it on the map.
- Instantiate a new Coordinate.
- Instantiate your game object.
- Convert the GPS coordinates to a Vector3 using the method convertCoordinateToVector passing the desired height value at which your game object will be spawned.
- Assign the converted vector3 to the transform.position of the new GameObject.
As promised we still have to do a method to filter out the Places outside the current tile, remember?
We are going to use a fantastic property of the Coordinate class that gives the current tile coordinates (in the tile coordinates system) at a given zoom level.
1 | Vector2 tileCoordinates = coordinates.tileCoordinates (goMap.zoomLevel); |
The returned tile coordinates are just a way to represent the tile in which a given GPS coordinate is in. The zoom level of the map is needed to make this work right.
So basically, if the tileCoordinates are the same as the current GOTile the Place is in the tile, otherwise it’s not.
1 2 3 4 5 6 7 8 9 10 11 12 13 | bool TileFilter (GOTile tile, Coordinates coordinates) { Vector2 tileCoordinates = coordinates.tileCoordinates (goMap.zoomLevel); if (tile.goTile.tileCoordinates.Equals (tileCoordinates)) return true; Debug.LogWarning ("Coordinates outside the tile"); return false; } |
That’s it, you will find the GOPlaces script here and in the next release of GOMap.
Or do we want to make things a little bit cooler?
I think we do!
Customize each POI, GOPlacesPrefab.cs component
Let’s add a component to each POI created, storing all the place properties in it.
1 2 3 4 5 | if (addGOPlaceComponent) { place.AddComponent ().placeInfo = result; } |
Then create a new prefab instance that is sort of a coin with a sprite renderer in it.
The GOPlacesPrefab.cs component will take care of the further customization in its start method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | public class GOPlacesPrefab : MonoBehaviour { public IDictionary placeInfo; public SpriteRenderer spriteRenderer; public GOPlaces goPlaces; void Start () { spriteRenderer = GetComponentInChildren (); spriteRenderer.sprite = null; string url = (string)placeInfo["icon"]; StartCoroutine (DownloadIcon (url)); } private IEnumerator DownloadIcon (string url) { WWW www = new WWW(url); yield return www; spriteRenderer.sprite = Sprite.Create (www.texture, new Rect (0, 0, 71, 71), new Vector2 (0.5f, 0.5f)); } } |
That’s all, I’ve added just a simple cache system for the icons in the final version that you can find here.
Hope this was useful to you,
Let me know if you have questions.
Bye
Alan