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¶
- Rendering: Too many draw calls
- Collision detection: Too many collision checks
- Memory allocation: Dynamic allocation in game loop
- Complex calculations: Expensive math operations
- 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