JavaScriptCore for game scripting with iOS & Swift

by

A casual game side project I work on from time to time recently reached the point where it made sense to incorporate an embedded scripting engine. In the past I’ve had good experiences with Lua, however since the release of iOS 7 I’ve been looking for an excuse to play with the JavaScriptCore framework.

The JavaScriptCore Framework allows you to evaluate JavaScript programs from within a C-based program. It also lets you insert custom objects to the JavaScript environment.

This post is not intended to be an introduction to the JavaScriptCore Framework. Instead, If you’re looking for a starting point then take a look at the following first:

In the interest of brevity I’ll cover only a small subset of the game functionality which is currently handled by my scripting engine – the in-game tutorial system. This should provide a good foundation to build on for your own games.

The Scripts

Tutorials in my game are linear in nature with the player being introduced to various game mechanics in sequence. A typical (although simplified) in-game tutorial script in JavaScript would look something like this:

/* 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")

Given the linearity of my in-game tutorials it makes sense to be able to “pause” script execution until certain goals have been completed by the player. The astute JavaScript developers amongst you may immediately question how the various wait() functions work, after all JavaScript by it’s very nature is event-driven with most (if not all) virtual machines relying on a single threaded execution model. All I/O is typically non-blocking and asynchronous. So how do we pause execution within a JavaScriptCore JSVirtualMachine? Well I’ll get to that, first lets lay the foundations for the scripting engine itself.

Exposing Swift functions to JavaScript

To kick things off lets look at the Swift methods that we plan on making accessible within JavaScript:

/* 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()
}

We’ll get to the implementations shortly but nothing too tricky here. They closely match up with the functions seen earlier in the tutorial script.

The JavaScriptCore Scripting Engine

Next lets start to look at the definition of the scripting engine:

/* 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... */ 
}

The engineQueue is a dispatch queue to which we will submit any task that manipulates the JSContext, this is done to keep JavaScript to Swift callback execution off the main thread.

The key thing notice in the above code snippet is that the JSContext is instantiated on a separate thread and not the main thread. This needs to be the case (unfortunately for reasons that are not entirely obvious or documented outside of source code) to avoid blocking the main thread when we “pause” JavaScript execution in the various wait() functions.

The ScriptEngine object instance is made accessible in JavaScript through the $ variable. In the evaluateScript call we expose the core functions from ScriptingEngineExports that that will be accessible to our external JavaScript scripts. Calling any of these functions from within JavaScript will result in the related Swift method being invoked (not on the main thread) on the ScriptEngine object instance.

Running scripts

Loading a running a script is relatively straightforward. Included is some logic that means the extension is assumed to be .js if not specified. Notice again how we manipulate the JSContext off the main thread by submitting a task to the engineQueue, this guarantees all JavaScript to Swift calls to occur off the main thread:

/* 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
    }
}

A script can be run in the following fashion:

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

Pausing JSVirtualMachine execution

Now lets take a look at how we can pause JavaScript execution in the various wait() functions. We’ll start with the simplest variant that pauses execution for a specified period of time:

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

This wait() function (as well as all others that we’ll cover shortly) is guaranteed to be called on a separate thread managed by the engineQueue i.e. not on the main thread. sleepForTimeInterval will thus put this thread to sleep rather than the main thread. JavaScript execution will stop until the thread wakes up from sleeping whilst our main thread is still free to handle user input and run other game simulation logic. Next onto the marginally trickier wait functions:

/* 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() are exported instance methods called when the corresponding JavaScript functions are invoked, they’re guaranteed to be run off the main thread. waitForCondition(condition:predicate:) is where engineQueue thread execution is blocked (and thus JavaScript execution paused) until the appropriate condition is signalled.

notifyTap() & notifyLevelEnd() signal on the appropriate condition and wake the engineQueue thread (assuming it was blocked on the signalled condition) allowing JavaScript execution to resume. These two methods should be called by your game engine when the appropriate events arise.

Triggering game events

The remaining exported functions are implementation specific and will vary from game to game. The way I like to deal with them in the ScriptEngine is to post NSNotification’s and then deal those notifications elsewhere as appropriate.

/* 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)
}

The end

Hopefully the above should provide a foundation to build on for your own games. I’ll be sure to update here with a link to the full game source code once it hits the App Store.