iOS2018. 2. 13. 15:01


iOS에도 이런게 나왔을 줄이야..


안드로이드 하이브리드 개발자들은 알고 있겠지만 addJavascriptInterface를 통해서 자바스크립트에 java object를 바인딩이 가능하다.

이를 통해 하이브리드를 통해서 동기화 호출도 가능하다.

하지만 iOS는 불가했었다.

cordova도 NSURLProtocol을 통해서 하이브리드를 지원하는데 역시 동기화 호출을 불가하다.

하지만 불가능하지는 않다. 다른 방법으로 가능한 방법이 있으며 이를 통해서 실제 동기화 호출을 구현하기도 하였다.


그런데 iOS7부터 지원하는 JavaScriptCore프레임웍으로 가능한것 같다.


http://www.dbotha.com/2014/11/19/using-javascript-core-for-game-scripting-on-ios/ 를 참조하였다.


게임스크립팅 엔진을 javascriptcore로 변경하는것에 대한 글이다.


일반적인 게임튜터리얼의 스크립트는 아래와 같다.


/* tutorial1.js */
showText("Hello World!", /*x:*/ 160, /*y:*/ 100);
wait(5); // sleep 5 seconds
hideText()

showText("Tap the screen to continue", 160, 100);
waitForTap(); // pause execution until the user taps the UI
hideText();

showText("Match three coloured blocks to win!", 160, 100);
waitForLevelEnd(); // pause execution until a level end event occurs
hideText();

loadLevel("level2.map")

예제에서 보듯이 어떤 레벨에 도달할때까지 스크립트실행을 중지하는것이 좋다. 위에서 wait()함수에 대해 의문을 품게 될것이다. 싱글스레드의 자바스크립트 엔진에서는 의도한대로 작동을 기대하기 어렵다. 완전히 스레드가 멈추게 될것이므로..

JSVirtualMachine은 어떻게 실행을 중단할까?


스크립팅엔진자체에 대해 먼저 더 알아보자.


자바스크립트에 접근가능하도록 작성된 스위프트 프로토콜 메서드들을 보자


/* ScriptEngine.swift */
@objc protocol ScriptingEngineExports : JSExport {
    func wait(duration: Double)
    func waitForTap()
    func waitForLevelEnd()
    func loadLevel(mapName: String)
    func showText(text: String, _ x: Double, _ y: Double)
    func hideText()
}


금방 구현가능하고 앞서 기술한 튜터리얼 스크립트에 매우 가깝게 매칭된다.


이번엔 스크립팅 엔진을 살펴보자.


/* ScriptEngine.swift */
@objc class ScriptEngine : NSObject, ScriptEngineExports {
    var context: JSContext!
    let engineQueue = dispatch_queue_create(
            "script_engine", DISPATCH_QUEUE_SERIAL)
    
    override init() {
        super.init()
        
        dispatch_async(engineQueue) {
            self.context = JSContext()
            self.context.setObject(self, forKeyedSubscript: "$")
            self.context.evaluateScript(
                "function wait(duration) {$.wait(duration);}" + 
                "function waitForTap() {$.waitForTap();}" +
                "function waitForLevelEnd(){$.waitForLevelEnd();}" +
                "function print(text) {$.print(text);}" + 
                "function println(text) {$.print(text);}" + 
                "function loadLevel(name) {$.loadLevel(name);}")
        }
    }
    
    /* to be continued... */ 
}

engineQueue은 JSContext를 사용하기 위한 dispatch_queue이다. 이는 메인스레드와 분리하여 스크립트의 콜백을 스위프트가 받을 수 있도록 한다.

여기서 중요한것은 JSContext가 메인스레드에서 분리되어서 인스턴스화되었다는 것이다. 이는 메인스레드가 블럭되는것을 방지한다.


ScriptEngine 객체 인스턴스는 $변수를 통해서 접근이 가능하다.  evaluateScript가 호출되어 스크립트에 억세스 할 수 있는 ScriptEngineExports의 기능을 노출한다. 스위프트의 함수를 호출함으로 인해 ScriptEngine의 메서드가 호출된다.


