gdext
gdext copied to clipboard
High-level abstractions for `engine` API
Some patterns in the code-generated engine
API are relatively frequent and could benefit from thin higher-level wrappers.
This issue tracks such ideas, please add one post for each idea 🙂
Please don't start implementing those just yet.
I'd like to keep this for a bit later, when we have a clearer idea of common use cases. We could then also decide if such features are in scope for gdext (and where they'd fit best), or if they could be done in an external project, for example.
Example from https://github.com/godot-rust/gdext/issues/366#issuecomment-1659427166:
let mut surface_array = VariantArray::new();
surface_array.resize(ArrayType::ARRAY_MAX.to_index());
surface_array.set(ArrayType::ARRAY_VERTEX.to_index(), verts.to_variant());
surface_array.set(ArrayType::ARRAY_TEX_UV.to_index(), uvs.to_variant());
...
let mut mesh = ArrayMesh::new();
mesh.add_surface_from_arrays(PrimitiveType::PRIMITIVE_TRIANGLES, surface_array);
could be abstracted into:
pub struct SurfaceArray {
array: VariantArray
}
impl SurfaceArray {
pub fn set_vertices(&mut self, verts: PackedVector3Array);
pub fn set_uvs(&mut self, verts: PackedVector3Array);
pub fn set_indices(&mut self, indices: PackedInt32Array);
pub fn add_to_mesh(self, &ArrayMesh); // calls add_surface_from_arrays()
}
I'm not quite sure if convenience functionality for add_child
belongs into that category, but since I wanted to ask if there is interest anyways, I'm just posting it here... If preferred, we can move it into a separate issue.
Macro to assemble node tree
In my current experimental project I rely on generating the entire node tree programmatically. For that purpose I came up with a small macro that has improved assembling deep node trees considerably for me. Here is a small demo:
#[godot_api]
impl ControlVirtual for UiDemo {
fn init(mut base: Base<Self::Base>) -> Self {
// Example of an explicitly named node (logic relevant)
let button = make_button("Ok");
// Example of dynamically generated children
let children: Vec<_> = (1..=10)
.map(|i| make_label(format!("Label: {i}")))
.collect();
assemble_tree!(
base => {
MarginContainer::new_alloc().set_full_rect() => {
VBoxContainer::new_alloc() => {
PanelContainer::new_alloc() => {
MarginContainer::new_alloc() => {
HBoxContainer::new_alloc() => {
make_label("left"),
make_label("middle").add_h_size_flags(SizeFlags::SIZE_EXPAND),
make_label("right"),
}
}
},
PanelContainer::new_alloc() => {
MarginContainer::new_alloc() => {
VBoxContainer::new_alloc() => {
.. children,
button.share(),
}
}
},
},
},
}
);
set_full_rect(&mut base);
Self { base, button }
}
}
which produces this node tree:
i.e. this UI:
My current implementation is
source
macro_rules! assemble_tree {
($base:expr => { $($other:tt)* }) => {
assemble_tree!( @recurse, $base, $($other)*)
};
// Patterns for '.. children' syntax
(@recurse, $base:expr, .. $child_iter:expr $(,)?) => {
for child in $child_iter {
// $base.add_child($child.upcast())
$base.share().upcast::<::godot::engine::Node>().add_child(child.upcast())
}
};
(@recurse, $base:expr, .. $child_iter:expr, $($other:tt)+) => {
for child in $child_iter {
// $base.add_child($child.upcast());
$base.share().upcast::<::godot::engine::Node>().add_child(child.upcast());
}
assemble_tree!( @recurse, $base, $($other)*)
};
// Patterns for 'child' syntax
(@recurse, $base:expr, $child:expr $(,)?) => {
// $base.add_child($child.upcast())
$base.share().upcast::<::godot::engine::Node>().add_child($child.upcast())
};
(@recurse, $base:expr, $child:expr, $($other:tt)+) => {
// $base.add_child($child.upcast());
$base.share().upcast::<::godot::engine::Node>().add_child($child.upcast());
assemble_tree!( @recurse, $base, $($other)*)
};
// Patterns for 'child => { ... }' syntax
(@recurse, $base:expr, $child:expr => { $($children:tt)* } $(,)?) => {
let temp = $child;
assemble_tree!( temp => { $($children)* });
// $base.add_child(temp.upcast())
$base.share().upcast::<::godot::engine::Node>().add_child(temp.upcast())
//godot::engine::Node::add_child(&mut $base, temp.upcast())
};
(@recurse, $base:expr, $child:expr => { $($children:tt)* }, $($other:tt)+) => {
let temp = $child;
assemble_tree!( temp => { $($children)* });
// $base.add_child(temp.upcast());
$base.share().upcast::<::godot::engine::Node>().add_child(temp.upcast());
assemble_tree!( @recurse, $base, $($other)*)
};
}
Notes:
- I like that the syntax resembles the node tree quite explicitly.
- Also convenient is that only logic-relevant nodes need an explicit variable. All internal boiler-plate nodes can stay anonymous (temp variables of the macro).
- The
..
syntax makes it relatively easy to mix the static prats of the node tree with dynamic parts (children depending on runtime). - The node assembly goes from child to parent direction, which is in line with how
_ready
is called and fits with move semantics (firstx.add_child(<sub-child>)
which doesn't move, then<parent>.add_child(x)
which movesx
). - The demo also contains a few other "builder like" convenience methods like
set_full_rect
andadd_h_size_flags
that make live easier in this case. I've defined them on an extension trait, and they always consumeself
and return it. I'm not entirely happy about them, because they obviously clash with the scope of the actual class methods.
The main drawbacks / reasons to keep it outside gdext
:
- The
=>
part of the syntax is a bit unusual, but the rules forexpr
doesn't leave much option and it doesn't look too bad. - There is the drawback that apparently rustfmt cannot really format these expressions.
- The implementation isn't perfect yet (e.g. the macro should return the outermost node itself, which I haven't figured out yet), and I'm not sure if there are gotchas I haven't encountered yet.
- Even though it is only a few lines of code, I find the macro very hard to understand -- admittedly it is the first non-trivial macro I've written, but I'm still confused by it :wink:
Here's something I wrote for when I wanted a Rusty iterator interface for listing files recursively. It's neat to have, as a way to interface with other parts of Rust and the ecosystem.
The error handling can clearly be improved, but I probably forgot to fix that (and it worked on my machine :sweat_smile:), plus other things that probably can be made more robust. It's obviously written for exactly whatever I needed, so a "proper" implementation would also need to decide which string type to use and other things like that. I think I used a Rust String
here to be able to filer it more easily, since the GodotString
interface is much more limited at the moment.
Full implementation
pub(crate) trait DirAccessExt {
fn iter_filenames_recursive(&mut self) -> FilesRecursive;
}
impl DirAccessExt for Gd<DirAccess> {
fn iter_filenames_recursive(&mut self) -> FilesRecursive {
let error = self.list_dir_begin();
if error != Error::OK {
godot_error!(
"could not list content of {}: {}",
self.get_current_dir(),
error_string(error.ord() as i64)
);
}
FilesRecursive {
stack: vec![self.share()],
}
}
}
pub(crate) struct FilesRecursive {
stack: Vec<Gd<DirAccess>>,
}
impl Iterator for FilesRecursive {
type Item = String;
fn next(&mut self) -> Option<Self::Item> {
while let Some(dir) = self.stack.last_mut() {
let entry_name = dir.get_next();
// Empty name means there's nothing more in the directory.
if entry_name.chars_checked().is_empty() {
dir.list_dir_end();
self.stack.pop();
continue;
}
// get_next gives the name of the item, and not the full path that's needed for file access,
// so this constructs the full path so we can open it as a file or look inside it as a directory.
let path = format!("{}/{entry_name}", dir.get_current_dir());
// Put it on the stack instead of returning it, if it's a directory.
if dir.current_is_dir() {
if let Some(mut child) = DirAccess::open(path.into()) {
let error = child.list_dir_begin();
if error != Error::OK {
godot_error!(
"could not list content of {}: {}",
child.get_current_dir(),
error_string(error.ord() as i64)
);
}
self.stack.push(child)
}
continue;
}
return Some(path);
}
None
}
}
Example use:
fn read_files(mut directory: Gd<DirAccess>) {
for path in directory.iter_filenames_recursive().filter(some_name_filter) {
// ...
}
}