Great, now that we understand how to determine distance and how GPS accuracy can alter trilateration of location, it is time to start tracking the monsters around the character. For now, we will use a simple method to randomly place monsters around the player. In a future chapter, we will locate monsters with the help of a web service.
At this point, we have already covered a fair amount of scripting, and we have more to do by the end of the chapter. Also, the scripting changes we need to make are more intricate and prone to mistakes. So, in order to avoid putting you through that turmoil, we will import the next section of changes. For the rest of this chapter, we will switch between manual edits and script imports wherever appropriate. Perform the following instructions to do the first script asset import:
Chapter_4_Assets
folder.Chapter4_import1.unitypackage
file to import and click on the Open button.Let's open up the new MonsterService
script in your code editor and take a look at what has changed:
MonsterService
script in the Project window and double-click on it to open it in your code editor.[Header("Monster Spawn Parameters")] public float monsterSpawnRate = .75f; public float latitudeSpawnOffset = .001f; public float longitudeSpawnOffset = .001f; [Header("Monster Visibility")] public float monsterHearDistance = 200f; public float monsterSeeDistance = 100f; public float monsterLifetimeSeconds = 30; public List<Monster> monsters;
Update
method and note that the distance test code was removed and replaced with CheckMonsters()
. CheckMonsters
is a new method we added to spawn and check the current state of the monsters.CheckMonsters
method. The following is the first section of that method:if (Random.value > monsterSpawnRate) { var mlat = gpsLocationService.Latitude + Random.Range(-latitudeSpawnOffset, latitudeSpawnOffset); var mlon = gpsLocationService.Longitude + Random.Range(-longitudeSpawnOffset, longitudeSpawnOffset); var monster = new Monster { location = new MapLocation(mlon, mlat), spawnTimestamp = gpsLocationService.PlayerTimestamp }; monsters.Add(monster); }
monsterSpawnRate
. If a monster is spawned, new latitude or longitude coordinates are calculated from the current GPS location and random range +/- the spawn offsets. After that, a new monster data object is created and added to the monsters list.MapLocation
type. We do this in order to speed up calculations. In game programming, store everything you may need later and avoid creating new objects.Epoch or Unix time is a standard for time measurement defined as the number of seconds that have elapsed since 00:00:00 time, which is 1, 1, 1970.
foreach
loop that checks whether the distance from the monster or player is under the see or hear threshold. If the monster is seen or heard, a print statement outputs the state and distance to the player. The entire remaining section of code is as follows://store players location for easy access in distance calculations var playerLocation = new MapLocation(gpsLocationService.Longitude, gpsLocationService.Latitude); //get the current Epoch time in seconds var now = Epoch.Now; foreach (Monster m in monsters) { var d = MathG.Distance(m.location, playerLocation); if (MathG.Distance(m.location, playerLocation) < monsterSeeDistance) { m.lastSeenTimestamp = now; print("Monster seen, distance " + d + " started at " + m.spawnTimestamp); continue; } if (MathG.Distance(m.location, playerLocation) < monsterHearDistance) { m.lastHeardTimestamp = now; print("Monster heard, distance " + d + " started at " + m.spawnTimestamp); continue; }
Great, now we have a way to spawn and track monsters around the player as they move. The obvious next step is to start showing our monsters on the map. However, before we do that, we have to add the code to convert map coordinates to game world coordinates for our Monster service.
If you recall, in the CharacterGPSCompassController
class the Update method we used already did the conversion from map coordinates to 3D world space. Unfortunately, that code requires a dependency on the GPS Location Service to determine the world map tile scale. So, as much as we would like to create a library function for the conversion, it will just be easier to add it as a helper method to the Monster service.
Fortunately, that helper method was already added as part of the last script asset import. Just go back to your code editor, and assuming you still have the Monster service open from the last section, scroll down to the bottom of the file. You will notice that a private method has been added to do the conversion and is shown as follows:
private Vector3 ConvertToWorldSpace(float longitude, float latitude) { //convert GPS lat/long to world x/y var x = ((GoogleMapUtils.LonToX(longitude) - gpsLocationService.mapWorldCenter.x) * gpsLocationService.mapScale.x); var y = (GoogleMapUtils.LatToY(latitude) - gpsLocationService.mapWorldCenter.y) * gpsLocationService.mapScale.y; return new Vector3(-x, 0, y); }
This is the same code we use to convert the player 's coordinates into world space. Essentially, what we do is project the map coordinates to x,y map tile image space and then convert them to world space.
The following figure will hopefully illustrate this concept better:
Transformation of latitude or longitude to (x,y,z) world space
So, now that we have everything lined up, it is time to start actually placing monsters on the map. Well, at least monster objects, to start with. Open up Unity and use the following instructions to add the monster instantiation code to our Monster service:
monsters
. The assets should automatically filter the items with monsters in their name. Double-click on the MonsterService script in the filtered list to open the script in your editor of choice, as shown in the following screenshot:
Searching for monsters assets in the Project window
GPSLocationService
variable declaration, add the following line of code:public GameObject monsterPrefab;
SpawnMonster
with the following code:private void SpawnMonster(Monster monster) { var lon = monster.location.Longitude; var lat = monster.location.Latitude; var position = ConvertToWorldSpace(lon, lat); monster.gameObject = (GameObject)Instantiate(monsterPrefab, position, Quaternion.identity) }
SpawnMonster
is another helper method we will use to spawn our monster prefab. The Instantiate
method dynamically creates and returns an object, given a prefab game object and a position or rotation. The returned game object is then added as a reference to the Monster
data object, which will provide direct access to the game object later.SpawnMonster
inside the CheckMonsters
method. Locate the following line of code in the CheckMonsters
method:m.lastSeenTimestamp = now;
if (m.gameObject == null) SpawnMonster(m);
SpawnMonster
to instantiate a new monster.monsterCube
in the Inspector window. Assets/FoodyGo/Prefabs
folder in the Project window. Then, drag the new monsterCube
game object to the Prefabs
folder to create a new prefab.monsterCube
game object from the Hierarchy window.Assets/FoodyGo/Prefabs
folder, drag the monsterCube prefab onto the empty Monster Prefab slot on the Monster Service component in the
Inspector
window.
Instantiated monsterCube(Clone) shown in the Hierarchy
Well, obviously, our blocks don't look like very convincing monsters, so let's do something about that. We will use another Reallusion character as the base for our monster. If you recall, Reallusion is the company that creates those great iClone characters we are using for our player character. Perform the following instructions to set up the new monster character:
groucho
and press Enter or click on search.
Importing the Groucho character
Assets/Groucho/Prefab
folder in the
Project
window. Then, drag the groucho prefab into the
Hierarchy
window.
Setting the Wrap Mode of the Walk_Loop animation
Assets/FoodyGo/Prefabs
folder in the
Project
window to create a new Monster
prefab.Assets/FoodyGo/Prefabs
folder in the
Project
window to the
Monster Prefab
field on the
Monster Service
component in the
Inspector
window.
Monsters spawning around the player
The 3D character is designed by Reallusion iClone Character Creator. To create more customized characters, please visit HERE for more details.
Great, now, we have our monsters spawning on the map. While you were running the game, you likely noted that there are a few new issues. Here is a list of those issue that we need to address:
In order to fix the first three issues, we will do another script asset import and then review the changes. After that, we will fix the final issue by adding a UI element to the scene. Perform the following instructions to import the new scripts and other assets we will need:
Chapter_4_Assets
, select Chapter4_import2.unitypackage
and then click on
Open
.Assets/FoodyGo
folder in the
Project
window. You should see a some new folders, such as Images
and Scripts/UI
.The first three issues were all fixed with a few additions to MonsterService
script. Open up the MonsterService
script in the editor of your choice and review the fixes and respective changes given in the following list:
OnMapRedraw
event of the GPSLocationService
. If you recall, that event fires when the center map tile redraws itself. Here, you can see the code changes://event hookup inside Start() gpsLocationService.OnMapRedraw += GpsLocationService_OnMapRedraw; //event method private void GpsLocationService_OnMapRedraw(GameObject g) { //map is recentered, recenter all monsters foreach(Monster m in monsters) { if(m.gameObject != null) { var newPosition = ConvertToWorldSpace(m.location.Longitude, m.location.Latitude); m.gameObject.transform.position = newPosition; } } }
MonsterService
, checks to see if they have an instantiated game object. If they do, the game object is repositioned on the map.CheckMonsters
method. The first fix handles when a monster is not seen or heard; we want to ensure that they are not visible. We do this by checking whether a monster's gameObject
field is not null then we set the Active
property to false using SetActive(false)
, which is the same as making the object invisible. Here is the section of that code://hide monsters that can't be seen if(m.gameObject != null) { m.gameObject.SetActive(false); }
gameObject
field was null. Now, we also need to make sure that if the monster does have a gameObject,
the object is active and visible. We do this almost exactly like we did above, but now we make sure that the game object is active and visible using SetActive(true)
. The following is the section of code for review:
if (m.gameObject == null) { print("Monster seen, distance " + d + " started at " + m.spawnTimestamp); SpawnMonster(m); } else { m.gameObject.SetActive(true); //make sure the monster is visible }
Up
vector to be random. Here is the section of code, as updated in the SpawnMonster
method:private void SpawnMonster(Monster monster) { var lon = monster.location.Longitude; var lat = monster.location.Latitude; var position = ConvertToWorldSpace(lon, lat); var rotation = Quaternion.AngleAxis(Random.Range(0, 360), Vector3.up); monster.gameObject = (GameObject)Instantiate(monsterPrefab, position, rotation); }
For the final issue, we want the player to be able to track monsters that are nearby but not seen. We will do this by providing a visual cue to the player in the form of a footsteps icon or image. One footstep or paw/claw print is very close, two prints not as close, and three prints are just within hearing range. Since we only have one type of monster, at least for now, we will only show the player a single icon representing the closest monster.
Before we get into the code, let's take a look at the new properties that were added to the MonsterService
in order to support footstep ranges. Expand the Services object and then select the Monster object. The following is a view of the
Inspector
window for the
Monster
service:
Monster service parameters in the Inspector window
As you can see, in the Inspector window, there is a new section added to the Monster Service component. The new section defines at what ranges the various steps activate, with the value being the maximum range. For example, if the closest monster is at 130 meters distance, the player will see two footsteps because 130 is greater than the 125 set for One Step Range , but less than the 150 for Two Step Range .
Open up the MonsterService
script back in your favorite code editor. The following are the script changes where the footstep range is determined and set:
CheckMonsters
method inside the if
statement that checks whether the monster is audible:var footsteps = CalculateFootsetpRange(d); m.footstepRange = footsteps;
CalculateFootstepRange
. This method just simply determines the range based on the footstep range parameters and is as follows:private int CalculateFootstepRange(float distance) { if (distance < oneStepRange) return 1; if (distance < twoStepRange) return 2; if (distance < threeStepRange) return 3; return 4; }
In order to show the player the footstep's range, we will add an icon view to the UI, as follows:
DualTouchControls
object and add a new RawImage
object as a child.RawImage
object to
Footsteps
in the
Inspector
window. Assets/FoodyGo/Scripts/UI
folder. Drag the FootstepTracker
script onto the Footsteps
object in the
Inspector
window. This will add the
Footstep Tracker (Script) component to the
Inspector
window, as follows:
Empty Footstep Tracker (Script) component
Filled-in Footstep Tracker Script Component
Selecting the Anchor Preset
One footstep icon showing
After you are done testing the game in the editor, build and deploy it to your mobile device. Then, walk around your house or neighborhood and try to track monsters. Check how close you can get to the monsters. As you are live-testing, be aware of the various distances we set in the Monster Service. Consider whether any of those distance values need to be changed.
18.222.240.21