Create your own
Lesson illustration

Refactoring Scene Class Responsibilities

Welcome back. In the previous lesson, you mapped each major class or subsystem to a coherent primary responsibility, recorded its authoritative state, and noted where responsibilities were already mixed. You now have the evidence needed to examine the most likely pressure point: Scene.

This lesson is still a design audit, not a coding task. The goal is to identify each responsibility currently performed by Scene, decide whether it genuinely belongs there, and name the subsystem that should eventually own it. By the end, you will have a practical Scene extraction ledger: a list of responsibilities to keep, move, or defer until ownership and dependency questions are clearer.


Scene is not supposed to mean “everything involved in a scene”

A Scene is allowed to be important. It can represent a loaded world, structure actor membership, hold scene-local data, and perhaps coordinate high-level lifecycle phases. What causes trouble is treating it as the place where every feature happens merely because that feature happens in a scene.

For example, collision occurs within a scene, but collision detection and resolution are still a physics/collision-domain responsibility. Rendering depicts a scene, but issuing OpenGL commands is a rendering responsibility. An editor camera may look at a scene, but its navigation and selection behavior are editor responsibilities.

A useful distinction is:

Scene may reasonably…Scene should not become…
maintain the membership and identity of objects in one loaded worldthe implementation of all actor behavior
provide scene-local context such as level metadataa platform input adapter
coordinate broad frame phasesthe collision algorithm
expose narrow scene queries or registration pointsan OpenGL renderer
participate in loading/unloading a worldthe owner of editor UI state
provide world data to specialized servicesa universal service locator

The important word is coordinate. A coordinator chooses when a phase occurs and supplies the relevant context. It should not quietly absorb the specialist logic of every phase.

For your prototype, a healthy eventual distinction might look like this:

  • Scene or World maintains which actors and components belong to the loaded gameplay world.
  • A collision service evaluates movement against the world’s colliders.
  • A renderer turns visible renderable data plus a chosen camera into draw submissions.
  • player code decides what “jump,” “sprint,” and directional movement mean.
  • editor code manages the editor camera, selected object, gizmos, and ImGui panels.
  • integration code translates GLFW, OpenGL, ImGui, and Photon behavior at their respective boundaries.

This does not imply that you need to imitate a large engine, adopt ECS, or create a class for every noun. It means each domain should have an honest home.

Godot’s architecture diagram separates the scene-facing layer from specialized servers such as rendering, physics, audio, and navigation, and separates those from platform-facing drivers. Use it as an example of boundary placement, not as a blueprint to reproduce.

Godot's architecture overview — Godot Engine (4.6) documentation in English

Read Godot Engine’s short architecture overview as a boundary example. The value here is not its specific class hierarchy or use of singleton servers, but the distinction it makes between scene structure, reusable domain services, and platform-facing code.

In the “Scene Layer” section, read the scene-layer description. Then read “Server Layer,” especially the server boundary. Finish by skimming “Drivers / Platform Interface” and notice that graphics APIs and operating-system details are below the scene-facing layer, not embedded in it.

A smaller C++ engine can use ordinary objects and explicit dependencies instead of Godot-style servers. The architectural lesson remains: the object that structures a world is not automatically the right place to implement every reusable subsystem that operates on that world.


Use the “reason to change” test to find what must leave

Take every field and method in Scene, plus every function it calls directly during a frame. For each one, ask:

What change would require this code to change?

If the answer is “a change to world composition or scene lifecycle,” the responsibility may belong in Scene. If the answer involves graphics, raw input, player rules, an SDK, or editor workflow, it is probably misplaced.

For instance, imagine a current update function conceptually does all of this:

void Scene::Update(float deltaTime)
{
    PollKeyboardAndMouse();
    UpdatePlayerMovement(deltaTime);
    ResolveCollisions(deltaTime);
    SelectActiveCamera();
    DrawImGui();
    RenderMeshes();
    ProcessPhotonCallbacks();
}

This tells you Scene has at least seven different reasons to change. The fact that these calls are in one function does not prove every call must be removed immediately. It does prove that you should identify the proper authority for each one.

Use this classification table while reading your own Scene code.

Current Scene responsibilityWhy it does not primarily belong to SceneLikely eventual homeWhat Scene may still do
Poll GLFW/window inputDepends on window library and device APIsplatform input adapter or input subsystemprovide the runtime phase in which input is updated
Interpret WASD, Shift, and jumpEncodes your game’s player rules and tuningplayer controller or movement componentcontain the player actor
Move against colliders and determine contactsUses world-wide collision data and collision algorithmscollision world / physics servicemake colliders available for registration or queries
Update transform hierarchy or matricesConcerns spatial-state propagation, not scene membership alonetransform service or transform-component logiccoordinate update order if necessary
Pick character camera versus editor cameraDepends on play/edit mode and editor workflowplay-mode controller or editor controllerprovide candidate runtime cameras
Bind shaders and issue draw callsDepends on OpenGL and render-pipeline decisionsrenderersupply a narrow view of renderable world data
Build ImGui panels and store selection stateExists for authoring, inspection, and debuggingeditor layerexpose scene data through editor-safe queries
Process Photon callbacksDepends on an external networking SDKPhoton adapter / networking boundaryprovide game-world operations requested by replication logic
Decide what actor state is replicatedEncodes your game’s networking policygame-specific replication systemidentify actors or components eligible for replication
Load meshes, textures, or shaders on demandConcerns assets and GPU-resource managementasset/resource serviceretain asset references used by scene content

