Plain C++ Structs and Loops: A Simulation Before ECS¶
Start with one struct per body and a loop over the world. This gives us a readable C++ baseline, an explicit simulation contract, and tests that later designs must preserve.
Simulation Contract¶
Input is a list of finite positions and velocities, three independent boolean properties, and finite remaining lifetimes. The supplied fixture uses small values; handling overflow or validating arbitrary external input is outside this first example.
- Each step requires a finite, positive
dt. - Move each live moving body by
velocity * dt. - Decrease an expiring body's lifetime by
dt. - Remove bodies whose remaining lifetime is zero or negative.
- A body with an expired lifetime starts absent.
- Rendering returns visible survivors in their existing order.
- Reject invalid
dtbefore changing any state.
A body that expires during a step still participates in that step's movement. There is no sub-step collision handling or interpolation at its exact death time. That is a specification choice, not a physical simulation guarantee.
Build the Plain C++ Simulation¶
On Raspberry Pi OS, install build tools if needed:
Clone the source repository into a new directory:
Use another build directory if that path already belongs to a different project. The code requires C++17 and uses no third-party libraries.
You can also download the current project's sixteen files into one local folder:
CMakeLists.txt,
simulation.hpp,
demo.cpp, tests.cpp,
entity_identity.hpp,
identity_demo.cpp,
identity_tests.cpp,
minimal_ecs.hpp,
ecs_demo.cpp, ecs_tests.cpp,
generational_ecs.hpp,
generation_demo.cpp,
generation_tests.cpp,
deferred_ecs.hpp,
deferred_demo.cpp, and
deferred_tests.cpp.
The identity and ECS files supply the separate Chapter 4–7 targets; the original
simulation sources and demonstration remain unchanged.
Then point cmake -S at that folder.
Expected demonstration output from the supplied fixture:
This is a correctness example, not a benchmark.
Verification scope: the supplied project was compiled and its CTest suite run in Debug and Release with GNU C++ 15.2.0 on the development host. It has not yet been run on Raspberry Pi hardware. Record your OS, compiler version, and test output when reproducing it on a Pi.
One Record per Body¶
The baseline uses this input and storage record:
A stationary body still contains velocity fields, and a non-expiring body still contains a remaining-lifetime field. The flags determine whether those values are relevant. That is a simple representation with some unused data, not necessarily a problem worth fixing.
The plain::World class owns a std::vector<Seed>. Its step method is:
The full header supplies move, time-step validation, construction, and
snapshots. Deletion happens after iteration rather than erasing elements
inside the range loop. Erasure and vector growth can invalidate references;
do not retain a pointer to a body across those operations.
Check Behaviour Independently¶
Tests should contain expected results, not just compare two implementations that could share the same mistake.
For a body at (0, 0) with velocity (2, -4), a 0.25 second step should
produce (0.5, -1). A stationary body must not move even if its stored velocity
is nonzero. A moving invisible body moves but does not appear in rendering.
The supplied tests check those cases, exact-boundary expiration, initially
expired bodies, an empty world, and invalid time steps. They also run all eight
combinations of the three flags. Failures use explicit checks rather than
assert, so the checks remain active in Release builds.
Test the Release build separately:
Costs and Limits¶
The baseline mixes behaviour flags and data in one record. It does not support stable entity handles, concurrent mutation, arbitrary component registration, or a renderer. Those are explicit non-goals.
Keep this design if your workload and changes remain easy to express. Do not introduce a registry merely because the course title contains ECS.
Exercise¶
Add a stationary, invisible, expiring seed. Write its expected survivor count
before running the program. Then change dt to a value larger than its
lifetime and explain the movement-before-expiration rule.
Previous: Design boundaries. Next: Inheritance and composition.