Reassign values on reattaching script - godot

I have a custom node with some export variables but when I remove the script and then reattach it the export variable values get lost,
so I used get_meta() to reassign the values like this:
tool
extends Node2D
export(int) var value=0 setget set_value
func set_value(new_val=null,initial=false):
print("Invoked set_value")
if(initial and self.has_meta("data")):
value=self.get_meta("data")
return
value=new_val
self.set_meta("data",value)
func _init():
set_value(null,true)
but there are 2 problems:
I have to write every setget function inside _init()
When I make changes to the script and save it the setget functions get invoked twice

Try this approach:
export var value:int = get_meta("value", 0) setget set_value
func set_value(new_val:int) -> void:
value = new_val
set_meta("value", value)
When the script is loaded it should execute the initialization which takes the value from metadata (or a default value if the metadata is not set). Godot, nor the core, will call the setter. Since the setter always sets the metadata, the value should not be lost.
When the script is modified Godot will set all the properties, which results in Godot calling all the setters (each setter will run only once). In this case Godot is setting the same value it had, so the value should not be lost.
This approach does not require _init.

Related

Deleting/Disconnecting a SceneTreeTimer

I'm trying to disconnect a SceneTreeTimer to avoid a function being called on timeout
like this:
extends Node2D
onready var something = $Node2D
var timer
func abort():
timer.disconnect("timeout",something,"queue_free")
timer.emit_signal("timeout")
print("timer=>",timer)
func _ready():
timer=get_tree().create_timer(3)
timer.connect("timeout",something,"queue_free")
...
abort()
And while it does stop the timer from invoking the function
I'm still seeing the timer after aborting it, Output:
timer=>[SceneTreeTimer:1276]
Shouldn't it be something like this since it's time has elapsed?
timer=>[Deleted Object]
SceneTreeTimer unlike Node is a Reference.
If you have a look at the good old class diagram. You are going to see that some classes extend Node others Reference (including Resource) and other extend Object directly.
The classes that extend Reference are reference counted, they won't be deleted as long as you hold (a not WeakRef) reference to them.
While the classes that extend Node use explicit memory management, so they are deleted by calling free or queue_free on them.
Thus, drop the reference once you are no longer using the SceneTreeTimer:
func abort():
timer.disconnect("timeout",something,"queue_free")
timer.emit_signal("timeout")
timer = null
print("timer=>",timer) # null, duh
Godot will still emit the "timeout" signal, and when it does it releases its internal reference. We find this in "scene_tree.cpp" (source):
if (time_left < 0) {
E->get()->emit_signal("timeout");
timers.erase(E);
}
We can also experiment using a WeakRef to get a result similar to the one you expect. However, remember that since Godot is holding a reference internally the timer won't be deleted before its normal timeout.
extends Node2D
onready var something = $Node2D
var timer_ref:WeakRef
func abort():
var timer := timer_ref.get_ref() as SceneTreeTimer
timer.disconnect("timeout",something,"queue_free")
timer.emit_signal("timeout")
func _ready():
var timer := get_tree().create_timer(3)
# warning-ignore:return_value_discarded
timer.connect("timeout",something,"queue_free")
timer_ref = weakref(timer)
abort()
func _process(_delta: float) -> void:
print("timer=>", timer_ref.get_ref())
You should see it change from
timer=>[SceneTreeTimer:1234]
To
timer=>null
After 3 seconds, since that is the argument we gave to create_timer.
Trivia: Here you will get some number where I put "1234", that number is the instance id of the object. You can get it with get_instance_id and you can get the instance from the id with instance_from_id. We saw an example of instance_from_id in FauxBody2D.
You might also find it convenient to create an Autoload where you create, stop, and even pause your timers while keeping a API similar to create_timer, for example see Godot 4.0. how stop a auto call SceneTreeTimer?.
Addendum:
DON'T DO THIS
You might actually mess up with Godot. Since it is reference counted, and we can freely change the count, we can make it release the timer early:
var timer := timer_ref.get_ref() as SceneTreeTimer
timer.disconnect("timeout",something,"queue_free")
timer.emit_signal("timeout")
timer.unreference()
I tested this both on the debugger and on a release export, with Godot 3.5.1, and it didn't crash the game, not output any errors.
For clarity unreference is not the same as free, instead:
reference increases the count by one.
unreference decreases the count by one.
I'm calling unreference to cancel out the internal reference that Godot has.
We can confirm that the timer is being freed, either by using a weak reference or by looking at Godot's profiler. However, Godot has an internal list with references to the timers which are not being cleared properly.
I made this code to test out if the timer loop was being affected by the timers being released early by the above means.
extends Node2D
var can_fire := true
func _process(_delta: float) -> void:
var timer := get_tree().create_timer(60)
# warning-ignore:return_value_discarded
timer.unreference()
if can_fire:
can_fire = false
print("CREATED")
# warning-ignore:return_value_discarded
get_tree().create_timer(2).connect("timeout", self, "fire")
func fire() -> void:
print("FIRED")
can_fire = true
You might expect it to output FIRED each couple seconds. However, what I found out is that by using unreference on unrelated timers, we get the others to fire much faster.
My hypothesis is that Godot is keeping the dead reference in its internal list, then when another timer is allocated it takes the same memory. Then the timer counts faster because it appears multiple times in the list.
Removing unreference results in the expected behavior.
It does seem to exist still because calling "disconnect" function won't automatically free itself. Try doing timer.stop() instead.

