Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RichTextLabel - New Real Time Text Effects and Custom BBCode Extensions #23658

Merged
merged 1 commit into from
Sep 4, 2019

Conversation

Eoin-ONeill-Yokai
Copy link
Contributor

@Eoin-ONeill-Yokai Eoin-ONeill-Yokai commented Nov 12, 2018

Summary

This PR adds a new ItemFX type to RichTextLabel which supports real time text effects. This also adds the ability to add additional bbcode commands using custom RichTextEffect extensions (inspired by: #23135 ).

I will need to write more documentation on this when/if it gets approved as a new feature. Here's a quick rundown of the two features:

You can download the example project for this commit here: RichTextEffects.zip. This is the best way to experience the changes made via this commit, but some gfycat examples are linked below.

New Real Time Text Effects

Real time effects will update and draw every frame. ItemFX typed effects will automatically enable set_process when found in a bbcode string. This means that the RichTextLabel will only need to process every frame if a dynamic text effect is being used.

Wave

screenshot from 2018-11-11 16-16-11

[center]Here's a look at the [wave amp=50 freq=2]wave text effect[/wave].[/center]

Text will wave up and down on the Y axis based on its relative X position.

  • amp= The amplitude of the wave.
  • freq= The frequency of the wave.

Tornado

screenshot from 2018-11-11 16-11-34

[center]Here's a look at the [tornado radius=5 freq=5]tornado[/tornado] text effect.[/center]

Text will move in a circular fashion. Each characters position on the circumference will be determined by their x position, creating an interesting ripple effect.

  • radius= The radius of the circle which controls its offset.
  • freq= The frequency of full circle motions.

Shake

screenshot from 2018-11-11 16-17-32

[center]Here's a look at the [shake rate=15 level=10]shake[/shake] text effect.[/center]

  • rate= How often the text relocates
  • level= How far the text is offset from its origin

Fade

screenshot from 2018-11-11 16-19-58

[center]Here's a look at the [fade start=4 length=14]fade text effect...[/fade][/center]
Fades out the text based on a starting position and a length value.

  • start= The starting position the falloff. Relative to where fade command is inserted.
  • length= Over how many characters should the fade out take place. (Rough falloff control.)

Rainbow

screenshot from 2018-11-11 16-31-01

[center]Here's a look at the [rainbow freq=0.2]rainbow[/rainbow] text effect.[/center]
Gives the text a rainbow color assignment based on x position and time.

  • freq= The number of full rainbow cycles per second.
  • sat= Saturation of rainbow.
  • val= Value (e.g. brightness) of rainbow.

Custom BBCodes and Text Effects

screenshot from 2018-11-11 16-44-13

You can now extend the RichTextEffect resource type to create your own custom BBCodes. You can view the example project for better details, but here's a rough example below.

Extend:

You begin by extending the RichTextEffect resource type. Add the tool prefix to your gdscript file if you wish to have these custom effects run within the editor itself. The RichTextLabel does not need to have a script attached, nor does it need to be running in tool mode.

Here are some examples of custom effects:

Ghost

tool extends RichTextEffect

var bbcode = "ghost"

func _process_custom_fx(char_fx):
	
	var speed = char_fx.get_or("freq", 5.0)
	var span = char_fx.get_or("span", 10.0)
	
	var alpha = sin(char_fx.elapsed_time * speed + (char_fx.absolute_index / span)) * 0.5 + 0.5
	char_fx.color.a = alpha
	return true;

Pulse

tool extends RichTextEffect

var bbcode = "pulse"

func _process_custom_fx(char_fx):
	var color = char_fx.get_or("color", char_fx.color)
	var height = char_fx.get_or("height", 0.0)
	var freq = char_fx.get_or("freq", 2.0)
	
	var sinedTime = (sin(char_fx.elapsed_time * freq) + 1.0) / 2.0
	var y_off = sinedTime * height
	color.a = 1.0
	char_fx.color = char_fx.color.linear_interpolate(color, sinedTime)
	char_fx.offset = Vector2(0, -1) * y_off
	return true

Matrix

tool extends RichTextEffect

var bbcode = "matrix"

func _process_custom_fx(char_fx):
	var clear_time = char_fx.get_or("clean", 2.0)
	var dirty_time = char_fx.get_or("dirty", 1.0)
	var text_span = char_fx.get_or("span", 50)
	
	var value = char_fx.character
	
	var matrix_time = fmod(char_fx.elapsed_time + (char_fx.absolute_index / float(text_span)), \
							clear_time + dirty_time)
	
	matrix_time = 0.0 if matrix_time < clear_time else \
				(matrix_time - clear_time)/dirty_time
	
	if( value >= 65 && value < 126 && matrix_time > 0.0 ):
		value -= 65
		value = value + int((1 * matrix_time * (126-65)))
		value %= (126 - 65)
		value += 65
	char_fx.character = value
	return true

There is only one function you need to extend named _process_custom_fx(char_fx). Optionally, you can also provide a custom bbcode identifier simply by adding a member name bbcode. The code will check the bbcode automagically or will simply use the name of the file to determine what the bbcode should be.

_process_custom_fx.

This is where the logic of each effect takes place and is called once per character during the draw phase of text rendering. This passes in a CharFXTransform object, which holds a few variables that control how the associated character is rendered. The first is identity, which will specify which custom effect is being processed. You should use that for code flow control. relative_index will tell you how far into a given custom effect block you are in as an index. absolute_index will tell you how far into the entire text you are as an index. elapsed_time is the total amount of time the text effect has been running. visible will tell you whether the character is visible or not and will also allow you to hide a given portion of text. offset is an offset position relative to where given character should render under normal circumstances. color is the color of a given character. There's also env which is a dictionary of variables assigned to a given custom effect (for an example look at the code above). The last thing to note about this function is that it is necessary to return a boolean true value to verify that the effect processed correctly. This way, if there's a problem with rendering a given character, it will back out of rendering custom effects entirely until the user fixes whatever error cropped up in their custom effect logic.

This will add a few new bbcode commands, which can be used like so:

[center][ghost]This is a custom [matrix]effect[/matrix][/ghost] made in [pulse freq=5.0 height=2.0][pulse color=#00FFAA freq=2.0]GDScript[/pulse][/pulse].[/center]

@Zylann
Copy link
Contributor

Zylann commented Nov 12, 2018

Instead of overriding functions of RichTextLabel and checking the name of the effect each time, why not define a Resource type which you can extend with GDScript? This way you will be able to have one script per effect if you want, and no need to check the name of the effect in your code.

@Eoin-ONeill-Yokai Eoin-ONeill-Yokai force-pushed the rich-text-plus branch 4 times, most recently from a37ef18 to 755bb12 Compare November 12, 2018 03:45
@Eoin-ONeill-Yokai
Copy link
Contributor Author

Yeah I've been considering doing something like that to help make it a bit simpler. I may try messing with something along those lines a bit tonight.

Is it strange or out of character for resources to be used in a way where they're mostly used for the functionality instead of the data though?

@Eoin-ONeill-Yokai
Copy link
Contributor Author

Eoin-ONeill-Yokai commented Nov 12, 2018

@Zylann Is there actually a way to make it so that I check for any custom resource script that exists within a project? So if you define a custom effect resource in gdscript (Let's say pulse.gd extends RichTextEffect ) I could actually look for any script that extends RichTextEffect and run those effects? Or would it be better to use the resource system in a way where you load up custom effects into an instance of a RichTextLabel?

Also, is there a good example in the code for dealing with adding custom resources that you can think of?

@Zylann
Copy link
Contributor

Zylann commented Nov 12, 2018

Sorry Godot generally avoids doing global file walks to accomplish auto-registration like you suggest (I once proposed that for custom loaders but was told no). Also it's better to register it in this case because otherwise every RichTextLabel would end up doing that and always registering every effect without control.

For custom resources like that, I guess in your case RichTextFX would be a resource. For an example, I made a voxel module in which you can define your own terrain provider (as a resource) https://github.com/Zylann/godot_voxel/blob/master/voxel_provider.cpp (nevermind call_multilevel, call should be enough)
Basically you can then make a script (or a subclass) which extends that resource type and either set it on the RichTextLabel with something like label.add_custom_fx(preload("custom.gd").new()), or from the editor make a new RichTextFX on which you add your script and then add it to a list in the inspector. Having it shown directly in New Resource could help too if the script has class_name X, 'icon'.

Just suggestions to make it easier to share, I'm not particularly in this kind of effects but I find your PR interesting^^

@Eoin-ONeill-Yokai
Copy link
Contributor Author

Eoin-ONeill-Yokai commented Nov 13, 2018

Hey @Zylann thanks for taking the time to help me out here. I have one more quick question...

What is the best way to expose this type of behavior to the editor? I have it working by doing basically what you had above ( self.install_effect(preload("script.gd").new()) ) but it would actually be really nice if you could install effects without having to attach a script at all. I'm not sure how I should expose my Vector<RichTextEffect> while making use of Godots inspector ui features.

@Zylann
Copy link
Contributor

Zylann commented Nov 13, 2018

@THEYOKAI to expose it to the editor you could expose an array of resources to the inspector, it was improved recently so you can certainly show this with an easy to use UI.
If you don't want the user to go through two steps (create resource + add script on it), there are several ways:

  • Expose adding scripts directly in the inspector (not sure how idiomatic it is tho)
  • Scripts with class_name and icon show up in the new Node dialog, so if the custom resource uses class_name, you could request that they show up in new Resource too so users can pick it up in one step
  • Otherwise, the old fashioned way: devs have to create a plugin in which they register their custom resources to be made available in the new Resource dialog

@Eoin-ONeill-Yokai
Copy link
Contributor Author

I'm going to push up what I have (it mostly works) but I think there's a few problems with the resource method atm. I'll detail it more in a response...

@Eoin-ONeill-Yokai
Copy link
Contributor Author

Quick update, I have made text effects a resource type now named RichTextEffect that can be extended using GDScript. I've updated the summary to reflect this new change.

There are a few issues I'm having right now regarding how to streamline the user experience of making and sharing text effects though. Ideally, I'd actually like to be able to let users attach scripts to a RichTextEffect internally to the asset (like how Nodes can have internal scripts that aren't saved in the res:\\\ folder structure) but trying to open a script that has been made that way produces a crash. I haven't tried it with other resource types yet, so it could be something on my end, but I think that would be the ideal way to share these scripts with other users. As it is right now, you have to make a script (res:\\\someeffect.gd) and then attaching that to an additional asset file (res:\\\someeffect.tres) and then attaching that resource to the effect array. Alternatively, like @Zylann mentioned, you can add class_name to the head of the file to show it in the new resource browser -- but I still think that having the scripts embedded in the assets themselves would be a much nicer way to share these specific types of scripts.

I've also added a get_or method to CharFXTransform which should make fetching variables from the bbcode faster. I may push this change to the dictionary type itself, as it is a useful method for fetching something from a dictionary or getting a default value from the dictionary if that fails.

Let me know what you think of these changes. The example project has also been updated to reflect the new system, but please let me know if you're having problems with that testing project.

@Eoin-ONeill-Yokai Eoin-ONeill-Yokai force-pushed the rich-text-plus branch 3 times, most recently from 6676845 to eb0e5e1 Compare November 14, 2018 21:36
@ghost
Copy link

ghost commented Nov 15, 2018

Very nice. X)

I wish things like these were also modules so it wouldn't have to wait for 3.2.

@ElfEars
Copy link

ElfEars commented Nov 26, 2018

(I'm not sure if you're allowed to leave comments on pull requests so forgive me if I'm being rude but since @avencherus posted one I now have an excuse)
I'm personally honoured to be credited as an inspiration. This looks absolutely ace!
It's super clean too!

@toger5
Copy link
Contributor

toger5 commented Nov 26, 2018

looks really cool,
I feel like this is too specific for core though. I think this should be a plugin. Although i dont really know how to implement it easily since you cannot really change the drawing code.
Maybe you could give the RichTextLabel a better (more advanced) api. so that you can do your drawing changes in a plugin?

@Eoin-ONeill-Yokai
Copy link
Contributor Author

@toger5

Maybe you could give the RichTextLabel a better (more advanced) api. so that you can do your drawing changes in a plugin?

Since text effects themselves are resources, you should be able to easily share your own rendering style with other users and across projects that use a RichTextLabel node. Any more advanced rendering needs can be handled as a plugin but it should be noted that the RichTextEffect resource is a very simple class that could be reused for any custom text rendering system you have designed.

There could be gains to simplifying the RichTextLabel class for more predictable behaviour and better extensibility, but I believe that this solution for text effects is simple enough that any redesign or fork could use the same basic logic. The only issue I have right now is that the user experience of making a Resource + Script could be streamlined a tad, I'll continue to look into that.

@blurymind
Copy link

this is absolutely fantastic! :) would be very excited to see it in godot.
Do you guys know if there are any js libraries that would enable me to implement similar effect tags in my bbcode parser?

@Eoin-ONeill-Yokai
Copy link
Contributor Author

I've gone ahead and fixed the merge conflicts. Now that 3.1 is out and about, I'll continue to push changes to this branch to try to make the user experience better.

@Zylann , sorry to request for more help, but I was hoping to get your opinion on something. Currently the resource method works, but it sometimes feels a bit overkill to make a class_name definition out of a single-use text effect. I was wondering if it is currently possible in godot to make a resource and embed a script inside of that resource that defines its behaviour? When I have tried to do this before, it would either crash or not work as I expected. Basically, the way I see a RichTextEffect is as a RichTextEffect resource with a script attached to it that supplies its behaviour. Right now, it feels like every text effect needs to have both a gdscript file (the resource definition) and also a tres file (the physical resource) which ends up making the user experience a bit complicated. I would prefer to just have a single tres file with an embedded script behaviour.

The alternative solution I have to simply allow a list of gd script files, and manually attach them to RichTextEffect resources at runtime, but I worry about the effects this would have when running deployed.

@Zylann
Copy link
Contributor

Zylann commented Apr 3, 2019

@THEYOKAI if added by code (with MyEffect.new) such custom resources don't need to have a .tres file, but if you want to save such effects as standalone .tres, it is possible to embed the script inside, just like you can do with nodes. Unfortunately, the dialog with the easy checkbox to do this ("built-in script") is dedicated to nodes only, and Godot lacks of accessibility for doing the same with resources. But that doesn't mean you can't do it: go to the bottom of the resource inspector, click on the script property and choose New GDScript. This will assign a new script embedded inside the resource, so saving that resource will save the script inside. It can be edited by clicking on the property again and the script editor will open.
Giving a class_name is easier because it allows the editor to register that resource type directly so you don't need those steps, but its drawback is, it forces having a .gd file separately. Not necessarily wrong though, you could have several of those with different params/textures, who knows (i.e "SparklingEffect" with different sparks textures).

@ghost
Copy link

ghost commented Apr 3, 2019

@THEYOKAI Haven't had the fullest opportunity to test it, but in the demo when you turn off the time related parameter, the results stop. Wondering if maybe you'd want to reset the time in those case.

Like in Shake text, turned the shake rate to 0.

image

@Eoin-ONeill-Yokai
Copy link
Contributor Author

Eoin-ONeill-Yokai commented Aug 27, 2019

Corrected some of the snake_casing that I missed in my previous commit.

@akien-mga Regarding the stress testing, I have actually tested by animating visible_character and I haven't found any major slowdown caused by the animation of any parameters. Examples of this, including a project with variable sized texts, can be found in my test project.

Regarding Chinese text, I can try, though I'm not actually sure how Chinese text should change the performance outside of maybe taking slightly more time to draw due to slightly larger datatype size (UTF8 vs UTF16, I would presume?). I haven't tried that yet though, so I may as well give it a try and see how it changes anything / if it changes anything.

Of course, the important part of the testing I've done so far is that if you don't use any rich text effects, you will not endure any of the performance cost that comes with the real time effects. Also, having a lot of different effects at once may affect performance since manually running each script comes with an inherit runtime cost. I would assume that GDNative implementations of text effect would similarly be less costly though, so I don't think that it should be an issue.

edit: Also, yes, I will squash all of the commits. I will probably do that soon now that I no longer need the revision history (I tend to squash as I get reassured I won't have to step back, which I'm getting there now.)

Added a new ItemFX type to RichTextLabel which supports dynamic text
effects.

RichTextEffect Resource Type was added which can be extended for more
real time text effects.
@Eoin-ONeill-Yokai
Copy link
Contributor Author

This branch has now been rebased / cleaned. I've tested it a few different ways and I don't seem to have any significant problems on my end.

@akien-mga akien-mga merged commit 3d76eb8 into godotengine:master Sep 4, 2019
@akien-mga
Copy link
Member

Thanks!

@realkotob
Copy link
Contributor

This looks awesome thanks!

I just want to ask, how are making the text display one character at a time?

@Eoin-ONeill-Yokai
Copy link
Contributor Author

I just want to ask, how are making the text display one character at a time?

I have a timer set up to increment the visible text number on every tick. If you want to fully explore it, you can find the example project source code on my git repository:

https://github.com/Eoin-ONeill-Yokai/Godot-Rich-Text-Effect-Test-Project

@Calinou
Copy link
Member

Calinou commented Sep 4, 2019

@Eoin-ONeill-Yokai We should add a project to godot-demo-projects to demonstrate the new RichTextLabel features 🙂

How many changes would be required for it to become an official demo?

@Eoin-ONeill-Yokai
Copy link
Contributor Author

Eoin-ONeill-Yokai commented Sep 5, 2019 via email

@Calinou
Copy link
Member

Calinou commented Dec 6, 2019

@Eoin-ONeill-Yokai Out of curiosity, what purpose does CharFXTransform.get_value_or() serve? Wouldn't a ternary operator or Dictionary.get()'s optional default argument suffice here?

@akien-mga
Copy link
Member

akien-mga commented Dec 6, 2019

Out of curiosity, what purpose does CharFXTransform.get_value_or() serve? Wouldn't a ternary operator or Dictionary.get()'s optional default argument suffice here?

Indeed, it might not be necessary. In @skyace65's PR adding the above examples to the docs, the docs actually didn't work as get_or() is undefined (and was apparently renamed get_value_or()). I missed that fact when proofreading and simply used Dictionary.get(key, default) as a replacement, which works well: godotengine/godot-docs@e7819e3

The PR #20627 adding that Dictionary method was merged a few days after this PR, which can explain why @Eoin-ONeill-Yokai missed it and implemented it ad hoc in CharFXTransform. If someone can confirm that get_value_or() is not necessary, it would be good to remove it while we can before the 3.2 release.

@Eoin-ONeill-Yokai
Copy link
Contributor Author

Yeah, it can be removed now. I simply implemented it on my own as I didn't have it available to me at the time. Nice catch @Calinou and @akien-mga.

It does mean that you'll have to get_environment and then get(key, default) but that's more than fine IMO.

@Eoin-ONeill-Yokai
Copy link
Contributor Author

Eoin-ONeill-Yokai commented Dec 12, 2019

@Calinou or @akien-mga , Should I personally handle the removal of the duplicate get_value_or as a new PR or should I leave it to maintainers?

@akien-mga
Copy link
Member

Thanks for the reminder, I'll do it. (Otherwise it would be fine for you or anyone else to do a PR too, but now that I'm on it I'll just push the removal.)

akien-mga added a commit that referenced this pull request Dec 12, 2019
See #23658 (comment)
The method was implemented back when Dictionary.get(key, default) did not
exist, but now that it does we do not need a custom method in CharFXTransform.

It's a new feature in 3.2, so does not break compat with 3.1.x.
@Eoin-ONeill-Yokai
Copy link
Contributor Author

Eoin-ONeill-Yokai commented Dec 12, 2019

Ok... I'll probably get to updating some of my example code on my repository to work with the changes in the future (This weekend?).

marstaik pushed a commit to marstaik/godot that referenced this pull request Dec 24, 2019
See godotengine#23658 (comment)
The method was implemented back when Dictionary.get(key, default) did not
exist, but now that it does we do not need a custom method in CharFXTransform.

It's a new feature in 3.2, so does not break compat with 3.1.x.
@naturally-intelligent
Copy link

Are there instructions on how to add custom effects to a project?

This tutorial doesn't work for me: https://www.youtube.com/watch?v=o-cLQ7J1mc8

@Eoin-ONeill-Yokai
Copy link
Contributor Author

@naturally-intelligent You might want to take a look at my repository of example scripts. I think there are definitely ways the process could be streamlined though.

@naturally-intelligent
Copy link

Thanks Eoin, I have studied your project! My problem is I don't understand the process of adding a custom effect script to a Godot project so that it is recognized by a RichTextLabel.

akien-mga pushed a commit to akien-mga/godot that referenced this pull request Jan 5, 2022
…eviously identical)

Confusingly, these two properties had identical descriptions even though they measure different things.

"relative_index" measures character count from the custom effect's bbcode opening tag.
"absolute_index" measures character count from the start of the bbcode text that includes the custom effect.

See the code author's own explanation here: godotengine#23658

NOTE: Doco for CharFXTransform.xml has changed significantly in 4.0, where terminology has changed to "glyph".  Therefore, proposing this change for 3.x branch only.
(cherry picked from commit 89cebd7)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.