Make 2D games with Python - No C++ required!
Source Code • Downloads • Quickstart • Tutorials • API Reference • Cookbook • C++ Extensions
🚀 Take your game to the next level by extending the engine with custom C++ components
While McRogueFace provides a powerful Python API for game development, there are scenarios where extending the engine in C++ provides significant benefits:
McRogueFace follows a layered architecture that makes it extensible:
┌─────────────────────────────────────────────────┐
│ Python Game Scripts │
│ (game.py, entities.py, etc.) │
├─────────────────────────────────────────────────┤
│ Python API Layer (mcrfpy) │
│ (McRFPy_API.cpp, bindings) │
├─────────────────────────────────────────────────┤
│ Core Engine Components │
│ ┌─────────────┬─────────────┬─────────────┐ │
│ │ UI System │ Scene │ Entity │ │
│ │ (UIFrame, │ Manager │ System │ │
│ │ UISprite) │ │ │ │
│ └─────────────┴─────────────┴─────────────┘ │
├─────────────────────────────────────────────────┤
│ SFML Framework │
│ (Graphics, Audio, Window, System) │
└─────────────────────────────────────────────────┘
# Ubuntu/Debian
sudo apt-get install -y \
build-essential \
cmake \
libsfml-dev \
python3.12-dev \
git
# macOS
brew install cmake sfml python@3.12
# Windows (using vcpkg)
vcpkg install sfml
git clone https://github.com/mcrogueface/engine.git
cd engine
git submodule update --init --recursive
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Debug
make -j$(nproc)
git checkout -b feature/my-extension
code .
### Recommended IDE Setup
For VS Code, install these extensions:
- C/C++ (Microsoft)
- CMake Tools
- Python
Create `.vscode/c_cpp_properties.json`:
```json
{
"configurations": [{
"name": "Linux",
"includePath": [
"${workspaceFolder}/src/**",
"${workspaceFolder}/deps/**",
"/usr/include/python3.12"
],
"defines": [],
"compilerPath": "/usr/bin/g++",
"cStandard": "c17",
"cppStandard": "c++20"
}]
}
McRogueFace/
├── src/ # C++ source files
│ ├── UI*.h/cpp # UI component files
│ ├── Py*.h/cpp # Python binding helpers
│ ├── McRFPy_API.h/cpp # Main Python API
│ ├── Scene.h/cpp # Scene management
│ └── main.cpp # Entry point
├── deps/ # Dependencies
│ ├── cpython/ # Python headers
│ └── libtcod/ # Roguelike utilities
├── assets/ # Game assets
├── scripts/ # Python game scripts
└── CMakeLists.txt # Build configuration
UI[Component].h/cpp
(e.g., UIFrame, UISprite)Py[Type].h
(e.g., PyTexture, PyColor)typedef struct { PyObject_HEAD ... } Py[Type]Object;
Let’s create a custom progress bar component step by step.
Create src/UIProgressBar.h
:
#pragma once
#include "UIDrawable.h"
#include "PyColor.h"
#include <SFML/Graphics.hpp>
class UIProgressBar : public UIDrawable {
private:
sf::RectangleShape background;
sf::RectangleShape fill;
float progress = 0.0f; // 0.0 to 1.0
float width, height;
public:
UIProgressBar(float x, float y, float w, float h);
// UIDrawable interface
void render(sf::Vector2f position, sf::RenderTarget& target) override;
PyObjectsEnum derived_type() override { return UIPROGRESSBAR; }
UIDrawable* click_at(sf::Vector2f point) override;
// Phase 1 implementations
sf::FloatRect get_bounds() const override;
void move(float dx, float dy) override;
void resize(float w, float h) override;
// Progress bar specific
void setProgress(float value);
float getProgress() const { return progress; }
void setFillColor(const sf::Color& color);
void setBackgroundColor(const sf::Color& color);
// Animation support
bool setProperty(const std::string& name, float value) override;
bool getProperty(const std::string& name, float& value) const override;
};
Update src/UIDrawable.h
:
enum PyObjectsEnum : int
{
UIFRAME = 1,
UICAPTION,
UISPRITE,
UIGRID,
UIPROGRESSBAR // Add this
};
Create src/UIProgressBar.cpp
:
#include "UIProgressBar.h"
UIProgressBar::UIProgressBar(float x, float y, float w, float h)
: width(w), height(h) {
position = sf::Vector2f(x, y);
// Set up background
background.setSize(sf::Vector2f(width, height));
background.setFillColor(sf::Color(50, 50, 50, 255));
background.setOutlineColor(sf::Color(100, 100, 100, 255));
background.setOutlineThickness(2.0f);
// Set up fill
fill.setSize(sf::Vector2f(0, height));
fill.setFillColor(sf::Color(0, 255, 0, 255));
}
void UIProgressBar::render(sf::Vector2f position, sf::RenderTarget& target) {
if (!visible) return;
// Apply opacity
auto bgColor = background.getFillColor();
bgColor.a = static_cast<sf::Uint8>(255 * opacity);
background.setFillColor(bgColor);
auto fillColor = fill.getFillColor();
fillColor.a = static_cast<sf::Uint8>(255 * opacity);
fill.setFillColor(fillColor);
// Position elements
sf::Vector2f absolute_pos = position + this->position;
background.setPosition(absolute_pos);
fill.setPosition(absolute_pos);
// Draw
target.draw(background);
if (progress > 0.0f) {
target.draw(fill);
}
}
UIDrawable* UIProgressBar::click_at(sf::Vector2f point) {
if (background.getGlobalBounds().contains(point)) {
return this;
}
return nullptr;
}
void UIProgressBar::setProgress(float value) {
progress = std::max(0.0f, std::min(1.0f, value));
fill.setSize(sf::Vector2f(width * progress, height));
}
sf::FloatRect UIProgressBar::get_bounds() const {
return sf::FloatRect(position.x, position.y, width, height);
}
void UIProgressBar::move(float dx, float dy) {
position.x += dx;
position.y += dy;
}
void UIProgressBar::resize(float w, float h) {
width = w;
height = h;
background.setSize(sf::Vector2f(width, height));
setProgress(progress); // Update fill size
}
bool UIProgressBar::setProperty(const std::string& name, float value) {
if (name == "progress") {
setProgress(value);
return true;
}
return false;
}
bool UIProgressBar::getProperty(const std::string& name, float& value) const {
if (name == "progress") {
value = progress;
return true;
}
return false;
}
Now let’s expose our progress bar to Python.
Create the Python object structure in src/UIProgressBar.h
:
// Python object structure
typedef struct {
PyObject_HEAD
std::shared_ptr<UIProgressBar> data;
} PyUIProgressBarObject;
// Forward declarations
extern PyMethodDef UIProgressBar_methods[];
extern PyGetSetDef UIProgressBar_getsetters[];
Add to src/UIProgressBar.cpp
:
// Python property getters/setters
static PyObject* UIProgressBar_get_progress(PyObject* self, void* closure) {
auto obj = (PyUIProgressBarObject*)self;
return PyFloat_FromDouble(obj->data->getProgress());
}
static int UIProgressBar_set_progress(PyObject* self, PyObject* value, void* closure) {
auto obj = (PyUIProgressBarObject*)self;
if (!PyFloat_Check(value)) {
PyErr_SetString(PyExc_TypeError, "progress must be a float");
return -1;
}
obj->data->setProgress(PyFloat_AsDouble(value));
return 0;
}
// Python methods
static PyObject* UIProgressBar_setFillColor(PyObject* self, PyObject* args) {
PyUIProgressBarObject* obj = (PyUIProgressBarObject*)self;
PyObject* color_obj;
if (!PyArg_ParseTuple(args, "O", &color_obj)) {
return NULL;
}
// Convert Python color to sf::Color
sf::Color color;
if (!PyColor::from_python(color_obj, color)) {
return NULL;
}
obj->data->setFillColor(color);
Py_RETURN_NONE;
}
// Method definitions
PyMethodDef UIProgressBar_methods[] = {
{"setFillColor", UIProgressBar_setFillColor, METH_VARARGS,
"setFillColor(color: Color) -> None\n\n"
"Set the fill color of the progress bar.\n\n"
"Args:\n"
" color: Color object for the fill"},
{NULL}
};
// Property definitions
PyGetSetDef UIProgressBar_getsetters[] = {
{"progress", UIProgressBar_get_progress, UIProgressBar_set_progress,
"Current progress value (0.0 to 1.0)", NULL},
{"x", UIDrawable::get_float_member, UIDrawable::set_float_member,
"X position in pixels", (void*)offsetof(UIProgressBar, position.x)},
{"y", UIDrawable::get_float_member, UIDrawable::set_float_member,
"Y position in pixels", (void*)offsetof(UIProgressBar, position.y)},
{NULL}
};
// Init function
static int UIProgressBar_init(PyObject* self, PyObject* args, PyObject* kwds) {
auto obj = (PyUIProgressBarObject*)self;
float x = 0, y = 0, w = 100, h = 20;
static char* kwlist[] = {"x", "y", "w", "h", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffff", kwlist,
&x, &y, &w, &h)) {
return -1;
}
obj->data = std::make_shared<UIProgressBar>(x, y, w, h);
return 0;
}
Create the type definition in a namespace:
namespace mcrfpydef {
static PyTypeObject PyUIProgressBarType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.ProgressBar",
.tp_basicsize = sizeof(PyUIProgressBarObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)[](PyObject* self) {
auto obj = (PyUIProgressBarObject*)self;
obj->data.reset();
Py_TYPE(self)->tp_free(self);
},
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("ProgressBar(x=0, y=0, w=100, h=20)\n\n"
"A progress bar UI element.\n\n"
"Args:\n"
" x, y: Position in pixels\n"
" w, h: Size in pixels"),
.tp_methods = UIProgressBar_methods,
.tp_getset = UIProgressBar_getsetters,
.tp_base = &mcrfpydef::PyDrawableType,
.tp_init = UIProgressBar_init,
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* {
auto self = (PyUIProgressBarObject*)type->tp_alloc(type, 0);
if (self) self->data = std::make_shared<UIProgressBar>(0, 0, 100, 20);
return (PyObject*)self;
}
};
}
Update src/McRFPy_API.cpp
to register the new type:
// In api_init() function, add:
if (PyType_Ready(&mcrfpydef::PyUIProgressBarType) < 0) {
PyErr_SetString(PyExc_RuntimeError, "Failed to ready ProgressBar type");
return;
}
Py_INCREF(&mcrfpydef::PyUIProgressBarType);
PyModule_AddObject(mcrf_module, "ProgressBar",
(PyObject*)&mcrfpydef::PyUIProgressBarType);
Add the new files to CMakeLists.txt
or ensure they’re picked up by the glob pattern.
Create a test script to verify your progress bar works:
# tests/test_progress_bar.py
import mcrfpy
from mcrfpy import ProgressBar, Frame, Color
import sys
def test_progress_bar():
"""Test the custom progress bar implementation"""
# Create a test scene
mcrfpy.createScene("test")
# Create a frame to hold our progress bar
frame = Frame(100, 100, 400, 300)
frame.fill_color = Color(20, 20, 20, 255)
# Create progress bars
progress1 = ProgressBar(50, 50, 300, 30)
progress1.progress = 0.0
progress1.setFillColor(Color(0, 255, 0, 255))
progress2 = ProgressBar(50, 100, 300, 30)
progress2.progress = 0.5
progress2.setFillColor(Color(255, 255, 0, 255))
progress3 = ProgressBar(50, 150, 300, 30)
progress3.progress = 1.0
progress3.setFillColor(Color(255, 0, 0, 255))
# Add to frame
frame.children.append(progress1)
frame.children.append(progress2)
frame.children.append(progress3)
# Add frame to scene
ui = mcrfpy.sceneUI("test")
ui.append(frame)
# Animate progress bars
def update_progress(elapsed):
# Increment first progress bar
progress1.progress = min(1.0, progress1.progress + 0.01)
# Pulse second progress bar
import math
progress2.progress = abs(math.sin(elapsed * 2))
# Check if done
if progress1.progress >= 1.0:
print("PASS: Progress bar animation completed")
sys.exit(0)
# Set timer for animation
mcrfpy.setTimer("animate", update_progress, 16) # ~60 FPS
# Switch to test scene
mcrfpy.setScene("test")
# Run test
test_progress_bar()
Run the test:
cd build
./mcrogueface --exec ../tests/test_progress_bar.py
Let’s build a more complex extension - a particle system for visual effects.
Our particle system will:
// src/UIParticleSystem.h
#pragma once
#include "UIDrawable.h"
#include <vector>
#include <random>
struct Particle {
sf::Vector2f position;
sf::Vector2f velocity;
sf::Color color;
float lifetime;
float age;
float size;
};
class UIParticleSystem : public UIDrawable {
private:
std::vector<Particle> particles;
std::vector<sf::Vertex> vertices; // For batch rendering
// Emitter properties
sf::Vector2f emission_area;
float emission_rate = 10.0f;
float emission_timer = 0.0f;
// Particle properties
float initial_velocity = 100.0f;
float velocity_variation = 50.0f;
sf::Vector2f gravity = {0, 100};
float particle_lifetime = 2.0f;
float particle_size = 2.0f;
// System state
bool emitting = true;
std::mt19937 rng;
std::uniform_real_distribution<float> dist;
void emitParticle();
void updateParticle(Particle& p, float dt);
public:
UIParticleSystem(float x, float y);
void update(float dt);
void render(sf::Vector2f position, sf::RenderTarget& target) override;
PyObjectsEnum derived_type() override { return UIPARTICLESYSTEM; }
// Control methods
void start() { emitting = true; }
void stop() { emitting = false; }
void burst(int count);
void clear() { particles.clear(); }
// Property access
void setGravity(float x, float y) { gravity = {x, y}; }
void setEmissionRate(float rate) { emission_rate = rate; }
void setParticleLifetime(float time) { particle_lifetime = time; }
};
Implementation details:
// src/UIParticleSystem.cpp
#include "UIParticleSystem.h"
UIParticleSystem::UIParticleSystem(float x, float y)
: rng(std::random_device{}()), dist(0.0f, 1.0f) {
position = sf::Vector2f(x, y);
emission_area = sf::Vector2f(10, 10);
}
void UIParticleSystem::emitParticle() {
Particle p;
// Random position within emission area
p.position = position + sf::Vector2f(
(dist(rng) - 0.5f) * emission_area.x,
(dist(rng) - 0.5f) * emission_area.y
);
// Random velocity
float angle = dist(rng) * 2 * M_PI;
float speed = initial_velocity + (dist(rng) - 0.5f) * velocity_variation;
p.velocity = sf::Vector2f(cos(angle) * speed, sin(angle) * speed);
// Initial properties
p.color = sf::Color(255, 200, 100, 255);
p.lifetime = particle_lifetime;
p.age = 0.0f;
p.size = particle_size;
particles.push_back(p);
}
void UIParticleSystem::update(float dt) {
// Emit new particles
if (emitting) {
emission_timer += dt;
float emit_interval = 1.0f / emission_rate;
while (emission_timer >= emit_interval) {
emitParticle();
emission_timer -= emit_interval;
}
}
// Update existing particles
for (auto it = particles.begin(); it != particles.end();) {
updateParticle(*it, dt);
if (it->age >= it->lifetime) {
it = particles.erase(it);
} else {
++it;
}
}
// Prepare vertices for rendering
vertices.clear();
vertices.reserve(particles.size() * 6); // 2 triangles per particle
for (const auto& p : particles) {
float alpha = 1.0f - (p.age / p.lifetime);
sf::Color color = p.color;
color.a = static_cast<sf::Uint8>(255 * alpha * opacity);
// Create a quad with two triangles
float half_size = p.size / 2.0f;
sf::Vector2f tl = p.position + sf::Vector2f(-half_size, -half_size);
sf::Vector2f tr = p.position + sf::Vector2f(half_size, -half_size);
sf::Vector2f br = p.position + sf::Vector2f(half_size, half_size);
sf::Vector2f bl = p.position + sf::Vector2f(-half_size, half_size);
// Triangle 1
vertices.emplace_back(tl, color);
vertices.emplace_back(tr, color);
vertices.emplace_back(br, color);
// Triangle 2
vertices.emplace_back(tl, color);
vertices.emplace_back(br, color);
vertices.emplace_back(bl, color);
}
}
void UIParticleSystem::updateParticle(Particle& p, float dt) {
// Apply physics
p.velocity += gravity * dt;
p.position += p.velocity * dt;
// Age particle
p.age += dt;
// Color fade (example: fade from yellow to red)
float age_ratio = p.age / p.lifetime;
p.color.g = static_cast<sf::Uint8>(200 * (1.0f - age_ratio));
}
void UIParticleSystem::render(sf::Vector2f offset, sf::RenderTarget& target) {
if (!visible || vertices.empty()) return;
// Apply position offset to all vertices
sf::Transform transform;
transform.translate(offset);
// Draw all particles in one batch
target.draw(vertices.data(), vertices.size(), sf::Triangles, transform);
}
The Python bindings follow the same pattern as before, but with additional methods:
# Example usage in Python
import mcrfpy
from mcrfpy import ParticleSystem, Frame
# Create a particle system
particles = ParticleSystem(400, 300)
particles.setGravity(0, 200) # Downward gravity
particles.setEmissionRate(50) # 50 particles per second
particles.setParticleLifetime(3.0) # 3 second lifetime
# Add to scene
frame = Frame(0, 0, 800, 600)
frame.children.append(particles)
# Control emission
def on_click(x, y, button):
if button == 1: # Left click
particles.burst(100) # Emit 100 particles at once
elif button == 3: # Right click
particles.stop() # Stop emitting
frame.click = on_click
✅ DO:
std::shared_ptr
for objects shared between C++ and Python❌ DON’T:
✅ DO:
// Proper reference counting
static PyObject* get_property(PyObject* self, void* closure) {
PyObject* result = PyFloat_FromDouble(value);
return result; // New reference
}
// Proper error handling
if (!PyArg_ParseTuple(args, "ff", &x, &y)) {
return NULL; // Python exception already set
}
❌ DON’T:
// Missing incref - will crash!
static PyObject* bad_getter(PyObject* self, void* closure) {
return some_cached_object; // Missing Py_INCREF!
}
// Missing error check
float x = PyFloat_AsDouble(obj); // Could be -1 with error set!
McRogueFace runs Python in a single thread. When adding C++ extensions:
// Class naming: UI prefix for UI components
class UIMyComponent : public UIDrawable {
// Private members first
private:
float my_property;
// Then public interface
public:
UIMyComponent();
// Override virtual methods
void render(sf::Vector2f pos, sf::RenderTarget& target) override;
};
// Python type naming: Py prefix
typedef struct {
PyObject_HEAD
std::shared_ptr<UIMyComponent> data;
} PyUIMyComponentObject;
// Namespace for type definitions
namespace mcrfpydef {
static PyTypeObject PyUIMyComponentType = { ... };
}
Every public method must have inline documentation:
{"myMethod", (PyCFunction)UIMyComponent::myMethod, METH_VARARGS,
"myMethod(arg1: float, arg2: int = 0) -> bool\n\n"
"Brief description of what the method does.\n\n"
"Args:\n"
" arg1: Description of first argument\n"
" arg2: Description of second argument (default: 0)\n\n"
"Returns:\n"
" True if successful, False otherwise\n\n"
"Example:\n"
" result = component.myMethod(1.5, 2)"},
Create comprehensive tests:
# tests/test_my_component.py
import mcrfpy
from mcrfpy import MyComponent
import sys
def test_basic_functionality():
"""Test basic component creation and properties"""
comp = MyComponent(10, 20)
assert comp.x == 10
assert comp.y == 20
def test_edge_cases():
"""Test boundary conditions and error handling"""
comp = MyComponent()
comp.setProperty(-1) # Should handle gracefully
def run_all_tests():
test_basic_functionality()
test_edge_cases()
print("PASS: All tests completed successfully")
sys.exit(0)
# Schedule tests to run after game loop starts
mcrfpy.setTimer("test", run_all_tests, 100)
Extending McRogueFace with C++ opens up endless possibilities for enhancing your roguelike games. Whether you’re adding particle effects, custom UI components, or integrating external libraries, the engine’s architecture makes it straightforward to add new functionality while maintaining compatibility with the Python scripting layer.
Remember:
Happy hacking! 🚀
For more information, check out the API Reference or join our Discord community.