Dictionary with multiple keys (or something else w same function?)

Godot Version

4.6

Question

Currently, in my game, you can combine two different “elements” to get an effect unique to each combination. For example, a combo of fire and water would trigger the “Boil” effect. As it stands, there are 9 unique elements, making for 36 unique combos (elements combined with themselves don’t trigger a unique effect). Is there some simple, elegant way to identify the relevant effect based on what’s played, without having to resort to something like a massive switch case?

Supposing your elements are an enum and your effects are in resources, you can create a lookup dictionary based on the sorted array of the two elements (sorted so you don’t need to define [fire, water] and [water, fire].

var combo_lookup: Dictionary[Array, <ResourceType>] = {
    [Elements.FIRE, Elements.WATER]: load("<PathToBoilEffectResource>"),
}

I’d make the Element a Resource.

class_name Element
extends Resource

And the Effect a Resource.

class_name Effect
extends Resource

@export var elements_to_combo: Array[Element]

Then you can create your Elements, like Water, Fire, etc., your Effects like Boil etc. and load the Elements into the Effects.


1 Like

Assuming that the same two elements in the opposite order produce the same effect. I’d make each effect a resource.

class_name Effect
extends Resource

@export var name: String

And save each effect to disk by right clicking on the res:// folder (or whichever folder you’d like), and creating a new Effect Resource.
Then I’d have a singleton for managing the combinations. So let’s say this script is a global named ECM (for ElementComboManager).

# ECM
extends Node

# enum of all elements
enum Elements{
    # each element is represented by an integer with only one bit flipped to 1
    WATER = 1<<0, # element 1 or 0001 in binary
    FIRE = 1<<1, # element 2 or 0010 in binary
    AIR = 1<<2, # element 3 or 0100 in binary
}

var element_combo_to_effect: Dictionary[int, String] = {
    # even better if you used the resource's uid instead of its path.
    Elements.WATER | Elements.FIRE : "res://path_to_boil_effect.tres", # bitwise OR 0001 | 0010 produces 0011
}

func get_effect_from_elements(element_1: Element, element_2: Element) -> Effect:
    if element_1 | element_2 in element_combo_to_effect.keys():
        return load(element_combo_to_effect[element_1 | element_2]) # loads effect element_1 | element_2 which is 0001 | 0010 == 0011 == Elements.WATER | Elements.FIRE
    else:
        push_error("There is not an assigned effect for the combination: `%s` and `%s`." % [element_1, element_2])
        return null

To get the combination of two elements:

func _ready() -> void:
    ECM.get_effect_from_elements(ECM.Elements.WATER, ECM.Elements.FIRE) # returns Boil effect as a loaded resource (path_to_boil_effect.tres)
    ECM.get_effect_from_elements(ECM.Elements.FIRE, ECM.Elements.WATER) # returns Boil effect as a loaded resource (path_to_boil_effect.tres)
    ECM.get_effect_from_elements(ECM.Elements.FIRE, ECM.Elements.FIRE) # returns null

If you prefer this could be a static library instead of a global

@abstract
class_name ECM
extends Node

# enum of all elements
enum Elements{
    # each element is represented by an integer with only one bit flipped to 1
    WATER = 1<<0, # element 1 or 0001 in binary
    FIRE = 1<<1, # element 2 or 0010 in binary
    AIR = 1<<2, # element 3 or 0100 in binary
}

static var element_combo_to_effect: Dictionary[int, String] = {
	Elements.WATER | Elements.FIRE : "res://path_to_boil_effect.tres", # bitwise OR 0001 | 0010 produces 0011
}

static func get_effect_from_elements(element_1: Elements, element_2: Elements) -> Effect:
	if element_1 | element_2 in element_combo_to_effect.keys():
		return load(element_combo_to_effect[element_1 | element_2]) # loads effect element_1 | element_2 which is 0001 | 0010 == 0011 == Elements.WATER | Elements.FIRE
	else:
		push_error("There is not an assigned effect for the combination: `%s` and `%s`." % [element_1, element_2])
		return null

You could just use nested dictionaries.

enum Element { WATER, FIRE, AIR }

var effects_table: Dictionary[Element, Dictionary[Element, String]] = {
  Element.FIRE: {
    Element.WATER: "boil",
  },
  Element.WATER: {
    Element.FIRE: "boil",
  },
}

# Access as effects_table[Element.FIRE][Element.WATER]

I’d use bitmasking, enum and a dictionary.

In my opinion, it’s easier to read, easier to setup and most importantly only one instance of combined elements needed in the dictionary.

You can also easily add a third, forth, fifth etc.. element for combining, if you want to do it at some point…

#Element should be enum so it has int values for bitmasking
enum Element { WATER, FIRE, AIR }

#Effect can be enum or Resource or whatever...
enum Effect { WATER_EFFECT, FIRE_EFFECT, AIR_EFFECT, BOIL, WATER_AIR_EFFECT, AIR_FIRE_EFFECT}


var effects: Dictionary[int, Effect]{
	2**Element.WATER : Effect.WATER_EFFECT,
	2**Element.FIRE : Effect.FIRE_EFFECT,
	2**Element.AIR: Effect.AIR_EFFECT
	2**Element.WATER + 2**Element.FIRE : Effect.BOIL
	2**Element.WATER + 2**Element.AIR : Effect.WATER_AIR_EFFECT
	2**Element.FIRE + 2**Element.AIR : Effect.AIR_FIRE_EFFECT
}

func get_effect(element1: Element, element2: Element = -1) -> Effect:
	var bitmask : int = 2 ** element1
	if element2 > -1:
		bitmask += 2 ** element2
	return effects(bitmask)