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>
295 lines
10 KiB
C++
295 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.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 towards 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 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)
|
|
{
|
|
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.
|
|
static void presentation_update(Game& game)
|
|
{
|
|
game.effects.update();
|
|
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<Attack> 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;
|
|
|
|
// 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.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.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.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
|
|
while (frame_run())
|
|
{
|
|
}
|
|
|
|
game.settings.save(game);
|
|
game.net.shutdown();
|
|
SDL_Quit();
|
|
return 0;
|
|
}
|