matthewrdev / UnityUaal.Maui

Embedding the Unity game engine into .NET MAUI.
MIT License
112 stars 12 forks source link

UnityUaal.Maui

This code sample demonstrates how to embed Unity into .NET MAUI.

See: https://github.com/Unity-Technologies/uaal-example

Requires:

Additionally, assumes familarity, or at least exposure to:

If you need assistance in doing the Unity => MAUI integration, please reach out via:

I am always happy to help diagnose issues and provide guidance.

I can also offer engineering assistance if required.

Why Embed Unity Into MAUI?

Lets compare the strengths and weakness of app development using Unity vs .NET MAUI:

Unity Pro Con
Fully fledged 3D engine Multiple In-Engine UI frameworks (no native UI support)
Simplified augmented and virtual reality Indirect access to native platform features (requires plugin)
Rich eco-system of 3D tools, plugins and assets Dated versions of .NET
Difficult integration of SQLite
Limited Authentication Vendors
Highly specialised skillset to hire for.
MAUI Pro Con
Native UIs in a single language and project No 3D engine support
Easily access native features in C# Access to 3D features requires complex per-platform implementation
Use latest .NET version
Rich ecosystem of packages (nuget.org)
"Simple" binding and consumption of native libraries

While Unity is an incredible platform to create 3D experiences, it is not a good choice for a modern, native apps. Unity to simplifies building augmented or virtual reality experiences, and gives us a full engine for rendering 3D environments however it's UI frameworks are limited as are its access to 3rd party vendors.

On the other side, MAUI can create fully native iOS and Android apps. We gain access to the full .NET ecosystem, utilising the latest language features and a large libary of NuGet packages. However creating a 3d experience in MAUI requires writing a rendering pipeline from near scratch... and this is not an easy task!

By combining these two technologies, we can get the best of both worlds:

Integrating Unity Into MAUI

Unity Project Overview

The Unity Project contains the following:

Exporting Platform Projects

When building a Unity app for Android and iOS, it exports itself as an Android gradle project and Xcode project.

Each of these projects contains two parts:

Please follow the instructions in the Unity As A Library for iOS and Android to create the native projects.

Building Native Frameworks

Once you have exported the native projects for iOS and Android, you will need to build the framework (iOS) and aar (Android) that will be consumed in .NET.

Android To create the Android aar:

The created aar will be found under [Exported Unity App Android Folder]/unityLibrary/build/outputs/aar/unityLibrary-release.aar.

Please note that you may need to install several additional components for Android Studio such as the NDK and cmake.

iOS To create the iOS framework:

Make the following modifications to the main.mm and UnityFramework.h file:

Additions to Classes\main.mm

+ (UnityFramework*)loadUnity
{
    UnityFramework* ufw = [UnityFramework getInstance];
    if (![ufw appController])
    {
        // unity is not initialized
        //[ufw setExecuteHeader: &_mh_execute_header];
    }

    [ufw setDataBundleId: "com.unity3d.framework"];

    return ufw;
}

- (char**)getArgumentArray
{
    NSArray* args = [[NSProcessInfo processInfo]arguments];

    unsigned count = [args count];
    char** array = (char **)malloc((count+ 1) * sizeof(char*));

    for (unsigned i = 0; i< count; i++)
    {
        array[i] = strdup([[args objectAtIndex:i] UTF8String]);
    }
    array[count] = NULL;
    return array;
}

- (unsigned)getArgumentCount
{
    NSArray* args = [[NSProcessInfo processInfo]arguments];

    unsigned count = [args count];
    return count;
}

- (void)freeArray:(char **)array
{
    if (array != NULL)
    {
        for (unsigned index = 0; array[index] != NULL; index++)
        {
            free(array[index]);
        }
        free(array);
    }
}

- (void)runEmbedded
{
    char** argv = [self getArgumentArray];
    unsigned argc = [self getArgumentCount];
    NSDictionary* appLaunchOpts = [[NSDictionary alloc] init];

    if (self->runCount)
    {
        // initialize from partial unload ( sceneLessMode & onPause )
        UnityLoadApplicationFromSceneLessState();
        [self pause: false];
        [self showUnityWindow];
    }
    else
    {
        // full initialization from ground up
        [self frameworkWarmup: argc argv: argv];

        id app = [UIApplication sharedApplication];

        id appCtrl = [[NSClassFromString([NSString stringWithUTF8String: AppControllerClassName]) alloc] init];
        [appCtrl application: app didFinishLaunchingWithOptions: appLaunchOpts];

        [appCtrl applicationWillEnterForeground: app];
        [appCtrl applicationDidBecomeActive: app];
    }

    self->runCount += 1;
}

//this method already exists, just add the difference
- (void)unloadApplication
{
    freeArray:([self getArgumentArray]); //added line of code
    UnityUnloadApplication();
}

Replace the unloadApplication implementation generated by Unity with the one above.

Additions to UnityFramework\UnityFramework.h

+ (UnityFramework*)loadUnity;

- (void)runEmbedded;

These changes make it much simpler for Unity to run in embedded mode in our MAUI app.

Credit

Finally, select Product => Build to compile and generate the release framework.

The created framework will be found under [Exported Unity App iOS Folder]/Build/Products/Release-iphoneos/UnityFramework.framework.

Check that the outputted framework contains the following content:

.NET Native Bindings

