![]() |
vkmEngine 1.0.0
A C++ game engine · vkmengine.com
|
Where a change belongs, what shape it takes, and what "finished" means. engine.md says what you are building and inside which limits; this says where your change goes within it. The bar for the code itself is implementation.md.
Before writing anything new, find the closest sibling and match it. Not because consistency is pleasant, but because predictability is the second thing the engine optimises for: a reader who can guess the shape of the next file works quickly and is right.
A new thing that cannot take the shape of its siblings is a signal: you have misread the pattern, or you have a genuinely new kind of problem. The second is rare; check before assuming it.
A system that writes a pointer onto FrameContext owes that pointer on every return path, including the ones where it did nothing. Clear the buffer at the top of update, then work - not the other way round. system/visibility/visibility_system.cpp publishes ctx.visibility at every exit, early returns included, and says why:
// Cleared here, not at the serial gather, so the early-return paths below // still publish an empty result instead of last frame's stale entries.
A consumer must tell "ran, found nothing" from "never ran", and a null can only say the second. Bailing out without publishing leaves them last frame's answer.
A half-written abstraction is worse than none. If most of what you need is in HierarchyOperations, the resource/generate/ helpers or a culling stage, extend that. The engine has more reusable machinery than is obvious: SparseSet<T>, SlotAllocator, TypeRegistry<Base> (one slot per type - the Scene, the ResourceManager and the EventBus are built on it), parallelFor() (a free function in platform/threading/thread_pool.h, not a ThreadPool method), the field reflection in core/reflect.h, and the event bus on ctx.events.
The directory tree encodes responsibility. Let it place your code:
| If the code is... | It belongs in... |
|---|---|
| Per-frame behavior over the scene | src/engine/system/<name>/ |
| Pure data attached to an entity | src/engine/ecs/component/<subject>/ |
| An operation on the entity graph many systems share (the hierarchy) | src/engine/ecs/ |
| A new asset kind - the Resource subclass | src/engine/resource/asset/ |
| Low-level container / handle / type machinery | src/engine/core/memory/ |
| Window, input, threading, dynamic library | src/engine/platform/<area>/ |
| Profiling and error-reporting facades | src/engine/debug/ |
| Scene / asset / component (de)serialization | src/engine/io/ |
| What goes on the wire, or who decides what | src/engine/net/ - not a system; see engine.md |
| GPU-specific work | src/backend/opengl/ |
| A shader | shaders/<pass>/ |
| Editor-only UI or interaction | src/editor/ - read 2.5 first |
| Importing an asset from its source art (cook-only) | src/tools/import/ |
| Loading what the runtime reads, or generating an asset | src/tools/loader/ or src/engine/resource/generate/ |
| Baking an asset into the form the runtime reads | src/tools/cook/ |
| Registering a system: the stack every host stands up | app/engine_app.h |
| The prologue every host runs - root, working directory, log, module, and the project's world (tick rate, entry scene, its fingerprint) | src/tools/project_boot.h |
| Gameplay | examples/<project>/src/ - never in the engine |
| A test of engine code | tests/<area>/<suite>_tests.cpp, the area named for the source folder it tests; tests/render/ is the GPU binary |
src/engine/net/ is the row most often read wrong, because it is not a system: NetSession is owned by Engine by value and brackets the frame. A change about when the wire is read or written belongs in Engine::run; a system that wants to know who decides an entity asks ctx.net.simulates(entity). The socket is platform/net/.
Two rows are easy to miss because they leave src/engine/: a new asset kind is a type in resource/asset/ plus a loader in src/tools/ that reads a file into it, and a new system comes alive at one addSystem<T>(stage) line in setupEngineApp (app/engine_app.h), where its stage is argued beside it.
If it fits nowhere obvious, the tree is telling you the design is off.
How a folder grows. A folder's root holds its entry point and what every part of it shares; each feature it grows gets a subfolder named for that feature. system/physics/ (physics_system at the root; collision/, solver/, query/, character/, authoring/) and net/ (net_session; wire/, transport/, replication/, prediction/) are the model. A folder gets feature subfolders only when it needs them - more than a screenful of files, or two features someone would look for by name. A folder of many members of one kind, each named for that kind (pass/gl_*_pass, panels/*_panel), stays flat, because the name pattern is its index. Nothing is named common, misc, utils or helpers: shared code sits at the root of the narrowest folder that shares it, named for what it is. Tests follow the source.
A one-line method on an existing class beats a new helper file; a new enum value beats a parallel type. Prefer the change that adds the least new surface while staying clean.
Except where the thing you are adding to has already absorbed cases this way. SceneIOController recorded a play session as m_playSnapshot, then m_playAssets, then m_playSnapshotDirty, then m_playSnapshotHistory - each, on the day it landed, the smallest change that fit, and together the reason the file kept needing fixes (review.md). They are one PlaySnapshot now, and that change was larger than any of the four.
So the smallest change is the smallest one that does not add the fifth case. When you are about to add a field, flag or branch beside three that arrived the same way, the honest change removes the reason for cases - and naming and costing it is yours, while deciding to do it is the owner's (README.md).
Any plain struct is a component: scene.add<T>(e, ...) is the whole mechanism. Surviving a save is a separate step, and skipping it fails silently - the component works all session and is simply absent next load.
Where a component needs more than the driver - an entity reference, private state, polymorphic behaviours - and why, is ../reference/io.md.
Replicating is a fourth step and fails the same silent way. Give it netEncode / netDecode beside it and register it from the project's vkmSetupNetwork (networking.md). Then ask whether it should travel at all: is this a cause only the authority knows, or an effect every end recomputes? An effect on the wire is a second writer for a value that already had one.
Authoring is separate again. The inspector card, the Create-menu entry and the hierarchy badge are hand-written in src/editor/, and the component must be in VKM_EDITOR_COMPONENTS (editor/command/editor_commands.h) or undo cannot restore it and the inspector cannot edit it undoably (../reference/editor.md).
src/editor/ is where the local conventions are load-bearing and the compiler enforces none of them:
Passes are an OpenGL detail and stay in the backend (engine.md), so the whole procedure is in src/backend/opengl/:
A new shader folder uses the loader's filenames (vertex.shader, fragment.shader, compute.shader - building.md). Several effects an agent reaches for are on engine.md's rejected list.
Deliberation scales with what a decision carries; the quality bar does not.
But load accumulates while you are not looking. Everything load-bearing here started as something small that other things then leaned on. So the question for a small thing is not "how much does this carry today" but "what happens to everything else if this is wrong?" Anything naming a format, an order, a lifetime or an identity becomes a seam whether you meant it to or not; those get the full argument now.
A change is rarely one file. Follow it outward:
It builds clean, with zero first-party warnings, and the suites pass - ctest --test-dir build, all three (../reference/building.md). A bug fix comes with a test you have seen fail without the fix.
You used it. Not "the logic is correct" - you ran it and watched it work. Press Play, Pause, Stop; open the panel; load the scene and save it; look at the frame. Every host takes a project directory, and where you cannot open a window the runtime's exit code is meant to be read (../reference/building.md).
Most defects in this engine were found by someone using it. If a change has a visible result and you have not looked at it, it is not finished - and if you could not look, say so.
A performance change carries its measurement, before and after, on the scenes it touches (building.md).
Docs ship with it. A reference page describing last month's design is worse than none, because it is believed.
It is committed in verified batches. One coherent change per commit, built and checked before it lands.
The message says why, briefly. Lowercase type(scope): summary under 72 characters, then a few lines of prose at about 80 columns: what the change does as a whole, and why. Not a tour of what it touched - the diff already says that, and a reader who wants the detail reads it. A commit that folds others in sums up its subject; it does not list what was folded. No line-initial - , no Co-Authored-By, no self-attribution trailer.
A change is finished when a reader cannot tell which lines are new from the style alone, only from the feature they add.