Integrating InkGameScript with Popular Game Engines

Integration Guide

Introduction

InkGameScript's flexibility and power make it an ideal choice for adding narrative elements to games built with various engines and frameworks. Whether you're developing a 3D adventure in Unity, a 2D RPG in Godot, or a web-based visual novel, InkGameScript can seamlessly integrate with your chosen technology stack. This comprehensive guide will walk you through the integration process for popular game engines, providing practical examples and best practices along the way.

By the end of this tutorial, you'll understand how to implement InkGameScript in your game project, handle dialogue display, manage choices, sync game state with narrative variables, and create rich interactive storytelling experiences. We'll cover Unity, Godot, web frameworks, and even touch on custom engine integration, ensuring you have all the knowledge needed regardless of your development platform.

Unity Integration: The Gold Standard

Unity remains one of the most popular choices for InkGameScript integration, thanks to the official Ink Unity Integration package maintained by Inkle. Let's explore how to set up and use Ink in your Unity projects.

Installation and Setup

First, install the Ink Unity Integration package. You have several options:

Method 1: Unity Package Manager (Recommended)

1. Open Package Manager (Window → Package Manager)
2. Click '+' → Add package from git URL
3. Enter: https://github.com/inkle/ink-unity-integration.git
4. Click 'Add' and wait for installation

Method 2: Direct Download

Download the latest release from the GitHub repository and import it into your project through Assets → Import Package → Custom Package.

Basic Unity Implementation

Once installed, here's how to create a basic dialogue system:

using UnityEngine;
using Ink.Runtime;
using UnityEngine.UI;
using System.Collections.Generic;

public class DialogueManager : MonoBehaviour
{
    [SerializeField] private TextAsset inkJSONAsset;
    [SerializeField] private Text dialogueText;
    [SerializeField] private Transform choiceButtonContainer;
    [SerializeField] private GameObject choiceButtonPrefab;
    
    private Story story;
    private List currentChoiceButtons = new List();
    
    void Start()
    {
        LoadStory();
        ContinueStory();
    }
    
    void LoadStory()
    {
        story = new Story(inkJSONAsset.text);
        
        // Bind external functions
        story.BindExternalFunction("GetPlayerName", () => {
            return PlayerPrefs.GetString("PlayerName", "Adventurer");
        });
        
        story.BindExternalFunction("AddInventoryItem", (string item) => {
            GameManager.Instance.AddToInventory(item);
        });
        
        // Observe variables
        story.ObserveVariable("player_health", (string varName, object newValue) => {
            GameManager.Instance.SetPlayerHealth((int)newValue);
        });
    }
    
    void ContinueStory()
    {
        // Clear previous choices
        ClearChoices();
        
        // Continue getting text from the story
        string text = "";
        while (story.canContinue)
        {
            text += story.Continue();
            
            // Check for tags
            ProcessTags();
        }
        
        dialogueText.text = text;
        
        // Display choices if any
        if (story.currentChoices.Count > 0)
        {
            DisplayChoices();
        }
        else if (!story.canContinue)
        {
            // End of story reached
            EndDialogue();
        }
    }
    
    void ProcessTags()
    {
        foreach (string tag in story.currentTags)
        {
            string[] splitTag = tag.Split(':');
            
            switch (splitTag[0])
            {
                case "speaker":
                    SetSpeaker(splitTag[1]);
                    break;
                case "mood":
                    SetCharacterMood(splitTag[1]);
                    break;
                case "scene":
                    LoadScene(splitTag[1]);
                    break;
            }
        }
    }
    
