Create your own
Lesson illustration

Defining Architectural Constraints for Refactoring

Good to see you again. You have mapped responsibilities, identified work that has collected inside Scene, and looked for cycles and broad two-way knowledge. That gives you a realistic picture of where a refactor could be risky. Now you need the guardrails that tell you whether the refactor is still preserving the engine you already have.

This lesson defines those guardrails as architectural constraints: observable behaviors and boundaries that must remain true while code moves. For your engine, the important ones are not abstract “quality” statements. They include entering play mode, using the editor and character cameras correctly, retaining the multiplayer path through Photon, and producing a working packaged build.

Plan for roughly 40 minutes: about 10 minutes of focused resources, then 25–30 minutes producing a first constraints register for your repository.


Constraints are promises, not hopes

A refactor changes structure without intentionally changing externally meaningful behavior. The problem is that “don’t break the engine” is too vague to guide a safe change.

Compare these two statements:

  • Vague: “Multiplayer must still work.”
  • Useful: “Two packaged builds using the current Photon setup can connect through the existing flow and exchange the currently supported game event without a crash, protocol mismatch, or unexpected disconnect.”

The useful version identifies an observable scenario, not an implementation. It does not say whether Scene, a networking adapter, or a future replication system owns the work. That is deliberate: a constraint protects behavior while leaving room to improve the design.

A constraint should answer five things:

FieldPurpose
PromiseWhat must remain true?
ScenarioWhat action demonstrates it?
Expected resultWhat does success look like?
Evidence/checkHow will you verify it?
Cadence and ownerWhen is it checked, and who maintains it?

This is closely related to an architectural fitness function: a repeatable check of an architectural characteristic that matters during incremental change.

Fitness Functions – Safeguard Architecture with Automated Checks

Read this Continuous Architecture practice to see how important qualities become explicit, measurable safeguards rather than informal intentions.

In the opening section, “Fitness functions or how to protect key characteristics from your product,” read the fitness-function rationale. Then go to “Defining a fitness functions is then a 3 step.” Read the selection reasoning, followed by the measurement fields. Focus on the distinction between choosing what matters and deciding how often it can realistically be checked.

A check does not need to be fully automated to be valuable. Your current engine may not yet have a test harness capable of launching two Photon clients or validating an exported build. A reliable written smoke test is still a legitimate constraint. The aim is to make the check repeatable enough that a future failure is unambiguous.

A five-step continuous-architecture process: identify architectural stakes, choose the non-functional requirements that matter, define what and how to measure, assign accountability, and track the checks during ongoing work.

For now, avoid turning your current performance observations—package size, RAM use, GPU use—into primary gates. You explicitly want to focus on structure rather than optimization. Record those numbers as context if useful, but only create a performance constraint when you have decided that a particular limit protects the product rather than distracting from the refactor.


Separate preservation constraints from target architecture rules

Your constraints register needs two categories.

1. Preservation constraints

These protect the working capabilities you already rely on. They are usually phrased as user-visible scenarios:

  • Can you enter and leave play mode safely?
  • Can you use both the editor camera and character camera in their intended modes?
  • Does the current movement, jump, speed boost, collision, and rendering path still work?
  • Can current multiplayer participants connect and perform the behavior presently supported?
  • Does the exported build run outside the IDE with its required content?

These are release and refactoring safety nets.

2. Target architecture rules

These protect the direction of the refactor. They concern code boundaries rather than player-visible behavior:

  • The core runtime must not depend on editor-only headers or ImGui panels.
  • A packaged game target must be buildable without editor-only code being required.
  • Photon SDK calls should gradually be isolated behind a focused networking integration boundary.
  • Components should not gain new unrestricted Scene& access merely because it is convenient.
  • Concrete service construction should become concentrated in startup or composition code rather than spreading further through Scene.

These may not all be true today. Therefore, do not pretend they are immediate pass/fail gates. Use a ratchet rule:

  1. Record the current baseline: for example, the known files where runtime code includes editor headers.
  2. Set the immediate threshold: no new violations during refactoring.
  3. Set the eventual target: zero violations after the relevant migration is complete.

That lets architecture improve incrementally without blocking all work because the old design still exists.

