godotjs / javascript

Javascript binding for godotengine
https://godotjs.github.io/
MIT License
963 stars 84 forks source link

cannot read property 'emit_signal' of null when emitting a custom signal #127

Closed Terria-K closed 1 year ago

Terria-K commented 2 years ago

Hello, I've having an issue about emitting the custom signal, I think it's broken. The built-in signal works just fine, but the custom are just having an issue. image

This is my code:


// UIPause.tsx
import { signal } from "../../decorators";

export default class UIPause extends godot.Control {
    @signal static readonly OnContinue: string;
    @signal static readonly OnExit: string;

    private continueButton: godot.Button;
    private exitButton: godot.Button;

    public override _ready(): void {
        this.continueButton = this.get_node<godot.Button>('ButtonContainer/Continue');
        this.exitButton = this.get_node<godot.Button>('ButtonContainer/Exit');
        this.continueButton.connect('pressed', this.onContinuePressed);
        this.exitButton.connect('pressed', this.onExitPressed);
    }

    private onContinuePressed(): void {
        godot.print('Continue') // works fine, prints Continue
        this.emit_signal("OnContinue"); // throws an error
    }

    private onExitPressed(): void {
        this.emit_signal("OnExit"); // also throws an error
    }
}

// World.tsx
import CameraControl from "../actors/CameraControl";
import Player from "../actors/Player";
import Scene from "../ecs/Scene";
import UIPause from "../ui/UIPause";

export default class World extends Scene {

    private player: Player;
    private camera: CameraControl;
    private pauseLayer: UIPause;

    constructor() {
        super();
    }

    public override initialize(): void {
        /** Snip */
    }

        // Will Run after the initialize method
    public override ready(): void {
        this.camera.setTarget(this.player.Body);

                // Connecting all of the Signals
        this.pauseLayer.connect('OnContinue', this.onContinuePlaying);
        this.pauseLayer.connect('OnExit', this.onExitMe);
    }

    public override _input(event: godot.InputEvent): void {
        /** Snip */
    }

        // Won't be called
    public onContinuePlaying(): void {
        this.player.active = true;
        this.pauseLayer.visible = false;
    }

        // Wont be called
    public onExitMe(): void {
        this.get_tree().quit();
    }
}
mageowl commented 2 years ago

May or may not help, but sometimes putting @gdclass on your class can help when using decorators.

Terria-K commented 2 years ago

May or may not help, but sometimes putting @gdclass on your class can help when using decorators.

It doesn't work too, but I found a workaround and I don't know why this works, but it shouldn't be this way too.

public override _ready(): void {
        this.continueButton = this.get_node<godot.Button>('ButtonContainer/Continue');
        this.exitButton = this.get_node<godot.Button>('ButtonContainer/Exit');
        this.continueButton.connect('pressed', () => {
            this.emit_signal("OnContinue");
        });
        // It also works with string method
        this.exitButton.connect('pressed', () => {
            this.emit_signal("OnExit");
        });
}

It works with Lambda or strings instead, I don't know why but it works, they should better make a custom error for that instead of a cryptic message. I will keep the issue open from now on.

mageowl commented 2 years ago

Ohhh...

This is a thing with basic JS. When passing a method to a function as a callback, the parent scope or this is reset to the scope of the function that calls it (or null, because native function don't have scope.) due to the fact that it is being separated from the parent, as your passing the function, not the parent, and the function does not contain any information about the parent. Lambdas fix this problem by automatically .bind()ing the function to the current this scope.

You can change your connect to

this.connect("pressed", this.onPressed.bind(this))

or the method declaration to

class MyClass {

  onPressed: Function = () => {
    ...
  }

}
mageowl commented 2 years ago

(also the errors are just piped from JS, they can't change it without rewriting the compliler)