Sunday, September 20, 2020

Tile Workflow and Sub-Distributions

I have been rather busy this week in my professional work, so I have not spent as much time working on the game as I would have liked.  However, I did manage to tweak a few things and add a few new features to the tile engine that make things a little more interesting.  For starters, I was able to change up my spritesheet workflow to be more productive.  Take a look at this grass tilesheet.

Buffalo Grass

Spritesheet Workflow

I use a plugin tool in Paint.NET named Outline Object to create the black edges of each tile.  But due to the way in which the tool works, the only way to get the edges correct was to copy each single tile into another image, run the tool on it, and then copy and paste it back onto my original tilesheet.  I decided it was time to create a template image that would allow me to stamp the entire tilesheet at once - saving a ton of manual work.  There are essentially four steps involved in making or changing one of my top-level tilesheets.  First, I start with a base texture (top-left square).  Once I am happy with the look, I create an empty image that can hold the 16 variations of my texture.  Since my tiles are 100x100 pixels, the tilesheet image is 400x400 pixels.  Next, I duplicate my base texture to fill the first row, then duplicate that row to fill in the remaining three rows.  When that is done, I can really see whether the texture is tiling properly.  The less I can notice the edges of each tile, the better!  Then, I use a 400x400 pixel template image to stamp in the transparent edges of the tilesheet (all the parts in white above).  Finally, I stamp in all of the black edges to create a sense of elevation to the edges of the grass.  This new process is much faster than before.  Introducing the new tilesheet into the game is also now very easy thanks to the Scriptable Object changes I have recently made.  Feel free to read my previous posts if you are interested in Scriptable Objects.

Custom World Terrain Tab

Cluster-Spawned Special Tiles

Next on my list of changes this week was the distribution of the Special Tiles.  I use a Perlin noise algorithm to generate the game map.  But the Perlin engine only distributes the first two columns of tile types you see in the Terrain tab above.  The special tiles were intended to be distributed within the other tiles based on some hard-coded relationships between the regular tiles and the special tiles.  For example, Moss only spawns within certain grasses.  The result of the original distribution was not very organic, just a random Moss tile here and there.  I decided to write a method to distribute the special tiles in clusters rather than sprinkled around the map.  The cluster logic basically chooses a random number of cluster spawns and then determines how many tiles each cluster should have based on the total tile count of all possible compatible tiles on the map.  A random spot on the map is then checked to see if the special tile can spawn at the location, and if so, that tile becomes the spawn anchor point.  Another method randomly determines the placement of the next adjacent tile, resetting the spawn point, and the loop continues.  Notice in the next screenshot how the Brackish water has spawned clustered  together without looking like a simple circle or rectangle.

A Brackish Water Cluster

Once the spawn-point is checked, the method doesn't care what type the next tile is unless it is an edge-of-the-map tile, then it switches to another random 8-direction choice for the next tile.  This allows the special tiles to cross-over into other adjacent tile types creating a very nice organic feel.

Shuffling Distribution

Notice in the second screenshot above that some of the tile types are named in green.  I thought it would be more interesting to allow some of the tile types to be shuffled in order when they are distributed based on their Perlin values.  By checking the box, the green tile types will have their order on the map shuffled randomly.  For anyone interested, I created a list of tile-type id's and populated the list with the tile type id's in their original order.  To shuffle, I pass the list into an extension method which shuffles the order of the integers and returns the list in a random shuffled order. Then, looping through the random list allowed the distribution to be shuffled.  Here is an extension method which does the shuffling.  By the way, this method can shuffle any List<T>, not just integers.  I am sure I will use it in the future to randomize NPC names, and many other things.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public static void Shuffle<T>(this IList<T> list)
        {  
            var rand = new System.Random();          
            int n = list.Count;
            while (n > 1)
            {
                n--;
                int k = rand.Next(n + 1);
                T value = list[k];
                list[k] = list[n];
                list[n] = value;
            }
        }

The following is a small map generated in the original un-shuffled order.  Then, the next map is one where the shuffle has been turned on.  Notice how the tile types have changed in order.  This creates some very nice differences between maps generated from the exact same Perlin data.  

