Open menu with table of contents Scripting Languages
Logo of Stuttgart Media University for light theme Logo of Stuttgart Media University for dark theme
Stuttgart Media University

1 Definition

"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."

  • Examples:
    • Visual Basic in MS Office, MEL/Python in Maya, Lua in CRYENGINE, Blueprints in Unreal, ...
    • ...and of course GDScript in Godot!
  • A scripting language (in a game engine) is...
    • A high-level and relatively easy-to-use programming language
    • It provides convenient access to most of the commonly used features of the engine
    • It can be used by programmers and non-programmers alike for game logic, modding, and so on.

1.1 Runtime versus Data Definition

  • Game scripting languages generally come in two flavors:
    • Data-definition language
      • Permit users to create and populate data structures
      • Such languages are often declarative (like HTML, XML, TeX, CSS, SQL, ...)
      • Are either executed or parsed offline or at runtime when the data is loaded into memory
      • Godot supports JSON, which is also a declarative language
      • Godot's ConfigFile (INI-style files) could also be put in this category
    • Runtime scripting language
      • Are intended to be executed within the context of the engine at runtime
      • Used to extend or customize the hard-coded functionality of game objects and/or other engine systems
      • GDScript is a runtime scripting language

2 Programming Language Characteristics

Programming languages can be classified approximately according to a relatively small number of criteria:

  • Interpreted versus compiled languages
    • A compiled language is translated by a compiler into machine code which is then executable directly by the CPU
    • An interpreted language is parsed directly at runtime or is precompiled into platform-independent byte code which is executed by a virtual machine (VM)
      • Can be easily ported/embedded to/within any platform (e.g. a game engine)
    • The biggest cost is execution speed: Byte code instructions run much more slowly than native machine code
  • Imperative languages
    • A program is described by a sequence of instructions, each of which performs an operation and/ or changes the state of data in memory (e.g. C/C++)
  • Declarative languages
    • Describes what is to be done but does not specify exactly how the result should be obtained
    • The how is left up to the people implementing the language
  • Functional languages (technically a subset of declarative languages)
    • Aim to avoid state altogether
    • Programs are defined by a collection of functions
    • Each function produces its results with no side-effects
    • A program is constructed by passing input data from one function to the next until the final desired result has been generated
    • Well-suited to implementing data processing pipelines
    • Examples include OCaml, Haskell and F#
  • Procedural versus object-oriented languages
    • In a procedural language, the primary atom of program construction is the procedure (or function)
      • Procedures and functions perform operations, calculate results and/or change the state of various data structures in memory
    • An object-oriented language’s primary unit of program construction is the class
      • A data structure that is tightly coupled with a set of procedures/functions that "know"” how to manage and manipulate the data within
  • Reflective languages
    • Information about the data types, data member layouts, functions and hierarchical class relationships are available for inspection at runtime
    • C# is an example of a reflective language, while C/C++ are examples of non-reflective languages
      • Note: This is why additional debug symbols are needed to debug C/C++

2.1 Characteristics of Game Scripting Languages

  • Interpreted
    • interpreted by a virtual machine (VM), not compiled
    • Flexible, portable and allows for rapid iteration
    • Can be represented as platform-independent byte code
    • Can easily be treated like data by the engine (loaded/unloaded like any other resource)
    • Offers great flexibility regarding how and when script code will be run
    • GDScript is an interpreted language
  • Lightweight
    • For most game scripting languages their VMs are simple, and their memory footprints are quite small
  • Support for rapid iteration
    • When script code is changed, the effects of the changes can usually be seen very rapidly
    • There is no need to recompile (or perhaps not even to restart) the game
    • Therefore, the turnaround time between making a change and seeing its effects is much faster
  • Convenience and ease of use
    • Features that make common tasks simple, intuitive and less error-prone
    • E.g. custom functions or syntax can be provided for finding game objects, sending and handling events, pausing/waiting, exposing parameters, and so on.
      • Respective GDScript examples are $NodePath, Signal and Callable, Tweens and yield, @export and @onready.

