Skip to content

Memory Management

ESP32 has limited memory, so efficient memory management is crucial for PixelRoot32 games. This guide covers memory constraints, object pooling, and best practices.

ESP32 Memory Constraints

Available Memory

ESP32 typically has: - RAM: ~320KB total (varies by model) - Flash: 4MB+ (for program storage) - Heap: Limited and fragmented over time

Real-World Limits

  • MAX_ENTITIES: 32 per scene (hard limit)
  • Sprite data: Stored in flash (const/constexpr)
  • Dynamic allocation: Should be avoided in game loop
  • Stack: Limited (~8KB), avoid large stack allocations

Object Pooling

Object pooling reuses objects instead of creating/destroying them, avoiding memory fragmentation.

Basic Pool Pattern

class ProjectilePool {
private:
    static const int POOL_SIZE = 10;
    ProjectileActor pool[POOL_SIZE];
    bool inUse[POOL_SIZE];

public:
    ProjectilePool() {
        for (int i = 0; i < POOL_SIZE; i++) {
            inUse[i] = false;
        }
    }

    ProjectileActor* getAvailable() {
        for (int i = 0; i < POOL_SIZE; i++) {
            if (!inUse[i]) {
                inUse[i] = true;
                return &pool[i];
            }
        }
        return nullptr; // Pool exhausted
    }

    void release(ProjectileActor* projectile) {
        for (int i = 0; i < POOL_SIZE; i++) {
            if (&pool[i] == projectile) {
                inUse[i] = false;
                projectile->isEnabled = false;
                projectile->isVisible = false;
                break;
            }
        }
    }
};

Using Object Pools

class GameScene : public pixelroot32::core::Scene {
private:
    ProjectilePool projectilePool;

public:
    void update(unsigned long deltaTime) override {
        Scene::update(deltaTime);

        // Fire projectile
        if (input.isButtonPressed(Buttons::A)) {
            ProjectileActor* proj = projectilePool.getAvailable();
            if (proj) {
                proj->x = player->x;
                proj->y = player->y;
                proj->isEnabled = true;
                proj->isVisible = true;
                // ... initialize projectile
            }
        }

        // Clean up projectiles that hit target
        for (auto* entity : entities) {
            if (auto* proj = dynamic_cast<ProjectileActor*>(entity)) {
                if (proj->hitTarget) {
                    projectilePool.release(proj);
                }
            }
        }
    }
};

Complete Example: Entity Pool

template<typename T, int POOL_SIZE>
class EntityPool {
private:
    T pool[POOL_SIZE];
    bool inUse[POOL_SIZE];
    int activeCount = 0;

public:
    EntityPool() {
        for (int i = 0; i < POOL_SIZE; i++) {
            inUse[i] = false;
        }
    }

    T* acquire() {
        if (activeCount >= POOL_SIZE) {
            return nullptr; // Pool full
        }

        for (int i = 0; i < POOL_SIZE; i++) {
            if (!inUse[i]) {
                inUse[i] = true;
                activeCount++;
                return &pool[i];
            }
        }
        return nullptr;
    }

    void release(T* obj) {
        for (int i = 0; i < POOL_SIZE; i++) {
            if (&pool[i] == obj) {
                inUse[i] = false;
                activeCount--;
                obj->isEnabled = false;
                obj->isVisible = false;
                break;
            }
        }
    }

    int getActiveCount() const { return activeCount; }
    int getAvailableCount() const { return POOL_SIZE - activeCount; }
};

// Usage
EntityPool<EnemyActor, 8> enemyPool;
EntityPool<ParticleEmitter, 5> particlePool;

Scene Arena (Experimental)

Scene Arena provides a memory arena for scene-specific allocations, reducing fragmentation.

What is Scene Arena?

Scene Arena is a contiguous memory block pre-allocated for a scene. All scene entities are allocated from this arena instead of the heap.

When to Use

  • Large scenes: Scenes with many entities
  • Frequent allocation: Scenes that create/destroy entities often
  • Memory fragmentation: When heap fragmentation is a problem
  • Performance: When you need predictable allocation performance

Configuration

#ifdef PIXELROOT32_ENABLE_SCENE_ARENA
#include <core/Scene.h>

// Define arena buffer (typically in scene header)
static unsigned char MY_SCENE_ARENA_BUFFER[8192]; // 8KB arena

class MyScene : public pixelroot32::core::Scene {
public:
    void init() override {
        // Initialize arena
        arena.init(MY_SCENE_ARENA_BUFFER, sizeof(MY_SCENE_ARENA_BUFFER));

        // Now entities allocated with arena will use this memory
        // (Requires custom allocation functions)
    }
};
#endif

Limitations

  • Experimental: May have bugs or limitations
  • Fixed size: Arena size must be determined at compile time
  • No reallocation: Can't resize arena at runtime
  • Manual management: Requires careful memory management

Note: Scene Arena is an experimental feature. Use object pooling for most cases.

Best Practices

Avoid Dynamic Allocation in Game Loop

// ❌ BAD: Allocates every frame
void update(unsigned long deltaTime) override {
    if (shouldSpawnEnemy) {
        EnemyActor* enemy = new EnemyActor(x, y);
        addEntity(enemy);
    }
}

// ✅ GOOD: Use pool
void update(unsigned long deltaTime) override {
    if (shouldSpawnEnemy) {
        EnemyActor* enemy = enemyPool.getAvailable();
        if (enemy) {
            enemy->reset(x, y);
            enemy->isEnabled = true;
        }
    }
}

Pre-allocate Resources

class GameScene : public pixelroot32::core::Scene {
private:
    // Pre-allocated pools
    ProjectilePool projectiles;
    EnemyPool enemies;
    ParticlePool particles;

public:
    void init() override {
        // All pools created in constructor
        // No allocation in init() or update()
    }
};

Reuse Objects

class EnemyActor : public pixelroot32::core::Actor {
public:
    void reset(float x, float y) {
        this->x = x;
        this->y = y;
        this->isEnabled = true;
        this->isVisible = true;
        this->health = maxHealth;
        // Reset all state
    }

    void deactivate() {
        isEnabled = false;
        isVisible = false;
    }
};

// Usage
EnemyActor* enemy = enemyPool.getAvailable();
if (enemy) {
    enemy->reset(spawnX, spawnY);
    addEntity(enemy);
}

Avoid Strings and Dynamic Memory

// ❌ BAD: String allocation
void draw(Renderer& renderer) override {
    std::string scoreText = "Score: " + std::to_string(score);
    renderer.drawText(scoreText.c_str(), 10, 10, Color::White, 1);
}

// ✅ GOOD: Static buffer
void draw(Renderer& renderer) override {
    char scoreBuffer[32];
    snprintf(scoreBuffer, sizeof(scoreBuffer), "Score: %d", score);
    renderer.drawText(scoreBuffer, 10, 10, Color::White, 1);
}

Store Data in Flash

// ✅ GOOD: Stored in flash (const/constexpr)
static const uint16_t SPRITE_DATA[] = {
    0b00111100,
    0b01111110,
    // ...
};

// ❌ BAD: Stored in RAM
uint16_t spriteData[] = {
    0b00111100,
    0b01111110,
    // ...
};

Memory Monitoring

Check Available Memory

#ifdef PLATFORM_ESP32
#include <Arduino.h>

void checkMemory() {
    Serial.print("Free heap: ");
    Serial.println(ESP.getFreeHeap());
    Serial.print("Largest free block: ");
    Serial.println(ESP.getMaxAllocHeap());
}
#endif

Monitor Entity Count

void update(unsigned long deltaTime) override {
    Scene::update(deltaTime);

    // Check entity count
    int entityCount = getEntityCount();
    if (entityCount >= MAX_ENTITIES) {
        Serial.println("WARNING: Entity limit reached!");
    }
}

Common Patterns

Entity Lifecycle Management

class ManagedEntity {
private:
    bool isActive = false;

public:
    void activate(float x, float y) {
        this->x = x;
        this->y = y;
        isActive = true;
        isEnabled = true;
        isVisible = true;
    }

    void deactivate() {
        isActive = false;
        isEnabled = false;
        isVisible = false;
    }

    bool getIsActive() const { return isActive; }
};

// Pool manages lifecycle
class EntityManager {
private:
    EntityPool<ManagedEntity, 20> pool;

public:
    ManagedEntity* spawn(float x, float y) {
        auto* entity = pool.acquire();
        if (entity) {
            entity->activate(x, y);
        }
        return entity;
    }

    void despawn(ManagedEntity* entity) {
        if (entity) {
            entity->deactivate();
            pool.release(entity);
        }
    }
};

Memory-Efficient Collections

// Fixed-size array instead of vector
class EntityArray {
private:
    static const int MAX_SIZE = 32;
    pixelroot32::core::Entity* entities[MAX_SIZE];
    int count = 0;

public:
    bool add(pixelroot32::core::Entity* entity) {
        if (count >= MAX_SIZE) return false;
        entities[count++] = entity;
        return true;
    }

    void remove(pixelroot32::core::Entity* entity) {
        for (int i = 0; i < count; i++) {
            if (entities[i] == entity) {
                entities[i] = entities[--count];
                break;
            }
        }
    }

    int size() const { return count; }
    pixelroot32::core::Entity* operator[](int index) { return entities[index]; }
};

Troubleshooting

Out of Memory Errors

  • Reduce pool sizes
  • Use fewer entities
  • Store more data in flash
  • Avoid dynamic allocation
  • Check for memory leaks

Entity Limit Reached

  • MAX_ENTITIES = 32 is a hard limit
  • Use object pooling to reuse entities
  • Deactivate entities instead of removing
  • Combine multiple entities into one

Memory Fragmentation

  • Use object pooling
  • Pre-allocate all resources
  • Avoid frequent new/delete
  • Consider Scene Arena (experimental)

Next Steps

Now that you understand memory management, learn about: - Performance Optimization - Improve game performance - Platforms and Drivers - Understand platform specifics - Extensibility - Extend the engine


See also: - API Reference - Scene - Manual - Scenes and Entities