Un-Shuffled Map

Shuffled Map
The keen eye will notice subtle differences between the distribution of tiles in the two maps.  This isn't a bug.  Each tile type has a weight value (the sliders on the Custom World screen).  The weights determine how much of the map each tile type takes. It is my goal to provide a way to re-generate the exact same map terrain given a seed value and the weight values from the map builder.  Now, I can add to this the shuffle list included in the map template.  This will permit creating identical maps given these values.  Well, that is all for now.  Until next time...

Sunday, September 6, 2020

A Look At Scriptable Objects Part 2

In my last post, I introduced my recent journey into Unity's Scriptable Objects.  Since then I have been working to convert the tree objects and the tile engine to use them in place of Singleton classes that store tree and tile data in static lists.  I have now completed parts of this refactoring and am quite impressed with the new workflow, the added benefits, and the surprising performance boost I have seen moving to scriptable objects.  From here onward, I will abbreviate the term Scriptable Object to simply (SO) for brevity.

The Refactoring Process

I decided to refactor my tree game objects first since the tree code was less work than the tile engine. I started by creating an abstract base class which derived from (SO).  All my other (SO) classes derive from my base class.  I used this base class to store properties and fields that were common to all the (SO) classes I would eventually create.  The class needed to be abstract because I had no intention of instantiating this base (SO) class by itself.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    public abstract class STBaseObject : ScriptableObject
    {
        public string Id { get; set; }
        public string Name;
        public string FriendlyName;
        public STBaseType BaseType;        
        public GameObject DefaultPrefab;
        public GameObject InventoryPrefab;
        public float AdjustedXPos;
        public float AdjustedYPos;
        public int HighestMapLevel;
        public int LowestMapLevel;
        [TextArea(1, 20)]
        public string Description;        
    }    

The next (SO) class was my STTreeObject class.  This class would hold fields and properties that were unique to my trees.  Up to this point, all my tree data was stored in a public class that held the properties for my trees.  This class was accessed by another class which existed to simply to instantiate the list of trees at runtime.  That class was instantiated once as a public property on one of my Singleton classes, which also was only instantiated once.  What I am describing is a Singleton data storage mechanism.  Getting back to the (SO) alternative, have a look at the top portion of the new class showing the CreateAssetMenu attribute.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
[CreateAssetMenu(fileName = "TreeObject", menuName = "SurviveAndThrive/Flora/TreeObject", order = 1)]
    public class STTreeObject : STBaseObject
    {
        public int AssetId;
        public GameObject Sapling;
        public GameObject Ripe;
        public string FellingSound;
        public float FellSoundTimer;
        public STFloraAssetType AssetType;
        public STFloraAssetDistributionType DistributionMethod;
        public STFloraAssetAgeType MinimumAge;
        public STFloraAssetAgeType MaximumAge;
        public STFloraAssetAgeType MaturityAge;
        public STFloraAssetGrowthRateType GrowthRate;
        public STFloraAssetGrowthRateType FlowerRate;
        public STFloraAssetGrowthRateType SeedRate;

The class attribute is important because it allowed me to create a (SO) file in the Unity Editor by creating a menu item for it under Unity's Assets => Create menu.  Notice the fileName is the name of the (SO).  The menuName is the location within the menu structure where the (SO) will appear. 

Scriptable Object Menu

At first, I thought my tree class and (SO) tree class would be basically the same.  However, once I started refactoring, I realized some of the string values I was using to locate my tree objects were no longer needed because I could directly reference them as a GameObject on my (SO) class.  Here is what a new TreeObject looks like from the Inspector.
A New TreeObject

The New Workflow

There is much to be appreciated in the screenshot above.  For one, it is a lot easier to keep track of my tree data using the Project and Inspector tabs in Unity as opposed to scrolling down 1600+ lines of code in the class I was using to store this data before.  In a glance, I can see how many trees I have and I can compare the property settings of each one to the others.  Plus, by selecting a tree (SO) and locking the inspector, I can navigate to my prefabs and drag-and-drop them onto the (SO).  This means I no longer have to load those prefabs from the disk each time I need to instantiate a new tree in the game.  Yes, it is tedious to transfer all the information from the class to the (SO) files, but it is no less work than using the classes.  

The Added Benefits

This brings me to the benefits I have noticed using (SO) files instead of static classes.  Like I mentioned earlier, I was able to switch from storing and using strings in the tree class which held paths and filenames to using hard references in the (SO) file for things like the DefaultPrefab, InventoryPrefab, and Sapling and Ripe variants.  Another amazing, and I do mean AMAZING benefit over static class storage is the fact that all of the tree data can be altered at runtime.  For example, if I need to increase the number of logs that are produced from chopping down a particular type of tree, all I have to do is change the value in the inspector.  The next time I chop down that type of tree, it will produce the altered count of logs automatically.  This is perhaps the best overall benefit I have seen to using (SO) over other methods of storage of static data.

The Performance Boost

Once I had converted to using (SO) for the trees, I proceeded to build out the tile engine to use (SO) as well.  There is a nice method in my MapManager class which creates the preview map in the game.  I chose that as the first step to refactoring the tile engine.  What follows are comparisons between the pre-scriptable-object version of the game and the current using the new (SO) system.  This test involved loading an existing map template and rendering the preview of it.  The map is 1 MILLION tiles, being 1000 x 1000 tiles in size, rendered on a single Unity tilemap.  Notice in the Console the Start and Stop times.  These times captured how long it took to generate the preview map using the old and new systems.

Pre-SO-Test
35 Seconds

I didn't think 35 seconds was all that bad considering the map engine had to generate a million value noise map, distribute the tile types based on the user's chosen weight for each type, and then loop through to actually create the tilemap tiles using a custom rule-tile method.  But I was not really prepared for the results of the (SO) run.

SO Test
23 Seconds

The same template using (SO) files took only 23 seconds to create and render.  Doing the math, the pre-SO test processed 28,571 tiles per second.  The SO test processed 43,478 tiles per second.  That is a whopping 35% increase in performance! Now to be fair, there is a critical optimization that I used for the (SO) refactoring.  With the old classes, I had to build out a file path and filename to the required tile and then instantiate it with a call to Resources.Load<Tile>().  This was necessary simply because of the huge amount of tiles to manage.  But with the (SO) solution, I chose instead to reference all 15 tiles of each terrain type in their respective (SO) files.  This enabled me to reference all of the tile variations in a clean and organized manner. I believe the majority of the performance boost was due to this optimization as loading from a file system is always more costly than from memory.  The Task Manager didn't show significant differences, but here they are. 

Pre-SO-Test

SO Test

I am very happy that I decided to learn about (SO) and how to use them for static game data storage.  There are many other uses for them that I am sure I will discover.  And, I still have quite a bit of refactoring left to do.  The preview map is just a small part of the map engine.  I will likely follow-up this post with a part three to summarize any future findings.  But one thing is sure.  I will definitely be using Scriptable Objects as much as I can.  That real-time tweaking feature is just fantastic!  

Until next time...

[The Next Day UPDATE!]

I have spent pretty much all day working on refactoring the entire map engine to use (SO).  After some debugging, I ran some tests and I want to update this post to show the dramatic results of moving from Singleton class stores with resource loading to (SO) managed data.  I created a new test map and placed a stopwatch on the initial chunk loading, which loads all 9 chunks around the player in the initial load.  Here are the results.

Pre-SO Chunk Load Times

Post-SO Chunk Load Times

The above comparison shows before the (SO) refactoring, my chunk engine loaded the 9 chunks around the player in 2.91 seconds.  After implementing the (SO) solution, the 9 chunks loaded in only .73 seconds or 730 milliseconds!  I noticed also that the (SO) solution was extremely consistent across almost all the chunks after the initial chunk loaded.  Whereas the pre-SO load times are wildly different, ranging from 870 milliseconds down to 15 milliseconds.  Needless to say, I am incredibly impressed with the results of this work.  The (SO) path has been very rewarding thus far.  I will definitely be looking for every opportunity to use Scriptable Objects in as much of my game as possible.  I hope this has been as interesting to you as it has been rewarding for me.  

Until next time...