The distinction matters. “A game package launches and renders the ground” is a preservation constraint. “The package does not require the editor target” is a structural target rule. Both support the same long-term outcome, but they catch different failures.


Define the engine’s behavior in terms of real journeys

The Cherno’s discussion of engine, editor, and shipped game is useful here because it focuses on the different audiences and applications involved. The editor can use runtime capabilities, but the shipped game should serve the player without exposing authoring workflow.

Game Engine Architecture 101 // Code Review

Watch these short excerpts from “Game Engine Architecture 101 // Code Review” by The Cherno to frame editor functionality and the shipped runtime as related but distinct uses of shared engine capabilities.

First watch the boundary critique, beginning where he discusses mixing game and engine concerns. Then skip to editor and runtime, which explains why an editor can share runtime code while remaining distinct from the final player-facing application. Use this to evaluate dependency direction, not as a demand to split your current project into separate libraries immediately.

Now translate the current prototype into a set of journeys. The exact names and input bindings should match your engine; the entries below are a strong starting point based on the functionality you described.

Play-mode constraint

The refactor must preserve a coherent playable session.

Promise: Starting play mode still creates or activates the current intended runtime state, renders the scene, and allows the cube character to move, jump, use the shift-based speed boost, and interact with the ground through the current collision behavior.

Verification scenario:

  1. Open the known playable test scene.
  2. Enter play mode through the same route used today.
  3. Confirm the expected character camera behavior.
  4. Move in each supported direction, rotate the camera, jump, and use the speed boost.
  5. Confirm that the character remains grounded correctly and the frame renders without a crash or visibly missing world content.
  6. End play mode or return to the editor according to the engine’s existing workflow.

The important constraint is the experience, not the current call chain. During the refactor, Scene::Update, camera selection, collision dispatch, and render submission may move. The check remains the same.

Editor-camera constraint

Your editor camera is not merely “another camera component.” It supports an authoring workflow, while the character camera supports a runtime gameplay view. Treat their distinction as something the architecture must preserve.

Promise: In editor mode, the editor camera remains controllable through the current editor controls, and editor-only camera state remains usable after entering and leaving play mode.

Verification scenario:

  1. Open the test scene in edit mode.
  2. Navigate with the editor camera and verify that its intended controls operate.
  3. Enter play mode, use the gameplay/character camera as intended, then return to edit mode.
  4. Verify that the editor camera can still be selected and controlled, with no stale pointer, invalid camera reference, or accidental coupling to player state.

Do not invent a behavior that the engine does not currently have. For example, whether leaving play mode restores the editor camera to the exact previous position is a product decision. If that is how your engine behaves today and you value it, write it as a constraint. If it is not yet defined, mark it decision pending, rather than silently treating a later change as a regression.

Multiplayer constraint

Networking needs a narrower definition than “the Photon code compiles.” Compilation does not prove that callbacks still reach the right runtime state, that object lifetimes are safe, or that a packaged build contains compatible networking configuration.

Promise: The existing Photon integration remains capable of connecting the currently supported number of participants and performing the currently demonstrated multiplayer behavior.

Use one known multiplayer scenario. For example:

Constraint fieldRecord for your engine
EnvironmentTwo machines or two instances, required Photon settings, network availability
SetupLaunch the current multiplayer-capable build and follow the existing connection flow
ActionJoin the same room/session and perform the multiplayer action your prototype presently supports
Expected resultBoth participants reach the expected connected state and see the currently supported shared result
FailureConnection failure, crash, callback into destroyed scene data, duplicate actor, desynchronization of the supported behavior, or unexpected disconnect
EvidenceA short screen recording, console log, or checklist dated with the tested commit

Be exact about what has truly been demonstrated. If the present prototype proves connection establishment but not replicated jumping, constrain connection establishment now. Add replication constraints later only after that behavior exists. A constraint register should describe reality and intended guarantees, not aspirational features.

Packaging constraint

A packaged build is an architectural test because it exposes hidden editor assumptions, hard-coded paths, missing assets, missing runtime DLLs, and accidental dependence on development-only initialization.

