Skip to content

Advanced Usage

Ghost in a Nutshell

Welcome to the advanced documentation of Nutshell.

While the basic framework handles straightforward, single-window games, scaling a project requires mastering the underlying runtime environment.

Scripts & Assets

Script Modularization with require(path)

The require(path) utility allows you to modularize your code by loading and executing external scripts from within your current script.

This function is registered globally within the Squirrel root table of the active virtual machine. You can use it to keep your codebase clean, decoupled, and scaling beyond a single main.nut file:

main.nut
// Split game states out into dedicated sub-scripts
require("src/update.nut")
require("src/draw.nut")
src/update.nut
function update(dt) {
    // Process game logic and state changes here
}
src/draw.nut
function draw() {
    // Render visual elements here
    Font().draw(10, 10, "Hello Nutshell!")
}

Paths

When loading an image, loading an audio file, or requiring external scripts, file paths are resolved by Nutshell using specific rules.

Let's assume your project uses the following directory structure:

/path/to/my/game
├── main.nut
├── audio
│   ├── crunch.ogg
│   └── background-theme.ogg
├── images
│   ├── acorn.png
│   ├── forest.png
│   └── squirrel.png
└── src
    ├── draw.nut
    └── update.nut

Relative paths

By default, assets are resolved via a path relative to the script that executes the load call.

Relative Path Example

To reference acorn.png from inside src/update.nut, you must look up one directory level using ../images/acorn.png.

Using images/acorn.png would cause Nutshell to search for src/images/acorn.png, which does not exist.

Absolute paths

You can also use system-specific absolute paths to target files anywhere on the host machine.

Absolute Path Example

If you want to load a system font directly from a Linux directory to display localized Arabic text, you can specify its full absolute path:

local sampleFont = Font("/usr/share/fonts/truetype/noto/NotoSansArabic-Regular.ttf");
sampleFont.draw(10, 10, "مرحبا بالعالم")

Project Root & Resource Paths (res://)

The Project Root is explicitly defined as the directory containing your game's entry point, main.nut.

To reference files from anywhere in your codebase without writing complex relative paths, you can use a Resource Path. A resource path bypasses the current script's location and evaluates paths relative to the project root using the res:// prefix.

This approach is highly recommended because it prevents broken paths if you decide to reorganize or move scripts into different subfolders later.

Resource Path Example

To safely reference acorn.png inside src/update.nut, you can use res://images/acorn.png.

If you move update.nut to a different folder later, the asset reference remains perfectly valid and unbroken.

Dynamic Resolution: System.resolve_path()

While relative tracks and the res:// protocol handle the vast majority of asset loading use cases, you may occasionally need to inspect or dynamically manipulate paths at runtime—especially when writing reusable plugins, shared libraries, or advanced logging tools.

System.resolve_path() automatically detects the caller's script context via extracts its directory, and resolves relative paths accurately. It handles cross-platform path separators (\ vs /) and seamlessly bridges standard OS paths with resource paths.

The simplest and most common use case for this is loading adjacent data configurations. If you separate your game logic into isolated modules (like an achievement manager or an enemy spawner), you often want that script's data files to live in the exact same folder so everything stays organized.

Loading Adjacent Data Files

Imagine you have an enemy setup script (src/entities/enemy.nut) and you want it to load a balance configuration file (src/entities/enemy_stats.json) that sits in the exact same directory:

// Inside src/entities/enemy.nut
local dataPath = System.resolve_path("enemy_stats.json");
print("Loading stats from: " + dataPath + "\n");

By using System.resolve_path(), you can safely move the entire entities folder anywhere else in your project tree later on, and the script will never break or lose track of its data file.

Fonts

Font paths follow usual path management.

However, you can use font aliases to safely request system or user-installed fonts without hardcoding physical filesystem tracks.

No fallbacks

Nevertheless, if you specify any path or alias for a font, the font must exist on the user's system. Otherwise, an exception will be thrown. The paths array handles character/glyph-level fallbacks, not file-missing fallbacks.

Embedded font

The only font guaranteed to always be available is the Bitstream Vera Sans embedded font.

Cross-platform font loading

This is highly recommended for cross-platform games, as absolute font paths vary wildly between operating systems (e.g., /usr/share/fonts/ on Linux vs C:\Windows\Fonts\ on Windows) and predicting font availability across systems is unreliable.

Here is a robust example on how to query platform-specific system fonts and safely fall back to the embedded asset if a file is missing or corrupted:

function loadPlatformFont(fontSize) {
    // Default to an empty string to gracefully target the embedded font
    local alias = ""

    switch (System.os) {
        case "windows":
            alias = "Arial"
            break
        case "linux":
            alias = "DejaVu Sans"
            break
        case "macos":
        case "darwin":
            alias = "Helvetica"
            break
    }

    try {
        // Attempt to load the preferred system font
        return Font(alias, fontSize)
    } catch (exception) {
        // If the font doesn't exist, load the embedded font
        return Font(fontSize)
    }
}
local sampleFont = loadPlatformFont(20)

Primary Fonts

When you provide an array of paths or aliases, the first font in the list is designated as the primary font.

Nutshell uses this primary font to calculate the overall line height, baseline, ascent, and descent for the text. Think of it as creating an invisible bounding box based entirely on the first font's design.

If the primary font is missing a character (like a Japanese Kanji or an Arabic glyph), Nutshell pulls that character's shape from one of the available fallbacks fonts. However, it forces that fallback character to fit inside the primary font's invisible bounding box.

Mixing standard and vector fonts

If your primary font is a compact pixel-art font (like m5x7) loaded at size 48, its bounding box is very small.

If your fallback font is a standard vector font (like Noto Sans), its characters at size 48 are physically much taller. Forcing the large vector glyphs into the tight pixel-font bounding box will cause the fallback text to appear vertically misaligned.

Multi face fonts best practices

Match Styles: Use fallback fonts that share similar vertical proportions. If your primary font is a tiny pixel font, use a dedicated pixel-art fallback font (like Unifont) for CJK characters.

Order Matters: If you place the larger vector font first, its generous bounding box easily accommodates smaller fallback fonts without clipping (though any characters present in both fonts will default to the first one).

Font Modifiers Syntax

If you use an alias, you can specify custom weight or style modifiers by appending a colon (:) followed by the style= keyword:

Family Name:style=Modifier

Font aliases and modifiers

Some examples:

  • Standard Font Alias: Liberation Sans
  • Explicit Style Modifier: DejaVu Sans:style=ExtraLight
  • Combined Modifiers: Liberation Sans:style=BoldItalic

Images

Rendering Performance

When rendering images in Nutshell, choosing between standard rendering (draw), batched rendering (draw_batch / draw_batch_compact), and stateful batching (SpriteBatch) depends entirely on your target object count and performance requirements.

Method Performance Best Used For Pros & Cons
Image.draw Standard UI elements, static background items, or low-count entities (< 50 sprites per frame). Pros: Extremely easy to use, highly flexible.
Cons: High CPU overhead per call due to table creation and individual native boundary crossings.
SpriteBatch.draw High Tilemaps, entity managers (pools of enemies/bullets), and UI layers requiring persistent state. Pros: Easy object-oriented API, persistent state, automatic Z-sorting, global transforms (camera).
Cons: Slight CPU overhead compared to raw arrays, accepts an initial capacity hint for pre-allocation, but auto-expands dynamically if exceeded.
Image.draw_batch Very High Complex particle systems, bullets, or dynamic objects requiring unique source rectangles, per-axis scaling, or explicit individual color channels. Pros: Single native call overhead, supports full transformation/source rect control.
Cons: Larger memory footprint per record, requires manual array/blob manipulation every frame.
Image.draw_batch_compact Maximum Massive particle fields, dense bullet hell projectiles, or tilemaps (hundreds to thousands of sprites). Pros: Maximum performance, lowest memory overhead (3x smaller than full batch).
Cons: Uniform scaling only, packed RGB color, shares default origin coordinates across the batch, requires manual state management.

Standard rendering

Use image.draw when:

  • You are rendering UI screens, HUD elements, menus, or low-frequency foreground objects.
  • You only need to draw a handful of sprites per frame where per-frame overhead is negligible.
  • Code readability and simple table-based option overrides are preferred over raw performance.
local player = Image("res://assets/player.png")
player.draw(100, 200, { angle = 45, color = "red" })

Batch rendering

Standard batch

Use image.draw_batch when:

  • You are rendering moderate-to-high quantities of sprites (e.g., 100+ items) that require complex individual properties.
  • Different elements in the same batch need distinct source rectangles (e.g., a sprite sheet atlas of mixed tiles or animation frames) or separate per-axis scaling.

Array construction Example:

local bulletSheet = Image("res://assets/bullets.png")
local batchData = []

// 1st record
batchData.extend([
    100.0, 200.0,           // x, y
    0.0, 0.0,               // src_x, src_y
    32.0, 32.0,             // src_w, src_h
    32.0, 32.0,             // width, height
    1.0, 1.0,               // scale_x, scale_y
    0.0,                    // angle
    16.0, 16.0,             // origin_x, origin_y
    255.0, 0.0, 0.0, 255.0, // r, g, b, a (Red)
    Image.BlendNormal       // flags
])

// 2nd record
batchData.extend([
    150.0, 200.0,           // x, y
    32.0, 0.0,              // src_x, src_y (Different atlas frame)
    32.0, 32.0,             // src_w, src_h
    32.0, 32.0,             // width, height
    1.5, 1.5,               // scale_x, scale_y
    90.0,                   // angle
    16.0, 16.0,             // origin_x, origin_y
    0.0, 255.0, 0.0, 255.0, // r, g, b, a (Green)
    Image.BlendNormal       // flags
])

bulletSheet.draw_batch(batchData)

Blob construction Example:

local bulletSheet = Image("res://assets/bullets.png")
local recordCount = 2
local batchBlob = blob(Image.BatchRecordSize * recordCount)

// 1st record
batchBlob.writen(100.0, 'f')                // x
batchBlob.writen(200.0, 'f')                // y
batchBlob.writen(0.0,   'f')                // src_x
batchBlob.writen(0.0,   'f')                // src_y
batchBlob.writen(32.0,  'f')                // src_w
batchBlob.writen(32.0,  'f')                // src_h
batchBlob.writen(32.0,  'f')                // width
batchBlob.writen(32.0,  'f')                // height
batchBlob.writen(1.0,   'f')                // scale_x
batchBlob.writen(1.0,   'f')                // scale_y
batchBlob.writen(0.0,   'f')                // angle
batchBlob.writen(16.0,  'f')                // origin_x
batchBlob.writen(16.0,  'f')                // origin_y
batchBlob.writen(255.0, 'f')                // r
batchBlob.writen(0.0,   'f')                // g
batchBlob.writen(0.0,   'f')                // b
batchBlob.writen(255.0, 'f')                // a
batchBlob.writen(Image.BlendNormal, 'f')    // flags

// 2nd record
batchBlob.writen(150.0, 'f')                // x
batchBlob.writen(200.0, 'f')                // y
batchBlob.writen(32.0,  'f')                // src_x
batchBlob.writen(0.0,   'f')                // src_y
batchBlob.writen(32.0,  'f')                // src_w
batchBlob.writen(32.0,  'f')                // src_h
batchBlob.writen(32.0,  'f')                // width
batchBlob.writen(32.0,  'f')                // height
batchBlob.writen(1.5,   'f')                // scale_x
batchBlob.writen(1.5,   'f')                // scale_y
batchBlob.writen(90.0,  'f')                // angle
batchBlob.writen(16.0,  'f')                // origin_x
batchBlob.writen(16.0,  'f')                // origin_y
batchBlob.writen(0.0,   'f')                // r
batchBlob.writen(255.0, 'f')                // g
batchBlob.writen(0.0,   'f')                // b
batchBlob.writen(255.0, 'f')                // a
batchBlob.writen(Image.BlendNormal, 'f')    // flags

bulletSheet.draw_batch(batchBlob)
Compact batch

Use image.draw_batch_compact when:

  • Performance is paramount and you are pushing thousands of sprites per frame (e.g., particle engines, rain/snow effects, massive bullet hell patterns).
  • All sprites share uniform scaling and can use a unified anchor origin.
  • You want to minimize memory footprint by compressing record sizes to the minimum.

Because of its reduced footprint, draw_batch_compact extracts the RGB color payload from a single float (0xRRGGBB), and evaluates the alpha channel out of the flag bits.

Array construction Example:

local particle = Image("res://assets/particle.png")
local batchData = []

local redPacked = 0xFF0000
local bluePacked = 0x0000FF
local standardFlags = Image.BlendNormal | (255 << 8) // Alpha 255 in bits 8-15

// 1st record
batchData.extend([
    150.0, 250.0,  // x, y
    0.0,           // angle
    2.0,           // scale
    redPacked,     // rgb_color
    standardFlags  // flags
])

// 2nd record
batchData.extend([
    300.0, 400.0,  // x, y
    45.0,          // angle
    1.0,           // scale
    bluePacked,    // rgb_color
    standardFlags  // flags
])

particle.draw_batch_compact(batchData)

Blob construction Example:

local particle = Image("res://assets/particle.png")
local recordCount = 2
local batchBlob = blob(Image.BatchCompactRecordSize * recordCount)

local redPacked = 0xFF0000
local bluePacked = 0x0000FF
local standardFlags = Image.BlendNormal | (255 << 8)

// 1st record
batchBlob.writen(150.0,         'f')    // x
batchBlob.writen(250.0,         'f')    // y
batchBlob.writen(0.0,           'f')    // angle
batchBlob.writen(2.0,           'f')    // scale
batchBlob.writen(redPacked,     'f')    // rgb_color
batchBlob.writen(standardFlags, 'f')    // flags

// 2nd record
batchBlob.writen(300.0,         'f')    // x
batchBlob.writen(400.0,         'f')    // y
batchBlob.writen(45.0,          'f')    // angle
batchBlob.writen(1.0,           'f')    // scale
batchBlob.writen(bluePacked,    'f')    // rgb_color
batchBlob.writen(standardFlags, 'f')    // flags

particle.draw_batch_compact(batchBlob)
Stateful Batch

Use the SpriteBatch class when:

  • You want to avoid the complexity of managing and updating raw arrays or blobs.
  • You are building tilemaps, complex entity managers (like a pool of enemies or bullets), or UI layers where individual items need to be updated, moved, or deleted independently without rebuilding an entire data array from scratch.

It has persistent state: Sprites remember their position, scale, and color until you explicitly change or remove them. With automatic Z-Sorting, every sprite is assigned a Z-depth value. Nutshell automatically sorts them before rendering, ensuring UI or foreground elements always draw on top of backgrounds regardless of the order they were added. Finally, it has global transforms: You can move, scale, or rotate the entire batch at once using set_position(), set_scale(), and set_rotation().

Use this class for tilemaps, complex entity managers (like a pool of enemies or bullets), and UI layers where individual items need to be updated, moved, or deleted independently without rebuilding an entire data array from scratch.

local tileset = Image("res://assets/world_tiles.png")

// Create a batch capable of holding up to 500 sprites, using a 32x32 pixel grid
local mapBatch = SpriteBatch(tileset, 500, 32, 32)

// Generate a 10x10 grass background (Tile ID 0) at Z-depth 0.0
for (local y = 0; y < 10; y++) {
    for (local x = 0; x < 10; x++) {
        mapBatch.add_tile(0, x * 32, y * 32)
    }
}

// Add some trees (Tile ID 5) overlapping the grass at Z-depth 10.0
mapBatch.add_tile(5, 64, 64, 10.0)
mapBatch.add_tile(5, 200, 128, 10.0)

// Apply a global sunset tint to the entire map (orange-ish multiplier)
mapBatch.set_color(1.0, 0.6, 0.4, 1.0)

function update(dt) {}

function draw() {
    // Draws all 102 tiles efficiently in a single native call.
    // The trees will automatically render on top of the grass because of their
    // higher Z-depth, regardless of the order they were added to the batch.
    mapBatch.draw()
}

Emoji Fonts

To maintain high rendering performance and cross-platform consistency, Nutshell handles emoji characters using pre-rendered emoji atlases rather than parsing complex vector color font formats directly at runtime.

An emoji atlas consists of a directory containing a JSON manifest and one or more packed PNG image sheets.

The Emoji Atlas Structure

When Nutshell looks up an emoji font, it scans the atlas directory for a manifest.json file alongside the texture pages.

You can organize your assets to support either a single explicit layout size or multiple size variants nested under subdirectories (e.g., 32/, 64/).

A valid atlas directory must contain the following components:

  • manifest.json: A metadata file specifying the cell size and mapping individual emoji characters to their exact pixel coordinates and page indices.

  • Texture Pages (1.png, 2.png, etc.): High-density sprite sheets containing the emoji glyphs.

The manifest.json format follows this structural schema:

{
  "size": 32,
  "emojis": {
    "😀": [0, 0, 1],
    "👍": [34, 0, 1],
    "🚀": [0, 34, 2]
  }
}

The array payload represents [x, y, page_number] respectively. Nutshell automatically normalizes these inputs and handles variation selectors (like \uFE0F) to ensure reliable character lookups.

Native Plugins with load(path)

While require() handles Squirrel script modularization, the load() function allows you to load and execute native shared library plugins (.dll, .so, .dylib) directly into the active virtual machine.

This is essential for performance-critical systems, custom rendering logic, or bridging third-party C/C++ libraries into Nutshell.

Loading a Native Plugin

To load a native plugin named arithmetics, you simply call:

load("arithmetics")

load(path) uses the standard Nutshell path resolution rules. You can provide a relative path or use the res:// prefix for project-root resolution.

You do not need to specify the file extension or the platform-specific lib prefix. If you omit the extension, Nutshell automatically builds a list of candidate filenames to try based on the host operating system:

  • Windows: Appends .dll and tries an optional lib prefix.
  • Linux: Appends .so, .so.0, and tries an optional lib prefix.

For example, calling load("physics") seamlessly resolves to libphysics.so on Linux and physics.dll on Windows.

Creating Nutshell compatible plugins is discussed here.

Input

Terminal Input

Building text adventures or command-line interfaces in Nutshell requires handling terminal input without stalling the update loop. Because forcing the engine to wait for a user to type would freeze the entire application, terminal reading is strictly non-blocking.

You can capture terminal input using the Input.terminal_read() method. There are two primary ways to handling this in your Squirrel scripts: event-driven or linear.

Event-Driven

For non-linear games, the event-driven approach is the simplest. You check for input every frame and react to the command immediately.

local currentRoom = "Dungeon"

function update(dt) {
    local text = Input.terminal_read()

    // Process input only when the user has submitted a line
    if (text != null) {
        if (text == "look") {
            print("It's dark and damp in here.\n")
        } else if (text == "go north") {
            currentRoom = "Hallway"
            print("You walk into the Hallway.\n")
        } else {
            print("I don't understand '" + text + "'.\n")
        }
    }
}

Linear

If you prefer to write your story top-to-bottom, you can use Squirrel's threads (or coroutines) to suspend the execution of your script while waiting for input, and wake it up when Input.terminal_read() captures a string.

Because Squirrel threads return to an "idle" state when they finish executing, you can track whether the thread has already been started to detect when your story ends.

local currentInput = null
local adventureThread = null
local adventureStarted = false

// Blocking read function
function readln() {
    // Suspend execution. The update loop will only wake it when input is ready
    suspend()

    local text = currentInput
    currentInput = null
    return text
}

// Linear story script
function story() {
    print("What is your name?\n> ")
    local name = readln()

    print("Welcome, " + name + "!\n")
    print("What is your quest?\n> ")
    local quest = readln()

    print("Good luck with: " + quest + "\n")

    // The function ends here, returning the thread to an "idle" state
}

// Initialize the story thread
local adventureThread = newthread(story)

// The engine loop manages the thread state
function update(dt) {
    if (!adventureThread) {
        // Thread is gone, do nothing
        return
    }

    local status = adventureThread.getstatus()

    if (status == "idle") {
        if (!adventureStarted) {
            // Start the thread for the first time
            adventureStarted = true
            adventureThread.call()
        } else {
            // Thread is idle but was already started: function has ended
            adventureThread = null
            // End Nutshell execution
            System.exit(0)
        }
    } else if (status == "suspended") {
        // Fetch input
        local text = Input.terminal_read()
        if (text != null) {
            // If text was actually submitted, wake the thread
            currentInput = text
            adventureThread.wakeup()
        }
    }
}

Keyboard Layouts

Nutshell handles keyboard inputs using a physical mapping architecture based on a standard US qwerty blueprint layout.

When a script checks for Input.KeyW, it does not care about the literal letter W. It evaluates the physical switch located on the keyboard grid.

Positional vs. Mnemonic Key Controls

When designing inputs, group your actions into two distinct categories:

  • Positional Controls (Movement): Use the raw Input.Key* constants directly.

    • If you use Input.KeyW for moving forward, a US user presses W and a French user presses Z. The physical hand posture remains identical across all layout variants globally.
  • Mnemonic Controls (UI Toggles, Hotkeys): Use Input.keychar_to_key() to dynamically resolve the keycap letter.

    • If pressing the letter M should open the Map, hardcoding Input.KeyM would place the map shortcut at an unexpected position on an azerty layout. Using Input.keychar_to_key("m") guarantees the letter M triggers the action regardless of where it lives on the user's keyboard.

Operating System Dead Keys (Linux / X11)

Certain international layout slots are treated as low-level hardware text composers by specific operating systems (for example, the ^ / ¨ circumflex key directly to the right of P on French azerty setups).

On Linux environments like Debian, the desktop display manager intercepts these dead keys to construct accent characters (like ê). Because this consumes the event before it can dispatch down to Nutshell, Nutshell cannot detect them as instant discrete button presses.

Gamepads

To handle local multiplayer and hot-plugging smoothly, Nutshell splits gamepad processing into a physical layer and a virtual gameplay layer.

Understanding the distinction between hardware indices, logical slots, and hardware offsets is useful for building custom lobbies or rebinding screens.

Hardware Index vs. Logical Slot

First, some notions:

  • Hardware count: Input.gamepad_count() returns the count of currently connected physical controller.
  • Hardware index (hw): The raw physical port identifier assigned sequentially as gamepads are connected or disconnected.
  • Logical slot count: Input.GamepadSlots returns the count of virtual slots available.
  • Logical slot (slot): A stable virtual input channel mapped to a specific gameplay character (e.g., Slot 0 for Player 1, Slot 1 for Player 2).

Gameplay logic should always query the logical slot rather than a raw hardware index.

You bridge these two layers using Input.gamepad_reassign_slot(hw, slot) to route a physical controller's data to a virtual slot.

The Hardware Offset

Standard input functions like Input.gamepad_button_pressed(btn, slot) expect a valid logical slot index.

When a physical controller is unassigned, querying its slot via Input.gamepad_slot(hw) returns -1.

To poll inputs directly from unassigned physical hardware, you must use a virtual hardware offset slot located at Input.GamepadSlots + hw.

Passing an index equal to or greater than Input.GamepadSlots signals Nutshell to bypass the logical mapping table completely and read the raw button state straight from that specific physical hardware port.

Ready player one?

This offset allows you to implement a standard "Press Start to Join" system by listening exclusively to unassigned controllers:

// Scan through all physical hardware ports
for (local hw = 0; hw < Input.gamepad_count(); hw++) {

    // Process only if this hardware device is currently unassigned (-1)
    if (Input.gamepad_slot(hw) == -1) {

        // Calculate the hardware bypass index
        local offset_slot = Input.GamepadSlots + hw;
        local pressed = false;

        // Listen for any button press on this specific unassigned controller
        for (local btn = 0; btn < Input.GamepadButtonCount; btn++) {
            if (Input.gamepad_button_pressed(btn, offset_slot)) {
                pressed = true;
                break;
            }
        }

        // Assign the physical device to the next available gameplay slot
        if (pressed && next_player_assignment_slot < Input.GamepadSlots) {
            if (Input.gamepad_reassign_slot(hw, next_player_assignment_slot)) {
                next_player_assignment_slot++;
            }
        }
    }
}

Gamepads vs. Raw Joysticks

Nutshell categorizes connected input hardware into two distinct runtime archetypes: Standard Gamepads and Raw Joysticks.

Standard modern gamepads (such as Xbox, PlayStation, or Nintendo Switch controllers) follow a predictable geometric shape. Because their physical layouts are almost identical, Nutshell automatically maps their inputs to semantic, layout-invariant constants like Input.GamepadButtonNorth (Y / Triangle) or Input.GamepadAxisLeftX.

Raw joysticks - such as flight simulation sticks, racing steering wheels, retro arcade decks, or unprofiled generic controllers - do not have a standardized layout. Instead of an ABXY diamond or dual thumbsticks, they report their inputs to the operating system as a flat sequence of integers: Axis 0, 1, 2... and Button 0, 1, 2....

Because a raw joystick's Button 0 is entirely hardware-dependent, querying it directly using standard gamepad constants would cause code conflicts (for example, the integer value for Input.GamepadButtonSouth is also 0).

Isolating your logic into two paths prevents your gamepad controls from misfiring when a raw joystick is bound.

Handling a generic raw joystick requires branching your code when the device type is identified, then passing raw integers into safety-wrapper functions to isolate their IDs.

  • Always use Input.is_joystick(slot) to determine if a connected slot requires a generic layout routing instead of a traditional standard layout.
  • Because you cannot guess how many buttons or axes an arbitrary flight stick or steering wheel possesses, query the device hardware capacities dynamically using Input.gamepad_axis_count(slot) and Input.gamepad_button_count(slot).
  • To safely look up inputs on a raw device without bleeding into standard gamepad maps, wrap your raw indices using Input.joystick_axis(axisIndex) and Input.joystick_button(buttonIndex). These generate shifted, unique identifiers safe to supply to standard polling functions.

ImGui

Dear ImGui provide Nutshell with immediate mode graphical user interface capabilities. While it is pretty straightforward to use, knowing how it integrates with the rendering pipeline can enable one to use it in interesting manners.

Contexts

Nutshell handles two kinds of ImGui contexts: native window and offscreen render targets.

Native window contexts

Each native window opened by Nutshell is automatically assigned an ImGui context. It automatically initialize frames and finalizes rendering. You can immediately (pun intended) execute ImGui widget calls.

In a multi-window context, ImGui context is automatically switched when window.activate() is called.

Native window lifecycle rules

Never execute ImGui.new_frame() and ImGui.render() inside their render loops.

Offscreen context

Contexts instantiated via ImGui.create_context() operate independently of native windows.

They require explicit lifecycle control through ImGui.new_frame() and ImGui.render(). They also require custom virtual input mapping and setting ImGui IO properties.

Interleaving pipeline

By default, Nutshell's automated ImGui handling renders the user interface after all other canvas draw commands.

To place an ImGui windows behind 2D sprites or specific canvas layers, you need to render the UI into an offscreen render target image to convert it into a drawable texture following these steps:

  1. Initialization: Create a target buffer using Image.create_render_target(width, height) and allocate an isolated context with ImGui.create_context().

  2. Render Pass: Route rendering to the target buffer using Canvas.push_render_target(), bind the offscreen context with ImGui.set_context(context), inject frame delta time with ImGui.io_set("deltatime", dt), update virtual dimensions via ImGui.io_set("DisplaySize{X,Y}", value), manually drive the frame lifecycle with ImGui.new_frame() / ImGui.render(), then pop the target buffer with Canvas.pop_render_target().

  3. Composition: Restore the primary context by calling window.activate(). Draw the offscreen target image onto the canvas with image.draw() prior to rendering foreground sprites.

Offscreen context configuration

Bounds clipping

When managing an offscreen context, you must explicitly define its resolution using ImGui.io_set("DisplaySizeX", width) and ImGui.io_set("DisplaySizeY", height).

Because the offscreen context is completely isolated from the native window, it has no inherent concept of screen size. ImGui requires these dimensions to accurately position elements that rely on screen edges, optimize rendering by ignoring widgets that fall outside the defined viewport and prevent users from dragging windows completely outside the boundaries of your offscreen render target.

Virtual input

Also, offscreen targets do not inherit global viewport coordinates. Input must be translated relative to the target's position on the screen, and bounds checks must strictly match the target dimensions to avoid ignoring mouse events or interacting with hidden elements.

You can avoid input bleeding (where interactions with a foreground window accidentally trigger background elements) by checking ImGui.io_get("WantCaptureMouse") when another context is active.

To feed input to the offscreen context, explicitly call ImGui.io_add_mouse_pos_event(x, y) and ImGui.io_add_mouse_button_event(button, state).

When the global mouse isn't hovering over the target's specific bounds, you must send out-of-bounds fake mouse events (e.g., -32768.0) and reset the button state to false to clear the active input state.

Frame timing

Finally, when managing an offscreen context, you must explicitly inject your Nutshell draw(dt) frame delta time into the IO struct using ImGui.io_set("deltatime", dt) before calling ImGui.new_frame().

Multi-Virtual Machine Architecture

By default, Nutshell initializes and runs a single virtual machine. However, you can spawn and run multiple independent virtual machines simultaneously using the System API.

Virtual Machine Initialization Rules

Every new virtual machine MUST be initialized with a valid target script path. These initialization scripts carry the same architectural requirements as your main entry point: they must define, at minimum, a global update(dt) function.

If a specific VM ID is requested during spawning, that ID MUST be unique in the current Nutshell application. Attempting to spawn a VM with an identifier that is already in use will result in a failure.

Inter-VM Communication

At the moment, you can't share data between two virtual machines.

The only actions you can take in a virtual machine are either to spawn or kill another virtual machine by its identifier.

Multi-Window Management

Nutshell supports multiple windows. You can spawn and configure multiple concurrent game windows using the Window API.

Contextual Limitations

A disclaimer though:

Windows are bound to their Parent VM

Nutshell windows are strictly bound to the specific virtual machine that spawned them.

A script running inside one virtual machine CANNOT access, manipulate, or draw to windows owned by a separate virtual machine.

Assets are bound to Windows

Image assets are cached and allocated explicitly per window context.

You CANNOT render an image or onto Window B if that asset resource was loaded while Window A was active. When loading assets, ensure the target window is actively set beforehand.

Active Window

An active window is where all current canvas drawing operations are actively directed.

  • To get the active window reference: Window.active().
  • To bind drawing operations to a specific window: window.activate().

Focused Window

A focused window is the one currently intercepting active OS hardware mouse, keyboard, or controller inputs.

  • To get the currently focused window: Window.focused_id().
  • To check if a window instance has the focus: window.focused().
  • To forcefully request focus for a specific window: window.focus().

Automatic Windows

By default, Nutshell automatically instantiates a new window with default dimensions whenever a new virtual machine is initialized.

You can pass the following command-line flags to the nutshell binary to alter or override this automatic window behavior:

Option Type Default Description
--title string nutshell Title string for automatic windows
--width int 640 Width for automatic windows
--height int 480 Height for automatic windows
--no-window bool false When set to true, disable the automatic creation of windows for new virtual machines.

Security Considerations

Script Execution Privileges

Nutshell does not currently sandbox script execution. Scripts have full access to the host filesystem via absolute paths and run with the same permissions as the compiled binary executable.

Additionally, native plugins loaded via load() execute raw machine code directly within the engine's process space. They bypass VM limitations, access all system resources, and can execute arbitrary OS commands natively.

  • Players: Only run games or binaries from creators you trust.
  • Developers: Never pass unsanitized player input, chat commands, or remote network payloads into functions like require() or System.spawn_vm().