Souleng is a component-based game engine which allows for the creation of games entirely in Python, utilizing the modern pybind11
fork pybind11k
to allow users to derive their own custom Components in Python and effortlessly interface with the C++ side of things.
You can build the engine on Linux with build.sh
, provided in the engine
directory.
You can run the engine with run.sh
, which will automatically set the LD_LIBRARY_PATH
to be the lib/
directory. You can also set this variable yourself and run the binary directly. The expected format is
$ ./run.sh <scene>
where the scene is either $ ./run.sh games/mario
would load and run the scene in the file games/mario.py
.scenes
extension. This file should contain the relative path from itself to any number of Python files, specified as in the above bullet points. For example, the file mario.scenes
with content
ui/mario_menu
mario
ui/game_over
would load the scenes in ui/mario_menu.py
, mario
, and ui/game_over
, and would start running the scene in ui/mario_menu.py
To create a scene in Python, at minimum the following functions must be present in the script:
scene_startup
scene_shutdown
scene_input
scene_update
scene_render
Custom components can be created as follows:
import souleng as sw
class Foo(sw.ScriptComponent):
def __init__():
super().__init__()
...
def input():
... # handle input here
def update(dt: float):
...
def render():
...
Custom functions can be written as well, but these functions must be overridden for the component to run code every frame, as the C++ engine will only call these functions.
Three additional submodules exist, to allow the user to interface with different systems in the game engine:
input
- allows the user to query the input manager for keys pressed and mouse clicks get_key(key: str)
- returns an object for the key
showing if it's been pressed
, held
, or released
this frame
get_mouse_click()
- returns an object with mouse click data from the current frame pressed
- True
if the mouse button was first pressed this frame
held
- True
if the mouse button was held down this frame
released
- True
if the mouse button was released this frame
clicks
- number of times the mouse was clicked this frame
pos
- vector with x and y for the current mouse position
button
- the mouse button pressed this frame
get_mouse_motion()
- returns an object with mouse motion data from the current frame motion
- vector with x and y for the amount the mouse moved in each direction since the last time it was polled
pos
- vector with x and y for the current mouse position
render
- allows the user to call rendering methods necessary to run a scene, interfacing with SDL set_render_draw_color(r: int, g: int, b: int, a: int)
- sets the background color. equivalent to SDL_SetRenderDrawColor
render_clear()
- clears the rendering target. equivalent to SDL_RenderClear
render_present()
- presents rendered data to the screen. equivalent to SDL_RenderPresent
director
- allows the user to change scenes change_scene(name: str)
- changes the current scene to the one specified by name
The full list of available Python methods can be found in engine/src/bindings.cpp
.
My main inspiration for making Souleng was the video for the Eternal engine, linked as a good sample game engine in the final project repository. In their video demonstration, they showed games using Python components such that most of the game could just be scripted in Python. However, these components were a bit ad-hoc: they required the module and class name of the Python module to link properly. I wanted to improve on this idea, to create a seamless integration between the scripting side of the game engine and the actual components.
In order to do this, I looked more closely at how Pybind worked, and tried to understand exactly why it was not possible to inherit from components in Python, and then use them in C++. I ran into the issue of this not being possible when adapting Space Invaders for this new engine, but researching it shows that this would only happen if Python no longer held any references to the object being passed to C++. It would "slice" components, so that any Python specific logic was lost. A more modern fork of pybind11
addressed this issue, so I opted to use that library and included it in my game engine.
Ultimately, my game engine accomplished what it set out to do, and I'm happy with that component of it. The game development process in Python has as much power as it did in C++, but all of the parts of the engine which require high performance can still run in C++, including the game loop itself. This engine also adds some smaller features onto the previous iteration, including support for spritesheets (swapping textures), loading multiple scenes, and much more comprehensive input handling (handling all keys, and mouse clicks/motion).
There was still a lot I wish I could've accomplished for this project, as much of it was spent understanding the internals of pybind rather than focusing on more user-friendly features. Scenes in the game engine can be layed out in scene_startup
in Python, but there is no GUI editor to accomplish this. Additionally, I would've liked to had this GUI editor parse the AST for any Python scenes, to detect any custom components and allow users to add them through the editor, as with a game engine like Unity. I would also add some more builtin components, for things like sound and animation- sound would be something that C++ is needed for, but even something like animation could be implemented from Python, which would be a good showcase of the potential of this scripting feature. I would give more thought to the memory model used for the game engine, possibly using a memory arena to manage lifetimes rather than shared pointers, to allow for more granular control over the lifetimes of game objects. Finally, I would like to evaluate the way I was using singleton data structures- they felt necessary at times, but there might've been room for some other construct (maybe even just static classes, since they need to live for the lifetime of the program).