Dynamic Feature Modules - Is there a unity implementaion I am missing?

Hi,

Aside from this post, I cannot find any indication of how dynamic feature modules are supported in Unity. When I build I just get a large aab file, that when I unpack I see it does not split in any way (tried marking some assets to belong to AssetBundles, but I think now it only applies for either the unity ones or OBB which is not supported with aab).

Can anyone link me to a resource, or even a post by someone with experience on the matter?

Thanks,

I’m going to bump this up as I am also looking for more information on how to use Android App Bundles with Unity. Apart from that one blog post I can’t seem to find much consistent information or guides on how to set this up properly in Unity.

Hello, that old blog post states

In latest Unity versions this is still the case, no additional splitting options are supported. So APK files generated from the AAB are split only by architecture. Everything else is included in all APK variants.

So in regards to this section of the blog…

Would it be true that we can not currently use Dynamic Features?

Additionally, in the linked Android documentation it states the following

Do we have control over that or is Unity somehow taking care of that when we build our Android projects?

Yes, Dynamic Features are not supported for now.

The resources mentioned in the linked page are the ones normally used by java applications. Unity does not use them. We only specify launcher icons and static splash screen image (if it is enabled).

Okay. I am admittedly a bit confused as to what the advantages of using AAB is then.

The blog states the following

Does Google Play just generate several APKs for each possible configuration of Android? That seems… kind of crazy. It then proceeds to state.

If Configuration APKs and Dynamic Delivery are not supported for now, how would Google know to generate several APKs?

I feel like I am missing a key component of this to understand the benefits of using AAB over something like Split Binary or just an APK.

There are no practical benefits currently.

Google will stop nagging you to use .aab.

And the resulting APKs May be ever so slightly smaller (maybe?)?

The AAB is a great improvement for android apps which use standard drawable resources, and other resources which are used by a java activity. It does not really benefit applications which do rendering in the native code (such as Unity). From the Unity perspective multiple APK files split by architecture is essentially the same as AAB. Split Binary is different, because using that the code for all architectures will end up on the device, regardless of the architecture of the phone.

So currently, from what I understand these are our options. As an FYI, I am using 2019.1.12f1 as reference. Correct me if I’ve misunderstood anything.

In Player Settings.
Split Binary Application

  • Results in an OBB file and an APK file
  • This also results in the code for all architectures being present on the device regardless of whether that architecture is matched by the device itself

Split APKs by Target Architecture (Experimental)

  • Target archs would be set to ARMv7 and ARM64 as x86 is now deprecated
  • This would result in two APKs where ARMv7 architecture code is in one and vice versa for the other

In Build Settings.
Build App Bundle (Google Play)

  • Checking this will result in a bundle file to be uploaded to Google Play Console where Google generates what I would assume is two APKs for the selected architectures in your Player Settings (ARMv7 and ARM64)
  • These APKs may be smaller than the APKs generated by Unity in the option above

As a follow up question, can Split APK by Target Archiecture and Split Binary Application be used together? I haven’t actually tried this. Would this results in two APK’s with a single OBB file that is uploaded with both to the Google Play Console?

You are correct regarding the build options. Well, there is also an option to export gradle project, but that does not add additional options for splitting the build out of the box.

Yes, they can be used together and yes, you would get two APK files with a single OBB file. When uploading your APK files you would have to upload the same OBB file twice, so each APK would have OBB file assigned to it (Play Store will complain that the OBB file has already been uploaded, but you will have to do it anyway).

This has been very informative @JuliusM . Thank you for the prompt replies and I’m sure this will likely turn up in some Google search results.

Would you be able to expand on what the timeline is potentially looking like for AAB to be fully implemented in Unity?

As I’ve mentioned before, AAB is mostly useful for apps that use standard resources described here App resources overview  |  Android Developers. Unity does not use them, so it is not very useful for us. Currently there are no plans to do anything else regarding the AAB.
If you are interested in a dynamic delivery concept, check out the asset bundles Unity - Manual: AssetBundles which work on multiple platforms supported by Unity.

