Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Assigning Primary Responsibilities to Engine Classes and Subsystems

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.

A high-level Godot Engine architecture diagram showing separated Core, Scene, Servers, and Drivers layers. It illustrates that scene objects, domain services such as rendering or physics, and platform-facing drivers can have distinct responsibilities even when they collaborate at runtime.

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:

Name is responsible for strong verb + domain object + outcome.

Good statements are specific enough to rule things out:

  • TransformComponent is responsible for maintaining an actor’s local spatial state and derived world transform.
  • CollisionWorld is responsible for answering collision and movement-resolution queries against world colliders.
  • Renderer is responsible for turning renderable world state and an active camera into OpenGL draw submissions.
  • EditorCameraController is responsible for updating editor-only navigation from editor input.
  • PhotonAdapter is responsible for translating between the engine’s networking boundary and Photon callbacks/messages.

Weak statements merely repeat a name:

  • “Renderer handles rendering.”
  • “Scene manages the scene.”
  • “Actor does 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:

  1. Input versus player controller. Raw key and mouse state are not movement rules. If PlayerMovementComponent calls GLFW directly today, write that as evidence of mixed responsibilities. Do not necessarily add a large input-action framework yet.

  2. 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.

  3. 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.

  4. 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.

  5. 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