305 lines
10 KiB
C++
305 lines
10 KiB
C++
#include "game.h"
|
|
#ifdef __EMSCRIPTEN__
|
|
#include <emscripten.h>
|
|
#endif
|
|
#include <SDL3/SDL.h>
|
|
#include <SDL3/SDL_main.h>
|
|
#include <algorithm>
|
|
|
|
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<Attack> 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;
|
|
}
|