Quantcast
Channel: Анал со зрелой
Viewing all articles
Browse latest Browse all 47

Pretending emscripten_set_main_loop Doesn’t Exist with Coroutines

$
0
0

Writing a main loop for a game usually looks something like this, an infinite loop inside the main entry point.

int main(int argc, const char** argv)
{
    app_t* app = create_app(/* params */);

    while (app_is_running(app)) {
        float dt = calc_dt();
        /* game logic here */
        /* draw to screen, etc. */
    }

    destroy_app(app);
    return 0;
}

As random stuff gets added to the game, often times some amount of state will accumulate in your main function. For example if using SDL2 and starting with some example code from their wiki, we can see quite a bit of state gets added to the main function as local variables. These little variables are usually accessed within the main loop.

A super annoying aspect of adding local state to main like this, is sometimes the loop needs to be moved into its own function, as your program doesn’t really have an actual entry point. For example, if using a library like WxWidgets it’s likely your entire game can be run within a small window, and WxWidgets will need access to a function that runs a single tick of your game. Similarly when cross-compiling for the web with emscripten the special function emscripten_set_main_loop must be used, as the Javascript environment running your game has it’s own loop, and needs to call your game periodically. In both cases the game doesn’t really have an entry point like a traditional main function, and can not run infinitely — instead a single tick must be run, and returned.

app_t* app;

void my_loop()
{
	/* game logic here */
	/* draw to screen, etc. */
}

int main(int argc, const char** argv)
{
	app = create_app(/* params */);

	while (app_is_running(app)) {
#ifdef __emscripten
		emscripten_set_main_loop(my_loop, 0, true);
#else
		float dt = calc_dt();
		/* game logic here */
		/* draw to screen, etc. */
#endif
	}

	destroy_app(app);
	return 0;
}

Unfortunately the side effect of pound defines and non-portable code starts to arise, and worse the local variables of main need to be moved elsewhere so they persist between calls to the my_loop function.

An alternative is to instead use a coroutine to preserve local variable state. With a small refactor the entire game loop can be left as-is while also implementing some compatibility with emscripten-style looping.

// Does this look familiar? It's almost identical to the first example of a
// traditional C-style main entry point and infinite loop! Lots of local variable
// state can be piled up here without much trouble. In this example it's just one
// pointer, but in a larger project there can be a lot more things here.
void loop_fn(coroutine_t* co)
{
	app_t* app = create_app(/* params */);

	while (app_is_running(app)) {
		float dt = coroutine_yield(co);
		/* game logic here */
		/* draw to screen, etc. */
	}

	destroy_app(app);
}

// The coroutine and emscripten stuff is localized to this small bit of code,
// which wouldn't need to be changed regardless of how complicated the game's
// main loop becomes.
coroutine_t* app_co;

void my_loop()
{
	float dt = calc_dt();
	coroutine_resume(app_co, dt);
}

int main(int argc, const char** argv)
{
	app_co = coroutine_make(loop_fn);

#ifdef __emscripten
	emscripten_set_main_loop(my_loop, 0, true);
#else
	while (app_is_running(app)) {
		my_loop();
	}
#endif

	coroutine_destroy(app_co);

	return 0;
}

Coroutines are not natively implemented in C or C++ (though this might change in the future for C++), and might be a bit of a new concept for many of us. The wikipedia page on coroutines admittedly kind of sucks… Personally I learned about coroutines and their utility by using the Lua programming language, which implement coroutines much like the ones in the above example code.

Actually, the coroutines in the above example code come from my own coroutine API in Cute Framework, where the underlying coroutine implementation comes from my friend edubart on GitHub by his minicoro library. Check it out! It’s rather cross-platform, not much code, and really easy to use.

Since coroutines provide a way to preserve local variables of functions it’s possible to mostly pretend emscripten_set_main_loop doesn’t even exist. This can be a great way to easily port larger or older projects to the web without heavy code refactors. Here’s an example game I’ve made using this coroutine trick. The main function is quite complicated, as there are multiple different main loops for title screen and preamble, though porting to emscripten was a breeze thanks to coroutines.

Coroutines are popular for Lua users, Unity users, and more recently they are more accessible to C users. If you’re curious about coroutines I did make some slides for a university guest lecture last year filled with a live demo — warning, the video is sped up slightly and the audio is a little hard to hear. I suggest turning on closed captioning.

Share


Viewing all articles
Browse latest Browse all 47

Trending Articles