C++ Examples
Currently, the Godot Sandbox has access to all of Godot through calls, properties, objects, nodes and so on. This might not be apparent from looking at the APIs, especially if one is unfamiliar with how Godot works under the hood.
Node paths
All node paths are relative to the node that has the script program attached.
The current node can be accessed using .
and get_node()
:
Node current_node(".");
get_node()
get_node()
also takes an optional node path:
AnimatedSprite2D n = get_node("MyAnimatedSprite2D");
which is equivalent to:
AnimatedSprite2D n("MyAnimatedSprite2D");
AnimatedSprite2D n = get_node<AnimatedSprite2D>("MyAnimatedSprite2D");
As written before, paths are relative to the owner of the sandbox. If the sandbox is attached as a script to a node, then the owner is that node.
You can also retrieve a node relative to a node:
Node2D n("MyAnimatedSprite2D");
Label n2 = n.get_node<Label>("../Texts/CoinLabel");
Getting the parent of the current node (".."
) can be used to access the tree of the current scene, even when the current node is in its own tree. Eg. ../Texts/CoinLabel
could access a coin label in another scene.
The current scene tree is also accessible through a global helper:
get_tree()
So, to reload the current scene after the frame ends:
get_tree().call_deferred("reload_current_scene");
VM function calls
The Sandbox is by default in unboxed arguments mode, which means that it will prefer to pass primitive or performant arguments to the VM. As a result, incoming arguments are usually not a Variant. However, the return value is always a Variant.
The simplest function is one that returns only a Variant.
Variant my_function() {
return Nil;
}
Every function that is to be called from Godot must return a Variant. The C++ API has a shortcut for returning nothing: Nil
. It's a default-constructed Variant.
Example function
Example:
Variant function_that_takes_string_node_input(String str, Node node, Object input) {
Dictionary d = Dictionary::Create();
d["123"] = "456";
d[str] = node;
return d;
}
The unboxed arguments mode can be disabled per sandbox as a property: "Use Unboxed Arguments". The default can be changed in the project settings, in the Script section. Unticking "Unboxed Types for Sandbox Arguments" will make all Sandboxes stop using unboxed arguments by default. When disabled, all arguments are Variants. In that case the function looks like this:
Variant function_that_takes_string_node_input(Variant str, Variant node, Variant input) {
Dictionary d = Dictionary::Create();
d["123"] = "456";
d[str] = node;
return d;
}
Again, the above example function is for when "Use Unboxed Arguments" are disabled in the Sandbox with the function.
Primitive types
When primitive types and small structs are passed to the Sandbox, they get passed by value directly in registers, which is beneficial for performance.
Godot integers become int64_t
or long
, floats become double
and booleans become bool
. 2-vectors become Vector2
and Vector2i
.
Variant function_with_int_float_and_bool(int x, double y, bool z) {
return Nil;
}
We always need to return a variant. We are not required to read a 64-bit integer. We can choose to read it as any integer, eg. uint8_t
, int64_t
, int
, long
etc.
Floating-point values are double
as arguments. Make sure you don't try to read a float
as it will be read as garbage. Booleans are just bool
(or an integer, technically).
Complex types
Complex types are those that benefit greatly from being accessed by reference. Examples of complex types are String, Array, Dictionary, Object, Callables and all nodes.
The C++ API has wrappers for most types:
Variant function_that_takes_wrapped_types(String str, Array a, Dictionary d, Variant callable) {
return Nil;
}
As shown, Callable
does not yet have a wrapper, but can still be used. Simply use callable(x, y, z)
.
Nodes, methods and properties
Let's take an example: Playing animations using an AnimatedSprite2D. First we will use generic calls and properties, and at the end we will use the actual class.
AnimatedSprite2D mysprite = get_node<AnimatedSprite2D>("MyAnimatedSprite2D");
or through the path constructor:
AnimatedSprite2D mysprite("MyAnimatedSprite2D");
And now, what if we want to play an animation? If we look at the documentation for AnimatedSprite2D it has a play method used to start playing animations.
In Godot, methods are callable functions on objects, including nodes. So, we can just call play
as a function on our node object:
mysprite.call("play", "idle");
We don't actually need to "have" an AnimatedSprite2D to call any function that it has. Further, the call is such an important function that there is a dedicated operator (...)
for it:
mysprite("play", "idle");
That means we have access to all methods of all objects via calls. What about properties? Well, the API has support for get()
and set()
, which lets us get and set properties. The current animation is a property that we can use get()
on:
Variant current_animation = mysprite.get("animation");
With access to methods, properties, objects, nodes and node-operations, globals and Variants, we can say that the Godot Sandbox has the entire Godot engine available to it.
The run-time API
All classes are available, generated at run-time:
AnimatedSprite2D mysprite("MyAnimatedSprite2D");
mysprite.play("idle");
String animation = mysprite.animation();
CharacterBody2D player("%Player");
const bool f = player.is_on_floor();
In practice you can write code just like GDScript. Even classes loaded at run-time should be accessible with auto-complete.
Variant Lifetimes
The sandbox has to maintain integrity, and so any Variant created during a call without outside backing will be lost. You can think of the sandbox as becoming forgetful after initialization is complete. You can think of initialization as anything happening during int main()
or in global constructors.
static std::vector<int> vec;
Variant myfunction() {
Dictionary d = Dictionary::Create();
d["123"] = "456";
vec.push_back(123);
return Nil;
}
The above dictionary will disappear unless returned by the call. The vector is modified as expected.
static std::vector<int> vec;
Variant myfunction(Dictionary d) {
d["123"] = "456";
vec.push_back(123);
return Nil;
}
All modifications to the Dictionary and vector above will be permanent.
Variant myfunction() {
Dictionary d = Dictionary::Create();
d["123"] = "456";
return d;
}
The returned Dictionary can be used by the caller.
The sandbox has storage too, of course. You can use the sandbox any way you like, with exception to non-trivial Variants that lack backing.
However, during initialization we can create Variants with backing:
static Dictionary d = Dictionary::Create();
Variant myfunction() {
d["123"] = "456";
return Nil;
}
Since the Dictionary was created during initialization, it can be used by future calls into the VM. Since we know that modifications to dictionaries are also permanent, it becomes a fully usable Dictionary.
Coin pickup example
The sandbox is a node, and so the usual functions like _process
, _ready
and _input
will get called in the Sandbox. If the program running inside the sandbox implements any of these functions, the sandbox will forward the call to the program inside the sandbox.
As an example, this C++ example program implements _ready
, and so when the ready callbacks are triggered on the scene tree, this C++ function will also get called as part of that.
#include "api.hpp"
static int coins = 0;
extern "C" Variant reset_game() {
coins = 0;
return Nil;
}
static void add_coin(const Node& player) {
coins ++;
// In our demo project we can access the coin label from the player
// using a node path: Player -> get_parent() -> Texts -> CoinLabel
Label coinlabel = player.get_node("../Texts/CoinLabel");
coinlabel.set_text("You have collected "
+ std::to_string(coins) + ((coins == 1) ? " coinerino" : " coinerinos"));
}
extern "C" Variant _on_body_entered(CharacterBody2D body) {
// This function is called when a body enters the coin
// Most likely it's the player, but we still check!
if (body.get_name() != "Player")
return Nil;
Node coin = get_node();
coin.queue_free(); // Remove the current coin!
add_coin(body);
return Nil;
}
extern "C" Variant _ready() {
if (is_editor_hint()) {
get_node().set_process_input(false);
}
return Nil; //
}
extern "C" Variant _process(double delta) {
if (is_editor_hint()) {
AnimatedSprite2D("AnimatedSprite2D").play("idle");
}
return Nil;
}
extern "C" Variant _input(InputEvent event) {
if (event.is_action_pressed("jump")) {
get_node<Node2D>().set_modulate(0xFF6060FF);
} else if (event.is_action_released("jump")) {
get_node<Node2D>().set_modulate(0xFFFFFFFF);
}
return Nil;
}
We can check if we are in the editor using is_editor()
and do different things based on that. For example, the coin idle animation is automatically playing in the editor.
Signals
It's also possible to attach signals to VM functions, like _on_body_entered
.
You won't be able to see the sandbox program functions on the image above, but they can still be connected.
Once connected, the Godot engine will directly call the function _on_body_entered
in our sandboxed program.
Sandboxed Properties
#include "api.hpp"
static float jump_velocity = -300.0f;
static float player_speed = 150.0f;
SANDBOXED_PROPERTIES(3, {
.name = "player_speed",
.type = Variant::FLOAT,
.getter = []() -> Variant { return player_speed; },
.setter = [](Variant value) -> Variant { return player_speed = value; },
.default_value = Variant{player_speed},
}, {
.name = "player_jump_vel",
.type = Variant::FLOAT,
.getter = []() -> Variant { return jump_velocity; },
.setter = [](Variant value) -> Variant { return jump_velocity = value; },
.default_value = Variant{jump_velocity},
}, {
.name = "player_name",
.type = Variant::STRING,
.getter = []() -> Variant { return "Slide Knight"; },
.setter = [](Variant value) -> Variant { return Nil; },
.default_value = Variant{"Slight Knight"},
});
// More code ...
Properties in the Sandbox are supported. They are stored in the global scope, and each one has a custom getter and setter function.
Properties can be edited in the Godot inspector and is a powerful and simple way to expose data from the script. The values of these properties are saved in the Godot project and restored on reopening the project.
In this example, the players name cannot be changed in the editor, as the property will just keep returning the same value, effectively making it read-only. That is, the getter for the player_name
property only returns "Slide Knight"
.
Per-instance Sandboxed Properties
Sometimes we want properties per instance, even if we have several. In that case, we can access the property based on the value of get_node()
. A std::unordered_map
on the get_node().address()
can be used to achieve this:
// If we need per-node properties in shared sandboxes, we will
// need to use the PER_OBJECT macro to distinguish between them.
struct PlayerState {
float jump_velocity = -300.0f;
float player_speed = 150.0f;
std::string player_name = "Slide Knight";
};
PER_OBJECT(PlayerState);
static PlayerState &get_player_state() {
return GetPlayerState(get_node());
}
SANDBOXED_PROPERTIES(3, {
.name = "player_speed",
.type = Variant::FLOAT,
.getter = []() -> Variant { return get_player_state().player_speed; },
.setter = [](Variant value) -> Variant { return get_player_state().player_speed = value; },
.default_value = Variant{get_player_state().player_speed},
}, {
.name = "player_jump_vel",
.type = Variant::FLOAT,
.getter = []() -> Variant { return get_player_state().jump_velocity; },
.setter = [](Variant value) -> Variant { return get_player_state().jump_velocity = value; },
.default_value = Variant{get_player_state().jump_velocity},
}, {
.name = "player_name",
.type = Variant::STRING,
.getter = []() -> Variant { return get_player_state().player_name; },
.setter = [](Variant value) -> Variant { return get_player_state().player_name = value.as_std_string(); },
.default_value = Variant{"Slide Knight"},
});
In this program, each instance will have their own separate properties.
Timers
#include "api.hpp"
extern "C" Variant _on_body_entered(CharacterBody2D body) {
Engine::get_singleton().set_time_scale(0.5f);
body.set_velocity(Vector2(0.0f, -120.0f));
body.get_node("CollisionShape2D").queue_free();
body.get_node("AnimatedSprite2D")("play", "died");
CallbackTimer::native_oneshot(1.0f, [] (Timer timer) -> Variant {
timer.queue_free();
Engine::get_singleton().set_time_scale(1.0f);
get_tree().call_deferred("reload_current_scene");
return Nil;
});
return Nil;
}
This is the code from a killzone. Once the player touches it, we slow down the game and make the player appear dead. After a second, we reload the scene.
The Sandbox API supports timers with lambda capture storage. Timers are implemented by creating a Timer node, which means we must remember to remove it after we're done with it. In this case we are reloading the scene, but we still do it properly:
timer.queue_free();
Capture storage
Capture storage allows us to bring some data with us into the callback:
struct SomeData {
int some_int = 1;
float some_float = 2.0f;
} somedata;
// Capture 'somedata' by value
Timer::oneshot(1.0f, [somedata] (Node timer) -> Variant {
timer.queue_free();
print("Float: ", somedata.some_float);
return Nil;
});
We should not try to capture complex variants in the capture storage, as they have special sandboxing restrictions. Complex variants are Array, String, Dictionary, Callable, packed arrays etc.
Callables
We can create a way to make function calls into the sandbox at a later time. In C++ this would be like creating a lambda callback that we can call later.
var func = my_program.vmcallable("my_function")
# Now call it later
func.call(1, 2, 3)
Callables can also take pre-passed arguments in an array. Arguments passed to call()
later will get appended.
var func = my_program.vmcallable("my_function", [1, 2, 3])
# Now call it later
func.call(4, 5, 6)
The above call will call the C++ function my_function
in the sandbox with the arguments (1, 2, 3, 4, 5, 6)
.
A complete callable example
Let's make a complete working example. In GDScript:
extends Node
@export var my_program : Sandbox
func _ready() -> void:
var func = my_program.vmcallable("sum_function", [1, 2, 3])
# Now call it and print the result
print("Sum: ", func.call(4, 5, 6))
Remember to assign a Sandbox instance to the GDScript export variable:
You can create a new Sandbox node and hang it under the node with the GDScript attached. Now we implement the sum_function
in the C++ program that was assigned to the variable:
extern "C" Variant sum_function(int a1, int a2, int a3, int a4, int a5, int a6)
{
return a1 + a2 + a3 + a4 + a5 + a6;
}
We should see the console print 21:
Sum: 21