#include "game.h" #ifdef __EMSCRIPTEN__ #include #endif #include #include #include constexpr int WINDOW_SCALE = 2; constexpr double MAX_FRAME_TIME = 0.25; #ifdef SDL_PLATFORM_EMSCRIPTEN constexpr SDL_WindowFlags WINDOW_FILL = SDL_WINDOW_FILL_DOCUMENT; #else constexpr SDL_WindowFlags WINDOW_FILL = 0; #endif constexpr float CAMERA_FOLLOW = 0.15f; // Where the camera's corner wants to be: this screen's living fighters' middle (or the spectated fighter) in the // 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 canvas = {CANVAS_WIDTH, CANVAS_HEIGHT}; Vec2 center = {}; int count = 0; for (int i = 0; i < game.fighterCount; i++) { if (!game.is_local(i) || !game.fighters[i].isAlive) continue; center = center + game.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(); count = 1; } Vec2 target = (count > 0 ? center * (1.0f / count) : game.local_get().center_get()) - canvas * 0.5f; target.x = world.x > canvas.x ? std::clamp(target.x, 0.0f, world.x - canvas.x) : (world.x - canvas.x) / 2; target.y = world.y > canvas.y ? std::clamp(target.y, 0.0f, world.y - canvas.y) : (world.y - canvas.y) / 2; return target; } // Eases the camera towards its target. 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. 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(); } // Steps what only this screen sees, once per live tick: particles, HUD fades and the following camera. static void presentation_update(Game& game) { game.effects.update(); game.ui.update(game); game.sprites.update(game); game.cameraPrevious = game.camera; camera_update(game); } // Draws the world part way between the last two ticks. Fighters and attacks are swapped // for blended copies while drawing, then the real simulation state is put back. static void game_draw(Game& game) { Rect boxes[MAX_FIGHTERS]; std::vector attacks = game.weapons.attacks; for (int i = 0; i < game.fighterCount; i++) { boxes[i] = game.fighters[i].body.box; game.fighters[i].blend(game.alpha); } game.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.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; } // 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. 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); 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. struct FrameClock { Uint64 previous = 0; double accumulator = 0; }; static Game game; static FrameClock frameClock; // One frame: input, as many fixed ticks as the time since the last frame allows, then drawing (unless it's not to be // drawn). Returns whether to keep going. static bool frame_run(bool isDrawn = true) { Uint64 now = SDL_GetPerformanceCounter(); frameClock.accumulator += std::min((now - frameClock.previous) / (double)SDL_GetPerformanceFrequency(), MAX_FRAME_TIME); frameClock.previous = now; game.input.poll(game); game.net.update(); game.chat.update(game); Session& session = game.session; for (; frameClock.accumulator >= TICK; frameClock.accumulator -= TICK) { // Online, this screen's own work happens in the game it shows (the arena while it plays or watches a queued // game), and the session runs the shared games, taking this screen's controls; otherwise each fighter on this // screen takes its device's. Watching, they do nothing. session.view_enter(game); game.menu.update(game); if (session.is_wanted(game)) { Fighter& own = game.local_get(); bool isHeld = game.menu.isOpen || game.chat.isOpen || session.is_watching(); Controls controls = game.input.controls_get(DEVICE_KEYBOARD, game.camera, own.center_get(), own.slot, isHeld); session.view_leave(game); session.update(game, controls); game.input.tick_end(); session.view_enter(game); if (session.isViewChanged) { session.isViewChanged = false; game.camera = game.cameraPrevious = camera_target_get(game); } presentation_update(game); session.view_leave(game); continue; } session.view_leave(game); for (int i = 0; i < game.fighterCount; i++) { Fighter& fighter = game.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(), fighter.slot, isMenuOpen); } game.rollback.update(game); presentation_update(game); } game.settings.update(game); game.alpha = frameClock.accumulator / TICK; if (isDrawn) { session.view_enter(game); game_draw(game); session.view_leave(game); } return !game.input.isQuit; } #ifdef SDL_PLATFORM_EMSCRIPTEN static void frame_callback() { frame_run(); } // A hidden tab gets no frames from the browser, but online the shared game can't wait for it: everyone else would // stall. So a worker's timer (which the browser doesn't hold back the way it does a hidden page's own) calls this // every 16 ms while the tab is hidden, to keep the game running, undrawn. extern "C" EMSCRIPTEN_KEEPALIVE void frame_hidden() { if (game.session.is_wanted(game)) frame_run(false); } #endif // Runs the game in a window (on the web, filling the page and driven by the browser's frames), or with "--balance N" // plays N bot matches headless and prints the results ("--balance N race" plays races). // The lobby to join on starting, if any: from the page's ?lobby= in the browser, or --join on the command line. static std::string lobby_requested(int argc, char** argv) { #ifdef SDL_PLATFORM_EMSCRIPTEN char* code = (char*)EM_ASM_PTR({ var code = new URLSearchParams(location.search).get('lobby'); return code ? stringToNewUTF8(code) : 0; }); std::string lobby = code ? code : ""; free(code); return lobby; #else for (int i = 1; i + 1 < argc; i++) if (SDL_strcmp(argv[i], "--join") == 0) return argv[i + 1]; return ""; #endif } int main(int argc, char** argv) { int balanceMatches = argc > 2 && SDL_strcmp(argv[1], "--balance") == 0 ? SDL_atoi(argv[2]) : 0; if (balanceMatches > 0) { simulation_init(game); bool isRace = argc > 3 && SDL_strcmp(argv[3], "race") == 0; game.balance.matches_run(game, balanceMatches, isRace ? MODE_RACE : MODE_DEATHMATCH); return 0; } if (!SDL_Init(SDL_INIT_VIDEO)) { SDL_Log("Failed to init SDL: %s", SDL_GetError()); return 1; } if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) SDL_Log("Gamepads unavailable: %s", SDL_GetError()); game.settings.load(game); game.window = SDL_CreateWindow(GAME_NAME, CANVAS_WIDTH * WINDOW_SCALE, CANVAS_HEIGHT * WINDOW_SCALE, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY | WINDOW_FILL); if (!game.window || !game.renderer.init(game.window)) { SDL_Log("Failed to create window: %s", SDL_GetError()); return 1; } game.text.init(game.renderer); game.audio.init(); game.ui.init(game); game.sprites.init(game); simulation_init(game); game.renderer.atlas_upload(); game.settings.apply(game); game.settings.restore(game); std::string lobby = lobby_requested(argc, argv); if (!lobby.empty()) { game.net.lobby_join(game, lobby.c_str()); game.menu.online_open(); } game.camera = camera_target_get(game); game.cameraPrevious = game.camera; SDL_HideCursor(); frameClock.previous = SDL_GetPerformanceCounter(); #ifdef SDL_PLATFORM_EMSCRIPTEN // The browser calls each frame from here on; main returns but the game lives on. emscripten_set_main_loop(frame_callback, 0, false); EM_ASM({ var ticker = new Worker(URL.createObjectURL( new Blob(["setInterval(function() { postMessage(0); }, 16);"], {type : "text/javascript"}))); ticker.onmessage = function() { if (document.hidden) _frame_hidden(); }; }); return 0; #endif while (frame_run()) { } game.settings.save(game); game.net.shutdown(); SDL_Quit(); return 0; }