refactor game speed to provide a constant frame rate

This commit is contained in:
Tim Felgentreff 2022-08-24 20:21:32 +02:00
parent 3ba7987ffe
commit 15986db050
14 changed files with 80 additions and 99 deletions

View file

@ -1205,21 +1205,20 @@ Sets the resolution of the video display, valid options are 640x480, 800x600, 10
<a name="SetVideoSyncSpeed"></a> <a name="SetVideoSyncSpeed"></a>
<h3>SetVideoSyncSpeed(speed)</h3> <h3>SetVideoSyncSpeed(speed)</h3>
Sets the video sync speed for the game. 100 is equivilent to 30 frames/second. 200 Sets the video render speed for the game. 30 means 30 frames/second. 60 means 60 frames/second. Default is the native monitor refresh rate.
is equivilent to 60 frames/second.
<dl> <dl>
<dt>speed</dt> <dt>speed</dt>
<dd>The speed to set the game to as described, 100 is default</dd> <dd>The speed to set the game to as described, default is the monitor refresh rate.</dd>
<dt><i>RETURNS</i></dt> <dt><i>RETURNS</i></dt>
<dd>Nothing</dd> <dd>Nothing</dd>
</dl> </dl>
<h4>Exmaple</h4> <h4>Example</h4>
<pre> <pre>
-- Set Speed to 60 frames/second -- Set Speed to 60 frames/second
SetVideoSyncSpeed(200) SetVideoSyncSpeed(60)
</pre> </pre>
<a name="ShowEnergySelectedOnly"></a> <a name="ShowEnergySelectedOnly"></a>

View file

