godotjs / javascript

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

@onready raise Error #64

Open dicarne opened 4 years ago

dicarne commented 4 years ago

The code is:

  @onready("..")
  parent: godot.Node;

and some errors were printed in console:

ERROR: object_method: Parameter "bind" is null.     At: modules/ECMAScript/quickjs/quickjs_binder.cpp:146
ERROR: object_method: Parameter "bind" is null.     At: modules/ECMAScript/quickjs/quickjs_binder.cpp:146
ERROR: object_method: Parameter "bind" is null.     At: modules/ECMAScript/quickjs/quickjs_binder.cpp:146  

What's wrong?

averagehat commented 4 years ago

I get the same error.

As a minimal example, replace paintings.jsx with compiled form of this file:

https://gist.github.com/averagehat/55a96c33149b4702dac17a2c9acd2161

this.get_node works but @onready doesn't.

Geequlim commented 4 years ago

I think only default class and classes with '@gdclass' decorator can using this kind of decorators

jerrychoux commented 3 years ago

要怎么使用这个@onready呢?会爆bind错误。现在暂时如下使用。

import { onready, signal, tool } from "../decorators";

@tool
export default class Hud extends godot.CanvasLayer {
  @signal
  static readonly OnStartGame: string;

  //   @onready("scoreLabel")
  scoreLabel: godot.Label;
  //   @onready("message")
  message: godot.Label;
  //   @onready("startButton")
  startButton: godot.Button;
  //   @onready("messageTimer")
  messageTimer: godot.Timer;

  _ready() {
    this.scoreLabel = this.$("scoreLabel") as godot.Label;
    this.message = this.$("message") as godot.Label;
    this.startButton = this.$("startButton") as godot.Button;
    this.messageTimer = this.$("messageTimer") as godot.Timer;
    this.showGameOver();
  }

  showMessage(text: string) {
    this.message.text = text;
    this.message.show();
    this.messageTimer.start();
  }

  async showGameOver() {
    this.showMessage("Game Over");
    await godot.yield(this.messageTimer, "timeout");

    this.message.text = "Dodge the\nCreeps!";
    this.message.show();

    godot.yield(this.get_tree().create_timer(1), "timeout");
    this.startButton.show();
  }
}
ThinCats commented 3 years ago

1

按照Typescript官方文档的说法,propertyDecorator只接收两个参数,忽略了返回值,建议只用于观察属性

property-decorators

NOTE  A Property Descriptor is not provided as an argument to a property decorator due to how property decorators are initialized in TypeScript. This is because there is currently no mechanism to describe an instance property when defining members of a prototype, and no way to observe or modify the initializer for a property. The return value is ignored too. As such, a property decorator can only be used to observe that a property of a specific name has been declared for a class.

目前版本@onready的实现,返回一个descriptor,不是太符合文档中定义。

/**
 * Return the node with `path` if the `_onready` is called
 * @param path The path or the type to get the node
 */
export function onready<T extends godot.Node>(path: string | (new()=>godot.Node)) {
    return function (target: T, property: string, descriptor?: any) {
        const key = `$onready:${property}`;
        descriptor = descriptor || {};
        descriptor.set = function(v) { this[key] = v; };
        descriptor.get = function() {
            let v = this[key];
            if (!v) {
                v = (this as godot.Node).get_node(path as (new()=>godot.Node));
                this[key] = v;
            }
            return v;
        };
        return descriptor;
    }
}

2

造成 Parameter "bind" is null 错误的代码位置可能在https://github.com/GodotExplorer/ECMAScript/blob/master/ecmascript.cpp#L112。Editor会在脚本加载时,通过owner->get(*p)获取对象属性值,调用相应的getter方法。此时,this对象似乎没有bind to native data,导致在getter中this.get_node失败,报错。

3

一个workaround,修改@onready装饰器行为,使其接受一个factory函数,用于在_ready中初始化属性值。这样的行为也符合GDScript官方文档的定义 https://docs.godotengine.org/en/latest/tutorials/scripting/gdscript/gdscript_basics.html#doc-gdscript-onready-annotation

@onready 将factory函数添加到队列中,然后统一在对象的_ready方法执行

export function onready<T extends godot.Node, ValueType>(
    factory: (node: T) => ValueType
) {
    return function (target: T, property: string) {
        const handlersKey = '$onready:handlers'
        let handlers = Reflect.get(target, handlersKey) as {
            property: string
            factory: (node: godot.Node) => unknown
        }[]

        // set onready callbacks
        if (!handlers) {
            handlers = []
            Reflect.set(target, handlersKey, handlers)

            // get _ready
            const oldReady: () => void = Reflect.get(target, '_ready') || (() => {})
            // modify _ready
            Reflect.defineProperty(target, '_ready', {
                value: function () {
                    for (const { property, factory } of handlers) {
                        // init value
                        this[property] = factory(this)
                    }
                    oldReady.call(this)
                },
            })
        }

        // add handler
        handlers.push({ factory, property })
    }
}

之后能够在代码中使用@onready,且没有bind错误

4 Example

export default class Player extends godot.Area2D {
    @onready((n) => n.$('AnimatedSprite'))
    private animatedSprite: godot.AnimatedSprite

    @onready((n: Player) => n.get_viewport_rect().size)
    private screenSize: godot.Vector2

    @onready((n) => n.$('CollisionShape2D'))
    private collisionShape2D: godot.CollisionShape2D

        _ready() {
          console.log(`ready`)
        }
}
SamirL commented 2 years ago

Hi! Any fix regarding this ? It works but still error messages, thanks a lot!

Geequlim commented 2 years ago

Are you missing @gdclass decorator on your class?

SamirL commented 2 years ago

Tried it but still displays the error, but the error doesn't prevent things from working, so I guess I will just ignore it.