How to build a Pokemon Go App

 

Back4app provides Parse server service to take care of your backend and APIs development. Parse also provides basic application activity APIs like Login-in, push notification, payment…etc. For this demonstration, we can use Parse Geo Point Service and back4app as our backend to build a simple Pokémon-Go.

 

Part 1 Setting up Back4app Parse on your Unity3d.

 

Go to https://parse.com/docs/downloads

Screen Shot 2016-07-20 at 10.38.13 am

Download Unity Blank Project (v1.7.0)

 

Download https://drive.google.com/file/d/0B7k7pGQXUypGTWRqS1BqVDBWQUU/view?usp=sharing for updated Parse SDK for Unity3d

Screen Shot 2016-07-20 at 10.44.14 am

 

Open Unity3d Parse Blank Project -> Click Upgrade if required.

 

Screen Shot 2016-07-20 at 10.44.49 am

Replace Asset/Parse/Parse.Unity.dll to the latest one.

 

 

Go to Asset/Scene

 

 

The Parse Initializer GameObject will miss some scripts. Add ParseInitializeBehaviour to it.

Screen Shot 2016-07-20 at 10.45.40 am

 

Type your Application ID, dotNet Key, serverUrl to a parameter from your Back4app dashboard.

Screen Shot 2016-07-20 at 10.45.57 am

Screen Shot 2016-07-20 at 10.55.51 am

Create a GameObject and add script component to the Scene for testing.

Screen Shot 2016-07-20 at 10.47.56 am

 

Add following code to TestParse.cs

 

 

Screen Shot 2016-07-20 at 10.55.51 am

 

Screen Shot 2016-07-20 at 10.59.40 am

 

using UnityEngine;
using System.Collections;
using Parse;
using System.Threading.Tasks;
public class TestParse : MonoBehaviour {
// Use this for initialization
void Start () {
ParseObject testObj = new ParseObject("TestObject");
testObj["a"] = "HI";
testObj["b"] = "Back4app";
Task saveTask = testObj.SaveAsync();
}
// Update is called once per frame
void Update () {
}
}

 

Press Play Button on your Unity3d Editor

Screen Shot 2016-07-20 at 11.01.04 am

 

Go to Back4app database dashboard to check the success or not.

Screen Shot 2016-07-20 at 11.02.37 am

 

It is connected with your back4app account successfully if you find your uploaded data in the database.

 

 

Screen Shot 2016-07-20 at 11.03.21 am

 

 

 

Part 2 Integrate Google Map to your Unity3d Project

 

To generate the terrain like Pokémon-Go, we need to integrate Google map your Unity3d Project.

 

First, create Plane and Name it as “Map”, create directional light.

 

Screen Shot 2016-07-20 at 11.12.32 am

Second, Create a script “GoogleMap” and add to Map Game object.

