Skip to content

Extensibility

PixelRoot32 is designed to be extensible. This guide covers how to extend the engine's core systems, including graphics drivers, audio backends, and game entities.

Graphics Drivers

The graphics system uses a Bridge Pattern to decouple the rendering logic from the physical hardware. You can implement your own driver by inheriting from BaseDrawSurface.

For a detailed walkthrough on creating display drivers, see the Custom Drivers Guide.

BaseDrawSurface vs DrawSurface

  • DrawSurface: The low-level interface defining all possible drawing operations.
  • BaseDrawSurface: A helper class that provides default implementations for most drawing primitives (lines, circles, etc.) by calling drawPixel(). Always prefer inheriting from this class to minimize boilerplate code.

Audio Backends

Implement the AudioBackend interface for custom audio hardware.

AudioBackend Interface

#include <audio/AudioBackend.h>

class MyCustomAudioBackend : public pixelroot32::audio::AudioBackend {
public:
    // Required methods
    void init() override;
    void start() override;
    void stop() override;
    uint32_t getSampleRate() const override;

    // Audio generation
    int16_t generateSample() override;

    // Channel management
    void setChannelWave(int channel, pixelroot32::audio::WaveType type, float frequency, float duty) override;
    void setChannelVolume(int channel, float volume) override;
    void stopChannel(int channel) override;
};

Example: Custom Audio Backend

#include <audio/AudioBackend.h>

class CustomAudioBackend : public pixelroot32::audio::AudioBackend {
private:
    uint32_t sampleRate;
    float phase[4] = {0, 0, 0, 0}; // 4 channels
    float frequency[4] = {0, 0, 0, 0};
    float volume[4] = {0, 0, 0, 0};
    pixelroot32::audio::WaveType waveType[4];

public:
    CustomAudioBackend(uint32_t rate) : sampleRate(rate) {
        for (int i = 0; i < 4; i++) {
            waveType[i] = pixelroot32::audio::WaveType::PULSE;
            volume[i] = 0.0f;
        }
    }

    void init() override {
        // Initialize your audio hardware
    }

    void start() override {
        // Start audio output
    }

    void stop() override {
        // Stop audio output
    }

    uint32_t getSampleRate() const override {
        return sampleRate;
    }

    int16_t generateSample() override {
        float sample = 0.0f;

        for (int ch = 0; ch < 4; ch++) {
            if (frequency[ch] > 0 && volume[ch] > 0) {
                float phaseIncrement = frequency[ch] / sampleRate;
                phase[ch] += phaseIncrement;
                if (phase[ch] >= 1.0f) phase[ch] -= 1.0f;

                float channelSample = 0.0f;
                switch (waveType[ch]) {
                    case pixelroot32::audio::WaveType::PULSE:
                        channelSample = (phase[ch] < 0.5f) ? 1.0f : -1.0f;
                        break;
                    case pixelroot32::audio::WaveType::TRIANGLE:
                        channelSample = (phase[ch] < 0.5f) ? 
                            (phase[ch] * 4.0f - 1.0f) : 
                            (3.0f - phase[ch] * 4.0f);
                        break;
                    // ... other wave types
                }

                sample += channelSample * volume[ch];
            }
        }

        // Clamp and convert to int16_t
        if (sample > 1.0f) sample = 1.0f;
        if (sample < -1.0f) sample = -1.0f;
        return static_cast<int16_t>(sample * 32767.0f);
    }

    void setChannelWave(int ch, pixelroot32::audio::WaveType type, 
                       float freq, float duty) override {
        if (ch >= 0 && ch < 4) {
            waveType[ch] = type;
            frequency[ch] = freq;
        }
    }

    void setChannelVolume(int ch, float vol) override {
        if (ch >= 0 && ch < 4) {
            volume[ch] = vol;
        }
    }

    void stopChannel(int ch) override {
        if (ch >= 0 && ch < 4) {
            frequency[ch] = 0.0f;
            volume[ch] = 0.0f;
        }
    }
};

Extending Existing Systems

Custom Entity Types

Create specialized entity types:

class PowerUpActor : public pixelroot32::core::Actor {
private:
    PowerUpType type;
    float lifetime = 5.0f; // 5 seconds

public:
    PowerUpActor(float x, float y, PowerUpType t)
        : Actor(x, y, 8, 8), type(t) {
        setRenderLayer(1);
        setCollisionLayer(Layers::POWERUP);
        setCollisionMask(Layers::PLAYER);
    }

    void update(unsigned long deltaTime) override {
        lifetime -= deltaTime * 0.001f;
        if (lifetime <= 0) {
            isEnabled = false;
            isVisible = false;
        }

        // Animate (bob up and down)
        y += sin(millis() * 0.005f) * 0.5f;
    }

