Building a real-time strategy game in Unity 4.1 using C# scripting

<
>

Part 19: Loading A Game

Last time we went through the process of storing details about a game to a save file. This is useful, but it is really only half of the picture. If we cannot load those details back we are merely left with a file full of interesting, but ultimately useless, information. The goal this time is to make sure that we can load those details from a save file, thus allowing a player to continue a game from where they were last time.

Load Menu

Once again, we want to be able to trigger the load from a Menu - in fact, we want to be able to do this from both the Main Menu and the Pause Menu. First we need to make some changes to PauseMenu.cs. Update the list of options in SetButtons() to be

				buttons = new string[] {"Resume", "Save Game", "Load Game", "Exit Game"};

so that when we open the Pause Menu we have the option to load an existing game. Now we need to add the following case

				case "Load Game": LoadGame(); break;

to the switch statement in HandleButton() so that when the button is clicked we can handle it appropriately. Now we need to make almost identical changes to MainMenu.cs. Update the list of options in SetButtons() to be

				buttons = new string[] {"New Game", "Load Game", "Change Player", "Quit Game"};

so that when the Main Menu is opened the option to load a game is there. Then add the following case

				case "Load Game": LoadGame(); break;

to the switch statement in HandleButton() so that we can handle the button click. You will notice that in both of these cases we are making a call to LoadGame(). We only want to write (and to maintain) this code in one place, so we will add this method to Menu.cs.

				private void LoadGame() {
					HideCurrentMenu();
					LoadMenu loadMenu = GetComponent< LoadMenu >();
					if(loadMenu) {
						loadMenu.enabled = true;
						loadMenu.Activate();
					}
				}

Since we do not know which Menu is currently open we make the call to HideCurrentMenu() to make sure that a Menu will be closed. This does mean that we need to define the base version of this in Menu.cs

				protected virtual void HideCurrentMenu() {
					//a child class needs to set this to hide itself when appropriate
				}

and then provide a definition for this method in MainMenu.cs

				protected override void HideCurrentMenu () {
					GetComponent< MainMenu >().enabled = false;
				}

and in PauseMenu.cs.

				protected override void HideCurrentMenu () {
					GetComponent< PauseMenu >().enabled = false;
				}

Once the Menu has been hidden we find the Load Menu and enable it. We are then calling loadMenu.Activate() to allow us to do some extra initialization each time the Load Menu is opened. Of course, before we go any further we need to create the Load Menu. Inside the Scripts folder found in the Menu folder create a new C# script called LoadMenu.cs. Set the code in this file to be the following.

				using UnityEngine;
				using RTS;

				public class LoadMenu : MonoBehaviour {

					public GUISkin mainSkin, selectionSkin;

					void Start() {
						Activate();
					}
					
					void Update() {
						if(Input.GetKeyDown(KeyCode.Escape)) CancelLoad();
					}
					
					void OnGUI() {
						if(SelectionList.MouseDoubleClick()) StartLoad();
						
						GUI.skin = mainSkin;
						float menuHeight = GetMenuHeight();
						float groupLeft = Screen.width / 2 - ResourceManager.MenuWidth / 2;
						float groupTop = Screen.height / 2 - menuHeight / 2;
						Rect groupRect = new Rect(groupLeft, groupTop, ResourceManager.MenuWidth, menuHeight);
						GUI.BeginGroup(groupRect);
						//background box
						GUI.Box(new Rect(0, 0, ResourceManager.MenuWidth, menuHeight), "");
						//menu buttons
						float leftPos = ResourceManager.Padding;
						float topPos = menuHeight - ResourceManager.Padding - ResourceManager.ButtonHeight;
						if(GUI.Button(new Rect(leftPos, topPos, ResourceManager.ButtonWidth, ResourceManager.ButtonHeight), "Load Game")) {
							StartLoad();
						}
						leftPos += ResourceManager.ButtonWidth + ResourceManager.Padding;
						if(GUI.Button(new Rect(leftPos, topPos, ResourceManager.ButtonWidth, ResourceManager.ButtonHeight), "Cancel")) {
							CancelLoad();
						}
						GUI.EndGroup();
						
						//selection list, needs to be called outside of the group for the menu
						float selectionLeft = groupRect.x + ResourceManager.Padding;
						float selectionTop = groupRect.y + ResourceManager.Padding;
						float selectionWidth = groupRect.width - 2 * ResourceManager.Padding;
						float selectionHeight = groupRect.height - GetMenuItemsHeight() - ResourceManager.Padding;
						SelectionList.Draw(selectionLeft,selectionTop,selectionWidth,selectionHeight,selectionSkin);
					}
					
					private float GetMenuHeight() {
						return 250 + GetMenuItemsHeight();
					}
					
					private float GetMenuItemsHeight() {
						return ResourceManager.ButtonHeight + 2 * ResourceManager.Padding;
					}
					
					public void Activate() {
						SelectionList.LoadEntries(PlayerManager.GetSavedGames());
					}
					
					private void StartLoad() {
						string newLevel = SelectionList.GetCurrentEntry();
						if(newLevel!="") {
							ResourceManager.LevelName = newLevel;
							if(Application.loadedLevelName != "BlankMap1") Application.LoadLevel("BlankMap1");
							else if(Application.loadedLevelName != "BlankMap2") Application.LoadLevel("BlankMap2");
							//makes sure that the loaded level runs at normal speed
							Time.timeScale = 1.0f;
						}
					}
					
					private void CancelLoad() {
						GetComponent< LoadMenu >().enabled = false;
						PauseMenu pause = GetComponent< PauseMenu >();
						if(pause) pause.enabled = true;
						else {
							MainMenu main = GetComponent< MainMenu >();
							if(main) main.enabled = true;
						}
					}

				}

