Class: RGame::Core::AssetManager

Inherits:
Object
  • Object
show all
Defined in:
lib/rgame/core/asset_manager.rb

Overview

The one place file-backed assets are loaded and cached.

app.assets.image('space.png')
app.assets.sheet('example 09/player.json')
app.assets.preload(:level1, image: ['lvl1/bg.png'], sound: ['lvl1/hit.ogg'])
app.assets.release(:level1)

The built-in types are image, sound, song and read, plus the composites sheet and ui_atlas. A game or its glue adds more with #add_loader, which is how a tile map gets loaded without Core having to know what one is.

Every accessor takes a path relative to the media root — or an absolute one, which is used as it stands — and returns the same object every time, so a file asked for twice is read, decoded and uploaded once. Two spellings of one path are one entry, not two. That is the whole point: loading stops being scattered across a game's setup code, building paths ad hoc and constructing images inline, and becomes one object that knows what is loaded.

A game does not build one of these. App#assets does, rooted at the app's media_root:, on first use — see RGame::Core::App.

Groups, and what release frees

Each cached asset remembers the set of groups that asked for it. Ungrouped loads belong to PERMANENT and survive every release; only clear drops those. A grouped load is reference counted, so an asset two levels both loaded stays until both release it:

assets.image('ui/buttons.png')                    # PERMANENT
assets.preload(:level1, image: ['lvl1/bg.png'])   # :level1
assets.release(:level1)                           # drops lvl1/bg.png only

Releasing drops this cache's reference. The GPU texture goes when the last reference anywhere goes, which is the collector's business, not this class's — see Image.debug_live_textures if you need to watch it happen.

Composites share their parts

A sprite sheet is a descriptor plus an image, and both are pulled through this same cache. So assets.sheet('hero.json') and assets.image('hero.png') hand back one upload between them, and the sheet's PNG is released with the sheet's group.

Loaders are injectable, and that is deliberate

Each asset type maps to a proc, and the defaults name Image, Audio and friends only inside their bodies — never at load time. Passing your own loaders: is what lets the caching, path resolution and grouping be specced with no window, no GL context and no files at all.

Constant Summary collapse

PERMANENT =

Owner of every ungrouped load. Never released, only cleared.

:__permanent__

Instance Method Summary collapse

Constructor Details

#initialize(root:, app:, loaders: nil) ⇒ AssetManager

app is what images are loaded into and where the audio device comes from; root is what every path is resolved against.



63
64
65
66
67
68
69
70
71
# File 'lib/rgame/core/asset_manager.rb', line 63

def initialize(root:, app:, loaders: nil)
  @root = root
  @app = app
  @loaders = {}
  @cache = {}
  @owners = {} # cache key => Set of groups holding it

  (loaders || default_loaders).each { |type, loader| add_loader(type, &loader) }
end

Instance Method Details

#add_loader(type, &loader) ⇒ Object

Teaches this manager a new asset type, and gives it an accessor:

assets.add_loader(:tilemap) { |path| ... }
assets.tilemap('map/island.tmx')

The built-in types go through this too, at construction — there is one mechanism, not a privileged set plus an extension point.

It exists because some asset types cannot be built from inside RGame::Core at all. A tile map is the case that forced it: parsing a .tmx belongs to the engine layer, and Core may not name that layer (see CLAUDE.md, "The rule points both ways"). So the glue installs the loader, and Core never learns what a tile map is.

Defining the accessor rather than routing everything through a generic load(type, path) keeps assets.tilemap(path) reading like the built-ins — and makes respond_to?(:tilemap) false until a loader exists, which is exactly what Renderer#resolve_asset asks before offering it an id.



92
93
94
95
96
97
98
99
# File 'lib/rgame/core/asset_manager.rb', line 92

def add_loader(type, &loader)
  @loaders[type] = loader
  # A singleton method rather than a class-level one: two managers may
  # know different types, and a game that adds `:tilemap` should not be
  # teaching it to everyone else's.
  define_singleton_method(type) { |path, group = PERMANENT| leaf(type, path, group) }
  self
end

#clearObject

Drops everything, PERMANENT included.



150
151
152
153
154
# File 'lib/rgame/core/asset_manager.rb', line 150

def clear
  @cache.clear
  @owners.clear
  self
end

#preload(group, **manifest) ⇒ Object

Loads a set of assets under one group, so they can be released together:

assets.preload(:level1, image: ['lvl1/bg.png'], sheet: ['lvl1/foes.json'])


119
120
121
122
123
124
125
126
127
128
# File 'lib/rgame/core/asset_manager.rb', line 119

def preload(group, **manifest)
  manifest.each do |type, paths|
    unless respond_to?(type)
      raise ArgumentError, "unknown asset type #{type.inspect} in preload(#{group.inspect})"
    end

    Array(paths).each { |path| public_send(type, path, group) }
  end
  self
end

#release(group) ⇒ Object

Takes group off every asset's owner set and drops whatever no group still holds.

Raises:

  • (ArgumentError)


132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/rgame/core/asset_manager.rb', line 132

def release(group)
  # Without this, `release(PERMANENT)` would empty every ungrouped
  # asset's owner set and drop the lot — the exact opposite of what the
  # constant's name promises, and silent. It is only reachable by naming
  # the sentinel, so saying what to use instead beats ignoring the call.
  raise ArgumentError, 'ungrouped assets are dropped by #clear, not #release' if group == PERMANENT

  @owners.each_value { |groups| groups.delete(group) }
  @owners.reject! do |key, groups|
    next false unless groups.empty?

    @cache.delete(key)
    true
  end
  self
end

#sheet(path, group = PERMANENT) ⇒ Object

A sprite sheet, assembled through the cache: its descriptor is a cached read and its image a cached image, so nothing is loaded twice.



107
108
109
# File 'lib/rgame/core/asset_manager.rb', line 107

def sheet(path, group = PERMANENT)
  fetch(:sheet, path, group) { build_sprite_sheet(path, group) }
end

#sizeObject

How many assets are cached. For tests and for a debug overlay; a game has no reason to ask.



158
# File 'lib/rgame/core/asset_manager.rb', line 158

def size = @cache.size

#typesObject

The types this manager can load. :image, :sound, :song and :read are built in; anything else came from #add_loader.



103
# File 'lib/rgame/core/asset_manager.rb', line 103

def types = @loaders.keys

#ui_atlas(path, group = PERMANENT) ⇒ Object

A UI atlas, assembled the same way.



112
113
114
# File 'lib/rgame/core/asset_manager.rb', line 112

def ui_atlas(path, group = PERMANENT)
  fetch(:ui_atlas, path, group) { build_ui_atlas(path, group) }
end