godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.16k stars 97 forks source link

Allow enums and local classes to be used outside of the file they are defined #240

Open telaviv opened 4 years ago

telaviv commented 4 years ago

Describe the project you are working on: 2D Fighting Game.

Describe the problem or limitation you are having in your project: I'm writing my project using entirely static typing. Unfortunately, it's currently impossible to share something as simple as enum definition across multiple files. Let's say I have an enum definition in 1 file like so:

# attacks_enum.gd
enum Attacks {
    Normal,
    Special,
    Aerial,
}

If I'd like to use this definition across files outside of attacks_enum.gd, I'm completely out of luck.

# slime.gd

func attack(attack: Attacks):
    pass
# vampire.gd

func attack(attack: Attacks):
    pass

The same goes for classes defined using the class keyword

# puppet.gd
class Puppet:
    pass
# game_simulation.gd

func move_puppet(puppet: Puppet):
    pass

Describe how this feature / enhancement will help you overcome this problem or limitation: The solution I propose will allow the use of the class name for both referencing the class as well as static typing for method definitions.

Show a mock up screenshots/video or a flow diagram explaining how your proposal will work: My solution would be to have both the enum and class keywords make their definitions global by default similar to how the class_name keyword works. This would be a breaking change to how things currently work since now there would be a possibility of global name conflict in existing projects.

This suggestion makes the assumption that most of the time you want to enum/class definition, you really want to use it across your project. For use cases where you really want your enum/class definitions just in your file I propose a new keyword. local. It would work like this:

# local.gd
local enum Attacks {
    Normal,
    Special,
    Aerial,
}

local class Puppet:
    pass
# other.gd
func attack(attack: Attacks): # this fails because Attacks is out of scope
    pass

func move_puppet(puppet: Puppet): # this fails because Puppet is out of scope
    pass

Similarly if we don't want a breaking change, and if we believe that users would prefer a more explicit way of sharing enums/classes we can instead introduce a global keyword.

# global.gd
global enum Attacks {
    Normal,
    Special,
    Aerial,
}

global class Puppet:
    pass
# other.gd
func attack(attack: Attacks): # this works because Attacks is in scope
    pass

func move_puppet(puppet: Puppet): # this works because Puppet is in scope
    pass

If this enhancement will not be used often, can it be worked around with a few lines of script?: For the case of the class keyword, yes. You can create a new file and use the new class_name keyword to have a global reference to the class. For enum, the best you can do is copy and paste the definition every time you want to reference it.

Is there a reason why this should be core and not an add-on in the asset library?: When it comes to implementation of things that involve scopes, that needs to be a language level detail.

lawnjelly commented 4 years ago

Note that you can create an enum to use globally, create an autoload singleton and put the enums in the singleton:

attacks_enum.gd

extends Node

enum Attacks {
    Normal,
    Special,
    Aerial,
}

Usage:

var a = attacks_enum.Attacks.Special

Whether this works for classes too I'm not sure, never tried.

https://docs.godotengine.org/en/3.1/getting_started/step_by_step/singletons_autoload.html

Calinou commented 4 years ago

@lawnjelly I'm fairly sure this works for classes too. The only issue is that you can't use enums as type hints, but this isn't specific to enums being local to a script somehow.

I don't think we need a local/global keyword for enums, as this proposal is purely related to static typing.

telaviv commented 4 years ago

I do kind of also want to highlight how Enum's in GDScript with the introduction of the class_name keyword, are now second class citizens.

I mean consider all these types:

For all these, there is a way to get an instance of that type globally. Every type in GDScript can be created/referenced globally. All types, except ones declared with the Enum keyword.

var a = null
var b = Vector2(0, 0)
var c = Enemy() # declared with class_name
var d = Attacks.Special # this breaks, since Attacks, as an enum, can never be global

In fact an alternative, maybe more radical version of a solution could look something like this:

# attacks_enum.gd
extends Enum

class_name Attacks

_types = [
    'Normal',
    'Special',
    'Aerial',
]

Where Enum would be a sibling type of Object. My goal with this is to hopefully get to some sort of feature parity with classes.

CowThing commented 4 years ago

Using class_name you can access inner classes and enums from other scripts, and you don't need a singleton nor do you need to instance the script. I don't think there needs to be a global keyword, because everything in a script is already global, you just have to specify the script you're getting it from.

