C++ · SFML · Visitor Pattern

Behavior by composition,
not inheritance.

A game engine where every operation is a Visitor and every entity is a Sprite. No entity subclasses. No type switches. Just double dispatch — two vtable lookups per sprite per frame.

2 virtual calls per sprite
8 composable visitors
0 entity subclasses
0 dynamic_cast calls
The Idea

Everything is a Visitor.
Sprites are just data.

Most game engines reach for inheritance: Enemy extends Character, FlyingEnemy extends Enemy, until the hierarchy collapses under its own weight. This engine takes the opposite stance.

Sprites hold state. Visitors hold behavior. A Sprite is nothing but position, velocity, size, and a texture path. It knows nothing about physics, rendering, or collision. All of that lives in Visitors — small, single-purpose objects that are composed at runtime by adding them to the engine.

📦

Open/Closed

Add new behavior by writing a new Visitor. Sprite, Scene, and GameEngine never change.

No RTTI

No dynamic_cast, no typeid, no if-else chains on entity type. Just vtable dispatch.

🧩

Composable

Gravity + Bounce + Draw? Three visitors, added in order. Remove gravity — just don't add it.

🔁

Swappable backends

Renderer and input are abstract. SFML today, WebAssembly or SDL2 tomorrow.

Double Dispatch

Two pointer dereferences.
That's the whole engine loop.

The term "double dispatch" sounds heavy. It isn't. The entire update loop is five lines, and each frame does exactly two virtual calls per sprite — one to pick the scene, one to pick the visitor.

GameEngine.cpp
void GameEngine::update() {
    for (auto &visitor : this->sceneVisitors)
        this->scene->accept(visitor); // ← dispatch #1: which scene?
}

scene->accept() is a virtual call. The vtable resolves whether this is a SimpleScene, LayeredScene, or anything else. Inside the scene:

SimpleScene.cpp
void SimpleScene::accept(std::shared_ptr<Visitor> v) {
    for (auto &sprite : this->spriteList)
        v->visit(sprite); // ← dispatch #2: which visitor?
}

v->visit() is the second virtual call. Each concrete Visitor implements its own visit() — physics, collision, drawing — and the vtable picks the right one. The CPU branch predictor gets very good at this loop very quickly.

GameEngine::update()
  ├── for each Visitor in sceneVisitors:
  │       │
  │       └── scene->accept(visitor)          ← vtable[scene]
  │                   │
  │                   └── for each Sprite in spriteList:
  │                               │
  │                               └── visitor->visit(sprite)   ← vtable[visitor]
  
    ✓  No dynamic_cast   ✓  No typeid   ✓  No if-else chains
Adding a new behavior — say, a MagnetVisitor that attracts sprites toward a point — means writing one new class with one method. Nothing else changes. The engine is open for extension and closed for modification in the most literal sense.
Architecture

The full picture.

The engine sits at the center, composing a scene with a list of visitors. The renderer and input handler are fully abstracted behind interfaces.

GameEngine
  
  ├── scene ──► AbstractScene
  │               ├── SimpleScene       (flat sprite list)
  │               └── LayeredScene      (z-ordered layers)
  
  └── visitors ──► list<Visitor>
  
                   ├── SimpleDrawingVisitor         rendering
                   ├── GridDrawingVisitor           grid → pixel coords
                   ├── BoundingBoxCollisionVisitor  AABB hit test
                   ├── ForceVisitor                 apply velocity
                   ├── BounceBoundsVisitor          reflect at boundary
                   ├── WrapBoundsVisitor            teleport at boundary
                   └── GravityVisitor               accelerate downward

AbstractRenderer ──► SFMLRenderer         (or SDL2, WASM, …)
AbstractInputWrapper ──► SFMLInputWrapper
Visitor Catalog

Eight behaviors. Zero entity subclasses.

Every behavior in the engine is a Visitor. Compose them freely at game startup.

Visitor Category What it does
SimpleDrawingVisitor Rendering Draws each sprite at its (x, y) position using the renderer.
GridDrawingVisitor Rendering Same as above, but converts grid coordinates to pixel space first. Used by Quoridor.
BoundingBoxCollisionVisitor Collision AABB test against a single watched sprite. Accumulates hits; drain with getCollisions().
ForceVisitor Physics Moves sprites by their (dx, dy) each frame. applyForce(s, magnitude, angle) converts polar to Cartesian.
BounceBoundsVisitor Physics Reflects dx/dy when a sprite exits the scene boundary.
WrapBoundsVisitor Physics Teleports sprites from one edge to the opposite (Pac-Man style).
GravityVisitor Physics Adds a constant acceleration to dy each frame.
RayCastCollisionVisitor Stub Unimplemented. Interface is in place for future ray-based collision detection.
Building a Game