The logic and structure of this is almost the same as the Save Menu that we created last time. Once again we want to use a SelectionList to show the options that the Player has available to choose from. If they click on Cancel then we want to return them to the Menu that they came from - this will be either the Main Menu or the Pause Menu, we determine this by which script is actually found. The only real difference here is found in StartLoad(). Obviously we want to load the level that was chosen from the Selection List and to set that value in our ResourceManager. Once we have done that we are actually going to make use of a pair of empty scenes to load details into. We need two of these for the scenario where we load a game into one and then want to load a game again. Let's create these now, before we go any further. Create a new Scene (File -> New Scene) and then save it in the Assets folder as "BlankMap1", and then save it there again as "BlankMap2". To make sure that we can reference these from our code we need to add these scenes to our Game. Open the build settings for your project (File -> Build Settings). Up the top of the dialog you should see a list titled "Scenes in Build" with a button at the bottom of this list labelled "Add Current". Open "BlankMap1" and then click "Add Current", then open "BlankMap2" and click "Add Current". The list of scenes in your build should now include "BlankMap1" and "BlankMap2". We will handle the actual load of a saved game when a level is loaded, which we will get to shortly.

In order to actually be able to open and interact with our Load Menu we need to add it to the appropriate places in our game. First off, to get it to show from the Pause Menu we need to add it to our HUD. Expand the Player prefab in the Player folder (as you did last time), select the HUD object, add LoadMenu.cs to it, and make sure that the script is disabled. Make sure that you also attach MenuSkin and SelectionSkin to the appropriate fields of the attached Menu script so that the Menu looks right. Finally, to get the Menu to show from the Main Menu we need to add it to the Camera in the MainMenu scene. Once again make sure that it is disabled and that the skins are attached to the appropriate fields. Now if you run your game you should be able to enter / exit the Load Menu from both the Main Menu and the Pause Menu. Attempting to load a saved game should bring up a blank map at the moment.

Start Load

We need somewhere central to handle the actual loading of a saved game. Last time we added a LevelLoader to handle assigning objectIds to all of our WorldObjects. We will make use of this to initiate a load as well, where appropriate of course. In LevelLoader.cs update the contents of OnLevelWasLoaded() to be the following code.

				void OnLevelWasLoaded() {
					if(initialised) {
						if(ResourceManager.LevelName != null && ResourceManager.LevelName != "") {
							LoadManager.LoadGame(ResourceManager.LevelName);
						} else {
							WorldObject[] worldObjects = GameObject.FindObjectsOfType(typeof(WorldObject)) as WorldObject[];
							foreach(WorldObject worldObject in worldObjects) {
								worldObject.ObjectId = nextObjectId++;
								if(nextObjectId >= int.MaxValue) nextObjectId = 0;
							}
						}
					}
				}

All we have done here is to add a check to see whether the currently saved Level Name is blank or not. There is one thing that we need to do before we go any further. Since we are wanting to load a level if the Level Name is not blank we need to make sure that when we return to the Main Menu (e.g. quit an existing game) that we set the Level Name to be blank. We can do that by adding this code

				ResourceManager.LevelName = "";

to the top of ReturnToMainMenu() in PauseMenu.cs

If the Level Name is blank we want to assign objectIds to the WorldObjects as before. However, if it is not empty then we want a Load Manager to load the specified level for us. Of course, this means that we need to create a class to do this for us. Inside the RTS folder create a new C# script called LoadManager.cs and start it off with the following code.

				using UnityEngine;
				using Newtonsoft.Json;
				using System.IO;
				using System.Collections.Generic;

				namespace RTS {
					public static class LoadManager {
						
						public static void LoadGame(string filename) {
							char separator = Path.DirectorySeparatorChar;
							string path = "SavedGames" + separator + PlayerManager.GetPlayerName() + separator + filename + ".json";
							if(!File.Exists(path)) {
								Debug.Log("Unable to find " + path + ". Loading will crash, so aborting.");
								return;
							}
							string input;
							using(StreamReader sr = new StreamReader(path)) {
								input = sr.ReadToEnd();
							}
							if(input != null) {
								//parse contents of file
								using(JsonTextReader reader = new JsonTextReader(new StringReader(input))) {
									while(reader.Read()) {
										if(reader.Value!=null) {
											if(reader.TokenType == JsonToken.PropertyName) {
												string property = (string)reader.Value;
												switch(property) {
													case "Sun": LoadLighting(reader); break;
													case "Ground": LoadTerrain(reader); break;
													case "Camera": LoadCamera(reader); break;
													case "Resources": LoadResources(reader); break;
													case "Players": LoadPlayers(reader); break;
													default: break;
												}
											}
										}
									}
								}
							}
						}
						
						private static void LoadLighting(JsonTextReader reader) {
							
						}
						
						private static void LoadTerrain(JsonTextReader reader) {
							
						}
						
						private static void LoadCamera(JsonTextReader reader) {
							
						}
						
						private static void LoadResources(JsonTextReader reader) {
							
						}
						
						private static void LoadPlayers(JsonTextReader reader) {
							
						}
						
					}
				}

