Entities & scene tutorial
Not an
examples/project — This page is a didactic walkthrough (bouncing entities,Entitysubclass, scene wiring). There is no matching folder underexamples/with this code. For a real, minimal PlatformIO project, start withhello_worldand its README.
The snippets below illustrate fundamentals: scene lifecycle, custom Entity subclasses, input, and drawing — useful once you have already opened hello_world in the repo.
Complete Source
cpp
#include <Arduino.h>
#include <Engine.h>
#include <Scene.h>
#include <Renderer.h>
#include <Entity.h>
#include <InputManager.h>
using namespace pixelroot32;
// ============================================================================
// Custom Entity: A bouncing ball
// ============================================================================
class Ball : public core::Entity {
math::Scalar velocityX;
math::Scalar velocityY;
graphics::Color color;
int screenWidth;
int screenHeight;
public:
Ball(int x, int y, int screenW, int screenH)
: Entity(math::toScalar(x), math::toScalar(y), 8, 8, core::EntityType::GENERIC),
screenWidth(screenW),
screenHeight(screenH),
color(graphics::Color::RED) {
// Random velocity using math::Scalar
velocityX = math::toScalar(random(50, 150));
velocityY = math::toScalar(random(50, 150));
if (random(2) == 0) velocityX = -velocityX;
if (random(2) == 0) velocityY = -velocityY;
}
void update(unsigned long deltaTime) override {
using namespace math;
// Convert deltaTime to seconds as Scalar
Scalar dt = toScalar(deltaTime) / toScalar(1000);
// Update position
position.x += velocityX * dt;
position.y += velocityY * dt;
// Bounce off walls
if (position.x <= toScalar(0) ||
position.x >= toScalar(screenWidth - width)) {
velocityX = -velocityX;
color = static_cast<graphics::Color>(random(1, 16));
}
if (position.y <= toScalar(0) ||
position.y >= toScalar(screenHeight - height)) {
velocityY = -velocityY;
color = static_cast<graphics::Color>(random(1, 16));
}
// Clamp to screen
if (position.x < toScalar(0)) position.x = toScalar(0);
if (position.y < toScalar(0)) position.y = toScalar(0);
if (position.x > toScalar(screenWidth - width))
position.x = toScalar(screenWidth - width);
if (position.y > toScalar(screenHeight - height))
position.y = toScalar(screenHeight - height);
}
void draw(graphics::Renderer& r) override {
r.drawFilledRectangle(
static_cast<int>(position.x),
static_cast<int>(position.y),
width,
height,
color
);
}
};
// ============================================================================
// Game Scene
// ============================================================================
class BouncingBallsScene : public core::Scene {
static constexpr int NUM_BALLS = 10;
std::unique_ptr<Ball> balls[NUM_BALLS];
unsigned long spawnTimer = 0;
int ballsActive = 0;
int screenWidth;
int screenHeight;
public:
void init() override {
// Nothing to do - balls spawned in first update
screenWidth = engine->getRenderer().getLogicalWidth();
screenHeight = engine->getRenderer().getLogicalHeight();
}
void update(unsigned long deltaTime) override {
auto& input = engine->getInputManager();
// Spawn balls gradually
spawnTimer += deltaTime;
if (spawnTimer > 500 && ballsActive < NUM_BALLS) { // Every 500ms
spawnTimer = 0;
balls[ballsActive] = std::make_unique<Ball>(
screenWidth / 2,
screenHeight / 2,
screenWidth,
screenHeight
);
addEntity(balls[ballsActive].get());
ballsActive++;
}
// Reset with button A
if (input.isButtonJustPressed(input::ButtonName::A)) {
resetBalls();
}
// Update all entities (calls Ball::update)
Scene::update(deltaTime);
}
void draw(graphics::Renderer& r) override {
// Clear handled by beginFrame, draw background
r.drawFilledRectangle(0, 0, screenWidth, screenHeight, graphics::Color::BLACK);
// Draw all entities (balls)
Scene::draw(r);
// Draw UI
r.drawText("Balls: " + std::to_string(ballsActive), 5, 5, graphics::Color::WHITE, 1);
r.drawText("Press A to reset", 5, 15, graphics::Color::GRAY, 1);
}
private:
void resetBalls() {
// Remove existing balls
for (int i = 0; i < ballsActive; ++i) {
removeEntity(balls[i].get());
balls[i].reset();
}
ballsActive = 0;
spawnTimer = 0;
}
};
// ============================================================================
// Main Setup
// ============================================================================
void setup() {
// Seed random
randomSeed(analogRead(0));
// Display configuration
graphics::DisplayConfig displayConfig(240, 240);
// Input configuration
input::InputConfig inputConfig;
inputConfig.addButton(input::ButtonName::A, 0); // BOOT button
// Create engine
core::Engine engine(std::move(displayConfig), inputConfig);
// Create and set scene
BouncingBallsScene scene;
engine.setScene(&scene);
// Initialize and run
engine.init();
engine.run();
}
void loop() {
// Empty - engine.run() contains the game loop
}Key Concepts Demonstrated
1. Entity Creation
cpp
class Ball : public core::Entity {
public:
Ball(int x, int y, int w, int h)
: Entity(x, y, w, h, EntityType::GENERIC) {
// Initialize
}
void update(unsigned long deltaTime) override;
void draw(graphics::Renderer& r) override;
};- Extend
Entityfor game objects - Implement
update()for logic - Implement
draw()for rendering
2. Frame-Rate Independent Movement
cpp
void update(unsigned long deltaTime) override {
// Convert to seconds
math::Scalar dt = math::toScalar(deltaTime) / math::toScalar(1000);
// Move at constant speed regardless of FPS
position.x += velocityX * dt;
}Always use deltaTime for time-based calculations.
3. Entity Management
cpp
// Create
balls[i] = std::make_unique<Ball>(...);
addEntity(balls[i].get()); // Scene manages the pointer
// Remove
removeEntity(balls[i].get()); // Remove from scene
balls[i].reset(); // Free memoryUse smart pointers for automatic memory management.
4. Input Handling
cpp
auto& input = engine->getInputManager();
// Button pressed this frame (edge trigger)
if (input.isButtonJustPressed(ButtonName::A)) {
// Trigger action once
}
// Button held (continuous)
if (input.isButtonPressed(ButtonName::LEFT)) {
// Continuous action
}5. Scene Structure
cpp
class MyScene : public core::Scene {
public:
void init() override; // Setup
void update(unsigned long deltaTime) override; // Logic
void draw(graphics::Renderer& r) override; // Rendering
};Build Configuration
ini
; platformio.ini
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
; C++17 required
build_unflags = -std=gnu++11
build_flags =
-std=gnu++17
-fno-exceptions
; Library dependencies
lib_deps =
https://github.com/PixelRoot32-Game-Engine/PixelRoot32-Game-Engine.gitRunning the Example
Create project:
bashmkdir bouncing_balls cd bouncing_ballsCopy code to
src/main.cppCreate
platformio.iniwith configuration aboveBuild and upload:
- Open in VS Code with PlatformIO
- Select environment (ESP32 or Native)
- Build and upload
Expected Behavior
- Balls spawn one at a time every 500ms
- Balls bounce off screen edges
- Ball color changes on each bounce
- Press button A to reset
- Counter shows active ball count
Variations
Add Physics
cpp
// Change to physics-enabled actor
class Ball : public physics::KinematicActor {
public:
Ball(...) : KinematicActor(x, y, w, h) {
setCollisionLayer(DefaultLayers::kEnvironment);
}
void onCollision(Actor* other) override {
// Bounce with physics response
velocityX = -velocityX;
}
};Add Audio
cpp
#if PIXELROOT32_ENABLE_AUDIO
void onCollision(Actor* other) override {
engine->getAudioEngine().playSFX(sound_bounce);
}
#endifTouch Control
cpp
#if PIXELROOT32_ENABLE_TOUCH
void onUnconsumedTouchEvent(const input::TouchEvent& event) override {
if (event.type == input::TouchEventType::CLICK) {
spawnBallAt(event.x, event.y);
}
}
#endifNext Steps
- Samples index — Real folders under
examples/ - Physics (
physics) — Collision and actors - Sprites (
sprites) — Sprite graphics - Audio — See
snake,tic_tac_toe,music_demo
