見出し画像

Unity as a LibraryのサンプルプロジェクトをSwiftで書き直した

先週、Unity as a Library のMainViewController.mmをじっくり読んだ という記事で、iOSアプリ内にUnityを取り入れる実装について理解を深めました。
公式が配布しているサンプルはObjective-c++で書かれたものなのですが、今回はそれをSwiftにて書き直してみました。

ソースコードはGithubに公開するつもりですが、取り急ぎは今書きあげたものを解説して共有したいと思います。

(2020/05/15)追記:公開しました。

実装の際のポイント

Obj-cとSwiftの連携については以下の記事を参考にさせていただきました。

実際の移植は以下の記事を参考にさせていただきました。より実践的な書き方は、以下の記事が参考になると思います。

実際のコード

MyViewController.mmは、
* AppDelegate.swift
* HostViewController.swift
* UnityUIView.swift
* NativeiOSApp-Bridging-Header.h
という4つのファイルに分割しました。

AppDelegate.swift

import Foundation

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UnityFrameworkListener, NativeCallsProtocol {
   var window: UIWindow?
   var application: UIApplication?
   var storyboard: UIStoryboard?
   var hostViewController: HostViewController!
   var appLaunchOpts: [UIApplication.LaunchOptionsKey: Any]?
   var unityView: UnityUIView?
   var didQuit: Bool = false

   @objc var currentUnityController: UnityAppController!
   @objc var ufw: UnityFramework?

   func UnityFrameworkLoad() -> UnityFramework {
       let bundlePath = Bundle.main.bundlePath
       let path = bundlePath + "/Frameworks/UnityFramework.framework"
       let bundle = Bundle(path: path)!
       if !bundle.isLoaded {
           bundle.load()
       }
       let fw = bundle.principalClass as! UnityFramework.Type
       let ufw = fw.getInstance()!
       if ufw.appController() == nil {
           var header = _mh_execute_header
           ufw.setExecuteHeader(&header)
       }
       return ufw
   }

   func showAlert(_ title:String, _ msg:String) {
       let alert: UIAlertController = UIAlertController(title: title, message: msg, preferredStyle:  .alert)

       let defaultAction: UIAlertAction = UIAlertAction(title: "OK", style: .default, handler:{
           (action: UIAlertAction!) -> Void in
       })
       
       alert.addAction(defaultAction)
       window?.rootViewController?.present(alert, animated: true)
   }
   
   func unityIsInitialized()->Bool {
       return (self.ufw != nil && self.ufw?.appController() != nil)
   }

   //Unityのウインドウを表示する
   func showMainView() {
       if(!unityIsInitialized()) {
           showAlert("Unity is not initialized", "Initialize Unity first");
       } else {
           //Unityのウインドウを表示する
           self.ufw?.showUnityWindow();
       }
   }
   
   func showHostMainWindow() {
       self.showHostMainWindow("")
   }

   func showHostMainWindow(_ color: String!) {
       if(color == "blue") {
           self.hostViewController.unpauseBtn.backgroundColor = .blue
       } else if(color == "red") {
           self.hostViewController.unpauseBtn.backgroundColor = .red
       }else if(color == "yellow") {
           self.hostViewController.unpauseBtn.backgroundColor = .yellow
       }
       
       //windowを前面に
       window?.makeKeyAndVisible()
   }
   
   func sendMsgToUnity() {
   //ここでufwにメッセージを送信する
       self.ufw?.sendMessageToGO(withName: "Cube", functionName: "ChangeColor", message: "yellow");
   }
   
   func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
       self.application = application
       ufw = UnityFrameworkLoad()

       appLaunchOpts = launchOptions
       
       storyboard = UIStoryboard(name: "Main", bundle: .main)
       hostViewController = storyboard?.instantiateViewController(withIdentifier: "Host") as! HostViewController
       
       // Set root view controller and make windows visible
       self.window = UIWindow.init(frame: UIScreen.main.bounds)
       self.window?.rootViewController = hostViewController;
       
       window?.makeKeyAndVisible()
       return true
   }

   func initUnity(){
       if(unityIsInitialized()) {
           showAlert("Unity already initialized", "Unload Unity first")
           return
       }
       
       if(didQuit) {
           showAlert("Unity cannot be initialized after quit", "Use unload instead")
           return
       }

       ufw?.register(self)
       FrameworkLibAPI.registerAPIforNativeCalls(self)
       ufw?.setDataBundleId("com.unity3d.framework")
       ufw?.runEmbedded(withArgc: CommandLine.argc, argv: CommandLine.unsafeArgv, appLaunchOpts: appLaunchOpts)
       
       // set quit handler to change default behavior of exit app
//        ufw.appController()?.quitHandler
       
       //ufwのUIView
       let view:UIView? = ufw?.appController()?.rootView;
       
       //各種ボタンが配置されていない場合にviewにボタンを再配置する
       if self.unityView == nil {
           self.unityView = UnityUIView()
           view?.addSubview(self.unityView as! UnityUIView)
       }
   }

   func unloadButtonTouched(_ sender: UIButton)
   {
       if(!unityIsInitialized()) {
           showAlert("Unity is not initialized", "Initialize Unity first")
       } else {
           UnityFrameworkLoad().unloadApplication()
       }
   }
   
   func quitButtonTouched(_ sender:UIButton)
   {
       if(!unityIsInitialized()) {
           showAlert("Unity is not initialized", "Initialize Unity first");
       } else {
           UnityFrameworkLoad().quitApplication(0)
       }
   }
   
   //UnityFramework.h にて UnityFrameworkListenerとして宣言されている
   func unityDidUnload(notification: Notification)
   {
       print("unityDidUnload called")
       self.ufw?.unregisterFrameworkListener(self)
       self.ufw = nil
       self.showHostMainWindow("")
   }
   
   //UnityFramework.h にて UnityFrameworkListenerとして宣言されている
   func unityDidQuit(notification: Notification)
   {
       print("unityDidQuit called")
       self.ufw?.unregisterFrameworkListener(self)
       self.ufw = nil
       self.didQuit = true
       self.showHostMainWindow("")
   }

   func applicationWillResignActive(_ application: UIApplication) {
       // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
       // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
       self.ufw?.appController()?.applicationWillResignActive(application)
   }
   
   func applicationDidEnterBackground(_ application: UIApplication) {
       // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
       // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
       self.ufw?.appController()?.applicationDidEnterBackground(application)
   }
   
   func applicationWillEnterForeground(_ application: UIApplication) {
       // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
       self.ufw?.appController()?.applicationWillEnterForeground(application)
   }
   
   func applicationDidBecomeActive(_ application: UIApplication) {
       // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
       self.ufw?.appController()?.applicationDidBecomeActive(application)
   }
   
   func applicationWillTerminate(_ application: UIApplication) {
       // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
       self.ufw?.appController()?.applicationWillTerminate(application)
   }

