Skip to content

How does py4godot work?

Niklas Zimmer edited this page Nov 13, 2024 · 20 revisions

Motivation

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.

Cython

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

gdextension

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

Structure of Python classes

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.pyand generation_files/generate_classes_pxd.py

pxd class files

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

pyx class files

Clone this wiki locally