Skip to content

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 by Velocity times 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 PathFollower component 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.