Scripting and physics¶
Requirements¶
- New enemies, attack patterns and tower behaviors without recompiling the engine or the game: the R-Type server and the tower defense load them as data.
- Scripts run on the authoritative server for R-Type: a broken or hostile script must never crash it, read its files or block a tick.
- The same code on Linux, Windows and macOS, built by CMake through CPM like every other dependency.
- Movement and collisions identical at any frame rate, cheap enough for hundreds of missiles at 60 ticks per second, and simple enough for a team of three to test and explain.
Scripting language¶
| Option | Embedding | Strengths | Weaknesses for this project |
|---|---|---|---|
| Data files only (JSON) | Already there: core::Config, the waves file |
No new dependency; values are validated when loaded | Numbers, not behaviors: a new movement pattern or tower effect still needs C++ code |
| Lua 5.4 + sol2 (chosen, planned: #352 to #356) | About 30 C files built as a static library; sol2 is header-only | Made to be embedded; a few hundred KB; the libraries a state opens are chosen one by one, so io, os and debug are simply not there; coroutines write a pattern that waits between steps as straight-line code; the most used language for game scripting, so the documentation and the examples are plentiful |
Dynamic typing: mistakes show at run time, so errors must be reported with their file and line; sol2's templates are slow to compile, hence kept in the .cpp files of the script module |
| LuaJIT | Its own makefiles (msvcbuild.bat on Windows) |
Much faster than plain Lua | Frozen at Lua 5.1; no CMake build to fetch with CPM; the speed is not needed for a few hundred entities per tick |
| Python (pybind11) | The interpreter and its standard library shipped with the game | Known by everyone | Tens of MB to ship; no real sandbox (any module reaches the file system); global interpreter lock |
| JavaScript (QuickJS) | A few C files | Small, modern language | Built with GCC and Clang extensions, Windows support only in the newer quickjs-ng fork; few game bindings to learn from |
| AngelScript | C++ library, a registration call per function and member | Static typing close to C++, fast | Little known; verbose bindings; a smaller community to ask |
| ChaiScript | Header-only C++ | Calls C++ directly, no binding layer | Slow at run time and very slow to compile; little maintained |
Decision: Lua 5.4 with sol2, in a new script module that the server and the samples
link and the other modules ignore. It gives the most behavior for the least integration
work, and its sandbox comes from what is not opened rather than from a list of things to
forbid. The waves stay in JSON: a script decides how an enemy moves, the data file decides
when and where it comes.
What a script sees (planned): the common components (Transform, Velocity,
Sprite...) through bindings (#354), entity creation and destruction and the event bus
(#355), and the tick's delta time. The C++ side keeps the order of the systems: the
scripted behaviors run as one system, at their place in the tick, so a script cannot
change what runs before or after it.
Risks and countermeasures (planned: #353)
| Risk | Countermeasure |
|---|---|
| A script reads or writes files, runs programs | The state opens base, string, table, math and coroutine only: no io, os, debug or package |
| Precompiled bytecode crafted to break the interpreter | Scripts are loaded as text only (load mode "t") |
| A syntax or run-time error | Caught at the call (sol::protected_function), logged with the file and the line, the entity keeps its last state; the server goes on |
| A script that never returns | A count hook (lua_sethook) stops a call after a fixed budget of instructions, so a tick always ends |
| Different results from one run to another | No clock and no unseeded random in the state: time comes from the tick, random numbers from the server's seeded generator |
Physics¶
What the two games need:
- R-Type: ships, missiles and Bydos move in straight lines or along waves; a hit removes health. Nothing bounces, stacks or falls.
- Tower defense: enemies walk along a path computed on the map's grid; towers look for the enemies within their range. Nothing touches anything.
| Option | Strengths | Weaknesses for this project |
|---|---|---|
| Box2D 3 | Complete and proven rigid-body physics: stacking, joints, continuous collision, sleeping bodies | A second world to keep in step with the ECS each tick; units in meters; a contact solver neither game uses; tuning time for a team of three |
| Chipmunk2D | Lighter rigid-body physics, simple C API | The same mismatch as Box2D, and little maintained |
| Own rigid bodies: gravity, AABB resolution against solid tiles, triggers | Fits the engine's components exactly | Tickets #334 to #337, closed as not planned on 2026-10-21: they serve a platformer, and neither game is one |
| Kinematic movement and collision queries (chosen, in place) | Exactly what the games ask for, deterministic, tested against brute force | No contact response: a game that needs bodies pushing each other would add it |
Decision: no physics engine. Entities move by what they are told, collisions are detected and turned into gameplay, never resolved:
- Movement (in place:
ecs::apply_velocity,ecs::apply_sine_motion): positions advance byVelocitytimes the fixed step of 1/60 s (core::FixedTimestep), so a ship covers the same distance at any frame rate and the server computes what the clients predict. The integration is exact for the constant speeds the games use; accelerations would switch it to semi-implicit Euler (velocity first, then position), the usual stable choice for games. - Paths (planned): A* on the tile grid, with the walkable cells and their costs read
from a Tiled layer (#402), then a
PathFollowercomponent that advances along the waypoints and tells which enemy is ahead (#403). - Collisions (in place:
ecs::CollisionGrid,ecs::find_contacts): a uniform grid for the broad phase and boxes with layer masks for the narrow phase, described in Algorithms, data structures and storage. Contacts become lost health and kills (ecs::apply_damage). - Range queries (planned: #404): the same grid answers "which enemies are within this radius", nearest or most advanced first, for the towers.
Tunneling: with discrete steps, a fast entity could jump over another between two ticks. It cannot happen as long as the distance covered in one step stays below the sum of the two boxes' widths along the motion. R-Type's fastest entity, the player missile (900 px/s, 16 px long), moves 15 px per tick, while the smallest gap it must not jump over, missile plus Bydos, is 48 px. A faster entity would need a swept test (the box stretched over its whole step) before a physics engine.
When to revisit: a third game with platforms, ropes or piles of objects would add Box2D
behind a physics module that owns the bodies and writes their positions into Transform
after each step, so the rest of the engine keeps reading Transform as today.