Promise: The current export/package process produces a standalone build that launches outside the IDE and reaches a playable rendered scene with the assets and runtime dependencies required by the current prototype.

A practical smoke check is:

  1. Produce the package using the current documented build/export path.
  2. Launch the executable from its output folder, not from Visual Studio.
  3. Confirm the window opens, the expected scene content renders, and the player controls work.
  4. If multiplayer is intended in the packaged build, run the defined multiplayer scenario from that package.
  5. Record the commit and result.

At this stage, “package successfully” should mean more than “an executable exists.” The executable must start and load the resources needed for the intended player path.


Turn each promise into a check with a sensible cadence

Not every constraint belongs in every build. A two-client network test and package smoke test may be too slow or environment-dependent to run after each rename. Conversely, compile errors and new editor dependencies should be caught immediately.

Use three verification levels:

LevelTypical checksWhen to run
Local change checkCompile, launch the normal editor path, targeted test where one exists, inspect whether a changed runtime file now includes editor codeAfter a small refactoring step
Migration smoke checkPlay mode, editor camera, movement, collision, renderer, and any affected feature pathAfter completing one coherent migration or before committing it
Integration checkpointTwo-client Photon scenario, package creation and launch, full playable-scene smoke testBefore merging a larger change, switching refactor branches, or declaring a milestone complete

For automated checks, start small. A unit test for a transform calculation is valuable, but it does not prove that play mode works. An integration test that creates a scene and validates active-camera selection is stronger, but may require seams that your current code lacks. A manual smoke test is appropriate while those seams are still being created.

This is the practical meaning of staying “green” during refactoring: each change gets the fastest relevant feedback available, and larger changes are not allowed to accumulate without running the slower checks.

Testing and Refactoring Legacy Code

Use these excerpts from Sandro Mancuso’s “Testing and Refactoring Legacy Code” to reinforce the safety principle behind the constraints register: protect existing behavior first, then make small structural changes with frequent feedback.

Watch the refactoring setup for the argument for learning and protecting behavior incrementally. Later, watch small green steps. Focus on the discipline of making the smallest useful change and returning quickly to a known working state; the exact Java tooling in the demonstration is not relevant to your C++ engine.


Build your first refactor guardrail register

Create this file now:

docs/architecture/refactor-guardrails.md

Start with the following structure. Replace bracketed wording with names, scenes, inputs, and current behavior from your repository.

# Refactor Guardrails

## Scope

These constraints protect the current playable prototype while engine
responsibilities and dependencies are refactored. They are not feature requests.

## Preservation Constraints

### G-01: Play mode remains playable

- Status: active
- Owner: [your name]
- Trigger: after any change to frame flow, scene, camera, input, collision, or rendering
- Scenario: Open [test scene], enter play mode, use [character camera mode].
- Expected: [movement directions], jump, [speed boost], collision with [ground],
  and rendering behave as they do at baseline.
- Failure: crash, missing scene content, wrong active camera, broken movement,
  broken collision, or inability to end play mode.
- Evidence: [manual smoke checklist, automated test, or recording]
- Baseline: verified at commit [commit hash/date].

### G-02: Editor camera remains independent

- Status: active
- Trigger: after changes to cameras, mode switching, Scene, editor code, or lifetimes
- Scenario: Navigate in edit mode, enter play mode, return to edit mode.
- Expected: editor camera can be selected and controlled according to current behavior.
- Failure: stale camera reference, crash, editor camera uses player state unexpectedly,
  or editor controls affect gameplay state in edit mode.
- Evidence: [manual smoke checklist]
- Baseline: verified at commit [commit hash/date].

### G-03: Current multiplayer path remains available

- Status: active
- Trigger: after changes to networking, actor creation, scene lifecycle, callbacks,
  serialization, or packaged-build configuration
- Scenario: Run two [editor/package] instances through [current Photon connection flow].
- Expected: both reach [connected/room state] and demonstrate [currently supported action].
- Failure: connection failure, crash, unexpected disconnect, duplicate runtime object,
  invalid callback target, or failure of the supported shared behavior.
- Evidence: [log/screen recording/checklist]
- Assumptions: Photon service and network are available.
- Baseline: verified at commit [commit hash/date].

