COMP 4300 Notes: Building a 2D Game Engine in C++

The four COMP 4300 reference games: bouncing shapes with an ImGui panel, a Geometry Wars clone, the Mega Mario platformer and a top-down Zelda-like game.

I finished Dave Churchill’s COMP 4300 several months ago. It is a game programming course at Memorial University (the recordings are from Fall 2025) in which you build a small 2D engine in C++23 on top of SFML 3 and Dear ImGui step by step, through four assignments (bouncing shapes, a Geometry Wars clone, a Mega Man platformer and a Zelda-like) and a group project.

Lectures and assignments are how the course is organized. These notes are grouped by topic instead, with links to the lectures behind each one. Every screenshot is a frame from Churchill’s own lecture demos.

Keep data and logic apart

The rule Churchill singles out as the most important one to remember is to keep logic and data apart (L5). In ECS terms an entity is an identity, a component is plain data, and a system is a function that runs over every entity that has the components it needs (L4). A CTransform stores a position and velocity and does nothing; sMovement is the only code that moves things.

L4 compares five ways to store components in C++ and ends on a std::tuple of component types inside Entity, with get<T>(), has<T>(), add<T>() and remove<T>(): contiguous, no allocation per component, looked up at compile time. The EntityManager keeps shared pointers in a vector plus a map from tag to entities, so “all the bullets” is one lookup. It also delays adds and removes: new entities wait in m_toAdd, destroy() only clears m_alive, and both take effect at the start of the next update, because changing a vector while a system iterates over it invalidates the iterators (L5 at 28:00).

Assignment 2 reference solution: dozens of colored polygons on a dark checkered background, a score in the top-left corner, and an ImGui window with checkboxes for the movement, lifespan, collision, spawning, GUI and rendering systems.
Assignment 2’s debug panel switches each system off and lists entities by tag. With movement and lifespan off, bullets freeze in place and never expire, which shows what each system owns (L6 at 5:06). Frame from the COMP 4300 lecture video by David Churchill.

An engine made of scenes, assets and actions

From Assignment 3 on, the single Game class of Assignment 2 splits in two (L8 at 4:34). GameEngine owns what is shared: the window, the loaded Assets, input and a map from names to scenes. Each Scene (menu, play, Zelda) owns an EntityManager, an action map and its own systems, and only the current scene is updated. Opening an inventory screen therefore leaves the gameplay scene alive but paused, and closing it resumes where it left off; Churchill likens the scene map to a finite state machine.

Two habits come with this design. Assets are loaded once and shared, so a thousand birds use one texture. And gameplay code never sees a key: the engine maps keys to action names per scene, and an Action is a name plus START or END (L9). Because actions are tiny, a replay is just a list of frames and actions, which is how Doom demo files worked (L9 at 43:00), and an AI can search over the same actions.

GameEngine
window · main loop · input handling
scenes: map<string, shared_ptr<Scene>>
changeScene(name, args)
Assets
textures · fonts · animations · sounds
owned by the engine; load once, use often
Scene abstract base class
owns an EntityManager, an action map and a frame counter
Scene_MenuScene_PlayScene_Zelda
EntityManager
vector<shared_ptr<Entity>> plus a map from tag to entities
addEntity(tag) · adds and removals are delayed
Entity
id · tag · alive · a std::tuple of components
CTransformCAnimationCInputCBoundingBoxCHealthCGravity
components are plain data, no behavior
Systems read and write components
sDoActionaction to CInput flags
sMovementvelocity, gravity
sAIpatrol, follow, vision
sCollisionAABB detect and resolve
sAnimationadvance sprite frames
sStatuslifespan, invincibility
sCameraroom or follow view
sRenderentities first, GUI last
Runtime structure of the assignment engine. System names follow the course’s sName convention; which systems exist depends on the scene.

Collision, gravity and movement

Collision is two problems, detection and resolution (L7). For axis-aligned bounding boxes (AABB) the overlap on one axis is (w1 + w2) / 2 - |x1 - x2|, and the boxes collide when both axes overlap. The side of the hit comes from the previous frame: if the boxes already overlapped vertically then, the new overlap is horizontal, and the other way round. That is how a brick hit from below breaks while one hit from above is stood on (L7 at 39:00).

Platforming adds little on top (L10). Gravity is an acceleration added to velocity every frame, a jump is an initial upward velocity, and releasing the key cuts it for variable jump height. Speed has to stay below the thinnest obstacle per update or objects tunnel through it; when that is not enough, run several updates per rendered frame, as Super Mario 64 does with four (L19 at 50:00).

Assignment 3 reference solution: a Mega Man sprite standing on a brick floor in a Super Mario style level with question blocks, a green pipe, clouds and a hill.
Assignment 3 (L10 at 3:37). Solid tiles carry bounding boxes, decorations such as clouds and bushes do not, and a debug toggle draws the boxes the physics system sees (L10 at 11:00). Frame from the COMP 4300 lecture video by David Churchill.

Line of sight, lighting and pathfinding

Seeing is segment intersection (L12). Write a ray as A + t·r and an edge as C + u·s; with the 2D cross product, t = ((C - A) × s) / (r × s) and u = ((C - A) × r) / (r × s), and the segments cross when both are in [0, 1] (L12 at 27:00). A rectangle is four such edges. In Assignment 4 an NPC follows the player only while no vision-blocking tile intersects the line between them, and otherwise returns home or patrols its waypoints (L15).

Lighting reuses the same test. Evenly spaced rays leak light past corners; casting three rays at every vertex (one at it, two nudged either side), sorting them by angle and drawing an sf::TriangleFan gives a clean visibility polygon (L12 at 46:00).

Getting somewhere is a separate problem (L14). Search runs on a cheap representation (a grid, or a navmesh of convex polygons) and the path is refined with line-of-sight checks. When many agents share one goal, a vector field replaces per-agent search: a breadth-first distance map from the goal and an arrow to the lowest neighbour. Steering handles the last stretch: desired = normalize(target - pos) * maxSpeed, steering = desired - velocity, with arrival scaling the speed down near the target.

A visibility demo: gray polygons on a black background with a yellow light source and a light-gray region showing everything the light can see. A flow-field demo: about 20,000 white and orange particles streaming around green walls over a blue distance map.
Left: Churchill’s visibility demo, a light source and everything it can see (L12 at 9:22). Right: about 20,000 particles routed around walls by a distance map in real time (L14 at 48:12). Frames from the COMP 4300 lecture videos by David Churchill.

Cameras and views

A 2D camera is just the rectangle of the world that gets drawn (L13). In SFML that is an sf::View: set its center and size, call window.setView, draw the world, then switch back to the default view to draw the HUD at fixed window coordinates. Camera behaviors are small variations on that: lock on the player, smoothed follow, a trap box, directional scrolling. Assignment 3 keeps Mega Man centered once he passes the middle of the screen; Assignment 4 shows one room at a time, where the room index is integer division of position by window size, with a follow camera you can toggle (L15). Viewports, given as fractions of the window, give split screen and minimaps.

Assignment 4 reference solution: a Link-like player with a health bar, a heart pickup, a tree-covered cave entrance and an orange enemy with its own health bar on a sand-colored background.
Assignment 4 draws one 1280 by 768 room of 64 pixel tiles at a time, with a health bar over the player and each enemy (L15 at 3:12). Frame from the COMP 4300 lecture video by David Churchill.

Data-driven design and tools

Almost nothing is hard-coded. Assignment 1 reads Window, Font, Circle and Rectangle lines from a text file (L2); later assets and levels follow the same pattern (Texture, Animation, tiles placed in 64 by 64 grid coordinates), and the project requires levels, player and options in external text files.

Dear ImGui makes tools cheap (L3). It is immediate-mode: the UI is redeclared every frame and widgets edit your variables in place, so a panel to tune a shape, toggle systems, list entities or draw collision boxes is a few lines. Assignment 4 goes one step further with an inspector: click an entity and draw a section for each component it has. That inspector is the first half of a level editor (L16).

The editor itself is a separate scene that ignores physics and AI. Left click selects, right click toggles dragging, positions snap to the grid, and each component the entity lacks gets an add button. Saving writes a temporary file and renames it over the old one so a crash cannot corrupt the save; because components are plain data, serializing them is just writing them out.

Assignment 1 reference solution: colored circles and rectangles with names drawn at their centers, and an ImGui Shape Properties panel with a shape drop-down open. The level editor from lecture 16: a grid with cell coordinates, a Link sprite with a health bar, and an ImGui Selected Entity inspector showing transform, health, damage, bounding box and input components.
Left: Assignment 1’s ImGui panel edits a shape’s visibility, scale, velocity, color and name (L2 at 15:05). Right: the level editor Churchill demonstrates in Lecture 16, with a grid and a Selected Entity inspector (L16 at 58:30). Frames from the COMP 4300 lecture videos by David Churchill.

Animation, shaders and particles

A texture holds pixels on the GPU, and a sprite is a cheap drawable that points at one (L8). A sprite sheet packs frames into one image and setTextureRect picks one; an Animation advances with currentFrame = (gameFrame / speed) % frameCount, and facing left is the same animation drawn with scale.x = -1.

Shaders (L18) are fragment programs that run once per pixel on the GPU. They receive uniforms such as time and resolution and work in normalized 0 to 1 coordinates. In SFML you load an sf::Shader and pass it to window.draw(sprite, &shader), and shaders can be edited and reloaded without recompiling the game; the project asks for three.

Particles (L21) are the one place ECS is the wrong tool: hundreds of thousands of entities waste memory on components they never use. A std::vector<Particle> plus one sf::VertexArray of quads draws everything in a single call.

The same Mega Man sprite twice on a gray background: faded and washed out on the left, tinted pink with red outlines on the right. About 100,000 small pink and white particles forming a dense circular cloud on a black background.
Left: one Mega Man sprite under two fragment shaders, a fade and a red tint (L18 at 60:00). Right: 100,000 particles drawn from one vertex array at 60 fps (L21 at 27:30). Frames from the COMP 4300 lecture videos by David Churchill.

Performance: caches, loops and measuring

The performance lectures are about hardware, not Big-O. Lecture 17 starts from the fact that a cache line is about 64 bytes (L17 at 22:30): entities scattered across the heap cost cache misses, and malloc and free during gameplay are slow. The fix is a pool allocated up front (100,000 entities in the example, L17 at 18:30) in which an entity is just an index into a tuple of component vectors, which also suits systems that read only one or two component types.

L19 covers the main loop. Tying the simulation to the frame rate makes a game run at different speeds on a 60 Hz and a 240 Hz monitor. A fixed-step loop instead accumulates elapsed time and runs as many fixed updates as are owed before it renders, with interpolation if needed, so the physics stays deterministic.

L20 closes the loop with measurement. A RAII timer plus a macro that compiles away writes chrome://tracing JSON. On the platformer it showed that creating the SFML window took longer than loading every asset (L20 at 72:30), that fonts were the slow assets with a 42 KB font the slowest (L20 at 75:30), and that the game’s own update took about a millisecond of each 60 fps frame while SFML waited on the buffer swap for the rest (L20 at 82:00).

The final project: where the topics meet

The final project (L11) is a group assignment for up to four people: build an original game on the same engine, in C++ with ECS, using only SFML and ImGui (Box2D is out). Assignment code can be reused, but assets and levels cannot, and the gameplay has to differ significantly from the assignments. It is worth 50% of the course and has to be passed to pass the course. The specification reads like a checklist of the sections above:

  • Scenes and progression (engine): main menu, overworld map with locked levels, gameplay, inventory, level editor and game over; save and load; an options menu with volume, difficulty and key rebinding.
  • Mechanics (ECS, collision): two movement abilities with a cooldown or cost, three swappable weapons (one using ammo), three NPC types with basic AI, hit points with invincibility frames, three items with an inventory UI, three status effects, moving tiles, gravity.
  • Seeing and thinking (vision): ray casting for visibility, ray-cast lighting, pathfinding or steering.
  • Presentation (cameras, graphics): two camera views, parallax backgrounds, three shaders, a polished HUD instead of ImGui debug UI, music and sound effects.
  • Tools (tools): a grid-based ImGui editor that places textures and edits entity parameters (blocks vision, hit points, patrol points). At least three levels plus a separate boss level. Levels, player and configuration are defined in external text files, and every level except the boss battle has to be buildable in the editor.
  • One extra: a major mechanic outside the list, worth 10% of the project mark.

The project mark is split into a proposal (5%), a short gameplay demo video (10%), the code and gameplay (60%), a 15 to 20 minute report video (20%) and a trailer (5%). The report has to demo every mechanic with a full level playthrough and the editor, and list what did not work.

Two diagrams summarize how the project fits together. First, the scenes it has to ship and how a player moves between them:

Main menuentry point
play
Overworld maplevel select, locks
select
Gameplaylevels from files
boss beaten
Game overcredits
Optionsfrom the main menu: audio, difficulty, key bindings
Level editorfrom the main menu: edits the level files that gameplay loads
Inventoryfrom gameplay: items and status effects
The scenes the specification requires and how a player moves between them. Clearing a level returns to the overworld, which unlocks the next level and saves progress.

Second, the data behind them. Levels, assets and configuration live in external text files, so the level editor edits the same level files that the gameplay scene loads.

Assets fileTexture, Animation, font and sound lines
loaded once
GameEngineshared by every scene
Level filestiles, NPCs, patrol points, per-entity parameters
load, edit, save
Level editorgrid snapping, ImGui inspector
Level filesand the player configuration
load at level start
Gameplayentities and systems from the assignments
Save fileprogress and unlocked levels
load, save
Overworld maplevel select and progression
The external files the specification requires and the scenes that read or write them.

What I would keep

The games are small, but the habits carry over: keep data and logic apart, change structure at frame boundaries, make debugging tools part of the engine, and measure before optimizing.

Sources