City Builder structure and save/load

Hi everybody,

Im a newbie and I started working on a City Builder where replacing parts of buildings is a key feature.
Im looking for help to try understand what are the best practices/structures for allowing players to swap building parts like a roof (replacing childprefabs) and having a saving/loading method that can save/load the latest state of the buildings on a grid.
Any simple example or reference scripts/videos would be really helpful!
Thank you for your help!

The bottom line of something like this is that the model-view approach is key. Ergo, you need to be able to represent a building (and your entire city) as purely data (no visuals). Notably, this data can’t have any direct references to Unity objects if you want to use this for save data as they can’t be written to disk (so you need ID’s to look up the assets, or indirect references).

You then use this pure data to build the visual representation.

Since your city/buildings are already purely data, this can be written to disk, and used to restore the state at a later point in time.

It’s a principle that underlies a lot of games from RTS games, city builder/simulations games, and other games like Minecraft.

There are probably resources for this out there though I don’t know any off the top of my head. In any case your data structure will depend on the project specifics. If this does seem complicated, it will be due to not being particularly beginner stuff. A city builder is definitely a tall ask for a beginner.

3 Likes

Just to provide an example. Assuming your building can be composed of tens of different prefabs for each floor, from cellar to roof. Then what you need to save is “which prefab is used for each floor”?

You could save references to each prefab asset, like its path, and that’ll work - but it’ll break your player’s savegames if you ever change your project structure and thus the prefab paths change. Also, a path is a string with possibly hundreds of bytes of data saved per floor.

A better solution is to make one or several ScriptableObject that has nothing but a List in it. To this you assign the building prefabs. Then you only need to store the prefab’s index in this list for each store. That’s just four bytes now - or even two or just one byte depending on the number of possible prefabs (65k vs 255) and savegame file size optimization requirements.

Reordering this list will still break player savegames but it’s more controllable and is less likely to happen by accident.

For both those reasons above is why I use a GUID/UUID type piece of data - similar to how Unity produces a GUID for all our assets - in order to save references to assets. Even so far as having an ‘indirect reference’ system which contains the implementation for looking up said asset on an as-need basis. Can even assign these indirect references via the inspector.

More work than simple index or path saving, but doesn’t break when renaming or reordering your asset structure.

Absolutely!

It’s one of those tradeoffs. GUID of course will increase the size per instance by tenfold but often enough, it’s more important to have the savegame work 100% reliably.

1 Like

To give you more context I managed to get into a place where replacing childprefab works and I manage to save it and load it properly. The problem I got is that after I load I cannot find a way of telling the save file about the childprefab replacement.

Any suggestions about a simple structure that records changes for child prefabs in a grid based game that make PlaceObject, SavePlacedObject and LoadPlacedObject methods coherent?
The basis of my data structure are two ScriptableObjects: one for preset objects and one for prefabs (which hold the parts for customization).
Attached the best logic I got so far and included all the building data components Im recording for context

Thank you all!

ObjectPlacer.cs (10.5 KB)

Modifications (particularly visual ones) need to be saved additively as additional pieces of data, which can can be used to modify the visual representation initially produced by the backing data.

A super-rough spit ball of what I mean:

public class Building // non monobehaviour
{
	[SerializeField]
	private BuilingID _prefabID;
	
	[System.NonSerialized]
	private BuildingComponent _component;
	
	[SerializeReference]
	private List<IBuildingModification> _modifications = new();
	
	public void InitializeVisuals(CityGrid grid)
	{
		_component = // look up prefab, work out position in world, instantiate, etc etc
		foreach (var modification in _modifications)
		{
			ApplyModification(modification);
		}
	}
	
	public void AddModification(IBuildingModification modification)
	{
		_modifications.Add(modification);
		ApplyModification(modification);
	}
	
	private void ApplyModification(IBuildingModification modification)
	{
		modification.ApplyModification(_component);
	}
}

public interface IBuildingModification
{
	void ModifyBuilding(BuildingComponent building);
}

public sealed class BuildingModificationColor : IBuildingModification
{
	[SerializeField]
	private Color _color;
	
	public void ModifyBuilding(BuildingComponent building)
	{
		// apply colour to building
	}
}

Not meant to be functioning code, just meant to showcase what I mean.

In any case it doesn’t change the advice given. Like we mentioned it’s about having a separate data model to your visuals. Namely, your data comes first, from which you build the visual representation. Starting with the view (in this context, game objects in a scene) and converting that into data isn’t going to be manageable moving forwards.

It definitely won’t boil down into one script as well. This stuff can cover dozens and dozens of scripts/classes.

1 Like