This will attempt to find the specified file in the Saved Games folder for the current player. If the file is found it will read the entire file (there is a potential problem here if save files end up being really big) and then parse it using a JsonReader. We want to handle each Json property name that we find, which is done in the switch statement. To keep things as clean as possible heading forward, we actually want this to mirror the structure of our Save Manager. So each of the primary save methods has a correpsonding load method. This way, if we want to add more complex lighting, terrain, etc. to our game there is a clear place as to where the saving / loading logic for this should go. Note: ideally the properties "Sun", "Ground", and "Camera" should be replaced with more generic terms and the values stored inside those wrappers, but that is not a major thing so I will leave it up to you to play around with if you feel like doing so.

Load World Objects

The first thing that we want to load up is the primary objects in our world - that is the terrain, lighting, and cameras. In our current environment this is actually not very complicated, since we just have a plane for the ground, a light for the sun, and a single camera. It is, however, still important to make sure that this is all working correctly before we go on to load more complex things. We will start by loading the lighting, so we want to add the following code to LoadLighting().

				if(reader == null) return;
				Vector3 position = new Vector3(0,0,0), scale = new Vector3(1,1,1);
				Quaternion rotation = new Quaternion(0,0,0,0);
				while(reader.Read()) {
					if(reader.Value != null) {
						if(reader.TokenType == JsonToken.PropertyName) {
							if((string)reader.Value == "Position") position = LoadVector(reader);
							else if((string)reader.Value == "Rotation") rotation = LoadQuaternion(reader);
							else if((string)reader.Value == "Scale") scale = LoadVector(reader);
						}
					} else if(reader.TokenType == JsonToken.EndObject) {
						GameObject sun = (GameObject)GameObject.Instantiate(ResourceManager.GetWorldObject("Sun"), position, rotation);
						sun.transform.localScale = scale;
						return;
					}
				}

We are wanting to find the values saved for the position, rotation, and scale. However, we also provide some defaults in case (for some weird reason) those values were not in the save file. The loading of these values is done by using a pair of helper methods which we should define before going on any further.

				public static Vector3 LoadVector(JsonTextReader reader) {
					Vector3 position = new Vector3(0,0,0);
					if(reader == null) return position;
					string currVal = "";
					while(reader.Read()) {
						if(reader.Value!=null) {
							if(reader.TokenType == JsonToken.PropertyName) currVal = (string)reader.Value;
							else {
								switch(currVal) {
									case "x": position.x = (float)(double)reader.Value; break;
									case "y": position.y = (float)(double)reader.Value; break;
									case "z": position.z = (float)(double)reader.Value; break;
									default: break;
								}
							}
						} else if(reader.TokenType == JsonToken.EndObject) return position;
					}
					return position;
				}
				
				public static Quaternion LoadQuaternion(JsonTextReader reader) {
					Quaternion rotation = new Quaternion(0,0,0,0);
					if(reader == null) return rotation;
					string currVal = "";
					while(reader.Read()) {
						if(reader.Value!=null) {
							if(reader.TokenType == JsonToken.PropertyName) currVal = (string)reader.Value;
							else {
								switch(currVal) {
									case "x": rotation.x = (float)(double)reader.Value; break;
									case "y": rotation.y = (float)(double)reader.Value; break;
									case "z": rotation.z = (float)(double)reader.Value; break;
									case "w": rotation.w = (float)(double)reader.Value; break;
									default: break;
								}
							}
						} else if(reader.TokenType == JsonToken.EndObject) return rotation;
					}
					return rotation;
				}

When we find the end of the object (in this case just the sun) we want to create a new instance of our Sun in the scene, using the position and rotation found. Then we want to set the scale value appropriately as well. Unfortunately, if we were to run this without any changes to our project we would encounter a number of problems. If you cast your memory back to Part 10 you may recall that we set up a GameObjectList to track all of the valid game objects that we can instantiate at runtime. When we created this we were careful to ensure that only one of these is present in the world at any given point in time. However, we only added one to the main scene of our game, since at that time we did not yet have any menus in place. This means that if we go to load our game from the Main Menu we will not find any Game Objects that we can create. To fix this we can add a new Empty Object to our MainMenu scene called GameObjectList and attach GameObjectList.cs to it. We then need to drag all of our Prefabs onto the appropriate fields so that it knows about them. Under Buildings I have Refinery and WarFactory. Then under Units I have Harvester, Tank, andWorker. Finally I have TankProjectile under World Objects and Player for the Player object.

Just having the GameObjectList in place is not enough to be able to load anything yet though. At the moment we have no Prefab for either the Sun (which we are trying to load here) or the Ground (which we will be attempting to load shortly). This is a problem since Unity does not like it if we try to instantiate a null object. Thankfully this is easy enough to fix. Open up the Map scene where we have been creating our world. Drag the Ground object from the Hierarchy into the Resources folder to create a Prefab for it. Do the same for the Sun object. While we are here we may as well place these into the GameObjectList for this scene (since ideally we would like to be able to launch from here while testing things, though I think this is broken at the moment). Now open the MainMenu scene and add these new Prefabs under Game Objects in our GameObjectList. If you run your game now you should be able to choose to load a game without errors showing up. Nothing will be visible yet, but you should be able to see a new object in the Hierarchy panel called Sun(Clone). So while we cannot see anything different in the scene, things are being loaded.

Now that we have our (simple) lighting being loaded it is time to load our terrain. Add the following code to LoadTerrain() in LoadManager.cs.

				private static void LoadTerrain(JsonTextReader reader) {
					if(reader == null) return;
					Vector3 position = new Vector3(0,0,0), scale = new Vector3(1,1,1);
					Quaternion rotation = new Quaternion(0,0,0,0);
					while(reader.Read()) {
						if(reader.Value != null) {
							if(reader.TokenType == JsonToken.PropertyName) {
								if((string)reader.Value == "Position") position = LoadVector(reader);
								else if((string)reader.Value == "Rotation") rotation = LoadQuaternion(reader);
								else if((string)reader.Value == "Scale") scale = LoadVector(reader);
							}
						} else if(reader.TokenType == JsonToken.EndObject) {
							GameObject ground = (GameObject)GameObject.Instantiate(ResourceManager.GetWorldObject("Ground"), position, rotation);
							ground.transform.localScale = scale;
							return;
						}
					}
				}

Since we have already done all of the hard work of making sure that objects are all present this code should just work. If you run your game you should be able to see that both the Sun and the Ground are being loaded correctly. The final thing we want to make sure is working in this section is the loading of the Camera, since we want to make sure the Player has the same view now as they had when the game was saved. This will be hard to validate at the moment, since we have no reference objects in the scene, but it will be easy enough to check later on. Add the following code to LoadCamera()

				private static void LoadCamera(JsonTextReader reader) {
					if(reader == null) return;
					Vector3 position = new Vector3(0,0,0), scale = new Vector3(1,1,1);
					Quaternion rotation = new Quaternion(0,0,0,0);
					while(reader.Read()) {
						if(reader.Value != null) {
							if(reader.TokenType == JsonToken.PropertyName) {
								if((string)reader.Value == "Position") position = LoadVector(reader);
								else if((string)reader.Value == "Rotation") rotation = LoadQuaternion(reader);
								else if((string)reader.Value == "Scale") scale = LoadVector(reader);
							}
						} else if(reader.TokenType == JsonToken.EndObject) {
							GameObject camera = Camera.mainCamera.gameObject;
							camera.transform.localPosition = position;
							camera.transform.localRotation = rotation;
							camera.transform.localScale = scale;
							return;
						}
					}
				}

and run your game to make sure that nothing has broken.

Load Resources

Now that we have the basic framework of our world being loaded it is time to turn our attention to more complicated things. We will start by enabling loading of the Resources on our map. Add the following code to LoadResources().

				private static void LoadResources(JsonTextReader reader) {
					if(reader == null) return;
					string currValue = "", type = "";
					while(reader.Read()) {
						if(reader.Value != null) {
							if(reader.TokenType == JsonToken.PropertyName) currValue = (string)reader.Value;
							else if(currValue == "Type") {
								type = (string)reader.Value;
								GameObject newObject = (GameObject)GameObject.Instantiate(ResourceManager.GetWorldObject(type));
								Resource resource = newObject.GetComponent< Resource >();
								resource.LoadDetails(reader);
							}
						}
						else if(reader.TokenType==JsonToken.EndArray) return;
					}
				}

Here we are looping through all of the Resources stored in our save file. For each one found we want to create a new Resource object and then load the saved details for that Resource into the newly created Resource. Before we can create a new OreDeposit, though, we need to create a Prefab and add it to the GameObjectList. This is done in the same way as we did for the Ground and the Sun earlier. Open the Map scene, drag your main OreDeposit object into the Resources folder, then add this Prefab to the World Objects list in your GameObjectList. Before going any further we need to create the LoadDetails() method. Since we want to load basic details for all World Objects (remember our Resource is a specfic type of World Object), we will add this to WorldObject.cs.

				public void LoadDetails(JsonTextReader reader) {
					while(reader.Read()) {
						if(reader.Value != null) {
							if(reader.TokenType == JsonToken.PropertyName) {
								string propertyName = (string)reader.Value;
								reader.Read();
								HandleLoadedProperty(reader, propertyName, reader.Value);
							}
						} else if(reader.TokenType == JsonToken.EndObject) {
							//loaded position invalidates the selection bounds so they must be recalculated
							selectionBounds = ResourceManager.InvalidBounds;
							CalculateBounds();
							loadedSavedValues = true;
							return;
						}
					}
					//loaded position invalidates the selection bounds so they must be recalculated
					selectionBounds = ResourceManager.InvalidBounds;
					CalculateBounds();
					loadedSavedValues = true;
				}

The idea here is that we want to find each of the properties that we saved for the World Object and then handle parsing those values. We will do this in a separate method because a) it makes our code cleaner and easier to maintain and b) that way we can handle some of those values in a child class. Once all of the properties have been read we want to recalculate the bounds of the object (since it will probably have shifted since being created) and then set a flag on the object to say that we have loaded saved values. We should create this variable at the top of WorldObject.cs now so that we do not forget.

				protected bool loadedSavedValues = false;

