Save Games in Unity: Building a robust, extendable save format in XML without tangling up your code

来源:互联网 发布:java金字塔图形的代码 编辑:程序博客网 时间:2024/06/03 21:36

Save Games in Unity: Building a robust, extendable save format in XML without tangling up your code

Unity is a wonderful game engine that we take great advantage of when creating our games. It offers great support for many things out of the box. This post deals with a common problem for game developers: creating save files. Along the way we’ll figure out how to make them readable, maintainable and easy to debug.

One complaint I often hear in the Unity Community Forums is that unity comes with little out of the box save capability, and there doesn’t seem to be a recommended solution.  Depending on the game you are creating, something as simple as thePlayerPrefs class could suit your needs. However, for games with more than a minimal amount of data, and more than one type of thing to save, PlayerPrefs won’t be ideal, and you’ll need to create your own save file (and therefore your own save format). Some people use C#’s built in serialization support, or create a binary save format. Since disk space is plentiful these days, and dependability and stability are very good things indeed, we’ve chosen to create our save files in XML. XML gives us access to many existing well tested tools for reading and processing large files written in a human-readable markup format, reducing bugs from the start.

Using XML in .NET

But XML is a pain to work with, huh? DOM traversal and SAX parsers are surely a curse on the world of developers! That used to be the case, but C# has support for XPath, an XML querying language that makes short work of finding the data you needs from an XML file fast (programmer time-wise). Writing XML to disk couldn’t be easier with Mono (and .NET) ’s XmlWriter class. So we’ve found the tools we need for the job, but how do we go about integrating it into a Unity project? Of course, we’re keeping in mind that the project may have a lot of code already, may need to be able to handle save files from different versions or the game, and respecting the debugging limitations of Unity?

But wait, I still don’t know what I’m saving!

Deciding what to save is no easy task. Different games have different requirements. Many games can get by only saving a level or checkpoint. Some need only to save the player’s state (Inventory, Health, position, etc.). You might have a world that the player can effect, he could move a bridge down, unlock doors, pickup and drop objects, and destroy persistent enemies, those things need to be saved. In still other games, the position and state of all the objects in the world need to be saved (building or resource management games). Work out the various things you will need to save by careful and systematic deliberation.

Putting it together

Also, in a save game you will need to load objects in a particular order, so saving them in that order makes sense too. For example, if you have a hierarchy of objects that need to have whether or not they are active saved, you can use SetActiveRecursively and not worry about saving every individual object, but you must do it in order of parent to child, or you’ll get the wrong results. Likewise, objects that rely on the state of other objects ought to be loaded after the objects they rely on. Unfortunately, the sorting criteria aren’t as simple as something like sorting by name.

An example of saving which objects are enabled in the heirarchy. We would need to load the parent "ActionMenu" before the children if using SetActiveRecursively

An example of saving which objects are enabled in the heirarchy. We would need to load the parent "ActionMenu" before the children if using SetActiveRecursively

In Unity, you often have many GameObjects taking care of themselves, leading to a situation where it can be hard to track down the objects you need to save, and a mess if you let objects access each other’s member variables. To get around this and to allow centralized sorting, we use a list of all the Components that need to be saved. Of course, in order to have a list that you can call the same functions on, they need to implement some common interface. We use the name Writable to refer to objects with save capability.

class Writable extends MonoBehaviour{private var priority : int;virtual function Priority() : int{return priority;}virtual function GetWritableType() : String {return this.GetType().ToString();}virtual function GetUniqueName() : String {return transform.name;}virtual function WriteToXml(writer : XmlWriter) : void{;}virtual function ReadFromXml(nav : XPathNavigator) : void{;}}

As unity users are aware, dragging upwards of 1000 objects into an array in the inspector (and re-arranging some that are already there) can be incredibly painful.  In the inspector, a context menu function of our save manager gathers all the objects that implement the interface Writable and sticks them into a list. That list is then sorted into the order that objects should load. When Save is called, each object uses there own WriteToXml function to save to a local position in a passed in XmlWriter. Writing Xml save functions is easy, it’s a lot like using UnityGUI 2.0.

function GatherWritables (){for(var c : component in root.GetComponentsInChildren(Writable, true)){toAdd.Add(c);}toAdd.Sort(new CompareWritables());var duplicates : ArrayList = new ArrayList();for (var i : int = 0; i < toAdd.Count; i++)for (var j : int = i+1; j < toAdd.Count; j++)if (toAdd[i].GetUniqueName() == toAdd[j].GetUniqueName() &&toAdd[i].GetWritableType() == toAdd[j].GetWritableType()){duplicates.Add(toAdd[j]);break;}for (var w : Writable in duplicates) {Debug.LogError("Error collecting Writables: \"" + w.GetUniqueName() + "\" exists more than once with Writable type: " + w.GetType(), w);}writables = new Writable[toAdd.Count];toAdd.CopyTo(writables);}

static function SaveGame(filename : String) : void{var settings : XmlWriterSettings = new XmlWriterSettings();settings.CloseOutput = true;settings.Indent = true;settings.IndentChars = ("\t");var writer : XmlWriter = XmlWriter.Create(Game.saveDirectoryPrefix + filename + ".xml",settings);try{writer.WriteStartDocument();writer.WriteStartElement("bksavegame");writer.WriteStartAttribute("version");writer.WriteValue(instance.version);writer.WriteEndAttribute();for(var writable : Writable in instance.writables){writable.WriteToXml(writer);}writer.WriteEndElement();}catch(e : System.Exception){Debug.LogError(e.ToString());}writer.Flush();writer.Close();if(instance.saveScreenShot)Screenshot.SaveGameShot(filename);lastfile = filename;}

class SaveGameObject extends Writable{public var savePosition : boolean = false;public var saveParent : boolean = false;function Priority() : int{return 1;}function WriteToXml(writer : XmlWriter) : void{writer.WriteStartElement(GetWritableType());writer.WriteStartAttribute("name");writer.WriteValue(gameObject.name);writer.WriteEndAttribute();writer.WriteStartElement("state");writer.WriteStartAttribute("active");writer.WriteValue(gameObject.active);writer.WriteEndAttribute();writer.WriteEndElement();if(savePosition){SaveLoad.WriteTransform(writer, transform, saveParent);}writer.WriteEndElement();}...

Loading it up

To properly load an object from the save file, we use an XPath query. We do this because over different versions of the game, the XML files might not be in exactly the same order as the current Writable list.

function Load(filename : String){var settings: XmlReaderSettings = new XmlReaderSettings();settings.IgnoreWhitespace = true;settings.CloseInput = true;var reader : XmlReader = XmlReader.Create(Game.saveDirectoryPrefix + filename + ".xml", settings);var doc : XPathDocument = new XPathDocument(reader);var nav : XPathNavigator = doc.CreateNavigator();try{for(var writable : Writable in writables){writable.ReadFromXml(nav);}}catch(e : System.Exception){Debug.LogError(e.ToString());}reader.Close();lastfile = filename;}

... // From SaveGameObject above.function ReadFromXml(nav : XPathNavigator) : void{var name : String = "/bksavegame/"+GetWritableType()+"[@name=\""+gameObject.name+"\"]";var tmp : XPathNavigator = nav.SelectSingleNode(name);if(tmp == null){Debug.LogError("No result found for savefile query: "+name);return;}var reader : XmlReader = tmp.ReadSubtree();reader.ReadStartElement(GetWritableType());if(reader.LocalName == "state")gameObject.SetActiveRecursively(System.Boolean.Parse(reader.GetAttribute("active")));else{Debug.LogError("Unexpected state");}reader.Read();if(reader.LocalName == "transform"){SaveLoad.ReadTransform(reader, transform);}reader.ReadEndElement();}}

Tips

Now that we’ve seen an example Writable, and the basics of the code to get started, Here’s some tips:

  • Each type of object that is saved should be carefully tested  (the different possible states for most types of objects aren’t usually large). Try to do it in a separate scene to avoid interactions between objects (and keep your code free of coupling!)
  • When writing the loading routines and deciding what to save, consider situations of loading objects from a clean state (i.e. when your scene starts) to a dirty state (after the player has modified their state), and from a dirty to clean state. However, if your scene loads quickly or you don’t mind making the player wait, you could design your save system to only load from a clean state, and always reload the scene when loading occurs.
  • When many types of object are saving the same things, consider using a separate script to save that part of the object, as in the SaveGameObject example above. This keeps the code to test in one place.
  • Write your Xml format to be hierarchical so that it is easy to navigate in an XML editor and easy to traverse when you load with XPath. For example, the player’s inventory might be saved as a child node of the player himself.
原创粉丝点击