Keel’s asset pipeline is built around three orthogonal concerns: loading (theDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/VKSFY/keel/llms.txt
Use this file to discover all available pages before exploring further.
AssetRegistry), watching for changes (the FileWatcher), and serializing world state (the Scene facade). All three are optional — setup_assets wires them together in one call, or you can use them independently.
Setup
setup_assets
AssetRegistry on app. Registers default loaders for .json (raw dict) and, when the 2D renderer is already active, image files (.png, .jpg, .jpeg, .bmp, .tga → TextureAtlas integer IDs). When watch_dirs is provided, starts a FileWatcher and registers a Phase.PRE_UPDATE poll system. Idempotent.
The Keel
App instance.List of directory paths to monitor for file changes. When provided, a
FileWatcher is created and its poll() method is called every Phase.PRE_UPDATE tick. Any registered asset whose source file changes is automatically reloaded.AssetRegistry inserted as a world resource.
AssetRegistry
AssetRegistry is the central handle-based asset cache. All paths are normalized to absolute form (os.path.normpath(os.path.abspath(path))) so that "./hero.png" and "/abs/path/hero.png" hit the same cache slot. Asset IDs are monotonically increasing and never reused after unload.
Loading assets
registry.load(path: str) -> AssetHandle
Load the asset at path and return a stable AssetHandle. If the path has already been loaded, the cached handle is returned without re-reading the file.
Raises:
AssetNotFoundError— path does not exist on diskNoLoaderError— no loader is registered for the file’s extension
registry.get(handle: AssetHandle) -> Any
Return the loaded asset object for handle. The return type depends on the loader:
- JSON loader →
dict - Texture loader →
int(texture ID in theTextureAtlas)
InvalidHandleError if the handle’s asset has been unloaded.
registry.reload(handle: AssetHandle) -> None
Re-invoke the loader for handle and replace the stored asset. Called automatically by FileWatcher.poll() for changed files. Can also be called manually for explicit cache invalidation.
registry.unload(handle: AssetHandle) -> None
Drop the asset for handle. Subsequent get or reload calls on this handle raise InvalidHandleError.
registry.handle_for_path(path: str) -> AssetHandle | None
Return the handle whose normalized path matches path, or None if it has not been loaded.
Registering custom loaders
registry.register_loader(extensions: list[str], loader_fn: Callable[[str], Any]) -> None
Register loader_fn as the handler for every extension in extensions. Extensions are normalized to lowercase with a leading dot. Overwrites any previously registered loader for the same extension.
Built-in loaders
| Extension(s) | Loaded type | Notes |
|---|---|---|
.json | dict | json.load |
.png, .jpg, .jpeg, .bmp, .tga | int (texture ID) | Requires setup_renderer_2d to be active; uploads to TextureAtlas via PIL |
Utility methods
AssetHandle
AssetHandle is an immutable, hashable reference to a loaded asset. Equality is by normalized path (not by id), so two handles for the same file are equal even if loaded in different calls.
Monotonically increasing integer assigned at load time. Unique per
AssetRegistry lifetime; never reused.The normalized absolute path the asset was loaded from.
The Python type of the loaded asset object (e.g.
dict, int). Informational only.Hot Reload
FileWatcher
FileWatcher wraps a watchdog Observer with a thread-safe queue.Queue. The watchdog thread only enqueues changed paths — all registry interaction (including GPU texture re-upload) happens on the main thread inside poll(), which is registered as a Phase.PRE_UPDATE system by setup_assets.
Methods
Start watching
directory recursively. Idempotent per directory. Raises RuntimeError if the watcher has been stopped.Stop watching
directory. No-op if it wasn’t being watched.Drain the event queue on the calling thread and reload any assets whose source files changed. Returns the number of assets reloaded this call. Failed reloads emit a
logging.WARNING but do not raise.Stop the watchdog observer thread and join it (timeout 2 s). Idempotent.
True if the observer has been started and not stopped.Snapshot of currently watched directories.
FileWatcher requires pip install watchdog. It is an optional dependency; setup_assets without watch_dirs does not import watchdog.Scene Serialization
Scene is a static facade for saving and loading every component of every live entity in a World to a JSON file. The schema is versioned ("version": 1); loading a file with a mismatched version raises SceneVersionError.
Scene.save
world to path. The write is atomic: data is first written to <path>.tmp and then os.replaced onto the target, so a crash mid-write cannot corrupt an existing save file.
Component fields must be JSON-serializable scalars (bool, int, float, str, None). Non-serializable fields emit a RuntimeWarning and are skipped.
On-disk schema
Scene.load
path and spawn them additively into world (the world is not cleared first). Returns the list of new entity IDs. Component classes are resolved by walking sys.modules for classes decorated with @component — all component modules must be imported before calling load.
Unknown component names emit RuntimeWarning and are skipped. Invalid field values for known components also emit RuntimeWarning and skip that component.
Scene.load_additive
Identical to Scene.load — the name documents that the world is not cleared first. Both methods are interchangeable.
SceneVersionError
Raised when Scene.load encounters a file whose "version" field does not equal Scene.VERSION (1). Catch it to handle forward/backward compatibility:
Exception types
| Exception | Inherits | Raised when |
|---|---|---|
AssetNotFoundError | FileNotFoundError | registry.load(path) — path does not exist |
NoLoaderError | KeyError | registry.load(path) — no loader for the extension |
InvalidHandleError | KeyError | registry.get(handle) / registry.reload(handle) — handle’s asset has been unloaded, or argument is not an AssetHandle |
SceneVersionError | Exception | Scene.load — file version ≠ Scene.VERSION |
