#include "GameEngine.h" #include "AssetManager.h" #include "BitmapFont.h" #include "ObjectStore.h" #include "Renderer.h" #include "SDL.h" #include "SDL_error.h" #include "SDL_events.h" #include "SDL_hints.h" #include "SDL_image.h" #include "SDL_keycode.h" #include "SDL_mixer.h" #include "SDL_pixels.h" #include "SDL_platform.h" #include "SDL_rect.h" #include "SDL_render.h" #include "SDL_surface.h" #include "SDL_system.h" #include "SDL_timer.h" #include "SDL_ttf.h" #include "SDL_video.h" #include "Window.h" #include "aliases.h" #include "constants.h" #include "reusable_ids.h" #include #include #include #include #if DEBUG_ENGINE #include #endif /* DEBUG_ENGINE */ INTERNAL struct game_engine { bool running; f32 delta; u64 last_time; Window window; SDL_Event event; ObjectStore *object_store; AssetManager *asset_manager; } GameEngine; bool init_engine(const char *game_name, WindowSize window_size, SDL_Color window_background_color, bool full_screen, u8 audio_tracks) { bool initialisation_complete = true; const char *current_platform = SDL_GetPlatform(); if (strcmp(current_platform, "Linux") == 0) { SDL_SetHint(SDL_HINT_VIDEODRIVER, "wayland,x11"); } if (SDL_Init(SDL_INIT_EVERYTHING) != 0) { #if DEBUG_ENGINE printf("Failed to initialise SDL. Error: %s\n", SDL_GetError()); #endif /* DEBUG_ENGINE */ return false; } u32 img_flags = IMG_INIT_PNG; if (strcmp(current_platform, "Android") != 0) { // Apparently, Android devices don't support TIFF images, so we only enable // TIFF support when we are not running Android. // // TODO (Abdelrahman): Check if iOS devices support TIFF images. img_flags |= IMG_INIT_TIF; } if ((IMG_Init(img_flags) & img_flags) != img_flags) { #if DEBUG_ENGINE printf("Failed to initialise SDL_image. Error: %s\n", IMG_GetError()); #endif /* DEBUG_ENGINE */ initialisation_complete = false; } if (TTF_Init() < 0) { #if DEBUG_ENGINE printf("Failed to initialise SDL_ttf. Error: %s\n", TTF_GetError()); #endif /* DEBUG_ENGINE */ initialisation_complete = false; } // We're only interested in initialising SDL_mixer with WAVE files, so no // need to pass any flags if (Mix_Init(0) != 0) { #if DEBUG_ENGINE printf("Failed to initialise SDL_mixer. Error: %s\n", Mix_GetError()); #endif /* DEBUG_ENGINE */ initialisation_complete = false; } if (Mix_OpenAudio(48000, MIX_DEFAULT_FORMAT, 2, 2048) < 0) { #if DEBUG_ENGINE printf("Failed to open audio device. Error: %s\n", Mix_GetError()); #endif /* DEBUG_ENGINE */ initialisation_complete = false; } if (audio_tracks > 0) { if (Mix_AllocateChannels(audio_tracks) != audio_tracks) { #if DEBUG_ENGINE printf("Failed to allocate the required tracks.\n"); #endif /* DEBUG_ENGINE */ initialisation_complete = false; } } if (!init_window(&(GameEngine.window), game_name, window_size, window_background_color, full_screen)) { #if DEBUG_ENGINE printf("Failed to initialise window\n"); #endif /* DEBUG_ENGINE */ initialisation_complete = false; } GameEngine.object_store = create_object_store(); if (!(GameEngine.object_store)) { initialisation_complete = false; } GameEngine.asset_manager = create_asset_manager(); if (!(GameEngine.asset_manager)) { initialisation_complete = false; } if (!initialisation_complete) { close_engine(); return false; } GameEngine.running = true; GameEngine.delta = 0.0f; GameEngine.last_time = SDL_GetTicks64(); #if DEBUG_ENGINE GameEngine.delta = 1.0f / 60.0f; #endif /* DEBUG_ENGINE */ #if DEBUG_ENGINE printf("Game initialised successfully\n"); #endif /* DEBUG_ENGINE */ return true; } bool game_running() { return GameEngine.running; } void stop_game() { GameEngine.running = false; } void close_engine() { destroy_asset_manager(&(GameEngine.asset_manager)); destroy_object_store(&(GameEngine.object_store)); destroy_window(&(GameEngine.window)); GameEngine.last_time = 0.0f; GameEngine.delta = 0.0f; GameEngine.running = false; Mix_CloseAudio(); Mix_Quit(); TTF_Quit(); IMG_Quit(); SDL_Quit(); #if DEBUG_ENGINE printf("Game closed successfully\n"); #endif /* DEBUG_ENGINE */ } i64 get_time() { return SDL_GetTicks64(); } ObjectID add_object(void *object, EventHandlerPtr event_handler, UpdateFuncPtr update_func, RenderFuncPtr render_func) { if (!(GameEngine.running) || !(GameEngine.object_store)) { return -1; } return add_object_to_store(GameEngine.object_store, object, event_handler, update_func, render_func); } #ifdef __ANDROID__ const char *get_android_internal_storage(void) { return SDL_AndroidGetInternalStoragePath(); } #endif // __ANDROID__ void remove_object(ObjectID *id) { if (!(GameEngine.running) || !(GameEngine.object_store)) { return; } remove_object_from_store(GameEngine.object_store, id); } AssetID load_asset_from_file(const char *path, AssetType type) { return type == ASSET_TYPE_IMAGE ? load_asset_to_manager(GameEngine.asset_manager, path, type, GameEngine.window.renderer, NULL) : load_asset_to_manager(GameEngine.asset_manager, path, type, NULL, NULL); } void unload_asset(AssetID *id) { if (*id < 0) { return; } remove_asset_from_manager(GameEngine.asset_manager, id); } #ifdef __ANDROID__ BitmapFont *load_bitmap_font(const char *filepath, u64 tile_width, u64 tile_height) { return create_bitmap_font(GameEngine.asset_manager, GameEngine.window.renderer, filepath, tile_width, tile_height); } void unload_bitmap_font(BitmapFont **font) { destroy_bitmap_font(GameEngine.asset_manager, font); } #endif /* ifdef __ANDROID__ */ WindowSize get_window_defined_size() { return GameEngine.window.defined_size; } WindowSize get_window_real_size() { return GameEngine.window.real_size; } WindowDrawingArea get_window_drawing_area() { return GameEngine.window.drawing_area; } void draw_rectangle(RenderRect rect, SDL_Color color) { draw_rect(GameEngine.window.renderer, color, rect.x, rect.y, rect.width, rect.height); } void fill_rectangle(RenderRect rect, SDL_Color color) { fill_rect(GameEngine.window.renderer, color, rect.x, rect.y, rect.width, rect.height); } void clip_texture_render_rects_to_drawing_area(RenderRect *src, RenderRect *dest) { if (!dest) { return; } WindowDrawingArea area = get_window_drawing_area(); // If the rectangle is completely out of bounds, set its width and height to 0 if (dest->x > area.x + area.width || dest->x + dest->width < area.x || dest->y > area.y + area.height || dest->y + dest->height < area.y) { if (src) { src->width = 0.0f; src->height = 0.0f; } dest->width = 0.0f; dest->height = 0.0f; return; } f32 x_offset = 0.0f; f32 y_offset = 0.0f; f32 x_clip = 0.0f; f32 y_clip = 0.0f; // If the rectangle is partially out of the horizontal bounds of the drawing // area, calculate how much of its width will be clipped if (dest->x < area.x) { // When an object goes out of the left border of the screen, we need to both // clip and offset the source and destination rectangles to ensure that the // object is rendered properly x_clip = x_offset = area.x - dest->x; } else if (dest->x + dest->width > area.x + area.width) { x_clip = (dest->x + dest->width) - (area.x + area.width); } // If the rectangle is partially out of the vertical bounds of the drawing // area, calculate how much of its height will be clipped if (dest->y < area.y) { // When an object goes out of the top border of the screen, we need to both // clip and offset the source and destination rectangles to ensure that the // object is rendered properly y_clip = y_offset = area.y - dest->y; } else if (dest->y + dest->height > area.y + area.height) { y_clip = (dest->y + dest->height) - (area.y + area.height); } if (src) { // Offset and clipping of source rectangle needs to be calculated based on // the ratio of the x_clip and y_clip variables to the width and height of // the destination rectangle respectively. This is because the source // rectangle's width and height might be different from the destination's src->x += ((x_offset / dest->width) * src->width); src->y += ((y_offset / dest->height) * src->height); src->width *= 1 - (x_clip / dest->width); src->height *= 1 - (y_clip / dest->height); } dest->x += x_offset; dest->y += y_offset; dest->width -= x_clip; dest->height -= y_clip; } RenderRect get_texture_asset_rect(AssetID id) { RenderRect rect = {0.0f, 0.0f, 0.0f, 0.0f}; SDL_Texture *texture = get_texture(GameEngine.asset_manager, id); if (texture) { i32 w = 0; i32 h = 0; if (SDL_QueryTexture(texture, NULL, NULL, &w, &h) == 0) { rect.width = w; rect.height = h; } } return rect; } void render_texture_asset(AssetID id, const RenderRect *src, const RenderRect *dest) { if (id < 0) { return; } SDL_Texture *texture = get_texture(GameEngine.asset_manager, id); if (!texture) { return; } render_texture(GameEngine.window.renderer, texture, src, dest); } void modulate_texture_color(AssetID id, SDL_Color color) { if (id < 0) { return; } SDL_Texture *texture = get_texture(GameEngine.asset_manager, id); if (!texture) { return; } set_texture_color_modulation(texture, color); } void modulate_texture_alpha(AssetID id, u8 alpha) { if (id < 0) { return; } SDL_Texture *texture = get_texture(GameEngine.asset_manager, id); if (!texture) { return; } set_texture_alpha_modulation(texture, alpha); } TextSize get_text_size_from_font_asset(AssetID id, const char *text) { TextSize output = {}; if (id < 0) { return output; } TTF_Font *font = get_font(GameEngine.asset_manager, id); if (!font) { return output; } TTF_SizeUTF8(font, text, &(output.width), &(output.height)); return output; } void set_font_asset_size(AssetID id, u32 size) { if (id < 0) { return; } TTF_Font *font = get_font(GameEngine.asset_manager, id); if (!font) { return; } TTF_SetFontSize(font, size); } i32 set_font_direction(AssetID id, FontDirection direction) { if (id < 0) { return -2; } TTF_Font *font = get_font(GameEngine.asset_manager, id); if (!font) { return -2; } return TTF_SetFontDirection(font, (TTF_Direction)direction); } i32 set_font_script_name(AssetID id, const char *script) { if (id < 0) { return -2; } TTF_Font *font = get_font(GameEngine.asset_manager, id); if (!font) { return -2; } return TTF_SetFontScriptName(font, script); } void render_text_with_font_asset(AssetID id, const char *text, const RenderRect *src, RenderRect dest, SDL_Color color) { if (id < 0) { return; } TTF_Font *font = get_font(GameEngine.asset_manager, id); if (!font) { return; } SDL_Surface *surface = TTF_RenderUTF8_Solid(font, text, color); if (!surface) { return; } render_surface(GameEngine.window.renderer, surface, src, &dest); SDL_FreeSurface(surface); surface = NULL; } #ifdef __ANDROID__ void render_text_with_bitmap_font(BitmapFont *font, const char *text, u64 font_size, u64 start_index, RenderRect dest, SDL_Color color) { render_bitmap_font_text(GameEngine.asset_manager, GameEngine.window.renderer, font, font_size, text, start_index, dest, color); } #endif /* ifdef __ANDROID__ */ i8 play_audio_asset(AssetID id, i16 loops, bool fade_in, u32 fade_length_in_ms) { if (id < 0) { return -1; } Mix_Chunk *chunk = get_audio_chunk(GameEngine.asset_manager, id); if (!chunk) { return -1; } if (fade_in) { return Mix_FadeInChannel(-1, chunk, loops, fade_length_in_ms); } else { return Mix_PlayChannel(-1, chunk, loops); } } void pause_audio_asset_on_track(AssetID id, u8 track) { if (id < 0) { return; } const Mix_Chunk *chunk = get_audio_chunk(GameEngine.asset_manager, id); if (!chunk || chunk != Mix_GetChunk(track)) { return; } Mix_Pause(track); } void resume_audio_asset_on_track(AssetID id, u8 track) { if (id < 0) { return; } const Mix_Chunk *chunk = get_audio_chunk(GameEngine.asset_manager, id); if (!chunk || chunk != Mix_GetChunk(track)) { return; } Mix_Resume(track); } void stop_audio_asset_on_track(AssetID id, u8 track) { if (id < 0) { return; } const Mix_Chunk *chunk = get_audio_chunk(GameEngine.asset_manager, id); if (!chunk || chunk != Mix_GetChunk(track)) { return; } Mix_HaltChannel(track); } void fade_out_audio_asset_on_track(AssetID id, u8 track, u32 fade_length_in_ms) { if (id < 0) { return; } const Mix_Chunk *chunk = get_audio_chunk(GameEngine.asset_manager, id); if (!chunk || chunk != Mix_GetChunk(track)) { return; } Mix_FadeOutChannel(track, fade_length_in_ms); } void pause_all_instances_of_audio_asset(AssetID id) { if (id < 0) { return; } const Mix_Chunk *chunk = get_audio_chunk(GameEngine.asset_manager, id); if (!chunk) { return; } u64 channel_count = Mix_AllocateChannels(-1); for (size_t i = 0; i < channel_count; ++i) { if (chunk == Mix_GetChunk(i)) { Mix_Pause(i); } } } void resume_all_instances_of_audio_asset(AssetID id) { if (id < 0) { return; } const Mix_Chunk *chunk = get_audio_chunk(GameEngine.asset_manager, id); if (!chunk) { return; } u64 channel_count = Mix_AllocateChannels(-1); for (size_t i = 0; i < channel_count; ++i) { if (chunk == Mix_GetChunk(i)) { Mix_Resume(i); } } } void stop_all_instances_of_audio_asset(AssetID id) { if (id < 0) { return; } const Mix_Chunk *chunk = get_audio_chunk(GameEngine.asset_manager, id); if (!chunk) { return; } u64 channel_count = Mix_AllocateChannels(-1); for (size_t i = 0; i < channel_count; ++i) { if (chunk == Mix_GetChunk(i)) { Mix_HaltChannel(i); } } } void fade_out_all_instances_of_audio_asset(AssetID id, u32 fade_length_in_ms) { if (id < 0) { return; } const Mix_Chunk *chunk = get_audio_chunk(GameEngine.asset_manager, id); if (!chunk) { return; } u64 channel_count = Mix_AllocateChannels(-1); for (size_t i = 0; i < channel_count; ++i) { if (chunk == Mix_GetChunk(i)) { Mix_FadeOutChannel(i, fade_length_in_ms); } } } i8 get_audio_asset_volume(AssetID id) { if (id < 0) { return -1; } const Mix_Chunk *chunk = get_audio_chunk(GameEngine.asset_manager, id); if (!chunk) { return -1; } return chunk->volume; } void set_audio_asset_volume(AssetID id, i8 volume) { if (id < 0) { return; } Mix_Chunk *chunk = get_audio_chunk(GameEngine.asset_manager, id); if (!chunk) { return; } Mix_VolumeChunk(chunk, volume); } void play_music_asset(AssetID id, i16 loops, bool fade_in, u32 fade_length_in_ms) { if (id < 0 || is_music_asset_playing(id)) { return; } Mix_Music *music = get_music(GameEngine.asset_manager, id); if (!music) { return; } if (fade_in) { Mix_FadeInMusic(music, loops, fade_length_in_ms); } else { Mix_PlayMusic(music, loops); } } void pause_music_asset(AssetID id) { if (id < 0 || !is_music_asset_playing(id)) { return; } const Mix_Music *music = get_music(GameEngine.asset_manager, id); if (!music) { return; } Mix_PauseMusic(); } void resume_music_asset(AssetID id) { if (id < 0 || !is_music_asset_playing(id)) { return; } const Mix_Music *music = get_music(GameEngine.asset_manager, id); if (!music) { return; } Mix_ResumeMusic(); } void stop_music_asset(AssetID id) { if (id < 0 || !is_music_asset_playing(id)) { return; } const Mix_Music *music = get_music(GameEngine.asset_manager, id); if (!music) { return; } Mix_HaltMusic(); } void fade_out_music_asset(AssetID id, u32 fade_length_in_ms) { if (id < 0 || !is_music_asset_playing(id)) { return; } const Mix_Music *music = get_music(GameEngine.asset_manager, id); if (!music) { return; } Mix_FadeOutMusic(fade_length_in_ms); } i8 get_music_asset_volume(AssetID id) { if (id < 0) { return -1; } Mix_Music *music = get_music(GameEngine.asset_manager, id); if (!music) { return -1; } return Mix_GetMusicVolume(music); } void set_music_asset_volume(AssetID id, i8 volume) { if (id < 0 || !is_music_asset_playing(id)) { return; } const Mix_Music *music = get_music(GameEngine.asset_manager, id); if (!music) { return; } Mix_VolumeMusic(volume); } bool is_music_object_playing(const Mix_Music *music) { if (!Mix_PlayingMusic()) { return false; } // The Mix_GetMusic* functions used here all return the result for the // currently playing music when passed a NULL pointer. We use that to compare // the data of the Mix_Music object passed to the function to the data of the // currently playing music. // // TODO (Abdelrahman): This is a bit of a hack. Figure out if there is // a better alternative to check if a Mix_Music object is currently playing. return Mix_GetMusicType(music) == Mix_GetMusicType(NULL) && Mix_GetMusicTitle(music) == Mix_GetMusicTitle(NULL) && Mix_GetMusicArtistTag(music) == Mix_GetMusicArtistTag(NULL) && Mix_GetMusicAlbumTag(music) == Mix_GetMusicAlbumTag(NULL); } bool is_music_asset_playing(AssetID id) { if (id < 0) { return false; } const Mix_Music *music = get_music(GameEngine.asset_manager, id); if (!music) { return false; } return is_music_object_playing(music); } void pause_playing_music() { Mix_PauseMusic(); } void resume_playing_music() { Mix_ResumeMusic(); } void stop_playing_music() { Mix_HaltMusic(); } void fade_out_playing_music(u32 fade_length_in_ms) { Mix_FadeOutMusic(fade_length_in_ms); } void set_playing_music_volume(i8 volume) { Mix_VolumeMusic(volume); } bool is_music_playing() { return Mix_PlayingMusic(); } void handle_events() { if (!(GameEngine.running)) { return; } while (SDL_PollEvent(&(GameEngine.event))) { switch (GameEngine.event.type) { case SDL_QUIT: GameEngine.running = false; break; } if (!GameEngine.object_store) { break; } handle_all_objects_events(GameEngine.object_store, &(GameEngine.event)); } } void update() { if (!(GameEngine.running) || !(GameEngine.object_store)) { return; } u64 current_time = SDL_GetTicks64(); #ifndef DEBUG_ENGINE // Get the delta time in seconds since the SDL_GetTicks64() returns the // time in milliseconds GameEngine.delta = (current_time - GameEngine.last_time) / 1000.0f; #endif /* !DEBUG_ENGINE */ update_all_objects(GameEngine.object_store, GameEngine.delta); GameEngine.last_time = current_time; } void render() { if (!(GameEngine.running)) { return; } clear_window(&(GameEngine.window)); if (GameEngine.object_store) { render_all_objects(GameEngine.object_store); } swap_render_buffers(&(GameEngine.window)); }