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.