Best practices loading textures

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By VictorSeven

Hello all,

I always wonder what is the best way in order to do an efficient use of textures. Let me use a practical example, say I have a RPG game, and I want to display the inventory with all the items the player has. Each item will be a TextureButton so I can click it to use the object.

One way to do this is:

for item in inventory:
    button.texture_normal = load(item.texture)

This will simply load the image. However, if I have 6 potions, for example, does the texture load 6 times? Am I wasting memory doing this?
Would be more efficient to do something in the lines of:

#item_textures[j] contains texture of item j 
item_textures = get_textures() 
for item in inventory:
    #id identifies the object
    button.texture_normal = item_textures(item.id) 

In this last example, I preload the textures of the items, so if I have 6 potions, I will use the same texture for all of them instead of using load six times. Does this improve efficiency?

Any other tips to manage this kind of situations in an efficient, clean way? Thank you!

:bust_in_silhouette: Reply From: Calinou

Note: I am not sure if Godot caches texture loading out of the box; it’s probably a good idea to benchmark and compare a non-cached implementation with a cached one.

In case Godot does not handle this for you, you can probably implement your own caching system by storing paths to already-loaded textures and the associated texture resource in a dictionary. Then in your item loading loop, compare the path of the texture that’s currently being loaded to the keys present in the “loaded textures” dictionary. If it’s present, reuse the existing texture resource, otherwise, create a new texture resource.

The use of the dictionary is a nice idea. However, that would mean comparing all the keys of a dictionary each time I do an assignement which is slow in comparsion to the second method I proposed (which is to store all needed textures first and use them).

Also, the best would be to bechmark the use of the textures. Any idea on how to do that?

VictorSeven | 2018-06-16 13:26

I don’t know a lot about the engine, but Calinou’s suggestion seems sound. Already load the textures, do a comparison on the paths for the textures which you need, and assign the necessary ones. I would assume the dictionary’s hashes for the keys of the texture path values would be quicker to compare than constantly loading the textures when you need to show the item inventory.

Ertain | 2018-06-16 17:09

:bust_in_silhouette: Reply From: flowerPirate97

I don’t know whether Godot unloads the texture while it is shown. But it definitely unloads it when it is not shown (e.g. you close the inventory screen).

In my game I allow for mods to load their own pictures. Hence I don’t have control over their size and can’t expect the images to be optimized.

I found that loading 20 medium-sized JPGs from disc (modern HDD) using this command results in a noticable performance hit every time I open the same screen (meaning the textures are not cached):

func texture(path:String):
  var image = Image.new()
  image.load(path)
  var texture = ImageTexture.new()
  texture.create_from_image(image)
  return texture

That’s why I created a new class that helps me with caching:

extends Object

class_name Cache

var cache_size:int
var cache_history = []
var cache_items = {}
var funcRefLoad:FuncRef

func _init(funcRefLoadEntry:FuncRef, size:int = 50).():
	funcRefLoad = funcRefLoadEntry
	cache_size = size

func _get(property:String):
	if !cache_items.has(property): 
		loadEntry(property)
	else:
		touchEntry(property)
	return cache_items[property]

func addEntry(property:String):
	return funcRefLoad.call_func(property)

func loadEntry(property:String):
	cache_items[property] = addEntry(property)
	cache_history.push_front(property)
	removeSurplus()
	
func removeSurplus():
	var entriesToRemove:int = cache_history.size() - cache_size
	if entriesToRemove <= 0: return
	
	for i in range(entriesToRemove):
		var entryToRemove:String = cache_history.pop_back()
		cache_items.erase(entryToRemove)
	
func touchEntry(property:String):
	cache_history.erase(property)
	cache_history.push_front(property)

It get’s initialized like this (assuming the same function texture() as before is used):

func _ready():
	cache_texture = Cache.new(funcref(self,"texture"),200)

and called like this:

func getTexture(path):
	return cache_texture.get(path)

It is way faster than before (I don’t have exact metrics, but there is not noticable loading time left) and it makes sure not to load too many textures while keeping hold of the recently used ones.

Since you can pass any funcref you can use the same cache-object for other resources as well.

1 Like