Engine: exact physics underneath, modern engine on top
Airtime is a native game in a modern engine (Bevy or Godot), with modern visuals. Underneath, the movement and collision are exactly the old engine's: Urban Terror 4.3.4, bit for bit. The engine draws the world and handles input, sound and UI; it never decides where the player is.
The split#
Every map has two layers:
| Collision layer: the old engine | Visual layer: the modern engine | |
|---|---|---|
| What | where you can run, jump, slide, walljump, grab a ledge, climb a ladder, swim and water-jump; what is slick, what is clip, where triggers are | everything you see and hear: meshes, materials, light, sky, effects, sound |
| Made of | Quake 3 brushes (convex volumes) and patches (curves), with content and surface flags | modern geometry (glTF meshes), PBR materials, lights, decals, particles |
| Built with | the Airtime map editor (mapc), Radiant / q3map2, Rambetter's curve generators | Blender, the engine's editor, generated from the collision as a starting point |
| Compiled to | a BSP (or an equivalent structure that traces bit-identically) | the engine's scene / asset format |
| Rules | exactly UrT 4.3.4: urt-collision = ioq3's cm_*, bit for bit | free, as long as it never lies about the collision |
| Changes when | the gameplay changes (new board / map hash) | anytime (no effect on times and replays) |
What the collision layer holds#
- Brushes (convex solids) and patches (Bezier curves, tessellated exactly like Q3).
- Contents per brush: solid, player clip, water (and slime / lava: swim, water jump), triggers, …
- Surface flags per side: slick, ladder, no fall damage, no impact, sky, … and Airtime surfaces (bounce, wall-run) on maps that use them.
- Entities that act in the simulation: spawns, timer gates, teleports, jump pads, movers, filters, gravity zones, fuel pickups, enemies.
That's all the simulation needs. It runs headless on the server, in replays and in tests, without any visuals.
What the visual layer holds#
- Meshes and materials, as detailed as we want: rounded edges, broken concrete, icicles, rails.
- Light (baked and real-time), sky, weather, fog, reflections.
- Decoration that is not solid (marked as such): wires, plants, particles, signs.
- Sound zones, music.
Keeping the two layers honest#
The danger of splitting: the look lies. A visible ledge you can't stand on, an invisible lip that stops you, an icy-looking floor that grips. Rules:
- Generate first, decorate after: the visual layer starts as a mesh generated from the collision. Artists refine it. They don't start from scratch.
- Tolerances: visible surfaces lie within a few units of the collision surface they stand for. A checker compares both layers and lists every place they differ (like the curve clip checks).
- Surface type follows collision: the slick flag decides the ice material (or a material family), not the other way around. The slick view shows the collision layer over the visuals.
- Debug views: collision wireframe, slick surfaces, clip, triggers, water, all drawable over the modern visuals, in the game's map debug menu and in the editor.
Classic maps#
UrT / Q3 maps already are both layers in one file: the BSP is the collision, and its faces, shaders and lightmaps are the visuals. They load as they are, drawn by a Q3-style renderer inside the engine. A modern visual layer can be made for a classic map without touching its collision, so its records stay valid.
The simulation core#
The heart of the game is engine-independent Rust:
| Component | What |
|---|---|
urt-bsp | BSP loading |
urt-collision | the ioq3 cm_* port: traces against brushes and patches (the collision layer) |
urt-pmove | all movement: UrT 4.3.4 at its base, plus wall-runs, jetpacks, gravity flips and the rest |
urt-game | jump mode: entities, timers, teleports, movers, weapons and shots, enemies, replays, gravity |
| network | the Airtime server, relay and protocol |
mapc | the map compiler: collision BSP and lighting |
The engine is the host around it: rendering, HUD, menus, input, audio, the body and camera, effects. The game ticks the simulation in 8 ms commands, and the engine reads the state each frame to place the camera, the body, ghosts, enemies and effects.
Weapons live in the simulation too: shots (the player's and enemies') are traced against the collision layer in the same deterministic ticks, so a replay shows exactly the same shots. Firing never touches the movement code.
Engine: Bevy or Godot#
Both fit this architecture; the choice is still open.
| Bevy | Godot | |
|---|---|---|
| Language | Rust, the same as the core: no boundary, one build | GDScript / C#, with the Rust core through GDExtension (godot-rust) |
| Rendering | modern (wgpu): PBR, bloom, SSAO, volumetric fog, atmosphere; GI still maturing | mature 3D (Vulkan): PBR, SDFGI / VoxelGI, volumetric fog, lightmap baking, reflection probes |
| Editor | no full visual editor (scene tooling via third-party / Blender workflows) | a full editor: scenes, materials, animation, UI, import pipeline |
| UI | ECS-based UI, still young | a strong UI system (menus, HUD) |
| Platforms | desktop, web (wasm + WebGPU), mobile (less mature) | desktop, mobile, consoles (via partners), web (more limited for 3D) |
| Stability | fast-moving, breaking changes between versions | stable releases, large community |
| Fit for Airtime | the simulation is just a system; deterministic ticks are natural in ECS; a web build stays possible | the editor and the art pipeline are much stronger for artists and level designers |
- Bevy suits a team of programmers and keeps a web build possible: everything is Rust.
- Godot suits a team where artists and level designers build the visual layer: its editor, lighting, animation and UI tools save months.
The deciding test: load an icy map's collision through the core, draw a modern visual pass over it, run at 125 Hz input with a smooth frame rate, and play a ghost.
What stays true in any engine#
- The simulation is ours, not the engine's. No engine physics in the movement. The engine never moves the player; it only shows where the simulation says the player is.
- Fixed ticks: 8 ms commands (pmove_fixed), independent of frame rate. Rendering interpolates between ticks.
- Determinism across platforms: float32 rules and our own sin/cos, so every OS and every build gives bit-identical results.
- Headless: the server, replays, tests and verification run the same core without the engine.
- Input latency: raw mouse input, sampled per command, never smoothed by the engine.