Skip to main content
2 of 4
added 6061 characters in body
Theraot
  • 28.3k
  • 4
  • 55
  • 83

Original Answer

I believe Godot does not have a means to check if a call failed...

But you can check your concerns before making the call.

If you are going to call like this:

obj.call(method_name)

You can check if obj has that method like this:

if obj.has_method(method_name):
    obj.call(method_name)

Checking the number of parameters takes more effort, for example:

if has_method(method_name):
    var parameter_count := 0
    for method in obj.get_method_list():
        if method["name"] != method_name:
            continue

        var parameters:Array = method["args"]
        parameter_count = parameters.size()
        break

    if parameter_count == 3:
        obj.call(method_name, arg1, arg2, arg3)

If you also want to check the types, well:

var method_name := "my_method"
var signature := [TYPE_INT, TYPE_STRING]
var valid := false

for method in obj.get_method_list():
    if method["name"] != method_name:
        continue

    valid = true
    var parameters:Array = method["args"]
    if signature.size() != parameters.size():
        valid = false
        break

    for index in parameters.size():
        if parameters[index]["type"] != signature[index]:
            valid = false
            break

    break

if valid:
    obj.call(method_name, 123, "abc")

Oh, you want to check the class too? Hmm...

var method_name := "my_method"
var signature := [TYPE_INT, TYPE_STRING, "Node"]
var valid := false

for method in obj.get_method_list():
    if method["name"] != method_name:
        continue

    valid = true
    var parameters:Array = method["args"]
    if signature.size() != parameters.size():
        valid = false
        break

    for index in parameters.size():
        var expected = signature[index]
        if typeof(expected) == TYPE_STRING:
            if parameters[index]["class_name"] != expected:
                valid = false
                break

        else:
            if parameters[index]["type"] != expected:
                valid = false
                break

    break

if valid:
    obj.call(method_name, 123, "abc", self)

I presume you might want to encapsulate this for ease of reuse, but I'll leave that to you.


New Answer

When would this be useful?

  • You are writing some generic code, duck typing, etc.

    This is bound to what you write, and to code under your control, so you should be able to remove any possible errors.

    Presumably you test your game and identify and fix any problems, so this won't happen in runtime. And no, Godot won't chance versions underneath you (much less in runtime).

  • It might be useful to interface with code beyond your control. For example, if you are making an addon/plugin that calls into other game code yet to be written.

    In this case, I'd believe the underlying problem is the lack of interfaces/contracts in GDScript. Current plan for GDScript is that it will have traits.

    I submit to your consideration using a signal bus. The signal bus will be a global object that both caller and called have access to. So the caller can emit the signal, and the called connect to the signal... Without the caller and called knowing each other. This gives you fully decoupled code.

    For C# in particular, you do have interfaces, so you can define an interface and have the other code implement it.

  • You are doing code generation. And that is what I'll tackle here.

You can generate and run code in runtime in Godot two ways:

  • Create an Script object.
  • Create an Expression object.

Script

I'll create a new GDScript object:

var my_runtuime_script:= GDScript.new()

I need to set its source code, which will be some string with whatever code I need, generated by whatever means I have (I'm using a multiline String literal here):

var my_runtuime_script:= GDScript.new()
my_runtuime_script.source_code = """
func my_generated_func() -> void:
    prints("hello generated world")
"""

And then I need to reload it:

var my_runtuime_script:= GDScript.new()
my_runtuime_script.source_code = """
func my_generated_func() -> void:
    prints("hello generated world")
"""
my_runtuime_script.reload()

And reload will report errors! So if you have a syntax error, you find out here:

var my_runtuime_script:= GDScript.new()
my_runtuime_script.source_code = """
func my_generated_func() -> void:
    prints("hello generated world")
"""
var err := my_runtuime_script.reload()
prints(err)

Assuming everything is OK, we can instantiate the script and call into it:

var my_runtuime_script:= GDScript.new()
my_runtuime_script.source_code = """
func my_generated_func() -> void:
    prints("hello generated world")
"""
var err := my_runtuime_script.reload()
prints(err)
if err == OK:
    var instance:Object = my_runtuime_script.new()
    instance.call("my_generated_func")

Output:

0
hello generated world

*Here the 0 means no error (OK), and hello generated world is the output form the function created at runtime.


Expression

Again we start by creating an object, this time the class is Expression:

var expression := Expression.new()

And we have to give it the code, which has to be an expression. We can define parameters, for example:

var expression := Expression.new()
var parameters := PackedStringArray(["p"])
expression.parse("2 + p", parameters)

And parse returns! So we get any parse errors here:

var expression := Expression.new()
var parameters := PackedStringArray(["p"])
var parse_err := expression.parse("2 + p", parameters)

If there was a parse error, we can call get_error_text:

var expression := Expression.new()
var parameters := PackedStringArray(["p"])
var parse_err := expression.parse("2 + p", parameters)
if parse_err != OK:
    push_error(expression.get_error_text())

Then to execute it, we need to pass the arguments for the parameters we defined:

var expression := Expression.new()
var parameters := PackedStringArray(["p"])
var parse_err := expression.parse("2 + p", parameters)
if parse_err != OK:
    push_error(expression.get_error_text())
else:
    var arguments := [5]
    var result = expression.execute(arguments, null, true)

Here the null is the "self" reference (the instance on which the expression will be executed), and true means we do not want it to show errors... We will handle them!

How do we handle them? Like this:

var expression := Expression.new()
var parameters := PackedStringArray(["p"])
var parse_err := expression.parse("2 + p", parameters)
if parse_err != OK:
    push_error(expression.get_error_text())
else:
    var arguments := [5]
    var result = expression.execute(arguments, null, true)
    if expression.has_execute_failed():
        push_error(expression.get_error_text())
    else:
        print(result)

output:

7

This is 2 + 5

And we can leverage this to make a call:

var method_name := "my_method"
var method_arguments := [123, "abc"]

var expression := Expression.new()
var parameters := PackedStringArray(["method_name", "args"])
var parse_err := expression.parse("callv(method_name, args)", parameters)
if parse_err != OK:
    push_error(expression.get_error_text())
else:
    var arguments := [method_name, method_arguments]
    var result = expression.execute(arguments, obj, true)
    if expression.has_execute_failed():
        push_error(expression.get_error_text())
    else:
        print(result)

Here we attempt to call "my_method" on the object obj with arguments 123 and "abc".

This way you should get an error telling you if the method was not found or if there is any problem with the arguments.

Sadly, you have has_execute_failed returning a bool, and we lack an API to read which the error was other than get_error_text (which I guess you could parse).

However, this works similar to what you want: you call a method to know if there was a problem with the call.

Theraot
  • 28.3k
  • 4
  • 55
  • 83