3 Some Common Game Scripting Languages

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.

  • QuakeC
    • Originally created by Id Software’s John Carmack a a custom scripting language for Quake
    • Essentially a simplified variant of C (though no pointers or structs)
    • It could manipulate entities and receive/handle game events
    • It was one of the factors that gave birth to the mod community
  • Lua
    • One of the most well-known scripting languages
    • It is robust and mature, has good documentation, excellent runtime performance, is portable, designed for embedded systems, simple, powerful yet extensible, and free.
      • Its runtime requires only ~350 KiB for the interpreter and all libraries!
    • It is dynamically typed and its primary data structure is the table (an associative array)
      • OO features are realized using meta tables, i.e. tables that contain information about other tables
    • It provides a convenient interface to the C language (stack with push/pop semantics)
    • Code blocks (aka chunks) can be executed in source code format or in precompiled byte code format
      • A simple string can be executed as code during runtime!
    • It features powerful advanced programming constructs like coroutines (yield)
    • One of Lua's major drawbacks is that it only distinguishes between 'global' and 'local' scopes
      • E.g. it is very easy to accidentally overwrite/redefine a global meta table and thus destroy the program's functionality.
  • Python
    • One of the most popular scripting languages in the AI community
    • A procedural, object-oriented, dynamically typed scripting language designed with ease of use, integration with other programming languages, and flexibility in mind
    • It has clear and readable syntax, is well documented, it is a reflective language, and it is modular, supporting hierarchical packages
    • It has exception-based error handling, provides extensive standard libraries and third-party modules
    • Code indentation serves as the only means of defining scope
      • GDScript uses this principle as well
    • Its primary data structures are a list an a dictionary (key-value pairs)
      • GDScript features arrays and dictionaries
    • It supports duck typing, where the functional interface of an object determines its type
      • This allows the support of polymorphism without requiring the use of inheritance
      • GDScript features duck typing as well -> if an object has a function/parameter, it can be called/accessed, regardless of its "type"
  • Pawn/Small/Small-C
    • A lightweight, dynamically typed, C-like scripting language created by Marc Peter
    • Designed to have a small memory footprint and to execute its byte code very quickly
    • It natively supports finite state machines, which makes is very useful for games

4 Architectures for Scripting

Script code can play all sorts of roles within a game engine. Many architectures are thus possible. Here are just a few:

  • Scripted callbacks
    • Functionality is largely hard-coded but certain key bits are designed to be customizable
    • Often implemented via a hook function or callback
    • GDScript examples include _ready, _process, or _physics_process
  • Scripted event handler
    • A special type of hook function whose purpose is to allow a game object to respond to some relevant occurrence
    • GDScript's signals using connect and emit functions, which essentially implement the Observer design pattern
  • Extending game object types, or defining new ones, with script
    • This can be achieved via inheritance and/or composition
    • It allows the extension of native types or the creation of new types via script
    • GDScript supports both
      • E.g. a custom type can be derived from Node and then put into the scene tree.
  • Scripted components or properties
    • New components or property objects can be constructed partially or entirely in script
    • E.g. The Unity engine allows you to create your own components using scripts
  • Script-driven engine
    • Script might be used to drive an entire engine system, including the game object model
    • E.g. the LÖVE engine is largely based on Lua (though it has a C++ core)
  • Script-driven game
    • The script code runs the whole show, and the native engine code acts merely as a library that is called to access certain high-speed features of the engine
    • E.g. In the Panda3D engine, games are written in Python and the native engine (implemented in C++) acts like a library

5 Features of a Runtime Game Scripting Language

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.

5.1 Interface with the Native Programming Language

  • The game engine needs to be able to execute script code
  • The script code must to be capable of initiating operations within the engine
  • A runtime virtual machine (VM) is generally embedded within the game engine
    • Runs the script code and manages those scripts’ execution
    • Godot uses a more direct form of code execution (see below)

GDScript Language Binding System

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] );
}

5.2 Game Object References

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:

  • Opaque numeric handles can be used to refer to objects in script
    • Handles can be obtained, for example, via queries or string names
    • The script can then call native functions by passing the object’s handle as an argument
    • Efficient and simple, but somewhat difficult to work with
  • Strings as handles
    • Strings are human-readable and intuitive to work with
    • There is a direct correspondence to the names of the objects in the game’s world editor
    • Possibility for "magic meanings" such as self
    • However, strings typically occupy more memory than integer ids and dynamic memory allocation is required
      • Hashed string ids overcome most of these problems by converting any strings (regardless of length) into an integer
      • Ideally, we’d like the script compiler to convert our strings into hashed ids for us

Godot Game Object References

  • The Godot language bindings use StringName to identify all types, classes and their members, as well as functions and their parameters
  • The singleton class ClassDB stores all information about classes in a static HashMap
    • static HashMap<StringName, ClassInfo> classes;
    • ClassInfo can then quickly be retrieved based on the hashed name via a HashMap lookup
    • The macro GDCLASS(m_class, m_inherits), defined in core/objectr/object.h, adds boilerplate code to classes so they can properly integrate with ClassDB
  • The class ObjectID is used to identify individual object instances throughout the engine
    • This might be paired with StringName, e.g. in a HashMap, to find or identify objects by name

