Valerio “Lotti” Riva – Interactive Project
[email protected] @ValerioRiva
http://it.linkedin.com/in/valerioriva/
Avoid loss of hair while coding Unity3D plugins for mobile
ROME 24 June 2014 – Valerio Riva
Codemotion Tech Meetup #4 – Roma Summer Edition
Nice to meet you!
• Web / Game developer• Recent works @ Interactive Project
• World Cup Juggler• OverVolt: crazy slot cars
ROME 24 June 2014 – Valerio Riva
Why develop a Unity3D mobile plugin?
• Access to device native features• Implement third-party SDK (analytics, advertising,
in-app purchases, game services, etc.)• Save/Earn money• Personal growth
ROME 24 June 2014 – Valerio Riva
Extending Unity 4.x
• Unity supports C/C++ libraries. “extern”-alized functions/methods can be called from C#
• All plugins must be placed inside “Assets/Plugins” folder
• Platform-dependent plugins must be placed inside specific folders (x86, x86_64, Android, iOS, WP8, Metro, etc.)
• Available only on Unity3D Pro/Mobile
ROME 24 June 2014 – Valerio Riva
Extending Unity (iOS)
• Call externalized Object-C methods• Must wraps third-party SDK if their methods are not
externalized• Gameobjects can receive messages from native code• Receiver methods declared on GO’s components must
have only 1 string parameter as signature
ROME 24 June 2014 – Valerio Riva
Extending Unity (Android)
• Use JNI (Java Native Interface), Unity provides Helper classes
• Call native methods directly from Unity• Gameobjects can receive messages from native code• Receiver methods declared on GO’s components must have
only 1 string parameter as signature• On specific cases, Unity Player activity must be extended• Android Manifest editing is often required
ROME 24 June 2014 – Valerio Riva
Extending Unity (WP8)
• Access native code directly from Unity• Use of callbacks to return data from native code• Unity’s Mono (v2.6) doesn’t support .NET >= 4.0
• Artefacts are needed to use .NET >= 4.0 libaries
• “Always” needs a fake and a real plugin – Unity will overwrite fake one with the real one automatically
• In specific cases, write a plugin is a monkey job
ROME 24 June 2014 – Valerio Riva
Extending Unity (remarks)
• Scripting define symbols are your friends• Native calls are CPU intensive• Provide fake results for in-editor usage• Every native UI call must run inside native UI thread• Every callback must run inside Unity thread (WP8)• Save time by testing plugin on a native app• Remember to include Unity library if needed
• classes.jar• UnityEngine.dll
ROME 24 June 2014 – Valerio Riva
Case study: Flurry plugin
• Wrap Flurry SDK to made it accessible from Unity• Flurry SDK is simple to use, just call static methods
(Advertising, In-App Purchase, …, are more complex plugin)• We have to code wrappers for each platform• Place platforms SKDs on the right directories
• FlurryAnalytics-4.0.0.jar -> Plugins/Android/• libFlurry_5.0.0.a -> Plugins/iOS/• FlurryWP8SDK.dll -> Plugins/WP8/• FlurryWP8SDK.dll -> Plugins/ (the WP8 fake one)
ROME 24 June 2014 – Valerio Riva
Flurry plugin (iOS)
• Flurry SDK is not “extern”-alized• Dictionary<string, string> must be translated somewhat to
NSMutableDictionary• Each KeyValuePair<string,string> are concatenated to form a single
string
ROME 24 June 2014 – Valerio Riva
//FlurryiOS.h - created by PRADA Hsiung on 13/3/8.
extern "C" {
void FlurryiOS_startSession(unsigned char* apiKey);
void FlurryiOS_setEventLoggingEnabled(BOOL bEnabled);
void FlurryiOS_logEventWithParameters(unsigned char* eventId,unsigned char *parameters);
}
Flurry plugin (iOS)// FlurryiOS.m - created by Faizan Naqvi on 1/10/13.#import <stdio.h>#include "Flurry.h" //Flurry SDK headersvoid FlurryiOS_startSession(const char* apiKey) { NSString *str = [NSString stringWithUTF8String:apiKey]; [Flurry startSession:str];}void FlurryiOS_setEventLoggingEnabled(BOOL bEnabled){ [Flurry setEventLoggingEnabled:bEnabled];}
ROME 24 June 2014 – Valerio Riva
Flurry plugin (iOS)void FlurryiOS_logEventWithParameters(const char* eventId,const char *parameters) {
NSString *params = [NSString stringWithUTF8String:parameters];
NSArray *arr = [params componentsSeparatedByString: @"\n"];
NSMutableDictionary *pdict = [[[NSMutableDictionary alloc] init] autorelease];
for(int i=0;i < [arr count]; i++) {
NSString *str1 = [arr objectAtIndex:i];
NSRange range = [str1 rangeOfString:@"="];
if (range.location!=NSNotFound) {
NSString *key = [str1 substringToIndex:range.location];
NSString *val = [str1 substringFromIndex:range.location+1];
//NSLog(@"kv %@=%@\n",key,val);
[pdict setObject:val forKey:key];
} }
if([pdict count]>0) {
[Flurry logEvent:[NSString stringWithUTF8String:eventId] withParameters:pdict timed:false];
} else FlurryiOS_logEvent(eventId);
}
ROME 24 June 2014 – Valerio Riva
Flurry plugin (iOS)Meanwhile on Unity side… #region FlurryiOS_Imports
[DllImport("__Internal", CharSet = CharSet.Ansi)]
private static extern void FlurryiOS_startSession([In, MarshalAs(UnmanagedType.LPStr)]string apiKey);
[DllImport("__Internal")]
private static extern void FlurryiOS_setEventLoggingEnabled(bool bEnabled);
[DllImport("__Internal", CharSet = CharSet.Ansi)]
private static extern void FlurryiOS_logEventWithParameters([In, MarshalAs(UnmanagedType.LPStr)]string evendId, [In, MarshalAs(UnmanagedType.LPStr)]string parameters);
#endregion
ROME 24 June 2014 – Valerio Riva
Flurry plugin (WP8)
• Flurry SDK is compiled with .NET 4.5, import it and Unity will go mad!
• We need the “fake & real” library approach, but…• Using Flurry SDK doesn’t involve use of complex logic or complex
user defined classes…• … we can use the downloaded FlurryWP8SDK.dll as “real”• and code only the fake dll!
Yes, this is the particular case where you can bring in monkeys!
ROME 24 June 2014 – Valerio Riva
Flurry plugin (WP8)using FlurryWP8SDK.Models;using System;using System.Collections.Generic;namespace FlurryWP8SDK.Models { public enum Gender { Unknown = -1, Female = 0, Male = 1 } public class Parameter { public Parameter(string name, string value) { Name = name; Value = value; } public string Name { get; set; } public string Value { get; set; } }}namespace FlurryWP8SDK { public sealed class Api { private static Api instance = null; public static Api Current { get { return instance; } } public void DummyInitiator() {} public static void EndSession() {} public static void EndTimedEvent(string eventName) {} public static void EndTimedEvent(string eventName, List<Parameter> parameters) {} public static void LogEvent(string eventName) {} public static void LogEvent(string eventName, List<Parameter> parameters, bool timed) {} public static void SetAge(int age) {} public static void SetGender(Gender gender) {} public static void SetLocation(double latitude, double longitude, float accuracy) {} public static void SetSessionContinueSeconds(int seconds) {} public static void StartSession(string apiKey) {} }}
ROME 24 June 2014 – Valerio Riva
That was easy, now give me peanuts!
Flurry plugin (Android)
• As said before, Flurry doesn’t involve complex logic…• … so we save time and wrote plugin directly in Unity using JNI Helper
classes
ROME 24 June 2014 – Valerio Riva
public Flurry StartSession(string apiKey) {
#if !UNITY_EDITOR && UNITY_WP8
Api.StartSession(apiKey);
#elif !UNITY_EDITOR && UNITY_ANDROID
using(AndroidJavaClass cls_UnityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
using(AndroidJavaObject obj_Activity = cls_UnityPlayer.GetStatic<AndroidJavaObject>("currentActivity"))
using(AndroidJavaClass cls_FlurryAgent = new AndroidJavaClass("com.flurry.android.FlurryAgent")) {
cls_FlurryAgent.CallStatic("onStartSession", obj_Activity, apiKey);
}
#elif !UNITY_EDITOR && UNITY_IPHONE
FlurryiOS_startSession(apiKey);
#endif
return this;
}
Flurry plugin (Android)
ROME 24 June 2014 – Valerio Riva
public Flurry LogEvent(string eventId, Dictionary<string,string> parameters, bool timed) { #if !UNITY_EDITOR && UNITY_WP8 List<Parameter> p = new List<Parameter>(); foreach(KeyValuePair<string,string> i in parameters) { p.Add(new Parameter(i.Key,i.Value)); } Api.LogEvent(eventId, p, timed); #elif !UNITY_EDITOR && UNITY_ANDROID using(AndroidJavaObject obj_HashMap = new AndroidJavaObject("java.util.HashMap")) { // Call 'put' via the JNI instead of using helper classes to avoid: "JNI: Init'd AndroidJavaObject with null ptr!" IntPtr method_Put = AndroidJNIHelper.GetMethodID(obj_HashMap.GetRawClass(), "put“, "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); object[] args = new object[2]; foreach(KeyValuePair<string, string> kvp in parameters) { using(AndroidJavaObject k = new AndroidJavaObject("java.lang.String", kvp.Key)) using(AndroidJavaObject v = new AndroidJavaObject("java.lang.String", kvp.Value)) { args[0] = k; args[1] = v; AndroidJNI.CallObjectMethod(obj_HashMap.GetRawObject(), method_Put, AndroidJNIHelper.CreateJNIArgArray(args)); } } cls_FlurryAgent.CallStatic("logEvent", eventId, obj_HashMap, timed); } #elif !UNITY_EDITOR && UNITY_IPHONE FlurryiOS_logEventWithParametersTimed(eventId, dictionaryToText(parameters)); #endif return this;}
and for more complex plugins…?
• Do most of logic on native side to minimize native calls from Unity
• Messages examples (Android)UnityPlayer.UnitySendMessage("gameObjectName", "methodName", "message");
• Use “Action<…>” delegates to pass callbacks on WP8, Unity supports Action with max 4 parameters as .NET 3.5 does
• Structure plugin as a wrapper (mainly for third-party SDKs)• Hide user defined classes (e.g.: instantiate them with methods) so Unity can’t see
them• Wrap API calls with ones that use just primitive or built-in data type and the convert
them to user defined classes inside plugin!
ROME 24 June 2014 – Valerio Riva
Run on UI Thread
• iOSdispatch_async(dispatch_get_main_queue(), ^{ // Your code to run on the main queue/thread});
[[NSOperationQueue mainQueue] addOperationWithBlock:^{ // Your code to run on the main queue/thread }];
• AndroidUnityPlayer.currentActivity.runOnUiThread(new Runnable() { public void run() { //your code to run on the UI thread }});
ROME 24 June 2014 – Valerio Riva
Run on UI Thread (WP8)• MainPage.xaml.cs
public partial class MainPage : PhoneApplicationPage{ public void InvokeOnAppThread ( Action callback ) { UnityApp.BeginInvoke ( () => { callback (); } ); } public void InvokeOnUIThread ( Action callback ) { Dispatcher.BeginInvoke ( () => { callback (); } ); } private void Unity_Loaded() { … MyDispatcher.InvokeOnAppThread = InvokeOnAppThread; MyDispatcher.InvokeOnUIThread = InvokeOnUIThread; … }}
• MyPlugin.csMyDispatcher.InvokeOnAppThread(() => { //your Unity callbacks execution must be placed here});
MyDispatcher.InvokeOnUIThread(() => { //your code to run on UI Thread});
ROME 24 June 2014 – Valerio Riva
Resources and examplesResources
• http://docs.unity3d.com/Manual/Plugins.html• http://docs.unity3d.com/Manual/wp8-plugins.html• http://docs.unity3d.com/Manual/PluginsForAndroid.html• http://docs.unity3d.com/Manual/PluginsForIOS.html
Examples• https://github.com/playgameservices/play-games-plugin-for-unity• https://
github.com/googleads/googleads-mobile-plugins/tree/master/unity• https://github.com/guillermocalvo/admob-unity-plugin• https://github.com/mikito/unity-admob-plugin• https://github.com/bearprada/flurry-unity-plugin• https://github.com/mikito/unity-flurry-plugin• https://github.com/faizann/unity3d_flurry
ROME 24 June 2014 – Valerio Riva
Thank you!
ROME 24 June 2014 – Valerio Riva
Question Time
No animals were harmed in the making of this talk
[email protected] @ValerioRiva
http://it.linkedin.com/in/valerioriva/