Table of Contents

Plugin Development Guide

TAssetBundle can be extended through plugins that hook into build, runtime, and Inspector flows. This guide walks through how to build a plugin from scratch, with the bundled Play Asset Delivery plugin as a working reference.

For advanced scenarios that need deep Unity build pipeline integration (e.g. injecting files into the generated Gradle project), implement Unity's own hooks (IPostGenerateGradleAndroidProject, IPreprocessBuildWithReport, ...) alongside the TAssetBundle plugin interfaces — they compose naturally.


Anatomy of a Plugin

A plugin consists of two parts:

  1. Plugin asset — a ScriptableObject subclass of PluginBase. Holds user-facing configuration.
  2. Plugin modules — classes derived from PluginModuleBase<TPlugin> tagged with [PluginModule(typeof(TPlugin))]. Implement one or more module interfaces to hook into TAssetBundle's lifecycle.
MyPlugin.asset (PluginBase)
 ├── MyPluginSettings (fields)
 └── Modules (discovered at runtime by PluginModuleManager)
      ├── MyBuilderModule : PluginModuleBase<MyPlugin>, IAssetBundleBuilderPluginModule
      ├── MyProviderModule : PluginModuleBase<MyPlugin>, IAssetBundleProviderPluginModule
      └── MyManifestEditorModule : PluginModuleBase<MyPlugin>, IManifestEditorPluginModule

At runtime, TAssetBundle:

  1. Loads each plugin asset from Settings.plugins
  2. Instantiates every module class tagged with [PluginModule(typeof(ThatPlugin))]
  3. Calls each module's IsAvailable getter to gate on platform/conditions
  4. Invokes the appropriate interface method at the matching lifecycle point

Module Interfaces

IAssetBundleBuilderPluginModule (Editor)

Hooks into the AssetBundle build pipeline.

public interface IAssetBundleBuilderPluginModule : IPluginModule
{
    bool CanPushToBuiltinAssetBundle(AssetBundleInfo info);
    Task PrepareForBuild(BuildTarget buildTarget, AssetCatalog catalog);
    void PreprocessBuild(BuildTarget buildTarget, AssetCatalog catalog);
}
Method When it runs Typical use
CanPushToBuiltinAssetBundle Per-bundle, at catalog write Exclude bundles you're shipping via an alternate channel (e.g. asset packs) from the StreamingAssets copy
PrepareForBuild From TAssetBundle > Plugins > Run Pre-Build Actions menu Stage files, generate intermediate artifacts
PreprocessBuild At AssetBundle build start AND Unity player build start Snapshot editor state into the plugin asset for runtime reference

IAssetBundleProviderPluginModule (Runtime)

Registers a custom bundle loading provider.

public interface IAssetBundleProviderPluginModule : IPluginModule
{
    IAssetBundleProvider CreateAssetBundleProvider(IAssetBundleManager manager);
}

Returned provider is placed in the provider chain. IsSupported(info) decides per-bundle whether this provider handles the load.

IManifestEditorPluginModule (Editor)

Adds a section to every TAssetBundleManifest's Inspector. Receives every manifest currently selected so changes can be applied to all of them at once (Unity multi-object edit).

public interface IManifestEditorPluginModule : IPluginModule
{
    void OnManifestInspectorGUI(TAssetBundleManifest[] manifests);
}

Pair with ManifestPluginData (below) when you need per-manifest persistent data.

The display reads from manifests[0] (no mixed-value indicator by default); use TAssetBundleManifestUtil.ApplyToAll to propagate edits to every selected manifest with a single Undo step:

if (EditorGUI.EndChangeCheck())
    manifests.ApplyToAll("Change MyField", m => m.GetOrCreatePluginData<MyPluginData>().value = newValue);

Per-Manifest Data (ManifestPluginData)

To attach serialized data to individual manifests, derive from ManifestPluginData:

[Serializable]
public class MyPluginData : ManifestPluginData
{
    public string region = "global";
    public int priority;

    // Called during AssetBundle build — use SetCustomData to forward values
    // to the runtime catalog.
    public override void OnBuildAssetBundleInfo(AssetBundleInfo info)
    {
        info.SetCustomData("region", region);
        info.SetCustomData("priority", priority.ToString());
    }
}

Read from/write to it inside your IManifestEditorPluginModule.OnManifestInspectorGUI:

var data = manifest.GetPluginData<MyPluginData>();                 // null if not yet configured
var created = manifest.GetOrCreatePluginData<MyPluginData>();      // creates if missing
manifest.RemovePluginData<MyPluginData>();                         // opt-out cleanup

At runtime, pull the value back via AssetBundleRuntimeInfo.TryGetCustomData(...):

if (info.TryGetCustomData("region", out var region)) { /* ... */ }

Execution Order

Between plugins

The list in TAssetBundle Settings → Plugins is the execution order. Reorder entries to change relative priority across plugins. No dedicated priority attribute is needed — drag-and-drop the list.