Thank you. I am actually already using the Asset Bundles system in several of our projects already. The initial blog post give off a vibe like there was more coming but it seems like the only advantage is deferring APK generation and signing to Google for the two architectures as opposed to using Unity to do that. And also potentially smaller build sizes.

Also I just realized, by using an AAB and Google generating the APK, does that mean it would not generate an OBB and would instead attempt to package everything into the APK?

Yes, that is correct.

Is there a way to make an exported Gradle project using 2019.3.11b dynamic module compatible?. AAB file made by unity itself doesn’t support Dynamic Module currently as this thread clearly mentions that.
https://stackoverflow.com/questions/52059339/difference-between-apk-apk-and-app-bundle-aab
the thread above briefly explains
Will the asset bundle method you mentioned work with exported Gradle projects. We are trying to integrate unity library into a native android app, we were hoping to achieve a dynamic module feature so as to have multiple unity projects library in the base native app and the users could download any of the games in the base app from the play store.
The AAB file will be generated from Android Studio instead of unity itself.

Could we do something about it?. There are conflicts that occur when more than 1 unity libraries are imported into the base app, a conflict occurs stating there are same paths in both libraries unityLibrary\src\main\assets\bin\Data\Managed\Metadata\global-metadata.dat
We have been following this: Integration Unity as a library in native Android app Version 2

Is it possible to achieve this using unity projects?
Is there a way to achieve this?
Any help or information is appreciated. Thank you.

Are you able to install the Unity Library as Dynamic feature module on user demand? I am getting the “not enough storage space to install required resources” popup, when i download and install the Unity Library.

Can some one please help me with this?

7042345--834934--Screenshot_2021-04-12-18-26-39-385_com.thinkandlearn.k3us.png

1 Like

I’d really love to have this one solved too. Maybe someone from Unity can comment?

We don’t support running Unity player as a dynamic feature module.

Are you able to install the Unity Library as Dynamic feature module on user demand? I am getting the “unable to initialize the unity engine” popup. Can some one help me with us. i really need using DFM because my size my app very large

Just for information: recently I had a challenge to implement this feature for one our company’s clients. And in about 2-3 days of researching it, designing and coding it I was successful.

But it was done not for offloading main Unity runtime, I see it as a strange goal, as Unity runtime is usually your main runtime/engine where everything else works.

For our case it was quite big native C++ library(-ies) that gets delivered as an on-demand mode, e.g. explicitly requested and installed by user or app at some point. And this applies to the code which gets invoked from C# throug usual P/Invoke mechanism (aka [DllImport] attribute).

Unfortunately I cannot share any of the code needed to achieve so, because I am bound by NDA and basically it was an internal in-company development effort. But I can give some hints to point you into direction.

What basically you need is:

  1. Some Java wrapper over SplitInstallManager instance and related stuff. We have one class that implements a series of requests to download and install some Feature module and one interface that implements a “listener”. See below. You need all this it to simplify your work with more low-level components from C# side, streamline it and have all Java related burder on Java side. It’s the most convenient to have it fully Java-source Android library project located within directory with .plugin extension, then Unity treats it as a library project and puts it automatically into exported Android project. Just mark it as Android-only in Inspector. You will also have a build.gradle and project.properties here, as well as src/main/AndroidManifest.xml and that’s all .
  2. Write C# code over the abovementioned code on C# side with a help of AndroidJavaObject, AndroidJavaClass and AndroidJavaProxy to implement listener on the C# side. You will also need a small one-liner to get a Context object on which you run your SplitInstallManager Java wraper, here is it:
    var context = new AndroidJavaClass("com.unity3d.player.UnityPlayer").GetStatic<AndroidJavaObject>("currentContext");
  3. You need a separate Java class and C# wrapper over ReLinker if you want to immediately load (activate) you native code library after downloading and installing feature, it will not be done automatically (on next run it will be). ReLinker is a library recommended by Android team as a more feature rich and solid replacement for a Java-side System.load() API.
  4. And to bind all of this you basically need a changes to the Gradle scripts to support one of the projects as the installable on-demand. This is actually the most challenging part and we are still working on a both C# Unity build scripts (actually post-export scripts) to automate all of this, as well as some coding in Groovy inside Gradle templates code. Using Unity Gradle templates for pretty anything that can be templatized helped greatly, but we are not automated everything yet.

