Hello and welcome back to the Blightmare Development Blog!  Somehow in my excitement to get the editor out the door I completely forgot that we had scripting in the game.  I remembered early this week and set about rectifying my mistake.  I was very pleasantly surprised when it only took a few days to re-implement something that originally took me a couple of weeks.  I was able to re-use many of the utilities created for the mechanic editors and things just went really smoothly.  Perhaps the best part of the whole thing was being able to delete an absolute ton of prefabs and code that was now redundant.  The final amount was 208 changed files, 10 new lines, and 45,113 deleted lines.  There’s nothing better than deleting a bunch of stuff and ending up with more features than you had before.  It means things are simpler which probably also means more robust and in this case, faster too!

After merging in the bit cleanup, it was late and I was curious about just how big Blightmare actually is in terms of code and non-art assets.  I fired up cloc and had a peek.  The results are:

  • 343 C# files with 26,980 lines of code, 1,067 lines of comment, and 5,918 blank lines
  • 204 Prefab files for a total of 75,677 lines

If you’ve read a few of the recent posts, you know that we’ve been doing a lot of refactoring and cleanup on the editor so I went back to a commit from just before that started and did the same cloc analysis:

  • 405 C# files with 29,036 lines of code, 1,005 lines of comment, and 6,575 blank lines
  • 316 Prefab files for a total of 140,730 lines

Good stuff!

 

Okay, on to the actual topic for today.  I noticed that loading some of our larger levels was taking a really long time which was both annoying for me when trying to test out the design, and I’m sure it’s super bad for our designer who has to wait for it many more times than me.  The first step to any optimization effort is to identify what is actually slow.  Getting some data is not only important to focus efforts in the right place, but also to determine when it’s okay to stop.  The next question then is how to actually get this data.  We’ve shown the Unity profiler on the blog before, but for this task, it isn’t the best tool.  The problem with the profiler is that it doesn’t allow us to easily see what happens within a single function, and there’s a lot of noise typically which makes it harder to focus in on what we want.  Fortunately, it’s pretty easy to build our timing helper which helps us out here.  In C# there’s a built-in class called Stopwatch which is perfect for this task because it gives us access to the highest precision timing source on the system.

 

The whole timer class is this:

public class TimedSampler
{
    private class Sample
    {
        public string name;
        public long milliseconds;
    }

    private string _name;
    private List<Sample> _samples;
    private Stopwatch _stopwatch;
    private Stopwatch _totalStopwatch;

    public TimedSampler(string name = "")
    {
        _name = name;
        _samples = new List<Sample>();
        _stopwatch = new Stopwatch();
        _totalStopwatch = new Stopwatch();
        _totalStopwatch.Start();
    }
    
    public void BeginSample(string name)
    {
        Sample sample = new Sample { name = name, milliseconds = -1 };
        _samples.Add(sample);
        
        _stopwatch.Reset();
        _stopwatch.Start();
    }

    public void EndSample()
    {
        _stopwatch.Stop();
        _samples[_samples.Count - 1].milliseconds = _stopwatch.ElapsedMilliseconds;
    }

    public void Report()
    {
        _totalStopwatch.Stop();
        
        string report = $"{_name} [{_totalStopwatch.ElapsedMilliseconds} ms total]";
        foreach (var sample in _samples)
        {
            report += $"\n\t{sample.name} [{sample.milliseconds} ms]";
        }
        
        Debug.Log(report);
    }
}

If we needed to store lots of samples or time really high frequency blocks, we would have to do something better, but this is more than enough to do a bit of manually instrumented sections.  After doing that to the level loading code, the results were pretty clear:

 

Level Load Bootstrap 
    Init [2 ms] 
    Parse Level Data [1368 ms] 
    Spawn Points [0 ms] 
    Spike Tiles [4 ms] 
    Terrain Tiles [16053 ms] 
    Terrain Outline [356 ms] 
    Simple Spawners [13 ms] 
    Mechanics 1 [197 ms] 
    Mechanics 2 [18 ms] 
    Decorations [0 ms] 
    Scripts/Level Start [7 ms]

That big 16 second block in “Terrain Tiles” is definitely the outlier here so that’s where the focus needs to go.

 

I believe we’ve talked about how the tiles work on the blog before, but to do a quick recap: we use a “rule tile” to set which means that every time a tile changes on the map, we need to potentially recalculate 9 tiles which involves a lookup of up to 16 tiles, sometimes multiple times.  It would appear that this isn’t particularly efficient.  The plan then is to switch to using a simple tile that is just a single sprite and go ahead and store tile instances directly.  We can pre-compute all the placements for what we want in the final map, which will save all the computation at load time.  I went ahead and tested what this would be like by removing the logic for the rule tile and loading the level again.  The load time went down to right about 2 seconds total, which saves about 16 seconds – all in the Terrain Tiles section.  This test shows that we’re on the right track with this plan.

I haven’t finished the change yet, so I unfortunately don’t have the final results to share because there’s quite a bit of data entry involved with creating the flat tile lookup.  In this case, I’m using a dense array of all 256 options that map to a tile reference so that we can just store an index into this array when we serialize.  This should give the fastest loading time which is what we’re optimizing for.  Any computation that we can do before we actually load the level is computation we don’t need to do while a player is waiting to play.  Next week I’ll have results to share and some images of what the data actually looks like when it’s configured properly.

 

Thanks for reading and I hope you come back next week for the exciting conclusion!  As always, please add Blightmare to your wishlist and follow us on Twitter to stay connected to the game.  Have a great week!