Gameplay Guidelines - PixelRoot32 â
Patterns and best practices for game feel, mechanics, and common implementations.
đŽ Core Principles â
Frame-Rate Independence â
Always multiply movement by deltaTime:
cpp
// â
Good: Frame-rate independent
void update(unsigned long dt) {
x += speed * math::toScalar(dt * 0.001f);
}
// â Bad: Frame-rate dependent (different speeds at different FPS)
void update(unsigned long dt) {
x += speed; // Bug!
}Logic/Visual Decoupling â
For infinite runners and auto-scrollers:
- Logic progression (obstacle spacing, spawn timing): Constant in real time
- Visual speed: Can increase for difficulty without affecting game logic
cpp
// â
Decoupled: Logic constant, visual varies
void update(unsigned long dt) {
// Game logic: constant spacing
spawnTimer += dt;
if (spawnTimer > SPAWN_INTERVAL) {
spawnObstacle();
spawnTimer = 0;
}
// Visual: can speed up for effect
scrollX += visualSpeed * dt;
}đšī¸ Game Feel â
Snappy Controls â
For fast-paced games, prefer higher values to reduce "floatiness":
cpp
// â Floaty
constexpr Scalar GRAVITY = math::toScalar(0.3f);
constexpr Scalar JUMP_FORCE = math::toScalar(8.0f);
// â
Snappy
constexpr Scalar GRAVITY = math::toScalar(0.6f);
constexpr Scalar JUMP_FORCE = math::toScalar(12.0f);Slopes & Ramps on Tilemaps â
Treat contiguous ramp tiles as a single logical slope:
cpp
// â
Linear interpolation over world X
Scalar getRampHeight(int worldX) {
// Ramp from y=80 to y=48 across 4 tiles (64 pixels)
Scalar t = math::toScalar((worldX - rampStartX) / 64.0f);
return math::lerp(math::toScalar(80.0f), math::toScalar(48.0f), t);
}Keep gravity and jump parameters identical between flat ground and ramps for consistent jump timing.
đī¸ Architecture Patterns â
Tuning Constants â
Extract gameplay values to a dedicated header:
cpp
// GameConstants.h
namespace GameConstants {
constexpr Scalar PLAYER_SPEED = math::toScalar(120.0f); // px/sec
constexpr Scalar GRAVITY = math::toScalar(0.6f);
constexpr Scalar JUMP_FORCE = math::toScalar(12.0f);
constexpr int MAX_BULLETS = 50;
}Benefits:
- Designers can tweak without touching logic
- Single source of truth
- Easy balance testing
State Management with reset() â
Reuse actors across game sessions instead of destroying/recreating:
cpp
class PlayerActor : public PhysicsActor {
public:
void reset(Vector2 startPos) {
position = startPos;
velocity = Vector2::zero();
health = MAX_HEALTH;
isActive = true;
}
};
// In scene
void onGameOver() {
player->reset(START_POSITION); // â
Reuse
// NOT: player = new PlayerActor(); // â Allocates
}Component Pattern â
| Actor Type | Use For | Example |
|---|---|---|
Actor | Static objects | Walls, platforms |
PhysicsActor | Moving objects | Player, enemies |
KinematicActor | Controlled movement | Player with input |
SensorActor | Triggers | Goal zones, hazards |
đ Anti-Patterns (Common Mistakes) â
1. No Delta Time â
cpp
// â WRONG: Different behavior at different FPS
void update(unsigned long dt) {
x += speed;
y += velocity.y;
}
// â
CORRECT: Consistent regardless of FPS
void update(unsigned long dt) {
Scalar dtSec = math::toScalar(dt * 0.001f);
x += speed * dtSec;
y += velocity.y * dtSec;
}2. Logic in draw() â
cpp
// â WRONG: Game logic in render
void draw(Renderer& r) {
if (player->x > 100) { // Logic!
spawnEnemy();
}
player->draw(r);
}
// â
CORRECT: Logic in update, render in draw
void update(unsigned long dt) {
if (player->position.x > ENEMY_SPAWN_X) {
spawnEnemy();
}
}
void draw(Renderer& r) {
player->draw(r); // Pure rendering
}3. Runtime Allocation in Game Loop â
cpp
// â WRONG: Allocates every frame
void update(unsigned long dt) {
if (shootPressed) {
auto bullet = std::make_unique<Bullet>(x, y); // BAD!
scene.addEntity(bullet.get());
}
}
// â
CORRECT: Pool pattern
class BulletPool {
std::array<Bullet, MAX_BULLETS> bullets;
std::bitset<MAX_BULLETS> active;
public:
void spawn(Vector2 pos) {
for (size_t i = 0; i < MAX_BULLETS; ++i) {
if (!active[i]) {
active[i] = true;
bullets[i].reset(pos);
return;
}
}
}
};4. std::rand() in Hot Paths â
cpp
// â WRONG: Slow, uses division
void update(unsigned long dt) {
if (std::rand() % 100 < 5) { // Expensive!
spawnParticle();
}
}
// â
CORRECT: Fast Xorshift
void update(unsigned long dt) {
if (math::randomRange(0, 100) < 5) { // Optimized
spawnParticle();
}
}5. Magic Numbers â
cpp
// â WRONG: What do these mean?
if (player.y > 200) { ... }
if (enemy.hp < 25) { ... }
// â
CORRECT: Named constants
constexpr Scalar GROUND_Y = math::toScalar(200.0f);
constexpr int CRITICAL_HEALTH = 25;
if (player.position.y > GROUND_Y) { ... }
if (enemy.health < CRITICAL_HEALTH) { ... }6. Using std::vector in Game Loop â
cpp
// â WRONG: Potential reallocation
void update(unsigned long dt) {
enemies.push_back(new Enemy()); // May allocate!
}
// â
CORRECT: Fixed-size pool
std::array<Enemy, MAX_ENEMIES> enemies;
std::bitset<MAX_ENEMIES> enemyActive;
void spawnEnemy() {
for (size_t i = 0; i < MAX_ENEMIES; ++i) {
if (!enemyActive[i]) {
enemyActive[i] = true;
enemies[i].reset();
return;
}
}
}đ Related Documentation â
| Document | Topic |
|---|---|
| Coding Style | C++ conventions |
| Memory | Pool patterns, allocation |
| UI Guidelines | UI layouts, HUDs |
| Performance | Hot paths, optimization |
Good games feel responsive, consistent, and intentional.