Screen Shot 2016-07-20 at 11.20.28 am

	using UnityEngine;
	using System.Collections;

	public class GoogleMap : MonoBehaviour
	{
		public enum MapType
		{
			RoadMap,
			Satellite,
			Terrain,
			Hybrid
		}
		public bool loadOnStart = true;
		public bool autoLocateCenter = true;
		public GoogleMapLocation centerLocation;
		public int zoom = 13;
		public MapType mapType;
		public int size = 512;
		public bool doubleResolution = false;
		public GoogleMapMarker[] markers;
		public GoogleMapPath[] paths;

		void Start() {
			if(loadOnStart) Refresh();
		}

		public void Refresh() {
			if(autoLocateCenter && (markers.Length == 0 && paths.Length == 0)) {
				Debug.LogError("Auto Center will only work if paths or markers are used.");
			}
			StartCoroutine(_Refresh());
		}

		IEnumerator _Refresh ()
		{
			var url = "http://maps.googleapis.com/maps/api/staticmap";
			var qs = "";
			if (!autoLocateCenter) {
				if (centerLocation.address != "")
				qs += "center=" + WWW.UnEscapeURL (centerLocation.address);
				else {
					qs += "center=" + WWW.UnEscapeURL (string.Format ("{0},{1}", centerLocation.latitude, centerLocation.longitude));
				}

				qs += "&zoom=" + zoom.ToString ();
			}
			qs += "&size=" + WWW.UnEscapeURL (string.Format ("{0}x{0}", size));
			qs += "&scale=" + (doubleResolution ? "2" : "1");
			qs += "&maptype=" + mapType.ToString ().ToLower ();
			var usingSensor = false;
	#if UNITY_IPHONE
			usingSensor = Input.location.isEnabledByUser && Input.location.status == LocationServiceStatus.Running;
	#endif
			qs += "&sensor=" + (usingSensor ? "true" : "false");

			foreach (var i in markers) {
				qs += "&markers=" + string.Format ("size:{0}|color:{1}|label:{2}", i.size.ToString ().ToLower (), i.color, i.label);
				foreach (var loc in i.locations) {
					if (loc.address != "")
					qs += "|" + WWW.UnEscapeURL (loc.address);
					else
					qs += "|" + WWW.UnEscapeURL (string.Format ("{0},{1}", loc.latitude, loc.longitude));
				}
			}

			foreach (var i in paths) {
				qs += "&path=" + string.Format ("weight:{0}|color:{1}", i.weight, i.color);
				if(i.fill) qs += "|fillcolor:" + i.fillColor;
				foreach (var loc in i.locations) {
					if (loc.address != "")
					qs += "|" + WWW.UnEscapeURL (loc.address);
					else
					qs += "|" + WWW.UnEscapeURL (string.Format ("{0},{1}", loc.latitude, loc.longitude));
				}
			}

			var req = new WWW (url + "?" + qs);
			yield return req;
			GetComponent().material.mainTexture = req.texture;
		}

	}

	public enum GoogleMapColor
	{
		black,
		brown,
		green,
		purple,
		yellow,
		blue,
		gray,
		orange,
		red,
		white
	}

	[System.Serializable]
	public class GoogleMapLocation
	{
		public string address;
		public float latitude;
		public float longitude;
	}

	[System.Serializable]
	public class GoogleMapMarker
	{
		public enum GoogleMapMarkerSize
		{
			Tiny,
			Small,
			Mid
		}
		public GoogleMapMarkerSize size;
		public GoogleMapColor color;
		public string label;
		public GoogleMapLocation[] locations;

	}

	[System.Serializable]
	public class GoogleMapPath
	{
		public int weight = 5;
		public GoogleMapColor color;
		public bool fill = false;
		public GoogleMapColor fillColor;
		public GoogleMapLocation[] locations;
	}

 

 

 

Untick the AutoLocateCenter.

Screen Shot 2016-07-20 at 11.25.04 am

 

 

 

 

Type some dummy parameter to try the Google Map.

 

Screen Shot 2016-07-20 at 11.26.45 am

 

 

 

If the plane shows the correct location as your parameter, the Google map is successfully integrated. You can use Refresh method to refresh the Google map to change location.

 

Screen Shot 2016-07-20 at 11.44.02 am

 

 

 

 

 

 

Part 3 Integrate Character and Interacting with GPS Data

 

 

Create LocationManager Gameobject and add the following code.

This code is getting the GPS Data and updating the Google Map when GPS data is changed.

  using UnityEngine;
 using System.Collections;
 using UnityEngine.UI;
 
 public class LocationManager : MonoBehaviour {
 
     public GameObject map;
     public GameObject spawn;
     public float lat=0;
     public float lon=0;
     float lastlat=0,lastlon=0;
     public GameObject latText;
     public GameObject lonText;
 
     // Use this for initialization
     void Start () {
         Input.location.Start (); // enable the mobile device GPS
         if (Input.location.isEnabledByUser) { // if mobile device GPS is enabled
             lat = Input.location.lastData.latitude; //get GPS Data
             lon = Input.location.lastData.longitude;
             map.GetComponent<GoogleMap> ().centerLocation.latitude = lat;
             map.GetComponent<GoogleMap> ().centerLocation.longitude = lon;
         }
     
     }
 
     
     // Update is called once per frame
     void Update () {
 //      <---------Mobile Device Code----------->
         if (Input.location.isEnabledByUser) {
             lat = Input.location.lastData.latitude;
             lon = Input.location.lastData.longitude;
             if (lastlat != lat || lastlon != lon) {
                 map.GetComponent<GoogleMap> ().centerLocation.latitude = lat;
                 map.GetComponent<GoogleMap> ().centerLocation.longitude = lon;
                 latText.GetComponent<Text> ().text = "Lat" + lat.ToString ();
                 lonText.GetComponent<Text> ().text = "Lon" + lon.ToString ();
                 //spawn.GetComponent<Spawn> ().updateMonstersPosition (lon, lat);
                 //Add above after you complete spawn part
                 map.GetComponent<GoogleMap> ().Refresh ();
             }
             lastlat = lat;
             lastlon = lon;
         }
 //      <---------Mobile Device Code----------->
 
 //      <---------PC Test Code----------->
 //        if (lastlat != lat || lastlon != lon) {
 //        map.GetComponent<GoogleMap> ().centerLocation.latitude = lat;
 //        map.GetComponent<GoogleMap> ().centerLocation.longitude = lon;
 //        latText.GetComponent<Text> ().text = "Lat" + lat.ToString ();
 //        lonText.GetComponent<Text> ().text = "Lon" + lon.ToString ();
 //            spawn.GetComponent<Spawn> ().updateMonstersPosition (lon, lat);
 //        map.GetComponent<GoogleMap> ().Refresh ();
 //        }
 //                    lastlat = lat;
 //                    lastlon = lon;
 //      <---------PC Test Code----------->
 
     }
 
     public float getLon(){
         return lon;
     }
     public float getLat(){
         return lat;
     }
 
 }
 

 

 

 

 

 

Let test the Google map again.

It is successful if the Google map change when you change the GPS Data in Runtime.

Screen Shot 2016-07-20 at 11.44.19 am

 

Screen Shot 2016-07-20 at 11.44.34 am

 

After that, create a Capsule as a character and a Cube as Direction of the character on top of the Google Map. (For me, I place the character at X:0 Y:0.07 Z:0)

Add a compass to your character when you test with your mobile device.

 

Screen Shot 2016-07-20 at 11.47.57 am

 

Make sure your character is located correctly by checking Latitude and Longitude on Google Map.

 

Screen Shot 2016-07-20 at 11.50.41 am

 

Part 4 Generating Monsters on Map (Part A)

 

Foe Generating Monsters, we need to do some calculation on placing the monster with their longitude and latitude location. Because the ratio between Google map, reality and Unity3d XYZ-world is different.

 

To figure out how to calculate the relationship between them, we need dummy data.

For more detail : how to calculate.

http://www.movable-type.co.uk/scripts/latlong.html

 

First of all, we need to set initial latitude and longitude on Google Map first (You can choose where you want) and set the Markers on the Google Map.

Screen Shot 2016-07-20 at 1.46.56 pm

 

Press Run, You will find the label on your Google map.

Screen Shot 2016-07-20 at 1.47.30 pm

PS: The reason why we cannot use the marker to be a monster because it is combined with the texture. The marker itself is a part of the texture but not a Gmeobject so we cannot do any implementation on that.

 

Beside that, we will make some dummy GPS data of monster on Parse server, later on, so we need to know the corresponding ratio on the monster game object position.

Screen Shot 2016-07-20 at 1.50.45 pm

 

 

After that, we create a cube and place on top of the label.

 

 

Screen Shot 2016-07-20 at 1.50.58 pm

Record this position.

Now, we need to find the exact distance between the player GPS location and the marker GPS location.

 

The distance between start (37.38373, -122.0133) and marker (37.384782,-122.012893) can be calculated by this script.

 

 

public static double DistanceBetweenPlaces(double lon1, double lat1, double lon2, double lat2)
{
	float R = 6371000; // m
	double sLat1 = Math.Sin(deg2rad(lat1));
	double sLat2 = Math.Sin(deg2rad(lat2));
	double cLat1 = Math.Cos(deg2rad(lat1));
	double cLat2 = Math.Cos(deg2rad(lat2));
	double cLon = Math.Cos(deg2rad(lon1) - deg2rad(lon2));

	double cosD = sLat1*sLat2 + cLat1*cLat2*cLon;

	double d = Math.Acos(cosD);

	double dist = R * d;

	return dist;
}

 

and then calculate the XYZ world distance between the CUBE maker and the Player Capsule. (-0.563,0.07,-1.915) and (0,0.07,0). This is a simple calculation, so I don’t list how to do it.

 

After that, the ratio will be found by this equation

 

Ratio = XYZ world distance / Exact Distance.

 

