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 world | the implementation of all actor behavior |
| provide scene-local context such as level metadata | a platform input adapter |
| coordinate broad frame phases | the collision algorithm |
| expose narrow scene queries or registration points | an OpenGL renderer |
| participate in loading/unloading a world | the owner of editor UI state |
| provide world data to specialized services | a 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:
SceneorWorldmaintains 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 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 responsibility | Why it does not primarily belong to Scene | Likely eventual home | What Scene may still do |
|---|---|---|---|
| Poll GLFW/window input | Depends on window library and device APIs | platform input adapter or input subsystem | provide the runtime phase in which input is updated |
| Interpret WASD, Shift, and jump | Encodes your game’s player rules and tuning | player controller or movement component | contain the player actor |
| Move against colliders and determine contacts | Uses world-wide collision data and collision algorithms | collision world / physics service | make colliders available for registration or queries |
| Update transform hierarchy or matrices | Concerns spatial-state propagation, not scene membership alone | transform service or transform-component logic | coordinate update order if necessary |
| Pick character camera versus editor camera | Depends on play/edit mode and editor workflow | play-mode controller or editor controller | provide candidate runtime cameras |
| Bind shaders and issue draw calls | Depends on OpenGL and render-pipeline decisions | renderer | supply a narrow view of renderable world data |
| Build ImGui panels and store selection state | Exists for authoring, inspection, and debugging | editor layer | expose scene data through editor-safe queries |
| Process Photon callbacks | Depends on an external networking SDK | Photon adapter / networking boundary | provide game-world operations requested by replication logic |
| Decide what actor state is replicated | Encodes your game’s networking policy | game-specific replication system | identify actors or components eligible for replication |
| Load meshes, textures, or shaders on demand | Concerns assets and GPU-resource management | asset/resource service | retain 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.
Scenemay 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 Scene | Evidence | Domain | Decision | Proposed destination | Scene role after extraction | Risk / open question |
|---|---|---|---|---|---|---|
| Raw key/mouse polling | Scene::Update() calls GLFW functions | platform input | Move | PlatformInput | runs after input state has been updated | define action-input boundary later |
| Shift sprint and jumping | player logic in Scene::ProcessInput() | game runtime | Move | PlayerController or movement component | owns/contains player actor only | who owns velocity and grounded state? |
| Collision sweep and response | collision loop in Scene::Update() | collision runtime | Move | CollisionWorld | makes world colliders queryable | lifetime of collider registration |
| OpenGL mesh drawing | Scene::Render() binds material and draws | rendering | Move | Renderer | supplies renderable world view | render-data access must stay narrow |
| Editor camera switching | branch on editor/play state in Scene | editor/runtime mode | Move | editor/play-mode controller | exposes runtime camera candidates | preserve current editor camera behavior |
| Actor membership | std::vector of actors | world structure | Keep for now | Scene / World | maintains loaded-world membership | confirm 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 early | Better |
|---|---|
“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:
- Photon adapter: receives SDK callbacks, handles connection-facing events, and translates messages to engine-facing data.
- 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:
- a list of every nontrivial
Scenemethod and major state field; - one ledger row for each responsibility found there;
- a Keep, Move, or Defer decision for each row;
- a proposed domain destination for every Move entry;
- 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