It turns out that we need to have this in case some particular World Objects are doing any initialisation inside their Start() methods that would override values loaded from the save file. This is the case for our Resource, as we will see in just a moment. The core handling of loaded properties also needs to go into WorldObject.cs.

				protected virtual void HandleLoadedProperty(JsonTextReader reader, string propertyName, object readValue) {
					switch(propertyName) {
						case "Name": objectName = (string)readValue; break;
						case "Id": ObjectId = (int)(System.Int64)readValue; break;
						case "Position": transform.localPosition = LoadManager.LoadVector(reader); break;
						case "Rotation": transform.localRotation = LoadManager.LoadQuaternion(reader); break;
						case "Scale": transform.localScale = LoadManager.LoadVector(reader); break;
						case "HitPoints": hitPoints = (int)(System.Int64)readValue; break;
						case "Attacking": attacking = (bool)readValue; break;
						case "MovingIntoPosition": movingIntoPosition = (bool)readValue; break;
						case "Aiming": aiming = (bool)readValue; break;
						case "CurrentWeaponChargeTime": currentWeaponChargeTime = (float)(double)readValue; break;
						case "TargetId": loadedTargetId = (int)(System.Int64)readValue; break;
						default: break;
					}
				}

For each property we are either casting the value to the desired type or reading the complex object required (e.g. Vector). Note that we have an entry here for each property that we stored in the SaveDetails() method last time. This should always be the case. If a new property is added to the save file then we should add a corresponding case in HandleLoadedProperty() to make sure that value is also loaded. The one special case we have here is for TargetId, since this refers to the id value of a target the WorldObject may have. We need to add

				private int loadedTargetId = -1;

to the top of WorldObject.cs to be able to store the value. Then we need to update theStart() method (which will be called after we have finished loading, for all objects) to

				protected virtual void Start () {
					SetPlayer();
					if(player) {
						SetTeamColor();
						if(loadedSavedValues && loadedTargetId >= 0) target = player.GetObjectForId(loadedTargetId);
					}
				}

so that if an id value was stored for the target object then we load the reference to that object again. While we are at it we also need to add the method GetObjectForId() to Player.cs.

				public WorldObject GetObjectForId(int id) {
					WorldObject[] objects = GameObject.FindObjectsOfType(typeof(WorldObject)) as WorldObject[];
					foreach(WorldObject obj in objects) {
						if(obj.ObjectId == id) return obj;
					}
					return null;
				}

Since we are saving at least one specific field for our Resource we also need to add an override of HandleLoadedProperty() to Resource.cs.

				protected override void HandleLoadedProperty (JsonTextReader reader, string propertyName, object readValue) {
					base.HandleLoadedProperty (reader, propertyName, readValue);
					switch(propertyName) {
						case "AmountLeft": amountLeft = (float)(double)readValue; break;
						default: break;
					}
				}

The other thing we need to do is to adjust the Start() method in Resource.cs to make sure that the loaded value for amountLeft is not replaced by the default value. The resulting method should look as follows.

				protected override void Start () {
					base.Start();
					resourceType = ResourceType.Unknown;
					if(loadedSavedValues) return;
					amountLeft = capacity;
				}

If you run your game now you should be able to see the OreDeposit being loaded up correctly. To test things further you could harvest some (maybe half) of your OreDeposit, save, then load that saved game to see whether the OreDeposit looks smaller. Unfortunately selection of things in your game will not work yet since we have not loaded any Player so far.

Load Players

The final step of our loading process is to make sure that we load all of the Players, along with everything that they currently own / control. Start by adding this code to LoadPlayers() in LoadManager.cs.

				private static void LoadPlayers(JsonTextReader reader) {
					if(reader == null) return;
					while(reader.Read()) {
						if(reader.TokenType==JsonToken.StartObject) {
							GameObject newObject = (GameObject)GameObject.Instantiate(ResourceManager.GetPlayerObject());
							Player player = newObject.GetComponent< Player >();
							player.LoadDetails(reader);
						} else if(reader.TokenType==JsonToken.EndArray) return;
					}
				}

This is very straightforward since we simply want to find each Player and get it to load the details stored for it. So most of the logic for loading a Player will be found in LoadDetails() inside Player.cs, which we should create now.

				public void LoadDetails(JsonTextReader reader) {
					if(reader == null) return;
					string currValue = "";
					while(reader.Read()) {
						if(reader.Value!=null) {
							if(reader.TokenType == JsonToken.PropertyName) {
								currValue = (string)reader.Value;
							} else {
								switch(currValue) {
									case "Username": username = (string)reader.Value; break;
									case "Human": human = (bool)reader.Value; break;
									default: break;
								}
							}
						} else if(reader.TokenType == JsonToken.StartObject || reader.TokenType == JsonToken.StartArray) {
							switch(currValue) {
								case "TeamColor": teamColor = LoadManager.LoadColor(reader); break;
								case "Resources": LoadResources(reader); break;
								case "Buildings": LoadBuildings(reader); break;
								case "Units": LoadUnits(reader); break;
								default: break;
							}
						} else if(reader.TokenType==JsonToken.EndObject) return;
					}
				}

