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

Add static variables in GDScript #6156

Closed
vnen opened this issue Jan 23, 2023 · 14 comments · Fixed by godotengine/godot#76264
Closed

Add static variables in GDScript #6156

vnen opened this issue Jan 23, 2023 · 14 comments · Fixed by godotengine/godot#76264
Milestone

Comments

@vnen
Copy link
Member

vnen commented Jan 23, 2023

Describe the project you are working on

GDScript.

Describe the problem or limitation you are having in your project

There's no practical way to share data across the instances of the same class. The available solutions to this problem are cumbersome to use:

  • Use a const Dictionary and edit its values.
    • Remove the benefits of checks done at compile-time.
    • There's a chance all references to the script is lost which will unload it and delete your custom changes.
    • Not possible in 4.0 anymore as dictionary and array constants will be read-only.
  • Use an autoload singleton to keep static data.
    • Needs an extra class just for the data, which either needs a global one with everything or a lot of different autoloads.
    • Only accessible via qualifier (Singleton.data) instead of directly (data).

This makes autoloads the only way to store global data which can be cumbersome if you want them to be tied to a particular class. Given autoloads are always Nodes, it limits what you can do even if you are okay with using the same class as an autoload and as a base for other instances.
Many users in favor of static variables were against having to use autoloads for this.

Previous proposals for a feature like this:

Describe the feature / enhancement and how it helps to overcome the problem or limitation

Static variables provide a cleaner way to achieve global data, in particular when the data is pertinent to different instances of the same class.

Syntax:

static var foo = 1

They can be used by static functions as well.

static var foo = 1
static func bar():
	print(foo)

That is also available for inner classes:

class Foo:
	static var bar

There is no static constructor, so to have a value initialized when the script is loaded, you have to assign on the declaration (e.g. static var x = "here"). Those will be set when the script is loaded.

Static variables can also be typed:

static var name := "Me"
static var value: int

Once you use a static var in your class, it will not be unloaded until the application exits. This guarantees that the values stored in static variables aren't lost by accident. It also means that you are preloading heavy resources in a script with static variables (even if the preload isn't on a static variable itself) it will occupy memory until the game is closed. If an inner class has a static variable, its outermost script (the file it's defined in) will be kept loaded. This will be done by a list in GDScriptCache that will keep a reference to those scripts so they stay alive.

To solve this I have two proposals (not mutually exclusive):

1) Have a method in GDScript to unload scripts with static variables. E.g.

# file1.gd
class_name MyClass
static var x = 0

# file2.gd
func test():
	MyClass.unload_static()

This method will not immediately unload the class. It will simply remove the reference from GDScriptCache so it can be freed once nothing else references it. The name of this method can be different to better reflect what it does.

Note that if the class is loaded again, it will need another call to unload_static() if you want it to be freed.

2) Have an annotation that makes a script not stay in memory. This essentially makes the script behaves as if it doesn't have any static variable. E.g.

@ephemeral
class_name NotReallyStatic
static var foo = null

This can be used if you don't care about losing the data in static variables and don't want to manage the memory manually. Again, name is not final.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

Static variables will use a similar code to what is used for constants in GDScript, with the difference that they will allow being assigned to.

A list of scripts to be kept alive will be added to GDScriptCache, along with methods to add/remove items from the list. The GDScript compiler will detect usage of static variables to decide whether the script should be in the list or not (as well as the annotation), and proceed to store it in the cache if necessary.

The unload_stack() method will simply remove the reference to itself from the GDScriptCache. If no other reference is alive, this will proceed to free the script object.

If this enhancement will not be used often, can it be worked around with a few lines of script?

The only available work around is to use autoloads, which does limit how this can be used. It becomes cumbersome if needed often.

Is there a reason why this should be core and not an add-on in the asset library?

It must be implemented in the core given it's part of the GDScript syntax.

@anvilfolk
Copy link

For a preliminary implementation, we can also just let users manage memory themselves: if they want to free memory used up by static variables, they can just do my_static_var = null :)

@YuriSizov
Copy link
Contributor

I don't care much for unloading, as all my use cases need persistent data, and indeed I can clear the unused bits myself as needed. That said, out of those two options, the method one seems simpler to use situationally, and an extra annotation can always be added later.

@Calinou Calinou changed the title Static variables in GDScript Add static variables in GDScript Jan 23, 2023
@KoBeWi
Copy link
Member

