Welcome back. Your dependency map showed which parts of the engine call, create, and retain other parts; the frame trace showed how input, movement, collision, transforms, cameras, and rendering actually run during play. We can now name the job each major part is doing.
This lesson is still observation and design planning, not refactoring. You will create a responsibility map for the engine as it exists today: one clear primary responsibility for every major class or subsystem, plus evidence of responsibilities that do not fit. That map will make the next step—deciding what should leave Scene—much less speculative.
A primary responsibility is a coherent job, not one tiny action
“Single responsibility” is often misread as “a class may do only one thing.” That interpretation produces a forest of tiny wrapper classes and does not solve architectural confusion.
A better interpretation is:
A class or subsystem has one primary responsibility when its state, decisions, and operations serve one coherent purpose—and therefore tend to change for the same reason.
A Renderer, for example, can gather renderable objects, select shaders, bind materials, set camera matrices, and issue OpenGL draw calls. Those are several operations, but they all support one responsibility: submit the world’s visual representation to the graphics API.
By contrast, a class that polls keyboard state, decides whether the player can jump, resolves collision, switches cameras, and draws meshes has several unrelated reasons to change:
- A platform-library change affects input.
- A movement-design change affects jumping or sprinting.
- A collision change affects physical resolution.
- An editor workflow change affects camera selection.
- A shader or material change affects rendering.
That is the architectural smell you are looking for—not simply a class with many methods.
Game Engine Architecture: Bill Clark (CodeLabs Tech Talk 2020)
Watch “Game Engine Architecture” by Bill Clark on CodeDay. This short segment gives a useful boundary test: distinguish reusable engine machinery from the game-specific rules built with it.
Watch engine versus game. Focus on the reuse test: could this code plausibly support a substantially different game? Treat it as a guide, not an absolute rule; a small personal engine can intentionally contain game-oriented conveniences.
Your current player movement illustrates this boundary well. A reusable collision query such as “move this shape through the world and report contacts” is engine machinery. The rule “holding Shift raises this prototype’s player speed” is game behavior. A reusable engine may expose a CharacterMotor or action-input abstraction, but it should not need to know that your game has a cube character, a particular sprint key, or a particular jump feel.
Use roles to make vague labels precise
Names such as Manager, System, Utils, and even Scene do not reveal much by themselves. To assign responsibilities, classify each major type by the role it plays. Responsibility-Driven Design offers a compact vocabulary that is especially useful for engine code:
| Role | Primary job | Likely engine examples |
|---|---|---|
| Information holder | Maintains and provides information about one concept | Transform, CameraSettings, Material, collider shape |
| Structurer | Maintains relationships or membership | Scene/world object collection, actor-component container, resource registry |
| Service provider | Performs a domain operation on request | renderer, collision world, resource loader |
| Coordinator | Reacts to an event or phase and delegates work | runtime frame coordinator, play-mode controller |
| Controller | Makes higher-level decisions and directs others | player controller, editor command controller |
| Interfacer | Translates requests/data across boundaries | GLFW input adapter, OpenGL backend, Photon adapter, ImGui bridge |
[PDF] A Brief Tour of Responsibility-Driven Design - Wirfs-Brock
Read the selected material from Wirfs-Brock as a vocabulary and decision process for assigning responsibilities. It is useful here because it treats responsibilities as obligations to know, do, or decide—not merely as class names.
First, read the role-stereotype material on pages 5–7, especially the categories from information holder through interfacer. Then read “Guidelines for Assigning Responsibilities” on pages 19–20, beginning with the three-step process. Finish with the guidance on pages 24–27: find keeping behavior with data, then continue through the cautions against overburdening and overlapping responsibilities. Apply the ideas to the evidence in your dependency map and frame trace, rather than trying to adopt every term literally.
These roles are not rigid categories. A TransformComponent is primarily an information holder, but it can intelligently maintain its own cached matrix or dirty flag. That behavior belongs with the transform information. It does not make the transform responsible for collision response or rendering.
Likewise, coordination is legitimate. The problem is not that some object coordinates the frame; the problem begins when the coordinator makes every low-level decision itself. A coordinator should decide which phase runs and when. The collision service should decide how a movement query is resolved. The renderer should decide how visible objects become GPU commands.
This distinction matters for Scene. A scene may reasonably be a structurer of a loaded world, or a coordinator of world update phases. If it loads files, owns actors, interprets player input, performs collision math, selects the editor camera, and binds OpenGL state, it is serving too many unrelated roles.
Separate engine responsibilities from game and editor responsibilities
A future-proof engine does not mean every feature becomes maximally generic. It means the code has an honest home, so game rules do not quietly become platform code and editor behavior does not leak into runtime play.
Use these working classifications while assigning your responsibilities:
| Classification | Test | Examples in your project |
|---|---|---|
| Platform layer | Would it change if GLFW, the operating system, or graphics API changed? | window creation, raw keyboard/mouse access, OpenGL context and presentation |
| Reusable runtime engine | Could another 3D game use it without inheriting your game’s rules? | transforms, scene membership, mesh/material representation, rendering submission, collision queries |
| Game-specific runtime | Does it encode the particular game’s player behavior, rules, or content? | player movement tuning, jumping rule, Shift sprint behavior, cube-character setup |
| Editor-only | Does it exist to author, inspect, select, or navigate content rather than to run the shipped game? | editor camera, selection state, gizmos, inspection panels |
| Integration boundary | Does it translate between your code and an external SDK or tool? | Photon adapter, ImGui backend, asset import bridge |
The categories identify why code exists, not where it currently lives. Your Scene may currently hold both editor and play-mode camera state. That does not make either state “scene responsibility”; it reveals that the current boundary is carrying unrelated concerns.
The following architecture diagram is useful as an orientation device, not as a design to copy. It separates broadly reusable foundations, scene-facing object concepts, domain services, and platform-facing drivers. Your engine can be much smaller while still benefiting from this kind of separation.