실행중인 스크립트를 로드하는것은 간단하다. 



/* ScriptEngine.swift */
var currentScriptName: String?

func runScript(scriptName: String) {
    var ext = scriptName.pathExtension
    var fileName = scriptName.substringToIndex(
        advance(scriptName.startIndex, 
                scriptName.utf16Count - ext.utf16Count - 1))
    
    if fileExtension.utf16Count == 0 {
        fileName = scriptName
        ext = "js"
    }
    
    let url = NSBundle.mainBundle()
        .URLForResource(fileName, withExtension: ext)
    let scriptCode = String(contentsOfURL: url!, 
                            encoding: NSUTF8StringEncoding, 
                            error: nil)!

    self.currentScriptName = fileName + "." + fileExtension

    dispatch_async(engineQueue) {
        self.context.evaluateScript(scriptCode)
        return
    }
}

다음과 같이 실행이 가능하다.


let scriptEngine = ScriptEngine()
scriptEngine.runScript("tutorial1")

이제 어떻게 다양한 Wait()함수를 실행할 수 있는지 살펴보자


/* ScriptEngine.swift */
func wait(duration: Double) {
    NSThread.sleepForTimeInterval(duration)
}

이 wait함수는 engineQueue에 의해 관리되는 분리된 스레드에서 호출될것이 보장된다. 그래서 sleepForTimeInterval은 메인스레드가 아니라 이 스레드에서 동작할것이다. 자바스크립트는 실행이 잠시 중단될것이다.


다음은 좀 복잡한 대기로직들이다.


/* ScriptEngine.swift */
let tapCondition = NSCondition()
let levelEndCondition = NSCondition()
var seenTap = false
var seenLevelEnd = false

func waitForCondition(condition: NSCondition, 
        inout predicate: Bool) {
    condition.lock()
    while (!predicate) { // loop to protect against spurious wakes
        condition.wait()
    }
    predicate = false
    condition.unlock()
}

func notifyCondition(condition: NSCondition) {
    condition.lock()
    condition.signal()
    condition.unlock()
}

func waitForTap() {
    waitForCondition(tapCondition, &seenTap)
}

func notifyTap() {
    notifyCondition(tapCondition)
}

func waitForLevelEnd() {
    waitForCondition(levelEndCondition, &seenLevelEnd)
}

func notifyLevelEnd() {
    notifyCondition(levelEndCondition)
}

waitForTap()과 waitForLevelEnd()는 자바스크립에서 대응되는 함수가 호출되었을때 인스턴스 메서드로 노출된다. waitForCondition(condition:predicate:)는 해당 컨디션이 시그널될때까지 스레드를 블럭한다. notifyTap(), notifyLevelEnd()는 해당 컨티션발생시 시그널되고 engineQueue스레드를 깨워서 자바스크립트가 다시 살아나게 한다. 이 메서드들은 게임엔진에서 해당 이벤트가 발생하였을때 호출해야 한다.


이제 남은 노출된 함수는 구현하기에 따라 다르며 여기서는 다음과 같이 구현하였다.


/* ScriptEngine.swift */
func postNotification(type: GameNotification, 
        var userInfo: [NSObject : AnyObject]?) {
    
    if userInfo == nil {
        userInfo = [NSObject : AnyObject]()
    }
    
    userInfo![kGameNotificationUserInfoCurrentScriptName] 
        =  self.currentScriptName
    
    dispatch_async(dispatch_get_main_queue(), {
        NSNotificationCenter.defaultCenter()
            .postNotificationName(
                type.name(), object: self, userInfo: userInfo)
    })
}

func loadLevel(levelName: String) {
    postNotification(GameNotification.LoadLevel, 
        userInfo: [kGameNotificationUserInfoLevelName: levelName])
}

func showText(text: String, _ x: Double, _ y: Double) {
    postNotification(GameNotification.ShowText, 
        userInfo: [kGameNotificationUserInfoText: text, 
                   kGameNotificationUserInfoTextX: x, 
                   kGameNotificationUserInfoTextY: y])
}

func hideText() {
    postNotification(GameNotification.HideText, nil)
}


Posted by 삼스