@ -2105,7 +2105,7 @@ void EditorMainLoop()
bool start = true; bool start = true;
while (Editor.Running) { while (Editor.Running) {
if (FrameCounter % FRAMES_PER_SECOND == 0) { if (FrameCounter % CYCLES_PER_SECOND == 0) {
if (UpdateMinimap) { if (UpdateMinimap) {
UI.Minimap.Update(); UI.Minimap.Update();
UpdateMinimap = false; UpdateMinimap = false;

View file

@ -635,14 +635,14 @@ bool GetGamePaused()
} }
/** /**
** Set the game speed ** Set the game speed in range 0 .. 100
** **
** @param speed New game speed. ** @param speed New game speed.
*/ */
void SetGameSpeed(int speed) void SetGameSpeed(int speed)
{ {
if (GameCycle == 0 || FastForwardCycle < GameCycle) { if (GameCycle == 0 || FastForwardCycle < GameCycle) {
VideoSyncSpeed = speed * 100 / CYCLES_PER_SECOND; CyclesPerSecond = (static_cast<double>(speed) / 100) * CYCLES_PER_SECOND + static_cast<double>(CYCLES_PER_SECOND) / 3;
SetVideoSync(); SetVideoSync();
} }
} }
@ -654,7 +654,7 @@ void SetGameSpeed(int speed)
*/ */
int GetGameSpeed() int GetGameSpeed()
{ {
return CYCLES_PER_SECOND * VideoSyncSpeed / 100; return ((static_cast<double>(CyclesPerSecond) - static_cast<double>(CYCLES_PER_SECOND) / 3) / CYCLES_PER_SECOND) * 100;
} }
/*---------------------------------------------------------------------------- /*----------------------------------------------------------------------------

View file

@ -207,8 +207,8 @@ extern bool GamePaused;
extern bool GameObserve; extern bool GameObserve;
/// Flag telling if the game is in establishing mode /// Flag telling if the game is in establishing mode
extern bool GameEstablishing; extern bool GameEstablishing;
/// Flag telling not to advance to the next game cycle /// Counter of how many game cycles to skip for each rendered frame
extern char SkipGameCycle; extern double SkipGameCycle;
/// Invincibility cheat /// Invincibility cheat
extern bool GodMode; extern bool GodMode;
/// Whether the map is the only thing displayed or not /// Whether the map is the only thing displayed or not

View file

@ -65,8 +65,6 @@ constexpr unsigned short UpgradeMax = 2048; /// How many upgrades
constexpr unsigned char MAX_RACES = 8; constexpr unsigned char MAX_RACES = 8;
constexpr unsigned char PlayerNumNeutral = PlayerMax - 1; /// this is the neutral player slot constexpr unsigned char PlayerNumNeutral = PlayerMax - 1; /// this is the neutral player slot
/// Frames per second to display (original 30-40)
constexpr unsigned char FRAMES_PER_SECOND = 30; // 1/30s
/// Game cycles per second to simulate (original 30-40) /// Game cycles per second to simulate (original 30-40)
constexpr unsigned char CYCLES_PER_SECOND = 30; // 1/30s 0.33ms constexpr unsigned char CYCLES_PER_SECOND = 30; // 1/30s 0.33ms

View file

@ -405,15 +405,12 @@ public:
extern CVideo Video; extern CVideo Video;
/** /**
** Video synchronization speed. Synchronization time in percent. ** Target CyclesPerSecond that are simulated. The default is CYCLES_PER_SECOND.
** If =0, video framerate is not synchronized. 100 is exact
** CYCLES_PER_SECOND (30). Game will try to redraw screen within
** intervals of VideoSyncSpeed, not more, not less.
** @see CYCLES_PER_SECOND ** @see CYCLES_PER_SECOND
*/ */
extern int VideoSyncSpeed; extern int CyclesPerSecond;
extern int SkipFrames; extern double SkipCycles;
/// Fullscreen or windowed set from commandline. /// Fullscreen or windowed set from commandline.
extern char VideoForceFullScreen; extern char VideoForceFullScreen;
@ -421,6 +418,9 @@ extern char VideoForceFullScreen;
/// Next frame ticks /// Next frame ticks
extern double NextFrameTicks; extern double NextFrameTicks;
/// Target refresh rate for renderer
extern int RefreshRate;
/// Counts frames /// Counts frames
extern unsigned long FrameCounter; extern unsigned long FrameCounter;

View file

@ -370,7 +370,7 @@ void CMinimap::Update()
{ {
static int red_phase; static int red_phase;
int red_phase_changed = red_phase != (int)((FrameCounter / FRAMES_PER_SECOND) & 1); int red_phase_changed = red_phase != (int)((FrameCounter / CYCLES_PER_SECOND) & 1);
if (red_phase_changed) { if (red_phase_changed) {
red_phase = !red_phase; red_phase = !red_phase;
} }

View file

@ -1148,11 +1148,10 @@ static void CheckPlayerThatTimeOut(int hostIndex)
if (!lastFrame) { if (!lastFrame) {
return; return;
} }
const int framesPerSecond = FRAMES_PER_SECOND * VideoSyncSpeed / 100; const int secs = (FrameCounter - lastFrame) / CyclesPerSecond;
const int secs = (FrameCounter - lastFrame) / framesPerSecond;
// FIXME: display a menu while we wait // FIXME: display a menu while we wait
const int timeoutInS = CNetworkParameter::Instance.timeoutInS; const int timeoutInS = CNetworkParameter::Instance.timeoutInS;
if (3 <= secs && secs < timeoutInS && FrameCounter % framesPerSecond == 0) { if (3 <= secs && secs < timeoutInS && FrameCounter % CyclesPerSecond == 0) {
SetMessage(_("Waiting for player \"%s\": %d:%02d"), Hosts[hostIndex].PlyName, SetMessage(_("Waiting for player \"%s\": %d:%02d"), Hosts[hostIndex].PlyName,
(timeoutInS - secs) / 60, (timeoutInS - secs) % 60); (timeoutInS - secs) / 60, (timeoutInS - secs) % 60);
} }

View file

@ -110,15 +110,15 @@ void DoScrollArea(int state, bool fast, bool isKeyboard)
vp = UI.SelectedViewport; vp = UI.SelectedViewport;
if (fast) { if (fast) {
stepx = (int)(speed * vp->MapWidth / 2 * PixelTileSize.x * FRAMES_PER_SECOND / 4); stepx = (int)(speed * vp->MapWidth / 2 * PixelTileSize.x * CYCLES_PER_SECOND / 4);
stepy = (int)(speed * vp->MapHeight / 2 * PixelTileSize.y * FRAMES_PER_SECOND / 4); stepy = (int)(speed * vp->MapHeight / 2 * PixelTileSize.y * CYCLES_PER_SECOND / 4);
} else {// dynamic: let these variables increase up to fast.. } else {// dynamic: let these variables increase up to fast..
// FIXME: pixels per second should be configurable // FIXME: pixels per second should be configurable
stepx = (int)(speed * PixelTileSize.x * FRAMES_PER_SECOND / 4); stepx = (int)(speed * PixelTileSize.x * CYCLES_PER_SECOND / 4);
stepy = (int)(speed * PixelTileSize.y * FRAMES_PER_SECOND / 4); stepy = (int)(speed * PixelTileSize.y * CYCLES_PER_SECOND / 4);
} }
if ((state & (ScrollLeft | ScrollRight)) && (state & (ScrollLeft | ScrollRight)) != (ScrollLeft | ScrollRight)) { if ((state & (ScrollLeft | ScrollRight)) && (state & (ScrollLeft | ScrollRight)) != (ScrollLeft | ScrollRight)) {
stepx = stepx * 100 * 100 / VideoSyncSpeed / FRAMES_PER_SECOND / (SkipFrames + 1); stepx = stepx * 3;
remx += stepx - (stepx / 100) * 100; remx += stepx - (stepx / 100) * 100;
stepx /= 100; stepx /= 100;
if (remx > 100) { if (remx > 100) {
@ -129,7 +129,7 @@ void DoScrollArea(int state, bool fast, bool isKeyboard)
stepx = 0; stepx = 0;
} }
if ((state & (ScrollUp | ScrollDown)) && (state & (ScrollUp | ScrollDown)) != (ScrollUp | ScrollDown)) { if ((state & (ScrollUp | ScrollDown)) && (state & (ScrollUp | ScrollDown)) != (ScrollUp | ScrollDown)) {
stepy = stepy * 100 * 100 / VideoSyncSpeed / FRAMES_PER_SECOND / (SkipFrames + 1); stepy = stepy * 3;
remy += stepy - (stepy / 100) * 100; remy += stepy - (stepy / 100) * 100;
stepy /= 100; stepy /= 100;
if (remy > 100) { if (remy > 100) {
@ -255,7 +255,7 @@ static void GameLogicLoop()
// //
// Game logic part // Game logic part
// //
if (!GamePaused && NetworkInSync && !SkipGameCycle) { if (!GamePaused && NetworkInSync && SkipGameCycle < 1) {
SinglePlayerReplayEachCycle(); SinglePlayerReplayEachCycle();
++GameCycle; ++GameCycle;
MultiPlayerReplayEachCycle(); MultiPlayerReplayEachCycle();
@ -333,11 +333,6 @@ static void GameLogicLoop()
#endif #endif
} }
//#define REALVIDEO
#ifdef REALVIDEO
static int RealVideoSyncSpeed;
#endif
static void DisplayLoop() static void DisplayLoop()
{ {
/* update only if viewmode changed */ /* update only if viewmode changed */
@ -359,12 +354,6 @@ static void DisplayLoop()
ColorCycle(); ColorCycle();
#ifdef REALVIDEO
if (FastForwardCycle > GameCycle && RealVideoSyncSpeed != VideoSyncSpeed) {
RealVideoSyncSpeed = VideoSyncSpeed;
VideoSyncSpeed = 3000;
}
#endif
if (FastForwardCycle <= GameCycle || GameCycle <= 10 || !(GameCycle & CallPeriod::cEvery256th)) { if (FastForwardCycle <= GameCycle || GameCycle <= 10 || !(GameCycle & CallPeriod::cEvery256th)) {
//FIXME: this might be better placed somewhere at front of the //FIXME: this might be better placed somewhere at front of the
// program, as we now still have a game on the background and // program, as we now still have a game on the background and
@ -375,11 +364,6 @@ static void DisplayLoop()
UpdateDisplay(); UpdateDisplay();
RealizeVideoMemory(); RealizeVideoMemory();
} }
#ifdef REALVIDEO
if (FastForwardCycle == GameCycle) {
VideoSyncSpeed = RealVideoSyncSpeed;
}
#endif
} }
static void SingleGameLoop() static void SingleGameLoop()
@ -416,10 +400,6 @@ void GameMainLoop()
CParticleManager::init(); CParticleManager::init();
#ifdef REALVIDEO
RealVideoSyncSpeed = VideoSyncSpeed;
#endif
CclCommand("if (GameStarting ~= nil) then GameStarting() end"); CclCommand("if (GameStarting ~= nil) then GameStarting() end");
long ticks = SDL_GetTicks(); long ticks = SDL_GetTicks();
@ -440,11 +420,6 @@ void GameMainLoop()
return; return;
} }
#ifdef REALVIDEO
if (FastForwardCycle > GameCycle) {
VideoSyncSpeed = RealVideoSyncSpeed;
}
#endif
NetworkQuitGame(); NetworkQuitGame();
EndReplayLog(); EndReplayLog();

View file

@ -590,7 +590,7 @@ void ParseCommandLine(int argc, char **argv, Parameters &parameters)
AiSleepCycles = atoi(optarg); AiSleepCycles = atoi(optarg);
continue; continue;
case 'S': case 'S':
VideoSyncSpeed = atoi(optarg); RefreshRate = atoi(optarg);
continue; continue;
case 'u': case 'u':
if (!strcmp(optarg, "userhome")) { if (!strcmp(optarg, "userhome")) {

View file

@ -84,7 +84,7 @@ bool GameRunning; /// Current running state
bool GamePaused; /// Current pause state bool GamePaused; /// Current pause state
bool GameObserve; /// Observe mode bool GameObserve; /// Observe mode
bool GameEstablishing; /// Game establishing mode bool GameEstablishing; /// Game establishing mode
char SkipGameCycle; /// Skip the next game cycle double SkipGameCycle; /// Skip the next n game cycles
char BigMapMode; /// Show only the map char BigMapMode; /// Show only the map
enum _iface_state_ InterfaceState; /// Current interface state enum _iface_state_ InterfaceState; /// Current interface state
bool GodMode; /// Invincibility cheat bool GodMode; /// Invincibility cheat
@ -349,7 +349,7 @@ static void UiIncreaseGameSpeed()
if (FastForwardCycle >= GameCycle) { if (FastForwardCycle >= GameCycle) {
return; return;
} }
VideoSyncSpeed += 10; CyclesPerSecond++;
SetVideoSync(); SetVideoSync();
UI.StatusLine.Set(_("Faster")); UI.StatusLine.Set(_("Faster"));
} }
@ -362,12 +362,8 @@ static void UiDecreaseGameSpeed()
if (FastForwardCycle >= GameCycle) { if (FastForwardCycle >= GameCycle) {
return; return;
} }
if (VideoSyncSpeed <= 10) { if (CyclesPerSecond > 1) {
if (VideoSyncSpeed > 1) { --CyclesPerSecond;
--VideoSyncSpeed;
}
} else {
VideoSyncSpeed -= 10;
} }
SetVideoSync(); SetVideoSync();
UI.StatusLine.Set(_("Slower")); UI.StatusLine.Set(_("Slower"));
@ -381,7 +377,7 @@ static void UiSetDefaultGameSpeed()
if (FastForwardCycle >= GameCycle) { if (FastForwardCycle >= GameCycle) {
return; return;
} }
VideoSyncSpeed = 100; CyclesPerSecond = CYCLES_PER_SECOND;
SetVideoSync(); SetVideoSync();
UI.StatusLine.Set(_("Set default game speed")); UI.StatusLine.Set(_("Set default game speed"));
} }

View file

@ -1151,7 +1151,7 @@ static void InfoPanel_draw_no_selection()
y += 16; y += 16;
label.Draw(x, y, _("Cycle:")); label.Draw(x, y, _("Cycle:"));
label.Draw(x + 48, y, GameCycle); label.Draw(x + 48, y, GameCycle);
label.Draw(x + 110, y, CYCLES_PER_SECOND * VideoSyncSpeed / 100); label.Draw(x + 110, y, CyclesPerSecond);
y += 20; y += 20;
std::string nc; std::string nc;

View file

@ -99,7 +99,11 @@ static int NumRects;
static std::map<int, std::string> Key2Str; static std::map<int, std::string> Key2Str;
static std::map<std::string, int> Str2Key; static std::map<std::string, int> Str2Key;
double FrameTicks; /// Frame length in ms /// Frame length in ms
static double FrameTicks;
/// Target refresh rate for renderer
int RefreshRate = 0;
const EventCallback *Callbacks; const EventCallback *Callbacks;
@ -116,29 +120,47 @@ uint32_t SDL_CUSTOM_KEY_UP;
-- Sync -- Sync
----------------------------------------------------------------------------*/ ----------------------------------------------------------------------------*/
static int GetRefreshRate()
{
if (!RefreshRate) {
int displayCount = SDL_GetNumVideoDisplays();
SDL_DisplayMode mode;
for (int i = 0; i < displayCount; i++) {
SDL_GetDesktopDisplayMode(0, &mode);
if (mode.refresh_rate > RefreshRate) {
RefreshRate = mode.refresh_rate;
}
}
if (!RefreshRate) {
RefreshRate = 60;
}
}
return RefreshRate;
}
/** /**
** Initialise video sync. ** Initialise video sync.
** Calculate the length of video frame and any simulation skips. ** Calculate the length of video frame and any simulation skips.
** **
** @see VideoSyncSpeed @see SkipFrames @see FrameTicks ** @see CyclesPerSecond @see SkipCycles @see SkipFrames @see FrameTicks
*/ */
void SetVideoSync() void SetVideoSync()
{ {
double ms; int fps = GetRefreshRate();
int nativeFps = fps;
if (VideoSyncSpeed) { if (fps < CyclesPerSecond) {
ms = (1000.0 * 1000.0 / CYCLES_PER_SECOND) / VideoSyncSpeed; fprintf(stdout, "WARNING: Game speed is faster than monitor refresh rate.\n");
FrameTicks = 1000.0 / CyclesPerSecond;
SDL_GL_SetSwapInterval(0); // disable vsync, so we can run faster than the refresh
} else { } else {
ms = (double)INT_MAX; FrameTicks = 1000.0 / fps;
if (SDL_GL_SetSwapInterval(-1) < 0) { // try to set adaptive vsync
SDL_GL_SetSwapInterval(1); // if it failed, set vsync
}
} }
SkipFrames = ms / 400; SkipCycles = (static_cast<double>(fps) / CyclesPerSecond) - 1;
while (SkipFrames && ms / SkipFrames < 200) {
--SkipFrames;
}
ms /= SkipFrames + 1;
FrameTicks = ms / 10; DebugPrint("native fps: %d, render frame skip: %d, game cycle skip: %f\n" _C_ nativeFps _C_ Preference.FrameSkip _C_ SkipCycles);
DebugPrint("frames %d - %5.2fms\n" _C_ SkipFrames _C_ ms / 10);
} }
/*---------------------------------------------------------------------------- /*----------------------------------------------------------------------------
@ -785,8 +807,10 @@ void WaitEventsOneFrame()
} }
handleInput(NULL); handleInput(NULL);
if (!SkipGameCycle--) { if (SkipGameCycle < 0) {
SkipGameCycle = SkipFrames; SkipGameCycle += SkipCycles;
} else {
SkipGameCycle--;
} }
} }
@ -795,21 +819,10 @@ void WaitEventsOneFrame()
*/ */
static Uint32 LastTick = 0; static Uint32 LastTick = 0;
static int RefreshRate = 0;
static void RenderBenchmarkOverlay() static void RenderBenchmarkOverlay()
{ {
if (!RefreshRate) { int RefreshRate = GetRefreshRate();
int displayCount = SDL_GetNumVideoDisplays();
SDL_DisplayMode mode;
for (int i = 0; i < displayCount; i++) {
SDL_GetDesktopDisplayMode(0, &mode);
if (mode.refresh_rate > RefreshRate) {
RefreshRate = mode.refresh_rate;
}
}
}
// show a bar representing fps, where the entire bar is the max refresh rate of attached displays // show a bar representing fps, where the entire bar is the max refresh rate of attached displays
Uint32 nextTick = SDL_GetTicks(); Uint32 nextTick = SDL_GetTicks();
Uint32 frameTime = nextTick - LastTick; Uint32 frameTime = nextTick - LastTick;

View file

@ -168,8 +168,8 @@ int ClipY2; /// current clipping bottom right
static std::vector<Clip> Clips; static std::vector<Clip> Clips;
int VideoSyncSpeed = 100; /// 0 disable interrupts int CyclesPerSecond = CYCLES_PER_SECOND;
int SkipFrames; /// Skip this frames double SkipCycles; /// Skip this frames
Uint32 ColorBlack; Uint32 ColorBlack;
Uint32 ColorDarkGreen; Uint32 ColorDarkGreen;
@ -348,9 +348,10 @@ void DeInitVideo()
*/ */
static int CclSetVideoSyncSpeed(lua_State *l) static int CclSetVideoSyncSpeed(lua_State *l)
{ {
LuaCheckArgs(l, 1); LuaCheckArgs(l, 1);
VideoSyncSpeed = LuaToNumber(l, 1); RefreshRate = LuaToNumber(l, 1);
return 0; SetVideoSync();
return 0;
} }
void VideoCclRegister() void VideoCclRegister()