diff --git a/README.md b/README.md index e2b63c6..507667d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![Watch on GitHub][github-watch-badge]][github-watch] [![Star on GitHub][github-star-badge]][github-star] -Flutter unity 3D widget for embedding unity in flutter. Add a Flutter widget to show unity. Works on Android, iOS in works. +Flutter unity 3D widget for embedding unity in flutter. Add a Flutter widget to show unity. Works on Android and iOS. ## Installation First depend on the library by adding this to your packages `pubspec.yaml`: @@ -27,9 +27,10 @@ import 'package:flutter_unity_widget/flutter_unity_widget.dart'; ## Preview -Android (30 fps gif, showcasing communication between Flutter and Unity): +30 fps gifs, showcasing communication between Flutter and Unity: ![gif](https://github.com/snowballdigital/flutter-unity-view-widget/blob/master/preview_android.gif?raw=true) +![gif](https://github.com/snowballdigital/flutter-unity-view-widget/blob/master/preview_ios.gif?raw=true)
@@ -60,9 +61,11 @@ Now your project files should look like this. 1. First Open Unity Project. -2. Click Menu: File => Build Settings => Player Settings +2. Click Menu: File => Build Settings -3. Change `Product Name` to Name of the Xcode project, You can find it follow `ios/${XcodeProjectName}.xcodeproj`. +Be sure you have at least one scene added to your build. + +3. => Player Settings **Android Platform**: 1. Make sure your `Graphics APIs` are set to OpenGLES3 with a fallback to OpenGLES2 (no Vulkan) @@ -74,20 +77,22 @@ Now your project files should look like this. - ARM64 ✅ - x86 ✅ + - **IOS Platform**: - 1. Other Settings find the Rendering part, uncheck the `Auto Graphics API` and select only `OpenGLES2`. - 2. Depending on where you want to test or run your app, (simulator or physical device), you should select the appropriate SDK on `Target SDK`. + **iOS Platform**: + 1. This only works with Unity version >=2019.3 because uses Unity as a library! + 2. Other Settings find the Rendering part, uncheck the `Auto Graphics API` and select only `OpenGLES3`. + 3. Depending on where you want to test or run your app, (simulator or physical device), you should select the appropriate SDK on `Target SDK`.
- +
### Add Unity Build Scripts and Export -Copy [`Build.cs`](https://github.com/snowballdigital/flutter-unity-view-widget/tree/master/scripts/Editor/Build.cs) and [`XCodePostBuild.cs`](https://github.com/f111fei/react-native-unity-demo/blob/master/unity/Cube/Assets/Scripts/Editor/XCodePostBuild.cs) to `unity//Assets/Scripts/Editor/` +Copy [`Build.cs`](https://github.com/snowballdigital/flutter-unity-view-widget/tree/master/scripts/Editor/Build.cs) and [`XCodePostBuild.cs`](https://github.com/snowballdigital/flutter-unity-view-widget/tree/master/scripts/Editor/XCodePostBuild.cs) to `unity//Assets/Scripts/Editor/` Open your unity project in Unity Editor. Now you can export the Unity project with `Flutter/Export Android` (for Unity versions up to 2019.2), `Flutter/Export Android (Unity 2019.3.*)` (for Unity versions 2019.3 and up, which uses the new [Unity as a Library](https://blogs.unity3d.com/2019/06/17/add-features-powered-by-unity-to-native-mobile-apps/) export format), or `Flutter/Export IOS` menu. @@ -123,6 +128,24 @@ IOS will export unity project to `ios/UnityExport`. } ``` +**iOS Platform Only** + + 1. open your ios/Runner.xcworkspace (workspace!, not the project) in Xcode and add the exported project in the workspace root (with a right click in the Navigator, not on an item -> Add Files to “Runner” -> add the UnityExport/Unity-Iphone.xcodeproj file + + 2. Select the Unity-iPhone/Data folder and change the Target Membership for Data folder to UnityFramework + + 3. Add this to your Runner/Runner/Runner-Bridging-Header.h + +```c +#import "UnityUtils.h" +``` + 4. Add to AppDelegate.swift before the GeneratePluginRegistrant call: + +```swift +InitArgs(CommandLine.argc, CommandLine.unsafeArgv) +``` + 5. Opt-in to the embedded views preview by adding a boolean property to the app's `Info.plist` file with the key `io.flutter.embedded_views_preview` and the value `YES`. +
### AR Foundation (ANDROID only at the moment) @@ -274,7 +297,6 @@ class _UnityDemoScreenState extends State{ - `resume()` (Use this to resume unity player) ## Known issues - - no iOS support yet - Android Export requires several manual changes - Using AR will make the activity run in full screen (hiding status and navigation bar). diff --git a/change_target_membership_data_folder.png b/change_target_membership_data_folder.png new file mode 100644 index 0000000..1373a57 Binary files /dev/null and b/change_target_membership_data_folder.png differ diff --git a/ios/Classes/FlutterUnity.h b/ios/Classes/FlutterUnity.h deleted file mode 100644 index ca66ad7..0000000 --- a/ios/Classes/FlutterUnity.h +++ /dev/null @@ -1,25 +0,0 @@ -// -// Created by rex on 19/03/2019. -// - -#ifndef FLUTTER_UNITY_WIDGET_FLUTTERUNITY_H -#define FLUTTER_UNITY_WIDGET_FLUTTERUNITY_H - - -#import - -@interface FlutterUnityController : NSObject - -- (instancetype)initWithWithFrame:(CGRect)frame - viewIdentifier:(int64_t)viewId - arguments:(id _Nullable)args - binaryMessenger:(NSObject*)messenger; - -- (UIView*)view; -@end - -@interface FlutterUnityFactory : NSObject -- (instancetype)initWithMessenger:(NSObject*)messenger; -@end - -#endif //FLUTTER_UNITY_WIDGET_FLUTTERUNITY_H diff --git a/ios/Classes/FlutterUnity.m b/ios/Classes/FlutterUnity.m deleted file mode 100644 index 2eff0b8..0000000 --- a/ios/Classes/FlutterUnity.m +++ /dev/null @@ -1,87 +0,0 @@ -// -// Created by rex on 19/03/2019. -// - -#include "FlutterUnity.h" - -@implementation FlutterUnityFactory { - NSObject* _messenger; -} - -- (instancetype)initWithMessenger:(NSObject*)messenger { - self = [super init]; - if (self) { - _messenger = messenger; - } - return self; -} - - -@implementation FlutterUnityController { - WKWebView* _webView; - int64_t _viewId; - FlutterMethodChannel* _channel; -} - -- (instancetype)initWithWithFrame:(CGRect)frame - viewIdentifier:(int64_t)viewId - arguments:(id _Nullable)args - binaryMessenger:(NSObject*)messenger { - if ([super init]) { - _viewId = viewId; - _webView = [[WKWebView alloc] initWithFrame:frame]; - NSString* channelName = [NSString stringWithFormat:@"nativeweb_%lld", viewId]; - _channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:messenger]; - __weak __typeof__(self) weakSelf = self; - [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { - [weakSelf onMethodCall:call result:result]; - }]; - - } - return self; -} - -- (UIView*)view { - return _webView; -} - -- (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - if ([[call method] isEqualToString:@"postMessage"]) { - [self postMessage:call result:result]; - } else if ([[call method] isEqualToString:@"isReady"]) { - [self postMessage:call result:result]; - } else if ([[call method] isEqualToString:@"createUnity"]) { - [self postMessage:call result:result]; - } else if ([[call method] isEqualToString:@"pause"]) { - [self postMessage:call result:result]; - } else if ([[call method] isEqualToString:@"resume"]) { - [self postMessage:call result:result]; - } else { - result(FlutterMethodNotImplemented); - } -} - -- (void)postMessage:(FlutterMethodCall*)call result:(FlutterResult)result { - NSString* url = [call arguments]; - if (![self postMessage:url]) { - result([FlutterError errorWithCode:@"loadUrl_failed" - message:@"Failed parsing the URL" - details:[NSString stringWithFormat:@"URL was: '%@'", url]]); - } else { - result(nil); - } -} - -- (bool)onPostMessage:(NSString*)url { - NSURL* nsUrl = [NSURL URLWithString:url]; - if (!nsUrl) { - return false; - } - NSURLRequest* req = [NSURLRequest requestWithURL:nsUrl]; - [_webView loadRequest:req]; - return true; -} - -@end - - diff --git a/ios/Classes/FlutterUnityView.h b/ios/Classes/FlutterUnityView.h new file mode 100644 index 0000000..2b1bad0 --- /dev/null +++ b/ios/Classes/FlutterUnityView.h @@ -0,0 +1,18 @@ +// +// FlutterUnityView.h +// FlutterUnityView +// +// Created by krispypen on 8/1/2019 +// + +#import + +#import "UnityUtils.h" + +@interface FlutterUnityView : UIView + +@property (nonatomic, strong) UIView* uView; + +- (void)setUnityView:(UIView *)view; + +@end diff --git a/ios/Classes/FlutterUnityView.m b/ios/Classes/FlutterUnityView.m new file mode 100644 index 0000000..ea5753e --- /dev/null +++ b/ios/Classes/FlutterUnityView.m @@ -0,0 +1,37 @@ +// +// FlutterUnityView.m +// FlutterUnityView +// +// Created by krispypen on 8/1/2019 +// + +#import "FlutterUnityView.h" + +@implementation FlutterUnityView + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + return self; +} + +- (void)dealloc +{ +} + +- (void)setUnityView:(UIView *)view +{ + self.uView = view; + [self setNeedsLayout]; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + [(UIView *)self.uView removeFromSuperview]; + [self insertSubview:(UIView *)self.uView atIndex:0]; + ((UIView *)self.uView).frame = self.bounds; + [(UIView *)self.uView setNeedsLayout]; +} + +@end diff --git a/ios/Classes/FlutterUnityWidgetPlugin.h b/ios/Classes/FlutterUnityWidgetPlugin.h index 0de4a00..013da00 100644 --- a/ios/Classes/FlutterUnityWidgetPlugin.h +++ b/ios/Classes/FlutterUnityWidgetPlugin.h @@ -1,4 +1,25 @@ +// +// FlutterUnityWidgetPlugin.h +// FlutterUnityWidgetPlugin +// +// Created by Kris Pypen on 8/1/19. +// + #import @interface FlutterUnityWidgetPlugin : NSObject @end + +@interface FUController : NSObject + +- (instancetype)initWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args + registrar:(NSObject *)registrar; + +- (UIView*)view; +@end + +@interface FUViewFactory : NSObject +- (instancetype)initWithRegistrar:(NSObject *)registrar; +@end diff --git a/ios/Classes/FlutterUnityWidgetPlugin.m b/ios/Classes/FlutterUnityWidgetPlugin.m index 2a96730..4febb23 100644 --- a/ios/Classes/FlutterUnityWidgetPlugin.m +++ b/ios/Classes/FlutterUnityWidgetPlugin.m @@ -1,20 +1,106 @@ +// +// FlutterUnityWidgetPlugin.m +// FlutterUnityWidgetPlugin +// +// Created by Kris Pypen on 8/1/19. +// + #import "FlutterUnityWidgetPlugin.h" -#import +#import "UnityUtils.h" +#import "FlutterUnityView.h" -+ (void)registerWithRegistrar:(NSObject*)registrar { - FlutterNativeWebFactory* webviewFactory = - [[FlutterNativeWebFactory alloc] initWithMessenger:registrar.messenger]; - [registrar registerViewFactory:webviewFactory withId:@"unity_view"]; -} - - -/* -#import "FlutterUnityWidgetPlugin.h" -#import +#include @implementation FlutterUnityWidgetPlugin + (void)registerWithRegistrar:(NSObject*)registrar { - [SwiftFlutterUnityWidgetPlugin registerWithRegistrar:registrar]; + FUViewFactory* fuviewFactory = [[FUViewFactory alloc] initWithRegistrar:registrar]; + [registrar registerViewFactory:fuviewFactory withId:@"unity_view"]; } @end -*/ \ No newline at end of file + +@implementation FUViewFactory { + NSObject* _registrar; +} +- (instancetype)initWithRegistrar:(NSObject*)registrar { + self = [super init]; + if (self) { + _registrar = registrar; + } + return self; +} +- (NSObject*)createArgsCodec { + return [FlutterStandardMessageCodec sharedInstance]; +} + +- (NSObject*)createWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args { + FUController* controller = [[FUController alloc] initWithFrame:frame + viewIdentifier:viewId + arguments:args + registrar:_registrar]; + return controller; +} + +@end + +@implementation FUController { + FlutterUnityView* _uView; + int64_t _viewId; + FlutterMethodChannel* _channel; +} + +- (instancetype)initWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args + registrar:(NSObject*)registrar { + if ([super init]) { + _viewId = viewId; + + NSString* channelName = [NSString stringWithFormat:@"unity_view_%lld", viewId]; + _channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:registrar.messenger]; + __weak __typeof__(self) weakSelf = self; + [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { + [weakSelf onMethodCall:call result:result]; + }]; + + } + return self; +} + +- (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { + if ([[call method] isEqualToString:@"postMessage"]) { + [self postMessage:call result:result]; + } else { + result(FlutterMethodNotImplemented); + } +} + +- (void)postMessage:(FlutterMethodCall*)call result:(FlutterResult)result { + NSString* object = [call arguments][@"gameObject"]; + NSString* method = [call arguments][@"methodName"]; + NSString* message = [call arguments][@"message"]; + + UnityPostMessage(object, method, message); + + result(nil); +} + +- (UIView*)view { + _uView = [[FlutterUnityView alloc] init]; + if ([UnityUtils isUnityReady]) { + [_uView setUnityView: (UIView*)[GetAppController() unityView]]; + } else { + [UnityUtils createPlayer:^{ + [_uView setUnityView: (UIView*)[GetAppController() unityView]]; + }]; + } + return _uView; +} + +@end + + + + + diff --git a/ios/Classes/UnityUtils.h b/ios/Classes/UnityUtils.h index a0513cd..31fdca5 100644 --- a/ios/Classes/UnityUtils.h +++ b/ios/Classes/UnityUtils.h @@ -1,27 +1,23 @@ -// -// Created by rex on 15/03/2019. -// - -#ifndef FLUTTER_UNITY_WIDGET_UNITYUTILS_H -#define FLUTTER_UNITY_WIDGET_UNITYUTILS_H - #import +#ifndef UnityUtils_h +#define UnityUtils_h + #ifdef __cplusplus extern "C" { #endif -void InitArgs(int argc, char* argv[]); + void InitArgs(int argc, char* argv[]); -bool UnityIsInited(void); + bool UnityIsInited(void); -void InitUnity(); + void InitUnity(); -void UnityPostMessage(NSString* gameObject, NSString* methodName, NSString* message); + void UnityPostMessage(NSString* gameObject, NSString* methodName, NSString* message); -void UnityPauseCommand(); + void UnityPauseCommand(); -void UnityResumeCommand(); + void UnityResumeCommand(); #ifdef __cplusplus } // extern "C" @@ -40,5 +36,4 @@ void UnityResumeCommand(); @end -#endif //FLUTTER_UNITY_WIDGET_UNITYUTILS_H - +#endif /* UnityUtils_h */ diff --git a/ios/Classes/UnityUtils.mm b/ios/Classes/UnityUtils.mm index 8c152ee..7d59064 100644 --- a/ios/Classes/UnityUtils.mm +++ b/ios/Classes/UnityUtils.mm @@ -1,10 +1,8 @@ -#include "RegisterMonoModules.h" -#include "RegisterFeatures.h" #include #import -#import "UnityInterface.h" #import "UnityUtils.h" -#import "UnityAppController.h" + +#include // Hack to work around iOS SDK 4.3 linker problem // we need at least one __TEXT, __const section entry in main application .o files @@ -18,6 +16,8 @@ char** g_argv; void UnityInitTrampoline(); +UnityFramework* ufw; + extern "C" void InitArgs(int argc, char* argv[]) { g_argc = argc; @@ -29,6 +29,19 @@ extern "C" bool UnityIsInited() return unity_inited; } +UnityFramework* UnityFrameworkLoad() +{ + NSString* bundlePath = nil; + bundlePath = [[NSBundle mainBundle] bundlePath]; + bundlePath = [bundlePath stringByAppendingString: @"/Frameworks/UnityFramework.framework"]; + + NSBundle* bundle = [NSBundle bundleWithPath: bundlePath]; + if ([bundle isLoaded] == false) [bundle load]; + + UnityFramework* ufw = [bundle.principalClass getInstance]; + return ufw; +} + extern "C" void InitUnity() { if (unity_inited) { @@ -36,41 +49,30 @@ extern "C" void InitUnity() } unity_inited = true; - UnityInitStartupTime(); + ufw = UnityFrameworkLoad(); - @autoreleasepool - { - UnityInitTrampoline(); - UnityInitRuntime(g_argc, g_argv); - - RegisterMonoModules(); - NSLog(@"-> registered mono modules %p\n", &constsection); - RegisterFeatures(); - - // iOS terminates open sockets when an application enters background mode. - // The next write to any of such socket causes SIGPIPE signal being raised, - // even if the request has been done from scripting side. This disables the - // signal and allows Mono to throw a proper C# exception. - std::signal(SIGPIPE, SIG_IGN); - } + [ufw setDataBundleId: "com.unity3d.framework"]; + [ufw frameworkWarmup: g_argc argv: g_argv]; } extern "C" void UnityPostMessage(NSString* gameObject, NSString* methodName, NSString* message) { - UnitySendMessage([gameObject UTF8String], [methodName UTF8String], [message UTF8String]); + dispatch_async(dispatch_get_main_queue(), ^{ + [ufw sendMessageToGOWithName:[gameObject UTF8String] functionName:[methodName UTF8String] message:[message UTF8String]]; + }); } extern "C" void UnityPauseCommand() { dispatch_async(dispatch_get_main_queue(), ^{ - UnityPause(1); + [ufw pause:true]; }); } extern "C" void UnityResumeCommand() { dispatch_async(dispatch_get_main_queue(), ^{ - UnityPause(0); + [ufw pause:false]; }); } diff --git a/ios/flutter_unity_widget.podspec b/ios/flutter_unity_widget.podspec index 910e54c..ea1a834 100644 --- a/ios/flutter_unity_widget.podspec +++ b/ios/flutter_unity_widget.podspec @@ -15,7 +15,11 @@ Flutter unity 3D widget for embedding unity in flutter s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' + s.frameworks = 'UnityFramework' s.ios.deployment_target = '8.0' + s.xcconfig = { + 'FRAMEWORK_SEARCH_PATHS' => '"${PODS_ROOT}/../UnityExport" "${PODS_ROOT}/../.symlinks/flutter/ios-release" "${PODS_CONFIGURATION_BUILD_DIR}"', + 'OTHER_LDFLAGS' => '$(inherited) -framework UnityFramework ${PODS_LIBRARIES}' + } end - diff --git a/preview_ios.gif b/preview_ios.gif new file mode 100644 index 0000000..e28e08a Binary files /dev/null and b/preview_ios.gif differ diff --git a/pubspec.yaml b/pubspec.yaml index 4c29349..2828df2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,6 +3,8 @@ description: Flutter unity 3D widget for embedding unity in flutter version: 0.1.5 authors: - Rex Raphael + - Thomas Stockx + - Kris Pypen homepage: https://github.com/snowballdigital/flutter-unity-view-widget/tree/master environment: diff --git a/scripts/Editor/XCodePostBuild.cs b/scripts/Editor/XCodePostBuild.cs new file mode 100644 index 0000000..9ae503a --- /dev/null +++ b/scripts/Editor/XCodePostBuild.cs @@ -0,0 +1,295 @@ +/* +MIT License +Copyright (c) 2017 Jiulong Wang +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#if UNITY_IOS + +using System; + +using System.Collections.Generic; +using System.IO; + +using UnityEditor; +using UnityEditor.Callbacks; +using UnityEditor.iOS.Xcode; + +/// +/// Adding this post build script to Unity project enables the flutter-unity-widget to access it +/// +public static class XcodePostBuild +{ + + /// + /// The identifier added to touched file to avoid double edits when building to existing directory without + /// replace existing content. + /// + private const string TouchedMarker = "https://github.com/snowballdigital/flutter-unity-view-widget"; + + [PostProcessBuild] + public static void OnPostBuild(BuildTarget target, string pathToBuiltProject) + { + if (target != BuildTarget.iOS) + { + return; + } + + PatchUnityNativeCode(pathToBuiltProject); + + UpdateUnityProjectFiles(pathToBuiltProject); + } + + /// + /// We need to add the Data folder to the UnityFramework framework + /// + private static void UpdateUnityProjectFiles(string pathToBuiltProject) + { + var pbx = new PBXProject(); + var pbxPath = Path.Combine(pathToBuiltProject, "Unity-iPhone.xcodeproj/project.pbxproj"); + pbx.ReadFromFile(pbxPath); + + // Add UnityExport/Data + var targetGuid = pbx.TargetGuidByName("UnityFramework"); + var fileGuid = pbx.AddFolderReference(Path.Combine(pathToBuiltProject, "Data"), "Data"); + pbx.AddFileToBuild(targetGuid, fileGuid); + + pbx.WriteToFile(pbxPath); + } + + /// + /// Make necessary changes to Unity build output that enables it to be embedded into existing Xcode project. + /// + private static void PatchUnityNativeCode(string pathToBuiltProject) + { + EditUnityFrameworkH(Path.Combine(pathToBuiltProject, "UnityFramework/UnityFramework.h")); + EditUnityAppControllerH(Path.Combine(pathToBuiltProject, "Classes/UnityAppController.h")); + EditUnityAppControllerMM(Path.Combine(pathToBuiltProject, "Classes/UnityAppController.mm")); + } + + + /// + /// Edit 'UnityFramework.h': add 'frameworkWarmup' + /// + private static void EditUnityFrameworkH(string path) + { + var inScope = false; + + // Add frameworkWarmup method + EditCodeFile(path, line => + { + inScope |= line.Contains("- (void)runUIApplicationMainWithArgc:"); + + if (inScope) + { + if (line.Trim() == "") + { + inScope = false; + + return new string[] + { + "", + "// Added by " + TouchedMarker, + "- (void)frameworkWarmup:(int)argc argv:(char*[])argv;", + "" + }; + } + } + + return new string[] { line }; + }); + } + + /// + /// Edit 'UnityAppController.h': returns 'UnityAppController' from 'AppDelegate' class. + /// + private static void EditUnityAppControllerH(string path) + { + var inScope = false; + var markerDetected = false; + + // Add static GetAppController + EditCodeFile(path, line => + { + inScope |= line.Contains("- (void)startUnity:"); + + if (inScope) + { + if (line.Trim() == "") + { + inScope = false; + + return new string[] + { + "", + "// Added by " + TouchedMarker, + "+ (UnityAppController*)GetAppController;", + "" + }; + } + } + + return new string[] { line }; + }); + + inScope = false; + markerDetected = false; + + // Modify inline GetAppController + EditCodeFile(path, line => + { + inScope |= line.Contains("extern UnityAppController* GetAppController"); + + if (inScope && !markerDetected) + { + if (line.Trim() == "") + { + inScope = false; + markerDetected = true; + + return new string[] + { + "// }", + "", + "static inline UnityAppController* GetAppController()", + "{", + " return [UnityAppController GetAppController];", + "}", + }; + } + + return new string[] { "// " + line }; + } + + return new string[] { line }; + }); + + + } + + /// + /// Edit 'UnityAppController.mm': triggers 'UnityReady' notification after Unity is actually started. + /// + private static void EditUnityAppControllerMM(string path) + { + var inScope = false; + var markerDetected = false; + + EditCodeFile(path, line => + { + if (line.Trim() == "@end") + { + return new string[] + { + "", + "// Added by " + TouchedMarker, + "static UnityAppController *unityAppController = nil;", + "", + @"+ (UnityAppController*)GetAppController", + "{", + " static dispatch_once_t onceToken;", + " dispatch_once(&onceToken, ^{", + " unityAppController = [[self alloc] init];", + " });", + " return unityAppController;", + "}", + "", + line, + }; + } + + inScope |= line.Contains("- (void)startUnity:"); + markerDetected |= inScope && line.Contains(TouchedMarker); + + if (inScope && line.Trim() == "}") + { + inScope = false; + + if (markerDetected) + { + return new string[] { line }; + } + else + { + return new string[] + { + " // Modified by " + TouchedMarker, + @" [[NSNotificationCenter defaultCenter] postNotificationName: @""UnityReady"" object:self];", + "}", + }; + } + } + + return new string[] { line }; + }); + + inScope = false; + markerDetected = false; + + // Modify inline GetAppController + EditCodeFile(path, line => + { + inScope |= line.Contains("UnityAppController* GetAppController()"); + + if (inScope && !markerDetected) + { + if (line.Trim() == "}") + { + inScope = false; + markerDetected = true; + + return new string[] + { + "", + }; + } + + return new string[] { "// " + line }; + } + + return new string[] { line }; + }); + } + + private static void EditCodeFile(string path, Func> lineHandler) + { + var bakPath = path + ".bak"; + if (File.Exists(bakPath)) + { + File.Delete(bakPath); + } + + File.Move(path, bakPath); + + using (var reader = File.OpenText(bakPath)) + using (var stream = File.Create(path)) + using (var writer = new StreamWriter(stream)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + var outputs = lineHandler(line); + foreach (var o in outputs) + { + writer.WriteLine(o); + } + } + } + } +} + +#endif \ No newline at end of file diff --git a/workspace.png b/workspace.png new file mode 100644 index 0000000..4115386 Binary files /dev/null and b/workspace.png differ