PixelRoot32 Game Engine¶
PixelRoot32 is a lightweight 2D game engine designed for ESP32-based systems.
It focuses on simplicity, deterministic behavior, and low memory usage, making it suitable for embedded environments and small-scale games.
📐 Coding Style Guide¶
PixelRoot32 follows a strict set of conventions to ensure consistency, readability, and long-term maintainability of the engine.
Language¶
- C++17
- Avoid RTTI and exceptions (use
-fno-exceptions) - Prefer deterministic and explicit control flow
Modern C++ Features (C++17)¶
PixelRoot32 embraces C++17 to write safer and more expressive code without sacrificing performance.
- Smart Pointers:
std::unique_ptrfor exclusive ownership. - String Views:
std::string_viewfor non-owning string references (avoidstd::stringcopies). - Optional:
std::optionalfor values that may or may not exist (cleaner than pointer checks or magic values). - Attributes: Use
[[nodiscard]]for functions where the return value must not be ignored (e.g., error codes). - Constexpr: Use
constexprfor compile-time constants andif constexprfor compile-time branching.
Files¶
.hfiles define interfaces and public types.cppfiles contain implementations- Public headers must not contain heavy logic (only trivial inline code if needed)
Includes¶
- User code must include headers only from
include/ - Headers in
include/may include headers fromsrc/ - Source files in
src/must never include headers frominclude/ - Internal headers that are not part of the public API must not be exposed via
include/
Naming Conventions¶
- Classes and structs: PascalCase
- Methods and functions: camelCase
- Variables and members: camelCase
- No Hungarian notation
- No
m_or_prefixes for members
Order inside classes¶
- Public members first
- Protected members second
- Private members last
🧩 Namespace Design¶
PixelRoot32 uses namespaces to clearly separate public API from internal implementation details.
Root Namespace¶
All engine symbols live under the root namespace:
pixelroot32
Public Namespaces (API)¶
These namespaces are considered part of the stable public API and may be used directly by game projects:
pixelroot32::corepixelroot32::graphicspixelroot32::graphics::uipixelroot32::inputpixelroot32::physicspixelroot32::mathpixelroot32::drivers
Example usage in a game project:
Internal Namespaces (Non-API)¶
The following namespaces are intended for internal engine use only and are not part of the stable public API:
pixelroot32::platformpixelroot32::platform::mockpixelroot32::platform::esp32pixelroot32::internalpixelroot32::detail
Rules for internal namespaces:
- They may change without notice
- They must not be included directly by user projects
- They must not be exposed through headers in
include/
Namespace Usage Rules¶
- Public headers must not use
using namespace - Public headers must always reference fully-qualified names
- In internal implementation files (
.cpp), namespace aliases are preferred
Recommended internal alias:
namespace pr32 = pixelroot32;
The use of using namespace pixelroot32::... is discouraged even internally, except in very small, localized implementation files.
📦 Library Usage Expectations¶
- Users are expected to include headers only from
include/ - Users should reference engine types via fully-qualified namespaces
- The engine does not pollute the global namespace
🚀 Best Practices & Optimization¶
These guidelines are derived from practical implementation in examples/GeometryJump, examples/BrickBreaker, examples/Pong, and the side-scrolling platformer prototype used in the camera demo.
💾 Memory & Resources¶
- Smart Pointers (C++17): Prefer
std::unique_ptrfor owning objects (like Scenes, Actors, UI elements) to automate memory management and document ownership. - Use
std::make_unique<T>(...)to create objects. - Pass raw pointers (via
.get()) to functions that do not take ownership (likeaddEntity). - Use
std::moveonly when transferring ownership explicitly. - Object Pooling: Pre-allocate all game objects (obstacles, particles, enemies) during
init(). - Pattern: Use fixed-size arrays (e.g.,
Particle particles[50]) and flags (isActive) instead ofstd::vectorwithpush_back/erase. - Trade-off: Eliminates runtime allocations and fragmentation at the cost of a slightly higher fixed RAM footprint; dimension pools to realistic worst-case usage.
- Zero Runtime Allocation: Never use
newormallocinside the game loop (updateordraw). -
String Handling: Avoid
std::stringcopies. Usestd::string_viewfor passing strings. For formatting, usesnprintfwith stack-allocatedcharbuffers. -
Scene Arenas (
PIXELROOT32_ENABLE_SCENE_ARENA): - Use a single pre-allocated buffer per scene for temporary entities or scratch data when you need strict zero-allocation guarantees.
- Trade-off: Very cache-friendly and fragmentation-proof, but the buffer cannot grow at runtime; oversizing wastes RAM, undersizing returns
nullptrand requires graceful fallback logic.
Recommended Pooling Patterns (ESP32)¶
- High-rotation entities (bullets, snake segments, particles):
- Create all instances once in
init()or in an initialresetGame(). - Keep a usage flag (for example
isActive) or a separate container that represents the active subset. - Reactivate entities with a
reset(...)method that configures position/state without allocating memory again. - Avoid calling
deleteinside the game loop; deactivate and recycle entities instead. - Engine examples:
- Space Invaders projectiles: fixed-size bullet pool reused via
reset(...). - Snake segments: segment pool reused for growth without
newduring gameplay.
⚡ Performance (ESP32 Focus)¶
- Inlining:
- Define trivial accessors (e.g.,
getHitBox,getX) in the header (.h) to allow compiler inlining. - Keep heavy implementation logic in
.cpp. - Fast Randomness:
std::rand()is slow and uses division. Usemath::randomScalar()ormath::randomRange()(which use optimized Xorshift algorithms compatible withFixed16) for visual effects. - Collision Detection:
- Use simple AABB (Axis-Aligned Bounding Box) checks first. Use Collision Layers (
GameLayers.h) to avoid checking unnecessary pairs. - For very fast projectiles (bullets, lasers), prefer lightweight sweep tests:
- Represent the projectile as a small
physics::Circleand callphysics::sweepCircleVsRect(startCircle, endCircle, targetRect, tHit)against potential targets. - Use sweep tests only for the few entities that need them; keep everything else on basic AABB to avoid unnecessary CPU cost.
- Represent the projectile as a small
🏗️ Code Architecture¶
- Tuning Constants: Extract gameplay values (gravity, speed, dimensions) into a dedicated
GameConstants.h. This allows designers to tweak the game without touching logic code. - State Management: Implement a
reset()method for Actors to reuse them after "Game Over", rather than destroying and recreating the scene. - Component Pattern: Inherit from
PhysicsActorfor moving objects andActorfor static ones.
🎮 Game Feel & Logic¶
- Frame-Rate Independence: Always multiply movement by
deltaTime. - Example:
x += speed * math::toScalar(deltaTime * 0.001f); - Logic/Visual Decoupling: For infinite runners, keep logic progression (obstacle spacing) constant in time, even if visual speed increases.
- Snappy Controls: For fast-paced games, prefer higher gravity and jump forces to reduce "floatiness".
- Slopes & Ramps on Tilemaps: When implementing ramps on a tilemap, treat contiguous ramp tiles as a single logical slope and compute the surface height using linear interpolation over world X instead of resolving per tile. Keep gravity and jump parameters identical between flat ground and ramps so jump timing remains consistent.
🧮 Math & Fixed-Point Guidelines¶
The engine uses a Math Policy Layer to support both FPU (Float) and non-FPU (Fixed-Point) hardware seamlessly.
- Use
Scalareverywhere: Never usefloatordoubleexplicitly in game logic, physics, or positioning. Usepixelroot32::math::Scalar. - Literals: Use
math::toScalar(0.5f)for floating-point literals. This ensures they are correctly converted toFixed16on integer-only platforms.- Bad:
Scalar speed = 2.5;(Implicit double conversion, slow/error-prone on Fixed16) - Good:
Scalar speed = math::toScalar(2.5f);
- Bad:
- Renderer Conversion: The
Rendererworks with pixels (int). Keep positions asScalarlogic-side and convert tointonly when calling draw methods.- Example:
renderer.drawSprite(spr, static_cast<int>(x), static_cast<int>(y), ...)
- Example:
- Audio Independence: The audio subsystem is optimized separately and does not use
Scalar. It continues to use its own internal formats (integer mixing).
🎨 Sprite & Graphics Guidelines¶
- 1bpp Sprites: Define sprite bitmaps as
static const uint16_tarrays, one row per element. Use bit0as the leftmost pixel and bit (width - 1) as the rightmost pixel.
📐 UI Layout Guidelines¶
- Use Layouts for Automatic Organization: Prefer
UIVerticalLayout(for vertical lists),UIHorizontalLayout(for horizontal menus/bars), orUIGridLayout(for matrix layouts like inventories) over manual position calculations when organizing multiple UI elements. This simplifies code and enables automatic navigation. - Use Padding Container for Spacing: Use
UIPaddingContainerto add padding around individual elements or to nest layouts with custom spacing. This is more efficient than manually calculating positions and allows for flexible UI composition. - Use Panel for Visual Containers: Use
UIPanelto create retro-style windows, dialogs, and menus with background and border. Panels typically contain layouts (Vertical, Horizontal, or Grid) which then contain buttons and labels. Ideal for Game & Watch style interfaces. - Use Anchor Layout for HUDs: Use
UIAnchorLayoutto position HUD elements (score, lives, health bars) at fixed screen positions without manual calculations. Supports 9 anchor points (corners, center, edges). Very efficient on ESP32 as it has no reflow - positions are calculated once or when screen size changes. - Performance on ESP32: Layouts use viewport culling and optimized clearing (only when scroll changes) to minimize rendering overhead. The layout system is designed to be efficient on embedded hardware.
- Scroll Behavior: Vertical and horizontal layouts use NES-style instant scroll on selection change for responsive navigation. Smooth scrolling is available for manual scrolling scenarios.
- Navigation:
UIVerticalLayouthandles UP/DOWN navigation,UIHorizontalLayouthandles LEFT/RIGHT navigation, andUIGridLayouthandles 4-direction navigation (UP/DOWN/LEFT/RIGHT) with wrapping. All layouts support automatic selection management and button styling. - Grid Layout:
UIGridLayoutautomatically calculates cell dimensions based on layout size, padding, and spacing. Elements are centered within cells if they're smaller than the cell size. Ideal for inventories, level selection screens, and item galleries. - Sprite Descriptors: Wrap raw bitmaps in
pixelroot32::graphics::SpriteorMultiSpritedescriptors and pass them toRenderer::drawSprite/Renderer::drawMultiSprite. - No Bit Logic in Actors: Actors should never iterate bits or draw individual pixels. They only select the appropriate sprite (or layered sprite) and call the renderer.
- Layered Sprites First: Prefer composing multi-color sprites from multiple 1bpp
SpriteLayerentries. Keep layer datastatic constto allow storage in flash and preserve the 1bpp-friendly pipeline. - Optional 2bpp/4bpp Sprites: For higher fidelity assets, you can enable packed 2bpp/4bpp formats via compile-time flags (for example
PIXELROOT32_ENABLE_2BPP_SPRITES/PIXELROOT32_ENABLE_4BPP_SPRITES). Treat these as advanced options: they improve visual richness (better shading, logos, UI) at the cost of 2x/4x sprite memory and higher fill-rate. Use them sparingly on ESP32 and keep gameplay-critical sprites on the 1bpp path. - Integer-Only Rendering: Sprite rendering must remain integer-only and avoid dynamic allocations to stay friendly to ESP32 constraints.
🧱 Render Layers & Tilemaps¶
- Render Layers:
- Use
Entity::renderLayerto separate concerns:0– background (tilemaps, solid fills, court outlines).1– gameplay actors (player, enemies, bullets, snake segments, ball/paddles).2– UI (labels, menus, score text).
- Scenes draw entities by iterating these layers in ascending order. Higher layers naturally appear on top.
- Background Entities:
- Prefer lightweight background entities in layer
0(for example, starfields or playfield outlines) instead of redrawing background logic inside every scenedraw(). - Tilemaps:
- For grid-like backgrounds, use the
TileMaphelper with 1bppSpritetiles andRenderer::drawTileMap. - Keep tile indices in a compact
uint8_tarray and reuse tiles across the map to minimize RAM and flash usage on ESP32. - Trade-off: Greatly reduces background RAM compared to full bitmaps, but adds a predictable per-tile draw cost; avoid unnecessarily large maps or resolutions on ESP32.
- For side-scrolling platformers, combine tilemaps with
Camera2DandRenderer::setDisplayOffsetinstead of manually offsetting individual actors. Keep camera logic centralized (for example in aScene-level camera object) and use different parallax factors per layer to achieve multi-layer scrolling without additional allocations.
PixelRoot32 Game Engine aims to remain simple, explicit, and predictable, prioritizing clarity over abstraction and control over convenience.