Split the simulation from presentation into an SDL-free library

Sim holds one whole game (tick, snapshots, checksum) and talks to the screen only
through Cues, which the client's Scene plays and the server ignores. All drawing
moved to scene.cpp. Match takes this screen's wishes as request flags instead of
reading the menu, input and settings. The sim library builds without SDL and the
server links it, ready to run games itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shweetandClaude Opus 5.5 committed 2026-11-08 01:46:07 -05:00
1 parent 2007fb7edc
commit 465f75f6d7
38 files changed
+2225 -1965

No files matched your search

+34 -44
View File
@@ -19,21 +19,21 @@ constexpr float CAMERA_FOLLOW = 0.15f;
// middle, clamped to the world or centred when the world is smaller.
static Vec2 camera_target_get(Game& game)
{
Vec2 world = game.world.size_get();
Vec2 world = game.sim.world.size_get();
Vec2 canvas = {CANVAS_WIDTH, CANVAS_HEIGHT};
Vec2 center = {};
int count = 0;
for (int i = 0; i < game.fighterCount; i++)
for (int i = 0; i < game.sim.fighterCount; i++)
{
if (!game.is_local(i) || !game.fighters[i].isAlive)
if (!game.is_local(i) || !game.sim.fighters[i].isAlive)
continue;
center = center + game.fighters[i].center_get();
center = center + game.sim.fighters[i].center_get();
count++;
}
// Spectating, it follows whoever is being watched instead.
if (game.ui.spectated >= 0)
{
center = game.fighters[game.ui.spectated].center_get();
center = game.sim.fighters[game.ui.spectated].center_get();
count = 1;
}
Vec2 target = (count > 0 ? center * (1.0f / count) : game.local_get().center_get()) - canvas * 0.5f;
@@ -48,18 +48,20 @@ static void camera_update(Game& game)
game.camera = game.camera + (camera_target_get(game) - game.camera) * CAMERA_FOLLOW;
}
// One simulation tick: everything rollback saves and replays. Fighters hold still through a round's countdown. The
// menu is this screen's alone, so it updates before the tick (see frame_run), not in it.
// One tick of the game this screen shows, with what it asks of the match offline: the menu's Play, Escape (or B)
// leaving the results unless it's just closed the menu, and its player's colour. The menu is this screen's alone, so
// it updates before the tick (see frame_run), not in it.
void game_tick(Game& game)
{
game.match.update(game);
if (game.match.state != MATCH_COUNTDOWN)
game.bot.update(game);
for (int i = 0; i < game.fighterCount && game.match.state != MATCH_COUNTDOWN; i++)
game.fighters[i].update(game);
game.weapons.update(game);
game.world.update(game);
game.input.tick_end();
Match& match = game.sim.match;
Input& input = game.input;
match.isOnline = game.session.isActive;
match.isPlayRequested = input.isPlayRequested;
match.isSandboxRequested = !game.menu.isOpen && !game.menu.isEscapeTaken &&
(input.is_pressed(SDL_SCANCODE_ESCAPE) || input.is_pad_tapped(SDL_GAMEPAD_BUTTON_EAST));
match.colorWanted = game.settings.color_get();
game.sim.tick();
input.tick_end();
}
// Steps what only this screen sees, once per live tick: particles, HUD fades and the following camera.
@@ -67,7 +69,7 @@ static void presentation_update(Game& game)
{
game.effects.update();
game.ui.update(game);
game.sprites.update(game);
game.scene.update();
game.cameraPrevious = game.camera;
camera_update(game);
}
@@ -77,50 +79,37 @@ static void presentation_update(Game& game)
static void game_draw(Game& game)
{
Rect boxes[MAX_FIGHTERS];
std::vector<Attack> attacks = game.weapons.attacks;
for (int i = 0; i < game.fighterCount; i++)
std::vector<Attack> attacks = game.sim.weapons.attacks;
for (int i = 0; i < game.sim.fighterCount; i++)
{
boxes[i] = game.fighters[i].body.box;
game.fighters[i].blend(game.alpha);
boxes[i] = game.sim.fighters[i].body.box;
game.sim.fighters[i].blend(game.alpha);
}
game.weapons.blend(game.alpha);
game.sim.weapons.blend(game.alpha);
Vec2 camera = vec2_lerp(game.cameraPrevious, game.camera, game.alpha);
if (game.settings.isShaking)
camera = camera + game.effects.shake_get();
game.renderer.camera = {std::round(camera.x), std::round(camera.y)};
game.world.draw(game);
game.bot.draw(game);
game.effects.afterimage_draw(game);
game.weapons.blast_draw(game);
for (int i = 0; i < game.fighterCount; i++)
game.fighters[i].draw(game);
game.weapons.draw(game);
if (game.settings.isCollisionShown)
game.weapons.collision_draw(game);
game.scene.draw();
game.effects.draw(game);
game.ui.draw(game);
game.renderer.present();
for (int i = 0; i < game.fighterCount; i++)
game.fighters[i].body.box = boxes[i];
game.weapons.attacks = attacks;
for (int i = 0; i < game.sim.fighterCount; i++)
game.sim.fighters[i].body.box = boxes[i];
game.sim.weapons.attacks = attacks;
}
// Sets up everything the simulation needs: the world, the bot graph, fighters, weapons, trinkets and the seeded
// generator (RPG_SEED, every number its own odd state, or the clock). Without a renderer the sprites come back empty,
// which the simulation never needs.
// Sets up the game, seeded from RPG_SEED (every number its own odd state) or the clock. Its cues go nowhere until a
// window shows them.
static void simulation_init(Game& game)
{
game.world.init(game);
game.bot.init(game);
for (int i = 0; i < MAX_FIGHTERS; i++)
game.fighters[i].init(game, i);
const char* seed = SDL_getenv("RPG_SEED");
game.sim.init(seed ? (Uint32)SDL_atoi(seed) * 2 + 1 : (Uint32)SDL_GetPerformanceCounter() | 1);
game.local_get().isBot = SDL_getenv("RPG_BOT_LOCAL");
game.rollback.init();
const char* seed = SDL_getenv("RPG_SEED");
game.rng.state = seed ? (Uint32)SDL_atoi(seed) * 2 + 1 : (Uint32)SDL_GetPerformanceCounter() | 1;
}
// The frame clock: when the last frame began, and the time not yet simulated.
@@ -173,9 +162,9 @@ static bool frame_run(bool isDrawn = true)
continue;
}
session.view_leave(game);
for (int i = 0; i < game.fighterCount; i++)
for (int i = 0; i < game.sim.fighterCount; i++)
{
Fighter& fighter = game.fighters[i];
Fighter& fighter = game.sim.fighters[i];
bool isMenuOpen = game.menu.isOpen && i == game.local;
if (game.is_local(i))
fighter.controls = game.input.controls_get(game.devices[i], game.camera, fighter.center_get(),
@@ -265,6 +254,7 @@ int main(int argc, char** argv)
game.ui.init(game);
game.sprites.init(game);
simulation_init(game);
game.sim.cues = &game.scene;
game.renderer.atlas_upload();
game.settings.apply(game);
game.settings.restore(game);