Class: RGame::Core::AssetManager
- Inherits:
-
Object
- Object
- RGame::Core::AssetManager
- 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
-
#add_loader(type, &loader) ⇒ Object
Teaches this manager a new asset type, and gives it an accessor:.
-
#clear ⇒ Object
Drops everything, PERMANENT included.
-
#initialize(root:, app:, loaders: nil) ⇒ AssetManager
constructor
appis what images are loaded into and where the audio device comes from;rootis what every path is resolved against. -
#preload(group, **manifest) ⇒ Object
Loads a set of assets under one group, so they can be released together:.
-
#release(group) ⇒ Object
Takes
groupoff every asset's owner set and drops whatever no group still holds. -
#sheet(path, group = PERMANENT) ⇒ Object
A sprite sheet, assembled through the cache: its descriptor is a cached
readand its image a cachedimage, so nothing is loaded twice. -
#size ⇒ Object
How many assets are cached.
-
#types ⇒ Object
The types this manager can load.
-
#ui_atlas(path, group = PERMANENT) ⇒ Object
A UI atlas, assembled the same way.
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 |
#clear ⇒ Object
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.
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 |
#size ⇒ Object
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 |
#types ⇒ Object
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 |