Two subtleties matter here.

Scene data is not the same as scene behavior

A Scene may hold actors with MeshComponent, TransformComponent, and collision components. It does not follow that it should render meshes, calculate every transform, or resolve every collision itself.

Likewise, a Scene can contain a player actor without owning the meaning of “the player presses Shift.” Your sprint behavior is game-specific. A reusable engine layer should not need to know that your cube character has a particular speed boost key or jump tuning.

Calling a subsystem is not the same as implementing it

It is not automatically wrong for Scene to call renderer.Render(...) as part of a top-level frame coordinator. The problem is when Scene itself contains shader binding, mesh iteration rules, OpenGL state management, and draw calls.

Think in terms of authority:

  • The renderer has authority over how visual data becomes GPU work.
  • The collision subsystem has authority over how movement and contacts are evaluated.
  • the player controller has authority over what the player intends to do.
  • Scene may have authority over which objects exist in this loaded world.

That distinction lets your coordinator remain thin rather than becoming a dumping ground.

Component · Decoupling Patterns

Read the “Component” chapter from Game Programming Patterns to reinforce why domain boundaries matter. Although its example is a monolithic game entity rather than a monolithic Scene, the failure mode is the same: input, physics, graphics, and other domains become difficult to change when their code is mixed together.

In “Motivation,” begin at the domain-isolation argument. Continue through “The Gordian knot,” “Cutting the knot,” and “Tying back together.” Focus on the reason for separating domains: each can change without requiring knowledge of unrelated domains. Do not treat this as a requirement to convert your actor-component design into a different architecture.


Build a Scene extraction ledger from evidence

Create this beside your existing responsibility map:

docs/architecture/scene-extraction-ledger.md

Start from evidence, not from an imagined final architecture. Search for:

  • methods implemented in Scene.cpp;
  • state fields in Scene.hpp;
  • calls made from Scene::Update, Tick, Render, loading functions, or editor-mode functions;
  • references to GLFW, OpenGL, ImGui, Photon, player-specific classes, and camera-switching logic;
  • objects constructed or destroyed directly by Scene.

For each item, make one entry in this form:

Responsibility now in SceneEvidenceDomainDecisionProposed destinationScene role after extractionRisk / open question
Raw key/mouse pollingScene::Update() calls GLFW functionsplatform inputMovePlatformInputruns after input state has been updateddefine action-input boundary later
Shift sprint and jumpingplayer logic in Scene::ProcessInput()game runtimeMovePlayerController or movement componentowns/contains player actor onlywho owns velocity and grounded state?
Collision sweep and responsecollision loop in Scene::Update()collision runtimeMoveCollisionWorldmakes world colliders queryablelifetime of collider registration
OpenGL mesh drawingScene::Render() binds material and drawsrenderingMoveRenderersupplies renderable world viewrender-data access must stay narrow
Editor camera switchingbranch on editor/play state in Sceneeditor/runtime modeMoveeditor/play-mode controllerexposes runtime camera candidatespreserve current editor camera behavior
Actor membershipstd::vector of actorsworld structureKeep for nowScene / Worldmaintains loaded-world membershipconfirm ownership next module

Use three decision labels only:

  • Keep: clearly part of loaded-world structure or high-level coordination.
  • Move: clearly belongs to another domain, even if you do not yet know the exact class name.
  • Defer: its correct home depends on an ownership, lifetime, or dependency decision you have not yet made.

“Defer” is not avoidance. It prevents premature restructuring. For example, collision code may obviously need to leave Scene, but whether your collision service owns registered collider proxies, borrows component data, or uses IDs is an ownership question for the next module.

A proposed destination should normally name a domain first, then a class only if the class already exists:

Too earlyBetter
“Make ScenePhysicsManager.”“Collision-domain service; exact type deferred.”
“Put it in Actor.”“Game-specific player controller.”
“Create a CameraManager.”“Editor play/edit-mode selection controller.”
“Use an ECS system.”“A rendering subsystem that consumes renderable scene data.”

This protects you from merely relocating the same mixed responsibility into a newly named “manager.”


High-confidence removals in your current prototype

Based on the systems you described, these are strong candidates to mark Move if they are currently implemented inside Scene.

Platform input and window behavior

Scene should not directly own GLFW polling, window-close decisions, mouse capture implementation, or raw device state. Those change when the platform library changes.