Once you have built the framework and aar libraries, you will need to create an Android and iOS binding project alongside the

For Android:

For iOS:

ApiDefinitions.cs

using System;
using CoreAnimation;
using Foundation;
using ObjCRuntime;
using UIKit;

namespace iOSBridge
{
    interface IUnityContentReceiver { }

    [BaseType(typeof(NSObject))]
    [Model]
    [Protocol]
    interface UnityContentReceiver 
    {
        [Export("receiveUnityContent:eventContent:")]
        void ReceiveUnityContent ([PlainString] string eventName, [PlainString] string eventContent);
      }

    [BaseType(typeof(NSObject))]
    interface Bridge
    {
        [Static, Export("registerUnityContentReceiver:")]
        void RegisterUnityContentReceiver(IUnityContentReceiver contentReceiver);
    }

    [BaseType(typeof(NSObject))]
    interface UnityAppController : IUIApplicationDelegate
    {
        [Export("quitHandler", ArgumentSemantic.Copy)]
        Action QuitHandler { get; set; }

        [Export("rootView", ArgumentSemantic.Copy)]
        UIView RootView { get; }

        [Export("rootViewController", ArgumentSemantic.Copy)]
        UIViewController RootViewController { get; }
    }

    interface IUnityFrameworkListener { }

    [BaseType(typeof(NSObject))]
    [Model]
    [Protocol]
    interface UnityFrameworkListener
    {
        [Export("unityDidUnload:")]
        void UnityDidUnload(NSNotification notification);

        [Export("unityDidQuit:")]
        void UnityDidQuit(NSNotification notification);
    }

    [BaseType(typeof(NSObject))]
    interface UnityFramework
    {
        [Export("appController")]
        UnityAppController AppController();

        [Static, Export("getInstance")]
        UnityFramework GetInstance();

        [Export("setDataBundleId:")]
        void SetDataBundleId([PlainString] string bundleId);

        [Static, Export("loadUnity")]
        UnityFramework LoadUnity();

        [Internal, Export("runUIApplicationMainWithArgc:argv:")]
        void RunUIApplicationMainWithArgc(int argc, IntPtr argv);

        [Export("runEmbedded")]
        void RunEmbedded();

        [Internal, Export("runEmbeddedWithArgc:argv:appLaunchOpts:")]
        void RunEmbeddedWithArgc(int argc, IntPtr argv, NSDictionary options);

        [Export("unloadApplication")]
        void UnloadApplication();

        [Export("quitApplication:")]
        void QuitApplication(int exitCode);

        [Export("registerFrameworkListener:")]
        void RegisterFrameworkListener(IUnityFrameworkListener obj);

        [Export("unregisterFrameworkListener:")]
        void UnregisterFrameworkListener(IUnityFrameworkListener obj);

        [Export("showUnityWindow")]
        void ShowUnityWindow();

        [Export("pause:")]
        void Pause(bool pause);

        [Export("setExecuteHeader:")]
        void SetExecuteHeader(ref MachHeader header);

        [Export("sendMessageToGOWithName:functionName:message:")]
        void SendMessageToGOWithName([PlainString] string goName, [PlainString] string functionName, [PlainString] string msg);
    }
}
using System.Runtime.InteropServices;
using Foundation;

namespace iOSBridge
{
    [StructLayout(LayoutKind.Sequential)]
    public struct MachHeader
    {
        public uint magic;     /* mach magic number identifier */
        public int cputype; /* cpu specifier ; cpu_type_t*/
        public int cpusubtype;   /* machine specifier ; cpu_subtype_t */
        public uint filetype;  /* type of file */
        public uint ncmds;     /* number of load commands */
        public uint sizeofcmds;    /* the size of all the load commands */
        public uint flags;     /* flags */
        public uint reserved;  /* reserved */
    }
}

Starting Unity In MAUI

To start the Unity app in MAUI:

Android

Create a new Activity under Platforms/Android/ named UnityActivity and replace it with the content defined in /UnityActivity.cs.

Please review the code carefully as this file:

To start Unity, start the activity with a new Intent:

public static void ShowUnityWindow()
{
    var intent = new Android.Content.Intent(Microsoft.Maui.ApplicationModel.Platform.CurrentActivity, typeof(UnityActivity));
    intent.AddFlags(Android.Content.ActivityFlags.ReorderToFront);

    Microsoft.Maui.ApplicationModel.Platform.CurrentActivity.StartActivity(intent);
}

iOS

To start Unity, first initialise the Unity framework:

private static UnityFramework framework = null;
public static bool IsUnityInitialised => framework != null && framework.AppController() != null;

private static void InitialiseUnity()
{
    if (IsUnityInitialised)
    {
    return;
    }

    framework = UnityFramework.LoadUnity();

    framework.RegisterFrameworkListener(new UnityBridge_UnityFrameworkListener());
    Bridge.RegisterUnityContentReceiver(new UnityBridge_UnityContentReceiver());

    framework.RunEmbedded();
}

Then open the Unity ViewController by calling framework.ShowUnityWindow():

public static void ShowUnityWindow()
{
    if (!IsUnityInitialised)
    {
    InitialiseUnity();
    }

    if (framework != null)
    {
    framework.ShowUnityWindow();
    }
}

Communicating Between Unity and MAUI

To send and receive content from Unity, please review the platform specific implementations of the UnityBridge:

Known Issues + Limitations