test_script.gd

extends Object
class_name TestScript

enum Attacks {
    Normal,
    Special,
    Aerial
}

class test_class:
    var variable = 1

some_other_script.gd

extends Node

func _ready() -> void:
    var attack_type = TestScript.Attacks.Normal
    var my_class : = TestScript.test_class.new()

    do_thing(my_class)

func do_thing(test : TestScript.test_class) -> void:
    test.variable += 1

But it would be nice if enums could be used as types like other values can. At the moment enums are just a shorthand for making a constant dictionary on a class, so it can't be used as a type hint.

bojidar-bg commented 4 years ago

For all these, there is a way to get an instance of that type globally. Every type in GDScript can be created/referenced globally. All types, except ones declared with the Enum keyword.

This should work, no?

class_name Attacks # Or better yet, extends Node and make it a singleton

enum {
    Normal,
    Special,
    Aerial
}
starry-abyss commented 4 years ago

@telaviv Not sure if I understand correctly, but at least for enums I'm just doing this (no autoload or class_name required):

dialogue.gd:

enum ActionType { START_LEVEL, FINISH_LEVEL, OPEN_MAP, UI_INTERACTION }

script2.gd:

var Dialogue = load("res://ui/dialogue.gd")

...

someFunctionUsingEnum(Dialogue.ActionType.UI_INTERACTION)
ghost commented 4 years ago

This has always been possible:

extends Node
const MyEnum = preload("res://script_with_enum.gd").MyEnum
const MyClass = preload("res://script_with_class.gd").MyClass

export(MyEnum) var enum_val

func _ready():
    var obj = MyClass.new()
    obj.wow_cool()
    obj.val = MyEnum.Woah

Ofc, nowadays you can use class_name for this purpose, which is functionally similar to preloading entire scripts in every other script. Anyhow, the points made about this being impossible in the proposal are incorrect; it may not be as convenient as, say, C#, but it's certainly not impossible. I think this proposal should be closed, and separate proposals opened for the various different features being asked for (registering enums in an enum database, registering inner classes in the class database, enums as types.)

nobuyukinyuu commented 4 years ago

Unless/until the proposals to address this are split into their own issues, I'd like to suggest another alternative: File level local scope import, kinda like importing a namespace. This makes it so you don't have to register a class (which can create issues with cyclic dependency), or require a named autoload before every invocation. import or imports would be fine for this.

frozenMustelid commented 4 years ago

/signed

I'm working on an RPG. Enemies can have elements, PCs can have elements, attacks can have elements. I try to use static typing everywhere because I'm used to statically-typed languages and I like both the extra checks and the guarantees that a certain variable will be of a certain type. Trying to work around this enum limitation is frustrating.

dalexeev commented 2 years ago

I don’t know, in this way or in some other way, but probably we should add the ability to declare global enums. Because when enums are used outside of a class, at least 3 name components are required: ClassName.EnumName.ELEMENT_NAME. It's ok, but a little annoying for commonly used enums. Especially now, when autocompletion doesn't work very well.

  1. Enums are types, just like classes. Since we can declare classes globally using class_name, then we need to add the ability to do this for enums as well.
  2. 1426. If built-in enums (such as Error) can be global, then it should also be possible to declare custom enums as global.

  3. This is really inconvenient for commonly used enums. Constantly adding the class name before the enum name does not add readability IMO.

There are other options to achieve a similar effect, such as the use/import keyword (but I don't like that).

dalexeev commented 2 years ago

This should work, no?

class_name Attacks # Or better yet, extends Node and make it a singleton

enum {
  Normal,
  Special,
  Aerial
}

In this case, you will not be able to use Attacks as a type hint.

const MyEnum = preload("res://script_with_enum.gd").MyEnum

Doesn't work in 4.0-dev.

BinaryGleam commented 2 years ago

To me this issue concerns more having something easy to iterate on and distribute. Defining your enum once is handy so everyone can work on the same base, and that same base can be easy to modify. I don't like the idea of something like that being global because not every script will need the enum. But that every script that needs the enum search it in the same place is cool!

Anyway thank everyone for the work arounds! Works well and I don't feel like it's a "dirty" solution!

metacoding commented 2 years ago

This worked for me: CleanShot 2022-10-23 at 20 42 29