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.
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:
The Unity Project contains the following:
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.
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:
Release
by selecting Build => Select Build Variant and changing the :unityLibrary configuration to ReleaseThe 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:
Release
by selecting Product => Scheme => Edit Scheme and then selecting the Unity Framework item from the title of the window, then changing build configuration to Release.Data
folder is included into the framework (click on item and set to public)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.
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:
Data
folder in the root of the framework.Headers/Bridge.h
.Once you have built the framework
and aar
libraries, you will need to create an Android and iOS binding project alongside the
For Android:
unityLibrary-release.aar
into the Jars folder.For iOS:
UnityFramework.framework
release artifact from the previous step.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 */
}
}
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();
}
}
To send and receive content from Unity, please review the platform specific implementations of the UnityBridge: