Classes in GDLisp
GDLisp prides itself on being a functional language, capable of manipulating functions as first-class objects and providing powerful abstraction techniques to support common functional paradigms. But GDLisp targets the Godot platform, which is designed with an object-oriented paradigm in mind. GDLisp fully supports this paradigm and provides mechanisms for working with classes, both those built-in to Godot and those from GDScript source files, as well as for declaring new classes in GDLisp proper.
Using Classes
Every built-in GDScript class is available in GDLisp as a global name.
This includes classes deriving from Object, like Node,
Spatial, and Control, as well as typenames that are considered
primitive in Godot, such as Int and Vector2.
To access a field on a class or an object, put a colon : after the
class or object name and follow it by the name of the constant or
variable. For example:
Vector2:ZERO
Transform:FLIP_X
Quat:IDENTITY
some-timer-node:paused
some-material:render_priority
Likewise, to call a method on an object or a class, use the colon syntax as the first term of an S-expression.
(Reference:new)
(Engine:get_singleton "MySingleton")
(some-node:get_path)
(my-object:free)
Declaring Classes
Classes can be declared inside a module using the following syntax.
(defclass ClassName (ParentClassName)
body ...)
Note that ParentClassName must always be a (possibly qualified)
class name. Unlike GDScript, GDLisp does not allow string paths to
be used as the name of a parent class.
As with functions and other declarations, defclass can be suffixed
with the symbol private to make the class invisible outside of the
current module.
(defclass ClassName (ParentClassName) private
body ...)
Inside the class, several types of declarations are supported. Note that declarations inside a class are always public, as GDLisp cannot enforce access restrictions on class members.
Signal Declarations
(defsignal signal_name (args ...))
The defsignal form declares the existence of a signal with the
given name and argument list. Note that signals are conventionally
named in snake_case, not train-case, for maximum compatibility
with Godot signals. If the argument list is empty, it can be omitted.
The argument list, if provided, must be a simple lambda list. That means that the &opt, &rest, and
&arr directives are not supported.
Constant Declarations
(defconst ConstantName value)
Constants inside a class work identically to those at the
module-level, with the exception that a class-scoped defconst
cannot be marked private. Note that defconst is evaluated at
class scope, so self does not exist in the right-hand side of a
defconst.
Variable Declarations
(defvar var-name initial-value)
Variables declared inside a class function as instance variables. When
an instance of the class is initialized, it will receive an instance
variable with the given name. initial-value is optional, but if
provided it will be used as the initial value of the instance variable
at initialization time. initial-value can contain arbitrary
expressions and can use self.
(defvar var-name initial-value onready)
If the defvar form is followed by the onready keyword, then
the initial value will be applied during the _ready method (when
the node is added to the tree) instead of the _init method (when
the object is constructed). The onready modifier makes no sense on
objects that are not nodes.
Note
If you’re wondering where the GDScript setget keyword
is, GDLisp doesn’t have setget. GDLisp implements proxy
properties in a different way. See
Getters and Setters below.
Export Modifiers
A variable declaration in a class can be followed by an export
form, which will be compiled into a GDScript export clause. This
provides certain information to the Godot editor indicating what
values are acceptable for the instance variable.
Examples:
(defvar player-hp 10 (export int))
(defvar character-name "Alice" (export String "Alice" "Bob" "Charlie"))
(defvar background-color Color:red (export Color))
(defvar player-node (export NodePath))
The defvars Macro
If you’re simply declaring several instance variables in a row and do
not need to provide initial values for any of them, you may use the
defvars macro. defvars takes any number of instance
variable names and declares those variables, without initial values.
defvars does not support export or onready.
(defvars player-hp character-name background-color)
Instance Functions
The main lifeblood of a class is its instance methods, which are
declared using a similar defn syntax to module functions.
(defn method-name (args ...)
body ...)
Like signal declarations, instance function declarations take a simple
lambda list, which means modifiers such as &opt, &arr, and
&rest are not allowed in this context.
Inside the instance method body, the variable self is available
and refers to the current instance of the class. Note that GDLisp does
not implicitly insert self in any context. That is, a bare name
like example will always refer to a statically-scoped local
variable or module constant with the name example, even inside a
class. To refer to the instance variable with that name, you must
explicitly write self:example. The syntax sugar @example is
provided, which desugars to self:example.
Methods may be marked static, to indicate that they should be
called on the class itself rather than an instance.
(defn method-name (args ...) static
body ...)
Inside a static method, the name self is not bound.
Tip
You probably shouldn’t be using the static modifier very
often. Usually, when you find yourself writing a static
class method, that method would be better written as a
top-level module function instead. static is provided
mainly for compatibility with GDScript.
Super Calls
Inside an instance method, you may use the special syntax
(super:method-name ...) to invoke the method called
method-name on the superclass of the current class.
method-name need not be the name of the currently-executing method
(though it usually will be, in practice). Note that super on its
own is not a variable, so attempting to assign super to a local
variable or pass it as a function argument will fail.
Constructors
Class constructors in GDLisp are a bit special and have some
additional syntax to accommodate that. A constructor is called
_init and is declared using defn like any other instance
function. Constructors do not return values, though (return nil)
can still be used to exit the constructor early.
(defn _init (args ...)
body ...)
A class constructor cannot be made static. Inside the body of the
constructor, if you wish to call the superclass’ constructor, you may
do so by calling the function super as the first expression in
the constructor.
(defn _init (args ...)
(super args ...)
body ...)
The super call, if present, must be the first expression in the
constructor body.
The argument list to a constructor supports a special syntax unique to
constructors. An @ sign can be placed before the name of an
argument.
(defn _init (@foo @bar))
In this case, foo and bar are not local variables to the
constructor. Instead, the values given to those parameters are
assigned directly to instance variables on the class itself.
Essentially, the above is equivalent to
(defn _init (foo bar)
(set @foo foo)
(set @bar bar))
Note that the automatic assignment happens after any super
constructor call.
Getters and Setters
Getters and setters are special instance methods that look like ordinary instance variables, from a caller’s perspective.
Getters are declared using defn with a special method name of the
form (get ...). A getter method must be non-static and cannot take
any arguments.
(defn (get variable-name) ()
body ...)
When a user of the class attempts to get the instance variable
variable-name from the class, the method (get variable-name)
will be called instead, and its result will be used as the value of
the expression.
Likewise, setters are declared using a (set ...) name. A setter
method must be non-static and must take exactly one argument. Like a
constructor, a setter does not return any values, though it can exit
early with (return nil).
(defn (set variable-name) (value)
body ...)
The setter will be invoked when a caller attempts to set the given
instance variable.
(set my-instance:variable-name value)
The same variable name can be used for a setter and a getter. The name
of a setter/getter cannot coincide with the name of an actual,
concrete defvar instance variable on the class. Setters and
getters are fully compatible with GDScript, in that a caller from
GDScript who attempts to get or set the given variable will correctly
invoke the getter or setter function, respectively.
If your goal is to wrap a defvar with some validation or some
actions, a common idiom is to precede the defvar with an
underscore and then use the property outside the class.
Example:
(defclass Player (Node2D)
(defsignal hp_changed)
(defvar _hp 10)
(defvar max_hp 10)
(defn (get hp) ()
;; Simply return the instance variable
@_hp)
(defn (set hp) (x)
;; Make sure the HP value is in bounds
(set @_hp (clamp x 0 @max_hp))
;; Let everyone know the value has changed
(@emit_signal "hp_changed")))
Main Classes
Godot is based around the idea that every file is also a class, in some form or another. In GDLisp, this is not the case. However, it is often useful to designate one class in a GDLisp module as the “primary” class of that module, so that Godot can link scenes and other resources up to it in the editor.
For this reason, GDLisp classes can be marked with the main
modifier.
(defclass ClassName (ParentClassName) main
body ...)
Every file can have at most one main class, and the main class
cannot be private. From the perspective of GDLisp modules, the
main modifier changes nothing. A GDLisp module will still import
the class name with use just like any other name. However, in the
resulting GDScript file, the main class will be compiled to the
top-level class of the file, rather than a nested class. This allows
the editor to bind the main class to a packed scene or other
resource.
Signals
Godot communicates between nodes and other objects using signals. For the most part, GDLisp supports signals and connections just like GDScript:
(my-object:connect "signal" target-object "_target_method_name")
(my-object:disconnect "signal" target-object "_target_method_name")
However, GDLisp also offers some convenience functions to deal with common use cases.
(connect>> my-object "signal" (lambda () ...))
The built-in function connect>> connects a signal to a lambda
function. When the signal is fired, the lambda function will be called
and can accept the arguments of the signal. As a first-class function,
the lambda is free to close around any local variables (including
self, if applicable) in the current scope, so you should never
have to explicitly bind variables of a signal connection when using
connect>>.
GDLisp also provides a function connect1>>, which takes the same
arguments but connects a signal as if using the one-shot flag. That
is, the connection removes itself after firing once.
Both connect>> and connect1>> return a unique value to
identify the connection. To disconnect a signal that was connected in
this way, use disconnect>>, which takes the return value of
connect>> or connect1>> and disconnects it.
Note
You may freely intermix GDLisp-style connect>> calls and
Godot connect method calls in the same program. However,
disconnect>> should only be used on connections
established with connect>> or connect1>>, while the
built-in Godot method disconnect should be used for
those established with connect or through the Godot UI.