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:
// Split game states out into dedicated sub-scripts
require("src/update.nut")
require("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:
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:
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(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
.dlland tries an optionallibprefix. - Linux: Appends
.so,.so.0, and tries an optionallibprefix.
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.KeyWfor moving forward, a US user presses W and a French user presses Z. The physical hand posture remains identical across all layout variants globally.
- If you use
-
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.KeyMwould place the map shortcut at an unexpected position on an azerty layout. UsingInput.keychar_to_key("m")guarantees the letter M triggers the action regardless of where it lives on the user's keyboard.
- If pressing the letter M should open the Map,
hardcoding
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.GamepadSlotsreturns the count of virtual slots available. - Logical slot (
slot): A stable virtual input channel mapped to a specific gameplay character (e.g., Slot0for Player 1, Slot1for 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)andInput.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)andInput.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:
-
Initialization: Create a target buffer using
Image.create_render_target(width, height)and allocate an isolated context withImGui.create_context(). -
Render Pass: Route rendering to the target buffer using
Canvas.push_render_target(), bind the offscreen context withImGui.set_context(context), inject frame delta time withImGui.io_set("deltatime", dt), update virtual dimensions viaImGui.io_set("DisplaySize{X,Y}", value), manually drive the frame lifecycle withImGui.new_frame()/ImGui.render(), then pop the target buffer withCanvas.pop_render_target(). -
Composition: Restore the primary context by calling
window.activate(). Draw the offscreen target image onto the canvas withimage.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()orSystem.spawn_vm().