Once we have this ratio, we able to calculate how far the monster should be placed according to the Player location. However, the possible location still undefined because we don’t have the bearing between the player and monster.

 

To calculate the bearing. We can use this code.

 

public static double BearingBetweenPlaces(double lon1,double lat1,double lon2,double lat2){
	double y = Math.Sin (deg2rad (lon2) - deg2rad (lon1)) * Math.Cos (deg2rad (lat2));
	double x = Math.Cos (deg2rad (lat1)) * Math.Sin (deg2rad (lat2)) - Math.Sin (deg2rad (lat1)) * Math.Cos (deg2rad (lat2)) * Math.Cos (deg2rad (lon2) - deg2rad (lon1));
	double bearing = Math.Atan2 (y, x);
	return bearing;
}

 

After we get the ratio and bearing, it is possible to convert Longitude Latitude location to be XYZ-world coordination, which means we can place any monsters with latitude longitude location to correct game world location.

 

 

public static double[] convertXZ(double lon1,double lat1,double lon2,double lat2){
	double ratio = 0.0162626572;
	double bearing = BearingBetweenPlaces (lon1, lat1, lon2, lat2);
	double distance = DistanceBetweenPlaces(lon1, lat1, lon2, lat2);
	double x = Math.Sin (-bearing) * distance * ratio;
	double z = -Math.Cos (bearing) * distance * ratio;
	Debug.Log ("X" + x.ToString () + "Z" + z.ToString ());
	double[] xz = { x, z };
	return xz;
}

 

 

With the above code, we know the correct XYZ coordination to spawn monster.

 

 

 

 

 

Part 5 Generating Monsters on Map (Part B)

 

While we are able to convert Longitude and Latitude data to the game world, it is time now to create some dummy monster data on Back4app parse server.

 

Go to your Parse back4app dashboard and create a class call Monster.

 

 

Screen Shot 2016-07-20 at 2.40.09 pm

 

 

Add a column: Location with data type Geopoint.

 

Screen Shot 2016-07-20 at 2.40.24 pm

 

 

Insert some monster dummy row that around you.

 

Screen Shot 2016-07-20 at 2.40.48 pm

 

The dummy monsters are set, they are waiting for you to call.

Then I create a MonsterSpawn GameObject with script Spawn.

 

Drop the LocationManager GameObject to it (For getting updated GPS Data)

M prefab monster for spawning a monster.

Screen Shot 2016-07-20 at 2.44.02 pm

 

 

It is time to fetch the dummy monster data from back4app to your program.

 

  void Start () {
         monsterXZCoordination = new List<double[]>();
         monsterLL = new List<double[]> ();
         var query = ParseObject.GetQuery ("Monster");
         //you can use WhereWithinGeoBox or WhereNear or WhereWithinDistance to simulate pkmgo serach range
         playerlon = locationManager.GetComponent<LocationManager>().getLon();
         playerlat = locationManager.GetComponent<LocationManager>().getLat();
         query.FindAsync ().ContinueWith (t => {
             IEnumerable<ParseObject> results = t.Result;
             foreach (var result in results) {
                 ParseGeoPoint temp = result.Get<ParseGeoPoint>("Location");
                 double[] tempxz = GeoDistance.convertXZ(playerlon,playerlat,temp.Longitude,temp.Latitude);
                 double[] trueLL = {temp.Longitude,temp.Latitude};
                 monsterLL.Add(trueLL);
                 monsterXZCoordination.Add(tempxz);
             }
             spawn = true;
         });
 
 
     } 

