- Chat goes to your room (the sandbox, or your game) by default, and to the whole lobby with /all or a leading !. Lines from elsewhere say where they came from ([Sandbox], [Game 2]), lobby-wide ones from your room say [All]. Typing shows only to your room. Protocol 8. - Hosting: everyone is told who hosts now, and the host can hand it over with /host "player". - /fashion <item> wears a fashion locally, like /color. - Lines that mention a player show in that player's color, looked up as they draw. - Tips are tagged with the item and situations they fit, and those that fit are 1.5x as likely. They turn over every 10 seconds on one continuous run, show under the menu too, and can be turned off with Tips in HUD / Gameplay. The HUD hotbars hide while the menu is up (except on its item tabs). - American spelling throughout: color, center, gray, mustache, and so on.
323 lines
11 KiB
C++
323 lines
11 KiB
C++
#include "game.h"
|
|
#ifdef __EMSCRIPTEN__
|
|
#include <emscripten.h>
|
|
#endif
|
|
#include <SDL3/SDL.h>
|
|
#include <SDL3/SDL_main.h>
|
|
#include <algorithm>
|
|
|
|
// 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<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;
|
|
// 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;
|
|
}
|