    void onCollision(pixelroot32::core::Actor* other) override {
        if (other->isInLayer(Layers::PLAYER)) {
            applyPowerUp(other);
            isEnabled = false;
            isVisible = false;
        }
    }

private:
    void applyPowerUp(pixelroot32::core::Actor* player) {
        switch (type) {
            case PowerUpType::SPEED:
                // Increase player speed
                break;
            case PowerUpType::HEALTH:
                // Restore health
                break;
            // ...
        }
    }
};

Custom UI Layouts

Create new layout types:

#include <graphics/ui/UILayout.h>

class UICircularLayout : public pixelroot32::graphics::ui::UILayout {
private:
    float radius;
    float startAngle;

public:
    UICircularLayout(float x, float y, float w, float h, float r)
        : UILayout(x, y, w, h), radius(r), startAngle(0.0f) {
    }

    void updateLayout() override {
        int count = elements.size();
        float angleStep = 360.0f / count;

        for (size_t i = 0; i < elements.size(); i++) {
            float angle = startAngle + (i * angleStep);
            float rad = angle * M_PI / 180.0f;

            float elementX = x + (radius * cos(rad)) - (elements[i]->width / 2);
            float elementY = y + (radius * sin(rad)) - (elements[i]->height / 2);

            elements[i]->x = elementX;
            elements[i]->y = elementY;
        }
    }

    void handleInput(const pixelroot32::input::InputManager& input) override {
        // Implement circular navigation
        // ...
    }
};

Custom Collision Primitives

Extend collision system with new shapes:

// Add to your game code (not engine modification)
struct Triangle {
    float x1, y1, x2, y2, x3, y3;
};

bool intersects(const Triangle& tri, const pixelroot32::core::Rect& rect) {
    // Implement triangle-rectangle intersection
    // ...
    return false;
}

bool intersects(const Triangle& tri1, const Triangle& tri2) {
    // Implement triangle-triangle intersection
    // ...
    return false;
}

Best Practices

Maintain Compatibility

  • Don't break existing APIs: Extend, don't modify
  • Use inheritance: Inherit from base classes
  • Follow patterns: Match existing code patterns
  • Document extensions: Comment your custom code

Testing

  • Test on both platforms: ESP32 and Native
  • Test edge cases: Boundary conditions, null pointers
  • Performance testing: Ensure extensions don't hurt performance
  • Memory testing: Check for leaks with custom code

Documentation

  • Comment your code: Explain why, not just what
  • Provide examples: Show how to use your extensions
  • Document limitations: State what doesn't work
  • Version compatibility: Note which engine version

Common Extension Patterns

Factory Pattern

#include <memory>

class EntityFactory {
public:
    static std::unique_ptr<pixelroot32::core::Entity> createEnemy(EnemyType type, float x, float y) {
        switch (type) {
            case EnemyType::BASIC:
                return std::make_unique<BasicEnemy>(x, y);
            case EnemyType::FAST:
                return std::make_unique<FastEnemy>(x, y);
            case EnemyType::TANK:
                return std::make_unique<TankEnemy>(x, y);
            default:
                return nullptr;
        }
    }

    static std::unique_ptr<pixelroot32::core::Entity> createPowerUp(PowerUpType type, float x, float y) {
        return std::make_unique<PowerUpActor>(x, y, type);
    }
};

Strategy Pattern

class MovementStrategy {
public:
    virtual void update(pixelroot32::core::Actor* actor, unsigned long deltaTime) = 0;
};

class LinearMovement : public MovementStrategy {
public:
    void update(pixelroot32::core::Actor* actor, unsigned long deltaTime) override {
        actor->x += speed * (deltaTime * 0.001f);
    }
};

class CircularMovement : public MovementStrategy {
public:
    void update(pixelroot32::core::Actor* actor, unsigned long deltaTime) override {
        float angle = millis() * 0.001f;
        actor->x = centerX + radius * cos(angle);
        actor->y = centerY + radius * sin(angle);
    }
};

class SmartEnemy : public pixelroot32::core::Actor {
private:
    MovementStrategy* movement;

public:
    void setMovement(MovementStrategy* strat) {
        movement = strat;
    }

    void update(unsigned long deltaTime) override {
        if (movement) {
            movement->update(this, deltaTime);
        }
    }
};

Troubleshooting

Driver Not Working

  • Verify all interface methods are implemented
  • Check initialization order
  • Test with simple drawing first
  • Verify hardware connections

Audio Backend Issues

  • Check sample rate matches hardware
  • Verify audio generation logic
  • Test with simple tones first
  • Check channel management

Extension Conflicts

  • Ensure namespace isolation
  • Avoid modifying engine code directly
  • Use composition over modification
  • Test with engine updates

Next Steps

Now that you understand extensibility, you've completed the optimization section. Continue with: - API Reference - Complete API documentation - Examples - Code examples - Resources - Tools and troubleshooting


See also: - API Reference - DrawSurface - API Reference - AudioBackend - Manual - Platforms and Drivers