Custom Python Module
Creating a Game Library
There will be code shared between each lesson and we wouldn't want to copy and paste the same code again and again as that goes against the DRY principle. Moving forward, we will keep all code files that will be shared within the folder ./include/re
. With that being said we will move our python code (including from the previous section) into ./include/re/python
.
Python Modules Class
We have successfully called functions from python modules and class instances but we have yet to call C++ code from python. The best way to do that is to start defining the api for Red Engine. To keep things simple and concise for now we'll just define one python module named engine
which contains two functions. The first function get_version
doesn't take any parameters and will just return a hard coded string that represents the version of Red Engine. Our second function print_log
will simply take a string as a parameter and print it out to the console. Now that we have an idea of what to do let's create our class for defining and implementing custom python modules.
#ifndef PYTHON_MODULES_H
#define PYTHON_MODULES_H
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <string>
class EnginePythonModule {
public:
static PyObject* get_version(PyObject* self, PyObject* args);
static PyObject* print_log(PyObject* self, PyObject* args, PyObject* kwargs);
};
static struct PyMethodDef engineModuleMethods[] = {
{
"get_version", EnginePythonModule::get_version,
METH_VARARGS, "Gets version of the engine."
},
{
"print_log", (PyCFunction) EnginePythonModule::print_log,
METH_VARARGS | METH_KEYWORDS, "Logs a message to the console."
},
{nullptr, nullptr, 0,nullptr },
};
static struct PyModuleDef engineModuleDefinition = {
PyModuleDef_HEAD_INIT, "engine", nullptr, -1, engineModuleMethods,
nullptr, nullptr, nullptr, nullptr
};
static char *enginePrintLogKWList[] = {"message", nullptr};
static PyObject* PyInit_engine(void) {
return PyModule_Create(&engineModuleDefinition);
}
#endif //PYTHON_MODULES_H
The EnginePythonModule
class defines the two functions that we went over earlier. Notice that print_log
has an extra parameter for kwargs
as the parameter will have a keyword named message
. After that we define a struct which contains our module function definitions. Take note how print_log
has to cast it's function to PyCFuntion
to support keyword arguments. Next we define our module definition for engine
which we passed in our previously created engineModuleMethods
struct. enginePrintLogKWList
contains the keywords for our print_log
function. message
is passed in by reference so we can set the value to what's passed in as the message
keyword argument. Lastly PyInit_engine
will be used to create our module. We will return to PyInit_engine
later as we'll need to import the engine
module before initializing the python interpreter.
With the header out of the way let's write the implementation of our two functions.
#include "python_modules.h"
#include <iostream>
PyObject* EnginePythonModule::get_version(PyObject *self, PyObject *args) {
return Py_BuildValue("s", "v0.0.1");
}
PyObject* EnginePythonModule::print_log(PyObject *self, PyObject *args, PyObject *kwargs) {
char *message;
if (PyArg_ParseTupleAndKeywords(args, kwargs, "s", enginePrintLogKWList, &message)) {
std::cout << "[INFO] " << message << std::endl;
Py_RETURN_NONE;
}
return nullptr;
}
get_version
is really simple as it is just returning a string which contain 'v0.0.1' with Py_BuildValue
. The next function print_log
is slightly more complicated but not by much. We use PyArg_ParseTupleKeywords
to get the values of the argument passed in. enginePrintLogKWList
is passed in as we have the defined message
as a keyword argument.
With our new module defined and implemented, we have to now import it into the python interpreter. Update the construtor in pyhelper.hpp
to add our newly created module with PyImport_AppendInittab
. This should be called before Py_Initialize
.
CPyInstance() {
Py_SetProgramName(L"red_engine");
PyImport_AppendInittab("engine", &PyInit_engine);
Py_Initialize();
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append(\".\")");
}
Don't forget to include the header for python_modules.h
!
We can finally call C++ code from python. Let's update our game.py
script to import the engine
module and call the two functions we defined in C++ earlier.
import engine
class Player:
def talk(self, message: str) -> None:
engine_version = engine.get_version()
engine.print_log(message=f"Engine version = {engine_version}")
We're going to keep things easy and just use the same function talk
that was used in the previous section. There are a few differences now, the first line imports our engine
module to be used by the python script. We're going to ignore the message
parameter of the talk
function. Next we call engine.get_version()
which returns from our C++ function we defined early a hard coded version string. We then call engine.print_log()
to print a log statement to the console.
We will keep our main
function defined in our C++ code the same as changes aren't needed except to update the path of PythonObjectManager
header:
#include "./re/python/python_object_manager.h"
The Final output when we run the engine will be [INFO] Engine version = v0.0.1
. All the code for this section can be found here. Now that we have a good foundation for the scripting system, it's time to focus next on creating the game loop.