A Player has certain properties that we want to simply set, and then there are some more complex values in the save file that we need to handle. The first extra method that we need to add is the helper method LoadColor() in LoadManager.cs.

				public static Color LoadColor(JsonTextReader reader) {
					if(reader == null) return;
					Color color = new Color(0,0,0,0);
					string currVal = "";
					while(reader.Read()) {
						if(reader.Value!=null) {
							if(reader.TokenType == JsonToken.PropertyName) currVal = (string)reader.Value;
							else {
								switch(currVal) {
									case "r": color.r = (float)(double)reader.Value; break;
									case "g": color.g = (float)(double)reader.Value; break;
									case "b": color.b = (float)(double)reader.Value; break;
									case "a": color.a = (float)(double)reader.Value; break;
									default: break;
								}
							}
						} else if(reader.TokenType == JsonToken.EndObject) return color;
					}
					return color;
				}

Next we need to make sure that a Player can load any Resources they control by adding LoadResources() to Player.cs.

				private void LoadResources(JsonTextReader reader) {
					if(reader == null) return;
					string currValue = "";
					while(reader.Read()) {
						if(reader.Value != null) {
							if(reader.TokenType == JsonToken.PropertyName) currValue = (string)reader.Value;
							else {
								switch(currValue) {
									case "Money": startMoney = (int)(System.Int64)reader.Value; break;
									case "Money_Limit": startMoneyLimit = (int)(System.Int64)reader.Value; break;
									case "Power": startPower = (int)(System.Int64)reader.Value; break;
									case "Power_Limit": startPowerLimit = (int)(System.Int64)reader.Value; break;
									default: break;
								}
							}
						} else if(reader.TokenType == JsonToken.EndArray) {
							return;
						}
					}
				}

It turns out that we want to know what the initial limits on the Resources are, as well as how much the Player has of each Resource. At the moment we are not saving these, so we need to make sure we remedy this problem before we carry on. Change SavePlayerResources() in SaveManager.cs to be the following code

				public static void SavePlayerResources(JsonWriter writer, Dictionary< ResourceType, int > resources, Dictionary< ResourceType, int > resourceLimits) {
					if(writer == null) return;
					
					writer.WritePropertyName("Resources");
					writer.WriteStartArray();
					foreach(KeyValuePair< ResourceType, int > pair in resources) {
						writer.WriteStartObject();
						WriteInt(writer, pair.Key.ToString(), pair.Value);
						writer.WriteEndObject();
					}
					foreach(KeyValuePair< ResourceType, int > pair in resourceLimits) {
						writer.WriteStartObject();
						WriteInt(writer, pair.Key.ToString() + "_Limit", pair.Value);
						writer.WriteEndObject();
					}
					writer.WriteEndArray();
				}

and then update the call to this in SaveDetails() in Player.cs to be as follows.

				SaveManager.SavePlayerResources(writer, resources, resourceLimits);

That should do it nicely, though we will not be able to test that (either the saving or the loading) just yet. Next we need to add a definition for LoadBuildings() to Player.cs.

				private void LoadBuildings(JsonTextReader reader) {
					if(reader == null) return;
					Buildings buildings = GetComponentInChildren< Buildings >();
					string currValue = "", type = "";
					while(reader.Read()) {
						if(reader.Value != null) {
							if(reader.TokenType == JsonToken.PropertyName) currValue = (string)reader.Value;
							else if(currValue == "Type") {
								type = (string)reader.Value;
								GameObject newObject = (GameObject)GameObject.Instantiate(ResourceManager.GetBuilding(type));
								Building building = newObject.GetComponent< Building >();
								building.LoadDetails(reader);
								building.transform.parent = buildings.transform;
								building.SetPlayer();
								building.SetTeamColor();
								if(building.UnderConstruction()) {
									building.SetTransparentMaterial(allowedMaterial, true);
								}
							}
						}
						else if(reader.TokenType==JsonToken.EndArray) return;
					}
				}

As we have seen before, we are finding each Building in the saved list, creating one of them, and then getting it to load up the correct details. There are a couple of extra things we are doing here, though. We want to find the Buildings object that belongs to the Player and make sure that the newly created Building belongs to this (e.g. the Player now owns the Building). Once that has been done we want to make sure that the Team Color is set. This is because if the Building is under construction we need to make it transparent here, since it is only our Player that knows what the appropriate transparent colours are. To allow us to set the Team Color we need to make sure that we change the visiblity of SetTeamColor() in WorldObject.cs to be public. We also need to update Start() in WorldObject.cs to make sure that we do not reset the Team Color if we have loaded values.

				protected virtual void Start () {
					SetPlayer();
					if(player) {
						if(loadedSavedValues) {
							if(loadedTargetId >= 0) target = player.GetObjectForId(loadedTargetId);
						} else {
							SetTeamColor();
						}
					}
				}

