Sprites and Animation¶
This guide covers advanced sprite techniques and animation in PixelRoot32, including different sprite formats, creating animations, and best practices.
Sprite Formats¶
PixelRoot32 supports multiple sprite formats, each optimized for different use cases.
1bpp (Standard, Monochrome)¶
The standard format uses 1 bit per pixel (monochrome). This is the most memory-efficient format:
#include <graphics/Renderer.h>
// Define sprite data (8x8 example)
static const uint16_t PLAYER_SPRITE_DATA[] = {
0b00111100, // Row 0
0b01111110, // Row 1
0b11111111, // Row 2
0b11111111, // Row 3
0b11111111, // Row 4
0b01111110, // Row 5
0b00111100, // Row 6
0b00000000 // Row 7
};
// Create sprite descriptor
static const pixelroot32::graphics::Sprite PLAYER_SPRITE = {
PLAYER_SPRITE_DATA,
8, // width
8 // height
};
// Draw sprite
renderer.drawSprite(PLAYER_SPRITE, 100, 100, pixelroot32::graphics::Color::White);
Characteristics: - Most memory-efficient - 1 bit per pixel - Maximum width: 16 pixels - Color applied at draw time - Best for: Simple graphics, retro style
2bpp (Experimental, 4 Colors)¶
2 bits per pixel allows 4 colors per sprite:
#ifdef PIXELROOT32_ENABLE_2BPP_SPRITES
#include <graphics/Renderer.h>
// Define 2bpp sprite data
static const uint8_t COLORFUL_SPRITE_DATA[] = {
// Each byte represents 4 pixels (2 bits each)
// Format: [pixel3][pixel2][pixel1][pixel0]
0x00, 0x11, 0x22, 0x33, // Row 0
0x11, 0x22, 0x33, 0x00, // Row 1
// ... more rows
};
// Define palette (4 colors)
static const pixelroot32::graphics::Color SPRITE_PALETTE[] = {
pixelroot32::graphics::Color::Transparent,
pixelroot32::graphics::Color::Red,
pixelroot32::graphics::Color::Green,
pixelroot32::graphics::Color::Blue
};
// Create 2bpp sprite
static const pixelroot32::graphics::Sprite2bpp COLORFUL_SPRITE = {
COLORFUL_SPRITE_DATA,
SPRITE_PALETTE,
16, // width
8, // height
4 // palette size
};
// Draw 2bpp sprite
renderer.drawSprite(COLORFUL_SPRITE, 100, 100, false);
#endif
Characteristics: - 2 bits per pixel (4 colors) - Requires custom palette - More memory than 1bpp - Best for: More colorful sprites without full color
4bpp (Experimental, 16 Colors)¶
4 bits per pixel allows 16 colors per sprite:
#ifdef PIXELROOT32_ENABLE_4BPP_SPRITES
#include <graphics/Renderer.h>
// Define 4bpp sprite data
static const uint8_t RICH_SPRITE_DATA[] = {
// Each byte represents 2 pixels (4 bits each)
// Format: [pixel1][pixel0]
0x01, 0x23, 0x45, 0x67, // Row 0
// ... more rows
};
// Define palette (16 colors)
static const pixelroot32::graphics::Color RICH_PALETTE[] = {
pixelroot32::graphics::Color::Transparent,
pixelroot32::graphics::Color::Black,
pixelroot32::graphics::Color::DarkGray,
// ... 13 more colors
};
// Create 4bpp sprite
static const pixelroot32::graphics::Sprite4bpp RICH_SPRITE = {
RICH_SPRITE_DATA,
RICH_PALETTE,
16, // width
16, // height
16 // palette size
};
// Draw 4bpp sprite
renderer.drawSprite(RICH_SPRITE, 100, 100, false);
#endif
Characteristics: - 4 bits per pixel (16 colors) - Requires custom palette - Most memory-intensive - Best for: Detailed sprites with many colors
MultiSprite (Multi-Layer)¶
MultiSprite combines multiple 1bpp layers to create multi-color sprites:
#include <graphics/Renderer.h>
// Define layers (each is 1bpp)
static const uint16_t BASE_LAYER_DATA[] = {
0b00111100,
0b01111110,
0b11111111,
0b11111111,
0b11111111,
0b01111110,
0b00111100,
0b00000000
};
static const uint16_t HIGHLIGHT_LAYER_DATA[] = {
0b00000000,
0b00011000,
0b00111100,
0b00111100,
0b00111100,
0b00011000,
0b00000000,
0b00000000
};
// Create layers
static const pixelroot32::graphics::SpriteLayer LAYERS[] = {
{ BASE_LAYER_DATA, pixelroot32::graphics::Color::Blue }, // Base layer
{ HIGHLIGHT_LAYER_DATA, pixelroot32::graphics::Color::Cyan } // Highlight layer
};
// Create MultiSprite
static const pixelroot32::graphics::MultiSprite PLAYER_MULTI = {
8, // width
8, // height
LAYERS, // layers array
2 // layer count
};
// Draw MultiSprite
renderer.drawSprite(PLAYER_MULTI, 100, 100, false);
Characteristics: - Combines multiple 1bpp layers - Each layer can have different color - Layers drawn in order (first = bottom) - Best for: Complex sprites with highlights, outlines, etc.
Creating Sprites¶
Manual Creation (1bpp)¶
For simple sprites, you can create them manually:
// 8x8 sprite: Simple circle
static const uint16_t CIRCLE_SPRITE_DATA[] = {
0b00111100, // ####
0b01111110, // ######
0b11111111, // ########
0b11111111, // ########
0b11111111, // ########
0b11111111, // ########
0b01111110, // ######
0b00111100 // ####
};
static const pixelroot32::graphics::Sprite CIRCLE_SPRITE = {
CIRCLE_SPRITE_DATA,
8,
8
};
Tips: - Use binary notation for clarity - Comment each row to visualize - Keep sprites small (8x8, 16x16) - Reuse sprites when possible
Using Sprite Compiler¶
For complex sprites, use the Sprite Compiler tool (if available) to convert PNG images to sprite data.
Sprite Animation¶
PixelRoot32 uses a step-based animation system that's lightweight and efficient.
SpriteAnimation Structure¶
#include <graphics/Renderer.h>
// Define animation frames
static const uint16_t FRAME1_DATA[] = { /* ... */ };
static const uint16_t FRAME2_DATA[] = { /* ... */ };
static const uint16_t FRAME3_DATA[] = { /* ... */ };
static const pixelroot32::graphics::Sprite FRAME1 = { FRAME1_DATA, 8, 8 };
static const pixelroot32::graphics::Sprite FRAME2 = { FRAME2_DATA, 8, 8 };
static const pixelroot32::graphics::Sprite FRAME3 = { FRAME3_DATA, 8, 8 };
// Create animation frames
static const pixelroot32::graphics::SpriteAnimationFrame ANIMATION_FRAMES[] = {
{ &FRAME1, nullptr }, // Frame 1 (no mask)
{ &FRAME2, nullptr }, // Frame 2
{ &FRAME3, nullptr } // Frame 3
};
// Create animation
pixelroot32::graphics::SpriteAnimation walkAnimation;
walkAnimation.frames = ANIMATION_FRAMES;
walkAnimation.frameCount = 3;
walkAnimation.current = 0;
Using Animations in Actors¶
#include <core/Actor.h>
#include <graphics/Renderer.h>
class AnimatedPlayer : public pixelroot32::core::Actor {
private:
pixelroot32::graphics::SpriteAnimation walkAnimation;
unsigned long animationTimer = 0;
const unsigned long FRAME_DURATION_MS = 100; // 100ms per frame
public:
AnimatedPlayer(float x, float y)
: Actor(x, y, 8, 8) {
setRenderLayer(1);
// Initialize animation
walkAnimation.frames = WALK_ANIMATION_FRAMES;
walkAnimation.frameCount = 3;
walkAnimation.current = 0;
}
void update(unsigned long deltaTime) override {
// Update animation
animationTimer += deltaTime;
if (animationTimer >= FRAME_DURATION_MS) {
animationTimer -= FRAME_DURATION_MS;
walkAnimation.step(); // Advance to next frame
}
// Movement logic...
}
void draw(pixelroot32::graphics::Renderer& renderer) override {
// Get current frame
const pixelroot32::graphics::Sprite* currentFrame =
walkAnimation.frames[walkAnimation.current].sprite;
// Draw current frame
renderer.drawSprite(
*currentFrame,
static_cast<int>(x),
static_cast<int>(y),
pixelroot32::graphics::Color::White,
false // flipX
);
}
pixelroot32::core::Rect getHitBox() override {
return {x, y, width, height};
}
void onCollision(pixelroot32::core::Actor* other) override {
// Handle collision
}
};
Animation Control¶
// Reset animation to first frame
walkAnimation.reset();
// Step to next frame (loops automatically)
walkAnimation.step();
// Get current frame
const pixelroot32::graphics::Sprite* frame =
walkAnimation.frames[walkAnimation.current].sprite;
// Check if animation is at specific frame
if (walkAnimation.current == 0) {
// At first frame
}
Multiple Animations¶
For actors with multiple animations (idle, walk, jump):
class PlayerActor : public pixelroot32::core::Actor {
private:
enum class AnimationState {
IDLE,
WALK,
JUMP
};
AnimationState currentState = AnimationState::IDLE;
pixelroot32::graphics::SpriteAnimation idleAnim;
pixelroot32::graphics::SpriteAnimation walkAnim;
pixelroot32::graphics::SpriteAnimation jumpAnim;
pixelroot32::graphics::SpriteAnimation* getCurrentAnimation() {
switch (currentState) {
case AnimationState::IDLE: return &idleAnim;
case AnimationState::WALK: return &walkAnim;
case AnimationState::JUMP: return &jumpAnim;
}
return &idleAnim;
}
public:
void update(unsigned long deltaTime) override {
// Update current animation
auto* anim = getCurrentAnimation();
animationTimer += deltaTime;
if (animationTimer >= FRAME_DURATION_MS) {
animationTimer -= FRAME_DURATION_MS;
anim->step();
}
// Change animation state based on game logic
if (isMoving) {
if (currentState != AnimationState::WALK) {
currentState = AnimationState::WALK;
walkAnim.reset();
}
} else if (isJumping) {
if (currentState != AnimationState::JUMP) {
currentState = AnimationState::JUMP;
jumpAnim.reset();
}
} else {
if (currentState != AnimationState::IDLE) {
currentState = AnimationState::IDLE;
idleAnim.reset();
}
}
}
void draw(pixelroot32::graphics::Renderer& renderer) override {
auto* anim = getCurrentAnimation();
const auto* frame = anim->frames[anim->current].sprite;
renderer.drawSprite(*frame, static_cast<int>(x), static_cast<int>(y),
pixelroot32::graphics::Color::White);
}
};
Sprite Flipping¶
Flip sprites horizontally for facing direction:
bool facingRight = true;
void draw(pixelroot32::graphics::Renderer& renderer) override {
renderer.drawSprite(
PLAYER_SPRITE,
static_cast<int>(x),
static_cast<int>(y),
pixelroot32::graphics::Color::White,
!facingRight // Flip if facing left
);
}
Best Practices¶
Memory Optimization¶
- Reuse sprites: Define sprites once, use many times
- Use 1bpp when possible: Most memory-efficient
- Store in flash: Use
static constto keep in flash memory - Limit sprite count: Too many unique sprites can exhaust memory
Performance¶
- Pre-calculate animations: Set up animations in
init(), notupdate() - Limit active animations: Only animate visible entities
- Use appropriate formats: Don't use 4bpp if 1bpp works
- Batch similar sprites: Draw similar sprites together
Organization¶
- Group related sprites: Keep sprite data together
- Use meaningful names: Name sprites clearly
- Document complex sprites: Comment sprite bit patterns
- Create sprite libraries: Reusable sprite collections
Common Patterns¶
Sprite Sheet Pattern¶
Organize multiple sprites in arrays:
namespace PlayerSprites {
static const uint16_t IDLE_FRAME1[] = { /* ... */ };
static const uint16_t IDLE_FRAME2[] = { /* ... */ };
static const uint16_t WALK_FRAME1[] = { /* ... */ };
static const uint16_t WALK_FRAME2[] = { /* ... */ };
static const pixelroot32::graphics::Sprite IDLE[] = {
{ IDLE_FRAME1, 8, 8 },
{ IDLE_FRAME2, 8, 8 }
};
static const pixelroot32::graphics::Sprite WALK[] = {
{ WALK_FRAME1, 8, 8 },
{ WALK_FRAME2, 8, 8 }
};
}
Animation Helper¶
Create a helper class for animation management:
class AnimationController {
private:
pixelroot32::graphics::SpriteAnimation* currentAnim;
unsigned long timer = 0;
unsigned long frameDuration;
public:
void setAnimation(pixelroot32::graphics::SpriteAnimation* anim) {
if (currentAnim != anim) {
currentAnim = anim;
currentAnim->reset();
timer = 0;
}
}
void update(unsigned long deltaTime) {
if (currentAnim) {
timer += deltaTime;
if (timer >= frameDuration) {
timer -= frameDuration;
currentAnim->step();
}
}
}
const pixelroot32::graphics::Sprite* getCurrentFrame() {
if (currentAnim && currentAnim->frameCount > 0) {
return currentAnim->frames[currentAnim->current].sprite;
}
return nullptr;
}
};
Troubleshooting¶
Sprites Not Displaying¶
- Check sprite data is valid
- Verify width/height match data
- Ensure sprite is within screen bounds
- Check render layer is correct
Animation Not Playing¶
- Verify animation frames are set
- Check
step()is being called - Ensure timer logic is correct
- Verify frame count matches array size
Memory Issues¶
- Reduce sprite count
- Use 1bpp instead of 2bpp/4bpp
- Reuse sprites more
- Check available flash memory
Next Steps¶
Now that you understand sprites and animation, learn about: - Color Palettes - Use different color schemes - Cameras and Scrolling - Create scrolling levels - Tilemaps - Build levels with tiles
See also: - API Reference - Sprite - API Reference - SpriteAnimation - Manual - Basic Rendering