Refactor the game event loop and add menus to the Linux build

Patch from @x-qq
Addresses #195
This commit is contained in:
2020-01-25 17:57:32 -05:00
parent 1fcbd8e922
commit 29ef8b9c1e
17 changed files with 600 additions and 155 deletions

View File

@@ -33,7 +33,6 @@
#include "enum_map.hpp"
bool All_Done = false;
sf::Event event;
sf::RenderWindow mainPtr;
short had_text_freeze = 0,num_fonts;
bool first_startup_update = true;
@@ -92,6 +91,7 @@ short missile_firer,current_monst_tactic;
short store_current_pc = 0;
sf::Clock animTimer;
extern long anim_ticks;
static void init_boe(int, char*[]);
@@ -117,26 +117,9 @@ int main(int argc, char* argv[]) {
menu_activate();
restore_cursor();
// As of year 2020, the game is causing too high CPU load on modern CPUs (i7-7700HQ), but due
// to mistakes in architecture we cannot simply use SFML's native framerate capping
// mechanism (setFramerateLimit), so we have to make our own.
//
// Note that due to the fact that various underlying pieces of code redraw, redisplay and
// pause the game as they see fit, this is not exactly framerate capping.
// ~xq
sf::Clock framerate_clock;
const sf::Int64 desired_microseconds_per_frame { 1000000 / 60}; // us / FPS
while(!All_Done) {
// If this call indicates that it did something expensive by returning true,
// we do the performance capping. The logic here is that we do not want to
// do expensive things too many times per second.
// ~xq
if(!Handle_One_Event()) continue;
const sf::Int64 remaining_time_budget = desired_microseconds_per_frame - framerate_clock.getElapsedTime().asMicroseconds();
if(remaining_time_budget > 0) sf::sleep(sf::microseconds(remaining_time_budget));
framerate_clock.restart();
}
handle_events();
close_program();
return 0;
} catch(std::exception& x) {
@@ -169,7 +152,9 @@ static void init_btn(std::shared_ptr<cButton>& btn, eBtnType type) {
void init_boe(int argc, char* argv[]) {
set_up_apple_events(argc, argv);
init_directories(argv[0]);
#ifdef __APPLE__
init_menubar(); // Do this first of all because otherwise a default File and Window menu will be seen
#endif
sync_prefs();
init_shaders();
init_tiling();
@@ -210,51 +195,61 @@ void init_boe(int argc, char* argv[]) {
showMenuBar();
}
// Return true if we redrew the screen
bool Handle_One_Event() {
static const long twentyTicks = time_in_ticks(20).asMilliseconds();
static const long fortyTicks = time_in_ticks(40).asMilliseconds();
through_sending();
if((animTimer.getElapsedTime().asMilliseconds() >= fortyTicks) && (overall_mode != MODE_STARTUP) && (anim_onscreen) && get_bool_pref("DrawTerrainAnimation", true)) {
animTimer.restart();
draw_terrain();
}
if((animTimer.getElapsedTime().asMilliseconds() > twentyTicks) && (overall_mode == MODE_STARTUP)) {
animTimer.restart();
draw_startup_anim(true);
}
clear_sound_memory();
if(map_visible && mini_map.pollEvent(event)){
if(event.type == sf::Event::Closed) {
mini_map.setVisible(false);
map_visible = false;
} else if(event.type == sf::Event::GainedFocus)
makeFrontWindow(mainPtr);
}
if(!mainPtr.pollEvent(event)) {
void handle_events() {
sf::Event currentEvent;
sf::Clock framerate_clock;
const sf::Int64 desired_microseconds_per_frame { 1000000 / 60 }; // us / FPS
while(!All_Done) {
while(mainPtr.pollEvent(currentEvent)) handle_one_event(currentEvent);
// It would be nice to have minimap inside the main game window (we have lots of screen space in fullscreen mode).
// Alternatively, minimap could live on its own thread.
// But for now we just handle events from both windows on this thread.
while(map_visible && mini_map.pollEvent(currentEvent)) handle_one_minimap_event(currentEvent);
if(changed_display_mode) {
changed_display_mode = false;
adjust_window_mode();
}
// Still no idea what this does. It's possible that this does not work at all.
flushingInput = false;
redraw_screen(REFRESH_NONE);
return true;
// Ideally this call should update all of the things that are happening in the world current tick.
// NOTE that update does not mean draw.
update_everything();
// Ideally, this should be the only draw call that is done in a cycle.
redraw_everything();
// Prevent the loop from executing too fast.
const sf::Int64 remaining_time_budget = desired_microseconds_per_frame - framerate_clock.getElapsedTime().asMicroseconds();
if(remaining_time_budget > 0) sf::sleep(sf::microseconds(remaining_time_budget));
framerate_clock.restart();
}
}
void handle_one_event(const sf::Event& event) {
// What does this do and should it be here?
through_sending();
// What does this do and should it be here?
clear_sound_memory();
// Check if the menubar wants to handle this event.
if(menuBarProcessEvent(event)) return;
switch(event.type) {
case sf::Event::KeyPressed:
if(flushingInput) return false;
if(!(event.key.*systemKey))
handle_keystroke(event);
if(flushingInput) return;
if(!(event.key.*systemKey)) handle_keystroke(event);
break;
case sf::Event::MouseButtonPressed:
if(flushingInput) return false;
Mouse_Pressed();
if(flushingInput) return;
Mouse_Pressed(event);
break;
case sf::Event::MouseLeft:
@@ -263,18 +258,16 @@ bool Handle_One_Event() {
break;
case sf::Event::GainedFocus:
Handle_Update();
makeFrontWindow(mainPtr);
change_cursor({event.mouseMove.x, event.mouseMove.y});
return true;
return;
case sf::Event::MouseMoved:
change_cursor({event.mouseMove.x, event.mouseMove.y});
flushingInput = false;
return false;
return;
case sf::Event::MouseWheelMoved:
if(flushingInput) return false;
if(flushingInput) return;
handle_scroll(event);
break;
@@ -308,17 +301,47 @@ bool Handle_One_Event() {
default:
break; // There's several events we don't need to handle at all
}
flushingInput = false; // TODO: Could there be a case when the key and mouse input that needs to be flushed has other events interspersed?
return true;
}
void handle_one_minimap_event(const sf::Event& event) {
if(event.type == sf::Event::Closed) {
mini_map.setVisible(false);
map_visible = false;
} else if(event.type == sf::Event::GainedFocus) {
makeFrontWindow(mainPtr);
}
}
void Handle_Update() {
redraw_screen(REFRESH_NONE);
void update_terrain_animation() {
static const long fortyTicks = time_in_ticks(40).asMilliseconds();
if(overall_mode == MODE_STARTUP) return;
if(!get_bool_pref("DrawTerrainAnimation", true)) return;
if(!anim_onscreen) return;
if(animTimer.getElapsedTime().asMilliseconds() < fortyTicks) return;
anim_ticks++;
animTimer.restart();
}
void update_startup_animation() {
static const long twentyTicks = time_in_ticks(20).asMilliseconds();
if(overall_mode != MODE_STARTUP) return;
if(animTimer.getElapsedTime().asMilliseconds() < twentyTicks) return;
draw_startup_anim(true);
animTimer.restart();
}
void update_everything() {
update_terrain_animation();
update_startup_animation();
}
void redraw_everything() {
redraw_screen(REFRESH_ALL);
if(map_visible) draw_map(false);
else mini_map.setVisible(false);
}
static void handleUpdateWhileScrolling(volatile bool& doneScrolling, int refresh) {
@@ -329,8 +352,9 @@ static void handleUpdateWhileScrolling(volatile bool& doneScrolling, int refresh
mainPtr.setActive(false);
}
// TODO: Pass the event object around instead of keeping a global one
void Mouse_Pressed() {
void Mouse_Pressed(sf::Event const & event) {
// What is this stuff? Why is it here?
if(had_text_freeze > 0) {
had_text_freeze--;
return;
@@ -364,6 +388,7 @@ void Mouse_Pressed() {
} else All_Done = handle_action(event);
} else All_Done = handle_startup_press({event.mouseButton.x, event.mouseButton.y});
// Why does every mouse click activate a menu?
menu_activate();
}