Finally, we need to add a definition for LoadUnits() to Player.cs.

				private void LoadUnits(JsonTextReader reader) {
					if(reader == null) return;
					Units units = GetComponentInChildren< Units >();
					string currValue = "", type = "";
					while(reader.Read()) {
						if(reader.Value != null) {
							if(reader.TokenType == JsonToken.PropertyName) currValue = (string)reader.Value;
							else if(currValue == "Type") {
								type = (string)reader.Value;
								GameObject newObject = (GameObject)GameObject.Instantiate(ResourceManager.GetUnit(type));
								Unit unit = newObject.GetComponent< Unit >();
								unit.LoadDetails(reader);
								unit.transform.parent = units.transform;
								unit.SetPlayer();
								unit.SetTeamColor();
							}
						}
						else if(reader.TokenType==JsonToken.EndArray) return;
					}
				}

This is almost identical to what we have done for loading Buildings, except that we are using Units instead. Before we can actually test that our loading is working we also need to provide a definition for HandleLoadedProperty() to all of the child classes of WorldObject that store extra details. Let's start with Building.cs.

				protected override void HandleLoadedProperty (JsonTextReader reader, string propertyName, object readValue) {
					base.HandleLoadedProperty (reader, propertyName, readValue);
					switch(propertyName) {
						case "NeedsBuilding": needsBuilding = (bool)readValue; break;
						case "SpawnPoint": spawnPoint = LoadManager.LoadVector(reader); break;
						case "RallyPoint": rallyPoint = LoadManager.LoadVector(reader); break;
						case "BuildProgress": currentBuildProgress = (float)(double)readValue; break;
						case "BuildQueue": buildQueue = new Queue< string >(LoadManager.LoadStringArray(reader)); break;
						case "PlayingArea": playingArea = LoadManager.LoadRect(reader); break;
						default: break;
					}
				}

There are two new methods that we need to create because of this. The first is the call to LoadStringArray() which we need to add to LoadManager.cs.

				public static List< string > LoadStringArray(JsonTextReader reader) {
					List< string > values = new List< string> ();
					while(reader.Read()) {
						if(reader.Value!=null) {
							values.Add((string)reader.Value);
						} else if(reader.TokenType == JsonToken.EndArray) return values;
					}
					return values;
				}

The second is LoadRect() which we also need to add to LoadManager.cs.

				public static Rect LoadRect(JsonTextReader reader) {
					Rect rect = new Rect(0,0,0,0);
					if(reader == null) return rect;
					string currVal = "";
					while(reader.Read()) {
						if(reader.Value!=null) {
							if(reader.TokenType == JsonToken.PropertyName) currVal = (string)reader.Value;
							else {
								switch(currVal) {
									case "x": rect.x = (float)(double)reader.Value; break;
									case "y": rect.y = (float)(double)reader.Value; break;
									case "width": rect.width = (float)(double)reader.Value; break;
									case "height": rect.height = (float)(double)reader.Value; break;
									default: break;
								}
							}
						} else if(reader.TokenType == JsonToken.EndObject) return rect;
					}
					return rect;
				}

Of course, if we are going to load the playing area, we need to make sure that we are also saving the playing area when necessary, since this is not a value that we are currently storing. To do this add

				if(needsBuilding) SaveManager.WriteRect(writer, "PlayingArea", playingArea);

to the end of SaveDetails() in Building.cs and then add

				public static void WriteRect(JsonWriter writer, string name, Rect rect) {
					if(writer == null) return;
					
					writer.WritePropertyName(name);
					writer.WriteStartObject();
					writer.WritePropertyName("x");
					writer.WriteValue(rect.x);
					writer.WritePropertyName("y");
					writer.WriteValue(rect.y);
					writer.WritePropertyName("width");
					writer.WriteValue(rect.width);
					writer.WritePropertyName("height");
					writer.WriteValue(rect.height);
					writer.WriteEndObject();
				}

to SaveManager.cs to handle the actual saving of a Rect.

It turns out that none of the specific Building types I have created so far have anything extra being saved so we can now turn our attention to Unit.cs.

				protected override void HandleLoadedProperty (JsonTextReader reader, string propertyName, object readValue) {
					base.HandleLoadedProperty (reader, propertyName, readValue);
					switch(propertyName) {
						case "Moving": moving = (bool)readValue; break;
						case "Rotating": rotating = (bool)readValue; break;
						case "Destination": destination = LoadManager.LoadVector(reader); break;
						case "TargetRotation": targetRotation = LoadManager.LoadQuaternion(reader); break;
						case "DestinationTargetId": loadedDestinationTargetId = (int)(System.Int64)readValue; break;
						default: break;
					}
				}

Again this is all very straightforward. Once more we are loading an id value for a linked object, so add

				private int loadedDestinationTargetId = -1;

to the top of Unit.cs and add the check

				if(player && loadedSavedValues && loadedDestinationTargetId >= 0) {
					destinationTarget = player.GetObjectForId(loadedDestinationTargetId).gameObject;
				}

to the end of Start() in Unit.cs.

Our Harvester is saving lots of details, so we need to add the appropriate code to handle loading these to Harvester.cs now.

				protected override void HandleLoadedProperty (JsonTextReader reader, string propertyName, object readValue) {
					base.HandleLoadedProperty (reader, propertyName, readValue);
					switch(propertyName) {
						case "Harvesting": harvesting = (bool)readValue; break;
						case "Emptying": emptying = (bool)readValue; break;
						case "CurrentLoad": currentLoad = (float)(double)readValue; break;
						case "CurrentDeposit": currentDeposit = (float)(double)readValue; break;
						case "HarvestType": harvestType = WorkManager.GetResourceType((string)readValue); break;
						case "ResourceDepositId": loadedDepositId = (int)(System.Int64)readValue; break;
						case "ResourceStoreId": loadedStoreId = (int)(System.Int64)readValue; break;
						default: break;
					}
				}

