godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.17k stars 98 forks source link

Add anonymous classes to GDScript #10132

Open gunbuilderguy opened 4 months ago

gunbuilderguy commented 4 months ago

Describe the project you are working on

I'm currently porting a java game that heavily uses anonymous classes amongst other things. I've managed to cope with things like passing from java's generous enum behaviours by making them public static constants etc.

But anonymous classes is something i cannot easily cope with without, here's a simple dummy example:

if(usecase_1){
  CombatManager manager = new CombatManager("fast-ride"){
    @Override
    public float getAggressionWeighing(GameCharacter target, ArrayList<GameCharacter> Enemies){
      switch(target.getDefaultStance()){
        case STANCE_FRONT:
          ...
      }
    }
  }
} else if(usecase_2){
  CombatManager manager = new CombatManager("indoors"){
//12 other cases

Describe the problem or limitation you are having in your project

The limitation here comes to translating this to godot while keeping it reasonably readable, as the current ways to translate it is to:

This kind of limitation becomes especially painful when you make multiple constants that are based off anonymous classes, for example; a colour database that has functions for more specific behaviors:

  public static Colour RED = new Colour(false, BaseColour.RED, "red", "red").setLinkedLighter(RED_LIGHT);

  public static Colour COVERING_RAINBOW = new Colour(false, BaseColour.BLUE_LIGHT,
      "<span style='color:#E64C4C;'>r</span>"
      + "<span style='color:#E6854C;'>a</span>"
      + "<span style='color:#E6C74C;'>i</span>"
      + "<span style='color:#6EE64C;'>n</span>"
      + "<span style='color:#4CB2E6;'>b</span>"
      + "<span style='color:#AD4CE6;'>o</span>"
      + "<span style='color:#E64CA8;'>w</span>", 
      "rainbow") {
    @Override
    public List<String> getRainbowColours() {
      return Util.newArrayListOfValues(
        "#E64C4C",
        "#E6854C",
        "#E6C74C",
        "#6EE64C",
        "#4CB2E6",
        "#AD4CE6",
        "#E64CA8");
    }
  };

In the game's database, most colours don't use anonymous classes, but given there are about five hundred members, the translated version ends up with nearly a hundred script files acting as anonymous classes, and having them be inner classes would make the colour database much harder to read than it has any reason to.

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

Adding an anonymous class structure in GDscript would go a long way fixing that kind of translation woe, something similar to:

var watch : Item = Item.new("watch", args[...]):
  func advance(ms : float, owner: GameCharacter) -> void:
    self.customData["timerS"] = self.customData.get("timerS", 0) + ms*0.001

equivalent to

class watch extends Item:
  func advance(ms : float, owner: GameCharacter) -> void:
    self.customData["timerS"] = self.customData.get("timerS", 0) + ms*0.001

and

class_name watch
extends Item

func advance(ms : float, owner: GameCharacter) -> void:
  self.customData["timerS"] = self.customData.get("timerS", 0) + ms*0.001

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

The compiler, specifically in GDScriptCompiler::_prepare_compilation, handles inner classes, the code can be reused in the individual function compiling to prepare the compilation of those anonymous classes when processing those functions to treat them in a similar fashion.

Currently, i'm sifting through the code and i'm working towards making a PR that would allow anonymous classes, figuring out the token system and its handlers. If this issue is just not brought up enough (a few reddit posts or godot forum posts), i'll figure out in time a working PR to implement it in godot.

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

As specified above, it CAN be worked around but it comes at the cost of readability, and in the case of the colour database mentioned bloats the script to about twice its line count on top of hurting its readability. And as an extra inconvenience, you will have your autocomplete flooded with dozens of classes that are meant to be anonymous.

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

I don't think this can simply be made in an add-on at that level of code.

gunbuilderguy commented 4 months ago

In short, the addition would consist of modifying GDScriptParser::parse_precedence or add to the tokenizer's get_rule's colon token an anonymous class parser infix, getting the node's class and making a class extending it.

The problem i can see arising would be that if the value is already parsed and creates a new instance of that existing class, that you would have to assign it as being that inheriting anonymous class, and my knowledge of how that works is too limited to know if that is feasible. I at the very least think this is possible in theory.

Ivorforce commented 2 months ago

Anonymous classes were very important in java, for example they were used to implement lambda functions in the stream APIs (iirc). In many languages, you can work around most similar use-cases pretty easily with callables and inner classes, both of which are supported in gdscript already.

jujuteux commented 1 month ago

Anonymous classes were very important in java, for example they were used to implement lambda functions in the stream APIs (iirc). In many languages, you can work around most similar use-cases pretty easily with callables and inner classes, both of which are supported in gdscript already.

Sadly, while you can do something similar to an anonymous class in godot with inner classes (make an in-file inner class with some arbitrary name and make it override what you need then instantiate it) or with separate scripts (make a .gd file without class_name and instantiate it), both cases hurt readability, often forcing you to ping pong between windows/files.

Callables aren't really a good idea as it forces you to make more or less every base function in the class a callable variable with base value, just incase it gets modified later on and it's just bad practice and a new avenue for problems to infiltrate through.

I'm still fiddling around with the complier code to see if i can make a Class.new(): pattern be transcribed as an inner class definition extending that class and as an initializer of that extending class