    void DisplayChoices()
    {
        foreach (Choice choice in story.currentChoices)
        {
            GameObject choiceButton = Instantiate(
                choiceButtonPrefab, 
                choiceButtonContainer
            );
            
            choiceButton.GetComponentInChildren().text = choice.text;
            
            int choiceIndex = choice.index;
            choiceButton.GetComponent

Advanced Unity Features

The Unity integration supports advanced features that enhance your narrative implementation:

Save System Integration

public class InkSaveManager : MonoBehaviour
{
    private Story story;
    private const string SAVE_KEY = "InkStorySave";
    
    public void SaveStory()
    {
        string saveData = story.state.ToJson();
        PlayerPrefs.SetString(SAVE_KEY, saveData);
        PlayerPrefs.Save();
    }
    
    public void LoadStory()
    {
        if (PlayerPrefs.HasKey(SAVE_KEY))
        {
            string saveData = PlayerPrefs.GetString(SAVE_KEY);
            story.state.LoadJson(saveData);
            
            // Continue from saved position
            DialogueManager.Instance.ContinueStory();
        }
    }
    
    public void DeleteSave()
    {
        PlayerPrefs.DeleteKey(SAVE_KEY);
        PlayerPrefs.Save();
    }
}

Localization Support

public class LocalizedInkManager : MonoBehaviour
{
    [System.Serializable]
    public class LocalizedInkFile
    {
        public SystemLanguage language;
        public TextAsset inkFile;
    }
    
    [SerializeField] private LocalizedInkFile[] localizedStories;
    private Dictionary storyDictionary;
    
    void Awake()
    {
        // Build dictionary for quick lookup
        storyDictionary = new Dictionary();
        foreach (var localizedFile in localizedStories)
        {
            storyDictionary[localizedFile.language] = localizedFile.inkFile;
        }
    }
    
    public TextAsset GetLocalizedStory()
    {
        SystemLanguage currentLanguage = Application.systemLanguage;
        
        if (storyDictionary.ContainsKey(currentLanguage))
        {
            return storyDictionary[currentLanguage];
        }
        
        // Fallback to English
        return storyDictionary[SystemLanguage.English];
    }
}

Godot Integration: Open Source Excellence

Godot's open-source nature and GDScript make it an excellent platform for InkGameScript integration. While there's no official Ink plugin, the community has created robust solutions.

Setting Up godot-ink

The most popular Ink integration for Godot is godot-ink. Here's how to set it up:

1. Download godot-ink from the Asset Library or GitHub
2. Extract the 'addons' folder to your project root
3. Enable the plugin in Project Settings → Plugins
4. Add InkStory nodes to your scenes

Basic Godot Implementation

extends Control

# UI References
onready var dialogue_label = $DialogueBox/DialogueText
onready var choice_container = $DialogueBox/ChoiceContainer
onready var speaker_label = $DialogueBox/SpeakerName

# Ink story resource
export(Resource) var ink_story

# Story instance
var story

# Choice button scene
var choice_button = preload("res://UI/ChoiceButton.tscn")

func _ready():
    # Initialize the story
    story = InkStory.new(ink_story)
    
    # Bind external functions
    story.bind_external_function("get_player_level", self, "_get_player_level")
    story.bind_external_function("give_item", self, "_give_item")
    
    # Observe variables
    story.observe_variable("player_mood", self, "_on_mood_changed")
    
    # Start the story
    continue_story()

func continue_story():
    # Clear previous choices
    for child in choice_container.get_children():
        child.queue_free()
    
    # Get story text
    var text = ""
    while story.can_continue():
        text += story.continue_story()
        _process_tags()
    
    # Animate text display
    _animate_text(text)
    
    # Handle choices
    if story.has_choices():
        _display_choices()
    elif not story.can_continue():
        _end_dialogue()

func _process_tags():
    for tag in story.current_tags:
        var parts = tag.split(":")
        match parts[0]:
            "speaker":
                speaker_label.text = parts[1]
            "emotion":
                _set_portrait_emotion(parts[1])
            "sound":
                _play_sound_effect(parts[1])
            "music":
                _change_background_music(parts[1])

func _display_choices():
    var index = 0
    for choice in story.current_choices:
        var button = choice_button.instance()
        choice_container.add_child(button)
        
        button.text = choice.text
        button.connect("pressed", self, "_on_choice_selected", [index])
        
        # Add keyboard shortcut
        if index < 9:
            var shortcut = ShortCut.new()
            var input_event = InputEventKey.new()
            input_event.scancode = KEY_1 + index
            shortcut.shortcut = input_event
            button.shortcut = shortcut
        
        index += 1

func _on_choice_selected(index):
    story.choose_choice_index(index)
    continue_story()

func _animate_text(text):
    dialogue_label.visible_characters = 0
    dialogue_label.text = text
    
    # Create tween for typewriter effect
    var tween = Tween.new()
    add_child(tween)
    
    tween.interpolate_property(
        dialogue_label, 
        "visible_characters",
        0, 
        text.length(),
        text.length() * 0.02  # Adjust speed as needed
    )
    
    tween.start()
    yield(tween, "tween_completed")
    tween.queue_free()

# External function implementations
func _get_player_level():
    return GameManager.player_level

func _give_item(item_name):
    GameManager.inventory.add_item(item_name)
    
func _on_mood_changed(variable_name, new_value):
    # Update UI or game state based on mood
    emit_signal("mood_changed", new_value)

Advanced Godot Features

Custom Ink Functions in GDScript

# Register complex game functions with Ink
func setup_ink_functions():
    # Combat function
    story.bind_external_function("roll_damage", self, "_roll_damage", 3)
    
    # Inventory checking
    story.bind_external_function("has_items", self, "_check_inventory", 2)
    
    # Quest system
    story.bind_external_function("complete_quest", self, "_complete_quest", 1)

func _roll_damage(min_damage, max_damage, weapon_bonus):
    var base_damage = randi() % (max_damage - min_damage + 1) + min_damage
    var total_damage = base_damage + weapon_bonus
    
    # Apply critical hit chance
    if randf() < 0.1:  # 10% crit chance
        total_damage *= 2
        emit_signal("critical_hit")
    
    return total_damage

func _check_inventory(item_names):
    # Check if player has all required items
    for item in item_names:
        if not GameManager.inventory.has_item(item):
            return false
    return true

func _complete_quest(quest_id):
    GameManager.quest_system.complete_quest(quest_id)
    # Return reward description for Ink to display
    var reward = GameManager.quest_system.get_quest_reward(quest_id)
    return reward.description

Visual Novel Style Implementation

extends Node2D

# Visual novel components
onready var background = $Background
onready var character_left = $Characters/LeftCharacter
onready var character_right = $Characters/RightCharacter
onready var dialogue_box = $UI/DialogueBox

# Transition effects
onready var fade_overlay = $UI/FadeOverlay
onready var screen_shake = $Effects/ScreenShake

# Character portrait mapping
var character_portraits = {
    "alice": preload("res://portraits/alice.png"),
    "bob": preload("res://portraits/bob.png"),
    "narrator": null
}

func _ready():
    story.bind_external_function("set_background", self, "_set_background")
    story.bind_external_function("show_character", self, "_show_character")
    story.bind_external_function("hide_character", self, "_hide_character")
    story.bind_external_function("screen_effect", self, "_screen_effect")

func _set_background(background_name, transition_type = "fade"):
    var new_background = load("res://backgrounds/" + background_name + ".png")
    
    match transition_type:
        "fade":
            _fade_transition(new_background)
        "slide":
            _slide_transition(new_background)
        "instant":
            background.texture = new_background

func _show_character(character_name, position = "left", emotion = "neutral"):
    var portrait_path = "res://portraits/%s_%s.png" % [character_name, emotion]
    var portrait = load(portrait_path)
    
    var character_sprite = character_left if position == "left" else character_right
    
    # Fade in character
    character_sprite.modulate.a = 0
    character_sprite.texture = portrait
    
    var tween = Tween.new()
    add_child(tween)
    tween.interpolate_property(
        character_sprite, 
        "modulate:a",
        0.0, 1.0, 0.5
    )
    tween.start()

Web Integration: JavaScript and inkjs

For web-based games and interactive fiction, inkjs provides a JavaScript runtime for Ink stories. This opens up integration possibilities with any web framework or vanilla JavaScript.

Basic Web Setup

First, include inkjs in your project:

<!-- Via CDN -->
<script src="https://unpkg.com/inkjs/dist/ink.min.js"></script>

<!-- Or via npm -->
<!-- npm install inkjs -->

Vanilla JavaScript Implementation

class InkGame {
    constructor(storyContent) {
        this.story = new inkjs.Story(storyContent);
        this.dialogueContainer = document.getElementById('dialogue');
        this.choicesContainer = document.getElementById('choices');
        
        // Bind external functions
        this.bindExternalFunctions();
        
        // Start the story
        this.continueStory();
    }
    
    bindExternalFunctions() {
        // Game state functions
        this.story.BindExternalFunction("GetPlayerScore", () => {
            return parseInt(localStorage.getItem('playerScore') || '0');
        });
        
        this.story.BindExternalFunction("AddScore", (points) => {
            const currentScore = this.GetPlayerScore();
            localStorage.setItem('playerScore', currentScore + points);
            this.updateScoreDisplay();
        });
        
        // Audio functions
        this.story.BindExternalFunction("PlaySound", (soundName) => {
            const audio = new Audio(`/sounds/${soundName}.mp3`);
            audio.play();
        });
        
        // Visual effects
        this.story.BindExternalFunction("ScreenFlash", (color, duration = 200) => {
            this.flashScreen(color, duration);
        });
    }
    
    continueStory() {
        // Clear previous content
        this.dialogueContainer.innerHTML = '';
        
        // Get all text until choices
        while (this.story.canContinue) {
            const text = this.story.Continue();
            this.displayText(text);
            
            // Process tags
            this.processTags();
        }
        
        // Handle choices or story end
        if (this.story.currentChoices.length > 0) {
            this.displayChoices();
        } else if (!this.story.canContinue) {
            this.endStory();
        }
    }
    
    displayText(text) {
        const paragraph = document.createElement('p');
        paragraph.classList.add('dialogue-text');
        
        // Animate text appearance
        paragraph.style.opacity = '0';
        paragraph.innerHTML = this.parseTextEffects(text);
        
        this.dialogueContainer.appendChild(paragraph);
        
        // Fade in animation
        setTimeout(() => {
            paragraph.style.transition = 'opacity 0.3s ease-in';
            paragraph.style.opacity = '1';
        }, 50);
    }
    
    parseTextEffects(text) {
        // Parse custom markup for text effects
        return text
            .replace(/\*\*(.*?)\*\*/g, '$1')
            .replace(/\*(.*?)\*/g, '$1')
            .replace(/\~(.*?)\~/g, '$1')
            .replace(/\^(.*?)\^/g, '$1');
    }
    
    processTags() {
        this.story.currentTags.forEach(tag => {
            const [command, ...args] = tag.split(':');
            
            switch (command) {
                case 'background':
                    this.setBackground(args[0]);
                    break;
                case 'music':
                    this.playMusic(args[0], args[1] === 'loop');
                    break;
                case 'character':
                    this.showCharacter(args[0], args[1]);
                    break;
                case 'wait':
                    this.pauseStory(parseInt(args[0]));
                    break;
            }
        });
    }
    
    displayChoices() {
        this.choicesContainer.innerHTML = '';
        
        this.story.currentChoices.forEach((choice, index) => {
            const button = document.createElement('button');
            button.classList.add('choice-button');
            button.textContent = choice.text;
            
            // Add keyboard navigation
            button.setAttribute('data-key', index + 1);
            
            button.addEventListener('click', () => {
                this.selectChoice(index);
            });
            
            this.choicesContainer.appendChild(button);
            
            // Animate choice appearance
            setTimeout(() => {
                button.classList.add('visible');
            }, index * 100);
        });
        
        // Enable keyboard selection
        this.enableKeyboardNavigation();
    }
    
    selectChoice(index) {
        // Disable further interaction
        this.choicesContainer.classList.add('disabled');
        
        // Make the choice
        this.story.ChooseChoiceIndex(index);
        
        // Continue story after brief delay
        setTimeout(() => {
            this.choicesContainer.classList.remove('disabled');
            this.choicesContainer.innerHTML = '';
            this.continueStory();
        }, 300);
    }
    
    enableKeyboardNavigation() {
        const keyHandler = (e) => {
            const keyNum = parseInt(e.key);
            if (keyNum >= 1 && keyNum <= this.story.currentChoices.length) {
                this.selectChoice(keyNum - 1);
                document.removeEventListener('keydown', keyHandler);
            }
        };
        
        document.addEventListener('keydown', keyHandler);
    }
    
    // Save/Load functionality
    saveGame(slot = 'autosave') {
        const saveData = {
            story: this.story.state.toJson(),
            timestamp: Date.now(),
            metadata: {
                currentScene: this.currentScene,
                playTime: this.getPlayTime()
            }
        };
        
        localStorage.setItem(`inkSave_${slot}`, JSON.stringify(saveData));
    }
    
    loadGame(slot = 'autosave') {
        const saveDataStr = localStorage.getItem(`inkSave_${slot}`);
        if (saveDataStr) {
            const saveData = JSON.parse(saveDataStr);
            this.story.state.LoadJson(saveData.story);
            this.currentScene = saveData.metadata.currentScene;
            this.continueStory();
            return true;
        }
        return false;
    }
}

// Initialize the game
fetch('/stories/main.json')
    .then(response => response.text())
    .then(storyContent => {
        const game = new InkGame(storyContent);
        window.inkGame = game; // Make available globally
    })
    .catch(error => {
        console.error('Failed to load story:', error);
    });

React Integration

For modern web applications using React, here's a complete Ink integration:

import React, { useState, useEffect, useCallback } from 'react';
import { Story } from 'inkjs';

// Custom hook for Ink story management
function useInkStory(storyContent) {
    const [story] = useState(() => new Story(storyContent));
    const [currentText, setCurrentText] = useState('');
    const [currentChoices, setCurrentChoices] = useState([]);
    const [tags, setTags] = useState([]);
    const [variables, setVariables] = useState({});
    
    // Continue story execution
    const continueStory = useCallback(() => {
        let text = '';
        const newTags = [];
        
        while (story.canContinue) {
            text += story.Continue();
            newTags.push(...story.currentTags);
        }
        
        setCurrentText(text);
        setTags(newTags);
        setCurrentChoices(story.currentChoices);
        
        // Update observed variables
        const vars = {};
        story.variablesState.forEach((value, name) => {
            vars[name] = value;
        });
        setVariables(vars);
    }, [story]);
    
    // Make a choice
    const makeChoice = useCallback((choiceIndex) => {
        story.ChooseChoiceIndex(choiceIndex);
        continueStory();
    }, [story, continueStory]);
    
    // Initialize story
    useEffect(() => {
        continueStory();
    }, [continueStory]);
    
    return {
        currentText,
        currentChoices,
        tags,
        variables,
        makeChoice,
        story
    };
}

// Main game component
function InkGame({ storyContent }) {
    const {
        currentText,
        currentChoices,
        tags,
        variables,
        makeChoice,
        story
    } = useInkStory(storyContent);
    
    const [background, setBackground] = useState('default');
    const [speaker, setSpeaker] = useState('');
    
    // Process tags
    useEffect(() => {
        tags.forEach(tag => {
            const [command, value] = tag.split(':');
            
            switch (command) {
                case 'background':
                    setBackground(value);
                    break;
                case 'speaker':
                    setSpeaker(value);
                    break;
                default:
                    break;
            }
        });
    }, [tags]);
    
    // Bind external functions
    useEffect(() => {
        story.BindExternalFunction("GetItem", (itemName) => {
            const inventory = JSON.parse(
                localStorage.getItem('inventory') || '[]'
            );
            inventory.push(itemName);
            localStorage.setItem('inventory', JSON.stringify(inventory));
            return true;
        });
        
        story.BindExternalFunction("HasItem", (itemName) => {
            const inventory = JSON.parse(
                localStorage.getItem('inventory') || '[]'
            );
            return inventory.includes(itemName);
        });
    }, [story]);
    
    return (
        <div className={`ink-game background-${background}`}>
            <div className="dialogue-container">
                {speaker && <div className="speaker">{speaker}</div>}
                <div className="dialogue-text">
                    {currentText}
                </div>
            </div>
            
            {currentChoices.length > 0 && (
                <div className="choices-container">
                    {currentChoices.map((choice, index) => (
                        <button
                            key={index}
                            className="choice-button"
                            onClick={() => makeChoice(index)}
                        >
                            {choice.text}
                        </button>
                    ))}
                </div>
            )}
            
            <div className="game-stats">
                <div>Health: {variables.player_health || 100}</div>
                <div>Gold: {variables.player_gold || 0}</div>
            </div>
        </div>
    );
}

// App component
function App() {
    const [storyContent, setStoryContent] = useState(null);
    const [loading, setLoading] = useState(true);
    
    useEffect(() => {
        fetch('/stories/main.json')
            .then(res => res.text())
            .then(content => {
                setStoryContent(content);
                setLoading(false);
            })
            .catch(error => {
                console.error('Failed to load story:', error);
                setLoading(false);
            });
    }, []);
    
    if (loading) {
        return <div>Loading story...</div>;
    }
    
    if (!storyContent) {
        return <div>Failed to load story</div>;
    }
    
    return <InkGame storyContent={storyContent} />;
}

export default App;

Custom Engine Integration

If you're working with a custom engine or less common framework, you can still integrate InkGameScript by understanding the core integration principles.

Core Integration Requirements

Any Ink integration needs to handle these core features:

  1. Story Loading: Parse compiled Ink JSON
  2. Text Retrieval: Get narrative text from the story
  3. Choice Handling: Present and select choices
  4. State Management: Save/load story state
  5. Variable Binding: Sync story and game variables
  6. External Functions: Call game code from Ink

Generic Integration Pattern

// Example C++ integration pattern
class InkIntegration {
private:
    void* storyHandle;
    std::map<std::string, std::function<InkValue(std::vector<InkValue>)>> externalFunctions;
    
public:
    InkIntegration(const std::string& storyJson) {
        // Initialize Ink runtime (implementation depends on binding)
        storyHandle = CreateInkStory(storyJson);
    }
    
    std::string Continue() {
        return InkContinue(storyHandle);
    }
    
    std::vector<Choice> GetCurrentChoices() {
        std::vector<Choice> choices;
        int count = InkGetChoiceCount(storyHandle);
        
        for (int i = 0; i < count; i++) {
            choices.push_back({
                InkGetChoiceText(storyHandle, i),
                i
            });
        }
        
        return choices;
    }
    
    void MakeChoice(int index) {
        InkChooseChoice(storyHandle, index);
    }
    
    void BindExternalFunction(
        const std::string& name, 
        std::function<InkValue(std::vector<InkValue>)> func
    ) {
        externalFunctions[name] = func;
        InkBindExternalFunction(storyHandle, name, 
            [](const char* funcName, InkValue* args, int argCount) -> InkValue {
                // Call the bound C++ function
                auto& functions = GetInstance()->externalFunctions;
                if (functions.find(funcName) != functions.end()) {
                    std::vector<InkValue> argVec(args, args + argCount);
                    return functions[funcName](argVec);
                }
                return InkValue();
            }
        );
    }
    
    std::string SaveState() {
        return InkSaveState(storyHandle);
    }
    
    void LoadState(const std::string& savedState) {
        InkLoadState(storyHandle, savedState);
    }
};

Performance Considerations

When integrating InkGameScript with any engine, keep these performance tips in mind:

1. Lazy Loading for Large Stories

Split large narratives into chunks that load on demand:

class ChunkedStoryLoader {
    constructor() {
        this.loadedChunks = new Map();
        this.currentChunk = null;
    }
    
    async loadChunk(chunkName) {
        if (!this.loadedChunks.has(chunkName)) {
            const response = await fetch(`/stories/chunks/${chunkName}.json`);
            const chunkData = await response.text();
            this.loadedChunks.set(chunkName, chunkData);
        }
        
        return this.loadedChunks.get(chunkName);
    }
    
    async switchToChunk(chunkName) {
        const chunkData = await this.loadChunk(chunkName);
        // Reinitialize story with new chunk
        this.currentChunk = new inkjs.Story(chunkData);
        
        // Restore relevant state
        this.restoreSharedState();
    }
}

2. Optimize External Function Calls

Cache results of expensive external functions when possible:

private Dictionary<string, object> functionCache = new Dictionary<string, object>();

private object CachedExternalFunction(string functionName, Func<object> function) {
    if (!functionCache.ContainsKey(functionName)) {
        functionCache[functionName] = function();
    }
    return functionCache[functionName];
}

// Use in binding
story.BindExternalFunction("GetExpensiveCalculation", () => {
    return CachedExternalFunction("expensive_calc", () => {
        // Perform expensive calculation
        return PerformComplexCalculation();
    });
});

3. Batch UI Updates

When displaying text, batch updates to reduce rendering overhead:

class BatchedTextDisplay {
    constructor() {
        this.pendingText = [];
        this.updateScheduled = false;
    }
    
    addText(text) {
        this.pendingText.push(text);
        
        if (!this.updateScheduled) {
            this.updateScheduled = true;
            requestAnimationFrame(() => this.flushText());
        }
    }
    
    flushText() {
        const combinedText = this.pendingText.join('\n');
        document.getElementById('dialogue').innerHTML = combinedText;
        
        this.pendingText = [];
        this.updateScheduled = false;
    }
}

Testing Your Integration

Comprehensive testing ensures your Ink integration works correctly across all scenarios:

Integration Test Suite

// Example test suite for Ink integration
describe('InkGameScript Integration', () => {
    let game;
    
    beforeEach(() => {
        game = new InkGame(testStoryContent);
    });
    
    test('Should load and display initial text', () => {
        expect(game.currentText).toContain('Welcome to the story');
    });
    
    test('Should handle choices correctly', () => {
        game.makeChoice(0);
        expect(game.currentText).toContain('You chose the first option');
    });
    
    test('Should bind external functions', () => {
        const result = game.story.EvaluateFunction('TestFunction', [5, 10]);
        expect(result).toBe(15);
    });
    
    test('Should save and load state', () => {
        game.makeChoice(0);
        const savedState = game.save();
        
        // Reset game
        game = new InkGame(testStoryContent);
        game.load(savedState);
        
        expect(game.currentText).toContain('You chose the first option');
    });
    
    test('Should handle variable changes', () => {
        game.story.variablesState['player_score'] = 100;
        game.continueStory();
        
        expect(game.currentText).toContain('Your score: 100');
    });
});

Conclusion

Integrating InkGameScript with your game engine of choice opens up powerful possibilities for narrative-driven experiences. Whether you're using Unity's robust C# integration, Godot's flexible GDScript approach, or building a web-based experience with JavaScript, the principles remain the same: load your story, handle text and choices, manage state, and bridge your narrative with your game logic.

Remember that the best integration is one that feels seamless to both developers and players. Take time to polish the connection between your narrative and gameplay, ensuring that story moments enhance rather than interrupt the gaming experience. With the techniques and examples provided in this guide, you're well-equipped to create compelling narrative experiences in any game engine.

Happy integrating, and may your stories come to life in exciting new ways!

Integration Unity Godot JavaScript Game Development