How to know when a reference has been edited in an tool script

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

I am trying to create a reference script that can be placed on a class and define extra parameters.

And it works! EXCEPT for when the reference script is edited. Then I get this error:

EnemyController.gd:100 - Invalid call. Nonexistent function 'get_script_export_list' in base 'Reference (paraenemy.gd)'.

Here’s the code:

tool

var controller_script = null
var controller : Reference
func get_controller_script():
    if controller_script == null:
	    controller_script = controller.new(base_direction, controller_variables)
    return controller_script

func _set(property, value):
    # Changing controller
    if property == "movement/controller":
        controller = value
	    if controller != null:
		    reset_controller_script()
	    property_list_changed_notify() # update inspect
	    return true
    # Controller variables
    if controller_script != null:
	    for prop in get_controller_script().get_script_export_list():
		    if prop.name == property:
			    controller_variables[prop.name] = value
			    get_controller_script().update_exports(controller_variables)
			    property_list_changed_notify() # update inspect
			    return true
    return false

When a controller (which is a script with its own class) is edited, the whole thing falls apart, but I have no idea why. Does the reference from controller_script become invalid? Is there a way to check that? Even has_method says the method on the controller_script exists only for the program to immediately claim it doesn’t.

I’ve also tried both weakref.get_ref and is_instance_valid. They turn up the same value before and after the editing.

:bust_in_silhouette: Reply From: Kanor

After some work, I have come to a simple answer:

_set and _get_property_list are used in saving a file. You cannot keep _set and _get_property_list from being called after a file is saved.

This was my first attempt and object composition: The art of splitting scripts according to the work they do for better maintenance. But there are several SUPPORTED, NON-BUGGY ways of doing this same thing. Here is a list for those who come after me who want to do some beautiful object composition:

  • Node Composition

Yeah, so, Godot has nodes containing nodes. Nothing beats that. A “parent” can consider all children as accessible variables. It’s guaranteed that those nodes will exist and can be reached from a consistent path. If you need to split functionality (such as when a Mario hero needs to split movement logic from damage and HP logic), this is what I consider the best way:

RootNode (Script: enemyController)
  -> Health (Script: playerHealth)

The rootnode can call damage() on the $Health when needed.

This also fixes the “Export properties” problem above, as each node can specify its own export properties which can be accessed in the editor.

  • Inheritance

Subclassing! Useful when you want to extend existing behavior with slight alterations. For instance, my problem was with a movement method that needed the slight addition of a frame() function. This is different depending on the enemy type. Goombas, Koopas, and Bowser all move different ways, even though basic things like dying, freezing and respawning are the same for all of them. Since an enemy freezing and an enemy moving belong to the same type of script, and depend on the same variables, it makes no sense to split them into two nodes. Instead, I solved the problem using a template method that gets overriden!

Superclass:

extends KinematicBody2D

func _physics_process(delta):
	self.frame(sprite, delta)

func frame(sprite : Sprite, delta):
	pass

Subclass:

extends "res://enemy/Scripts/EnemyController.gd"

func frame(sprite : Sprite, delta):
    # Moving functionality
  • Using a script variable

This is what I was doing above, and it IS useful in some cases.

This option is useful if the script being accessed changes during RUNTIME. The movement logic for enemies and the health logic for a player do not change after the game starts. This is why giving them nodes to attach to is safe and useful. It also allows for editting export variables! BUT if a script is swapped out with others on the fly, you’re not going to be specifying export variables for them, are you?

Example: A Mario hero needs to have a script for shooting fireflowers. It has a press_power_button function. BUT that script needs to get swapped out with a tanooki power script. In this case, having your script contain an “export(Resource) var power” variable and assigning instantiated classes on the fly is the best solution.

onready var body : KinematicBody2D = get_node("..")
export(Resource) var defaultPower = preload("res://items/fireFlowerPower.gd")
onready var power = defaultPower.new(body)

func change_to_tanooki():
    var script = preload("res://items/tanookiPower.gd")
    power = script.new(body)

This does not have problems when saving the scripts because the scripts are not depended on or used until runtime. That is when defaultPower is instantiated. In cases like this, the scripts MAY use variables, but they all use the same set, like how the Power scripts would probably use the player’s rigidbody.