Scene handling in Saturn Engine (WT)

After a rough start with teaching myself game programming I spent many hours of 2017 and 2018 working on a Tohou clone based on this tutorial. Eventually I made it to the point where I could almost call the program a game: you could move a sprite, shoot bullets, destroy enemies that shoot their own bullets and so on. But there was a problem: this was my first C program that incorporated more than a main function, so my general programming skills with the language were really shallow at the time. Naturally my codebase reflected that. Working with the code I had written felt far from pleasant, especially because I had locked myself in. Had I continued working on the game without rewriting the entire thing there would have been little room for me to try out new things and more advanced concepts. So I started over with the goal to write a more general engine that I could use as the basis for future projects as well. That was back in march. Setting up the basics (input handling, rendering, framerate control etc.) took me a while, but now I finally reached the point where I could write a new scene management system. Here's the main portion of my old main() function:

001  int main(int argc, char *argv[])
002  {
003      // omitted: almost 200 lines of initializations for all kinds of things
004  
005      while (!g_quit) {
006  
007          update_fps();
008          SDL_Event sdl_event;
009          while (SDL_PollEvent(&sdl_event) != 0) {
010              if (sdl_event.type == SDL_QUIT) {
011                  g_quit = 1;
012              }
013          }
014          (g_pad) ? update_player_input_pad_yes(key_state) :
015              update_player_input_pad_no(key_state);
016          update_user_input(key_state);
017          SDL_RenderClear(g_renderer);
018          if (g_scene == scene_loadscreen)
019              g_scene++;
020  
021          / * Scene Init */
022          if (!g_scene_initialized) {
023              mutex = SDL_CreateMutex();
024              if (mutex == NULL) {
025                  //TODO error
026                  g_quit = 1;
027              }
028              g_data.mutex = mutex;
029              g_data.assets_loaded = 0;
030              load_thread_done = false;
031              g_data.thread_done = &load_thread_done;
032              switch (g_scene) {
033              case scene_main:
034                  //SDL_RenderSetLogicalSize(renderer, 320, 240);
035                  //SDL_RenderSetScale(renderer, 2, 2);
036                  //SDL_SetWindowSize(window, 640, 480);
037                  scene_main_set_data();
038                  break;
039              case scene_game:
040                  scene_game_set_data();
041                  break;
042              }
043              g_surfaces = (SDL_Surface**) calloc(g_data.img_t,
044                          sizeof(SDL_Surface*));
045              g_textures = (SDL_Texture**) calloc(g_data.img_t,
046                          sizeof(SDL_Texture*));
047              g_sfx = (Mix_Chunk**) calloc(g_data.sfx_t, sizeof(Mix_Chunk*));
048              g_bgm = (Mix_Music**) calloc(g_data.bgm_t, sizeof(Mix_Music*));
049  
050              g_data.textures = g_textures;
051              g_data.surfaces = g_surfaces;
052              g_data.sfx = g_sfx;
053              g_data.bgm = g_bgm;
054              g_data.assets_to_load = g_data.img_t + g_data.sfx_t + g_data.bgm_t;
055  
056              thread = SDL_CreateThread(scene_load, "load_scene",
057                          (void*) &g_data);
058              g_scene_initialized = true;
059              scene_load_init();
060          }
061          if (!g_load_comp) {
062              scene_get_textures(g_data.surfaces, g_data.textures, g_data.mutex,
063                      g_data.img_t, &(g_data.assets_loaded));
064              scene_load_calc();
065              goto AFTER_SCENE_CALC;
066          }
067  POST_LOAD_INIT:
068          if (g_load_comp && !g_scene_initialized2) {
069          switch (g_scene) {
070              case scene_main:
071                  scene_main_init();
072                  break;
073              case scene_game:
074                  scene_game_init();
075                  break;
076              }
077              g_scene_initialized2 = true;
078          }
079          /* Calc */
080          switch (g_scene) {
081          case scene_main:
082              scene_main_calc();
083              break;
084          case scene_game:
085              scene_game_calc();
086              break;
087          }
088  AFTER_SCENE_CALC:
089          if (g_load_comp && !g_scene_initialized2)
090              goto POST_LOAD_INIT;
091          if (g_user_input.resolution_switch == 1) {
092              switch (g_settings.scale) {
093              case 0:
094                  g_settings.scale = 1;
095                  update_window();
096                  break;
097              case 1:
098                  g_settings.scale = 0;
099                  update_window();
100                  break;
101              }
102          }
103          if (g_user_input.fullscreen_switch == 1) {
104              switch (g_settings.fullscreen) {
105              case false:
106                  g_settings.fullscreen = true;
107                  update_window();
108                  break;
109              case true:
110                  g_settings.fullscreen = false;
111                  update_window();
112                  break;
113              }
114          }
115          /* Draw */
116          SDL_SetRenderDrawBlendMode(g_renderer, SDL_BLENDMODE_BLEND);
117          if (g_settings.widescreen) {
118              SDL_RenderSetViewport(g_renderer, NULL);
119              switch (g_settings.wallpaper) {
120              case wp_none:
121                  SDL_SetRenderDrawColor(g_renderer, 0, 0, 0, 255);
122                  break;
123              case wp_pattern1:
124                  SDL_SetRenderDrawColor(g_renderer, 0, 0, 255, 255);
125                  break;
126              case wp_pattern2:
127                  SDL_SetRenderDrawColor(g_renderer, 155, 0, 110, 255);
128                  break;
129              }
130              SDL_RenderFillRect(g_renderer, NULL);
131              SDL_RenderSetViewport(g_renderer, &view);
132          }
133          if (!g_load_comp) {
134              scene_load_draw();
135              goto AFTER_SCENE_DRAW;
136          }
137          switch (g_scene) {
138          case scene_main:
139              scene_main_draw();
140              break;
141          case scene_game:
142              scene_game_draw();
143              break;
144          }
145  AFTER_SCENE_DRAW:
146          /* Scanlines */
147  #ifdef SCANLINES
148          if (g_settings.scale == 1 ) {
149              SDL_RenderSetScale(g_renderer, 1, 1);
150              SDL_SetRenderDrawBlendMode(g_renderer, SDL_BLENDMODE_BLEND);
151              SDL_SetRenderDrawColor(g_renderer, 0, 0, 0, 120);
152              int i = 479;
153              while (i >= 0) {
154                  SDL_RenderDrawLine(g_renderer, 0, i, 640-1, i);
155                  i = i-2;
156              }
157              SDL_RenderSetScale(g_renderer, 2, 2);
158          }
159  #endif
160          /* Clean */
161          if (!g_load_comp) {
162              scene_load_finish();
163          }
164          if (g_scene_finished) {
165              switch (g_scene) {
166              case scene_main:
167                  scene_main_finish();
168                  break;
169              case scene_game:
170                  scene_game_fin();
171                  break;
172              }
173              if (Mix_PlayingMusic() != 0) {
174                  Mix_HaltMusic();
175              }
176              g_scene_initialized = false;
177              g_scene_initialized2 = false;
178              g_scene_finished = false;
179              g_load_comp = false;
180              for (int i = 0; i < g_data.img_t; i++) {
181                  SDL_DestroyTexture(g_textures[i]);
182              }
183              for (int i = 0; i < g_data.sfx_t; i++) {
184                  Mix_FreeChunk(g_sfx[i]);
185              }
186              for (int i = 0; i < g_data.bgm_t; i++) {
187                  Mix_FreeMusic(g_bgm[i]);
188              }
189              free(g_surfaces); g_surfaces = NULL;
190              free(g_textures); g_textures = NULL;
191              free(g_sfx); g_sfx = NULL;
192              free(g_bgm); g_bgm = NULL;
193              SDL_DestroyMutex(mutex); mutex = NULL;
194              SDL_DetachThread(thread); thread = NULL;
195          }
196          //printf("%.1f\n", fps);
197          SDL_RenderPresent(g_renderer);
198          wait_fps();
199  
200      }
201  
202      // free resources
203      ...
204  }

