Type alias in godot

Godot Version

Goddot 4.4

Question

Hi, I’m making a roguelike and I have a data structure that stores how many of the possible room types a dungeon has

var cell_list:Dictionary[Room.RoomType, int]

My problem is that now all my methods look like this

func set_cell_list(n_cell_list:Dictionary[Room.RoomType, int]):

I think explicitly declaring the type like that is a bad pracitce because if I change what my cell_list is I would need to track every method that uses it and change its typing.

I could create a class for that specific data type but I think that is too overkill and I don’t want to access my cell list as CellList.list. I’m looking for something like typescript’s type aliases or c’s structs

export type CellList = Dictionary<Room.RoomType, int>;
function set_cell_list(list:CellList){
/**/
}

Does anything like this exist in godot or should I just use a class?

1 Like

Others are interested in something like that as well:

For things like that in gdscript, I’ve generally wound up making global vars that can be referenced directly without handing them around as function args. It’s not ideal, but it does cut down the boilerplate a bit. It also saves you from having to re-plumb whole branches of your call stack when you realize you need something in a deeply nested function.

1 Like

I get your point but I always try to adhere to functional programming and using the global variable would go against it. I guess I’ll just live with it and hope I never have to change the data structure.

I hope they add this feature soon

You need to program in the style that suits your tastes, but you may find that functional programming isn’t an ideal match for games, and I’m saying this as someone who used TinyScheme as the scripting layer in my own game engine.

Games (on average! Your particular game may vary…) tend to require a lot of internal communications; to an extent, nearly everything needs to know about nearly everything else. In a functional programming setup, this tends to mean either you’re handing around some massive world database object (in which case either it’s passing by value and generating masses of work for the garbage collector, or it’s passing by reference and you lose all the optimization benefits that functional ought to be delivering), or it’s talking to an actual database.

Functional programming has some real strengths in some domains, but (at least in my experience), games are not one of those domains.

YMMV, though. Obviously, you do whatever works for you.

1 Like

This is not available in Godot

It’s almost a thing when preloading scripts

const ScriptType = preload("res://my_script_type.gd")

func hello(script_type: ScriptType) -> void:

So syntactically it would be nice to define other types as const too

const CellList = Dictionary[Room.RoomType, int]
2 Likes

Its only overkill if you overkill it.
Small data structures are no different than large data structures from the point of view of the class user. That user is not required to even know what goes on inside the class ie the principle of abstraction.
And a class is only too small when it fails to fulfill its purpose.
One glaring exception would be if this is process-critical wherein using a class may drag the fps.

What you are describing is a collection class and is not uncommon.
Calling it "list’ would be less than ideal as that name is too generic.
Call it RoomList or RoomCollection.

1 Like

I’m not too experienced, so I’m still figuring what works best for me. The approach I’ve taken is to make a lot of generic pure functions for “chores” and then invoke them in more specific methods like enemy_behaviour or something like that. Those specific methods are not pure functions and are the ones that “know about everything else”. I try to use as few of them as I can

This is not functional programming, but I like to think that it is close enough. I just try to adhere to it whenever it is possible, always leveraging the implications of that choice. It’s worked fine for now but maybe I’m just missing the bigger picture because of my inexperience and the scope of my games (really small 2d stuff).

Yeah, after giving it some though I think I’ll do this. I can even include some more functionality, like delegating into the class the generation of random rooms or at least the automatic update of its data when a new room is created

You could ditch dictionaries and use resources, although not very cache friendly.

class Cell extends Resource:
  var key : Room.RoomType
  var value : int

var cell_list:Array[Cell]

func some_func(cell_list:Array[Cell]):
1 Like

I’ve been using a mix. I have a dungeon resource that contains said dictionary. It loooks like this.

class_name  DungeonResource extends Resource

const DEFAULT_CELLS = 5

@export var dungeon_dimensions:Vector2i = Vector2i(DEFAULT_CELLS,DEFAULT_CELLS) : set = set_dungeon_dimensions
@export var cell_list:Dictionary[Room.RoomType, int] = {
	Room.RoomType.normal:DEFAULT_CELLS,
	Room.RoomType.boss:1,
	Room.RoomType.initial:1
} : set = set_cell_list
@export var min_room_number:int = DEFAULT_CELLS : set = set_min_room_number
@export var max_room_number:int = DEFAULT_CELLS : set = set_max_room_number


#-------------------------------------------------------
# GETTERS / SETTERS
#-------------------------------------------------------

func set_min_room_number(n_number:int):
	var total_space := dungeon_dimensions.x * dungeon_dimensions.y
	if n_number >= total_space: n_number = total_space
	min_room_number = clamp(n_number, 3, max_room_number)
	set_cell_list(cell_list)


