Get node that a RichTextEffect is attached to

Godot Version

4.4.1

So first I’ll ask if this approach to my problem is even possible, and then I’ll explain what I’m actually trying to do.

So firstly, is there any way to access the RichTextLabel that a RichTextEffect is attached to? I would like to retrieve the label’s width and height from within my custom effect.

So my specific use case:
I am trying to create custom BBCode that emulates the old HTML <marquee> tag as closely as possible. In some instances (depending on the parameter), the text should wrap around the screen (ie. when it reaches one side it starts again on the other), in others it reverses direction when it reaches the edge, etc.

The only way I can think to achieve this is to detect the width of the RichTextLabel. I’m not sure if this is even possible.

If there is a better way to do this, I’d love to know, though I am also interested in the answer to the first question as well.

UPDATE
I have found formulas that emulate the behavior I want, but they both rely on defining a width, which currently needs to be done by the user. This is not ideal, as it is not how the original HTML tag worked, so I’m back to the original question. How do I find the width of the RichTextLabel?

Example Code:
In these examples, amplitude is the width that is being set manually.
Scroll Function

func sawtooth_wave(time: float, period: float, amplitude: float) -> float:
	var speed = 20/period
	var phase := fmod(time / speed, 1.0)
	return -amplitude + ((amplitude*2.0) * phase)

Alternate

func bounce_wave(time: float, period: float, amplitude: float = 1.0) -> float:
	var t_mod := fmod(time, period)
	var phase := t_mod / period  # Normalize to 0.0–1.0

	if phase < 0.25:
		return 4.0 * amplitude * phase    
	elif phase < 0.75:
		return -4.0 * amplitude * (phase - 0.5) 
	else:
		return 4.0 * amplitude * (phase - 1.0)  

You can pass the RichTextLabel to the RichTextEffect when attaching it to the label. Something like:

# RichTextEffect
class_name MyEffect extends RichTextEffect


var label:RichTextLabel

# ...

func _process_custom_fx(char_fx: CharFXTransform) -> bool:
    var width = label.size.x
    # ...
# RichTextLabel
extends RichTextLabel


var my_effect:MyEffect


func _ready() -> void:
    my_effect = MyEffect.new()
    my_effect.label = self
    install_effect(my_effect)
1 Like

The effect doesn’t seem to be installing… prints(my_effect, my_effect.label) returns <RichTextEffect#-9223371978520394294> RenderedPage:<RichTextLabel#46288340747> , so I can see the effect is being instantiated, but the code in RichTextEffect doesn’t end up running at any point (I put a print statement in there just to be sure).

I’m not sure I follow, what does not work? It seems to print the correct info. The code I posted is not complete.

I mean when I tried installing the effect from within a script rather than within the editor, it doesn’t seem to install at all. As in the RichTextEffect script doesn’t even load when I install it from within the RichTextLabel script.

The full code:

RichTextLabel

extends RichTextLabel
var my_effect:Marquee

func _ready() ->void:
	my_effect = Marquee.new()
	my_effect.label = self
	install_effect(my_effect)
	
	#Tried this, it doesn't work either:
	#install_effect(load("res://game_assets/bb_scripts/bb_marquee.gd").new())
	
	#Also this:
	#install_effect(preload("res://game_assets/bb_scripts/bb_marquee.gd").new())

RichTextEffect

@tool
extends RichTextEffect
class_name Marquee

var bbcode = "marquee"
var label:RichTextLabel

func _process_custom_fx(char_fx: CharFXTransform) -> bool:
	#Sets how the text is scrolled within the marquee.
	#Possible values are scroll, slide and alternate.
	#If no value is specified, the default value is scroll.
	var behavior = char_fx.env.get("behavior", "scroll")
	var width = char_fx.env.get("width", 1200)
	#var label_width = label.size.x
	var scrollamount = char_fx.env.get("scrollamount", 1)	#pixels per tick
	var scrolldelay = char_fx.env.get("scrolldelay", 85)	#in ms
	var truespeed = char_fx.env.get("truespeed", false)
	var direction = char_fx.env.get("direction", "left")
	var direction_int: int
	
	match direction:
		"left":
			direction_int = 1
		"right":
			direction_int = -1
		"up":
			pass
		"down":
			pass
	if !truespeed and scrolldelay < 60:
		scrolldelay = 60
	#var bgcolor = char_fx.env.get("bgcolor", 2)
	var amplitude = width
		
	var t = char_fx.elapsed_time	
	var speed = (scrolldelay*10)/scrollamount
	var period = amplitude/speed
	match behavior:
		"scroll":
			char_fx.offset.x = sawtooth_wave(t, period, amplitude) * direction_int
		"slide":
			pass
		"alternate":
			char_fx.offset.x = bounce_wave(t, period, amplitude)  * direction_int
	
	return true

func sawtooth_wave(time: float, period: float, amplitude: float) -> float:
	var speed = 20/period
	var phase := fmod(time / speed, 1.0)
	
	return (amplitude - (amplitude * 2.0) * phase)

func bounce_wave(time: float, period: float, amplitude: float = 1.0) -> float:
	var t_mod := fmod(time, period)
	var phase := t_mod / period 

	if phase < 0.25:
		return 4.0 * amplitude * phase
	elif phase < 0.75:
		return -4.0 * amplitude * (phase - 0.5)
	else:
		return 4.0 * amplitude * (phase - 1.0)  # -A → 0

Seems to be working fine on my end:

Do you mean it does not work in the editor? It won’t work until you make the RichTextLabel script a @tool script and reload the scene.

1 Like

I copied it over to a new project and found something out. Possibly a Godot bug?

It works, but only if I am installing a single effect. For example, try:

RichTextLabel

extends RichTextLabel
var my_effect:Marquee

func _ready() ->void:
	install_effect(Marquee.new())
	install_effect(Slinky.new())

bb_slinky.gd

@tool
extends RichTextEffect

class_name Slinky

var bbcode = "slinky"

func _process_custom_fx(char_fx: CharFXTransform) -> bool:
	var speed = char_fx.env.get("speed", 1)
	var power = char_fx.env.get("power", 1)
	char_fx.offset.x += pow(char_fx.relative_index * (sin(char_fx.elapsed_time * speed)), power)
	return true

Example text of RichTextLabel:

[marquee]This is a marquee[/marquee]
[slinky]Slinky[/slinky]

Yep, it was a bug RichTextLabel only renders first installed custom effect when using multiple install_effect() in a row · Issue #103630 · godotengine/godot · GitHub. It should be fixed in Godot 4.5

1 Like

Perfect thank you! I’ll work around it for now.

So FYI (more so for anyone else reading this in the future), an issue I had with the original solution of passing the reference to RichTextLabel through to RichTextEffect is that it doesn’t update when the size of RichTextLabel is changed. In my case, I had it in an HSplitContainer, and the effect wasn’t detecting changes. What I did is write a brief function to update it and then call that using a signal in HSplitContainer.

My final solution:

@tool
extends RichTextLabel
var marquee:Marquee

func _ready() ->void:
	marquee = Marquee.new()
	_update_label()
	install_effect(marquee)
	...

func _update_label():
	marquee.label = self


@tool
extends RichTextEffect
class_name Marquee

var bbcode = "marquee"
var label:RichTextLabel

func _process_custom_fx(char_fx: CharFXTransform) -> bool:
	var width = char_fx.env.get("width", label.size.x)
	...
1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.