GPUParticles don't emit finished signal when oneshot is set true in gdscript
Tested versions
- Reproducible in 4.3.beta2
- Reproducible in 4.2.1.stable
System information
Godot v4.3.beta2 - Windows 10.0.19045 - GLES3 (Compatibility) - NVIDIA GeForce RTX 3070 (NVIDIA; 31.0.15.3619) - 11th Gen Intel(R) Core(TM) i7-11700K @ 3.60GHz (16 Threads)
Issue description
When setting oneshot to true on a GPUParticles2D node, the particles visually stop emitting some time later as expected, however the 'finished' signal is not emitted. This does not occur with CPUParticles2D. I have not tested this for 3D particles, but I presume that this also applies to the GPUParticles3D.
Steps to reproduce
- Open the MRP below
- There are two particle emitters, the left is GPU and the right is CPU.
- Both are setup the same, and each have a custom script. In this script, the emitters are set to have oneshot enabled on ready. A function is connected to the finish signal which will print a message and free the particle emitters.
- Play the game. Both particle emitters visually stop emitting, however only the CPU particles prints a message and frees itself. Checking the remote hierarchy, you can see that the GPUParticles2D is still present despite nothing being emitted; the finished signal should have emitted!
- If the oneshot setting is enabled on the GPUParticles2D from the beginning (rather than being set through gdscript), then the finished signal correctly emits.
Minimal reproduction project (MRP)
I confirm this problem
I reported something similar in #93546.
Perhaps this is related https://github.com/godotengine/godot/pull/76859#issuecomment-1769504893
❗ UPDATE: try using restart() instead of emitting = true (https://github.com/godotengine/godot-proposals/issues/7322)
Perhaps this is related #76859 (comment)
❗ UPDATE: try using
restart()instead ofemitting = true(godotengine/godot-proposals#7322)
But that comment is about how to start a particle emitter, while this issue (and #93546) is about finishing it?
Not to mention on my issue #93546 I do use restart() and I still don't get a finished() signal.
This is script from the MRP in this issue (sorry, I didn't look at yours):
extends GPUParticles2D
func _ready() -> void:
one_shot = true
emitting = true
finished.connect(finish)
func finish() -> void:
print("GPU Particles Destroyed!")
queue_free()
After adding restart() in _ready() function signal will be emitted.
IMHO he problem lies in the fact that the one_shot value is changed when the emitter is already added to the scene tree and has an active emitting state.
https://github.com/godotengine/godot/blob/b97110cd307e4d78e20bfafe5de6c082194b2cd6/scene/2d/gpu_particles_2d.cpp#L43-L68
If signal_canceled is set, signal will not be emitted.
I'm experiencing this (or something very similar) in c# with GPUParticles3d
I am hoping to rework oneshot one day.
Would you you mind giving me more information on what you're using this signal for?
@QbieShay: My issue (#93546) is very similar.
I use the finished() signal mostly to know that the effect is done, and then clean itself up, something like this:
particle_effect.finished.connect(particle_effect.queue_free)
Besides for "clean-up", it could be used as a trigger for whatever comes next. Random examples:
- Play a sound-effect: Magic sparkle that fizzes out and want to play a sound at the end
- Trigger game-logic, like increase score: You picked up an object, sparkles fly around from object-location and towards a stamina bar or score, once it's there, the bar/number should increase (and not earlier)
Yes, you could use a timer (although for short effects that's not recommended), but it feels hacky. Especially since the signal exists, but doesn't seem to act/emit as expected.
@QbieShay My use case is very similar to 4X3L82 's , I just want to queue_free the GPUParticles3D object after it's done. In my game I instantiate particle effects produced by bullets, explosions etc under a generic "environment" node (as I didn't want their behaviour to be linked to their source in any way once they have been created). This means that ideally they should be able to queuefree themself once done in a neat way. I tried to use signals but they didn't work for me, I ended up just using a timer. Something like this:
private static async void Disintegrate(DestructibleBody3D destructibleBody3D)
{
PackedScene scene = ResourceLoader.Load<PackedScene>(((Resource)destructibleBody3D.GetScript()).ResourcePath + destructibleBody3D.CUSTOM_PARTICLE_EFFECTS_SCENE_RELATIVE_PATH);
Node instantiatedScene = scene.Instantiate();
destructibleBody3D.fragmentContainer.AddChild(instantiatedScene);
GpuParticles3D emitter = instantiatedScene.GetChild(0) as GpuParticles3D;
emitter.OneShot = true;
destructibleBody3D.meshInstance.Visible = false;
destructibleBody3D.LinearDamp = 1000f;
emitter.GlobalPosition = destructibleBody3D.GlobalPosition;
destructibleBody3D.Deactivate();
emitter.Restart();
await Task.Delay(5_000);
emitter.QueueFree();
}
If you have any other questions feel free to let me know. (sorry this code isnt the greatest, hopefully it illustrates what I am trying to do though)
No worries about the code. Thank you both for supplying your usecase. For now i suggest that you include the same code as the one used by cpp in your script: await get_tree().create_timer(lifetime * (2 - explosiveness_ratio)).timeout