Monday, January 6, 2014

Update 25: Loading, Saving Game Objects

I've spent most of the last month refactoring a lot of code, building in a basic campaign layer and then working on the connection between the campaign/strategy layer and the tactical layer. I'll talk more about the campaign later, as today I wanted to talk about XML serialization in more detail.

XML Serialization effectively means I’m saving and loading data to XML to persist game information on the hard disk.

When I start a mission, I populate my mission object with the details I need to start the mission. At the moment, it’s as simple as a level name, a list of player characters and a list of enemy characters. The class looks a bit like this:

using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;

public class Mission
{
    public string LevelName { get; set; }
    public List<Character> Players { get; set; }
    public List<Character> Enemies { get; set; }
}

Loading and saving the mission is straight forward with XML Serialization. Loading is basically opening the file with a StreamReader, De-Serializing the XML to the Mission object and returning the result. Saving is the inverse, creating a new file with the StreamWriter, Serializing the data to XML and then closing (saving) the save file.

public Mission LoadMission(string filePath)
{
    Mission gameToLoad = new Mission();
    StreamReader streamReader = new StreamReader(filePath);
    XmlSerializer reader = new XmlSerializer(typeof(Mission));
    return (Mission)reader.Deserialize(streamReader);   
}

public bool SaveMission(string filePath, Mission gameToSave)
{
    XmlSerializer writer = new XmlSerializer(typeof(Mission));
    StreamWriter file = new StreamWriter(filePath);
    writer.Serialize(file, gameToSave);
    file.Close();
    return true;
}

Let’s look a little closer at the Character class I'm using for players and enemies and how that saves. It has a name, photo, health, AP’s, a chance to hit  and a game object reference.

public class Character
{
    public string Name { get; set; }
    public string Photo { get; set; }
    public int Health { get; set; }
    public int ActionPointsRemaining { get; set; }
    public int ChanceToHit { get; set; }
    public CharacterObject GameObjectRef { get; set; }
}

Unfortunately, I can't directly serialize the GameObject type, (many binary types are not serializable), so I have to create a separate game object reference. They are big binary objects. At the end of the day, this isn't a big deal, I really only need to know what sort of game object the character is, the position/rotation/scale of the game object. Below is this CharacterObject class, with enough details of the game object to recreate it when I reload the level.

using UnityEngine;
using System;
using System.Collections;
using System.Xml.Serialization;

public class CharacterObject  {

    public string Name;
    public string PrefabName;
    public Vector3 Position;
    public Quaternion Rotation;
    public Vector3 Scale;
    [XmlIgnore]
    public GameObject GameObjectRef;

}

The result of all of this is the following XML, with one player and no enemies.

<?xml version="1.0" encoding="utf-8"?>
<Mission xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <LevelName>IsometricCore</LevelName>
  <Players>
    <Character>
      <Name>Sam</Name>
      <Photo>Photos/Player</Photo>
      <Health>10</Health>
      <ActionPointsRemaining>1</ActionPointsRemaining>
      <ChanceToHit>85%</ChanceToHit>
      <CharacterObject>
          <Name>Player1</Name>
          <PrefabName>CharacterPrefab</PrefabName>
          <Position>
            <x>3.7499404</x>
            <y>0.5</y>
            <z>3.9403758</z>
          </Position>
          <Rotation>
            <x>0</x>
            <y>1</y>
            <z>0</z>
            <w>-1.62920685E-07</w>
            <eulerAngles>
              <x>0</x>
              <y>180.000031</y>
              <z>0</z>
            </eulerAngles>
          </Rotation>
          <Scale>
            <x>1</x>
            <y>1</y>
            <z>1</z>
          </Scale>
          <Tag>Player</Tag>
      </CharacterObject>
    </Character>
  </Players>
  <Enemies />
</Mission>


So what do we have to do to reload the GameObject again? We just need to call the load, and then for each Player (or Enemy), recreate the prefab object and put it back in the same location we saved.

// Initialize the mission object and load the mission xml file
Mission MissionData = new Mission();
MissionData.LoadMission(@"C:\mySaves\Mission1.xml");
if (MissionData != null)
{
   foreach (Character item in MissionData.Players)
   {
// Check to see if the player object already exists
GameObject newObject = GameObject.Find(item.CharacterObject.Name);
// If we can’t find the object, create it and attach it to the game object
if (newObject == null)
{
   Object prefab = UnityEditor.AssetDatabase.LoadAssetAtPath("Assets/Resources/" + item.CharacterObject.PrefabName + ".prefab", typeof(GameObject));
   newObject = GameObject.Instantiate(prefab, Vector3.zero, Quaternion.identity) as GameObject;
   // Transfer over the new properties
   newObject.transform.position = item.CharacterObject.Position;
   newObject.transform.rotation = item.CharacterObject.Rotation;
   newObject.transform.localScale = item.CharacterObject.Scale;
   newObject.name = item.CharacterObject.Name;
}
   }

}

So that is it! We have reloaded the game object! 

1 comment:

  1. Nice! One thing I discovered recently... XmlSerializers are very expensive to create so you should make sure you only create it once, and only when needed (i.e. not on startup)

    ReplyDelete