Data Persistence - Save & load your game state while avoiding common mistakes | Unity Tutorial

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
so you need to persist data across runs of your game huh you've come to the right place in this video we're gonna take a look at how you can do that without using player preps because that is not the right way to do it we'll start writing files to the disk and reading them specifically using the json format in this video later on because json is very easy to read we'll also take a look at how you can make it a little bit harder for your players to modify that data directly hey chris here from mom academy here to help you ooh me yes you make your game dev dreams become a reality persisting data across runs of your game is something that many games need because you want your player to be able to resume wherever they were if you have any kind of progression in your game rpgs maybe you need to store stuff like inventory states quests that have been completed their stats all this kind of stuff and you want them to be able to not have to get that at the beginning of each round of their game i've seen a bunch of tutorials telling you hey here's how you persist your game state using the player prefs and that's not the right solution and a lot of the other ones that don't tell you to use player preps tell you to use dangerous classes like the binary formatter playerprefs is designed to allow you to store data about preferences of your player so that way you can have things like customize audio settings customize graphics settings data that you can afford to lose without it really making an impact on your game the api for storing data with the player press also isn't very conducive to you using it for storing game state you can only set strings floats and ends having things like dictionaries lists this kind of stuff isn't really easy to do also the player press at least on windows is stored in the registry which isn't designed for large quantities of data and really isn't a great place for you to store that in the first place so if we shouldn't use player preps because it's not designed for that kind of data and it's stored in kind of questionable places how do we store data for persistent data enter in application.persistentdatapath this is a property on the application class that will tell us exactly where we can write files to and this path will not change as long as you're not changing the player company or name of your project changing those two things will affect where you're going to store this data so even across different versions of unity as long as you don't change those two things you're safe so we know where we can store data but how we store data is also an important consideration we're using text based serialization storing it in a json format which is really a human readable format this is convenient because there's a lot of tooling in place and c-sharp and many other languages to use json and xml types of serialization the downside to this is it's relatively slow compared to some other ways and because it's a human readable format for both json and xml your players will have a very easy time knowing what to modify if they want to kind of cheat in your game later on the video we'll talk about how you can make it harder not impossible but harder for where most people won't be able to do it just out of the box one key thing anytime you're talking about persisting data is if you're storing it on the player's local machine you just need to assume they have full control over that data and they have the ability to cheat modify that data because they have full control of the machine that that data is stored on if you want to make absolutely sure the player cannot modify their source data here you have to store that not on the player's machine for most of us most of the time it's probably fine to store it on their local machine it's also significantly cheaper because you have to pay for that storage all right with all the disclaimers out of the way let's talk about how do we actually do this json serialization first off i would not recommend using the json utility class that unity gives us it's relatively slow and it can't serialize everything into a json format which makes it kind of pointless for complex data types instead we can use the json.net library which is one of the most popular json serialization libraries in all of c sharp unity also provides us a package that will allow us to bring that into our unity project very easily i couldn't find it on the unity registry which i found a little bit weird so instead what you can do is go into your manifest.json file which you can find here edit that and add in this line of text and that will add that as a dependency that unity will automatically import for you i'm hopeful because i'm using unique 2021 lts that maybe in uni 2022 this will show up on the unity registry not really sure why it's not there this is how you do it even though it's not on the registry in the package manager the cool thing about using json.net here is we don't really have to do anything additional to make our classes able to be serialized into a file we can simply do json convert dot serialize object and deserialize object to convert from a string to an object and from an object to a string this makes it where it's really easy you don't have to think about it very much whenever you're thinking about how am i going to construct my data you keep in the same data model that you're actually using for your game and that's kind of it let's hop into the unity editor check out the sample scene and implement serializing some data with a json serializer in the unity editor you can see the sample scene that i have two buttons at the top one to serialize json and one that will just remove a file that's clear all data button in the middle i have something about enable encryption we're going to get into that a little bit later let's start off just simply how do we serialize some objects into a json format and save them on the disk if we click play you'll see the data in the format that we want to see realize we've got some player object that has health a name a base attack speed a level some unlock stuff equipment and an inventory so we're going to save all this data onto the disk in exactly this format actually remember the whole project is available on github you can go in there check out the repo download everything here to see it in depth of how i set it up the models and stuff like this aren't really important for what we're talking about today just that we have complex data types that are being stored into a json format so i'm going to create two new c sharp scripts one that's going to be an interface defining how we will store data and one of them will be the json implementation if you follow this format then you can implement new data storage formats now maybe a little bit later we'll talk about some of those the first one we'll call i data service we prefix it with an i indicating it's an interface that's just kind of like a c-sharp standard thing and i'll make a second one called json data service keeping the data service part so i know it's a concrete implementation of the i data service class in the data service because it's not a model behavior it's just going to be a simple c-sharp interface what i'm going to do is remove the extended model behavior change it to an interface delete all the middle stuff and define the interface i want to implement so what functions should any data service implement one it's going to return a bool whether it's successfully saved or not it's going to be called save data we're going to put angle bracket t angle bracket which is how we define this is a generic type here so that way we can save any arbitrary data type it's going to accept a string of relative path a t data so that's the data that we want to save that's of that t type and a bool encrypted and we're going to come back to that later for now we're just going to ignore that that's there i just don't have to come back and modify the interface later the second function will be load data we're going to follow the same generic thing here with t load data angle bracket t close angle bracket saying that we're going to return the generic type here it'll also accept that relative path and a bool whether it's encrypted data or not for getting us started i think that's all we need to do now we've defined the interface type let's go ahead and go over to the json data service and implement the concrete implementation of this interface on this one instead of mono behavior because we want this to be a simple c sharp class we're going to extend only the i data service you'll notice it's complaining so go ahead and implement that interface i'm going to rearrange them because we're going to start with saving first instead of clearing an exception let's actually do something useful so we'll start off getting the path that we want to write to remember that that's at application.persistentdatapath so we'll do string path equals application.persistentdatapath plus the relative path then we need to check a couple of cases one is if the file already exists then we're going to want to delete that and just write a new file if it does not yet exist then we're just going to write the data to that file so if the file does exist let's first do a try block to say if something fails here we're going to want to catch that exception we'll just do a debug log informing us that hey this file exists we're going to delete the old one and write a new one just so that we can kind of trace through the console what's happening here we'll delete the old file with file.delete at that path and we'll do using file stream stream equals file.create at that path and we're going to want to immediately close that stream if we don't close it immediately then we will not be able to write to it we'll get an exception saying this file is already in use because on the next line what we're going to do is file dot write all text passing in a path and then json convert which comes from the newtonsoft.jsonnamespace dot serialize object passing in the data and then we'll return true we'll also catch the exception e if anything bad happens in that whole process we're going to just debug log an error telling us what that error was and then return false that's really all we have to do to serialize a json piece of text this file.writealltext at the path and then json convert serialize object so json converts serialize object does all the heavy lifting of converting whatever data we have into that json format with a single line of code that's pretty cool in the case where a file does not exist that'd be the else case of our if block here we're still going to need to do the try catch so we'll do try in this case it's a little bit more simple we don't have to delete a file first so we'll just tell us that we're creating the file for the first time and then doing using file stream stream equals file.createpath passing in the path again again we're going to close that immediately and then write all the text to the file this is almost the exact same thing we did before just not deleting the file first which means we can actually condense this where the only thing different is we will first check if the file exists delete it otherwise we'll just log over creating the file for the first time and then do the exact same piece of code cool let's see how that looks in the demo in the demo class the first thing we'll do is create an idata service and initialize it to be a new json data service we'll create a new public void serialize json and we'll check in there if data service not save data passing in some path we'll just do forward slash playerstats.json passing the second argument being that playerstatsdata object and we'll pass an encryption enabled even though we know that does nothing so far because that returns true or false we can check else that means it failed to save and i mean log in error and said maybe the input field text to be some error message but we set this up where nothing happens if this returns true so why don't we just log how long it takes so that way we can check out the performance of serializing this in a text based format to do that we'll make a private long called save time in serialized json what we'll do is define a long start time equals datetime.now.tx after we've completed that save data means some time has elapsed so we'll do save time equals datetime.now.tx minus the start time so that'll give us a difference in ticks which if we divide the ticks by 1000 i'm sorry everyone this is supposed to be 10 thousand not one thousand you can also use time span dot tix per millisecond instead of a hard-coded value here and that will be better and more accurate so on all of my times here you can move the decimal one place over that will give us the number of milliseconds that it took to perform that operation so let's go back to the unity editor and see how does this work in action under the canvas button container serialize json button i'll add a new on click listener drag the demo script from the canvas and set it to be demo.serialize.json to be the action that we want to take let's go ahead and click play if i click serialize json we'll see that it took about 40 milliseconds now at the persistent data path which is on windows this path where this part is your username this part is the company name and this part is the project name we'll see player stats json shows up here if we open that up in visual studio we'll see exactly what we saw there just not formatted properly i quickly format this you'll see the exact same thing that we saw in the unity editor just in a file that's exactly what we wanted to see here now we have a way to save that data how do we get it back after we saved it into our game we'll open back up the json data service and implement that load data function in here we'll define string path equals application persistent data path plus relative path just like before we'll first check if the file does not exist at this path then we're going to log an error saying we can't load the file at that path i noticed i typod it here i'll come back and fix that in a second and that the file doesn't exist then we're going to throw a new file not found exception telling us that the path does not exist this is important because whenever we're loading the data we want to receive that data back so if we can't get that data back we're going to throw an exception that way any consumers of blown data can put a try catch block around and appropriately handle those scenarios that somebody is probably us next we'll do try t data equals json convert dot d serialize object of type t which is the inverse of serialized object we were doing before and in there we're going to input a string which is just going to be file.read alt text at this path and then we're gonna return that data assuming that returned successfully so for any reason we can't read the text of that file or that does not conform to the object structure that t expects then we'll get an error an exception will be thrown and we're gonna catch that whenever we get that we're going to log that error saying we couldn't load the data because of whatever that error is and then we're going to re-throw that exception so that way again whenever we're going to call this we can handle that in our calling code to maybe show something on the ui or whatever it is we're going to do so if we hop back to the demo script let's just make it where we'll load the data immediately after we've saved it and show it on that text input field on the right side in the serialize json method what we'll do is immediately after we've saved that we're going to set the start time equal datetime.now.tx and we'll try to read that data so we'll do try playerstats data equals dataservice.loaddata of the playerstatstype pass in the same path we did before playerstats.json again whether encryption enabled or not then we'll set the load time to be datetime.now.tx minus start time exactly as we did before and if we've gotten this far that means we successfully loaded the data we have not thrown an exception yet so we'll set the input field text to be loaded from file then a new line and then we're going to just re-serialize the same object with json convert dot serialize object data and set the load time text to be the load time in a millisecond format we'll catch if any exception was thrown log in error and show that text on the input field that we had an error we'll quickly hop back to the unity editor and try it again from the top so we'll serialize the json and we'll see i forgot to format it here so let's go back real quick whenever we serialize the object we can optionally indent it so let's do that on the json convert serialized object we serialize it one more time it took us about 360 milliseconds to load it and serialize it back into objects but overall now we can easily save and load data to the file system that will persist across runs of our game the challenge that we have of course is that it's very easy to modify this data and it's a little bit slow which i mentioned earlier text-based serialization is not particularly fast it is just particularly convenient to use so let's take a look at what we can do for this enable encryption option thank you thank you thank you to all of my patreon supporters i am so grateful for your support every one of you is helping this channel grow reach more people and add value to more people and that means more people are making their game development dreams become a reality if you are enjoying this cause you go to patreon.com academy get your name up here on the screen and get a voice shout out starting at the awesome tier speaking of those awesome to your supporters i have andrew bowen gerald anderson autumn k alberry matt parkin and ivan i'm so grateful for your support thank you i want to be really clear that this is not foolproof because there are some secrets required for encryption and these secrets if we're not storing them remotely somewhere have to be included in your build which means somebody can decode your build and find them in this case i'm using this key and this iv initialization vector which got automatically generated for me earlier i'll show you how you can get new ones because you should not reuse these because these are publicly posted on the internet these are really bad for you to use now it's important that these stay secret and are not easily exposed at least because once you have this key in this iv you can decode it just like the application can and that's why it's not particularly secure to do it this way but what this does do is blocks anybody from just opening up the file in like notepad and modifying the values to save encrypted data what we need to do is basically write data to the file that is already encrypted so to do that we're just going to make a little bit modification to our save data function here where after we create the file stream we're going to check if it's encrypted then we're going to call a function called write encrypted data passing in the data and the stream notice here we're not closing that stream because we need it to be open still on else block that closes the stream and writes all the files of the text exactly as we did before and then we'll still return true after either one of these works because if there's an exception thrown at any point we're going to hit that catch block and return false so we'll define that write encrypted data function here with private void right encrypted data that accepts a type parameter we're going to have that t data in a file stream that come in as arguments and this is the general process that you're going to use whenever you want to encrypt data in c sharp we're going to use the aes algorithm and to do that we're going to do using aes aes provider equals aes.create that creates us a new instance of the aes provider there are some other subclasses of this aes class but you should not be using those almost all of them are deprecated maybe all of them microsoft recommends you use this way to create new aes instances from here it automatically generates us a key and an iv but because we're going to call this and get a new one each time that's not going to work for us because we want to be able to decrypt the file later and the next time that we try to load the file if it gives us a random key and random iv we're not going to be able to decrypt our file we need to have a known key and a known iv to encrypt and decrypt our files and they need to be the same each time so for this we're going to override the key and the id that they give us with convert dot from base64 string the key and the ib respectively now we've set up hey this is how we're going to encrypt our file from there kind of some boilerplate that you basically always need to do is get a crypto transform and a crypto stream so that's using i crypto transform cryptotransform equals aes provider.createincrypter because we're encrypting data and then using cryptostream cryptostream equals new cryptostream passing in the stream that we want to write to the crypto transform that we just created that's going to transform our data for us in the crypto stream mode of write because we're going to write data to this stream so it's going to write data to that file stream that we passed in as the first argument running it through that crypto transform that has the aes key and iv that's going to do that encryption for us so we'll do cryptostream.right that accepts some bytes not some strings so what we're going to do is encoding.ascii dot get bytes and then pass to it the serialized json object so that's json convert to serialize object data that's all that we need to do you can basically follow the same format regardless of what kind of data you're going to write even if you're not using json if you're using any other file based way of storing data you can call basically the same pattern where you have a key and an iv using the aes provider computer transform crypto stream and you're good to go it doesn't have to be json data if you want to generate new keys because you don't want to use the ones i'm using which you definitely should not be using those you can uncomment these two debug lines right here and do not override the key in the iv up here whenever we first create the provider instead let it give you the auto generated ones and then you can just copy paste these two values above over the ones that i've provided so now that we can encrypt data let's see how do we decrypt it on load again on the load data we're going to do almost the exact same thing we did before just slightly different we'll define that read encrypted data function as a private t read encrypted data again of type t that accepts a string path in that try block i'm going to define just t data and not assign it a value by default check if it's encrypted we'll leave that block alone for a second go to the else and assign the data to be the line that we had before with the json convert and deserialize object so that way when it's not encrypted it does exactly what it did before if it is encrypted we're going to do data equals read encrypted data that accepts a type t passing in the path these streams deal with bytes they don't like to deal with strings so what we're going to do is byte array file bytes equals file.read all bytes passing the path that gives us all the data that's already encrypted in this file into this byte array and then we're gonna do basically the same thing we did above just in reverse to decrypt it so we're gonna do using aes aes provider equals aes.create we're gonna assign the aes provider key and iv the same way we did before converting from the base64 string we're going to create a new i crypto transform using aes provider.create decrypter pass it the key and the id in the arguments here and here we're going to do something a little bit different we're going to create a new memory stream using memory stream decryption stream equals new memory stream passing in the file byte so now we have a memory stream that has all of that file data already in it and that's in place of having the file stream that we were using before we're then going to do using cryptostream cryptostream equals new cryptostream again passing in the decryption stream this time inside the stream we're going to write to we're going to pass in the stream that we want to read from first pass in that crypto transform that's going to understand how to translate that encrypted data to normal bytes and for the third argument we're going to do crypto stream mode dot read because we want to read that data we'll do using stream reader reader equals new stream reader that we pass into it the crypto stream because we want to read data out of that crypto stream and finally we can do string result equals reader dot read to end that's going to give us a string with all of our data that has been decrypted so we can return jsonconvert.dserialiseobject t passing in that string that should be our d serializable json object because it might not be because maybe our keys are wrong or something somebody messed with the file what we'll do is do a debug.log saying decrypt the result if the following is not legible probably you have the wrong key or the iv and then print out the result so that way we can see if it's wrong let's go back and see how does this work with encryption so open up the unity editor one quick note is the toggle button is tied to demo.toggle encryption which just sets the encryption enabled to be true or false based on that check mark if we go ahead and click play the first thing i'll do is enable that encryption we'll click serialize json and we'll see that it takes significantly longer we were talking 40 50 milliseconds before now we're talking almost 200 milliseconds to save it and almost 500 milliseconds to load it 460 milliseconds here versus the like 200 that we had before so adding encryption does make it take significantly longer we're talking four times almost longer to right and about two two and a half times longer to read but we pull up trusty visual studio drag this player stats json there it complains because this is not a really readable format and we get basically a bunch of gibberish in here we have no idea what this says and if i try to modify it it's going to be really hard to do that i had no idea what to modify here and now to see what happens in case maybe we run into some problems on the json data service on the read encrypted data what i'm going to do is comment out everything about passing in the key and the iv from our known values whenever we're reading the data so you'll notice now because we're not using the same key we got an error we have error reading save file so if we go to the console you'll see that's complaining here that you know something's wrong with our decryption process if i just change the iv of whatever we had before to a new thing we'll see what happens if maybe the json serialization fails on the load we've just changed the initialization vector here a little bit from whatever it was before there's an error reading save file we mostly decrypted it but at the beginning something wasn't right so it didn't come back with a proper json format and we couldn't deserialize that as i was saying towards the beginning of the video it's not always best to use a text-based realization because well first off it's not the most optimized way to store data storing large quantities of data using something like a binary format makes it a lot more optimized the primary use cases for using a json or text based serialization even if it's xml is really the ease of use of this it's very simple you don't have to do a lot and it just kind of works however using a binary formatting system is significantly more optimized it's a lot faster to write data significantly harder for a player to understand the data they're modifying they kind of have to guess and check at whatever they're modifying to see hey if i modify this what changes in the game versus with a text-based serialization probably have something like player health it's gonna be really easy for them to figure that out another potential option is you could store data in a database that may be using sqlite that's an embedded lightweight really fast embedded database that's quite popular wherever well supported as well a couple of notes on the binary formatting is you should absolutely not use the binary formatter class this has a really big security issue where if somebody especially crafts a file they can take over the computer so just don't use that do something like maybe protobuf which is a framework developed by google that supports binary formatting that'd be a great option remember as well if you're going to store data on that player's local machine you have to assume that they can modify that data so a game that relies heavily on microtransactions by limiting the progression of the player may not be the best use case for having local storage because somebody could modify that and just negate all of your revenue options if you're really hardcore about not wanting anybody to ever possibly cheat storing this data on a remote server is really the best option that's where the player is not going to be able to modify it you can maybe modify it at runtime but there are some ways you can kind of detect if somebody's doing that i don't want to get too much into these other use cases kind of going off too far already in this video what i hope that you got out was how to actually implement text-based serialization into your game persist data across runs of your game possibly encrypting that data to make it a little bit harder for the players to actually manipulate that data themselves and discover some potential alternatives in case text-based serialization doesn't work particularly well for your game if you got value at this video please consider liking and subscribing to help the channel grow reach more people and add value to more people so new videos posted every tutorial tuesday and i'll see you next week
Info
Channel: LlamAcademy
Views: 44,189
Rating: undefined out of 5
Keywords: Unity, Tutorial, How to, How to unity, unity how to, llamacademy, save data, load data, save, load, save load, save load system unity, unity save data, unity save game, unity load data, unity load game, unity game state, data persistence, persist, data, persist data unity, data persistence unity, unity data persistence, unity save load system, unity save system, easy save, intermediate, advanced, json, save json data unity, load json data unity
Id: mntS45g8OK4
Channel Id: undefined
Length: 26min 12sec (1572 seconds)
Published: Tue Jun 28 2022
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.