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:
- 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 .
- 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");
- 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.
- 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);
}