### G-04: Packaged build remains runnable

- Status: active
- Trigger: after changes to startup, assets, build configuration, platform integration,
  runtime/editor boundaries, or networking configuration
- Scenario: Create the current package and launch it from the output directory.
- Expected: it launches outside the IDE, loads [test scene/assets], renders, and accepts
  current player input. Run G-03 if multiplayer is included in the package.
- Failure: missing file or DLL, startup failure, blank or broken render, or editor-only
  initialization required to play.
- Evidence: [package path and checklist]
- Baseline: verified at commit [commit hash/date].

## Target Architecture Rules

### A-01: Runtime does not gain editor dependencies

- Status: ratchet
- Baseline: [list known runtime-to-editor includes or references]
- Immediate threshold: no new runtime dependency on ImGui/editor-only types.
- Target: runtime/game package can be built without requiring editor implementation.
- Check: code review now; later add include/dependency analysis.

### A-02: Photon SDK knowledge is contained

- Status: ratchet
- Baseline: [list current files that directly use Photon]
- Immediate threshold: no new direct Photon calls in gameplay components.
- Target: Photon-specific callbacks and types are confined to a networking adapter boundary.
- Check: search for Photon SDK types and review changed files.

### A-03: No new unrestricted Scene access

- Status: ratchet
- Baseline: [existing components/services storing Scene pointers or references]
- Immediate threshold: do not introduce new broad Scene access without recording why.
- Target: systems receive the smallest capability or data view they need.
- Check: constructor and member-field review.

Keep one rule per heading. This makes the record easy to update, and it prevents a statement such as “camera/editor/networking must work” from hiding three different failure modes.

For the first pass, spend most of your effort on baseline evidence. Run each scenario against the current main branch or a known working commit. Note where it was tested, what you observed, and any environmental assumption. If something is already flaky, do not label it “working” without qualification; record the flakiness and make it a risk to address before relying on the check.

A constraint is only trustworthy when a future version can be judged against the same scenario.


Use constraints to decide whether a proposed change is safe enough

Before moving a responsibility out of Scene, identify which guardrails could be affected.

Proposed refactor areaLikely guardrailsTypical hidden risk
Move frame update coordinationG-01, G-02, G-03Update ordering changes; callbacks occur after a scene or actor is destroyed
Change camera selection/storageG-01, G-02Editor and character cameras select the wrong state or hold invalid references
Extract collision serviceG-01Registration/query order changes; movement no longer receives expected collision results
Isolate Photon codeG-03, G-04, A-02SDK callbacks retain stale world objects; package omits configuration or runtime libraries
Split editor-facing codeG-02, G-04, A-01Runtime still relies on ImGui initialization or editor state
Replace broad Scene accessG-01, G-02, G-03, A-03A formerly hidden capability is not supplied explicitly at the new boundary

This is not a demand to run all four preservation checks after every line change. It is a way to choose the correct checkpoint before making the change.

Suppose you extract camera switching from Scene. The smallest safe cycle is:

  1. Record the current camera-selection behavior in G-01 and G-02.
  2. Make one focused structural change.
  3. Compile and run the camera smoke scenario.
  4. Commit if it passes.
  5. Run packaging only when the migration has reached a meaningful integration point or touched startup/build boundaries.

This rhythm protects the engine without making refactoring feel like a full release process every day.


Key takeaways

Architectural constraints are explicit promises that preserve the engine while its structure changes. They should describe observable scenarios, expected outcomes, evidence, cadence, and assumptions—not the current implementation that you intend to replace.

Your first active preservation constraints should cover:

  • entering and leaving a playable runtime session;
  • maintaining the intended distinction between editor-camera and character-camera behavior;
  • preserving the multiplayer behavior that Photon demonstrably supports today;
  • producing and launching a functioning packaged build outside the IDE.

Alongside them, use ratchet-style target rules to prevent new runtime-to-editor dependencies, new broad Scene access, and further leakage of Photon SDK details into gameplay code.

Next, the course moves into ownership and lifetimes. You will use these guardrails while distinguishing true ownership from temporary access in the engine object graph—a necessary step before relocating major responsibilities safely.

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

Sign up