Create your own
Lesson illustration

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:

RolePrimary jobLikely engine examples
Information holderMaintains and provides information about one conceptTransform, CameraSettings, Material, collider shape
StructurerMaintains relationships or membershipScene/world object collection, actor-component container, resource registry
Service providerPerforms a domain operation on requestrenderer, collision world, resource loader
CoordinatorReacts to an event or phase and delegates workruntime frame coordinator, play-mode controller
ControllerMakes higher-level decisions and directs othersplayer controller, editor command controller
InterfacerTranslates requests/data across boundariesGLFW 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:

ClassificationTestExamples in your project
Platform layerWould it change if GLFW, the operating system, or graphics API changed?window creation, raw keyboard/mouse access, OpenGL context and presentation
Reusable runtime engineCould another 3D game use it without inheriting your game’s rules?transforms, scene membership, mesh/material representation, rendering submission, collision queries
Game-specific runtimeDoes it encode the particular game’s player behavior, rules, or content?player movement tuning, jumping rule, Shift sprint behavior, cube-character setup
Editor-onlyDoes it exist to author, inspect, select, or navigate content rather than to run the shipped game?editor camera, selection state, gizmos, inspection panels
Integration boundaryDoes 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:

ItemCurrent primary responsibilityEvidenceResponsibilities that do not fit cleanly
SceneCoordinates the current prototype’s world frameScene::Update() invokes all listed phasesraw input processing, collision implementation, active-camera policy, rendering
RendererSubmits visual state to OpenGLcalled by Scene::Render(); owns draw codenone observed yet
PlayerMovementComponentApplies player movement and jump rulesreads movement action state and changes velocitydirect raw GLFW calls, if present
Collision... codeResolves collider movement/contactcalculates collision and grounded statenone 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/subsystemRolePrimary responsibilityAuthoritative state or decisionsCollaboratorsExplicitly not responsible forEvidence
Application / EngineCoordinatorRun application lifecycle and long-lived runtime servicesstartup, shutdown, frame boundarywindow, input, scene, renderergame movement rulesfile:function
Window/platform adapterInterfacerTranslate OS/window-library events into platform statewindow status, native eventsinput, renderer backendplayer action meaningfile:function
Input layerInterfacer / service providerExpose raw device state or named actionskey/mouse state; possibly action stateplatform, player/editor controllersplayer movement responsefile:function
SceneStructurer or coordinatorWrite what it actually does most centrallyactor membership, update ordering, or bothactors, serviceslist observed overreachfile:function
ActorStructurerRepresent one world identity and its attached componentscomponent membership, identitycomponents, sceneglobal rendering/collisionfile:function
Component baseInterfacerDefine the common lifecycle/attachment contract for componentscomponent interfaceactor, concrete componentsgame behavior itselfheader:function
TransformComponentInformation holderMaintain local pose and derived spatial dataposition, rotation, scale, matrix/dirty stateactor, camera, renderer, collisiongame movement decisionsfile:function
Collision componentInformation holderDescribe an actor’s collision shape and settingsshape, bounds, collision settingscollision worldworld-wide collision traversalfile:function
Collision serviceService providerEvaluate and resolve collision queriescontact/manifold or resolution resultcolliders, movement coderendering and inputfile:function
Character/player movementControllerApply this game’s movement, jump, and sprint rulesdesired movement, velocity, jump stateinput/action state, transform, collisioneditor navigation, GPU callsfile:function
CameraInformation holderRepresent a view and projection configurationpose, FOV, near/far, projectionrenderer, camera controllereditor-mode selectionfile:function
Spring armService provider / componentDerive a camera mount from a target and obstruction resultarm length, desired camera posetarget transform, collision service, cameraactive-camera policyfile:function
RendererService providerSubmit visible scene data to OpenGLrender pass state, draw submissioncamera, mesh/material, transformsplayer/game-rule decisionsfile:function
ImGui integrationInterfacerBridge the UI library into the window/render frameUI frame and capture stateplatform, renderer, editor UIgameplay decisionsfile:function
Editor camera/selectionControllerSupport authoring navigation and selected-object stateeditor pose, selectioneditor UI, scene queriesruntime player controlfile:function
Photon integrationInterfacerTranslate Photon communication into engine-facing messages/stateconnection and callback boundarygame replication logicgeneral actor update policyfile: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 subsystemChanges when…Assessment
TransformComponenttransform representation, hierarchy behavior, or matrix caching changescoherent spatial responsibility
Renderershader/material API, render pass, or OpenGL submission changescoherent rendering responsibility
PlayerMovementComponentjump rules, sprint tuning, acceleration, or player controls changecoherent game-play responsibility
Sceneactor storage, level loading, collision algorithm, rendering API, editor selection, and input routing all changeprobably mixed
Actoradding a component, changing rendering code, resolving collision, and selecting ImGui panels all require editsalmost 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