Files
punchie-engine/src/GameEngine.c
T

781 lines
18 KiB
C

#include "GameEngine.h"
#include "AssetManager.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_mixer.h"
#include "SDL_pixels.h"
#include "SDL_platform.h"
#include "SDL_rect.h"
#include "SDL_render.h"
#include "SDL_surface.h"
#include "SDL_timer.h"
#include "SDL_ttf.h"
#include "SDL_video.h"
#include "Window.h"
#include "aliases.h"
#include "constants.h"
#include "engine_build_config.h"
#include "reusable_ids.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#ifdef DEBUG_ENGINE
#include <stdio.h>
#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) {
#ifdef 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) {
#ifdef DEBUG_ENGINE
printf("Failed to initialise SDL_image. Error: %s\n", IMG_GetError());
#endif /* DEBUG_ENGINE */
initialisation_complete = false;
}
if (TTF_Init() < 0) {
#ifdef 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) {
#ifdef 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) {
#ifdef 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) {
#ifdef 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)) {
#ifdef 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();
#ifdef DEBUG_ENGINE
GameEngine.delta = 1.0f / 60.0f;
#endif /* DEBUG_ENGINE */
#ifdef DEBUG_ENGINE
printf("Game initialised successfully\n");
#endif /* DEBUG_ENGINE */
return true;
}
bool game_running() { return GameEngine.running; }
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();
#ifdef 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);
}
void remove_object(ObjectID *id) {
if (!(GameEngine.running) || !(GameEngine.object_store)) {
return;
}
remove_object_from_store(GameEngine.object_store, id);
}
AssetID load_asset(const char *path, AssetType type) {
return type == ASSET_TYPE_IMAGE
? load_asset_to_manager(GameEngine.asset_manager, path, type,
GameEngine.window.renderer)
: load_asset_to_manager(GameEngine.asset_manager, path, type,
NULL);
}
void close_asset(AssetID *id) {
if (*id < 0) {
return;
}
remove_asset_from_manager(GameEngine.asset_manager, id);
}
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_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, RenderRect *src, 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_SizeText(font, text, &(output.width), &(output.height));
return output;
}
void render_text_with_font_asset(AssetID id, const char *text, f32 x, f32 y,
SDL_Color color) {
if (id < 0) {
return;
}
TTF_Font *font = get_font(GameEngine.asset_manager, id);
if (!font) {
return;
}
SDL_Surface *surface = TTF_RenderText_Solid(font, text, color);
if (!surface) {
return;
}
// Get the width and height, in pixels, of the text rendered using the font
i32 w = 0;
i32 h = 0;
TTF_SizeText(font, text, &w, &h);
RenderRect rect = {x, y, w, h};
render_surface(GameEngine.window.renderer, surface, &rect);
SDL_FreeSurface(surface);
surface = NULL;
}
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);
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
Mix_Music *music = get_music(GameEngine.asset_manager, id);
if (!music) {
return;
}
Mix_VolumeMusic(volume);
}
bool is_music_object_playing(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;
}
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));
}