Pong in 40 lines of setup.

Pong is the flagship demo. It shows how the visitor composition model maps directly to a real game: entities are plain sprites, behaviors are visitors wired into the engine, and the game loop is just ge->update() plus collision response.

1 — Sprites are data

There is no Ball class. No Paddle class. Every game object is a Sprite constructed with a texture, position, and size.

Pong.cpp — sprite declarations
auto player1     = make_shared<Sprite>(tex, MINX+100, MAXY/2, paddleW, paddleH);
auto player2     = make_shared<Sprite>(tex, MAXX-100, MAXY/2, paddleW, paddleH);
auto player1Goal = make_shared<Sprite>(tex, MINX+10,  0,      5,       MAXY*2);
auto player2Goal = make_shared<Sprite>(tex, MAXX-10,  0,      5,       MAXY*2);
auto ball        = make_shared<Sprite>(tex, MAXX/2,   MAXY/2, 10,      10);

2 — Visitors are behavior

Each visitor is one responsibility. Compose them at startup:

Pong.cpp — visitor setup
auto bounce = make_shared<BounceBoundsVisitor>(MINX, MAXX, MINY, MAXY);
auto force  = make_shared<ForceVisitor>();
auto collide = make_shared<BoundingBoxCollisionVisitor>();
auto draw   = make_shared<SimpleDrawingVisitor>(renderer);

auto ge = make_shared<GameEngine>();

// sprites into scene
ge->addSprite(player1);  ge->addSprite(player2);
ge->addSprite(player1Goal); ge->addSprite(player2Goal);
ge->addSprite(ball);

// visitors run in order — draw must come last
ge->addVisitor(collide);
ge->addVisitor(force);
ge->addVisitor(bounce);
ge->addVisitor(draw);

collide->setWatched(ball);        // track ball against everything else
force->applyForce(ball, 10, rand()); // kick off at a random angle

3 — Game loop

ge->update() runs all visitors over all sprites. After that, drain the collision list and respond. The engine knows nothing about paddles or goals — that logic lives in game code, where it belongs.

Pong.cpp — game loop
while (draw->isOpen()) {
    ge->update(); // runs every visitor across every sprite

    // input
    for (int key : input->getKeyPresses()) {
        if (key == 73) force->applyForce(player1, 1, 0);   // up
        if (key == 74) force->applyForce(player1, 1, 180); // down
    }

    // collision response
    for (auto &hit : collide->getCollisions()) {
        if (hit == player1Goal) { p2score++; resetRound(); }
        if (hit == player2Goal) { p1score++; resetRound(); }
        if (hit == player1) {
            // angle off the paddle face based on where the ball hit
            ball->setDXY(
                fabs(ball->getDX()),
                SPRING * ((ball->getY() - player1->getY()) / paddleH - 0.5f)
            );
        }
    }

    // perfect AI: just track the ball
    player2->setXY(player2->getX(), ball->getY() - paddleH/2);

    draw->draw();
}
The paddle angle-response math — SPRING * (hit_offset - 0.5) — is the most interesting moment in Pong. Hit the top of the paddle: ball goes up. Hit the center: straight. Hit the bottom: ball goes down. All without any knowledge of a "paddle type" anywhere in the engine.
Games

Three demos. One engine.

🏓 Pong Complete

Two paddles, a ball, goals. Player 1 on keyboard, perfect-tracking AI on player 2. Full score counting and round resets. The flagship demo.

♟ Quoridor Demo

Two players on a 24×24 grid. Shows GridDrawingVisitor converting grid coordinates to pixels. Movement works; win condition absent.

❌ TicTacToe Unfinished

3×3 grid renders via GridDrawingVisitor. No game logic implemented — mostly a scaffold for future work.

Build & Run

One dependency. Three commands.

Requires SFML (libsfml-graphics, libsfml-window, libsfml-system).

1
Clone the repo git clone https://github.com/jdspille/Visitor-Game-Engine.git
2
Build make  ·  or make release for an optimized build
3
Run make run  ·  launches an interactive game-select menu

Known Limitations

This engine was built as a learning project, and the rough edges are part of the record.