Within one plugin

Modules of the same plugin are ordered by the optional priority parameter on [PluginModule]:

[PluginModule(typeof(MyPlugin), priority: 500)]
class MyEarlyModule : PluginModuleBase<MyPlugin>, IAssetBundleBuilderPluginModule { ... }

[PluginModule(typeof(MyPlugin), priority: 2000)]
class MyLateModule : PluginModuleBase<MyPlugin>, IAssetBundleBuilderPluginModule { ... }

Lower priority runs first. Default is 1000.


Platform Gating with IsAvailable

Each module overrides IsAvailable — return false to skip the module under the current conditions.

public override bool IsAvailable
{
    get
    {
#if UNITY_ANDROID && !UNITY_EDITOR
        return Owner.BuildAppBundle;
#else
        return false;
#endif
    }
}

The provider module above only activates on Android AAB device builds, so the provider is not registered on other platforms or in the Editor.


Minimal Template

1. Plugin asset

using UnityEngine;

namespace MyCompany.MyPlugin
{
    [CreateAssetMenu(menuName = "TAssetBundle/Plugin/My Plugin")]
    public class MyPlugin : PluginBase
    {
        public string someField = "hello";
    }
}

Create MyPlugin.asset via the Project window menu and drag it into TAssetBundle Settings → Plugins.

2. Runtime provider module

using TAssetBundle;

namespace MyCompany.MyPlugin
{
    [PluginModule(typeof(MyPlugin))]
    class MyProviderModule : PluginModuleBase<MyPlugin>, IAssetBundleProviderPluginModule
    {
        public override bool IsAvailable => true;

        public IAssetBundleProvider CreateAssetBundleProvider(IAssetBundleManager manager)
        {
            return new MyCustomProvider(Owner, manager);
        }
    }
}

3. Editor build hook module

using System.Threading.Tasks;
using TAssetBundle;
using TAssetBundle.Editor;
using UnityEditor;

namespace MyCompany.MyPlugin.Editor
{
    [PluginModule(typeof(MyPlugin))]
    class MyBuilderModule : PluginModuleBase<MyPlugin>, IAssetBundleBuilderPluginModule
    {
        public override bool IsAvailable => true;

        public bool CanPushToBuiltinAssetBundle(AssetBundleInfo info) => true;
        public Task PrepareForBuild(BuildTarget target, AssetCatalog catalog) => Task.CompletedTask;
        public void PreprocessBuild(BuildTarget target, AssetCatalog catalog) { }
    }
}

4. Manifest Inspector module (optional)

using TAssetBundle.Editor;
using UnityEditor;

namespace MyCompany.MyPlugin.Editor
{
    [PluginModule(typeof(MyPlugin))]
    class MyManifestEditorModule : PluginModuleBase<MyPlugin>, IManifestEditorPluginModule
    {
        public override bool IsAvailable => true;

        public void OnManifestInspectorGUI(TAssetBundleManifest[] manifests)
        {
            EditorGUILayout.LabelField("My Plugin Settings", EditorStyles.boldLabel);
            // Read current state from manifests[0], then use manifests.ApplyToAll(...)
            // to propagate changes to every selected manifest with Undo.
        }
    }
}

Suggested folder layout

Assets/TAssetBundlePlugins/MyPlugin/
├── README.md
├── my_plugin.asset
└── Scripts/
    ├── MyPlugin.cs                 // Plugin asset
    ├── MyProviderModule.cs         // Runtime provider
    ├── MyCustomProvider.cs         // Actual provider logic
    ├── MyPlugin.asmdef             // Optional, scopes runtime code
    └── Editor/
        ├── MyBuilderModule.cs      // Build hook
        ├── MyManifestEditorModule.cs
        ├── MyPluginData.cs         // ManifestPluginData
        └── MyPlugin.Editor.asmdef  // Optional, scopes editor code

Use separate .asmdef files so editor-only code never leaks into player builds.


Reference: Play Asset Delivery

The bundled Play Asset Delivery (PAD) plugin at Assets/TAssetBundlePlugins/PlayAssetDelivery/ exercises every part of the plugin system:

  • PlayAssetDelivery.cs — plugin asset with runtime event (OnDownloadProgress)
  • PADAssetBundleProvider.cs + AssetBundleProviderPluginModule.cs — custom runtime provider
  • Editor/AssetBundleBuilderPluginModule.cs — AssetBundle build hook
  • Editor/ManifestEditorPluginModule.cs — per-manifest Inspector UI
  • Editor/PlayAssetDeliveryData.csManifestPluginData with OnBuildAssetBundleInfo
  • Editor/PlayAssetDeliveryGradleModifier.cs — Unity's IPostGenerateGradleAndroidProject hook used alongside the plugin system to inject asset-pack modules into the generated Gradle project

Open the PAD plugin's README.md (bundled at Assets/TAssetBundlePlugins/PlayAssetDelivery/) for a fuller walk-through of a real production plugin.