I have a resource that is designed to store distance maps of hex tiles for pathfinding usage. I want to have this data be updated whenever it is assigned to an exported Resource variable. I originally had it set it up so that the Resource data gets saved the first time the scene is run. However, this results in a less than ideal workflow where every time I want to create a new map, I will always need to run the scene at least once in order to save the distance map data.
I have a node RangeFinder that has two export parameters: one for the path that stores the references to all map tile nodes, Tiles, one that is the resource that needs to be updated.
Whenever I reload the scene to test I get two errors: Invalid call. Nonexistant function 'get_all' in base 'Nil'. in the _update_distance_map function, and Invalid call. Nonexistent function 'distances_present' in base 'Resource (hex_map_distances.gd)'.
tool
class_name RangeFinder
extends Node
export(Resource) var dist_maps = null setget set_distance_map
export(NodePath) var map_tiles_reference = null
var _hm_astar: HexMapAStar = null
onready var _map_tiles: Tiles = get_node(map_tiles_reference)
# Updates the distance map when the reference is changed.
func set_distance_map(new_dist_map: Resource) -> void:
if new_dist_map == null:
dist_maps = null
property_list_changed_notify()
return
if not new_dist_map is HexMapDistances:
printerr("Resource is not of type HexMapDistances.")
dist_maps = null
property_list_changed_notify()
return
dist_maps = new_dist_map
property_list_changed_notify()
if Engine.is_editor_hint() and _hm_astar == null:
_hm_astar = HexMapAStar.new(
_map_tiles.get_all(),
_map_tiles.get_x_count()
)
_update_distance_map()
# Called when the node enters the scene tree for the first time.
func _ready():
_check_for_required_parameters()
# Waiting for _map_tiles to be ready allows for RangeFinder node being
# able to be placed in any position relative to node with map tiles data.
# Without this, RangeFinder node would always need to be after map tiles
# node.
yield(_map_tiles, "ready")
_hm_astar = HexMapAStar.new(_map_tiles.get_all(), _map_tiles.get_x_count())
_update_distance_map()
# Updates the distance map if necessary.
func _update_distance_map() -> void:
if dist_maps.distances_present():
return
dist_maps.create_from_map(_map_tiles.get_all(), _hm_astar)
var err: int = ResourceSaver.save(dist_maps.resource_path, dist_maps)
if err != OK:
printerr("Failed to save distance maps")
Tiles Script
tool
class_name Tiles
extends Spatial
"""
A container for map tiles. Generates an array of map tiles with z rows and
x columns. Positions each tile and sets up the connections between them.
"""
# Gets all the MapTiles.
func get_all() -> Array:
return get_children()
Resource Script
class_name HexMapDistances
extends Resource
"""
Stores the distance maps for all tiles for a given hex map.
"""
export(Dictionary) var d_maps = {}
var map_hash: int = -1
# Creates the distance maps for a given map.
func create_from_map(map_tiles: Array, hm_astar: HexMapAStar) -> void:
# Enable all connections to make sure distance can be found.
hm_astar.set_all_disabled(false)
var index: int
for tile in map_tiles:
index = tile.map_coordinate.get_index()
d_maps[index] = hm_astar.get_full_distance_map(index)
# Reset for future range finder operations.
hm_astar.set_all_disabled()
Running the scene reveals that the _map_tiles variable is not getting set to the Tiles node, even though the node path is set. This happens even when I explicitly define the path. Weirdly, moving the Resource node after the Tiles node causes the Invalid Call error for Resource to go away.
onready will init the variable when the program is run and its owner node is added to the scene tree.
This excludes _map_tiles from use during editing (tool mode).
I’m not exactly sure how to get around this with your situation.
You can try it without the annotation and see if that works but there is a lot going on here so you may run into more issues. var _map_tiles: Tiles = get_node(map_tiles_reference)
Using set/get in tool scripts can be tricky regarding readiness. I don’t know if that’s your problem, but if so here’s a solution for Godot 4.x. For 3.6 you might need to adjust, I don’t know if you can use await like that. But I hope it puts you in the right direction of what to look for.
I’ve figured it out. The main issue was that the _map_tiles node was not getting loaded before its relevant methods needed to be accesses. The variable map_tiles_reference was also null even though it had its value set in the editor. My best explanation is that the set_distance_map function runs before map_tiles_reference and map_tiles would populate. I would need to reference the documentation again, but that is what I observed.
The solution is to create a setter function for map_tiles_reference and make sure that the map_tiles_reference variable is defined before the dist_maps variable. Order matters here.
Getting the code to work when loading the scene, as in opening it up in the inspector, required the use of is_node_ready. The setter functions run before ready is executed, which resulted in errors with null values. I figured it would be best to let the ready function handle the initial loading logic, and have the setters handle updates while the scene is running in inspector.
tool
class_name RangeFinder
extends Node
"""
Contains the logic for determining area ranges and paths for a HexMap. Requires
a reference to the map tiles.
"""
export(NodePath) var map_tiles_reference = null setget set_map_tiles
export(Resource) var dist_maps = null setget set_distance_map
var _hm_astar: HexMapAStar = null
onready var _map_tiles: Tiles = get_node(map_tiles_reference)
# Updates the reference path for map tiles node. Is intended only for use when
# running the RangeFinder script in the inspector for the purposes of saving
# the distance map resource data.
func set_map_tiles(ref_path: NodePath) -> void:
if not Engine.is_editor_hint():
return
map_tiles_reference = ref_path
property_list_changed_notify()
# Allow for the _map_tiles variable to be set in the _ready function.
if is_node_ready():
_map_tiles = get_node(map_tiles_reference)
_update_distance_map()
# Updates the distance map when the reference is changed. Is intended only for
# use when running the RangeFinder script in the inspector for the purposes of
# saving the distance map resource data.
func set_distance_map(new_dist_map: Resource) -> void:
if not Engine.is_editor_hint():
return
if new_dist_map == null:
dist_maps = null
return
if not new_dist_map is HexMapDistances:
printerr("Resource is not of type HexMapDistances.")
dist_maps = null
property_list_changed_notify()
return
dist_maps = new_dist_map
# Allow for the distance_map to be updated in the _ready function.
if is_node_ready():
_update_distance_map()
# Called when the node enters the scene tree for the first time.
func _ready():
_check_for_required_parameters()
# Waiting for _map_tiles to be ready allows for RangeFinder node being
# able to be placed in any position relative to node with map tiles data.
# Without this, RangeFinder node would always need to be after map tiles
# node.
yield(_map_tiles, "ready")
_hm_astar = HexMapAStar.new(_map_tiles.get_all(), _map_tiles.get_x_count())
if Engine.is_editor_hint():
_update_distance_map()
# Updates the distance map if necessary. Should only ever be called when running
# the RangeFinder script in inspector.
func _update_distance_map() -> void:
if dist_maps == null:
printerr("No distance map has been defined in RangeFinder.")
return
var d_maps: Dictionary = {}
if _hm_astar == null:
_hm_astar = HexMapAStar.new(
_map_tiles.get_all(),
_map_tiles.get_x_count()
)
# Enable all connections to make sure distance can be found.
_hm_astar.set_all_disabled(false)
var index: int
for tile in _map_tiles.get_all():
index = tile.map_coordinate.get_index()
d_maps[index] = _hm_astar.get_full_distance_map(index)
# Reset for future range finder operations.
_hm_astar.set_all_disabled()
dist_maps.d_maps = d_maps
var err: int = ResourceSaver.save(dist_maps.resource_path, dist_maps)
if err != OK:
printerr("Failed to save distance maps")
I should also make a note that I moved the logic that populates the resource data from the resource itself to the script that initially called said logic. I don’t know if the functions of a resource can be called from the inspector so it was easier to just move that logic.