Skip to content

Tecs Assets

Tecs Assets is a non-blocking asset loading and management system. It loads game assets like images, sounds, fonts, and shaders in background threads without freezing gameplay.

  • Non-blocking threaded loading: Uses worker threads to load assets without blocking the main thread
  • Automatic memory management: Uses weak references for cached assets, allowing automatic garbage collection
  • Handle-based API: Returns promise-like handles that can block on first access or be checked for completion

Quickstart

You can access the asset manager from any system:

teal
local tecs = require("tecs")
local assets = require("tecs2d.assets")

world:addSystem({
    phase = tecs.phases.Startup,
    run = function()
        -- Get the asset manager instance
        local manager = world.resources[assets]

        -- Load assets (returns immediately with a handle)
        local playerImage = manager:loadImage("sprites/player.png")
        local jumpSound = manager:loadAudio("sounds/jump.wav", "static")
        local mainFont = manager:loadFont("fonts/main.ttf", { size = 16 })

        -- Check if a handle is complete
        if playerImage.isComplete then
            -- Access the value (will error if load failed)
            love.graphics.draw(playerImage.value, 100, 100)
        end
    end,
})

The game plugin automatically creates an asset manager with a root assets folder of "./assets". Use setRoot to change the asset root directory:

teal
world.resources[assets]:setRoot("game-assets")

Handles

All load methods return a Handle<T> immediately. Handles are smart references to assets being loaded.

teal
local handle = manager:loadImage("player.png")

-- You can check if the handle has an error
if handle.err then
    print("Failed to load:", handle.err)
end

-- You can check if the handle is done
if handle.isComplete then
    local image = handle.value
end

You can access the handle value directly at any time, blocking until it's fully loaded:

teal
local image = handle.value -- blocks!

You can react when the handle is done loaded or has an error:

teal
-- Listen for completion
handle:observe(function(h: assets.Handle<T>)
    if h.err then
        print("Error:", h.err)
    else
        print("Loaded successfully")
        playerSprite = h.value
    end
end)

You can transform handles into other types using map():

teal
-- Transform an image handle into a particle system
local particles = manager
    :loadImage("particle.png")
    :map(function(image: love.graphics.Image): love.graphics.ParticleSystem
        return love.graphics.newParticleSystem(image)
    end)

Handle Properties

NameTypeDescription
isCompletebooleantrue if the operation has completed
valueTThe loaded data. Blocks on first access, errors if failed
errstringError message if loading failed, nil otherwise

Blocking Behavior and Errors

When you access handle.value for the first time:

  1. If the asset is already loaded, it returns immediately
  2. If still loading, it blocks until complete
  3. Once loaded, the value is cached on the handle for instant future access
  4. If loading failed, accessing value throws an error

Memory Management

Handles are cached in storage using a weak reference, allowing automatic garbage collection of unused resources.

teal
-- Assets are cached automatically
local handle1 = manager:loadImage("player.png")
local handle2 = manager:loadImage("player.png")  -- Returns same handle

-- When all references are released, the asset can be garbage collected
handle1 = nil
handle2 = nil
collectgarbage()  -- Asset may be collected if no other references exist

-- Next load will create a new handle
local handle3 = manager:loadImage("player.png")  -- New load operation

Use the pin() method of a Handle to preload assets or to ensure it's never garbage collected:

teal
-- This asset won't be garbage collected
manager:loadImage("player.png"):pin()

Supported Asset Types

The asset manager provides built-in loaders for common asset types:

MethodReturnsDescription
loadImageImagePNG, JPG, and other image formats
loadSpriteSheetSpriteSheetSprite sheets with animations
loadStaticSheetSpriteSheetSingle images as sprite sheets
loadBMFontBMFontBitmap font atlases (.fnt, .json)
loadTiledMapTilemapDataTiled map editor exports
loadFontFontTrueType fonts (.ttf, .otf)
loadImageFontFontImage-based fonts
loadAudioSourceAudio files (WAV, OGG, MP3)
loadShaderShaderGLSL shader files
loadVideoVideoVideo files (OGV)
loadJsonanyJSON data files
loadFilestringRaw text file contents
loadCompressedImageCompressedImageDataCompressed image data (DDS, KTX, etc.)
loadFileDataFileDataRaw binary file contents

Game-Specific Assets

For sprite sheets, BMFonts, and Tiled maps, the loaders automatically load all required files concurrently:

  • Sprite Sheets: Loads the image, JSON metadata, and optional material maps (_n.png, _e.png, _s.png)
  • BMFonts: Loads the font definition and its atlas texture
  • Tiled Maps: Loads the map, all tileset images, external tilesets, material maps, and image layers

Custom Asset Types

For game-specific asset types, use registerAssetHandler to create custom loaders that integrate with the caching and threading system.

Preloading assets

To preload assets during startup, load and pin them in a Startup system:

teal
local tecs = require("tecs")
local assets = require("tecs2d.assets")

world:addSystem({
    phase = tecs.phases.Startup,
    run = function()
        local manager = world.resources[assets]
        -- Use :pin() to ensure assets stay in memory
        manager:loadImage("player.png"):pin()
        manager:loadAudio("music.ogg", "stream"):pin()
        manager:loadFont("ui.ttf", { size = 16 }):pin()
    end,
})

InitialLoadComplete

The InitialLoadComplete event is emitted on the first call to update() when there are no pending load operations. This means all assets queued during startup will have finished loading before the event fires. The event is only emitted once per AssetManager instance and is emitted even if nothing was ever loaded.

teal
world:observe(0, assets.InitialLoadComplete, function()
    print("All startup assets are ready!")
end)

This is useful for implementing loading screens or deferring gameplay until assets are available.

Loading Screens

Asynchronous asset loading can be used to implement animated loading screens with progress displays.

See working example

See the Loading Screen Example for a complete working example on GitHub.