Skip to content

Performance Optimization

This guide covers techniques to improve game performance on ESP32, including rendering optimization, logic optimization, and profiling.

ESP32 Performance Characteristics

CPU Limitations

  • Dual-core: 240MHz (typically)
  • Single-threaded game loop: One core handles everything
  • Target FPS: 30-60 FPS (depends on game complexity)
  • Frame budget: ~16-33ms per frame at 60 FPS

Common Bottlenecks

  1. Rendering: Too many draw calls
  2. Collision detection: Too many collision checks
  3. Memory allocation: Dynamic allocation in game loop
  4. Complex calculations: Expensive math operations
  5. String operations: String concatenation/formatting

Rendering Optimization

Viewport Culling

Only draw entities that are visible on screen:

bool isOnScreen(float x, float y, int width, int height, 
                const Camera2D& camera) {
    float cameraX = camera.getX();
    float cameraY = camera.getY();
    int screenWidth = engine.getRenderer().getWidth();
    int screenHeight = engine.getRenderer().getHeight();

    return !(x + width < cameraX || 
             x > cameraX + screenWidth ||
             y + height < cameraY || 
             y > cameraY + screenHeight);
}

void draw(pixelroot32::graphics::Renderer& renderer) override {
    camera.apply(renderer);

    // Only draw visible entities
    for (auto* entity : entities) {
        if (entity->isVisible && 
            isOnScreen(entity->x, entity->y, entity->width, entity->height, camera)) {
            entity->draw(renderer);
        }
    }
}

Reduce Draw Calls

Batch similar operations:

// ❌ BAD: Many individual draw calls
void drawBackground(Renderer& renderer) {
    for (int y = 0; y < 30; y++) {
        for (int x = 0; x < 30; x++) {
            renderer.drawSprite(tile, x * 8, y * 8, Color::White);
        }
    }
}

// ✅ GOOD: Use tilemap (single call)
void drawBackground(Renderer& renderer) {
    renderer.drawTileMap(backgroundTileMap, 0, 0, Color::White);
}

Optimize Sprite Drawing

  • Reuse sprites: Define once, use many times
  • Use 1bpp: Most efficient format
  • Limit sprite size: Smaller sprites = faster drawing
  • Avoid flipping: Flipping has overhead

Efficient Render Layers

// Organize by layer to minimize layer switches
void draw(pixelroot32::graphics::Renderer& renderer) override {
    // Draw all layer 0 entities
    for (auto* entity : layer0Entities) {
        if (entity->isVisible) entity->draw(renderer);
    }

    // Draw all layer 1 entities
    for (auto* entity : layer1Entities) {
        if (entity->isVisible) entity->draw(renderer);
    }

    // Draw all layer 2 entities
    for (auto* entity : layer2Entities) {
        if (entity->isVisible) entity->draw(renderer);
    }
}

Logic Optimization

Reduce Calculations Per Frame

Cache expensive calculations:

class OptimizedActor : public pixelroot32::core::Actor {
private:
    float cachedDistance = 0.0f;
    bool distanceDirty = true;

public:
    void update(unsigned long deltaTime) override {
        // Only recalculate when position changes
        if (distanceDirty) {
            cachedDistance = sqrt(x * x + y * y);
            distanceDirty = false;
        }

        // Use cached value
        if (cachedDistance > maxDistance) {
            // ...
        }
    }

    void setPosition(float newX, float newY) {
        x = newX;
        y = newY;
        distanceDirty = true; // Mark for recalculation
    }
};

Lazy Evaluation

Only calculate when needed:

class LazyCalculator {
private:
    mutable bool cached = false;
    mutable float cachedValue = 0.0f;

public:
    float getValue() const {
        if (!cached) {
            cachedValue = expensiveCalculation();
            cached = true;
        }
        return cachedValue;
    }

    void invalidate() {
        cached = false;
    }
};

Avoid Expensive Operations

// ❌ BAD: sqrt() every frame
float distance = sqrt(dx * dx + dy * dy);

// ✅ GOOD: Use squared distance
float distanceSq = dx * dx + dy * dy;
if (distanceSq > maxDistanceSq) {
    // ...
}

// ❌ BAD: sin/cos every frame
float x = cos(angle) * radius;
float y = sin(angle) * radius;

// ✅ GOOD: Pre-calculate or use lookup table
static const float COS_TABLE[360] = { /* ... */ };
static const float SIN_TABLE[360] = { /* ... */ };
float x = COS_TABLE[static_cast<int>(angle) % 360] * radius;

Collision Optimization

Use Collision Layers Efficiently

// ❌ BAD: Check everything against everything
for (auto* actor1 : actors) {
    for (auto* actor2 : actors) {
        if (actor1 != actor2) {
            checkCollision(actor1, actor2);
        }
    }
}

// ✅ GOOD: Use layers to reduce checks
// CollisionSystem automatically uses layers
// Only actors with matching layers/masks are checked

Reduce Collision Checks

// Only check collisions for active actors
void update(unsigned long deltaTime) override {
    for (auto* actor : actors) {
        if (actor->isEnabled && actor->isActive) {
            actor->update(deltaTime);
        }
    }

    // CollisionSystem only checks enabled actors
    Scene::update(deltaTime);
}

Simple Hitboxes

// ✅ GOOD: Simple AABB
pixelroot32::core::Rect getHitBox() override {
    return {x, y, width, height};
}

// ❌ BAD: Complex shape calculations
pixelroot32::core::Rect getHitBox() override {
    // Complex polygon calculations...
}

String Optimization

