3
\$\begingroup\$

I'm trying to design a character system for my game. Considering I'll need a player and non-player characters - and non-player characters will further come in many more forms, making use of inheritance sounds almost necessary.

Right now, I'm trying to make every instance hold a dictionary of context options and I need the dictionary to be modified with every level of inheritance. From the documentation I learned that the constructor implicitly calls the parent constructor, and so does the _ready() function as noted here.

... but first I wanted to test it, so I set up following scenes (slightly simplified):

New scene: Character

  • Character (Type: Character - here I'm not sure why, should be just KinematicBody)

... with the attached script:

extends KinematicBody

class_name Character

func _init():
    print_debug(self.to_string(), " Character _init()")

func _ready():
    print_debug(self.to_string(), " Character _ready()")

New inherited scene: Player

  • Player (Inherits: Character.tscn, Type: KinematicBody)

... and detatched the Character.gd script and attached a new script:

extends Character

class_name Player

func _init():
    print_debug(self.to_string(), " Player _init()")

func _ready():
    print_debug(self.to_string(), " Player _ready()")

... and I instantiate one Player scene as a child scene in my main scene (through editor, not code).


For some reason, whenever I instantiate Player, Godot consistently prints this:

[KinematicBody:1422] Character _init()
   At: res://scenes/characters/Character.gd:22:_init()
[KinematicBody:1422] Character _init()
   At: res://scenes/characters/Character.gd:22:_init()
[KinematicBody:1422] Player _init()
   At: res://scenes/characters/player/Player.gd:26:_init()
[KinematicBody:1422] Character _ready()
   At: res://scenes/characters/Character.gd:30:_ready()
[KinematicBody:1422] Player _ready()
   At: res://scenes/characters/player/Player.gd:34:_ready()

However, when I instantiate only Character in its place, the output is as I'd expect:

[KinematicBody:1425] Character _init()
   At: res://scenes/characters/Character.gd:22:_init()
[KinematicBody:1425] Character _ready()
   At: res://scenes/characters/Character.gd:30:_ready()

Where is the extra _init() call coming from? Sounds like something that could make a lot of mess if I didn't notice this and blindly used it.

Also why does Player show as a Type: KinematicBody when hovering over it in the scene inspector and not like Type: Player, the same way Character shows as Type: Character - or why don't they both show up as Type: KinematicBody?

Edit

Following @Vaillancourt's suggestion, I placed breakpoints at each of the print_debug() calls. Stepping through the code, the last 4 prints behave as expected - first there's the Character constructor, implicitly called from the Player constructor, then the body of the Player constructor itself. This is clear from the backtrace. Same for the _ready() calls. But before this, there's always one extra Character constructor call not called from the Player constructor.

Same goes when I put multiple Player instances into the scene. Both had the correct constructor calls preceded by one extra, separate call.

Putting a Character instance along with a Player instance to the scene resulted in a - once again - malfunctioning Player (three _init() and two _ready() calls) and a correctly functioning Character (just one _init() and one _ready() call).

\$\endgroup\$
2
  • \$\begingroup\$ Does godot allow you to put breakpoints in the code to see the stack trace when you get there? \$\endgroup\$
    – Vaillancourt
    Jul 29, 2021 at 13:52
  • 1
    \$\begingroup\$ @Vaillancourt There's definitely a way to place breakpoints, but I have little experience with debuggers (lived my whole life just logging everything), so I actually haven't thought of this. Thank you for the pointer! I'll look into it and see if I can get my answer there. \$\endgroup\$ Jul 29, 2021 at 14:04

1 Answer 1

2
\$\begingroup\$

Updated version of the initialization process (old version):

Node instancing

  1. If the Node is an scene instance, do Scene instancing for the corresponding scene, and take the root node. Otherwise, create a new Node instance.

  2. Script Change. If the Node script is different to the one we want (either it didn't have because we just created a new node, or it has a different one set by scene instancing):

    1. Change the Script.

    2. If the script has new variables, allocate them. Initialize variables which are not onready to their default value if specified, zeroed otherwise.

    3. Send script_changed signal.

    4. Call _init on it. Base class _init runs first.

  3. Set properties (initialize export variables, and any custom setters run if the value is not the default, even if the value is the same it has). I think should consider "Script change" a property that is being set, but it is such an special case.

Scene instancing

  1. Do Note instancing for the notes, from the root to the leafs.

  2. Connect IDE signals. Including connections from and to future children.

  3. Parenting. From root to the leafs:

    1. Add the node to its parent.

    2. NOTIFICATION_PARENTED (18).

  4. NOTIFICATION_INSTANCED (20). On the root node.

  5. Current scene handing. If instancing the current scene:

    1. Add the root node as child to the root Viewport.

    2. NOTIFICATION_PARENTED (18). On the root node.

    3. Entering the scene tree. From root to leafs:

      1. NOTIFICATION_ENTER_TREE (10).

      2. Call _enter_tree on it. Base class _enter_tree runs first.

      3. Send tree_entered signal.

    4. Ready mechanism. From leafs to root:

      1. NOTIFICATION_POST_ENTER_TREE (27).

      2. Initialize any onready variables.

      3. Call _ready on it. Base class _ready runs first.

      4. NOTIFICATION_READY (13).

      5. Send ready signal.


You have a stored scene Character.tscn where you have a KinematicBody that has a script Character.gd.

And then, in another scene, you have an instance of that scene. And to that instance you set a script Player.gd.

So, when that scene instantiates, it loads and instances the other scene (Character.tscn), which has Character.gd. Then Godot runs _init() on it:

[KinematicBody:1422] Character _init()
   At: res://scenes/characters/Character.gd:22:_init()

Then Godot sets the new script (Player.gd). And Godot runs _init() on it, which has to run _init() on the parent class first:

[KinematicBody:1422] Character _init()
   At: res://scenes/characters/Character.gd:22:_init()
[KinematicBody:1422] Player _init()
   At: res://scenes/characters/player/Player.gd:26:_init()

And then Godot moves on to running _ready. Which runs on the parent class first:

[KinematicBody:1422] Character _ready()
   At: res://scenes/characters/Character.gd:30:_ready()
[KinematicBody:1422] Player _ready()
   At: res://scenes/characters/player/Player.gd:34:_ready()

You can confirm the change of script by printing (self.get_script() as GDScript).resource_path. With that added to your debug prints, this is how it looks like:

[KinematicBody:1201] Character _init() res://Main/Character.gd
   At: res://Main/Character.gd:6:_init()
[KinematicBody:1201] Character _init() res://Main/Player.gd
   At: res://Main/Character.gd:6:_init()
[KinematicBody:1201] Player _init() res://Main/Player.gd
   At: res://Main/Player.gd:6:_init()
[KinematicBody:1201] Character _ready() res://Main/Player.gd
   At: res://Main/Character.gd:9:_ready()
[KinematicBody:1201] Player _ready() res://Main/Player.gd
   At: res://Main/Player.gd:9:_ready()

You have the current scene, so Godot goes over scene instancing for it, which does node instancing on its nodes from root to leafs. Which encounters a node that is an instance of Character.tscn. So Godot goes over scene instancing for Character.tscn, which does node instancing on its nodes. There Godot sets the script character.gd. Since the script changed (from none to character.gd) Godot and calls _init on it. That is the first _init call.

After scene instancing for Character.tscn finished, scene instancing for the current scene continues. So it changes the script to player.gd. Since the script changed (from character.gd to player.gd) Godot calls _init on it, but first it calls _init on the base class. So you get two more _init calls.

Afterwards, since we are loading the current scene, it reaches the ready mechanism, where Godot calls _ready. Which are also two calls because Godot call _ready on the base class first.


A few extra notes:

  • When the script changes, there is a script_changed. But if you connect to it from the current scene, you will not get it. This is because the IDE signals has not been connected yet at the time. However, the IDE signals for Character.tscn would have been connected for the second script change (from character.gd to player.gd).
  • The values of the properties of the node are not lost when switching scripts.
  • I don't think there is a way to know ahead of time if the script will be changed.
\$\endgroup\$
3
  • \$\begingroup\$ I had a chance to talk about this yesterday with a few people and was, among other things, forwarded this issue, which talks about this like it's a bug. From your explanation, it sounds like it's (albeit unfortunately) by design. That's why I'm even more confused to hear it should be fixed in the 3.4 release. \$\endgroup\$ Jul 30, 2021 at 8:54
  • 1
    \$\begingroup\$ As a side-note for anyone else coming here, this mechanism (multilevel callbacks) is being entirely removed in the 4.0 release in favor of the explicit super() calls. \$\endgroup\$ Jul 30, 2021 at 8:55
  • 1
    \$\begingroup\$ @MartyCagas It is a design, and not wrong per-se, but NOT the easier to understand and workaround. It wasn't well documented to begin with. There is not a place in the documentation that gives an overall picture of the process, just bits here and there, with gaps in between. I don't know if it is worth patching for 3.4. It makes sense to me it would be as a redesign like they did for Godot 4.0. I went over _ready in more detail in another answer here by the way. \$\endgroup\$
    – Theraot
    Jul 30, 2021 at 9:27

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .