Optimizing Game Loading Time: Sprite.CreateSprite() and Base64

The loading time of our game has been horrendous lately. I made an editor script where we just press Alt + Shift + Z to play the game in Unity editor. My definition of loading time is the time duration from pressing Alt + Shift + Z to the game being actually playable in the editor. The loading time scales depending on the game file being loaded. The bigger the school, the slower the loading time. I tested the starting map and even that loads after around 40 seconds. That’s horrible!

Reducing this load time is substantial. It will greatly improve productivity. When making games, we usually spend most of our time testing instead of writing code. Just imagine, you write a little change to your code, then you go to the editor. It compiles which also takes time. Then you play the game which takes at least another 40 seconds. When you test 10 edits, you’ll lose 400 seconds (6.67 minutes). At 50 edits, that amounts to 33.35 minutes. How many edits do you make in a day?

I spent some time today to try to reduce this unproductive time. I was able to shelve 20 seconds.

Sprite.CreateSprite()

I don’t know why but this method is so slow. The way we maintain our sprite assets might have something to do with this. We keep all our sprites in StreamingAssets folder. At runtime, we load them and put all of them in a single big atlas. We usually use Sprite.CreateSprite() on our dynamically loaded UI. Some buttons requests for icons that are in the atlas. I’ve fixed this by using RawImage instead of Image. Using RawImage requires extra handling, but we already have the data that it needs. Just avoiding Sprite.CreateSprite() saved 10 seconds in loading time.

public void InitIconImage() {
    PackedTextureEntry packedEntry = PackedTextureManager.GetPackedTextureEntry(this.buildSelectorObject.SpriteId);
    Assertion.AssertNotNull(packedEntry, this.buildSelectorObject.SpriteId, this.gameObject);

    // selectorRawImage is a RawImage
    this.selectorRawImage.texture = packedEntry.Atlas;
    this.selectorRawImage.uvRect = packedEntry.UvRect;
}

Too many string to int conversion

We use XML in our save file for readability purposes. It’s good for us and for our players. As a tile based game, a two dimensional vector with integers is ubiquitous. In fact we have a class for this even before Unity released theirs. We have a data model which is just a list of these vectors. In XML, it looks like this:

<Positions>
    <Entry x="93" y="7" />
    <Entry x="93" y="8" />
    <Entry x="93" y="9" />
    <Entry x="93" y="10" />
    <Entry x="93" y="11" />
    ... <!-- More entries -->
</Positions>

On deserialization, we convert the x and y values from these XML attributes which are strings. The more entries it has, the more conversion needed, the slower the loading time.

I did some research and I stumbled upon good old Base64. C# already has methods for converting byte arrays to Base64 strings and vice versa. So I thought of storing the coordinates to a byte array and use its Base64 string in the XML save file. This was easier than I thought.

private const string BASE64 = "Base64";
private const int INTEGER_SIZE = sizeof(int);

private readonly List<byte> bytes = new List<byte>();

private void WritePositions(XmlWriter writer, HashSet<IntVector2> positions) {
    writer.WriteStartElement(POSITIONS);

    // Write positions as Base 64 string of positions as bytes
    this.bytes.Clear();
    this.bytes.Capacity = zone.PositionsCount * (INTEGER_SIZE * 2); // Multiply by two here because there are two integers per position
    foreach (IntVector2 position in zone.Positions) {
        bytes.AddRange(BitConverter.GetBytes(position.x));
        bytes.AddRange(BitConverter.GetBytes(position.y));
    }

    string base64 = Convert.ToBase64String(bytes.ToArray());
    writer.WriteAttributeString(BASE64, base64);

    writer.WriteEndElement();
}

private void LoadPositions(SimpleXmlNode node, HashSet<IntVector2> positions) {
    SimpleXmlNode positionsNode = node.FindFirstNodeInChildren(POSITIONS);
    if(positionsNode != null) {
        byte[] bytes = Convert.FromBase64String(positionsNode.GetAttribute(BASE64));
        int positionCount = bytes.Length / (INTEGER_SIZE * 2);

        int index = 0;
        for (int i = 0; i < positionCount; ++i) {
            int x = BitConverter.ToInt32(bytes, index);
            index += INTEGER_SIZE;

            int y = BitConverter.ToInt32(bytes, index);
            index += INTEGER_SIZE;

            positions.Add(new IntVector2(x, y));
        }
    }
}

The classes BitConverter and Convert can be found in System namespace. This shelved another 10 seconds (or more if the data were big) which is huge! This method of saving a list of integer vectors is way faster than converting individual integer attributes.

Conclusion

I didn’t think I would be able to reduce loading time by at least 20 seconds with just a couple of changes. Shelving 5 seconds would have been fine by me. The work is far from over, though. I could still optimize other parts but they are more complicated. It will take more effort and I might introduce bugs. I’m happy with my huge savings for now.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s