InkSwift

2.0.0

Swift wrapper for the Ink narrative scripting language. Based on InkJS. Requires JavaScriptCore (so no Linux support).
maartene/InkSwift

What's New

Cross-platform support by default

2023-09-09T12:49:34Z

JXKit is now the default JavaScript engine, providing cross-platform support for Apple and Linux platforms.

InkSwift

Swift wrapper for the Ink narrative scripting language. Based on InkJS.

Supported features

  • Apple and Linux platforms (using JXKit). If you want to use the JavaScriptCore version (Apple platforms only), use release 0.0.4.
  • Loading compiled Ink stories loadStory(json: String) as well as Ink directly loadStory(ink: String);
  • Basic flow: continue story continueStory() and choices chooseChoiceIndex(_ index: Int);
  • Moving to knots/stitches moveToKnitStitch(_ knot: String, stitch: String? = nil);
  • Tag support. Read currentTags variable;
  • Setting and getting variable values (supports strings, 32-bit integers and doubles);
  • Loading and saving state stateToJSON() and loadState(_ jsonDataString: String);
  • On Apple platforms: Combine integration (subscribe to state changes, observe variables).

Linux installations

JXKit on Linux requires JavaScriptCore libraries, that are part of WebKit. For Ubuntu 20.04 and newer: sudo apt install libwebkit2gtk-4.0-dev

Limitations

  • When using the built in Ink compiler, don't use JavaScript tokens that can confuse string definitions, like `.

Getting started

Regular XCode project

Use 'File' -> 'Swift Packages' -> 'Add Package Dependency...' to add the package to your project.

Using SwiftPM

Add InkSwift as a dependency to Package.swift:

let package = Package(
    name: "test_swiftpm",   // choose your own name
    platforms: [
        .macOS(.v10_15),
    ],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
        .package(url: "https://github.com/maartene/InkSwift.git", from: "1.0.0")
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .target(
            name: "test_swiftpm",
            dependencies: ["InkSwift"]),
        .testTarget(
            name: "test_swiftpmTests",
            dependencies: ["test_swiftpm"]),
    ]
)

Usage

Start by creating a InkStory

let story = InkStory()

Then load a story from a Ink JSON (you can use Inklecate or Inky to convert an .ink file to a .json file.):

let storyJSON = ... //
story.loadStory(json: storyJSON)

Alternatively, you can load a story from Ink directly. Not however, that this might hinder performance for users.

story.loadStory(ink: "Hello, World!")

You can create a very basic command line 'player' using just a few lines of code:

// As long as the story can continue (either because there is more text or there are options you can choose)), keep the loop going
while story.canContinue || story.options.count > 0 {
    // Print the current text to the console/terminal
    print(story.currentText)

    // If you can continue the story, we wait for input before continuing.
    if story.canContinue {
        print("Press 'enter' to continue")
        _ = readLine()
        story.continueStory()
    }
    // If there are options, show the options and wait for player to choose
    else if story.options.count > 0 {
        // print every option
        for option in story.options {
            print("\(option.index). \(option.text)")
        }
        print("What is your choice?")

        // wait for input from player
        if let choice = readLine() {
            // try and convert input to an index.
            if let index = Int(String(choice.first ?? "a")) {
                // choose the selected option index
                story.chooseChoiceIndex(index)
            }
        }
    }
}
// no more content, story is done.
print("Story done!")

Using Combine/SwiftUI

InkStory conforms to the ObservableObject protocol. This makes using it in Combine possible and SwiftUI very easy. A simple example SwiftUI view that can play an Ink story would contain:

Import the InkSwift package

Add

import InkSwift

to ContentView.swift

The ink story as a @StateObject

Add the following property to your ContentView: @StateObject var story = InkStory()

Add a function that loads the Ink story:

Note: change the filename to load to your own JSON file. Don't forget to add it to the project.

func loadStory() {
guard let url = Bundle.main.url(forResource: "test.ink", withExtension: "json") else {
    fatalError("Could not find ink story file.")
}

guard let storyJSON = try? String(contentsOf: url) else {
    fatalError("Could not load story file.")
}

story.loadStory(json: storyJSON)
}

Create the body property

var body: some View {
    VStack {
        Text(story.currentText)
        if story.canContinue {
            Button("Continue") {
                story.continueStory()
            }
        }
        ForEach(story.options, id: \.index) { option in
            Button(option.text) {
                story.chooseChoiceIndex(option.index)
            }
        }
    }.padding()
    .onAppear {
        loadStory()
    }
}

Licenced content

  • The Ink runtime uses the official Ink Javascript port InkJS
  • The Compiler tests use The Intercept as an example story to compile
  • Uses JSONEquality in some of the tests
  • Cross-platform JavaScript runtime JXKit

Description

  • Swift Tools 5.6.0
View More Packages from this Author

Dependencies

Last updated: Thu May 02 2024 10:24:00 GMT-0900 (Hawaii-Aleutian Daylight Time)