func set_max_room_number(n_number:int):
	var total_space := dungeon_dimensions.x * dungeon_dimensions.y
	max_room_number = clamp(n_number, min_room_number, total_space)
	set_cell_list(cell_list)


func set_dungeon_dimensions(n_dimensions:Vector2i):
	var max_dimensions := Vector2i(Dungeon.MAX_DUNGEON_SIZE, Dungeon.MAX_DUNGEON_SIZE)
	var min_dimensions := Vector2i(Dungeon.MIN_DUNGEON_SIZE, Dungeon.MIN_DUNGEON_SIZE)
	dungeon_dimensions = n_dimensions.clamp(min_dimensions, max_dimensions)


## Makes sure the cell list does not contain less than min_cells or more than min_cells
func set_cell_list(n_cell_list:Dictionary[Room.RoomType, int]):
	n_cell_list[Room.RoomType.boss] = 1
	n_cell_list[Room.RoomType.initial] = 1
	if not n_cell_list[Room.RoomType.normal]: n_cell_list[Room.RoomType.normal] = 0
	
	for type in n_cell_list:
		n_cell_list[type] = maxi(0, n_cell_list[type])
	
	var cell_count = count_cells_in_list(n_cell_list)
	
	if cell_count > max_room_number:
		var extra_cells = cell_count - max_room_number
		var n_normal_cells = n_cell_list[Room.RoomType.normal] - extra_cells
		n_cell_list[Room.RoomType.normal] = maxi(0,n_normal_cells)
	
	# We need to recalculate cell count after each steap
	cell_count = count_cells_in_list(n_cell_list)
	if cell_count < min_room_number:
		var missing_cells = min_room_number - cell_count
		n_cell_list[Room.RoomType.normal] += missing_cells
	
	cell_count = count_cells_in_list(n_cell_list)
	if cell_count  > max_room_number \
	or cell_count < min_room_number: 
		assert(cell_count > max_room_number, "Cell count distribution is not among permited limits")
		return
	cell_list = n_cell_list
	notify_property_list_changed() 


#-------------------------------------------------------
# UTILS
#-------------------------------------------------------

## Counts the total number of cells in the current configuration
func count_cells_in_list(list:Dictionary[Room.RoomType, int])->int:
	var count := 0;
	for type in list:
		count += list[type]
		
	return count

The idea is that the resource should hold the dungeon data and the logic to ensure that data is valid (no more cells than space available, etc…)

One option could be to create the class you mention. Delegate the validation logic to that class and only use the resource for storage and visualization purposes.

Also why do you say it’s not cache friendly? Is it because Dictionaries are alredy optimized or because something resources do?

Correct, i dont know the official size but i think Object is around 300+ Bytes, which is huge relatively to your objective of two 64bit ints which would be at least 16 bytes, since a dictionary is a native type so it doesnt use the Object class. Basically object/resources classes have a lot of engine and lifecycle state that bulk them up, where dictionaries dont care about such things.

I dont see the code provided needing any cache performance with the number of elements less than maybe a 1000 elements, so it would be fine in my opinion to use a Resource class, if it has advantages.

I use Resources as a stand in for structs. Which i find much easier to work with than dictionaries. And Dictionaries are only useful for constant time lookup and unique mapping. You use the unique mapping bit, but i dont see any searching available in your API? Maybe not yet?

I could make an argument that my idea is not the best approach depending on your goals, i actually dont see any advantage to switch to resources, unless your dictionary needed more than just a count value.

3 Likes

Ok, thanks for the info. It was really useful. Knowing this, I think I’ll leave the resource as is but when tackling other projects, I will take this into account

I have one final question. I’m thinking of implementing a method that during the dungeon generation process handles the cell creation process, e.g.

  • Getting a random cell type (among those that whose quantity
  • Decreasing its quantity in the dictionary
  • Handling errors

Do you think this logic should go inside the resource, inside the dungeon class that initiates the dungeon generation process, or inside a new class CellList. I’m inclined to think that the first option is the best one by a great margin, but I’m not sure I’m not missing something

Getting a random cell should probably be part of your
DungeonResource and not the cell_list, the locality of the data is the main reason for ease of use. I like the idea of functional programming where data and behaviors are separate. Obviously this isnt pure functional programming when building classes, but there are no hard rules anyway.

The cell list is the makeup of your dungeon and is interchangable. The DungeonResource could really be a Node that you can place in a scnene and devise your dungeon. If the cell data needs to be passed around to many systems then DungeonResource should stay a resource. ( You could pass the dictionary around, but i dont really like dictionaries for how cumbersome they are to work with. But if you need to interact with the data externally, with code that already exists in the dungeonresource, just pass the resource around. )

1 Like

Thanks a lot, I think I’ll keep going this way

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.