Script-virtual functions
The GDExtension API allows you to define virtual functions in Rust, which can be overridden in scripts attached to your objects.
Note that these are conceptually different from virtual functions like ready(), which are defined by Godot and overridden by you (in Rust).
Hence the emphasis on "script-virtual".
Table of contents
A motivating example
To stay with our Monster example, let's say we have different monster types and would like to customize their behavior. We can write the logic
common to all monsters in Rust, and for quick prototyping use GDScript for the specific parts.
For example, we can experiment with two monsters: Orc and Goblin. Each of them comes with a different behavior, which is encoded in
a respective GDScript file. The project structure might look like this:
project_dir/
│
├── godot/
│ ├── .godot/
│ ├── project.godot
│ ├── MonsterGame.gdextension
│ └── Scenes
│ ├── Monster.tscn
│ ├── Orc.gd
│ └── Goblin.gd
│
└── rust/
├── Cargo.toml
└── src/
├── lib.rs
└── monster.rs
The Monster.tscn encodes a simple scene with the node Monster (our Rust class inheriting Node3D) at the root. This node would be the
one to attach scripts to.
Step by step
Rust default behavior
Let's start from this class definition:
#![allow(unused)] fn main() { use godot::prelude::*; #[derive(GodotClass)] #[class(init, base=Node3D)] struct Monster { base: Base<Node3D> } }
We can now implement a Rust function to calculate the damage a monster deals per hit. Traditionally, we would write this:
#![allow(unused)] fn main() { #[godot_api] impl Monster { #[func] fn damage(&self) -> i32 { 10 } } }
That method will always return 10, no matter what. To customize this behavior in scripts that are attached to the Monster node, we can
define a virtual method in Rust, which can be overridden in GDScript. The Rust code is called the default implementation.
Virtual (also called late-binding) means that dynamic dispatch is involved: the actual method to call is determined at runtime,
depending on whether a script is attached to the Monster node -- and if yes, which one.
This stands in contrast to early-binding, which is resolved at compile time, using static dispatch.
While traditional Rust might use trait objects (dyn Trait) for late binding, godot-rust provides a more direct way.
Making a method virtual is very easy: just add the virtual key to the #[func] attribute.
#![allow(unused)] fn main() { #[godot_api] impl Monster { #[func(virtual)] fn damage(&self) -> i32 { 10 } } }
That's it. Your monster can now be customized in scripts.
Overriding in GDScript
In the GDScript files, you can now override the Rust damage method as _damage. The method is prefixed with an underscore, following Godot
convention for virtual methods such as _ready or _process.
Here's an example for the Orc and Goblin scripts:
# Orc.gd
extends Monster
func _damage():
return 20
# Goblin.gd
extends Monster
# Random damage between 5 and 15.
# Type annotations are possible, but not required.
func _damage() -> int:
return randi() % 11 + 5
If your signature in GDScript does not match the Rust signature, Godot will cause an error.
Dynamic behavior
Now, let's call damage() in Rust code:
#![allow(unused)] fn main() { fn monster_attacks_player(monster: Gd<Monster>, player: Gd<Player>) { // Compute the damage. let damage_points: i32 = monster.bind().damage(); // Apply the damage to the player. player.bind_mut().take_damage(damage_points); } }
What value does damage_points have in the above example?
The answer depends on the circumstances:
- If the
Monsternode has no script attached,damage_pointswill be10(the default implementation in Rust). - If the
Monsternode has theOrc.gdscript attached,damage_pointswill be20. - If the
Monsternode has theGoblin.gdscript attached,damage_pointswill be a random number between5and15.
Trade-offs
You might ask: what's the point of all this, if one can achieve the same with a simple match statement?
And you're right; if a match in Rust is all you need, then use that. However, the script-based approach has a few advantages, especially
when it comes to more complex scenarios than just computing a single damage number:
- You can prepare a variety of scripts with different behaviors, e.g. for different levels or enemy AI behavior. In the Godot editor, you
can then simply swap out scripts as needed, or have different
Monsterinstances with different scripts, to compare them side-by-side. - Switching behaviors does not require recompiling Rust code. This can be useful if you work with game designers, modders or artists who are less familiar with Rust, but want to experiment nonetheless.
That said, if your compile times are short (godot-rust itself is quite lightweight) and you prefer having the logic in Rust, that is of course
also a valid choice. To retain the option to quickly switch behaviors, you could use an #[export]'ed enum to select the behavior, and
then dispatch on that in Rust.
Ultimately, #[func(virtual)] is just one extra tool that godot-rust offers among a variety of abstraction mechanisms. Since Godot's
paradigm revolves heavily around attaching scripts to nodes, this feature integrates very well with the engine.
Limitations
Godot script-virtual functions do not behave like OOP virtual functions in every aspect.
Make sure you understand the limitations.
In contrast to virtual methods from OOP languages (C++, C#, Java, Kotlin, PHP, ...), there are some important differences to be aware of.
-
The default implementation is unreachable from Godot.
In Rust, calling
monster.bind().damage()will automatically look for script overrides, and fall back to the Rust default if no script is attached. In GDScript however, you cannot call the default implementation. Callingmonster._damage()will fail without a script. The same is true for reflection calls from Rust (e.g.Object::call()).The
_prefix underlines that: ideally, you don't call virtual functions directly from scripts.To work around this, you can declare a separate
#[func] fn default_damage()in Rust, which will be registered as a regular method and thus can be called from scripts. To keep Rust's convenient fallback behavior, just invokedefault_damage()inside the Rustdamage()method. -
No access to
supermethods.In OOP languages, you can call the base method from the overriding method, typically using
superorbasekeywords.As a consequence of point 1), this default method is also not visible to the script overriding it. The same workaround can be used though.
-
Limited re-entrancy.
If you call a virtual method from Rust, it may dispatch to a script implementation. The Rust side holds either a shared (
&self) or exclusive borrow (&mut self) to the object -- an implicitGd::bind()orGd::bind_mut()guard. If the script implementation then accesses the same object (e.g. by setting a#[var]property), panics can occur due to double-borrow errors.For now, you can work around this by declaring the method with
#[func(gd_self, virtual)]. Thegd_selfrequires the first parameter to be of typeGd<Self>, which avoids the bind call and thus the borrow.
We are observing how virtual functions are used by the community and plan to mitigate the limitations where possible. If you have any inputs, feel free to let us know!
Types of scripts
While this page focuses on GDScript, Godot also provides other scripting capabilities. Notably, C# can be used for scripting, if you run Godot with the Mono runtime.
The library also provides a dedicated trait ScriptInstance, which allows users to provide Rust-based "scripts".
Consult its docs for detailed information.
You can also configure scripts entirely programmatically, using the classes::Script API and its inherited classes, such
as classes::GDScript. This typically defeats the purpose of scripting, but is mentioned here for completeness.
Conclusion
In this chapter, we have seen how to define virtual functions in Rust, and how to override them in GDScript. This provides an additional integration layer between the two languages and allows to effortlessly experiment with swappable behaviors from the editor.