A small input boundary is enough for now. It might expose raw key/mouse state, or it might expose named actions. The important boundary is that player movement and editor navigation do not call GLFW throughout their own logic.

Player behavior

Your cube character’s directional movement, jumping, and Shift speed boost are game behavior. They should not be general scene behavior, because another game using the engine might have a vehicle, strategy-unit selection, or no player character at all.

A movement controller can request collision-aware movement and update the player’s state. It should not need unrestricted access to every actor or renderer in the scene.

Collision implementation

A collider component describes a particular actor’s collision shape and settings. A collision world or collision service performs queries across multiple colliders and produces contacts or movement results.

Scene may retain, directly or indirectly, the set of actors that contain colliders. But collision traversal, broad-phase structures, contact generation, and resolution rules should not be embedded in Scene.

Rendering and OpenGL

A mesh-rendering component normally describes what an actor can render: mesh, material, visibility flags, perhaps render-layer data. The renderer owns the cross-actor work: selecting a camera, gathering renderables through a narrow interface, setting GPU state, and submitting draw calls.

Do not replace direct OpenGL calls in Scene with Scene::GetRenderer()->... everywhere. That merely turns Scene into a service locator. Prefer an explicit frame coordinator that has deliberate access to the renderer, scene/world, and camera choice.

Editor-only camera and ImGui state

Your editor camera is useful, but it is not runtime-world state. The same applies to selected actors, inspector panels, gizmo state, and editor-only shortcuts.

The editor can inspect and render the same game-world data used at runtime, but it should hold its own authoring state. That separation is particularly important when you enter play mode: editor navigation should not mutate the runtime player transform simply because both happen to use camera-like objects.

Game Engine Architecture 101 // Code Review

Watch these short excerpts from Game Engine Architecture 101 // Code Review by The Cherno for a practical distinction between game code, shared engine/runtime code, and editor tooling. The review is intentionally opinionated; use its boundary questions, not its exact proposed project structure.

Watch engine versus game for the argument that game-specific behavior should not be silently treated as engine code. Then watch editor and runtime, followed by the shared core. Relate this specifically to your editor camera and ImGui tools: they can use runtime capabilities without becoming responsibilities of Scene.

Photon integration and replication policy

Keep two responsibilities distinct:

  1. Photon adapter: receives SDK callbacks, handles connection-facing events, and translates messages to engine-facing data.
  2. Replication policy: decides which game objects are networked, which state is sent, and how remote players are represented.

Both may currently be near Scene; neither should become an indistinguishable part of its update logic. Mark them as separate ledger entries even if they are currently in the same source file.


What should remain in Scene—at least for now

The goal is not an empty Scene class. It is a class with a narrow, defensible job.

Until the ownership module gives you stronger lifetime rules, these are reasonable keep or defer candidates:

  • membership of actors/components in a loaded world;
  • scene-local metadata such as a level name, serialized object graph, or environment settings;
  • registration points for scene-local objects, such as adding/removing actors;
  • high-level coordination of lifecycle phases;
  • narrow queries that specialized systems need to perform their jobs.

A thin coordinator might establish the frame’s order, such as input update, gameplay update, collision processing, transform propagation, rendering, and editor presentation. But the coordinator should delegate each phase to the responsible subsystem. It should not contain the domain algorithms itself.

Be especially wary of this apparent fix:

scene.GetRenderer();
scene.GetCollisionWorld();
scene.GetInput();
scene.GetNetworkManager();
scene.GetEditor();

If every component receives Scene& and can retrieve anything, the code has not become meaningfully decoupled. The global access route is simply hidden behind a nicer name. Later lessons will define narrower interfaces and safer component communication; for now, record each place where Scene& is passed broadly as a coupling risk.


A sensible stopping point for this lesson

After about 35–45 minutes, you should have:

  1. a list of every nontrivial Scene method and major state field;
  2. one ledger row for each responsibility found there;
  3. a Keep, Move, or Defer decision for each row;
  4. a proposed domain destination for every Move entry;
  5. explicit open questions where moving code would require an ownership or lifetime decision.

Do not move code yet. In particular, avoid changing camera ownership, actor storage, collider registration, or Photon object references before you have mapped the bidirectional dependencies they may create.


Key takeaways

Scene can structure a loaded world and coordinate broad phases, but it should not become the implementation home for input, player rules, collision, rendering, editor UI, platform code, or networking-SDK callbacks.

Use the reason-to-change test to identify overreach. If a responsibility changes because of OpenGL, GLFW, Photon, ImGui, player movement design, or collision algorithms, it belongs to a more focused subsystem. Record the result in a Scene extraction ledger, using Keep, Move, and Defer rather than forcing uncertain decisions.

Next, you will inspect the current dependencies around these proposed moves and detect circular dependencies or bidirectional knowledge before any refactoring begins.

Can't find a good explanation? Sign up and we'll make it for you

Sign up