Invoke function on adding a keyframe in animation player

I have an animation player and I want to invoke a function anytime a keyframe gets added
like this:
extends AnimationPlayer
...
func key_added(track_indx,key_indx):
print("key added: ",key_indx," to track:",track_indx)
is something like this possible? is there any inbuilt function that I'm missing?
I did not have the time to fully experiment with it, but this should give you a direction you could look into.
first of you will need to alter the animation class to give you the needed event. To be able to use it in the editor keep in mind, that you need the tool flag:
extends Animation
tool
class_name ToolAnimation
signal key_added(track_idx, key_indx)
func track_insert_key ( track_idx : int, time : float, key, transition : float = 1) -> void:
.track_insert_key(track_idx, time, key, transition)
#need to find the key index of the key we added
var key_id = track_find_key ( track_idx, time, true )
emit_signal("key_added", track_idx, key_id)
All I do here is to overwrite the track_insert_key to search for the key Id after adding it and then emit a signal.
Now we need to tell our animation_player to add our newly created animations instead of normal animation classes so we change the script of the animation_player and override the add_animation function:
extends AnimationPlayer
tool
func add_animation(name : String, animation: Animation):
var tool_animation = ToolAnimation.new()
tool_animation.connect("key_added", self, "key_added")
.add_animation(name, tool_animation)
pass
func key_added(track_indx,key_indx):
print("key added: ",key_indx," to track:",track_indx)
Now everytime a key is added you should get into the key_added method.
This will only work for newly created animations, because existing ones will not have the toolanimation extensions. To add the feature to existing animations, you would need to deep copy them in your ready functions, for example.
Edit: As #cakelover pointed out in the comments: To alter existing animations, iterate over them and use their set_script() function.
Second thing I noticed, when trying it out was, that my key_added method was not called if a track is newly created and the first key is added simultaniously (basically pressing the key symbol on a property I did not track before). So thats something you should look into, if you also need the first key.

I want to make a dropdown container in my script

