Skip to content

Data types

Charles edited this page Jun 11, 2026 · 3 revisions

Introduction

ProtoJam provides several general purpose data types to help build your games without the extra boiler plate.

Using Memoizer

The Memoizer class is a basic in-memory cache for a single value (see memoization). When a value is requested, it checks the validity of the cached value returning it if valid and fetching if not. By default, the value is checked for nullness and instance validity but a custom validator may also be provided. Both the provider and validator may be coroutines.

This class is intended to help you cache values too expensive to lookup every frame like the player, an objective, or other object which must be found in the scene tree (though any type can be cached, not just nodes).

var _player_memo: Memoizer = Memoizer.new(_find_player)

func _process(_delta: float) -> void:
	var player: Node3D = await _player_memo.get_value()
	if null != player:
		print(player.global_position)


# Will only get invoked once as long as the player remains valid
func _find_player() -> Node3D:
	return get_tree().get_first_node_in_group(&"Player")

Using ObservableDictionary

ObservableDictionary performs the same function as Godot's Dictionary type but will invoke callbacks when values are added, removed, or changed. This is ideal inventories or other systems that need to perform some action when a dictionary is modified.

Important

Be aware that some functions, like set, have different names to avoid conflicts with Object.

func _ready() -> void:
	var my_dictionary: ObservableDictionary = ObservableDictionary.new(_on_added, _on_removed, _on_changed)
	my_dictionary.set_value("foo", 1) # Triggers _on_added
	my_dictionary.set_value("foo", 2) # Triggers _on_changed
	my_dictionary.erase("foo")        # Triggers _on_removed


func _on_added(key: Variant, value: Variant) -> void:
	print("Added %s: %s" % [key, value])


func _on_removed(key: Variant, value: Variant) -> void:
	print("Removed %s: %s" % [key, value])


func _on_changed(key: Variant, new_value: Variant, old_value: Variant) -> void:
	print("Changed %s from %s to %s" % [key, old_value, new_value])

Examples

Example 1 - Memoizer with custom validator

This example demonstrates adding a custom validator to ensure the cached value is still in the scene tree.

enemy.gd

class_name Enemy
extends Node3D

@export var speed: float = 30.0

var _objective_memo: Memoizer = Memoizer.new(_find_objective, _validate_objective)

func _physics_process(delta: float) -> void:
	var objective: Node3D = await _objective_memo.get_value()
	if null != objective:
		global_position.move_toward(objective.global_position, speed * delta)


func _find_objective() -> Node3D:
	return get_tree().get_first_node_in_group(&"Objectives")


func _validate_objective(objective: Variant) -> bool:
	# A custom validator is run after the default validation so we can be sure
	# this will not be called on a null value
	return objective.is_inside_tree()

Example 2 - Memoizer with coroutines

This example demonstrates using a memoizer to fetch its data using a coroutine.

spawner.gd

class_name Spawner
extends Node3D

const _ENEMY_SCENE_PATH: String = "res://enemy.tres"

var _enemy_scene_memo: Memoizer = Memoizer.new(_load_enemy_scene)

func spawn() -> void:
	var enemy_scene: PackedScene = await _enemy_scene_memo.get_value()
	if null != enemy_scene:
		var enemy: Node3D = enemy_scene.instantiate()
		add_child(enemy)


func _load_enemy_scene() -> PackedScene:
	var scene_handle: AsyncResourceHandle = BackgroundResourceLoader.load_async(_ENEMY_SCENE_PATH, "PackedScene")
	# scene_handle.ready is a signal which will be called when the scene is loaded
	if null != scene_handle and scene_handle.ready:
		return scene_handle.get_resource()
	return null

Clone this wiki locally