#include "game.h" #ifdef __EMSCRIPTEN__ #include #endif #include #include #include // How early a browser frame may come and still go under the frame rate limit. constexpr Uint64 FRAME_SLACK = 2 * SDL_NS_PER_MS; 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 centerd when the world is smaller. static Vec2 camera_target_get(Game& game) { Vec2 world = game.sim.world.size_get(); Vec2 canvas = {CANVAS_WIDTH, CANVAS_HEIGHT}; Vec2 center = {}; int count = 0; for (int i = 0; i < game.sim.fighterCount; i++) { if (!game.is_local(i) || !game.sim.fighters[i].isAlive) continue; center = center + game.sim.fighters[i].center_get(); count++; } // Spectating, it follows whoever is being watched instead. if (game.ui.spectated >= 0) { 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; 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 toward its target. static void camera_update(Game& game) { game.camera = game.camera + (camera_target_get(game) - game.camera) * CAMERA_FOLLOW; } // 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 color. The menu is this screen's alone, so // it updates before the tick (see frame_run), not in it. void game_tick(Game& game) { Match& match = game.sim.match; Input& input = game.input; match.isOnline = game.session.isActive; match.isPlayRequested = input.isPlayRequested; match.isSandboxRequested = game.menu.isLeaving || (!game.menu.isOpen && input.is_pad_tapped(SDL_GAMEPAD_BUTTON_EAST)); match.colorWanted = game.settings.color_get(); match.fashionWanted = game.settings.fashion; game.sim.tick(); 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.stats.update(game); game.ui.update(game); game.scene.update(); 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.sim.weapons.attacks; for (int i = 0; i < game.sim.fighterCount; i++) { boxes[i] = game.sim.fighters[i].body.box; game.sim.fighters[i].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.scene.draw(); game.effects.draw(game); game.ui.draw(game); game.renderer.present(); for (int i = 0; i < game.sim.fighterCount; i++) game.sim.fighters[i].body.box = boxes[i]; game.sim.weapons.attacks = attacks; } // 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) { 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(); } // 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; // When the next frame is due under the frame rate limit (see frame_wait_get), in nanoseconds. static Uint64 frameDue = 0; // One frame's input and as many fixed ticks as the time since the last frame allows. static void frame_step() { 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, the session runs the room this screen is in, taking its controls; otherwise each fighter on this // screen takes its device's. Watching, they do nothing. 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(game); Controls controls = game.input.controls_get(DEVICE_KEYBOARD, game.camera, own, isHeld); session.update(game, controls); game.input.tick_end(); if (session.isViewChanged) { session.isViewChanged = false; game.camera = game.cameraPrevious = camera_target_get(game); } presentation_update(game); continue; } for (int i = 0; i < game.sim.fighterCount; 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, isMenuOpen); } game.rollback.update(game); presentation_update(game); } game.settings.update(game); game.alpha = frameClock.accumulator / TICK; } // The frame rate limit in use: none while vsync paces the frames. static int frame_limit_get() { return game.settings.isVsync ? 0 : game.settings.fpsLimit; } // How long until the frame rate limit lets the next frame go, in nanoseconds. static Uint64 frame_wait_get() { Uint64 now = SDL_GetTicksNS(); return frame_limit_get() > 0 && frameDue > now ? frameDue - now : 0; } // Sets when the frame after this one is due under the limit: a limit's worth of time after this one was due, or after // now if it ran late, so a slow frame doesn't bring on a burst to catch up. static void frame_due_advance() { int limit = frame_limit_get(); if (limit > 0) frameDue = std::max(frameDue, SDL_GetTicksNS()) + SDL_NS_PER_SECOND / limit; } // One frame, stepped and drawn. Returns whether to keep going. static bool frame_run() { frame_step(); game_draw(game); return !game.input.isQuit; } #ifdef SDL_PLATFORM_EMSCRIPTEN // The browser paces the frames itself, so under a frame rate limit those that come too soon are skipped (one that's // only FRAME_SLACK early still goes, for screens refreshing a shade fast). static void frame_callback() { if (frame_wait_get() > FRAME_SLACK) return; frame_due_advance(); 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_step(); } #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.sim.cues = &game.scene; 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 do { SDL_DelayPrecise(frame_wait_get()); frame_due_advance(); } while (frame_run()); game.settings.save(game); game.net.shutdown(); SDL_Quit(); return 0; }