Do not interpret the diagram as an argument that you need Godot’s number of layers, an ECS conversion, or a large service framework. You already have an actor-component approach. The immediate aim is simply to make each existing part’s role legible.
Turn the frame trace into responsibility statements
Start with the major items from your dependency map, not every helper class. Include objects that own significant state, run each frame, interface with a library, or are depended on by many other objects.
For every candidate, write a statement in this form:
Nameis responsible for strong verb + domain object + outcome.
Good statements are specific enough to rule things out:
TransformComponentis responsible for maintaining an actor’s local spatial state and derived world transform.CollisionWorldis responsible for answering collision and movement-resolution queries against world colliders.Rendereris responsible for turning renderable world state and an active camera into OpenGL draw submissions.EditorCameraControlleris responsible for updating editor-only navigation from editor input.PhotonAdapteris responsible for translating between the engine’s networking boundary and Photon callbacks/messages.
Weak statements merely repeat a name:
- “
Rendererhandles rendering.” - “
Scenemanages the scene.” - “
Actordoes actor things.”
A good statement gives you a useful exclusion. If the renderer is responsible for GPU submission, it should not decide whether the player is allowed to jump. If the player controller translates game actions into movement intent, it should not call glDrawElements() or select ImGui windows.
Evidence first, ideal architecture second
For this lesson, state the actual current responsibility, even when it is too broad. Then record the responsibilities that do not belong. Do not silently describe a refactored engine that has not been built.
Suppose your code currently does this:
void Scene::Update(float dt) {
ProcessInput();
UpdateActors(dt);
ResolveCollisions();
SelectCamera();
Render();
}
A truthful first pass might be:
| Item | Current primary responsibility | Evidence | Responsibilities that do not fit cleanly |
|---|---|---|---|
Scene | Coordinates the current prototype’s world frame | Scene::Update() invokes all listed phases | raw input processing, collision implementation, active-camera policy, rendering |
Renderer | Submits visual state to OpenGL | called by Scene::Render(); owns draw code | none observed yet |
PlayerMovementComponent | Applies player movement and jump rules | reads movement action state and changes velocity | direct raw GLFW calls, if present |
Collision... code | Resolves collider movement/contact | calculates collision and grounded state | none observed yet |
This does not approve of the current Scene; it makes the problem visible. In the next lesson, you will decide which of its extra responsibilities should move and where.
A responsibility statement is about authority, not object ownership. If Renderer reads a mesh’s transform, the renderer does not automatically own the transform. If Scene stores actors, it may own their lifetime, but lifecycle decisions will be examined carefully in the next module.
A practical responsibility map for your current engine
Create this document beside your dependency map and frame trace:
docs/architecture/current-responsibilities.md
Use a table like this, adding only types or subsystems that genuinely exist in your project.
| Major class/subsystem | Role | Primary responsibility | Authoritative state or decisions | Collaborators | Explicitly not responsible for | Evidence |
|---|---|---|---|---|---|---|
Application / Engine | Coordinator | Run application lifecycle and long-lived runtime services | startup, shutdown, frame boundary | window, input, scene, renderer | game movement rules | file:function |
| Window/platform adapter | Interfacer | Translate OS/window-library events into platform state | window status, native events | input, renderer backend | player action meaning | file:function |
| Input layer | Interfacer / service provider | Expose raw device state or named actions | key/mouse state; possibly action state | platform, player/editor controllers | player movement response | file:function |
Scene | Structurer or coordinator | Write what it actually does most centrally | actor membership, update ordering, or both | actors, services | list observed overreach | file:function |
Actor | Structurer | Represent one world identity and its attached components | component membership, identity | components, scene | global rendering/collision | file:function |
Component base | Interfacer | Define the common lifecycle/attachment contract for components | component interface | actor, concrete components | game behavior itself | header:function |
TransformComponent | Information holder | Maintain local pose and derived spatial data | position, rotation, scale, matrix/dirty state | actor, camera, renderer, collision | game movement decisions | file:function |
| Collision component | Information holder | Describe an actor’s collision shape and settings | shape, bounds, collision settings | collision world | world-wide collision traversal | file:function |
| Collision service | Service provider | Evaluate and resolve collision queries | contact/manifold or resolution result | colliders, movement code | rendering and input | file:function |
| Character/player movement | Controller | Apply this game’s movement, jump, and sprint rules | desired movement, velocity, jump state | input/action state, transform, collision | editor navigation, GPU calls | file:function |
| Camera | Information holder | Represent a view and projection configuration | pose, FOV, near/far, projection | renderer, camera controller | editor-mode selection | file:function |
| Spring arm | Service provider / component | Derive a camera mount from a target and obstruction result | arm length, desired camera pose | target transform, collision service, camera | active-camera policy | file:function |
| Renderer | Service provider | Submit visible scene data to OpenGL | render pass state, draw submission | camera, mesh/material, transforms | player/game-rule decisions | file:function |
| ImGui integration | Interfacer | Bridge the UI library into the window/render frame | UI frame and capture state | platform, renderer, editor UI | gameplay decisions | file:function |
| Editor camera/selection | Controller | Support authoring navigation and selected-object state | editor pose, selection | editor UI, scene queries | runtime player control | file:function |
| Photon integration | Interfacer | Translate Photon communication into engine-facing messages/state | connection and callback boundary | game replication logic | general actor update policy | file:function |
A few rows deserve care:
-
Input versus player controller. Raw key and mouse state are not movement rules. If
PlayerMovementComponentcalls GLFW directly today, write that as evidence of mixed responsibilities. Do not necessarily add a large input-action framework yet. -
Collision representation versus collision work. A collider component can own shape data, while a collision service performs queries over many colliders. If your collision code currently lives inside an actor or movement component, record the current truth and mark the mixed boundary.
-
Camera data versus camera control. A camera can represent view state. A character camera controller or spring arm can update that state. Editor camera behavior belongs separately from play-mode character camera behavior, even if both use the same lower-level camera representation.
-
Renderer versus mesh-rendering component. A per-actor mesh component usually describes what this actor can render: mesh, material, visibility flags. A renderer generally decides how all renderables are submitted: iteration, camera uniforms, GPU state, and draw calls. Keeping this distinction prevents components from becoming miniature global renderers.
-
Photon boundary versus replication policy. The adapter’s responsibility is to deal with the SDK. The rule for which actor properties are sent, when they are sent, and how remote players are represented is game-level networking behavior. They can initially be close together, but they should not be conceptually indistinguishable.
Component · Decoupling Patterns
Read the relevant sections of “Component” from Game Programming Patterns. It gives a concrete example of separating input, physics, and graphics while preserving the fact that those domains must still coordinate through an entity.
Read the “Motivation” section through “Cutting the knot.” Use the domain-boundary claim as the anchor, and continue through the explanation of slicing a monolithic object along domain lines. Then read the “Sample Code” section, especially “A monolithic class,” “Splitting out a domain,” and “Splitting out the rest.” Focus on the distinction between the entity as a container and components as domain behavior; do not treat the example’s exact update order or shared-state design as a required implementation for your engine.
Detect mixed responsibilities with change pressure
Once the first table is filled, look at each row through the lens of change pressure. List the plausible reasons the class would need to change. Several methods can remain together when they change for the same underlying reason; unrelated change pressures reveal a boundary problem.
For example:
| Class or subsystem | Changes when… | Assessment |
|---|---|---|
TransformComponent | transform representation, hierarchy behavior, or matrix caching changes | coherent spatial responsibility |
Renderer | shader/material API, render pass, or OpenGL submission changes | coherent rendering responsibility |
PlayerMovementComponent | jump rules, sprint tuning, acceleration, or player controls change | coherent game-play responsibility |
Scene | actor storage, level loading, collision algorithm, rendering API, editor selection, and input routing all change | probably mixed |
Actor | adding a component, changing rendering code, resolving collision, and selecting ImGui panels all require edits | almost certainly mixed |
Use these red flags as annotations in the map:
- Different domain vocabulary in one method. A function that simultaneously uses GLFW, collision normals, OpenGL calls, and ImGui is crossing several domains.
- Writes state owned by several concepts. One class directly updates player velocity, collider contacts, camera pose, and shader uniforms.
- A global object is passed everywhere. If every component receives
Scene&and can do anything through it, the components are nominally separated but remain coupled through a universal back door. - Two places claim final authority. Both a movement component and collision component write actor position without a clear rule, or both editor and runtime code can choose the active camera.
- A class stores data it does not use to fulfill its own job. For example, a scene keeps editor-only selected-object state solely because it was convenient.
A class may collaborate broadly without being responsible for everything it touches. PlayerMovementComponent needs a collision query to move safely; that does not make it responsible for the collision algorithm. Renderer needs the active camera; that does not make it responsible for camera controls.
Finish with an explicit “not responsible for” boundary
The most valuable column in your map is often Explicitly not responsible for. It turns a pleasant class description into a practical rule for future code.
For each major item, complete one sentence:
[Class/subsystem] may collaborate with [other subsystem] through [current API],
but it must not [unrelated decision, storage, or low-level operation].
Examples suited to your prototype:
PlayerMovementComponent may request a collision move,
but it must not iterate every actor in the scene or issue rendering calls.
Renderer may read the selected active camera,
but it must not choose whether play mode uses the character camera or editor camera.
EditorCameraController may read raw input when edit mode is active,
but it must not mutate the runtime player's transform.
Scene may coordinate the currently loaded world,
but it must not become the implementation location for platform input,
OpenGL submission, or player-specific behavior.
Keep a short open decisions list below the table. These are uncertainties you discovered but should not “solve” by guessing:
## Open decisions
- Is grounded state authoritative in the collider, the movement component, or a collision result?
- Does Scene own actor lifetime, or does a higher-level world/application object?
- Where should play-mode versus edit-mode active-camera selection live?
- Is Photon callback processing a platform/integration concern or mixed into Scene update?
This is productive ambiguity. Your next lessons will supply the tools to decide ownership, lifetime, and communication rules safely. For now, your job is to make hidden overlap visible.
Key takeaways
A single primary responsibility is a coherent purpose with a coherent reason to change; it does not mean one method or one line of behavior. Use role labels—information holder, structurer, service provider, coordinator, controller, and interfacer—to state what each major engine part actually does.
Build current-responsibilities.md from your dependency map and frame trace. For each major class or subsystem, record its primary responsibility, authoritative state or decisions, collaborators, evidence, and an explicit statement of what it must not own. Treat Scene honestly: if it currently spans world structure, frame coordination, input, collision, cameras, rendering, editor state, or networking, record those overlaps rather than hiding them behind its name.
Next, you will focus directly on Scene and identify which of its current responsibilities belong in other subsystems.
Can't find a good explanation? Sign up and we'll make it for you
Sign up