This requires that we add the following variables to the top of Harvester.cs so that we can link objects.

				private int loadedDepositId = -1, loadedStoreId = -1;

We then need to update Start() in Harvester.cs to perform the linking.

				protected override void Start () {
					base.Start();
					if(loadedSavedValues) {
						if(player) {
							if(loadedStoreId >= 0) {
								WorldObject obj = player.GetObjectForId(loadedStoreId);
								if(obj.GetType().IsSubclassOf(typeof(Building))) resourceStore = (Building)obj;
							}
							if(loadedDepositId >= 0) {
								WorldObject obj = player.GetObjectForId(loadedDepositId);
								if(obj.GetType().IsSubclassOf(typeof(Resource))) resourceDeposit = (Resource)obj;
							}
						}
					} else {
						harvestType = ResourceType.Unknown;
					}
				}

There is also another method that we need to add to WorkManager.cs to help us find the desired ResourceType given a name.

				public static ResourceType GetResourceType(string type) {
					switch(type) {
						case "Money": return ResourceType.Money;
						case "Power": return ResourceType.Power;
						case "Ore": return ResourceType.Ore;
						default: return ResourceType.Unknown;
					}
				}

Our Tank also has an extra value it is saving, so we need to add the following code to Tank.cs.

				protected override void HandleLoadedProperty (JsonTextReader reader, string propertyName, object readValue) {
					base.HandleLoadedProperty (reader, propertyName, readValue);
					switch(propertyName) {
						case "AimRotation": aimRotation = LoadManager.LoadQuaternion(reader); break;
						default: break;
					}
				}

Finally, our Worker has some extra properties being saved, so the following code needs to be added to Worker.cs.

				protected override void HandleLoadedProperty (JsonTextReader reader, string propertyName, object readValue) {
					base.HandleLoadedProperty (reader, propertyName, readValue);
					switch(propertyName) {
						case "Building": building = (bool)readValue; break;
						case "AmountBuilt": amountBuilt = (float)(double)readValue; break;
						case "CurrentProjectId": loadedProjectId = (int)(System.Int64)readValue; break;
						default: break;
					}
				}

Once more we need to add an extra variable to the top of Worker.cs to handle a linked object

				private int loadedProjectId = -1;

and then the following check to the end of Start() in Worker.cs to handle the actual linking.

				if(player && loadedSavedValues && loadedProjectId >= 0) {
					WorldObject obj = player.GetObjectForId(loadedProjectId);
					if(obj.GetType().IsSubclassOf(typeof(Building))) currentProject = (Building)obj;
				}

That covers all of the special loading cases, so you should be able to run your game, load a saved game, and see everything appear where it should be. However, there is a serious issue that we need to address before we can move on. All of our checks for whether an object is the Ground or not are failing. This is because we based them on whether the name of the object is "Ground" - and for a loaded level our Ground now has the name "Ground(Clone)". We will actually make use of this opportunity to tidy up this check and make it so that the actual logic is only happening in one place. Add the following method to WorkManager.cs.

				public static bool ObjectIsGround(GameObject obj) {
					return obj.name == "Ground" || obj.name == "Ground(Clone)";
				}

That performs the logic of what we want for now. This may change at a later date, but it will do for the moment. And at least now we have a clear place as to where to change this if we need to. Now we have to add a call to this method in every place where we want to perform this check. In CanPlaceBuilding() Player.cs replace

				hitObject.name != "Ground"

with

				!WorkManager.ObjectIsGround(hitObject)

In WorldObject.cs replace

				hitObject && hitObject.name != "Ground"

in MouseClick() with

				!WorkManager.ObjectIsGround(hitObject)

and

				hoverObject.name != "Ground"

in SetHoverState() with

				!WorkManager.ObjectIsGround(hoverObject)

In Building.cs replace

				hoverObject.name == "Ground"

in SetHoverState() with

				WorkManager.ObjectIsGround(hoverObject)

and

				hitObject.name == "Ground"

in MouseClick() with

				WorkManager.ObjectIsGround(hitObject)

In Unit.cs replace

				hoverObject.name == "Ground"

in SetHoverState() with

				WorkManager.ObjectIsGround(hoverObject)

and

				hitObject.name == "Ground"

in MouseClick() with

				WorkManager.ObjectIsGround(hitObject)

In Harvester.cs replace

				hoverObject.name != "Ground"

in SetHoverState() with

				!WorkManager.ObjectIsGround(hoverObject)

and

				hitObject.name != "Ground"

in MouseClick() with

				!WorkManager.ObjectIsGround(hitObject)

Finally in Worker.cs replace

				hitObject && hitObject.name!="Ground"

in MouseClick() with

				!WorkManager.ObjectIsGround(hitObject)

And with that in place we should finally be able to load all of the relevant details from a saved game and then run that game with no issues (or very few issues anyway, since there are always bugs that I inevitably miss). This has been another long part, but I hope you agree that it has been worth it. As usual, all of the code for this part can be found on github under the commit for Part 19.

<
>