Saving and Loading Tilemaps - Unity Tutorial

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
If you create a level for your game, Unity will save it automatically. But what if there will be changes during runtime or even better, the player creates a big part of the tilemap? Well then you need to save it on your own. But don't worry, creating a basic saving and loading system is quite easy, of course the details will take more time, but this is your part, not mine, so let's start. All you need to save tilemaps is of course some way to edit a tilemap during runtime. In my case I use the code from my tutorial series about InGame Tilemap Editing. Here we can draw different tiles onto different tilemaps, so the result of this video will also work for all your existing tilemaps in your scene - yey. I start by adding two buttons, one for saving and one for loading. You can do this now however you want, but at least you need some way to trigger saving and loading. Of course this can also happen during exit and on start or something like this. Now to add some click events we need a new script first. Let's call it SaveHandler. And now add public methods for saving and loading. We will leave them empty for now. Also import the "UnityEngine.Tilemaps" package at the top. We will need it. I also add three variables: the first one is a private dictionary. You will see its use case soon. Just add the line like I did. The second one is a variable we need to fill in the inspector, so make it serialized. It is of type BoundsInt and will be the border of the tilemap area we are saving. More explanation later. And the third one is the filename for the file which will be saved and loaded. I make it serialized and also give it a default value. Make sure to always add the file type of "json" as well. Now… to save the content of one or more tilemaps we need those tilemaps. Of course you can just use a prefilled list where you add the maps. But in our case the tilemaps are generated during runtime based on the different layers we can draw onto. So I will show you how to get all the tilemaps from your scene. The initTilemaps function will be called on start. I will use the FindObjectsOfType method. This will now return all Tilemaps that exist in the scene. Just make sure to use "Objects" which returns an array. Explicit type declaration helps with errors like this, like you can see here. Now instead of just saving the list, I will rather save this data in the dictionary from earlier. By using the name of the tilemaps object it is easier to find the according map on loading, because the name will be our unique identifier for the different maps during saving. Now be aware that you can have tilemaps with the same name - this will lead to errors in this script. So if this is true in your case, you need to expand your identifier by something besides the name. Just make sure that it stays the same during play sessions. I loop through the retrieved maps and add the entry to the dictionary. Now that's nice, can work, sometimes. The thing here is, like I said, the tilemaps get created during runtime. In the Start Method of the according script. And we retrieve them also in a Start method. So we need to make sure that this method here is being executed later that the one that created the maps, right? It's actually quite easy. In Unity go to Edit > Project Settings. Now find the "Script Execution Order". Every script that is not mentioned somewhere here will be executed during this "Default Time" slot. So let's just add our SaveHandler and put it at the end, that's fine, as long as it's below the "Default Time". And while we are here, let's prepare the buttons. At first add the script to your scene, I will add it to my universal manager object. Then you can go to your buttons and add a new onClick event. Drag and drop your manager object into the GameObject slot and now you can look for your script and the according method. If you can't see your methods make sure they are public. Repeat this for your load button as well. Now that this is prepared let's switch back to the script. During start, all the tilemaps will be saved to the dictionary. So on load or save we can easily work with them. Of course, if you add or remove tilemaps during runtime you need to call the initTilemaps method before executing the saving or loading methods. For the saving part: I highly recommend you to watch my tutorial about Saving & Loading with JSON Files. We will use the FileHandler script I explain in this tutorial for this saving process as well. So it will definitely help your understanding of the process and might help you adapting the process to your own needs. But if you don't want to, you will find the script linked in the description. But the following explanations assume you know about this video. And not a requirement but recommended is my tutorial about saving high score lists. This will show you another use case on how to use the FileHandler script. At first we need to define a format which will be saved. We do this by creating a native C# class. Let's start by importing the "System" package. That we can convert a class to a JSON object, the class needs to have this Serializable tag above. Therefore the import of "System". The following variables now also need to be public. Our first one is the key. This must be the key of the dictionary we created earlier. Remember: In my case this is the name, but in all cases it should be a string. This variable will be used to search in the dictionary and to retrieve the according tilemap when loading. This is much faster than looking for a matching value in a list. The second variable is a list of type TileInfo which will hold all the tiles being used. What is TileInfo you might ask? This. A new class. Every single tile will be represented by an object of this class. It consists of the TileBase and the position in the tilemap as Vector3Int. That it's easier for me later to create instances of this class I also add a constructor here where I assign the given parameters to the variables. So for every tilemap that exists in our scene, we instantiate an object of type TilemapData. Then for every filled tile, we create an object of type TileInfo with the information of the position and the used TileBase. Then every TileInfo object will be saved in our TilemapData object. That should then be all information you need for one tilemap. Now let's code the process I just explained. Since we are working with several tilemaps - but don't worry it also works with one - we want to save all those TilemapData objects to a list. This list will then later be passed to the FileHandler script from my mentioned tutorial. For each tilemap in our tilemap dictionary: We create a new object of TilemapData. We don't have a constructor here, because it is not necessary. So I can only assign the key like this. Looping through a dictionary will retrieve an object where you can use .Value and .Key. So we can easily assign the key of the dictionary to this variable like this. Now we want to check the tilemap for existing tiles. For this you need some kind of boundaries. You can't just say "give me everything". You can either create those border manually, like I will show next, or you retrieve them automatically from your tilemap like this code snippet does. But let's do it by hand, so you have a chance to understand it better. Taking a look at my game, I can't pan. So my boundaries are quite simple. The center of my tilemap is also the center of my screen, so my width is currently like 1, 2, 3, 4, 5, 6, 7, 8, 9 tiles wide in one direction or 18 in total and and 1, 2, 3, 4, 5 - 10 tiles tall. Earlier we created that BoundsInt variable, remember? This is where you need to define those borders. Choose the lowest position on the bottom left. In my case this would be -9 for x and -5 for y. To reach to the top right just set the full size: here 18 and 10. For me, my tilemaps are all located at 0 for the z-axis. Please make sure to set them all to 0 or to change the boundaries value here, if your tilemaps are located differently. Now back in the code we can loop through all the (x,y) combinations of the given boundaries. This looks like this: with two for-loops. If your z-axis is not zero, you need to add this value to the loop as well - or if it is one static value - like 0 in my case - for all your tilemaps, just follow along and replace the zero in the upcoming variable. Since we want to save the position as Vector3Int, let's create a variable for it and use the values from the loops for x and y. I set the z-axis to zero. Change it if needed. Now we can retrieve the TileBase based on the position variable. At this point we either retrieve a TileBase or a null value. Saving null values might not be a great idea so let's add this if statement. If we retrieve an actual TileBase we can create an object for it of type TileInfo. Thanks to the constructor we can just pass the two parameters for the TileBase itself and the position to the brackets here. Then we can add this object to the list of TileInfo objects which exists in our TilemapData class. This list already exists because we not only declared it but also initialized it here. Now this will happen for all the tilemaps and all the tiles inside the given boundaries. But right at the end of our foreach loop, before the code jumps to the next tilemap, let's add this TilemapData object to the list we created at the start of this onSave method. Great, now we should have all the data we need. At least I do - like I said you might to need to add some more variables for extra data. But let's move on for now and you will later see if you need additional information. So I mentioned my FileHandler script from the JSON tutorial. Grab it from your storage or get it from the GitHub link in the description. Either way you need to drag and drop it into your scripts folder. Now we can just call the script from any other script by its classname. That works because it is static. We call the "SaveToJSON" method and assign the type of our list "TilemapData" as type. The method takes two arguments: the data itself and the filename. Now if you made everything correct the saving will work. Let's start play mode. I then draw on two of my tilemaps. Here in the hierarchy you can see all my five tilemaps. That means in the saved JSON we will then see five "TilemapData" objects, but only two of them will have entries of type "TileInfo". Now let's check it. I will click the save button. This logs the path where the file is saved, which comes from a line in the FileHandler script. So let's open this file. With some auto formatting it also looks more readable. Here we have the three empty entries and the two filled ones. "tiles", the array of the "TileInfo" objects, is here. But what happened to the file type we defined as TileBase? This is what a TileBase looks like during runtime if you debug it. Looks a bit different, doesn't it? Well what we can see in the json is - obviously - the instanceID. Ok, but what does this mean? The instance id of an object is an unique identifier. So based on this id you can find unambiguously an object. Those Ids here reference to the tiles that have been created earlier and it's quite simple to use them. But this approach comes with one downside: you can only use the instance id if you do NOT generate the tiles during runtime. Everything generated during runtime will have a different instance id every time. In this case you need to use something constant as reference for those tiles, like the position from the sprite sheet they are coming from or whatever you use. This won't be part of the tutorial. On load, the first thing we need to do, is to retrieve the parsed json file. Fortunately the FileHandler script will handle this part. Because we want to retrieve a list we also need to call the method which reads a list. The magic in the background: Remember the list we saved, where the TileBase contained some more information? Well when transforming the json back to the list, the InstanceID gets automatically converted to the TileBase object again. The saved and the loaded list look the same, taking a look in the debugger. That's the really big advantage of using the instanceId. Now we can loop through the data. Since we saved the dictionary key in the data, we can now easily retrieve the according tilemap for the data entry we have. But beforehand we should check if the key exists in the dictionary or else the code would throw an exception. We will prevent this error by check it. If it does not exist I will simply log an error message and skip the rest of this loop iteration and therefore this entry. Because we did not save the null-values let's clear the whole tilemap before drawing back onto it. Now I can create a variable which tilemap I want to fill. If there are entries, I loop trough the tiles list which contains the position and the TileBase. This one line will now fill the timemap with the data saved in the json. Well yeah, basically that's it. For all the tilemaps we assign the single tile values and there is nothing more to do here. Let's give it a try in play mode. I hope everything works for you - if not, try again, rewatch the tutorial or compare your own code with the GitHub repo, also works wonders and there are some additional comments. Of course and as usual this tutorial and script only provides the basic use case and I hope it is enough to help you to understand how you can save tilemap data or also other data in Unity. Thank you for watching and also thank you to everyone supporting me and the channel. Special thanks to my supporters on Patreon: Pat Rick & nuxvomo. Thank you all. And as usual: If you enjoyed the content consider liking and subscribing and I'll see you next time.
Info
Channel: Velvary
Views: 389
Rating: undefined out of 5
Keywords:
Id: icBN5Cr-kFk
Channel Id: undefined
Length: 16min 59sec (1019 seconds)
Published: Sat Nov 06 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.