diff --git a/example/integration_test/plugin_integration_test.dart b/example/integration_test/plugin_integration_test.dart index c47c599..022cf28 100644 --- a/example/integration_test/plugin_integration_test.dart +++ b/example/integration_test/plugin_integration_test.dart @@ -16,10 +16,7 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('getPlatformVersion test', (WidgetTester tester) async { - final FlutterCocosView plugin = FlutterCocosView(); - final String? version = await plugin.getPlatformVersion(); - // The version string depends on the host platform running the test, so - // just assert that some non-empty string is returned. - expect(version?.isNotEmpty, true); + + }); } diff --git a/ios/Classes/CocosPlayerUtils.swift b/ios/Classes/CocosPlayerUtils.swift new file mode 100644 index 0000000..c6473ee --- /dev/null +++ b/ios/Classes/CocosPlayerUtils.swift @@ -0,0 +1,281 @@ +// +// CocosPlayerUtils.swift +// flutter_cocos_widget +// +// Created by Rex Raphael on 30/01/2021. +// + +import Foundation + + +private var cocos_warmed_up = false +// Hack to work around iOS SDK 4.3 linker problem +// we need at least one __TEXT, __const section entry in main application .o files +// to get this section emitted at right time and so avoid LC_ENCRYPTION_INFO size miscalculation +private let constsection = 0 + +// keep arg for cocos init from non main +var gArgc: Int32 = 0 +var gArgv: UnsafeMutablePointer?>? = nil +var appLaunchOpts: [UIApplication.LaunchOptionsKey: Any]? = [:] + +/***********************************PLUGIN_ENTRY STARTS**************************************/ +public func InitCocosIntegration(argc: Int32, argv: UnsafeMutablePointer?>?) { + gArgc = argc + gArgv = argv +} + +public func InitCocosIntegrationWithOptions( + argc: Int32, + argv: UnsafeMutablePointer?>?, + _ launchingOptions: [UIApplication.LaunchOptionsKey: Any]?) { + gArgc = argc + gArgv = argv + appLaunchOpts = launchingOptions +} +/***********************************PLUGIN_ENTRY END**************************************/ + +// Load cocos framework for fisrt run +func CocosFrameworkLoad() -> CocosFramework? { + var bundlePath: String? = nil + bundlePath = Bundle.main.bundlePath + bundlePath = (bundlePath ?? "") + "/Frameworks/CocosFramework.framework" + + let bundle = Bundle(path: bundlePath ?? "") + if bundle?.isLoaded == false { + bundle?.load() + } + + return bundle?.principalClass?.getInstance() +} + +/*********************************** GLOBAL FUNCS & VARS START**************************************/ +public var globalControllers: Array = [FLTCocosWidgetController]() + +private var cocosPlayerUtils: CocosPlayerUtils? = nil +func GetCocosPlayerUtils() -> CocosPlayerUtils { + + if cocosPlayerUtils == nil { + cocosPlayerUtils = CocosPlayerUtils() + } + + return cocosPlayerUtils ?? CocosPlayerUtils() +} + +/*********************************** GLOBAL FUNCS & VARS END****************************************/ + +var controller: CocosAppController? +var sharedApplication: UIApplication? + +@objc protocol CocosEventListener: AnyObject { + + func onReceiveMessage(_ message: UnsafePointer?) + +} + +@objc public class CocosPlayerUtils: UIResponder, UIApplicationDelegate, CocosFrameworkListener { + var ufw: CocosFramework! + private var _isCocosPaused = false + private var _isCocosReady = false + private var _isCocosLoaded = false + + func initCocos() { + if (self.cocosIsInitiallized()) { + self.ufw?.showCocosWindow() + return + } + + self.ufw = CocosFrameworkLoad() + + self.ufw?.setDataBundleId("com.cocos3d.framework") + + registerCocosListener() + self.ufw?.runEmbedded(withArgc: gArgc, argv: gArgv, appLaunchOpts: appLaunchOpts) + + if self.ufw?.appController() != nil { + controller = self.ufw?.appController() + controller?.cocosMessageHandler = self.cocosMessageHandlers + controller?.cocosSceneLoadedHandler = self.cocosSceneLoadedHandlers + self.ufw?.appController()?.window?.windowLevel = UIWindow.Level(UIWindow.Level.normal.rawValue - 1) + } + _isCocosLoaded = true + } + + // check if cocos is initiallized + func cocosIsInitiallized() -> Bool { + if self.ufw != nil { + return true + } + + return false + } + + // Create new cocos player + func createPlayer(completed: @escaping (_ view: UIView?) -> Void) { + if self.cocosIsInitiallized() && self._isCocosReady { + completed(controller?.rootView) + return + } + + NotificationCenter.default.addObserver(forName: NSNotification.Name("CocosReady"), object: nil, queue: OperationQueue.main, using: { note in + self._isCocosReady = true + completed(controller?.rootView) + }) + + DispatchQueue.main.async { +// if (sharedApplication == nil) { +// sharedApplication = UIApplication.shared +// } + + // Always keep Flutter window on top +// let flutterUIWindow = sharedApplication?.keyWindow +// flutterUIWindow?.windowLevel = UIWindow.Level(UIWindow.Level.normal.rawValue + 1) // Always keep Flutter window in top +// sharedApplication?.keyWindow?.windowLevel = UIWindow.Level(UIWindow.Level.normal.rawValue + 1) + + self.initCocos() + + cocos_warmed_up = true + self._isCocosReady = true + self._isCocosLoaded = true + + self.listenAppState() + + completed(controller?.rootView) + } + + } + + func registerCocosListener() { + if self.cocosIsInitiallized() { + self.ufw?.register(self) + } + } + + func unregisterCocosListener() { + if self.cocosIsInitiallized() { + self.ufw?.unregisterFrameworkListener(self) + } + } + + @objc + public func cocosDidUnload(_ notification: Notification!) { + unregisterCocosListener() + self.ufw = nil + self._isCocosReady = false + self._isCocosLoaded = false + } + + @objc func handleAppStateDidChange(notification: Notification?) { + if !self._isCocosReady { + return + } + + let cocosAppController = self.ufw?.appController() as? CocosAppController + let application = UIApplication.shared + + if notification?.name == UIApplication.willResignActiveNotification { + cocosAppController?.applicationWillResignActive(application) + } else if notification?.name == UIApplication.didEnterBackgroundNotification { + cocosAppController?.applicationDidEnterBackground(application) + } else if notification?.name == UIApplication.willEnterForegroundNotification { + cocosAppController?.applicationWillEnterForeground(application) + } else if notification?.name == UIApplication.didBecomeActiveNotification { + cocosAppController?.applicationDidBecomeActive(application) + } else if notification?.name == UIApplication.willTerminateNotification { + cocosAppController?.applicationWillTerminate(application) + } else if notification?.name == UIApplication.didReceiveMemoryWarningNotification { + cocosAppController?.applicationDidReceiveMemoryWarning(application) + } + } + + + // Listener for app lifecycle eventa + func listenAppState() { + for name in [ + UIApplication.didBecomeActiveNotification, + UIApplication.didEnterBackgroundNotification, + UIApplication.willTerminateNotification, + UIApplication.willResignActiveNotification, + UIApplication.willEnterForegroundNotification, + UIApplication.didReceiveMemoryWarningNotification + ] { + NotificationCenter.default.addObserver( + self, + selector: #selector(self.handleAppStateDidChange), + name: name, + object: nil) + } + } + // Pause cocos player + func pause() { + self.ufw?.pause(true) + self._isCocosPaused = true + } + + // Resume cocos player + func resume() { + self.ufw?.pause(false) + self._isCocosPaused = false + } + + // Unoad cocos player + func unload() { + self.ufw?.unloadApplication() + } + + func isCocosLoaded() -> Bool { + return _isCocosLoaded + } + + func isCocosPaused() -> Bool { + return _isCocosPaused + } + + // Quit cocos player application + func quit() { + self.ufw?.quitApplication(0) + self._isCocosLoaded = false + } + + // Post message to cocos + func postMessageToCocos(gameObject: String?, cocosMethodName: String?, cocosMessage: String?) { + if self.cocosIsInitiallized() { + self.ufw?.sendMessageToGO(withName: gameObject, functionName: cocosMethodName, message: cocosMessage) + } + } + + /// Handle incoming cocos messages looping through all controllers and passing payload to + /// the controller handler methods + @objc + func cocosMessageHandlers(_ message: UnsafePointer?) { + for c in globalControllers { + if let strMsg = message { + c.handleMessage(message: String(utf8String: strMsg) ?? "") + } else { + c.handleMessage(message: "") + } + } + } + + func cocosSceneLoadedHandlers(name: UnsafePointer?, buildIndex: UnsafePointer?, isLoaded: UnsafePointer?, isValid: UnsafePointer?) { + if let sceneName = name, + let bIndex = buildIndex, + let loaded = isLoaded, + let valid = isValid { + + let loadedVal = Bool((Int(bitPattern: loaded) != 0)) + let validVal = Bool((Int(bitPattern: valid) != 0)) + + let addObject: Dictionary = [ + "name": String(utf8String: sceneName) ?? "", + "buildIndex": Int(bitPattern: bIndex), + "isLoaded": loadedVal, + "isValid": validVal, + ] + + for c in globalControllers { + c.handleSceneChangeEvent(info: addObject) + } + } + } +} diff --git a/ios/Classes/FLTCocosOptionsSink.swift b/ios/Classes/FLTCocosOptionsSink.swift new file mode 100644 index 0000000..430eaf0 --- /dev/null +++ b/ios/Classes/FLTCocosOptionsSink.swift @@ -0,0 +1,13 @@ +// +// FLTCocosOptionsSink.swift +// flutter_unity_widget +// +// Created by Rex Raphael on 30/01/2021. +// + +import Foundation + +// Defines map UI options writable from Flutter. +protocol FLTCocosOptionsSink: AnyObject { + func setDisabledUnload(enabled: Bool) +} diff --git a/ios/Classes/FLTCocosView.swift b/ios/Classes/FLTCocosView.swift new file mode 100644 index 0000000..a390100 --- /dev/null +++ b/ios/Classes/FLTCocosView.swift @@ -0,0 +1,19 @@ +// +// FLTCocosView.swift +// flutter_unity_widget +// +// Created by Rex Raphael on 30/01/2021. +// + +import Foundation +import UIKit + + +class FLTCocosView: UIView { + override func layoutSubviews() { + super.layoutSubviews() + if (!self.bounds.isEmpty) { + GetCocosPlayerUtils().ufw?.appController()?.rootView.frame = self.bounds + } + } +} diff --git a/ios/Classes/FLTCocosViewFactory.swift b/ios/Classes/FLTCocosViewFactory.swift new file mode 100644 index 0000000..2a94119 --- /dev/null +++ b/ios/Classes/FLTCocosViewFactory.swift @@ -0,0 +1,30 @@ +// +// FLTCocosViewFactory.swift +// flutter_unity_widget +// +// Created by Rex Raphael on 30/01/2021. +// + +import Foundation + +class FLTCocosWidgetFactory: NSObject, FlutterPlatformViewFactory { + private weak var registrar: FlutterPluginRegistrar? + + init(registrar: NSObjectProtocol & FlutterPluginRegistrar) { + super.init() + self.registrar = registrar + } + + func createArgsCodec() -> (NSObjectProtocol & FlutterMessageCodec) { + return FlutterStandardMessageCodec.sharedInstance() + } + + func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView { + let controller = FLTCocosWidgetController( + frame: frame, + viewIdentifier: viewId, + arguments: args, + registrar: registrar!) + return controller + } +} diff --git a/ios/Classes/FLTCocosWidgetController.swift b/ios/Classes/FLTCocosWidgetController.swift new file mode 100644 index 0000000..d0c8438 --- /dev/null +++ b/ios/Classes/FLTCocosWidgetController.swift @@ -0,0 +1,182 @@ +// +// FLTCocosViewController.swift +// flutter_cocos_widget +// +// Created by Rex Raphael on 30/01/2021. +// + +import Foundation +import CocosFramework + +// Defines cocos controllable from Flutter. +public class FLTCocosWidgetController: NSObject, FLTCocosOptionsSink, FlutterPlatformView { + private var _rootView: FLTCocosView + private var viewId: Int64 = 0 + private var channel: FlutterMethodChannel? + private weak var registrar: (NSObjectProtocol & FlutterPluginRegistrar)? + + private var _disposed = false + + init( + frame: CGRect, + viewIdentifier viewId: Int64, + arguments args: Any?, + registrar: NSObjectProtocol & FlutterPluginRegistrar + ) { + self._rootView = FLTCocosView(frame: frame) + super.init() + + globalControllers.append(self) + + self.viewId = viewId + + let channelName = String(format: "plugin.xraph.com/cocos_view_%lld", viewId) + self.channel = FlutterMethodChannel(name: channelName, binaryMessenger: registrar.messenger()) + + self.channel?.setMethodCallHandler(self.methodHandler) + self.attachView() + } + + func methodHandler(_ call: FlutterMethodCall, result: FlutterResult) { + if call.method == "cocos#dispose" { + self.dispose() + result(nil) + } else { + self.reattachView() + if call.method == "cocos#isReady" { + result(GetCocosPlayerUtils().cocosIsInitiallized()) + } else if call.method == "cocos#isLoaded" { + let _isUnloaded = GetCocosPlayerUtils().isCocosLoaded() + result(_isUnloaded) + } else if call.method == "cocos#createCocosPlayer" { + startCocosIfNeeded() + result(nil) + } else if call.method == "cocos#isPaused" { + let _isPaused = GetCocosPlayerUtils().isCocosPaused() + result(_isPaused) + } else if call.method == "cocos#pausePlayer" { + GetCocosPlayerUtils().pause() + result(nil) + } else if call.method == "cocos#postMessage" { + self.postMessage(call: call, result: result) + result(nil) + } else if call.method == "cocos#resumePlayer" { + GetCocosPlayerUtils().resume() + result(nil) + } else if call.method == "cocos#unloadPlayer" { + GetCocosPlayerUtils().unload() + result(nil) + } else if call.method == "cocos#quitPlayer" { + GetCocosPlayerUtils().quit() + result(nil) + } else if call.method == "cocos#waitForCocos" { + result(nil) + } else { + result(FlutterMethodNotImplemented) + } + } + } + + func setDisabledUnload(enabled: Bool) { + + } + + public func view() -> UIView { + return _rootView; + } + + private func startCocosIfNeeded() { + GetCocosPlayerUtils().createPlayer(completed: { [self] (view: UIView?) in + + }) + } + + func attachView() { + startCocosIfNeeded() + + let cocosView = GetCocosPlayerUtils().ufw?.appController()?.rootView + if let superview = cocosView?.superview { + cocosView?.removeFromSuperview() + superview.layoutIfNeeded() + } + + if let cocosView = cocosView { + _rootView.addSubview(cocosView) + _rootView.layoutIfNeeded() + self.channel?.invokeMethod("events#onViewReattached", arguments: "") + } + GetCocosPlayerUtils().resume() + } + + func reattachView() { + let cocosView = GetCocosPlayerUtils().ufw?.appController()?.rootView + let superview = cocosView?.superview + if superview != _rootView { + attachView() + } + + GetCocosPlayerUtils().resume() + } + + func removeViewIfNeeded() { + if GetCocosPlayerUtils().ufw == nil { + return + } + + let cocosView = GetCocosPlayerUtils().ufw?.appController()?.rootView + if _rootView == cocosView?.superview { + if globalControllers.isEmpty { + cocosView?.removeFromSuperview() + cocosView?.superview?.layoutIfNeeded() + } else { + globalControllers.last?.reattachView() + } + } + GetCocosPlayerUtils().resume() + } + + func dispose() { + if _disposed { + return + } + + globalControllers.removeAll{ value in + return value == self + } + + channel?.setMethodCallHandler(nil) + removeViewIfNeeded() + + _disposed = true + } + + /// Handles messages from cocos in the current view + func handleMessage(message: String) { + self.channel?.invokeMethod("events#onCocosMessage", arguments: message) + } + + + /// Handles scene changed event from cocos in the current view + func handleSceneChangeEvent(info: Dictionary) { + self.channel?.invokeMethod("events#onCocosSceneLoaded", arguments: info) + } + + /// Post messages to cocos from flutter + func postMessage(call: FlutterMethodCall, result: FlutterResult) { + guard let args = call.arguments else { + result("iOS could not recognize flutter arguments in method: (postMessage)") + return + } + + if let myArgs = args as? [String: Any], + let gObj = myArgs["gameObject"] as? String, + let method = myArgs["methodName"] as? String, + let message = myArgs["message"] as? String { + GetCocosPlayerUtils().postMessageToCocos(gameObject: gObj, cocosMethodName: method, cocosMessage: message) + result(nil) + } else { + result(FlutterError(code: "-1", message: "iOS could not extract " + + "flutter arguments in method: (postMessage)", details: nil)) + } + } +} diff --git a/ios/Classes/FlutterCocosViewPlugin.swift b/ios/Classes/FlutterCocosViewPlugin.swift index 280e2c4..1c62a17 100644 --- a/ios/Classes/FlutterCocosViewPlugin.swift +++ b/ios/Classes/FlutterCocosViewPlugin.swift @@ -6,6 +6,10 @@ public class FlutterCocosViewPlugin: NSObject, FlutterPlugin { let channel = FlutterMethodChannel(name: "flutter_cocos_view", binaryMessenger: registrar.messenger()) let instance = FlutterCocosViewPlugin() registrar.addMethodCallDelegate(instance, channel: channel) + + let fuwFactory = FLTCocosWidgetFactory(registrar: registrar) + registrar.register(fuwFactory, withId: "plugin.gem.com/cocos_view", gestureRecognizersBlockingPolicy: FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded) + } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { @@ -17,3 +21,4 @@ public class FlutterCocosViewPlugin: NSObject, FlutterPlugin { } } } + diff --git a/ios/Classes/FlutterCocosWidgetPlugin.h b/ios/Classes/FlutterCocosWidgetPlugin.h new file mode 100644 index 0000000..9f43b28 --- /dev/null +++ b/ios/Classes/FlutterCocosWidgetPlugin.h @@ -0,0 +1,13 @@ +// +// FlutterCocosWidgetPlugin.h +// FlutterCocosWidgetPlugin +// +// Created by Kris Pypen on 8/1/19. +// Updated by Rex Raphael on 8/27/2020. +// + +#import + +@interface FlutterCocosWidgetPlugin : NSObject +@end + diff --git a/ios/Classes/FlutterCocosWidgetPlugin.m b/ios/Classes/FlutterCocosWidgetPlugin.m new file mode 100644 index 0000000..30ed9bb --- /dev/null +++ b/ios/Classes/FlutterCocosWidgetPlugin.m @@ -0,0 +1,22 @@ +#import FlutterCocosWidgetPlugin.h +#import +#if __has_include() +#import +#else +// Support project import fallback if the generated compatibility header +// is not copied when this plugin is created as a library. +// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 +#import "flutter_unity_widget-Swift.h" +#endif + +@implementation FlutterCocosWidgetPlugin { + NSObject* _registrar; + FlutterMethodChannel* _channel; + NSMutableDictionary* _unityControllers; +} + ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftFlutterCocosWidgetPlugin registerWithRegistrar:registrar]; +} + +@end diff --git a/ios/flutter_cocos_view.podspec b/ios/flutter_cocos_view.podspec index ca2bc5a..997b9ae 100644 --- a/ios/flutter_cocos_view.podspec +++ b/ios/flutter_cocos_view.podspec @@ -15,7 +15,7 @@ A new Flutter plugin project. s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.platform = :ios, '12.0' + s.platform = :ios, '13.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }