Advanced Project 10 — Component Game Engine¶
Difficulty: Advanced · Estimated effort: 3 weeks · Prerequisites: Module 11 — RAII and Smart Pointers, Module 14 — The STL with OOP, Module 15 — SOLID and Design Patterns · Demanded pattern decisions: observer, composite, command
This is one of the designated integration projects, and one of the projects that requires a design-pattern decision: at least three patterns are named in this brief, and at least two must be justified by a named requirement while one alternative is considered and declined in writing.
No complete solution is provided. The engine's central data structure — objects holding behaviours — is the design decision this project is built around.
1. Scenario¶
A second-year group is building a small 2D game in this course and has hit the wall every
hand-rolled engine hits: the base GameObject class has grown to hold every field any object
might need, every subclass overrides update and ignores nine tenths of it, and adding a
behaviour means editing the base class and every subclass.
You are asked for the engine core that avoids that: objects that hold components, a frame loop that advances them, a scene graph for things that move together, an event system so components communicate without knowing each other, and commands so input can be remapped and replayed.
2. Problem statement¶
Build the core of a component-based engine: entities with a stable identity, components that hold state and behaviour, a scene graph with parent/child transforms, an event bus, a command layer for input, and a fixed-step frame loop with an injected clock. There is no rendering and no platform code: everything is text output and simulated time, so the whole engine is testable.
3. User requirements¶
- [ ] Compose an entity from components (position, health, movement, scripted behaviour).
- [ ] Group entities into a scene graph and move a parent, moving its children with it.
- [ ] Broadcast an event and have interested components react, without knowing who is listening.
- [ ] Map a text command to an action, so the same action can be triggered by different inputs.
- [ ] Run a fixed number of simulation steps with a fixed timestep and see a deterministic log.
- [ ] Add a new component and a new event type without editing the engine loop.
4. Functional requirements¶
- [ ]
Entitywith a stable id, owning a collection of components. - [ ] A
Componentinterface with lifecycle hooks (added, updated, removed) and an owningunique_ptrcollection. - [ ] At least five components: transform, health, movement, scripted behaviour, event listener.
- [ ] A scene graph: parent/child links, world transform derived from local transform.
- [ ] An event bus: typed subscription, broadcast, and unsubscription that actually works.
- [ ] A command layer: text input mapped to commands, with a replayable log.
- [ ] A fixed-step loop with an injected clock, producing a step log.
- [ ] Typed errors: unknown component or entity, duplicate id, unsubscribing something that was never subscribed.
5. Non-functional requirements¶
- [ ] Destroying an entity destroys its components exactly once, and removes its event subscriptions — verified with counters.
- [ ] A subscription made by a component cannot outlive that component; the design must make this true rather than testing for it after the fact.
- [ ] The same inputs and the same seed produce identical step logs, byte for byte.
- [ ] The scene graph stays acyclic; a cycle attempt is refused.
- [ ] Adding a component requires no edit to the engine, the loop or any other component.
- [ ] Builds warning-free with
g++ -std=c++17 -Wall -Wextra -Werror.
6. Suggested features¶
- A component that records the last n transform samples, as a cheap debugging aid.
- Hierarchical transform composition for a three-level chain, with the arithmetic verified by hand.
- A save/load of the entity hierarchy and its components, using a textual format.
- A profiling report: steps per second of simulated time, and the cost of the event broadcast.
7. OOP concepts involved¶
| Concept | Required | Where it appears |
|---|---|---|
| Multiple classes | yes | Entity, component interface, five components, scene graph node, event bus, command layer, loop, errors |
| Relationships | yes | Entity ◆ components; scene node ◆ children; component ──> bus (observer); command ──> entity |
| Encapsulation | yes | Entity ids, component lifetimes and transform invariants are internal to their owners |
| Inheritance or composition | yes | Components behind an interface; entities composed of behaviours — the whole point of the project |
| Polymorphism | yes | Component update and event handling dispatch; no component-type tags in the loop |
| The STL | yes | std::vector for components and steps, std::map for entities and subscriptions, std::function for handlers |
| Exception handling | yes | Four typed errors with entity and component names in the messages |
8. Suggested design considerations¶
The pattern decision you must make and justify¶
Observer, for the event bus. Components must react to events without knowing who else is listening, and the requirement that earns it is a new event type with no edit to publishers or subscribers. The alternative is a direct call, or a queue of variants consulted by the loop — argue which you chose.
Composite, for the scene graph. A parent's transform applies to all of its descendants, and moving a group must move everything in it. That is the composite pattern's uniform operation over a tree. The alternative is a flat list with cached world transforms, recomputed each step; that is a real engineering choice and must be argued, not dismissed.
Command, for input. The requirement is remappable input and a replayable log. That is the command pattern. The alternative is a switch on a key string inside the loop. The one you decline among these three is the declined-pattern item, and it must be argued against a named requirement.
Other ideas worth weighing: decorator for stacking modifiers on a stat, and flyweight for shared component data — both tempting in games, neither required, and both worth a sentence in the report.
Other design points¶
Subscription lifetime is the hardest bug in this project. A component that subscribes and then dies must be unsubscribed — ideally automatically, from its destructor, or by the bus holding only weak references. Design it, then test it by destroying an entity mid-simulation.
Fixed timestep, injected clock, determinism. These three together are what make a game simulation testable. Without them, every test is a flake and every comparison is a guess.
The scene graph must stay a tree. A cycle in the parent links turns an update into an infinite loop. Refuse it at the point of re-parenting, and test the refusal.
9. Minimum viable version¶
Entities with one component each, a flat update loop with a fixed timestep, a text log, and one event broadcast. No scene graph, no command layer.
10. Intermediate extension¶
Add the component lifecycle, the scene graph with unique_ptr children, and the transform
composition. Verify a three-level chain by hand arithmetic, then destroy a child mid-run and show
that its subscriptions and components are released exactly once.
11. Advanced extension¶
Add the event bus with automatic unsubscription, the command layer with a replay log, and the save/load of the hierarchy. Then write the design note: the patterns used, the one declined with its requirement, the determinism evidence, and the decision you would reverse — with its cost.
12. Testing checklist¶
- [ ] An entity's components are updated once per step, in the documented order
- [ ] Removing a component stops its updates and releases it exactly once
- [ ] Moving a parent moves its children by the same offset; a three-level chain matches hand arithmetic
- [ ] A cycle attempt in the scene graph is refused
- [ ] Destroying an entity mid-simulation removes its event subscriptions — verified by a counter
- [ ] Broadcasting an event reaches every subscriber exactly once, in a documented order
- [ ] A subscriber that throws does not corrupt the bus, per your documented policy
- [ ] A replay log of commands reproduces the identical step log when replayed
- [ ] The same seed produces byte-identical step logs, twice
- [ ] Adding a sixth component required no edit to the loop — file count recorded
- [ ] Save → load of the hierarchy reproduces entities, components and parent links
- [ ] A fixed timestep of zero or negative is refused at configuration time
13. Documentation checklist¶
- [ ] The class diagram, with entity/component ownership and the bus's references
- [ ] The component lifecycle contract: what a component may assume at each hook
- [ ] The subscription lifetime rule, and the mechanism that enforces it
- [ ] The determinism guarantees: timestep, seed, ordering rules
- [ ] The patterns used, the one declined, and the reversed decision with its cost
14. Self-evaluation rubric¶
| Criterion | Excellent looks like | Weight |
|---|---|---|
| Component model | Behaviours composed, not inherited; a new component needs no engine edit | 25% |
| Lifetime safety | Components and subscriptions released exactly once, by design | 25% |
| Pattern decisions | Two patterns justified; one declined against a named requirement | 20% |
| Determinism | Fixed step, injected clock, seeded, byte-identical replays | 15% |
| Scene graph | Transform composition correct; acyclic invariant enforced | 10% |
| Documentation | Contracts, lifetime rule and determinism guarantees written down | 5% |
Related: Advanced projects · Project roadmap · All projects · Module 15 — SOLID and Design Patterns