Checking a method signature with reflection
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.
Checking with code generation
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.
Checking with is_valid
from Callable
I originally skipped callables, because I took the question as face value.
Yet, Callable
has an is_valid
method that looks promising. So, let us define a callable:
var my_callable := Callable(obj, "my_method")
And check if it is valid before calling:
var my_callable := Callable(obj, "my_method")
if my_callable.is_valid():
my_callable.call()
else:
push_error("invalid callable")
The problem is that this does not take into account the arguments. We might bind the arguments (this returns a new callable):
var my_callable := Callable(obj, "my_method")
my_callable = my_callable.bind(123, "abc")
if my_callable.is_valid():
my_callable.call()
else:
push_error("invalid callable")
But - upon testing - is_valid
does not check if the bound arguments are valid. Thus, this call
might still fail.