Avoid String Operations

// ❌ BAD: String concatenation
std::string text = "Score: " + std::to_string(score);

// ✅ GOOD: Static buffer with snprintf
char buffer[32];
snprintf(buffer, sizeof(buffer), "Score: %d", score);
renderer.drawText(buffer, 10, 10, Color::White, 1);

Cache Text Rendering

class CachedText {
private:
    char buffer[32];
    bool dirty = true;
    int lastValue = -1;

public:
    void update(int value) {
        if (value != lastValue) {
            snprintf(buffer, sizeof(buffer), "Score: %d", value);
            lastValue = value;
            dirty = true;
        }
    }

    void draw(Renderer& renderer, int x, int y) {
        if (dirty) {
            renderer.drawText(buffer, x, y, Color::White, 1);
            dirty = false;
        }
    }
};

Profiling

Measure Frame Time

class PerformanceMonitor {
private:
    unsigned long frameTime = 0;
    unsigned long maxFrameTime = 0;
    unsigned long frameCount = 0;

public:
    void startFrame() {
        frameTime = millis();
    }

    void endFrame() {
        unsigned long elapsed = millis() - frameTime;
        if (elapsed > maxFrameTime) {
            maxFrameTime = elapsed;
        }
        frameCount++;

        // Log every 60 frames
        if (frameCount % 60 == 0) {
            Serial.print("Max frame time: ");
            Serial.println(maxFrameTime);
            maxFrameTime = 0;
        }
    }
};

// Usage
PerformanceMonitor perf;

void update(unsigned long deltaTime) override {
    perf.startFrame();
    Scene::update(deltaTime);
    perf.endFrame();
}

Identify Bottlenecks

#ifdef PLATFORM_ESP32
#include <Arduino.h>

class Profiler {
private:
    unsigned long updateTime = 0;
    unsigned long drawTime = 0;
    unsigned long collisionTime = 0;

public:
    void startUpdate() {
        updateTime = micros();
    }

    void endUpdate() {
        updateTime = micros() - updateTime;
    }

    void startDraw() {
        drawTime = micros();
    }

    void endDraw() {
        drawTime = micros() - drawTime;
    }

    void log() {
        Serial.print("Update: ");
        Serial.print(updateTime);
        Serial.print("us, Draw: ");
        Serial.print(drawTime);
        Serial.println("us");
    }
};
#endif

Best Practices Summary

Rendering

  • ✅ Use viewport culling
  • ✅ Batch similar draw operations
  • ✅ Use tilemaps for backgrounds
  • ✅ Limit sprite count and size
  • ✅ Organize by render layers

Logic

  • ✅ Cache expensive calculations
  • ✅ Use lazy evaluation
  • ✅ Avoid sqrt/sin/cos in loops
  • ✅ Pre-calculate lookup tables
  • ✅ Reduce update frequency for non-critical entities

Memory

  • ✅ Use object pooling
  • ✅ Avoid dynamic allocation
  • ✅ Store data in flash
  • ✅ Reuse objects

Collisions

  • ✅ Use collision layers efficiently
  • ✅ Only check active entities
  • ✅ Use simple hitboxes
  • ✅ Limit active collision pairs

Strings

  • ✅ Use static buffers
  • ✅ Cache text rendering
  • ✅ Avoid string operations in loops

Common Optimization Patterns

Update Frequency Reduction

class LowFrequencyUpdater {
private:
    unsigned long timer = 0;
    unsigned long interval = 100; // Update every 100ms

public:
    void update(unsigned long deltaTime) {
        timer += deltaTime;
        if (timer >= interval) {
            timer -= interval;
            // Do expensive update
            expensiveUpdate();
        }
    }
};

Spatial Partitioning (Simple)

// Divide screen into zones
class SpatialGrid {
private:
    static const int GRID_SIZE = 4;
    static const int CELL_WIDTH = 60;
    static const int CELL_HEIGHT = 60;

    std::vector<Actor*> grid[GRID_SIZE][GRID_SIZE];

public:
    void add(Actor* actor) {
        int cellX = static_cast<int>(actor->x) / CELL_WIDTH;
        int cellY = static_cast<int>(actor->y) / CELL_HEIGHT;
        if (cellX >= 0 && cellX < GRID_SIZE && 
            cellY >= 0 && cellY < GRID_SIZE) {
            grid[cellY][cellX].push_back(actor);
        }
    }

    void checkCollisions() {
        // Only check collisions within same cell
        for (int y = 0; y < GRID_SIZE; y++) {
            for (int x = 0; x < GRID_SIZE; x++) {
                auto& cell = grid[y][x];
                for (size_t i = 0; i < cell.size(); i++) {
                    for (size_t j = i + 1; j < cell.size(); j++) {
                        checkCollision(cell[i], cell[j]);
                    }
                }
            }
        }
    }
};

Troubleshooting

Low FPS

  • Profile to find bottlenecks
  • Reduce entity count
  • Optimize rendering (culling, batching)
  • Simplify collision detection
  • Reduce update frequency

Frame Drops

  • Check for expensive operations in update()
  • Avoid dynamic allocation
  • Cache calculations
  • Reduce draw calls

Stuttering

  • Ensure frame-rate independence (use deltaTime)
  • Avoid blocking operations
  • Pre-load resources
  • Use object pooling

Next Steps

Now that you understand performance optimization, learn about: - Memory Management - Manage memory efficiently - Platforms and Drivers - Platform-specific optimizations - Extensibility - Extend the engine


See also: - Manual - Basic Rendering - Manual - Physics and Collisions