avatar ryu

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 Touhou 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:

int main(int argc, char *argv[])
{
    // omitted: almost 200 lines of initializations for all kinds of things

    while (!g_quit) {

        update_fps();
        SDL_Event sdl_event;
        while (SDL_PollEvent(&sdl_event) != 0) {
            if (sdl_event.type == SDL_QUIT) {
                g_quit = 1;
            }
        }
        (g_pad) ? update_player_input_pad_yes(key_state) :
            update_player_input_pad_no(key_state);
        update_user_input(key_state);
        SDL_RenderClear(g_renderer);
        if (g_scene == scene_loadscreen)
            g_scene++;

        / * Scene Init */
        if (!g_scene_initialized) {
            mutex = SDL_CreateMutex();
            if (mutex == NULL) {
                //TODO error
                g_quit = 1;
            }
            g_data.mutex = mutex;
            g_data.assets_loaded = 0;
            load_thread_done = false;
            g_data.thread_done = &load_thread_done;
            switch (g_scene) {
            case scene_main:
                //SDL_RenderSetLogicalSize(renderer, 320, 240);
                //SDL_RenderSetScale(renderer, 2, 2);
                //SDL_SetWindowSize(window, 640, 480);
                scene_main_set_data();
                break;
            case scene_game:
                scene_game_set_data();
                break;
            }
            g_surfaces = (SDL_Surface**) calloc(g_data.img_t,
                        sizeof(SDL_Surface*));
            g_textures = (SDL_Texture**) calloc(g_data.img_t,
                        sizeof(SDL_Texture*));
            g_sfx = (Mix_Chunk**) calloc(g_data.sfx_t, sizeof(Mix_Chunk*));
            g_bgm = (Mix_Music**) calloc(g_data.bgm_t, sizeof(Mix_Music*));

            g_data.textures = g_textures;
            g_data.surfaces = g_surfaces;
            g_data.sfx = g_sfx;
            g_data.bgm = g_bgm;
            g_data.assets_to_load = g_data.img_t + g_data.sfx_t + g_data.bgm_t;

            thread = SDL_CreateThread(scene_load, "load_scene",
                        (void*) &g_data);
            g_scene_initialized = true;
            scene_load_init();
        }
        if (!g_load_comp) {
            scene_get_textures(g_data.surfaces, g_data.textures, g_data.mutex,
                    g_data.img_t, &(g_data.assets_loaded));
            scene_load_calc();
            goto AFTER_SCENE_CALC;
        }
POST_LOAD_INIT:
        if (g_load_comp && !g_scene_initialized2) {
        switch (g_scene) {
            case scene_main:
                scene_main_init();
                break;
            case scene_game:
                scene_game_init();
                break;
            }
            g_scene_initialized2 = true;
        }
        /* Calc */
        switch (g_scene) {
        case scene_main:
            scene_main_calc();
            break;
        case scene_game:
            scene_game_calc();
            break;
        }
AFTER_SCENE_CALC:
        if (g_load_comp && !g_scene_initialized2)
            goto POST_LOAD_INIT;
        if (g_user_input.resolution_switch == 1) {
            switch (g_settings.scale) {
            case 0:
                g_settings.scale = 1;
                update_window();
                break;
            case 1:
                g_settings.scale = 0;
                update_window();
                break;
            }
        }
        if (g_user_input.fullscreen_switch == 1) {
            switch (g_settings.fullscreen) {
            case false:
                g_settings.fullscreen = true;
                update_window();
                break;
            case true:
                g_settings.fullscreen = false;
                update_window();
                break;
            }
        }
        /* Draw */
        SDL_SetRenderDrawBlendMode(g_renderer, SDL_BLENDMODE_BLEND);
        if (g_settings.widescreen) {
            SDL_RenderSetViewport(g_renderer, NULL);
            switch (g_settings.wallpaper) {
            case wp_none:
                SDL_SetRenderDrawColor(g_renderer, 0, 0, 0, 255);
                break;
            case wp_pattern1:
                SDL_SetRenderDrawColor(g_renderer, 0, 0, 255, 255);
                break;
            case wp_pattern2:
                SDL_SetRenderDrawColor(g_renderer, 155, 0, 110, 255);
                break;
            }
            SDL_RenderFillRect(g_renderer, NULL);
            SDL_RenderSetViewport(g_renderer, &view);
        }
        if (!g_load_comp) {
            scene_load_draw();
            goto AFTER_SCENE_DRAW;
        }
        switch (g_scene) {
        case scene_main:
            scene_main_draw();
            break;
        case scene_game:
            scene_game_draw();
            break;
        }
AFTER_SCENE_DRAW:
        /* Scanlines */
#ifdef SCANLINES
        if (g_settings.scale == 1 ) {
            SDL_RenderSetScale(g_renderer, 1, 1);
            SDL_SetRenderDrawBlendMode(g_renderer, SDL_BLENDMODE_BLEND);
            SDL_SetRenderDrawColor(g_renderer, 0, 0, 0, 120);
            int i = 479;
            while (i >= 0) {
                SDL_RenderDrawLine(g_renderer, 0, i, 640-1, i);
                i = i-2;
            }
            SDL_RenderSetScale(g_renderer, 2, 2);
        }
#endif
        /* Clean */
        if (!g_load_comp) {
            scene_load_finish();
        }
        if (g_scene_finished) {
            switch (g_scene) {
            case scene_main:
                scene_main_finish();
                break;
            case scene_game:
                scene_game_fin();
                break;
            }
            if (Mix_PlayingMusic() != 0) {
                Mix_HaltMusic();
            }
            g_scene_initialized = false;
            g_scene_initialized2 = false;
            g_scene_finished = false;
            g_load_comp = false;
            for (int i = 0; i < g_data.img_t; i++) {
                SDL_DestroyTexture(g_textures[i]);
            }
            for (int i = 0; i < g_data.sfx_t; i++) {
                Mix_FreeChunk(g_sfx[i]);
            }
            for (int i = 0; i < g_data.bgm_t; i++) {
                Mix_FreeMusic(g_bgm[i]);
            }
            free(g_surfaces); g_surfaces = NULL;
            free(g_textures); g_textures = NULL;
            free(g_sfx); g_sfx = NULL;
            free(g_bgm); g_bgm = NULL;
            SDL_DestroyMutex(mutex); mutex = NULL;
            SDL_DetachThread(thread); thread = NULL;
        }
        //printf("%.1f\n", fps);
        SDL_RenderPresent(g_renderer);
        wait_fps();

    }

    // free resources
    ...
}

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!):

