using System; using System.IO; using System.Linq; using UnityEditor; using UnityEngine; public class AssetBundler { /// /// The types of assets to search for in checks. /// private static readonly string ASSET_SEARCH_QUERY = "t:prefab,t:textAsset,t:audioclip"; /// /// Temporary location for building AssetBundles. /// private static readonly string TEMP_BUILD_FOLDER = "Temp/AssetBundles"; /// /// Name of the output bundle file. This needs to match the bundle that you tag your assets with. /// private static readonly string BUNDLE_FILENAME = "mod.assets"; /// /// The output folder to place the completed bundle in. /// private static readonly string OUTPUT_FOLDER = "content"; /// /// The folders to not search for assets in. /// private static readonly string[] EXCLUDED_FOLDERS = new string[] { "Assets/Editor", "Packages" }; /// /// The build target of the asset bundle. Should either be StandaloneWindows or StandaloneOSX, depending on your platform. /// private BuildTarget Target = BuildTarget.StandaloneWindows; /// /// Number of warnings encountered. /// private int NumWarnings; /// /// Number of warnings encountered. /// private string GeneratedAssetBundleTag; [MenuItem("PlateUp!/Build Asset Bundle _F6")] public static void BuildAssetBundle() { Debug.LogFormat("Creating \"{0}\" AssetBundle...", BUNDLE_FILENAME); AssetBundler bundler = new AssetBundler(); if (Application.platform == RuntimePlatform.OSXEditor) bundler.Target = BuildTarget.StandaloneOSX; // Randomly generate the resulting name of the asset bundle bundler.GenerateRandomAssetBundleTag(); bool success = false; try { // Check for assets bundler.WarnIfAssetsAreNotTagged(); bundler.WarnIfZeroAssetsAreTagged(); bundler.WarnIfMeshAssetsAreTagged(); // bundler.WarnIfMaterialsAreTaggedOrIncluded(); // Delete the contents of OUTPUT_FOLDER bundler.CleanBuildFolder(); // Temporarily move the tagged assets to the temporary tag bundler.MoveAssetsToTemporaryAssetBundle(); // Lastly, create the asset bundle itself and copy it to the output folder bundler.CreateAssetBundle(); success = true; } catch (Exception e) { Debug.LogErrorFormat("Failed to build AssetBundle: {0}\n{1}", e.Message, e.StackTrace); } // Return assets to the original asset bundle tag bundler.RestoreAssetBundleTags(); AssetDatabase.RemoveUnusedAssetBundleNames(); if (success) { Debug.LogFormat("[{0}] Build complete with {1} warnings! Output: {2} (temporary ID: {3})", DateTime.Now.ToLocalTime(), bundler.NumWarnings, OUTPUT_FOLDER + "/" + BUNDLE_FILENAME, bundler.GeneratedAssetBundleTag); } } /// /// Generate the random asset bundle tag to use when building the asset bundle. /// private void GenerateRandomAssetBundleTag() { System.Random rand = new System.Random(); GeneratedAssetBundleTag = $"mod-{rand.Next(0, int.MaxValue)}.assets"; } /// /// Move assets tagged with BUNDLE_FILENAME to the temporary asset bundle /// private void MoveAssetsToTemporaryAssetBundle() { SubstituteAssetBundleTags(BUNDLE_FILENAME, GeneratedAssetBundleTag); } /// /// Move assets tagged with the temporary asset bundle back to BUNDLE_FILENAME /// private void RestoreAssetBundleTags() { SubstituteAssetBundleTags(GeneratedAssetBundleTag, BUNDLE_FILENAME); } /// /// Find all assets tagged with a certain asset bundle tag and replace them with another tag /// /// The asset bundle tag to search for /// The new asset bundle tag private void SubstituteAssetBundleTags(string from, string to) { string[] assetGUIDs = AssetDatabase.FindAssets($"b:{from}"); foreach (var assetGUID in assetGUIDs) { string path = AssetDatabase.GUIDToAssetPath(assetGUID); var importer = AssetImporter.GetAtPath(path); importer.assetBundleName = to; } } /// /// Delete and recreate the OUTPUT_FOLDER to ensure a clean build. /// protected void CleanBuildFolder() { Debug.LogFormat("Cleaning {0}...", OUTPUT_FOLDER); if (Directory.Exists(OUTPUT_FOLDER)) { Directory.Delete(OUTPUT_FOLDER, true); } Directory.CreateDirectory(OUTPUT_FOLDER); } /// /// Build the AssetBundle itself and copy it to the OUTPUT_FOLDER. /// protected void CreateAssetBundle() { Debug.Log("Building AssetBundle..."); // Build all AssetBundles to the TEMP_BUILD_FOLDER if (!Directory.Exists(TEMP_BUILD_FOLDER)) { Directory.CreateDirectory(TEMP_BUILD_FOLDER); } #pragma warning disable 618 // Build the asset bundle with the CollectDependencies flag. This is necessary or else ScriptableObjects will // not be accessible within the asset bundle. Unity has deprecated this flag claiming it is now always active, // but due to a bug we must still include it (and ignore the warning). BuildPipeline.BuildAssetBundles( TEMP_BUILD_FOLDER, BuildAssetBundleOptions.UncompressedAssetBundle | BuildAssetBundleOptions.CollectDependencies, Target); #pragma warning restore 618 // We are only interested in the BUNDLE_FILENAME bundle (and not any extra AssetBundle or the manifest files // that Unity makes), so just copy that to the final output folder string srcPath = Path.Combine(TEMP_BUILD_FOLDER, GeneratedAssetBundleTag); string destPath = Path.Combine(OUTPUT_FOLDER, BUNDLE_FILENAME); File.Copy(srcPath, destPath, true); } /// /// Checks if the given path is a search path. /// /// The path to check. /// true if the given path is a search path, otherwise false. protected static bool IsIncludedAssetPath(string path) { foreach (string excludedPath in EXCLUDED_FOLDERS) { if (path.StartsWith(excludedPath)) { return false; } } return true; } /// /// Log a warning for all potential assets that are not currently tagged to be in this AssetBundle. /// protected void WarnIfAssetsAreNotTagged() { string[] assetGUIDs = AssetDatabase.FindAssets(ASSET_SEARCH_QUERY); foreach (var assetGUID in assetGUIDs) { string path = AssetDatabase.GUIDToAssetPath(assetGUID); if (!IsIncludedAssetPath(path)) { continue; } var importer = AssetImporter.GetAtPath(path); if (!importer.assetBundleName.Equals(BUNDLE_FILENAME)) { // Debug.LogWarningFormat("Asset \"{0}\" is not tagged with \"{1}\" and will not be included in the AssetBundle!", path, BUNDLE_FILENAME); // ++NumWarnings; } } } /// /// Verify that there is at least one asset to be included in the asset bundle. /// protected void WarnIfZeroAssetsAreTagged() { string[] assetsInBundle = AssetDatabase.FindAssets($"{ASSET_SEARCH_QUERY},b:{BUNDLE_FILENAME}"); if (assetsInBundle.Length == 0) { // throw new Exception(string.Format("No assets have been tagged for inclusion in the {0} AssetBundle.", BUNDLE_FILENAME)); } } /// /// Warn if there are any mesh assets tagged. If so, the user probably meant to tag a prefab instead. /// protected void WarnIfMeshAssetsAreTagged() { string[] assetGUIDs = AssetDatabase.FindAssets($"t:mesh,b:{BUNDLE_FILENAME}"); foreach (var assetGUID in assetGUIDs) { string path = AssetDatabase.GUIDToAssetPath(assetGUID); if (!IsIncludedAssetPath(path)) { continue; } Debug.LogWarningFormat("Mesh asset \"{0}\" is tagged for inclusion in the {1} AssetBundle! This is likely a mistake. You should include a prefab instead.", path, BUNDLE_FILENAME); ++NumWarnings; } } /// /// Warn if there are any material assets tagged. If so, the user probably meant to tag a prefab instead. /// protected void WarnIfMaterialsAreTaggedOrIncluded() { // Check for directly tagged materials string[] assetGUIDs = AssetDatabase.FindAssets($"t:material,b:{BUNDLE_FILENAME}"); foreach (var assetGUID in assetGUIDs) { string path = AssetDatabase.GUIDToAssetPath(assetGUID); if (!IsIncludedAssetPath(path)) { continue; } Debug.LogWarningFormat("Material asset \"{0}\" is tagged for inclusion in the {1} AssetBundle! This is likely a mistake. You should use generate materials using the vanilla shaders instead.", path, BUNDLE_FILENAME); ++NumWarnings; } // Check for materials assigned to prefabs assetGUIDs = AssetDatabase.FindAssets($"t:prefab,b:{BUNDLE_FILENAME}"); foreach (var assetGUID in assetGUIDs) { string path = AssetDatabase.GUIDToAssetPath(assetGUID); if (!IsIncludedAssetPath(path)) { continue; } GameObject prefab = AssetDatabase.LoadAssetAtPath(path); MeshRenderer[] renderers = prefab.GetComponentsInChildren(); foreach (MeshRenderer renderer in renderers) { if (renderer.sharedMaterials.Any(m => m != null)) { Debug.LogWarningFormat("Material found attached to bundle prefab in \"{0}\" at \"/{1}\"! This is likely a mistake. To avoid log spam and texturing issues, you should remove these materials or set them to \"None\".", path, GetGameObjectPath(renderer.transform).Split(new char[] { '/' }, 3)[2]); ++NumWarnings; } } } } public static string GetGameObjectPath(Transform current) { if (current.parent == null) return "/" + current.name; return GetGameObjectPath(current.parent) + "/" + current.name; } [MenuItem("PlateUp!/Preparation/[Deprecated] Strip Materials From Prefabs")] public static void RemoveAllPrefabMaterials() { if (!EditorUtility.DisplayDialog("Confirm", "Stripping materials from prefabs is an irreversible process. Perform at your own risk.", "Proceed", "Cancel")) { return; } string[] assetGUIDs = AssetDatabase.FindAssets($"t:prefab,b:{BUNDLE_FILENAME}"); foreach (var assetGUID in assetGUIDs) { string path = AssetDatabase.GUIDToAssetPath(assetGUID); if (!IsIncludedAssetPath(path)) { continue; } GameObject prefab = AssetDatabase.LoadAssetAtPath(path); MeshRenderer[] renderers = prefab.GetComponentsInChildren(); foreach (MeshRenderer renderer in renderers) { if (renderer.sharedMaterials.Length > 0) { renderer.sharedMaterials = new Material[renderer.sharedMaterials.Length]; Debug.LogFormat("Stripped materials from \"{0}\" at \"/{1}\".", path, GetGameObjectPath(renderer.transform).Split(new char[] { '/' }, 3)[2]); } } } Debug.LogFormat("[{0}] Done stripping materials.", DateTime.Now.ToLocalTime()); } [MenuItem("PlateUp!/Preparation/[Deprecated] Set Prefab Materials to Default")] public static void SetAllPrefabMaterialsToDefault() { if (!EditorUtility.DisplayDialog("Confirm", "Changing the materials of prefabs is an irreversible process. Perform at your own risk.", "Proceed", "Cancel")) { return; } string[] assetGUIDs = AssetDatabase.FindAssets($"t:prefab,b:{BUNDLE_FILENAME}"); foreach (var assetGUID in assetGUIDs) { string path = AssetDatabase.GUIDToAssetPath(assetGUID); if (!IsIncludedAssetPath(path)) { continue; } GameObject prefab = AssetDatabase.LoadAssetAtPath(path); MeshRenderer[] renderers = prefab.GetComponentsInChildren(); Material defaultMaterial = AssetDatabase.GetBuiltinExtraResource("Default-Material.mat"); foreach (MeshRenderer renderer in renderers) { if (renderer.sharedMaterials.Length > 0) { var newMaterials = new Material[renderer.sharedMaterials.Length]; for (int i = 0; i < newMaterials.Length; i++) { newMaterials[i] = defaultMaterial; } renderer.sharedMaterials = newMaterials; Debug.LogFormat("Set materials from \"{0}\" at \"/{1}\".", path, GetGameObjectPath(renderer.transform).Split(new char[] { '/' }, 3)[2]); } } } Debug.LogFormat("[{0}] Done setting materials.", DateTime.Now.ToLocalTime()); } }