How Do I Add a Custom Build Script?

I am working through the Addressables Example project, Variants example. I can’t figure out how to add the custom build script BuildScriptInherited.cs to my project. It won’t let me add it the script to Build and Play Mode Scripts in the Addressables Settings. The file browser greys out the filename when I try to add it.

In the example project the script has a little blue icon with red curly brackets but I don’t know what that icon means. Some sort of script prefab?

I check the docs for my version 1.19.11 but it just says
See the [Addressable variants project](https://github.com/Unity-Technologies/Addressables-Sample/tree/master/Advanced/Addressable%20Variants) in the [Addressables-Sample](https://github.com/Unity-Technologies/Addressables-Sample) repository for an example.

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.AddressableAssets.Build;
using UnityEditor.AddressableAssets.Build.DataBuilders;
using UnityEditor.AddressableAssets.Settings;
using UnityEditor.AddressableAssets.Settings.GroupSchemas;
using UnityEditor.VersionControl;
using UnityEngine;
using UnityEngine.ResourceManagement.Util;
using UnityEngine.Serialization;

[CreateAssetMenu(fileName = "BuildScriptInherited.asset", menuName = "Addressables/Custom Build/Packed Variations")]
public class BuildScriptInherited : BuildScriptPackedMode, ISerializationCallbackReceiver
{
    public override string Name
    {
        get { return "Packed Variations"; }
    }

    Dictionary<string, Hash128> m_AssetPathToHashCode = new Dictionary<string, Hash128>();
    HashSet<string> m_DirectoriesInUse = new HashSet<string>();
    string m_BaseDirectory = "Assets/AddressablesGenerated";

    protected override TResult BuildDataImplementation<TResult>(AddressablesDataBuilderInput context)
    {
        var result = base.BuildDataImplementation<TResult>(context);

        AddressableAssetSettings settings = context.AddressableSettings;
        DoCleanup(settings);
        return result;
    }

    protected override string ProcessGroup(AddressableAssetGroup assetGroup, AddressableAssetsBuildContext aaContext)
    {
        if (assetGroup.HasSchema<BundledAssetGroupSchema>() && assetGroup.GetSchema<BundledAssetGroupSchema>().IncludeInBuild)
        {
            if (assetGroup.HasSchema<TextureVariationSchema>())
            {
                var errorString = ProcessTextureScaler(assetGroup.GetSchema<TextureVariationSchema>(), assetGroup, aaContext);
                if (!string.IsNullOrEmpty(errorString))
                    return errorString;
            }

            if (assetGroup.HasSchema<PrefabTextureVariantSchema>())
            {
                var errorString = ProcessVariants(assetGroup.GetSchema<PrefabTextureVariantSchema>(), assetGroup, aaContext);
                if (!string.IsNullOrEmpty(errorString))
                    return errorString;
            }
        }
        return base.ProcessGroup(assetGroup, aaContext);
    }

    List<AddressableAssetGroup> m_SourceGroupList = new List<AddressableAssetGroup>();
    Dictionary<string, AddressableAssetGroup> m_GeneratedGroups = new Dictionary<string, AddressableAssetGroup>();

  
    AddressableAssetGroup FindOrCopyGroup(string groupName, AddressableAssetGroup baseGroup, AddressableAssetSettings settings, TextureVariationSchema schema)
    {
        AddressableAssetGroup result;
        if (!m_GeneratedGroups.TryGetValue(groupName, out result))
        {
            List<AddressableAssetGroupSchema> schemas = new List<AddressableAssetGroupSchema>(baseGroup.Schemas);
            schemas.Remove(schema);
            result = settings.CreateGroup(groupName, false, false, false, schemas);
            m_GeneratedGroups.Add(groupName, result);
        }

        return result;
    }

    string ProcessVariants(PrefabTextureVariantSchema schema,
        AddressableAssetGroup group,
        AddressableAssetsBuildContext context)
    {
        var settings = context.Settings;
        Directory.CreateDirectory(m_BaseDirectory);

        var entries = new List<AddressableAssetEntry>(group.entries);
        foreach (var mainEntry in entries)
        {
            if (AssetDatabase.GetMainAssetTypeAtPath(mainEntry.AssetPath) != typeof(GameObject))
                continue;

            string fileName = Path.GetFileNameWithoutExtension(mainEntry.AssetPath);
            string ext = Path.GetExtension(mainEntry.AssetPath);
            mainEntry.SetLabel(schema.DefaultLabel, true, true);
          
            string mainAssetPath = AssetDatabase.GUIDToAssetPath(mainEntry.guid);
            Hash128 assetHash = AssetDatabase.GetAssetDependencyHash(mainAssetPath);

            bool assetHashChanged = false;
            if (!m_AssetPathToHashCode.ContainsKey(mainAssetPath))
                m_AssetPathToHashCode.Add(mainAssetPath, assetHash);
            else if (m_AssetPathToHashCode[mainAssetPath] != assetHash)
            {
                assetHashChanged = true;
                m_AssetPathToHashCode[mainAssetPath] = assetHash;
            }

            foreach (var variant in schema.Variants)
            {
                string groupDirectory = Path.Combine(m_BaseDirectory, $"{group.Name}-{Path.GetFileNameWithoutExtension(mainEntry.address)}").Replace('\\', '/');
                m_DirectoriesInUse.Add(groupDirectory);
                var variantGroup = CreateTemporaryGroupCopy($"{group.Name}_VariantGroup_{variant.Label}", group.Schemas, settings);

                string baseVariantDirectory = Path.Combine(groupDirectory, variant.Label).Replace('\\', '/');
                string newPrefabPath = mainAssetPath.Replace("Assets/", baseVariantDirectory + '/').Replace($"{fileName}{ext}", $"{fileName}_variant_{variant.Label}{ext}");

                string variantDirectory = Path.GetDirectoryName(newPrefabPath).Replace('\\', '/');
                m_DirectoriesInUse.Add(variantDirectory);
                Directory.CreateDirectory(variantDirectory);

                if (assetHashChanged || !File.Exists(newPrefabPath))
                {
                    if (!AssetDatabase.CopyAsset(mainAssetPath, newPrefabPath))
                        return $"Copying asset from {mainAssetPath} to variant path {newPrefabPath} failed.";
                }

                var dependencies = AssetDatabase.GetDependencies(newPrefabPath);
                foreach (var dependency in dependencies)
                {
                    if (AssetDatabase.GetMainAssetTypeAtPath(dependency) == typeof(GameObject))
                    {
                        var gameObject = AssetDatabase.LoadAssetAtPath<GameObject>(dependency);
                        foreach (var childRender in gameObject.GetComponentsInChildren<MeshRenderer>())
                            ConvertToVariant(childRender, variantDirectory, variant, assetHashChanged);
                    }
                }

                var entry = settings.CreateOrMoveEntry(AssetDatabase.AssetPathToGUID(newPrefabPath), variantGroup, false, false);
                entry.address = mainEntry.address;
                entry.SetLabel(variant.Label, true, true, false);
            }
        }

        if (!schema.IncludeSourcePrefabInBuild)
        {
            group.GetSchema<BundledAssetGroupSchema>().IncludeInBuild = false;
            m_SourceGroupList.Add(group);
        }

        return string.Empty;
    }

    void ConvertToVariant(MeshRenderer meshRenderer, string variantDirectory, PrefabTextureVariantSchema.VariantLabelPair variant, bool assetHashChanged)
    {
        if (meshRenderer != null && meshRenderer.sharedMaterial != null)
        {
            var mat = CreateOrGetVariantMaterial(meshRenderer.sharedMaterial, variantDirectory, variant.Label, assetHashChanged);
            var texture = CreateOrGetVariantTexture(meshRenderer.sharedMaterial.mainTexture,
                variantDirectory, variant.Label, variant.TextureScale, assetHashChanged);

            mat.mainTexture = texture;
            meshRenderer.sharedMaterial = mat;
            AssetDatabase.SaveAssets();
        }
    }

    AddressableAssetGroup CreateTemporaryGroupCopy(string groupName, List<AddressableAssetGroupSchema> schemas, AddressableAssetSettings settings)
    {
        var variantGroup = settings.CreateGroup(groupName, false, false, false, schemas);
        if(variantGroup.HasSchema<PrefabTextureVariantSchema>())
            variantGroup.RemoveSchema<PrefabTextureVariantSchema>();
        if (!m_GeneratedGroups.ContainsKey(variantGroup.Name))
            m_GeneratedGroups.Add(variantGroup.Name, variantGroup);
        return variantGroup;
    }

    Material CreateOrGetVariantMaterial(Material baseMaterial, string variantDirectory, string label, bool assetHashChanged)
    {
        string assetPath = AssetDatabase.GetAssetPath(baseMaterial);
        if(assetPath.StartsWith(variantDirectory))
            return AssetDatabase.LoadAssetAtPath<Material>(assetPath);

        string matFileName = Path.GetFileNameWithoutExtension(assetPath);
        string path = assetPath.Replace("Assets/", variantDirectory + '/').Replace(matFileName, $"{matFileName}_{label}");
        if (assetHashChanged || !File.Exists(path))
        {
            Directory.CreateDirectory(Path.GetDirectoryName(path));
            AssetDatabase.CopyAsset(assetPath, path);
        }

        var mat = AssetDatabase.LoadAssetAtPath<Material>(path);
        m_DirectoriesInUse.Add(Path.GetDirectoryName(path).Replace('\\', '/'));
        return mat;
    }

    Texture2D CreateOrGetVariantTexture(Texture baseTexture, string variantDirectory, string label, float scale, bool assetHashChanged)
    {
        string assetPath = AssetDatabase.GetAssetPath(baseTexture);
        if (assetPath.StartsWith(variantDirectory))
            return AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);

        string textureFileName = Path.GetFileNameWithoutExtension(assetPath);
        string path = assetPath.Replace("Assets/", variantDirectory + '/').Replace(textureFileName, $"{textureFileName}_{label}");

        if (assetHashChanged || !File.Exists(path))
        {
            Directory.CreateDirectory(Path.GetDirectoryName(path));
            AssetDatabase.CopyAsset(assetPath, path);

            var srcTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
            int maxDim = Math.Max(srcTexture.width, srcTexture.height);
            var aiDest = AssetImporter.GetAtPath(path) as TextureImporter;
            if (aiDest == null)
                return null;

            float scaleFactor = scale;
            float desiredLimiter = maxDim * scaleFactor;
            aiDest.maxTextureSize = NearestMaxTextureSize(desiredLimiter);
            aiDest.isReadable = true;
            aiDest.SaveAndReimport();
        }
        var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(path);
        m_DirectoriesInUse.Add(Path.GetDirectoryName(path).Replace('\\', '/'));
        return texture;

    }
  
    string ProcessTextureScaler(
        TextureVariationSchema schema,
        AddressableAssetGroup assetGroup,
        AddressableAssetsBuildContext aaContext)
    {
        var entries = new List<AddressableAssetEntry>(assetGroup.entries);
        foreach (var entry in entries)
        {
            var entryPath = entry.AssetPath;
            if (AssetDatabase.GetMainAssetTypeAtPath(entryPath) == typeof(Texture2D))
            {
                var fileName = Path.GetFileNameWithoutExtension(entryPath);
                if(string.IsNullOrEmpty(fileName))
                    return "Failed to get file name for: " + entryPath;
                if (!Directory.Exists("Assets/GeneratedTextures"))
                    Directory.CreateDirectory("Assets/GeneratedTextures");
                if (!Directory.Exists("Assets/GeneratedTextures/Texture"))
                    Directory.CreateDirectory("Assets/GeneratedTextures/Texture");
              
                var sourceTex = AssetDatabase.LoadAssetAtPath<Texture2D>(entryPath);
                var aiSource = AssetImporter.GetAtPath(entryPath) as TextureImporter;
                int maxDim = Math.Max(sourceTex.width, sourceTex.height);

                foreach (var pair in schema.Variations)
                {
                    var newGroup = FindOrCopyGroup(assetGroup.Name + "_" + pair.label, assetGroup, aaContext.Settings, schema);
                    var newFile = entryPath.Replace(fileName, fileName+"_variationCopy_" + pair.label);
                    newFile = newFile.Replace("Assets/", "Assets/GeneratedTextures/");
                 
                    AssetDatabase.CopyAsset(entryPath, newFile);
              
                    var aiDest = AssetImporter.GetAtPath(newFile) as TextureImporter;
                    if (aiDest == null)
                    {
                        var message = "failed to get TextureImporter on new texture asset: " + newFile;
                        return message;
                    }
                  
                    float scaleFactor = pair.textureScale;

                    float desiredLimiter = maxDim * scaleFactor;
                    aiDest.maxTextureSize = NearestMaxTextureSize(desiredLimiter);

                    aiDest.isReadable = true;

                    aiDest.SaveAndReimport();
                    var newEntry = aaContext.Settings.CreateOrMoveEntry(AssetDatabase.AssetPathToGUID(newFile), newGroup);
                    newEntry.address = entry.address;
                    newEntry.SetLabel(pair.label, true);
                }
                entry.SetLabel(schema.BaselineLabel, true);
            }
        }

        if (!schema.IncludeSourceTextureInBuild)
            assetGroup.GetSchema<BundledAssetGroupSchema>().IncludeInBuild = false;
        m_SourceGroupList.Add(assetGroup); // need to reset labels for every texture variant group

        return string.Empty;
    }

    static int NearestMaxTextureSize(float desiredLimiter)
    {
        float lastDiff = Math.Abs(desiredLimiter);
        int lastPow = 32;
        for (int i = 0; i < 9; i++)
        {

            int powOfTwo = lastPow << 1;
            float newDiff = Math.Abs(desiredLimiter - powOfTwo);
            if (newDiff > lastDiff)
                return lastPow;

            lastPow = powOfTwo;
            lastDiff = newDiff;

        }

        return 8192;
    }
  
    void DoCleanup(AddressableAssetSettings settings)
    {
        List<string> directories = new List<string>();
        foreach (var group in m_GeneratedGroups.Values)
        {
            if (group.HasSchema<TextureVariationSchema>())
            {
                List<AddressableAssetEntry> entries = new List<AddressableAssetEntry>(group.entries);
                foreach (var entry in entries)
                {
                    var path = entry.AssetPath;
                    AssetDatabase.DeleteAsset(path);
                }
            }

            settings.RemoveGroup(group);
            if (Directory.Exists(m_BaseDirectory) && group.HasSchema<PrefabTextureVariantSchema>())
            {
                foreach (var directory in Directory.EnumerateDirectories(m_BaseDirectory, "*", SearchOption.AllDirectories))
                {
                    string formattedDirectory = directory.Replace('\\', '/');
                    if (m_DirectoriesInUse.Contains(formattedDirectory))
                        continue;
                    Directory.Delete(formattedDirectory, true);
                }

            }

        }

        m_DirectoriesInUse.Clear();
        m_GeneratedGroups.Clear();

        foreach (AddressableAssetGroup group in m_SourceGroupList)
        {
            group.GetSchema<BundledAssetGroupSchema>().IncludeInBuild = true;

            var schema = group.GetSchema<TextureVariationSchema>();
            if (schema == null)
                continue;

            foreach (var entry in group.entries)
            {
                entry.labels.Remove(schema.BaselineLabel);
            }
        }
      
        m_SourceGroupList.Clear();
    }

    [SerializeField]
    List<string> m_AssetPathToHashKey = new List<string>();
    [SerializeField]
    List<Hash128> m_AssetPathToHashValue = new List<Hash128>();

    public void OnBeforeSerialize()
    {
        foreach (var key in m_AssetPathToHashCode.Keys)
        {
            m_AssetPathToHashKey.Add(key);
            m_AssetPathToHashValue.Add(m_AssetPathToHashCode[key]);
        }
    }

    public void OnAfterDeserialize()
    {
        for (int i = 0; i < Math.Min(m_AssetPathToHashKey.Count, m_AssetPathToHashValue.Count); i++)
        {
            if(!m_AssetPathToHashCode.ContainsKey(m_AssetPathToHashKey[i]))
                m_AssetPathToHashCode.Add(m_AssetPathToHashKey[i], m_AssetPathToHashValue[i]);
        }
    }
}

Maybe somebody from Unity can answer this? Gotta be a simple question.

Bump!

I searched through the documentation again and came up with nothing.

@unity_bill

Bump

Bump

Bump

@ohthepain @Uzike-Remona Hi apologies for not seeing this sooner.

The trick is that you’d need to create the ScriptableObject .asset file. In this example, you can use Assets > Create > Addressables > Custom Build > Packed Variations menu option to create the BuildScriptInherited.asset. Then add that file to the list of Build and Play Mode Scripts in the AddressableAssetSettings.

I can see how that’s not immediately clear in the documentation. Will create a ticket to make sure this is emphasized in the docs!