-
Notifications
You must be signed in to change notification settings - Fork 8
How does py4godot work?
The goal of this page is to clarify how py4godot works, making it easier for others to understand, fork, or contribute to the project. A common challenge I see with many open-source projects is that their complexity can make it hard for newcomers to get involved—I often feel the same way. Recognizing this, I decided to take on the challenge of documenting py4godot more clearly for anyone who may want to contribute.
Over the next few months, I plan to add detailed notes that explain the design and structure of py4godot. Please keep in mind that my code is a work in progress. It’s not polished, and you might encounter some messy C++ and Python code along the way. If that doesn’t appeal to you, feel free to skip this page—but if you're curious, I hope this documentation provides a helpful starting point.
Note: This is a work in progress. Hopefully, over the next few months, you'll see more content published here.
This project uses cython to bind python to C++. The reason, I use it is mostly historically, because when I started for Godot 3, I saw that it was being used by godot-python
You need a json file to get all the classes and their methods, Godot exposes. You can dump it like that: godot --dump-extension-api
. We also need a header file which exposes the gdextension functionality. So we can run godot --dump-gdextension-interface
to obtain it. The resulting header file exposes methods, we can later use for our bindings to godot
In the following the structure of the classes beign generated from the information in the json api file of godot will be explained. They are being generated by the file generation_files/generate_classes.py
and generation_files/generate_classes_pxd.py
Here is an example for the pxd file, I generate for the bindings:
# distutils: language=c++
cimport py4godot.classes.Node as py4godot_node
from py4godot.classes.Object cimport *
from libcpp.memory cimport shared_ptr, allocator
from py4godot.classes.cpp_bridge cimport Node3D as CPPNode3D
from py4godot.classes.core cimport *
cpdef enum Node3D__RotationEditMode:
Node3D__ROTATION_EDIT_MODE_EULER = 0
Node3D__ROTATION_EDIT_MODE_QUATERNION = 1
Node3D__ROTATION_EDIT_MODE_BASIS = 2
cdef class Node3D(py4godot_node.Node):
cdef shared_ptr [CPPNode3D] Node3D_internal_class_ptr
cdef void set_gdowner(self, void* owner)
cdef object py__transform
cdef object py__global_transform
cdef object py__position
cdef object py__rotation
cdef object py__rotation_degrees
cdef object py__quaternion
cdef object py__basis
cdef object py__scale
cdef object py__rotation_edit_mode
cdef object py__rotation_order
cdef object py__top_level
cdef object py__global_position
cdef object py__global_basis
cdef object py__global_rotation
cdef object py__global_rotation_degrees
cdef object py__visible
cdef object py__visibility_parent
cdef public object visibility_changed
Let's break down, what is happening here. First the imports:
cimport py4godot.classes.Node as py4godot_node
from py4godot.classes.Object cimport *
from libcpp.memory cimport shared_ptr, allocator
from py4godot.classes.cpp_bridge cimport Node3D as CPPNode3D
from py4godot.classes.core cimport *
First, we import the parent class, as we need to specify that our class will inherit from it. We can probably ignore the Object import you see here, as it isn’t actually used. Next, we import some pointer functionality from C++ so we can work with shared pointers. Later, we’ll see that we maintain a pointer to the equivalent C++ class for our Cython class. Finally, we import some core classes, though they won’t be used in the rest of the code.
cdef class Node3D(py4godot_node.Node):
cdef shared_ptr [CPPNode3D] Node3D_internal_class_ptr
cdef void set_gdowner(self, void* owner)
Here we define our Cython class. As we can see, it holds a pointer to the equivalent C++ class. We also have a method method set_gdowner
. This is used to set the shared_ptr
to the correct values. We'll dive later into it.
cdef object py__transform
cdef object py__global_transform
cdef object py__position
cdef object py__rotation
cdef object py__rotation_degrees
cdef object py__quaternion
cdef object py__basis
cdef object py__scale
cdef object py__rotation_edit_mode
cdef object py__rotation_order
cdef object py__top_level
cdef object py__global_position
cdef object py__global_basis
cdef object py__global_rotation
cdef object py__global_rotation_degrees
cdef object py__visible
cdef object py__visibility_parent
cdef public object visibility_changed
Finally, the py__
values, you see, are the properties of this class. As I'm managing memory in Python, I need some mechanism, when classes would be deleted by Python garbage collection. To make this feasible, I store them in the class and let python take care of it.
The last cdef public object visibility_changed
, you see here is actually a Signal, which the user can use when scripting. Unfortunately I couldn't specify the type here, as it would have lead to a cyclic import. We'll dive later into signals
cdef class Node3D(py4godot_node.Node):
@staticmethod
def constructor():
cdef Node3D class_ = Node3D.__new__(Node3D)
class_.Node3D_internal_class_ptr = construct_Node3D()
class_.set_gdowner(class_.Node3D_internal_class_ptr.get().get_godot_owner())
return class_
@staticmethod
def new():
cdef Node3D class_ = Node3D.__new__(Node3D)
class_.Node3D_internal_class_ptr = construct_Node3D()
class_.set_gdowner(class_.Node3D_internal_class_ptr.get().get_godot_owner())
return class_
def __init__(self):
if py_utils.shouldCreateObject:
self.Node3D_internal_class_ptr = construct_Node3D()
self.set_gdowner(self.Node3D_internal_class_ptr.get().get_godot_owner())
def __cinit__(self):
self.Node3D_internal_class_ptr = make_shared[CPPNode3D]()
self.already_deallocated = False
self.Node_internal_class_ptr = make_shared[CPPNode]()
self.Object_internal_class_ptr = make_shared[CPPObject]()
cdef StringName visibility_changed_name = py_c_string_to_string_name("visibility_changed")
self.visibility_changed = BuiltinSignal(self, visibility_changed_name)
cdef void set_gdowner(self, void* owner):
self.Node3D_internal_class_ptr.get().set_gdowner_Node3D(owner)
self.Node_internal_class_ptr.get().set_gdowner_Node(owner)
self.Object_internal_class_ptr.get().set_gdowner_Object(owner)
def __dealloc__(self):
pass
@staticmethod
def cast(Object other):
assert other != None # Object to be casted must not be None
cdef Node3D cls = Node3D.__new__(Node3D)
cls.Node3D_internal_class_ptr = cast_to_Node3D(other.Object_internal_class_ptr.get())
cls.set_gdowner(other.Object_internal_class_ptr.get().get_godot_owner())
return cls
@property
def transform(self):
cdef _ret = self. get_transform()
return _ret
@transform.setter
def transform(self, Transform3D value):
self.set_transform(value)
# More properties...
def set_transform(self, Transform3D local ):
assert(not local is None)
self.py__transform = local
self.Node3D_internal_class_ptr.get().py_set_transform(local.Transform3D_internal_class_ptr)
def get_transform(self):
cdef Transform3D _ret = Transform3D.__new__(Transform3D)
_ret.Transform3D_internal_class_ptr = self.Node3D_internal_class_ptr.get().py_get_transform()
_ret.set_gdowner(_ret.Transform3D_internal_class_ptr.get().get_godot_owner())
self.py__transform = _ret
return _ret
# More methods ...