Unity Native Plugins: OS X

When developing games in Unity3D it is often (if not always) necessary to access to platform specific features, hardware or anything not available to Unity via the API.

Plugins in Unity are a way to establish a bridge between your code in Unity (C#, Javascript, Boo) and the native platform. It allows us access to any non-supported features by creating a native binary bundle which we can access through an interface in our Unity script. Plugins in Unity work in both directions: we can call native code from Unity as well as call Unity code from the native plugin.

To start of this series of tutorials, I will cover desktop and mobile with a native plugin for Mac OS.

 

The problem.

When I was porting our Pig Rush game to the Mac App Store we thought:

- It would be great if you could play it using the touch capabilities of the Trackpad

The trackpad behaves much like a muli-touch screen. Not only does it detect taps, it can also track touch positions and gestures (like swipes).

- This will work great in our game that was originally conceived for playing on a touch screen device.

We then realized that these properties were not accessibles through the unity API.

- What to do?
- Native Plugins!

The solution.

1. Define in your Unity project the interface which we will use in our app for calling native methods as well as receive the information from a specific touch. Each method in this interface is defined as a static method. In our example it should look like this:

MyNativePlugin.cs

using UnityEngine;
using System.Collections;
using System;
using System.Runtime.InteropServices;

public class MyNativePlugin {

	//Calls to the native plugin

	[DllImport ("XCodePlugin")]
	public static extern int PluginInit(bool isDevice);

	[DllImport ("XCodePlugin")]
	public static extern int DebugLog(string message);

	//Callbacks from native plugin

	public static void TrackPadTouchBegan(float normalX, float normalY)
	{
		//Note here that the values here are normalized
		//Do something useful with them
	}

	public static void TrackPadTouchEnded(float normalX, float normalY)
	{
		//Note here that the values here are normalized
		//Do something useful with them
	}

	public static void TrackPadTouchCanceled(float normalX, float normalY)
	{
		//Note here that the values here are normalized
		//Do something useful with them
	}

}

2. Create a new project in XCode by selecting the Bundle template.

Choose "Bundle" and click next

3. Import the mono library into the project. It should be located in the Unity application folder at:
/Applications/Unity/Unity.app/Contents/Frameworks/MonoEmbedRuntime/osx/libmono.0.dylib

4. Add a new Objective-C class into your project, and name it Plugin.

At this point your project should look something like this.

5. Define all the mono objects and methods so they become accessible from the Objective-C code by adding the following code into the prefix class.

XCodePlugin_Prefix.pch
#ifdef __cplusplus
extern "C" {
#endif
	void UnitySendMessage(const char* obj, const char* method, const char* msg);
#ifdef __cplusplus
}
#endif

#ifdef __cplusplus
extern "C" {
#endif
	typedef void* MonoDomain;
	typedef void* MonoAssembly;
	typedef void* MonoImage;
	typedef void* MonoClass;
	typedef void* MonoObject;
	typedef void* MonoMethodDesc;
	typedef void* MonoMethod;
	typedef void* MonoString;
	typedef int gboolean;
	typedef void* gpointer;

	MonoDomain *mono_domain_get();
	MonoAssembly *mono_domain_assembly_open(MonoDomain *domain, const char *assemblyName);
	MonoImage *mono_assembly_get_image(MonoAssembly *assembly);
	MonoMethodDesc *mono_method_desc_new(const char *methodString, gboolean useNamespace);
	MonoMethodDesc *mono_method_desc_free(MonoMethodDesc *desc);
	MonoMethod *mono_method_desc_search_in_image(MonoMethodDesc *methodDesc, MonoImage *image);
	MonoObject *mono_runtime_invoke(MonoMethod *method, void *obj, void **params, MonoObject **exc);
	MonoClass *mono_class_from_name(MonoImage *image, const char *namespaceString, const char *classnameString);
	MonoMethod *mono_class_get_methods(MonoClass*, gpointer* iter);
	MonoString *mono_string_new(MonoDomain *domain, const char *text);
	char* mono_method_get_name (MonoMethod *method);
#ifdef __cplusplus
}
#endif

6. We now need to capture the trackpad touch events and call our methods back in Unity to pass the touch info.

Edit the Plugin.m class we created before and add the following code.
Note that you will need change: PATH_TO_YOUR_PROJECT to the appropriate path.

#import "Plugin.h"
#import "XCodePlugin_Prefix.pch"

MonoDomain *domain;
NSString *assemblyPath;
MonoAssembly *monoAssembly;
MonoImage *monoImage;

MonoMethodDesc *trackPadTouchBeganDesc;
MonoMethod *trackPadTouchBeganMethod;

MonoMethodDesc *trackPadTouchEndedDesc;
MonoMethod *trackPadTouchEndedMethod;

MonoMethodDesc *trackPadTouchCanceledDesc;
MonoMethod *trackPadTouchCanceledMethod;

NSView* view = nil;

@interface TrackingObject : NSResponder
{
}
- (void)touchesBeganWithEvent:(NSEvent *)event;
- (void)touchesEndedWithEvent:(NSEvent *)event;
- (void)touchesCancelledWithEvent:(NSEvent *)event;

@end

@implementation TrackingObject

- (void)touchesBeganWithEvent:(NSEvent *)event
{
	float posX = 0.0f;
	float posY = 0.0f;

	NSSet *touches = [event touchesMatchingPhase:NSTouchPhaseBegan inView:view];
	for (NSTouch *touch in touches)
	{
		posX = touch.normalizedPosition.x;
		posY = touch.normalizedPosition.y;
		break; //We just track one touch (no multi-touch in this example)
	}

	//Call method back
	void *args[] = { &posX, &posY };
	mono_runtime_invoke(trackPadTouchBeganMethod, NULL, args, NULL);
}

- (void)touchesEndedWithEvent:(NSEvent *)event
{
	float posX = 0.0f;
	float posY = 0.0f;

	NSSet *touches = [event touchesMatchingPhase:NSTouchPhaseBegan inView:view];
	for (NSTouch *touch in touches)
	{
		posX = touch.normalizedPosition.x;
		posY = touch.normalizedPosition.y;
		break;
	}

	//Call method back
	void *args[] = { &posX, &posY };
	mono_runtime_invoke(trackPadTouchEndedMethod, NULL, args, NULL);
}

- (void)touchesCancelledWithEvent:(NSEvent *)event
{
	float posX = 0.0f;
	float posY = 0.0f;

	NSSet *touches = [event touchesMatchingPhase:NSTouchPhaseBegan inView:view];
	for (NSTouch *touch in touches)
	{
		posX = touch.normalizedPosition.x;
		posY = touch.normalizedPosition.y;
		break;
	}

	//Call method back
	void *args[] = { &posX, &posY };
	mono_runtime_invoke(trackPadTouchCanceledMethod, NULL, args, NULL);
}

@end

TrackingObject* pTrackMgr = nil;

void InitPlugin(bool isDevice)
{
	NSLog(@"Native plugin -> PluginInit - device? : %d", isDevice);

	SetupTrackingObject();

	//We do this so it also works while we work on the Editor
	if(isDevice)
	{
		assemblyPath = [[[NSBundle mainBundle] bundlePath]
						stringByAppendingPathComponent:@"Contents/Data/Managed/Assembly-CSharp.dll"];
	}
	else
	{
		assemblyPath = @"[PATH_TO_YOUR_PROJECT]/Library/ScriptAssemblies/Assembly-CSharp.dll";
	}

	NSLog(@"Native plugin -> assembly path: %@", assemblyPath);

	domain = mono_domain_get();
	monoAssembly = mono_domain_assembly_open(domain, assemblyPath.UTF8String);
	monoImage = mono_assembly_get_image(monoAssembly);

	//Note here that "MyNativePlugin" is the name of the unity class and "TrackPadTouchBegan" is the name of the static method we defined before.
	trackPadTouchBeganDesc = mono_method_desc_new("MyNativePlugin:TrackPadTouchBegan", FALSE);
	trackPadTouchBeganMethod = mono_method_desc_search_in_image(trackPadTouchBeganDesc, monoImage);
	mono_method_desc_free(trackPadTouchBeganDesc);

	trackPadTouchEndedDesc = mono_method_desc_new("MyNativePlugin:TrackPadTouchEnded", FALSE);
	trackPadTouchEndedMethod = mono_method_desc_search_in_image(trackPadTouchEndedDesc, monoImage);
	mono_method_desc_free(trackPadTouchEndedDesc);

	trackPadTouchCanceledDesc = mono_method_desc_new("MyNativePlugin:TrackPadTouchCanceled", FALSE);
	trackPadTouchCanceledMethod = mono_method_desc_search_in_image(trackPadTouchCanceledDesc, monoImage);
	mono_method_desc_free(trackPadTouchCanceledDesc);

}

void SetupTrackingObject()
{
	NSLog(@"Native plugin -> SetupTrackingObject");

	NSApplication* app = [NSApplication sharedApplication];
	NSWindow* window = [app mainWindow];
	view = [window contentView];

	if(pTrackMgr != nil)
	{
		[pTrackMgr release];
		pTrackMgr = nil;
	}

	pTrackMgr = [TrackingObject alloc];
	[view setAcceptsTouchEvents:YES];
	[view setNextResponder:pTrackMgr];

}

7. By now you should be able to compile the project and build your bundle (XCodePlugin.bundle). Once you have built your bundle you have to copy it to the Assets/Plugin folder in your unity project. The name of the bundle is referenced in the Unity interface we wrote before so it needs to match with the one you have in your project:

MyNativePlugin.cs
...
	[DllImport ("XCodePlugin")]
	public static extern int PluginInit(...
...

8.  No we are ready to initialize our plugin and get the touches from the trackpad in Unity.

void Awake()
{
	bool isDevice = (Application.platform != RuntimePlatform.OSXEditor);
	MyNativePlugin.InitPlugin(isDevice);
}

9 Comments

  1. Kay | October 31st, 2011

    Very interesting post :-) I did something similar using a library for enhanced motion detection with gyro and accelerometer on iPhone. I found out that the callback mechanism is pretty expensive. Thus I used polling the interface which is faster although bad design. I wrote a blog about setting up the project, maybe interesting:
    http://www.scio.de/de/blog-a-neues/scio-development-blog/entry/iphone-a-unity3d-integrating-3rd-party-static-libraries-in-unity3d-generated-xcode-projects

  2. Greg | March 15th, 2012

    I’ve been using this tutorial for something similar. It’s been a huge help!

    I can’t help but wonder though. Have you considered putting something like this on the Unity Asset Store? Would that even be possible for a project like this?

  3. Pingback: Facebook Related Materials… « hansootransformula

  4. Thomas Phifer | October 11th, 2012

    I got it to work! Thank you for posting this! I’ll be able to take this further on multitouch tables at my company!

  5. phil | October 22nd, 2012

    Your tutorial is awesome!
    Out of curiosity, have you managed getting trackpad touches in fullscreen mode?

  6. Kokos | November 16th, 2012

    Im trying to make unity – xocde to work and im a bit stucked..

    Where can I found the template Bundle?
    It must have a new name now..

  7. prachi | March 14th, 2013

    Hi I wat my xcode file to read a text file stored in asset folder of unity. and using unitysendmessage () i want to send some string from that text file to unity project. can anybody help me?

  8. joe | June 25th, 2013

    is it possible to create a plugin with a GUI in xcode to be called in unity?. Im am very new to xcode so i dont know how it works.

  9. Pingback: Interesting Unity links | Robert Castle Consulting

Post a Comment