McRogueFace

Extending McRogueFace with C++

Make 2D games with Python - No C++ required!


Source CodeDownloadsQuickstartTutorialsAPI ReferenceCookbookC++ Extensions


🚀 Take your game to the next level by extending the engine with custom C++ components

Table of Contents

  1. Introduction: When and Why to Extend in C++
  2. Architecture Overview
  3. Setting Up Your Development Environment
  4. Understanding the Codebase Structure
  5. Creating a New UI Element
  6. Adding Python Bindings
  7. Testing Your Extension
  8. Case Study: Creating a Custom Particle System
  9. Best Practices and Common Pitfalls
  10. Contributing Back to Core

Introduction: When and Why to Extend in C++

While McRogueFace provides a powerful Python API for game development, there are scenarios where extending the engine in C++ provides significant benefits:

When to Extend in C++

Benefits of C++ Extensions

Architecture Overview

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)            │
└─────────────────────────────────────────────────┘

Key Components

  1. UIDrawable Base Class: All visual elements inherit from this
  2. Scene System: Manages game states and transitions
  3. Python Bindings: CPython API integration for scripting
  4. Resource Management: Textures, fonts, and audio handling

Setting Up Your Development Environment

Prerequisites

# 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

Building McRogueFace

  1. Clone the repository:
    git clone https://github.com/mcrogueface/engine.git
    cd engine
    git submodule update --init --recursive
    
  2. Build the project:
    mkdir build && cd build
    cmake .. -DCMAKE_BUILD_TYPE=Debug
    make -j$(nproc)
    
  3. Set up development environment: ```bash

    Create a development branch

    git checkout -b feature/my-extension

Set up your IDE (VS Code example)

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"
    }]
}

Understanding the Codebase Structure

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

Key File Patterns

Creating a New UI Element

Let’s create a custom progress bar component step by step.

Step 1: Define the C++ Class

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;
};

Step 2: Add to PyObjectsEnum

Update src/UIDrawable.h:

enum PyObjectsEnum : int
{
    UIFRAME = 1,
    UICAPTION,
    UISPRITE,
    UIGRID,
    UIPROGRESSBAR  // Add this
};

Step 3: Implement the Class

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;
}

Adding Python Bindings

Now let’s expose our progress bar to Python.

Step 1: Define Python Type Structure

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[];

Step 2: Implement Python Methods

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;
}

Step 3: Register the Type

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;
        }
    };
}

Step 4: Register with Module

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);

Step 5: Update Build System

Add the new files to CMakeLists.txt or ensure they’re picked up by the glob pattern.

Testing Your Extension

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

Case Study: Creating a Custom Particle System

Let’s build a more complex extension - a particle system for visual effects.

Design Overview

Our particle system will:

Implementation

// 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);
}

Python Integration

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

Best Practices and Common Pitfalls

Memory Management

✅ DO:

❌ DON’T:

Python Integration

✅ 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!

Performance Considerations

  1. Batch Rendering: Use vertex arrays for many similar objects
  2. Dirty Flags: Only update when properties change
  3. Object Pooling: Reuse objects instead of creating/destroying
  4. Profile First: Use profiling tools before optimizing

Thread Safety

McRogueFace runs Python in a single thread. When adding C++ extensions:

Contributing Back to Core

Contribution Process

  1. Discuss First: Open an issue describing your extension
  2. Follow Standards: Match the existing code style
  3. Write Tests: Add tests for your new functionality
  4. Document: Update both inline docs and user documentation
  5. Submit PR: Create a pull request with clear description

Code Style Guidelines

// 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 = { ... };
}

Documentation Requirements

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)"},

Testing Requirements

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)

Conclusion

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.