Files

378 lines
14 KiB
C#
Raw Permalink Normal View History

2026-04-05 18:57:16 +10:00
using System;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
public class AssetBundler
{
/// <summary>
/// The types of assets to search for in checks.
/// </summary>
private static readonly string ASSET_SEARCH_QUERY = "t:prefab,t:textAsset,t:audioclip";
/// <summary>
/// Temporary location for building AssetBundles.
/// </summary>
private static readonly string TEMP_BUILD_FOLDER = "Temp/AssetBundles";
/// <summary>
/// Name of the output bundle file. This needs to match the bundle that you tag your assets with.
/// </summary>
private static readonly string BUNDLE_FILENAME = "mod.assets";
/// <summary>
/// The output folder to place the completed bundle in.
/// </summary>
private static readonly string OUTPUT_FOLDER = "content";
/// <summary>
/// The folders to not search for assets in.
/// </summary>
private static readonly string[] EXCLUDED_FOLDERS = new string[] { "Assets/Editor", "Packages" };
/// <summary>
/// The build target of the asset bundle. Should either be StandaloneWindows or StandaloneOSX, depending on your platform.
/// </summary>
private BuildTarget Target = BuildTarget.StandaloneWindows;
/// <summary>
/// Number of warnings encountered.
/// </summary>
private int NumWarnings;
/// <summary>
/// Number of warnings encountered.
/// </summary>
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);
}
}
/// <summary>
/// Generate the random asset bundle tag to use when building the asset bundle.
/// </summary>
private void GenerateRandomAssetBundleTag()
{
System.Random rand = new System.Random();
GeneratedAssetBundleTag = $"mod-{rand.Next(0, int.MaxValue)}.assets";
}
/// <summary>
/// Move assets tagged with BUNDLE_FILENAME to the temporary asset bundle
/// </summary>
private void MoveAssetsToTemporaryAssetBundle()
{
SubstituteAssetBundleTags(BUNDLE_FILENAME, GeneratedAssetBundleTag);
}
/// <summary>
/// Move assets tagged with the temporary asset bundle back to BUNDLE_FILENAME
/// </summary>
private void RestoreAssetBundleTags()
{
SubstituteAssetBundleTags(GeneratedAssetBundleTag, BUNDLE_FILENAME);
}
/// <summary>
/// Find all assets tagged with a certain asset bundle tag and replace them with another tag
/// </summary>
/// <param name="from">The asset bundle tag to search for</param>
/// <param name="to">The new asset bundle tag</param>
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;
}
}
/// <summary>
/// Delete and recreate the OUTPUT_FOLDER to ensure a clean build.
/// </summary>
protected void CleanBuildFolder()
{
Debug.LogFormat("Cleaning {0}...", OUTPUT_FOLDER);
if (Directory.Exists(OUTPUT_FOLDER))
{
Directory.Delete(OUTPUT_FOLDER, true);
}
Directory.CreateDirectory(OUTPUT_FOLDER);
}
/// <summary>
/// Build the AssetBundle itself and copy it to the OUTPUT_FOLDER.
/// </summary>
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);
}
/// <summary>
/// Checks if the given path is a search path.
/// </summary>
/// <param name="path">The path to check.</param>
/// <returns>true if the given path is a search path, otherwise false.</returns>
protected static bool IsIncludedAssetPath(string path)
{
foreach (string excludedPath in EXCLUDED_FOLDERS)
{
if (path.StartsWith(excludedPath))
{
return false;
}
}
return true;
}
/// <summary>
/// Log a warning for all potential assets that are not currently tagged to be in this AssetBundle.
/// </summary>
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;
}
}
}
/// <summary>
/// Verify that there is at least one asset to be included in the asset bundle.
/// </summary>
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));
}
}
/// <summary>
/// Warn if there are any mesh assets tagged. If so, the user probably meant to tag a prefab instead.
/// </summary>
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;
}
}
/// <summary>
/// Warn if there are any material assets tagged. If so, the user probably meant to tag a prefab instead.
/// </summary>
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<GameObject>(path);
MeshRenderer[] renderers = prefab.GetComponentsInChildren<MeshRenderer>();
foreach (MeshRenderer renderer in renderers)
{
if (renderer.sharedMaterials.Any(m => m != null))
{
Debug.LogWarningFormat("Material found attached to bundle prefab in \"{0}\" at \"<root>/{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<GameObject>(path);
MeshRenderer[] renderers = prefab.GetComponentsInChildren<MeshRenderer>();
foreach (MeshRenderer renderer in renderers)
{
if (renderer.sharedMaterials.Length > 0)
{
renderer.sharedMaterials = new Material[renderer.sharedMaterials.Length];
Debug.LogFormat("Stripped materials from \"{0}\" at \"<root>/{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<GameObject>(path);
MeshRenderer[] renderers = prefab.GetComponentsInChildren<MeshRenderer>();
Material defaultMaterial = AssetDatabase.GetBuiltinExtraResource<Material>("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 \"<root>/{1}\".", path, GetGameObjectPath(renderer.transform).Split(new char[] { '/' }, 3)[2]);
}
}
}
Debug.LogFormat("[{0}] Done setting materials.", DateTime.Now.ToLocalTime());
}
}