I want to create a dropdown container to organize my export variable. Is it possible to create a custom dropdown container in the script?
Like this:
This is another approach to do this. It also requires the script to be tool.
What we need for this approach as a common prefix for the variables you want to group. The advantage is that we don't need _get and _set:
tool
extends Node
var custom_position:Vector2
var custom_rotation_degrees:float
var custom_scale:Vector2
func _get_property_list():
return [
{
name = "Custom",
type = TYPE_NIL,
hint_string = "custom_",
usage = PROPERTY_USAGE_GROUP
},
{
name = "custom_position",
type = TYPE_VECTOR2
},
{
name = "custom_rotation_degrees",
type = TYPE_REAL
},
{
name = "custom_scale",
type = TYPE_VECTOR2
}
]
As you can see we define a category with a name that will appear in the Inspector panel, and the hint_string is the prefix we will use. It is important to put the category before the properties in the array.
See: Adding script categories
Addendum: Using PROPERTY_USAGE_CATEGORY will produce a named header, similar to the one that says "Node2D" on the picture on the question. Use PROPERTY_USAGE_GROUP to make a collapsible group.
Yes, you can do this, but (in my opinion) it is a bit ugly and clutters up your script. You need to mark your script as a tool script and override the _get, _set, and _get_property_list functions.
An example based on your screenshot (not 100% sure this works exactly as-is; I'm also basing it on a recent project where I have since removed it and somewhat reorganized the project/code/node because the slightly nicer UI wasn't worth the additional clutter in the script):
tool
extends Node2D
# Note that these are NOT exported
var actual_position: Vector2
var actual_rotation: float
var actual_scale: Vector2
# Function to enumerate the properties to list in the editor
# - Not actually directly/automatically backed by variables
# - Note the naming pattern - it is {group heading}/{variable}
func _get_property_list():
var props = []
props.append({name="transform/position", type=TYPE_VECTOR2})
props.append({name="transform/rotation deg", type=TYPE_FLOAT}) # might not exist; look at docs to determine appropriate type hints for your properties
props.append({name="transform/scale", type=TYPE_VECTOR2})
return props
# Now the get/set functions to map the names shown in the editor to actual script variables
# Property names as input here will match what is displayed in the editor (what is enumerated in _get_property_list); just get/set the appropriate actual variable based on that
func _get(property: String):
if property == "transform/position":
return actual_position
if property == "transform/rotation deg":
return actual_rotation
if property == "transform/scale":
return actual_scale
func _set(property: String, value):
if property == "transform/position":
actual_position = value
return true
if property == "transform/rotation deg":
actual_rotation = value
return true
if property == "transform/scale":
actual_scale = value
return true
# Not a supported property
return false
Note that this answer is based on Godot 3.4. I'm not sure if a simpler approach is (or will be) available in Godot 4.

I have a error in my godot script gdscript can you please solve it

onready var healthBar : TextureProgress = get_node("HealthBar")
func update_health_bar (curHp, maxHp):
healthBar.max_value = maxHp
healthBar.value = curHp
Error is: Invalid set index 'max_value' (on base: 'Nil') with value of type 'int'.
your healthBar was null when you called update_health_bar.
This might be because you don't have a children node called "HealthBar" or it is not a TextureProgress node.
Can you send a screenshot of your scene tree ?
It should look like this:
When I encounter such an error, it usually is because I am instancing the scene dynamically via scripts, and to get around the null error I create a init function to assign the node.
So in your case, after instancing your scene and adding it to the scene tree, call init on your scene, where you init function is as follows:
func init():
healthBar=get_node("HealthBar")
Maybe that might work.

Error message "Invalid set index 'text' (on base: "null instance") with value of type 'String' " when trying to change the text of a label

i am trying to update the text of a label but keep getting the above error message. i don't know what i am doing wrong. here is my code:
extends Node
var PlayerScore = 0
var EnemyScore = 0
func _on_Left_body_entered(body):
$Ball.position = Vector2(640,360)
EnemyScore += 1
func _on_Right_body_entered(body):
$Ball.position = Vector2(640,360)
PlayerScore += 1
func _process(delta):
$PlayerScore.text = str(PlayerScore)
$EnemyScore.text = str(EnemyScore)
$PlayerScore is shorthand for get_node("PlayerScore") which means it checks direct child with name "PlayerScore". Your error shows that (on base: ”null instance“) which means that you were accessing null instance (it doesn't exists, at least as direct child or at that moment).
P.S.
Since you are calling it that often I'd suggest save the reference to that node in a script's global variable.
onready var playerScore: = $PlayerScore #variable will be created when script's owner is ready
The solution that I implemented was to put the Label in another scene.
You will decide if you put it above or below the scene you are using.
I do not understand why it happens but for some reason it does not allow to read or update the text in a main scene. It usually happens to me in the scenes that it starts to execute.

Resources