Godot's Variant Type

A 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, ...
};

5.3 Receiving and Handling Events in Script

  • Events are a ubiquitous communication mechanism in most game engines
  • Event handler functions in script, are a powerful avenue for customizing the hard-coded behavior of our game
  • The event handler functions themselves can be simple script functions, or they can be members of a class if the scripting language is object oriented
  • Events are usually sent to individual objects and handled within the context of that object
    • Scripted event handlers could be registered on a per-object-type basis
    • OR event handlers may be associated with individual object instances
      • Then, different instances of the same type might respond differently to the same event

5.4 Sending Events

  • The ability to generate and send events from script code either back to the engine or to other scripts makes events even more powerful
  • We’d like to be able not only to send predefined types of events from script but to define entirely new event types in script
    • This is trivial if event types are strings or string ids (which is the case in Godot)
  • In some game engines, event- or message-passing is the only supported means of inter-object communication in script
    • GDScript also supports direct function calls on objects

Godot's Event System: Signals

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

Godot's Object Class
// 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;

  // ..
};

Godot Signals How-To

  • Defining Signals
    • GDScript
      • signal signal_name(parameter_1_name, ...)
    • C++
      • add_user_signal(MethodInfo("signal_name", PropertyInfo(Variant::[[Type]], "parameter_1_name"), ...));
      • or...
      • ADD_SIGNAL(MethodInfo("signal_name", PropertyInfo(Variant::[[Type]], "parameter_1_name"), ...));
        • This must be called in _bind_methods.
  • Emitting Signals
    • GDScript
      • signal_name.emit(parameter_1_name, ...)
    • C++
      • emit_signal("signal_name", [[paramater_1_type parameter_1_name]], ...);
  • Connecting Signals
    • GDScript
      • var error = sending_node.connect("signal_name", Callable(receiving_node, "_on_Signal_Name"))
    • C++
      • Error error = p_sending_object->connect("signal_name", callable_mp(p_receiving_object, &ReceivingClass::_on_Signal_Name));
      • or...
      • p_sending_object->connect("signal_name", Callable(p_receiving_object, "_on_Signal_Name"));
        • Note that this only works if _on_Signal_Name was previously registered in _bind_methods.
  • Receiving Signals
    • GDScript
      • func _on_Signal_Name(parameter_1_name, ...): # ...
    • C++
      • void ReceivingClass::_on_Signal_Name([[paramater_1_type parameter_1_name]], ...) // ...

5.5 Object-Oriented Scripting Languages

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.

  • Classes in Scripts
    • A class is just some data with some associated functions
    • Some scripting languages use other data structures to realize this
      • E.g. in Lua, a class can be built using a table (data) and a meta table (functions)
    • Godot scenes and scripts are classes
  • Inheritance in Script
    • OO languages do not necessarily support inheritance, though it is very useful
    • In scripting languages, there are two types of inheritance
      1. Deriving scripted classes from other scripted classes
      2. Deriving scripted classes from native classes
      • GDScript supports both
        • Furthermore, it is possible to use scene inheritance as well
  • Composition/Aggregation in Script
    • Define classes and associate instances of those classes with objects that have been defined in the native programming language or in the scripting language
      • This can be realized by the means of object references as explained above
    • Then we can...
      • Delegate functionality from native classes to script components
      • Delegate functionality from script classes to hard-coded native components
      • GDScript supports both
  • Scripted Finite State Machines (FSMs)
    • Every game object can have one or more states, with differing behavior (e.g. update functions)
    • Even if the core game object model doesn’t support FSMs natively, one can still provide state-driven behavior by using a state machine on the script side
      • It is pretty straightforward to implement a FSM in GDScript
      • Also, there are plugins available from the Godot Asset Library
  • Multithreaded Scripts
    • If multiple scripts can run at the same time, we are in effect providing parallel threads of execution in script code
      • However, the scripts may not actually run in parallel, but instead may take turns
      • Most scripting systems that provide parallelism do so via cooperative multitasking
        • GDScript features Coroutines, Wait and Yield
      • In contrast, with a preemptive multitasking, the execution of any script could be interrupted at any time
        • This requires synchronization mechanisms to avoid race conditions!
        • GDScript does allow the use of Threads and provides the synchronization primitives Mutex and Semaphore

OO Concepts in GDScript

Based on https://www.analyticsvidhya.com/blog/2020/09/object-oriented-programming/