What a mess! The goto statements were quick hacks to avoid rewriting the entire program. Here's what main() of my new engine's demo program looks like (so far - it's a work in progress!):

001  int main (int argc, char *argv[])
002  {
003      memset(&scene, 0, sizeof(scenemngr));
004      // Engine initialization
005      if (saten_init("saturn_engine_demo", 320, 240, SATEN_MRBLOAD) < 0)
006          fprintf(stderr, "Init error...\n");
007      // Setting up unique IDs for scenes //TODO let a function handle this?
008      scene.root.uid = 0;
009      scene.title.uid = 1;
010      scene.title_menu.uid = 2;
011      scene.title_menu_settings.uid = 3;
012      scene.game.uid = 4;
013      scene.load.uid = 255;
014      // Create root scene
015      scene.root = saten_scene_create(scene.root, scene_root_init,
016              scene_root_update, scene_root_draw, scene_root_quit,
017              "script/load_resources.rb");
018      // Run the game loop
019      saten_run();
020  
021      return 0;
022  }

Now saten_run() only checks that a scene (in this case "root") has been created and passes saten_game() to saten_core_run() which runs its parameter in a loop together with updating inputs and the framerate control. Which leaves us with the scene management of the loop reduced to this:

001  void saten_game(void)
002  {
003      // Traverse top-bottom (quit scenes)
004      for (int i = SATEN_DARR_SIZE(saten_darr_scene)-1; i >= 0; i--) {
005          if (saten_darr_scene[i].quit_flag)
006              if (saten_darr_scene[i].quit != NULL)
007                  saten_darr_scene[i].quit();
008      }
009  
010      // Traverse bottom-top (play game)
011      for (int i=saten_scene_start.id;i< SATEN_DARR_SIZE(saten_darr_scene);i++) {
012          if (!saten_darr_scene[i].init_flag) {
013              if (saten_darr_scene[i].init != NULL)
014                  saten_darr_scene[i].init();
015          } else {
016              if (saten_darr_scene[i].update != NULL) 
017                  saten_darr_scene[i].update(
018                          (i == SATEN_DARR_SIZE(saten_darr_scene)-1));
019                  // ^only top scene gets user control
020              if (saten_darr_scene[i].draw != NULL)
021                  saten_darr_scene[i].draw();
022              saten_darr_scene[i].framecnt++;
023          }
024      }
025  }

And this part is not even visual to the game progammer who only needs to worry about properly creating and quitting his scenes. The engine now basically runs the scenes created by the developer in first to last order, passing each update function a boolean to let it know whether it's at the top of the stack. One use case for this is to make sure only the top-most scene reacts to user input (for example when the player opens a menu). On the other hand scenes are removed in last to first order. This was necessary because initially all resources were loaded into dynamic arrays shared by all scenes, so scenes had to be quit top-down to make sure the resource arrays would be resized appropriately when a scene's resources were freed. In the end I incorporated the resources into the scene structure for more flexibility. The downside here is that requesting a resource now requires an argument to specify the related scene, e.g. saten_resource_sprite(scene.title, BACKGROUND) where BACKGROUND is defined as the resource ID. I feel these resource accessors are quite unwieldy so coming up with shorter ones is my current challenge.
I'll show what a scene in my engine actually looks like in another post. It's all still very much a work in progress after all and I may or may not add another function to scenes to simply the code required to setup a load screen.