But what we already have is the set of a few native code libraries that gets downloaded, installed and immediately loaded and used in one session on-demand, with full control and information about the process (including download progress).

Here I can share an interfaces of the key Java and C# components, but not code.
Java

@Keep
public class SplitInstallWrapper implements SplitInstallStateUpdatedListener, SplitInstallHelperActivityListener {

    public SplitInstallWrapper(@NonNull final Context context, @NonNull final SplitInstallWrapperListener listener);
    public String[] getInstalledLanguages ();
    public String[] getInstalledModules ();
    public boolean startInstall (@Nullable final String[] moduleNames, @Nullable final String[] languages);
    public boolean cancelInstall (final int sessionId);
}

@Keep
public interface SplitInstallWrapperListener {
    void onStartInstallResult(@ErrorCode int errorCode, int sessionId, @Nullable final String[] moduleNames, @Nullable final String[] languages);
    void onCancelInstallResult(@ErrorCode int errorCode, int sessionId);
    void onStateUpdate(int sessionId, @ErrorCode int errorCode, @SessionStatus int status, boolean isTerminalStatus,
        long bytesDownloaded, long totalBytesToDownload, String[] moduleNames, String[] languages);
}

@Keep
public class NativeLibraryLoader {
    @Keep
    public static class Result {
        public final boolean isSuccess;
        public final Throwable error;

        public Result () {
            isSuccess = true;
            error = null;
        }

        public Result (final @NonNull Throwable t) {
            isSuccess = false;
            error = t;
        }
    }

    public interface LoadListener {
        void success(String library);
        void failure(String library, Throwable t);
    }

    static Result loadNativeLibrary(final @NonNull Context context, final @NonNull String library,
            final @Nullable String version, final boolean recursive);
    static Result loadNativeLibraryAsync(final @NonNull Context context, final @NonNull String library,
            final @Nullable String version, final boolean recursive, final @NonNull LoadListener loadListener);

C#:

public delegate void OnStartInstallResultDelegate(ErrorCode errorCode, int sessionId, string[] moduleNames, string[] languages);
public delegate void OnCancelInstallResultDelegate(ErrorCode errorCode, int sessionId);
public delegate void OnStateUpdateDelegate(int sessionId, ErrorCode errorCode, SessionStatus status, bool isTerminalStatus,
                long bytesDownloaded, long totalBytesToDownload, string[] moduleNames, string[] languages);

public static class PlayFeatureDelivery
{
        public static event OnStartInstallResultDelegate OnStartInstallResult;
        public static event OnCancelInstallResultDelegate OnCancelInstallResult;
        public static event OnStateUpdateDelegate OnStateUpdate;
        
        public static bool StartInstall(string[] moduleNames, string[] languageNames);
        public static bool CancelInstall(int sessionId);
        public static string[] GetInstalledLanguages();
        public static string[] GetInstalledModules();
}

public static class NativeLibraryLoader
{
        public delegate void OnSuccessDelegate(string library);
        public delegate void OnFailureDelegate(string library, string errorMessage);

        public static (bool success, string error) LoadNativeLibrary
            (string library, bool recursive, string version = null);
        public static (bool success, string error) LoadNativeLibraryAsync
            (OnSuccessDelegate onSuccess, OnFailureDelegate onFailure, string library, bool recursive, string version = null);
}