Following my tutorial on how you can create Android Plugins for Unity 3D, I decided to write a tutorial on how to create IOS plugins for Unity 3D with Objective C. I will be following the same incremental approach used in creating the Android plugin. This tutorial will cover the most basic aspects of writing a custom plugin for IOS. It covers the aspects of creating the XCode static library. Generating the Unity Project and running the project using Unity. To understand the tutorial you should have a basic understanding of Objective C, C, C# and XCode.
Setup Directory Structure
Directory Structure in Unity 3D
Create a new Project in Unity with the following directories among other directories in the project.
1. Project Root -> Assets -> Plugins -> IOS
2. Project Root -> Scripts
XCode Project
We need to build a library that can be included in out Unity 3D project. For this purpose we need to create a Cocoa Touch Static Library. To keep things simple we will just create three files in our XCode project. We cannot directly access Objective C code from C#. Only C++ and C code ca be accessed. For this purpose we will create a C header file which is able to execute functions in Objective C. The NativePlugin.h file and the NativePlugin.mm file reside in the XCode project. There is also a UnityBridge.h file in this directory which you will learn about later. As for now, this file will not be needed.
// NativePlugin.h #ifndef NativePlugin_h #define NativePlugin_h #endif /* NativePlugin_h */ // NativePlugin.mm // External C extern "C" { // End External C }
Once you have placed all your code in your XCode project, you need to build this into a static library or in other words a .a file. You can use the shortcut key CMD+B to build this static library. Now this static library needs to be placed within the IOS folder in your Unity 3D project. The static library can be found in the a location like this, based on where you have created your project.
The generated static library should now be placed within the Unity 3D project.
C# Scripts
A C# script is required to interface between C# and C. We’ll use a class like this for that purpose. This file can be placed at the root of the plugin directory but it can go anywhere you like. Just make sure that it makes sense to have this file at that location.
// NaivePlugin.cs using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Runtime.InteropServices; public class NativePlugin : MonoBehaviour { }
In the Scripts folder a new C# script should be created. This will be the class invoking the functions in our native library. Since we want this script to be executed it has to be linked ot a GameObject. In this case, we are going to link it with the main camera. You can click on the main camera game object and add this script. This file should look something like this.
// Plugin.cs using System.Collections; using System.Collections.Generic; using UnityEngine; public class Plugin : MonoBehaviour { // Use this for initialization void Start () { } // Update is called once per frame void Update () { } }
Building and Running the Code
As you would probably know by now, Unity 3D is not able to execute the project directly on an IOS device or simulator like Android. It instead generates a XCode project that can be executing using XCode. This is great when developing iOS plugins as it allows us to debug iOS with ease.
You can run your generated project through XCode and check if all is good and the project is up and running.
If you have set it up properly, you should be able to see this!
1. Hello World – writing the most basic plugin to print a hello world log
// NativePlugin.h #ifndef NativePlugin_h #define NativePlugin_h extern "C" { void c_debugLog() } #endif /* NativePlugin_h */ // NativePlugin.mm #import #import "os/log.h" // External C extern "C" { /* * Debug Log */ void c_debugLog() { // IOS 10 doesn't print NSLog, we need to use os_log if ([[[UIDevice currentDevice] systemVersion] compare:@"10.0" options:NSNumericSearch] != NSOrderedAscending) { os_log(OS_LOG_DEFAULT, @"Hello, Unity 3D!"); } else { NSLog(@"Hello, Unity 3D!"); } } // End External C }
// NaivePlugin.cs using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Runtime.InteropServices; public class NativePlugin : MonoBehaviour { /** * Native Debug Log */ [DllImport("__Internal")] private static extern void c_debugLog(string message); public static void debugLog() { c_debugLog(); } }
// Plugin.cs using System.Collections; using System.Collections.Generic; using UnityEngine; public class Plugin : MonoBehaviour { // Use this for initialization void Start () { NativePlugin.debugLog (); } // Update is called once per frame void Update () { } }
Lets look at what is going on here. We have first defined the c_debugLog function using the extern keyword in the NativePlugin.h file. The implementation of the method can be found in the NativePlugin.mm file. It is a simple NSLog statement printing out “Hello, Unity 3D!”. The NaivePlugin.cs file is acting as an interface to between C# and C. The debugLog method is declared here and using the extern keyword referenced to a method available in the static library that we imported. The C files should be first imported into a static library and then placed in the plugin directory before the project can be imported. Once you have placed your C# files your project can be imported. Now run your project using XCode and you should be able to see the log.
2. Message from C# – Passing a message from C# to iOS and printing it as a log
// NativePlugin.h #ifndef NativePlugin_h #define NativePlugin_h extern "C" { void c_debugLog(const char* message) } #endif /* NativePlugin_h */
// NativePlugin.mm #import #import "os/log.h" // External C extern "C" { /* * Debug Log */ void c_debugLog(const char* message) { // IOS 10 doesn't print NSLog, we need to use os_log if ([[[UIDevice currentDevice] systemVersion] compare:@"10.0" options:NSNumericSearch] != NSOrderedAscending) { os_log(OS_LOG_DEFAULT, @"%@", [NSString stringWithFormat:@"%@",[NSString stringWithUTF8String:message]]); } else { NSLog(@"%@", [NSString stringWithFormat:@"%@",[NSString stringWithUTF8String:message]]); } } // End External C }
// NaivePlugin.cs using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Runtime.InteropServices; public class NativePlugin : MonoBehaviour { /** * Native Debug Log */ [DllImport("__Internal")] private static extern void c_debugLog(string message); public static void debugLog(string message) { c_debugLog (message); } }
// Plugin.cs using System.Collections; using System.Collections.Generic; using UnityEngine; public class Plugin : MonoBehaviour { // Use this for initialization void Start () { NativePlugin.debugLog ("Hello, Native Unity!"); } // Update is called once per frame void Update () { } }
Here we are slightly modifying our code to be able to send a message to IOS. We are passing on a message from C# to Objective C to be printed as a log. The C method can be directly called with the argument.
3. Hello, World! Message Box in iOS
// NativePlugin.h #ifndef NativePlugin_h #define NativePlugin_h extern "C" { void c_debugLog(const char* message) void c_alert(const char* title, const char* message) } #endif /* NativePlugin_h */ // NativePlugin.mm #import #import "os/log.h" #import // External C extern "C" { /* * Debug Log */ void c_debugLog(const char* message) { // IOS 10 doesn't print NSLog, we need to use os_log if ([[[UIDevice currentDevice] systemVersion] compare:@"10.0" options:NSNumericSearch] != NSOrderedAscending) { os_log(OS_LOG_DEFAULT, @"%@", [NSString stringWithFormat:@"%@",[NSStringstringWithUTF8String:message]]); } else { NSLog(@"%@", [NSString stringWithFormat:@"%@",[NSString stringWithUTF8String:message]]); } } /* * Show Alert */ void c_alert(const char* title, const char* message) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:[NSString stringWithUTF8String:title] message:[NSString stringWithUTF8String:message] delegate:nil cancelButtonTitle:@"Close" otherButtonTitles:nil]; [alert show]; } // End External C }
We are going one step ahead and showing a Alert with a title and message that we are passing from C#.
// NaivePlugin.cs using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Runtime.InteropServices; public class NativePlugin : MonoBehaviour { /** * Native Debug Log */ [DllImport("__Internal")] private static extern void c_debugLog(string message); public static void debugLog(string message) { c_debugLog (message); } /** * Native Alert Box */ [DllImport("__Internal")] private static extern void c_alert(string title, string message); public static void alert(string title, string message) { c_alert(title, message); } } // Plugin.cs using System.Collections; using System.Collections.Generic; using UnityEngine; public class Plugin : MonoBehaviour { // Use this for initialization void Start () { NativePlugin.alert ("What's up!", "Hello, Native Unity!"); } // Update is called once per frame void Update () { } }
4. Message from iOS – Passing a message from IOS to C#
// NativePlugin.h #ifndef NativePlugin_h #define NativePlugin_h extern "C" { void c_debugLog(const char* message) void c_alert(const char* title, const char* message) void c_sendMessage(const char* object, const char* method, const char* message) } #endif /* NativePlugin_h */ // NativePlugin.mm #import #import "os/log.h" #import #import "UnityBridge.h" // External C extern "C" { /* * Debug Log */ void c_debugLog(const char* message) { // IOS 10 doesn't print NSLog, we need to use os_log if ([[[UIDevice currentDevice] systemVersion] compare:@"10.0" options:NSNumericSearch] !=NSOrderedAscending) { os_log(OS_LOG_DEFAULT, message); } else { NSLog(@"%@", [NSString stringWithFormat:@"%@",[NSString stringWithUTF8String:message]]); } } /* * Show Alert */ void c_alert(const char* title, const char* message) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:[NSString stringWithUTF8String:title] message:[NSString stringWithUTF8String:message] delegate:nil cancelButtonTitle:@"Close" otherButtonTitles:nil]; [alert show]; } /* * Send Message to Unity */ void c_sendMessage(const char* object, const char* method, const char* message) { UnitySendMessage( object, method, message); } // End External C }
We have to create a special file, let’s call it a UnityBridge.h. This file defines the function that objective C will call to execute a function in C#. As seen in the code below we define the function UnitySendMessage with 3 arguments. An object name, a method name and a parameter. The implementation of this function is within Unity itself. We are just defining it here so that our objective C code can understand what it needs to do. You can find it in the UnityInterface.h file.
// UnityBridge.h #ifndef UnityBridge_h #define UnityBridge_h #ifdef __cplusplus extern "C" { #endif void UnitySendMessage(const char* obj, const char* method, const char* msg); #ifdef __cplusplus } #endif #endif /* UnityBridge_h */ // NaivePlugin.cs using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Runtime.InteropServices; public class NativePlugin : MonoBehaviour { /** * Native Debug Log */ [DllImport("__Internal")] private static extern void c_debugLog(string message); public static void debugLog(string message) { c_debugLog (message); } /** * Native Alert Box */ [DllImport("__Internal")] private static extern void c_alert(string title, string message); public static void alert(string title, string message) { c_alert(title, message); } /** * Native Send Message */ [DllImport("__Internal")] private static extern void c_sendMessage(string objectName, string methodName, string parameter); public static void sendMessage(string objectName, string methodName, string parameter) { c_sendMessage(objectName, methodName, parameter); } }
// Plugin.cs using System.Collections; using System.Collections.Generic; using UnityEngine; public class Plugin : MonoBehaviour { // Use this for initialization void Start () { NativePlugin.sendMessage (this.name, "ShowMessage", "Hello"); } // Update is called once per frame void Update () { } public void ShowMessage(string message) { NativePlugin.alert("Native Message", message); } }
Let’s take a look at what exactly is going on here. We are calling sendMessage in our Plugin.cs C# script which will invoke the C method c_sendMessage. This C method will then call the C# method ShowMessage because that is the method name we are providing the c_sendMessage function. When the ShowMessage function is called it will show an alert again using the ObjectiveC alert method.
By now, you should have understood how custom plugins are written. If you have any queries please comment below and I will try to answer your questions as much as possible.
Leave a Reply