SporeLab

Concept

SporeLab is a niche tool that initially only aimed to reconcile missing references and empty folders caused by switching between branches in a version control system (mainly Git). Based on my experience, this issue persisted until the 2020 tech stream of the Unity editor.

SporeLab’s concept eventually evolved into being an experimentation tool, meaning it would allow developers to, for instance, quickly prototype an idea branching from their current project without breaking changes made in production.

Of course, one could just use version control and create another branch to experiment their idea, but the root problem aforementioned remains. Hence, I considered this tool to be very niche.

SporeLab main menu; large buttons from top to bottom and left to right: create, edit, archive, backup, and delete experiment
SporeLab main menu; large buttons from top to bottom and left to right: create, edit, archive, backup, and delete experiment

When an experiment is backed up, SporeLab creates a copy of the experiment in the project and moves its files to the project’s persistent data location. Users can create multiple backups differentiated by the time the backup was created.

Two backups were created for experiment `WorkerExpVariant`
Two backups were created for experiment WorkerExpVariant

Sampling Optimization

On much larger projects, each asset can depend on multiple assets, which would be a pain every time the user wants to create an experiment and load samples. So, I created a class that would refresh and cache assets to decrease the lookup time when loading samples and their dependencies.

Bracket indicates dependencies; arrow indicates sample size (excluding dependencies)
Bracket indicates dependencies; arrow indicates sample size (excluding dependencies)
Refreshing asset index to cache samples
Refreshing asset index to cache samples
AssetIndexer class

public static class AssetIndexer
{
    // Key: asset path; Value: asset dependencies
    private static Dictionary<string, string[]> assetIndex = new Dictionary<string, string[]>();

    /// <summary>
    /// Initializes asset index by caching dependencies for all valid asset paths.
    /// </summary>
    internal static void Initialize()
    {
        assetIndex.Clear();
        var paths = AssetDatabase.GetAllAssetPaths()
                                  .Where(path => path.StartsWith("Assets/"))  // Must be inside project
                                  .Where(path => !AssetDatabase.IsValidFolder(path))  // Must not be a folder
                                  .Where(path => !SporeLab.Configuration.Ignored(path)) // Must not be user-ignored
                                  .Where(path => !SporeLab.Configuration.ContainsIgnoredPath(path)) // Must not be subpath of a user-ignored path
                                  .ToArray();


        for (int i = 0; i < paths.Length; i++)
        {
            AddAsset(paths[i]);
        }
    }

    /// <summary>
    /// Gets the asset's dependencies.
    /// </summary>
    /// <param name="path">The asset path.</param>
    /// <returns>An array of asset paths that the input asset path depends on.</returns>
    internal static string[] TryGetDependencies(string path)
    {
        if (assetIndex.TryGetValue(path, out string[] dependencies))
        {
            return dependencies;
        }

        // On-demand caching
        AddAsset(path);
        return assetIndex[path];
    }

    /// <summary>
    /// Updates assets' dependencies.
    /// </summary>
    /// <param name="paths">The array of asset paths.</param>
    internal static void UpdateDependencies(string[] paths)
    {
        for (int i = 0; i < paths.Length; i++)
        {
            assetIndex[paths[i]] = AssetDatabase.GetDependencies(paths[i], true);
        }
    }

    static void AddAsset(string path)
    {
        if (!assetIndex.ContainsKey(path))
        {
            assetIndex.Add(path, AssetDatabase.GetDependencies(path, true));
        }
    }
}

Example Experimentation

While an experiment is being created, SporeLab looks for dependencies that a sample - an object selected for experimentation - depends on. In the image, an experiment called ExpCompare was created. These were the steps for populating the experiment:

  1. The file Tables.asset was selected as a sample.
  2. SporeLab searches dependencies for Tables.asset and finds TableCollection.cs.
  3. SporeLab experimentalizes Tables.asset and TableCollection.cs.
  4. The files ExpCompare_Tables.asset and ExpCompare_TableCollection.cs are created.
Assets are duplicated for experimentation
Assets are duplicated for experimentation
Tony Nguyen
Tony Nguyen
Technical Game Designer

Tenacious game developer with an unwavering passion for overcoming game development challenges.