r/godot 1d ago

help me Sfx Caching System

To reduce memory usage of preload() and to reduce overhead by constantly loading sfx from disc using load(), I was tasked by programming lead to develop a sfx caching system.

While I kinda understand the purpose and functionality of a cache, I'm unsure if I'm approaching this the right way and would appreciate some opinions/tips.

Here's the current architectural attempt:

  • class Sfx
    • static func play(parent: Node, stream: String)
  • subclass Library
    • a bunch of const: String holding stream file paths
  • subclass Cache
    • static var stream_path: Array[String]
    • static var stream_file: Array[AudioStream]
    • a bunch of static func for internal logic

Architecture and naming were chosen with syntax in mind. So to play a sfx from anywhere, you'd write Sfx.play(self, Sfx.Library.SFX_SOMETHING)

Cache is operating on 2 Arrays simultaneously. Sfx.Cache.stream_path[0] is the String file path of Sfx.Cache.stream_file[0].

When Sfx.play() is called, it'll be checked whether or not the supplied String file path is inside Cache.stream_path and if it is not, it'll be cached, which basically looks like this: stream_path.append(stream_to_cache) stream_file.append(load(stream_to_cache))

As none of all this exists as an instance in any scene tree, I can't use timers inside Sfx to do cache flushes every once in a while, so I did set up a max cache size and just delete the oldest index when it reaches capacity.

I could set up a timer elsewhere, but I hesitate bc it'd violate LoD.

3 Upvotes

15 comments sorted by

4

u/MattsPowers Godot Regular 1d ago

Your programming lead knows that load() is the same as ResourceLoader.load() which is already caching?

1

u/McCyberroy 1d ago

I'm unaware. But I'll go take a look at the ResourceLoader class now. Thanks for mentioning!

3

u/MattsPowers Godot Regular 1d ago

1

u/McCyberroy 1d ago edited 1d ago

Seems like this will do the trick:

load(path, "", CacheMode.CACHE_MODE_REUSE)

But since param_3 of load() defaults to 1 (CACHE_MODE_REUSE), load(path) is enough.

Again, thanks for pointing that out!

1

u/McCyberroy 1d ago edited 1d ago

I have to give an update on this. This doesn't work in our case. Cached stuff will be "uncached" as soon as there's no reference to the cached thing anymore. So I have to manually ensure there's a reference remaining somewhere. So my, or something like my approach, is required.

1

u/DwarfBreadSauce 1d ago

Why not store reference inside the node thats supposed to use it?

1

u/McCyberroy 1d ago edited 1d ago

Because it's de-centralized then. To keep track of what is referenced and what not, we need a centralized system.

We need sfx that'll be used frequently to be stored in cache. But if something loses reference, it gets removed from cache as well.

If we store a, say, sfx_bird_chirp.wav inside a bird.tscn and only load it from inside there, then there wouldn't be any reference to a sfx_bird_chirp.wav if no bird.tscn is instantiated. Every time we instantiate a bird.tscn, sfx_bird_chirp.wav would need to load() again. We want to avoid those unnecessaryload() 's

We basically want sfx_bird_chirp.wav to stay cached for a few seconds before losing reference.

1

u/DwarfBreadSauce 1d ago

But you dont need bird sfx if there are no birds, do you?

And if your issue is with frequent loading and unloading of the entitiies - why not pool those entities instead?

1

u/McCyberroy 1d ago

Okay look, say we randomized bird.tscn instantiation. If a bird.tscn is present while another one instantiates, then we wouldn't have a problem. But due to randomization, we may end up with 0 instances of bird.tscn for a few milliseconds, we want to bridge those few milliseconds and keep sfx_bird_chirp.wav cached till the next bird.tscn instantiates.

Otherwise we'd trigger load() every few milliseconds.

1

u/DwarfBreadSauce 1d ago

This is a perfect example for why you need object pooling thought. And you didnt provide any argument against it.

1

u/Alzurana Godot Regular 4h ago

Sooooo, thing is... The OS also caches sectors that have been read recently so calling load() a second time might not even be that terrible if your sound files are uncompressed. (By default they are compressed with godot btw)

Just adding this to the knowledgebase

5

u/nonchip Godot Regular 1d ago

load is a caching system.

1

u/megalate 1d ago

I would delete them based on last usage and not just time since it was cashed. I would put a time stamp when they are created/used and overwrite the oldest one when it's full.