Image sources: https://media.geeksforgeeks.org/wp-content/uploads/20231204181746/FEATURES-OF-SCRIPTING-LANGUAGE.webp
"A scripting language can be defined as a programming language whose primary purpose is to permit users to control and customize the behavior of a software application."
Programming languages can be classified approximately according to a relatively small number of criteria:
$NodePath
, Signal
and Callable
, Tweens
and yield
, @export
and @onready
.It is well worth discussing some other scripting languages, because even GDScript is just simply Godot engine module! In addition, C# is officially supported and community extensions for Lua, F# and other languages also exist.
Also, creating a custom language from scratch is usually not worth the hassle. It is more convenient to select a reasonably well-known and mature scripting language.
Script code can play all sorts of roles within a game engine. Many architectures are thus possible. Here are just a few:
_ready
, _process
, or _physics_process
connect
and emit
functions, which essentially implement the Observer design patternNode
and then put into the scene tree.The primary purpose of many game scripting languages is to implement gameplay features. Some of the most common requirements and features include the following. GDScript is used as a case study wherever possible.
Based on https://gist.github.com/reduz/cb05fe96079e46785f08a79ec3b0ef21
"[Godot uses a] generic glue API that you can use to expose any language to Godot efficiently."
Vector3 MyClass::my_function(const Vector3& p_argname) {
// ...
}
void MyClass::_bind_methods() { // Radicke: this is a static method, defined in the class Object
// Register the custom class to be available in script
ClassDB::register_class<Vector3>();
// Describe the method as having a name and the name of the argument, the pass the method pointer
ClassDB::bind_method(D_METHOD("my_function","my_argname"), &MyClass::my_function);
// Internally, `my_function` and `my_argument` are converted to a `StringName` [...],
// so from now onwards they are treated just as a unique pointer by the binding API.
}
// Radicke: Somewhere in Object, Variant, or GDExtension...
// Not really done like this, but simplifying as much as possible so you get an idea:
static void my_function_ptrcall(void *instance, void **arguments, void *ret_value) {
MyClass *c = (MyClass*)instance;
Vector3 *ret = (Vector3*)ret_value;
*ret = c->my_method( *(Vector3*)arguments[0] );
}
Script functions often need to interact with game objects, which themselves may be implemented partially or entirely in the engine’s native language. However, the pointers or references in C++ won’t necessarily be valid in the scripting language! Options to accomplish this include:
self
StringName
to identify all types, classes and their members, as well as functions and their parametersClassDB
stores all information about classes in a static HashMapstatic HashMap<StringName, ClassInfo> classes;
ClassInfo
can then quickly be retrieved based on the hashed name via a HashMap lookupGDCLASS(m_class, m_inherits)
, defined in core/objectr/object.h, adds boilerplate code to classes so they can properly integrate with ClassDB
ObjectID
is used to identify individual object instances throughout the engineStringName
, e.g. in a HashMap, to find or identify objects by nameA variant is a data object that is capable of holding more than one type of data. In Godot, all built-in types are defined in a the class Variant
which is used to exchange data between languages and interfaces:
// core/variant/variant.h
class Variant {
public:
// If this changes the table in variant_op must be updated
enum Type {
NIL,
// atomic types
BOOL,
INT,
FLOAT,
STRING,
// Radicke: MANY MORE TYPES...
};
//...
struct ObjData {
ObjectID id;
Object *obj = nullptr;
};
//...
union {
bool _bool;
int64_t _int;
double _float;
Transform2D *_transform2d;
::AABB *_aabb;
Basis *_basis;
Transform3D *_transform3d;
Projection *_projection;
PackedArrayRefBase *packed_array;
void *_ptr; //generic pointer
uint8_t _mem[sizeof(ObjData) > (sizeof(real_t) * 4) ? sizeof(ObjData) : (sizeof(real_t) * 4)]{ 0 };
} _data alignas(8);
// Radicke: MANY FUNCTIONS FOR CONSTRUCTION, ACCESS, CONVERSION, OPERATORS, ...
};
Based on https://gdscript.com/solutions/signals-godot/
"The Godot signalling system is actually an implementation of the Observer Pattern [...]. Events that happen within the system emit signals that are subscribed to by interested observers which run handler code in response to the events. [...] loose coupling between the emitter and observer [...] [allow them] to function independently."
Image sources: https://res.cloudinary.com/otakucms/image/upload/gdscript/godot-signals-2_tgvmvb.webp
// core/object/object.h
// ...
class Object {
// ...
struct Connection {
::Signal signal; // Radicke: defined in core/variant/callable.h
Callable callable;
uint32_t flags = 0;
bool operator<(const Connection &p_conn) const;
operator Variant() const;
Connection() {}
Connection(const Variant &p_variant);
};
// ...
void add_user_signal(const MethodInfo &p_signal);
template <typename... VarArgs>
Error emit_signal(const StringName &p_name, VarArgs... p_args) {
// ...
}
Error emit_signalp(const StringName &p_name, const Variant **p_args, int p_argcount);
bool has_signal(const StringName &p_name) const;
void get_signal_list(List<MethodInfo> *p_signals) const;
// ...
Error connect(const StringName &p_signal, const Callable &p_callable, uint32_t p_flags = 0);
void disconnect(const StringName &p_signal, const Callable &p_callable);
bool is_connected(const StringName &p_signal, const Callable &p_callable) const;
// ..
};
signal signal_name(parameter_1_name, ...)
add_user_signal(MethodInfo("signal_name", PropertyInfo(Variant::[[Type]], "parameter_1_name"), ...));
ADD_SIGNAL(MethodInfo("signal_name", PropertyInfo(Variant::[[Type]], "parameter_1_name"), ...));
_bind_methods
.signal_name.emit(parameter_1_name, ...)
emit_signal("signal_name", [[paramater_1_type parameter_1_name]], ...);
var error = sending_node.connect("signal_name", Callable(receiving_node, "_on_Signal_Name"))
Error error = p_sending_object->connect("signal_name", callable_mp(p_receiving_object, &ReceivingClass::_on_Signal_Name));
p_sending_object->connect("signal_name", Callable(p_receiving_object, "_on_Signal_Name"));
_on_Signal_Name
was previously registered in _bind_methods
.func _on_Signal_Name(parameter_1_name, ...): # ...
void ReceivingClass::_on_Signal_Name([[paramater_1_type parameter_1_name]], ...) // ...
Some scripting languages are inherently object-oriented, others do not support objects directly but provide mechanisms that can be used to implement classes and objects. The game object model is object oriented in most engines, thus OO scripting makes a lot of sense.
Based on https://www.analyticsvidhya.com/blog/2020/09/object-oriented-programming/