KoBeWi commented Jan 23, 2023

The available solutions to this problem are cumbersome to use:

You forgot the most cumbersome of them all, which is still available in 4.0 btw.

@vnen
Copy link
Member Author

vnen commented Jan 24, 2023

For a preliminary implementation, we can also just let users manage memory themselves: if they want to free memory used up by static variables, they can just do my_static_var = null :)

It's not only static variables, you may have other data that you cannot clear without unloading the script (mainly with preload). For instance:

static var forever = 0
func _ready():
	var scene = preload("res://super_big_scene.tscn")
	add_child(scene.instantiate())

In this case, while the static variable is negligible memory-wise, the preloaded scene will still be attached to the script and there is no way you can unload it unless we add special means for it.

@zinnschlag
Copy link

Maybe store the static variables of all scripts in a central repository independently from the script? Some kind of dictionary maybe?

This implementation would bypass the whole baggage of having to keep the script loaded. You could unload the script at any time and after reloading it can use the still available data from the repository.

This would also avoid the need for any kind of special treatment or clean up function. To remove the data from memory you could just assign null to the variable.

@vnen
Copy link
Member Author

vnen commented Jan 24, 2023

Maybe store the static variables of all scripts in a central repository independently from the script? Some kind of dictionary maybe?

I feel like this would be quite complex to implement and maintain. In particular keeping a valid connection between script and its static variables even when reloaded, especially considering there are built-in scripts (present in a scene file) and scripts created at runtime.

@dalexeev
Copy link
Member

There is no static constructor, so to have a value initialized when the script is loaded, you have to assign on the declaration (e.g. static var x = "here"). Those will be set when the script is loaded.

Non-static variable initializers do not have to be constant expressions, you can call methods. If this applies to static variables as well, then this can be worked around like this:

static var _class_loaded := _static_init()

static func _static_init() -> bool:
    # Your code here.
    return true

In this case, it makes sense to add a standard "static constructor" with a name that reflects that this method is not called when the application starts, but when the script is loaded.

@vnen
Copy link
Member Author

vnen commented Apr 13, 2023

There is no static constructor, so to have a value initialized when the script is loaded, you have to assign on the declaration (e.g. static var x = "here"). Those will be set when the script is loaded.

Non-static variable initializers do not have to be constant expressions, you can call methods. If this applies to static variables as well, then this can be worked around like this:

What I meant is that there is no custom static constructor. But the implementation does require an internal static constructor in order to properly execute the initializers (since they're not constant).

Given this, it would be easy to allow custom static constructors as well and just call it from the internal one.

@4d49
Copy link

4d49 commented Apr 27, 2023

Maybe also add a static signal?

@vnen
Copy link
Member Author

vnen commented Apr 27, 2023

Maybe also add a static signal?

@4d49 this is a different beast to tackle. If you need it you should open a proposal specifically about it.

@4d49
Copy link

4d49 commented May 10, 2023

If you need it you should open a proposal specifically about it.

@vnen done #6851

@ttencate
Copy link

Unfortunately, this proposal is silent about the order of static initialization. The C++ standard has the same omission, leading to the Static Initialization Order Fiasco.

If script B's _static_init() (or an inline static initializer) refers to a static variable in script A, is it guaranteed that A's statics are already initialized?

@vnen
Copy link
Member Author

vnen commented Jun 7, 2023

Unfortunately, this proposal is silent about the order of static initialization. The C++ standard has the same omission, leading to the Static Initialization Order Fiasco.

@ttencate given GDScript has a single official implementation, it is not as dependent of a standard as C++. Also, it does not really have the concept of "translation unit" like C++ does, so the order is always consistent.

If script B's _static_init() (or an inline static initializer) refers to a static variable in script A, is it guaranteed that A's statics are already initialized?

If B is accessing anything from A then it means A is already fully loaded, which in turn means the _static_init() is from A is already complete.

The only issue is if there is a dependency cycle. If A._static_init() uses something from B, and B._static_init() uses something from A, then whichever loads first won't be able to access the other one (such order depends on how your scripts are laid out). But there's no sane way to solve this anyway, it should probably be consider an error.

@LeonardMeagher2
Copy link

I'm curious what the real world use case of this ever was. I see a lot of hypotheticals. ( Maybe I missed something )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants