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