int main (int argc, char *argv[])
{
    memset(&scene, 0, sizeof(scenemngr));
    // Engine initialization
    if (saten_init("saturn_engine_demo", 320, 240, SATEN_MRBLOAD) < 0)
        fprintf(stderr, "Init error...\n");
    // Setting up unique IDs for scenes //TODO let a function handle this?
    scene.root.uid = 0;
    scene.title.uid = 1;
    scene.title_menu.uid = 2;
    scene.title_menu_settings.uid = 3;
    scene.game.uid = 4;
    scene.load.uid = 255;
    // Create root scene
    scene.root = saten_scene_create(scene.root, scene_root_init,
            scene_root_update, scene_root_draw, scene_root_quit,
            "script/load_resources.rb");
    // Run the game loop
    saten_run();

    return 0;
}

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:

void saten_game(void)
{
    // Traverse top-bottom (quit scenes)
    for (int i = SATEN_DARR_SIZE(saten_darr_scene)-1; i >= 0; i--) {
        if (saten_darr_scene[i].quit_flag)
            if (saten_darr_scene[i].quit != NULL)
                saten_darr_scene[i].quit();
    }

    // Traverse bottom-top (play game)
    for (int i=saten_scene_start.id;i< SATEN_DARR_SIZE(saten_darr_scene);i++) {
        if (!saten_darr_scene[i].init_flag) {
            if (saten_darr_scene[i].init != NULL)
                saten_darr_scene[i].init();
        } else {
            if (saten_darr_scene[i].update != NULL) 
                saten_darr_scene[i].update(
                        (i == SATEN_DARR_SIZE(saten_darr_scene)-1));
                // ^only top scene gets user control
            if (saten_darr_scene[i].draw != NULL)
                saten_darr_scene[i].draw();
            saten_darr_scene[i].framecnt++;
        }
    }
}

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 simplify the code required to setup a load screen.

Leave Comment | Comments (0) |
More: media
Layout: ellis ellis darkside
17913:17187