HostViewController.swift

import Foundation
import UIKit

class HostViewController: UIViewController {
   public var unityInitBtn:UIButton!
   public var unpauseBtn:UIButton!
   public var unloadBtn:UIButton!
   public var quitBtn:UIButton!
   private var delegate:AppDelegate = UIApplication.shared.delegate as! AppDelegate

   override func viewDidLoad() {
       super.viewDidLoad()

       // Do any additional setup after loading the view.
       self.view.backgroundColor = .blue
       
       // INIT UNITY
       self.unityInitBtn = UIButton(type: .system)
       self.unityInitBtn.setTitle("Init", for: .normal)
       self.unityInitBtn.frame = CGRect(x:0, y:0, width:100, height:44)
       self.unityInitBtn.center = CGPoint(x:50, y:120)
       self.unityInitBtn.backgroundColor = .green
       //タップ時のアクション:AppDelegate内に定義したメソッドを利用する
       self.unityInitBtn.addTarget(self.delegate, action: #selector(self.delegate.initUnity), for: .primaryActionTriggered)
       self.view.addSubview(self.unityInitBtn)

       // SHOW UNITY
       self.unpauseBtn = UIButton(type: .system)
       self.unpauseBtn.setTitle("Show Unity",for: .normal)
       self.unpauseBtn.frame = CGRect(x:100, y:0, width:100, height:44)
       self.unpauseBtn.center = CGPoint(x:150, y:120);
       self.unpauseBtn.backgroundColor = .lightGray
       //タップ時のアクション:AppDelegate内に定義したメソッドを利用する
       self.unpauseBtn.addTarget(self.delegate, action: #selector(self.delegate.showMainView), for: .primaryActionTriggered)
       self.view.addSubview(self.unpauseBtn)

       // UNLOAD UNITY
       self.unloadBtn = UIButton(type: .system)
       self.unloadBtn.setTitle("Unload", for: .normal)
       self.unloadBtn.frame = CGRect(x:200, y:0, width:100, height:44)
       self.unloadBtn.center = CGPoint(x:250, y:120)
       self.unloadBtn.backgroundColor = .red
       //タップ時のアクション:AppDelegate内に定義したメソッドを利用する
       self.unloadBtn.addTarget(self.delegate, action: #selector(self.delegate.unloadButtonTouched), for: .primaryActionTriggered)
       self.view.addSubview(self.unloadBtn)

       // QUIT UNITY
       self.quitBtn = UIButton(type: .system)
       self.quitBtn.setTitle("Quit",for: .normal)
       self.quitBtn.frame = CGRect(x:300, y:0, width:100, height:44)
       self.quitBtn.center = CGPoint(x:250, y:170)
       self.quitBtn.backgroundColor = .red
       //タップ時のアクション:AppDelegate内に定義したメソッドを利用する
       self.quitBtn.addTarget(self.delegate, action: #selector(self.delegate.quitButtonTouched), for: .primaryActionTriggered)
       self.view.addSubview(self.quitBtn)
   }
   

   /*
   // MARK: - Navigation

   // In a storyboard-based application, you will often want to do a little preparation before navigation
   override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
       // Get the new view controller using segue.destination.
       // Pass the selected object to the new view controller.
   }
   */
}

UnityUIView.swift

import UIKit

class UnityUIView: UIView {
   private lazy var showUnityOffButton: UIButton = {
       let button = UIButton(type: .system)
       button.setTitle("Show Main", for: .normal)
       button.frame = CGRect(x:0, y:0, width:100, height:44)
       button.center = CGPoint(x:50, y:300)
       button.backgroundColor = .green
       button.addTarget(self, action: #selector(showHostMainWindow), for: .primaryActionTriggered)
       return button
   }()
   private lazy var btnSendMsg: UIButton = {
       let button = UIButton(type: .system)
       button.setTitle("Send Msg", for: .normal)
       button.frame = CGRect(x:0, y:0, width:100, height:44)
       button.center = CGPoint(x:150, y:300)
       button.backgroundColor = .yellow
       button.addTarget(self, action: #selector(sendMsg), for: .primaryActionTriggered)
       return button
   }()
   private lazy var unloadBtn: UIButton = {
       let button = UIButton(type: .system)
       button.setTitle("Unload", for: .normal)
       button.frame = CGRect(x:0, y:0, width:100, height:44)
       button.center = CGPoint(x:250, y:300)
       button.backgroundColor = .red
       button.addTarget(self, action: #selector(unloadButtonTouched), for: .primaryActionTriggered)
       return button
   }()
   private lazy var quitBtn: UIButton = {
       let button = UIButton(type: .system)
       button.setTitle("Quit", for: .normal)
       button.frame = CGRect(x:250, y:0, width:100, height:44)
       button.center = CGPoint(x:250, y:350)
       button.backgroundColor = .red
       button.addTarget(self, action: #selector(quitButtonTouched), for: .primaryActionTriggered)
       return button
   }()
   private var delegate:AppDelegate = UIApplication.shared.delegate as! AppDelegate

   /*
   // Only override draw() if you perform custom drawing.
   // An empty implementation adversely affects performance during animation.
   override func draw(_ rect: CGRect) {
       // Drawing code
   }
   */
   override init(frame: CGRect) {
       //        super.init(frame: frame)
       super.init(frame: CGRect(x:0, y:200, width:300, height:400))
       setup()
   }
   
   required init?(coder aDecoder: NSCoder) {
       super.init(coder: aDecoder)!
       setup()
   }

   func setup(){
       self.addSubview(showUnityOffButton)
       self.addSubview(btnSendMsg)
       
       // Unload
       self.addSubview(unloadBtn)
       
       // Quit
       self.addSubview(quitBtn)
   }

   @objc func showHostMainWindow(){
      self.delegate.showHostMainWindow()
   }
   @objc func sendMsg(){
       self.delegate.sendMsgToUnity()
   }
   
   @objc func unloadButtonTouched(sender: UIButton){
       self.delegate.unloadButtonTouched(sender)
   }
   
   @objc func quitButtonTouched(sender: UIButton){
       self.delegate.quitButtonTouched(sender)
   }
}

UnityのviewにaddSubViewされるボタン群です。UIViewでラップして、Unity側への追加が冗長にならないようにしています。

NativeiOSApp-Bridging-Header.h

#include <UnityFramework/UnityFramework.h>
#include <UnityFramework/NativeCallProxy.h>

SwiftからObj-cの実装を利用したいときに必要なヘッダファイルです。

おわりに

慣れないObj-c++を一行ずつ読みながらさらにSwiftに書き換えていく作業はかなりの時間を要しましたが、既存のコードをコピペして動かすよりも当然ながら理解度が全く異なります。

時間がかかるのはストレスにはなりますが、なるべく多くコードを読んで理解を深めてから実装に入るほうが、結果スムーズに開発が進みそうですね。

(コード読むのは苦手なので、今後もコツコツと.継続したいと思います。)

この記事が気に入ったら、サポートをしてみませんか?
気軽にクリエイターの支援と、記事のオススメができます!
5
株式会社PARTYでソフトウェアエンジニアをやってます。