The above code gets the result one by one and converting the GeoPoint data to be XYZ-world coordination, and then push the result to the List. After adding the result is fetched, spawn becomes true to allow program spawns the monster.

 

  void Update () {
 
         playerlon = locationManager.GetComponent<LocationManager>().getLon();
         playerlat = locationManager.GetComponent<LocationManager> ().getLat ();
 
         if (spawn == true) {
             monsterSpawn ();
         }
         if (monster.Count != 0) {
             if (lastlon != playerlon || lastlat != playerlat) {
                 //DebugConsole.Log ("Changing");
                 updateMonstersPosition ();
             }
             
         }
         lastlat = playerlat;
         lastlon = playerlon;
 
     } 
      void monsterSpawn(){
         //DebugConsole.Log ("HIHIHI");
         for (int i = 0; i < monsterXZCoordination.Count; i++) {
             GameObject temp = Instantiate (m, new Vector3 ((float)monsterXZCoordination [i][0], 0.07f, (float)monsterXZCoordination [i][1]), new Quaternion (0, 0, 0, 0)) as GameObject;
 
             //DebugConsole.Log (temp.transform.position.ToString());
             monster.Add (temp);
         }
         spawn = false;
     }
     void updateMonstersPosition(){
         timeOfupdate++;
         for (int i = 0; i < monster.Count; i++) {
             double[] tempxz = GeoDistance.convertXZ(playerlon,playerlat,monsterLL[i][0],monsterLL[i][1]);
             monster [i].gameObject.transform.position = new Vector3 ((float)tempxz[0],0.07f,(float)tempxz[1]);
             //DebugConsole.Log (timeOfupdate.ToString()+"th update:"+i.ToString()+" "+monster [i].gameObject.transform.position.ToString ());
         }
     }
     public void updateMonstersPosition(double lon,double lat){
         timeOfupdate++;
         for (int i = 0; i < monster.Count; i++) {
             double[] tempxz = GeoDistance.convertXZ(lon,lat,monsterLL[i][0],monsterLL[i][1]);
             monster [i].gameObject.transform.position = new Vector3 ((float)tempxz[0],0.07f,(float)tempxz[1]);
             //DebugConsole.Log (timeOfupdate.ToString()+"th update:"+i.ToString()+" "+monster [i].gameObject.transform.position.ToString ());
         }
     } 

 

After the Monsters are spawned, the monsters will keep updating when the player GPS data has updated.

 

 

Testing. The monster is spawn at correct XYZ coordination with their Latitude Longitude data.

 

Screen Shot 2016-07-20 at 2.51.45 pm

 

Part 6 How to move the Player (solve google map texture slow problem!)

Until Part 5, the movement of the player is relied on the Google Map texture updating. However, it is not effective  to play it. The user experience is not good.

To solve this problem, we “unlock” the player. Make player move on the map.

The algorithm behind is similar to updating the monster. we apply on the capsule. compare the last GPS data and new GPS data to calculate how should the player move in the Game world. As we are updating the position of the player, therefore we don’t need monster update and google map refresh function anymore.

what you need to do : Modify LocationMaganger.cs

 

Add GameObject playerCapsule : Drag your capsule to this

comment or delete monster update and google map refresh in update()

Add player movement logic

                if (lastlon != 0 && lastlat != 0) {//skip at move player at the first update of GPS
                    double[] tempXZ = GeoDistance.convertXZ (lastlon, lastlat, lon, lat);
                    Debug.Log (Last lon: + lastlon.ToString () + lastlat: + lastlat.ToString () + lon: + lon.ToString () + lat: + lat.ToString ());
                    Debug.Log (Player should move to X: + tempXZ [0].ToString () +  Z: + tempXZ [1].ToString ());
                    Vector3 newPositionTarget = new Vector3 (playerCapsule.transform.position.x+(float)tempXZ [0], 0.07f, playerCapsule.transform.position.z+(float)tempXZ [1]);
                    playerCapsule.transform.position = newPositionTarget;
                }else map.GetComponent<GoogleMap> ().Refresh ();

***** this google map refresh does not need to be Commented, because we need it to init the texture and the first time

 

 

 

 

 

using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class LocationManager : MonoBehaviour {

    public GameObject map;
    public GameObject spawn;
    public GameObject playerCapsule;
    public float lat=0;
    public float lon=0;
    float lastlat=0,lastlon=0;
    public GameObject latText;
    public GameObject lonText;

    // Use this for initialization
    void Start () {
        Input.location.Start (); // enable the mobile device GPS
        if (Input.location.isEnabledByUser) { // if mobile device GPS is enabled
            lat = Input.location.lastData.latitude; //get GPS Data
            lon = Input.location.lastData.longitude;
            map.GetComponent<GoogleMap> ().centerLocation.latitude = lat;
            map.GetComponent<GoogleMap> ().centerLocation.longitude = lon;
        }
    
    }

    
    // Update is called once per frame
    void Update () {
//      <Mobile Device Code>
        if (Input.location.isEnabledByUser) {
            lat = Input.location.lastData.latitude;
            lon = Input.location.lastData.longitude;
            if (lastlat != lat || lastlon != lon) {
                map.GetComponent<GoogleMap> ().centerLocation.latitude = lat;
                map.GetComponent<GoogleMap> ().centerLocation.longitude = lon;
                latText.GetComponent<Text> ().text = Lat + lat.ToString ();
                lonText.GetComponent<Text> ().text = Lon + lon.ToString ();
                //spawn.GetComponent<Spawn> ().updateMonstersPosition (lon, lat);

                if (lastlon != 0 && lastlat != 0) {//skip at move player at the first update of GPS
                    double[] tempXZ = GeoDistance.convertXZ (lastlon, lastlat, lon, lat);
                    Debug.Log (Last lon: + lastlon.ToString () + lastlat: + lastlat.ToString () + lon: + lon.ToString () + lat: + lat.ToString ());
                    Debug.Log (Player should move to X: + tempXZ [0].ToString () +  Z: + tempXZ [1].ToString ());
                    Vector3 newPositionTarget = new Vector3 (playerCapsule.transform.position.x+(float)tempXZ [0], 0.07f, playerCapsule.transform.position.z+(float)tempXZ [1]);
                    playerCapsule.transform.position = newPositionTarget;
                }else map.GetComponent<GoogleMap> ().Refresh ();

                //map.GetComponent<GoogleMap> ().Refresh ();
            }
            lastlat = lat;
            lastlon = lon;}
//      <Mobile Device Code>

//      <PC Test Code>
//        if (lastlat != lat || lastlon != lon) {
//        map.GetComponent<GoogleMap> ().centerLocation.latitude = lat;
//        map.GetComponent<GoogleMap> ().centerLocation.longitude = lon;
//        latText.GetComponent<Text> ().text = Lat + lat.ToString ();
//        lonText.GetComponent<Text> ().text = Lon + lon.ToString ();
//        //spawn.GetComponent<Spawn> ().updateMonstersPosition (lon, lat);
//            if (lastlon != 0 && lastlat != 0) {//skip at move player at the first update of GPS
//                double[] tempXZ = GeoDistance.convertXZ (lastlon, lastlat, lon, lat);
//                Debug.Log (Last lon: + lastlon.ToString () + lastlat: + lastlat.ToString () + lon: + lon.ToString () + lat: + lat.ToString ());
//                Debug.Log (Player should move to X: + tempXZ [0].ToString () +  Z: + tempXZ [1].ToString ());
//                Vector3 newPositionTarget = new Vector3 (playerCapsule.transform.position.x+(float)tempXZ [0], 0.07f, playerCapsule.transform.position.z+(float)tempXZ [1]);
//                playerCapsule.transform.position = newPositionTarget;
//            }else map.GetComponent<GoogleMap> ().Refresh ();
//        }
//                    lastlat = lat;
//                    lastlon = lon;
//      <PC Test Code>

    }

    public float getLon(){
        return lon;
    }
    public float getLat(){
        return lat;
    }

//    public IEnumerator WaitForRequest(WWW www){
//        yield return www;
//        if (www.error == null)
//        {
//            Debug.Log(WWW Ok!:  + www.data);
//        } else {
//            Debug.Log(WWW Error: + www.error);
//        }   
//    }

}

Comment in Spawn.cs if you doesn’t

//        if (monster.Count != 0) {
//            if (lastlon != playerlon || lastlat != playerlat) {
//                DebugConsole.Log (Changing);
//                updateMonstersPosition ();
//            }
//        }

Second, attach following code to you camera make your cam follow you character ( Don’t forget to drag your capsule to inspector.

using UnityEngine;
using System.Collections;

public class follow : MonoBehaviour {

    public GameObject player;       //Public variable to store a reference to the player game object

    private Vector3 offset;         //Private variable to store the offset distance between the player and camera

    // Use this for initialization
    void Start () 
    {
        //Calculate and store the offset value by getting the distance between the players position and cameras position.
        offset = transform.position  player.transform.position;
    }

    // LateUpdate is called after Update each frame
    void LateUpdate () 
    {
        // Set the position of the cameras transform to be the same as the players, but offset by the calculated offset distance.
        transform.position = player.transform.position + offset;
    }
}

 

GM_20160731_142752

 

 

To view source code and whole project -> Raw Unity Project

 

Issues, comments or suggestions? Let’s discuss in our Developers Group Topic.