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.
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.
Add new behavior by writing a new Visitor. Sprite, Scene, and GameEngine never change.
No dynamic_cast, no typeid, no if-else chains on entity type. Just vtable dispatch.
Gravity + Bounce + Draw? Three visitors, added in order. Remove gravity — just don't add it.
Renderer and input are abstract. SFML today, WebAssembly or SDL2 tomorrow.
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.
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:
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
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.
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
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. |
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.
There is no Ball class. No Paddle class. Every game object is a
Sprite constructed with a texture, position, and size.
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);
Each visitor is one responsibility. Compose them at startup:
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
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.
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();
}
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.
Two paddles, a ball, goals. Player 1 on keyboard, perfect-tracking AI on player 2. Full score counting and round resets. The flagship demo.
Two players on a 24×24 grid. Shows GridDrawingVisitor converting grid
coordinates to pixels. Movement works; win condition absent.
3×3 grid renders via GridDrawingVisitor. No game logic implemented —
mostly a scaffold for future work.
Requires SFML (libsfml-graphics, libsfml-window, libsfml-system).
git clone https://github.com/jdspille/Visitor-Game-Engine.git
make · or make release for an optimized build
make run · launches an interactive game-select menu
This engine was built as a learning project, and the rough edges are part of the record.
sf::Clock leaks the SFML dependency into game logic. A GameEngine::setTargetFPS() would fix this.73 = up) instead of a named enum. Works, but fragile.dy, which is upward in SFML's coordinate space. Correct behavior, counter-intuitive name.AbstractRenderer has no text API.