diff --git a/assets/bounce.wav b/assets/bounce.wav new file mode 100644 index 0000000..e003dd9 Binary files /dev/null and b/assets/bounce.wav differ diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..9aeb9bb --- /dev/null +++ b/build.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +# define variables +PROJECT_NAME=${PWD##*/} +BUILD_DIR="./build" +INCLUDE_IN_BUILD="./assets" + +# compilation targets +args_linux86_64() +{ + ARCHITECTURE="linux-86_64" + COMPILER="/bin/gcc" + ARGS="-lSDL2" + FILE_EXSTENSION="" +} + +args_win86-64() +{ + ARCHITECTURE="win-86_64" + COMPILER="/usr/bin/x86_64-w64-mingw32-gcc" + ARGS="-lmingw32 -lSDL2main -lSDL2 -mwindows" + INCLUDE_IN_BUILD="$INCLUDE_IN_BUILD /usr/x86_64-w64-mingw32/bin/SDL2.dll" + FILE_EXSTENSION=".exe" +} + +args_emscripten() +{ + ARCHITECTURE="web" + COMPILER="/home/user/.local/bin/emcc" + ARGS="-s USE_SDL=2" + FILE_EXSTENSION=".html" +} + +# handle arguments +if [ "$1" = "linux86_64" ]; then args_linux86_64 +elif [ "$1" = "win86_64" ]; then args_win86-64 +elif [ "$1" = "web" ]; then args_emscripten +else echo -e "\033[91mdidn't include any arguments! D:\033[0m" && exit -1 +fi + +# check whether $BUILD_DIR or $ARCHITECTURE isn't set +if [[ -z $BUILD_DIR ]] || [[ -z "$ARCHITECTURE" ]]; then + echo -e "\033[91mBUILD_DIR or ARCHITECTURE not set D:\033[0m" + exit -1 +fi + + +# make (and clear) the build directory +mkdir $BUILD_DIR $BUILD_DIR/$ARCHITECTURE +rm -rf $BUILD_DIR/$ARCHITECTURE/* + +# copy included files or directories to the build directory +if [[ ! -z $INCLUDE_IN_BUILD ]]; then + cp -r $INCLUDE_IN_BUILD $BUILD_DIR/$ARCHITECTURE +fi + +# get the executable path +EXE_PATH=$BUILD_DIR/$ARCHITECTURE/$PROJECT_NAME$FILE_EXSTENSION +echo "building at: $EXE_PATH" + +# check whether the compiler path contains an executable at said path +if [ ! -x $COMPILER ]; then + echo -e "\033[91mCouldn't find an executable at path: \033[0m $COMPILER" + exit -1 +fi + +# compile the code +COMMAND="$COMPILER `find ./src -name *.c` -o $EXE_PATH -Wall -g -lm $ARGS" +echo "using command: $COMMAND" +$COMMAND +exit $? diff --git a/src/errors.c b/src/errors.c new file mode 100644 index 0000000..fa7d860 --- /dev/null +++ b/src/errors.c @@ -0,0 +1,32 @@ +#include "errors.h" + +#include +#include +#include + +#define MAX_STR_LEN 128 + +void error(const ErrorCode error_code, const char* format, ...) { + char buffer[MAX_STR_LEN] = {0}; // contains the buffer of the final string + + va_list args = {0}; + va_start(args, format); + vsnprintf(buffer, MAX_STR_LEN, format, args); + va_end(args); + + printf("\033[91mE\033[0m: %s\n", buffer); + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "something went wrong! :O", buffer, NULL); + + exit(error_code); +} + +void warn(const char* format, ...) { + char buffer[MAX_STR_LEN] = {0}; // contains the buffer of the final string + + va_list args = {0}; + va_start(args, format); + vsnprintf(buffer, MAX_STR_LEN, format, args); + va_end(args); + + printf("\033[93mW\033[0m: %s\n", buffer); +} diff --git a/src/errors.h b/src/errors.h new file mode 100644 index 0000000..2ad27d4 --- /dev/null +++ b/src/errors.h @@ -0,0 +1,24 @@ +#pragma once + +typedef unsigned char ErrorCode; +enum { + SUCCESS = 0, + FAILURE = -1, + + FAILURE_STD = 1 << 0, // 1 : marks a generic C library error + FAILURE_MEMORY = FAILURE_STD | (1 << 1), // 3 : marks a memory related error + + // SDL ERRORS + FAILURE_SDL = 1 << 1, // 2 : marks a generic SDL error + FAILURE_SDL_INIT = FAILURE_SDL | (1 << 2), // 6 : marks an error during SDL initialisation + FAILURE_SDL_WINDOW = FAILURE_SDL | (1 << 3), // 10 : marks an error with the window + FAILURE_SDL_RENDERER = FAILURE_SDL | (1 << 4), // 18 : marks an error with the renderer + FAILURE_SDL_AUDIO = FAILURE_SDL | (1 << 5), // 34 : marks an error with audio + + // GAME ERRORS + FAILURE_GAME = 1 << 2, // 4 : marks a generic game error +}; + +// call when a fatal error has occurred, the program will immediately terminate when called +void error(const ErrorCode error_code, const char* format, ...); +void warn(const char* format, ...); diff --git a/src/game/level.c b/src/game/level.c new file mode 100644 index 0000000..d968a42 --- /dev/null +++ b/src/game/level.c @@ -0,0 +1,168 @@ +#include "level.h" + +#include +#include + +#include "../constants.h" +#include "../main.h" +#include "../window/renderer.h" +#include "vector2.h" + + +// prepares the level to be in a playable state +void level_init(Level* level) { + // initialize bouncer + level->bouncer.pos.x = (SCREEN_WIDTH / 2) - (BOUNCER_WIDTH_DEFAULT / 2); + level->bouncer.pos.y = SCREEN_HEIGHT - (BOUNCER_HEIGHT * 2); + level->bouncer.width = BOUNCER_WIDTH_DEFAULT; + + // initialize ball + level->ball.pos.x = (SCREEN_WIDTH / 2) - (BALL_SIZE_DEFAULT / 2); + level->ball.pos.y = level->bouncer.pos.y - BOUNCER_HEIGHT - BALL_SIZE_DEFAULT; + level->ball.moving = false; + level->ball.size = BALL_SIZE_DEFAULT; + level->ball.direction = (Vector2){0, -BALL_SPEED}; + + // initialize bricks + // define the colours of the brick rows + const Colour colours[BRICK_COLUMNS] = { + {0x5BCEFAFF}, + {0xF5A9B8FF}, + {0xFFFFFFFF}, + {0xF5A9B8FF}, + {0x5BCEFAFF}}; + + int brick_width = BRICK_WIDTH + BRICK_PADDING; + int brick_height = BRICK_HEIGHT + (BRICK_PADDING / 2); + float level_padding_left = ((float)SCREEN_WIDTH - ((SCREEN_WIDTH / brick_width) * brick_width)) / 2.0F; // for centering + + // store bricks in the level + for (int x = 0; x < BRICK_COLUMNS; x++) { + for (int y = 0; y < BRICK_ROWS; y++) { + Brick* brick = &level->bricks[x][y]; + brick->colour = colours[y]; + brick->pos.x = (x * brick_width) + level_padding_left; + brick->pos.y = (y * brick_height) + BRICK_PADDING_TOP; + brick->destroyed = false; + } + } +} + +// updates the player's "bouncer" +static void update_player(Bouncer* bouncer, Ball* ball, const Uint8* keys) { + // if move bouncer LEFT + if (keys[SDL_SCANCODE_A] || keys[SDL_SCANCODE_LEFT]) { + if (((bouncer->pos.x) < 0) == false) { + bouncer->pos.x -= BOUNCER_SPEED; + + if (ball->moving == false) + ball->pos.x -= BOUNCER_SPEED; + } + } + + // if move bouncer RIGHT + if (keys[SDL_SCANCODE_D] || keys[SDL_SCANCODE_RIGHT]) { + if (((bouncer->pos.x + bouncer->width) > SCREEN_WIDTH) == false) { + bouncer->pos.x += BOUNCER_SPEED; // increase the bouncer pos + + if (ball->moving == false) + ball->pos.x += BOUNCER_SPEED; + } + } + + // ball launching + if (ball->moving == false && keys[SDL_SCANCODE_SPACE]) { + ball->moving = true; + } +} + +// updates the player's ball +static void update_ball(Level* level, Ball* ball, Bouncer* bouncer) { + // update ball position + ball->pos.x += ball->direction.x; + ball->pos.y += ball->direction.y; + + + // check X axis out of bounds collisions + if ((ball->pos.x + ball->size) > SCREEN_WIDTH || ball->pos.x < 0) { + audio_play(level->audio_device, level->bounce_sfx); + ball->direction.x *= -1; + ball->pos.x = (ball->pos.x < 0) ? 0 : SCREEN_WIDTH - ball->size; // correct the ball's position + } + + // check Y axis out of bounds collisions + if (ball->pos.y < 0) { + audio_play(level->audio_device, level->bounce_sfx); + ball->direction.y *= -1; + ball->pos.y = 0; // correct the ball's position + } + + + // check bouncer collisions + if ((ball->pos.x + ball->size) > bouncer->pos.x && ball->pos.x < (bouncer->pos.x + bouncer->width) && + (ball->pos.y + ball->size) > bouncer->pos.y && ball->pos.y < (bouncer->pos.y + BOUNCER_HEIGHT)) { + float x = ball->pos.x - bouncer->pos.x + (ball->size / 2) + 2; // get the X axis relative to the bouncer (add 2, see below) + unsigned max = bouncer->width + 4; // get the maxiumum of this X axis (add 4 to make it feel more accurate) + float angle = (x - (max / 2.0F)) / max * (PI / 1.5F); // calculate the angle in radians where the bouncer X axis falls on -60° to 60° + + // change the ball direction + audio_play(level->audio_device, level->bounce_sfx); + ball->direction.x = SDL_sinf(angle) * BALL_SPEED; + ball->direction.y = -SDL_cosf(angle) * BALL_SPEED; + } + + // check brick collisions + bool collided = false; + for (int x = 0; x < BRICK_COLUMNS; x++) { + for (int y = 0; y < BRICK_ROWS; y++) { + const Brick* brick = &level->bricks[x][y]; + + if (brick->destroyed == true) { + continue; + } + + const float max_brick_x = brick->pos.x + BRICK_WIDTH; + const float brick_max_y = brick->pos.y + BRICK_HEIGHT; + if (ball->pos.x < max_brick_x && (ball->pos.x + ball->size) > brick->pos.x && + ball->pos.y < brick_max_y && (ball->pos.y + ball->size) > brick->pos.y) { + level->bricks[x][y].destroyed = true; + + // skip changing direction of we already did + if (collided == true) { + continue; + } + + // float ball_centre_x = ball->pos.x + (ball->size / 2.0F); + float ball_centre_y = ball->pos.y + (ball->size / 2.0F); + + // manage ball bounce direction; only bounce along the X axis if the ball's Y centre is in between dthe top and bottom of the block + if (brick->pos.y < ball_centre_y && ball_centre_y < brick_max_y) { + ball->direction.x *= -1; + } else { + ball->direction.y *= -1; + } + + audio_play(level->audio_device, level->bounce_sfx); + collided = true; + } + } + } +} + +// updates the level +void level_update(Level* level, const Uint8* keys) { + Bouncer* bouncer = &level->bouncer; + Ball* ball = &level->ball; + + update_player(bouncer, ball, keys); + + if (ball->moving == true) + update_ball(level, ball, bouncer); + + + // check lose condition + if ((ball->pos.y + ball->size) > SCREEN_HEIGHT) { + stop(); + ball->moving = false; + } +} diff --git a/src/game/level.h b/src/game/level.h new file mode 100644 index 0000000..2fdb71b --- /dev/null +++ b/src/game/level.h @@ -0,0 +1,38 @@ +#pragma once +#include +#include + +#include "../constants.h" +#include "../window/colour.h" +#include "../window/audio.h" +#include "vector2.h" + +typedef struct { + Vector2 pos; + Vector2 direction; + unsigned size; + bool moving; +} Ball; + +typedef struct { + Vector2 pos; + unsigned width; +} Bouncer; + +typedef struct { + Vector2 pos; + Colour colour; + bool destroyed; +} Brick; + +typedef struct { + Ball ball; + Bouncer bouncer; + Brick bricks[BRICK_COLUMNS][BRICK_ROWS]; + AudioDevice* audio_device; + AudioData bounce_sfx; +} Level; + + +void level_init(Level* level); +void level_update(Level* level, const Uint8* keys); diff --git a/src/game/vector2.h b/src/game/vector2.h new file mode 100644 index 0000000..f1d79d6 --- /dev/null +++ b/src/game/vector2.h @@ -0,0 +1,18 @@ +#pragma once + +#define VECTOR2_UP ((Vector2){0, 1}) +#define VECTOR2_DOWN ((Vector2){0, -1}) +#define VECTOR2_LEFT ((Vector2){-1, 0}) +#define VECTOR2_RIGHT ((Vector2){1, 0}) +#define VECTOR2_ZERO ((Vector2){0, 0}) +#define VECTOR2_ONE ((Vector2){1, 1}) + +#define vec2_add(v1, v2) ((Vector2){v1.x + v2.x, v1.y + v2.y}) +#define vec2_subt(v1, v2) ((Vector2){v1.x - v2.x, v1.y - v2.y}) +#define vec2_mult(v, a) ((Vector2){v.x * a, v.y * a}) +#define vec2_div(v, a) ((Vector2){v.x / a, v.y / a}) + +typedef struct { + float x; + float y; +} Vector2; diff --git a/src/main.h b/src/main.h new file mode 100644 index 0000000..1369892 --- /dev/null +++ b/src/main.h @@ -0,0 +1,4 @@ +#pragma once + +// stops execution of the game +void stop(void); diff --git a/src/window/audio.c b/src/window/audio.c new file mode 100644 index 0000000..985142d --- /dev/null +++ b/src/window/audio.c @@ -0,0 +1,110 @@ +#include "audio.h" + +#include + +#include "../errors.h" + +// the maximum amount of sounds that can play at once +#define MAX_SOUNDS 64 + +typedef struct { + AudioData* playing_audio; + AudioDevice audio_device; +} AudioCallbackData; + +// audio callback from SDL_AudioSpec; called when the audio device needs more data +static void audio_mixer(void* userdata, Uint8* stream, int len) { + memset(stream, 0, len); // clear the playing audio + AudioDevice* device = userdata; // get the callback data + AudioData* audio = device->playing_audio; + + for (int i = 0; i < MAX_SOUNDS; i++) { + // skip if the audio doesn't conain any further data + if (audio[i].length <= 0) { + continue; + } + + // get the length of which we shall be mixing + Uint32 mix_length = SDL_min(audio[i].length, len); + + // mix the audio with the stream + SDL_MixAudioFormat(stream, audio[i].buffer, device->format, mix_length, SDL_MIX_MAXVOLUME); + audio[i].buffer += mix_length; // move the pointer up a a bit + audio[i].length -= mix_length; // move up the mixed amount + } +} + +// converts the audio to the format of the audio device +static void convert_audio(const AudioDevice* audio_device, const SDL_AudioSpec wav_spec, Uint8** wav_buffer, Uint32* wav_length) { + // build the audio converter with the audio given + SDL_AudioCVT cvt = {0}; + SDL_BuildAudioCVT(&cvt, wav_spec.format, wav_spec.channels, wav_spec.freq, audio_device->format, audio_device->channels, audio_device->freq); + + cvt.len = (*wav_length) * wav_spec.channels; // the buffer length + cvt.buf = (Uint8*)SDL_malloc(cvt.len * cvt.len_mult); // allocate size for the new buffer + memcpy(cvt.buf, *wav_buffer, *wav_length); // copy wav data to cvt buffer; + + // convert + SDL_ConvertAudio(&cvt); + + // output + *wav_length = cvt.len_cvt; // set the length to the new length + memcpy(*wav_buffer, cvt.buf, cvt.len_cvt); // copy converted cvt buffer back to wav buffer + + free(cvt.buf); // free the memory allocated to the cvt buffer +} + +// loads a WAV file and returns the relevant information +AudioData audio_load_wav(const AudioDevice* audio_device, const char* file_path) { + SDL_AudioSpec wav_spec = {0}; + AudioData audio = {0}; + + SDL_LoadWAV(file_path, &wav_spec, &audio.buffer, &audio.length); + convert_audio(audio_device, wav_spec, &audio.buffer, &audio.length); + + return audio; +} + +// initializes the audio device +AudioDevice* audio_device_init(const int freq, const SDL_AudioFormat format, const Uint8 channels, const Uint16 samples) { + // allocate memory for the audio device + AudioDevice* audio_device = malloc(sizeof(AudioDevice)); + + // define the audio specification + SDL_AudioSpec spec = {freq, format, channels, samples}; + spec.callback = audio_mixer; + spec.userdata = audio_device; + + // create the audio device + *audio_device = (AudioDevice){ + SDL_OpenAudioDevice(NULL, 0, &spec, NULL, 0), + freq, + format, + channels, + calloc(MAX_SOUNDS, sizeof(AudioData)), // allocate memory on the heap for the playing audio array + }; + + // if the audio device isn't set, cause an error + if (audio_device->id < 1) { + error(FAILURE_SDL_AUDIO, "AudioDivice failed to open! SDL Error: %s", SDL_GetError()); + return NULL; + } + + // default state of the device is paused, so we unpause it here + SDL_PauseAudioDevice(audio_device->id, 0); + return audio_device; +} + +// plays the audio +void audio_play(const AudioDevice* audio_device, const AudioData audio) { + AudioData* playing_audio = audio_device->playing_audio; + + for (int i = 0; i < MAX_SOUNDS; i++) { + // overrite audio that has been deallocated + if (playing_audio[i].length <= 0) { + // override the audio + playing_audio[i] = audio; + break; // don't continue. :3 + } + } +} diff --git a/src/window/audio.h b/src/window/audio.h new file mode 100644 index 0000000..3a03b7b --- /dev/null +++ b/src/window/audio.h @@ -0,0 +1,20 @@ +#pragma once +#include + +typedef struct { + Uint32 length; + Uint8* buffer; +} AudioData; + +typedef struct { + SDL_AudioDeviceID id; + int freq; + SDL_AudioFormat format; + Uint8 channels; + AudioData* playing_audio; +} AudioDevice; + + +AudioData audio_load_wav(const AudioDevice* audio_device, const char* file_path); +AudioDevice* audio_device_init(const int freq, const SDL_AudioFormat format, const Uint8 channels, const Uint16 samples); +void audio_play(const AudioDevice* audio_device, const AudioData audio); diff --git a/src/window/colour.h b/src/window/colour.h new file mode 100644 index 0000000..756bec9 --- /dev/null +++ b/src/window/colour.h @@ -0,0 +1,9 @@ +typedef union { + unsigned packed; + struct { + unsigned char a; + unsigned char b; + unsigned char g; + unsigned char r; + }; +} Colour; diff --git a/src/window/renderer.c b/src/window/renderer.c new file mode 100644 index 0000000..22deee6 --- /dev/null +++ b/src/window/renderer.c @@ -0,0 +1,73 @@ +#include "renderer.h" + +#include +#include + +#include "../constants.h" +#include "../errors.h" +#include "../game/level.h" + +// initializes the window and renderer +int renderer_init(SDL_Window** window, SDL_Renderer** renderer) { + // init the window + *window = SDL_CreateWindow("Quinn's Breakout Clone", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN); + if (*window == NULL) { + error(FAILURE_SDL_WINDOW, "Window failed to be created! SDL_Error: %s", SDL_GetError()); + return -1; + } + + // create a renderer + *renderer = SDL_CreateRenderer(*window, -1, SDL_RENDERER_PRESENTVSYNC); + + if (*renderer == NULL) { + error(FAILURE_SDL_RENDERER, "Renderer failed to be created! SDL_Error: %s", SDL_GetError()); + return -1; + } + + return SUCCESS; +} + +// renders the screen +void renderer_update(RenderData* render_data) { + SDL_Renderer* renderer = render_data->renderer; + Level* level = render_data->level; + + int success = 0; // if an error occurs, this value is <0 + + // render background + success |= SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0xFF); + success |= SDL_RenderClear(renderer); + + // draw player components + success |= SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF); + success |= SDL_RenderFillRectF(renderer, &(SDL_FRect){level->bouncer.pos.x, level->bouncer.pos.y, level->bouncer.width, 5}); // draw bouncer + success |= SDL_RenderFillRectF(renderer, &(SDL_FRect){level->ball.pos.x, level->ball.pos.y, level->ball.size, level->ball.size}); // draw ball + + // draw bricks + for (int x = 0; x < BRICK_COLUMNS; x++) { + for (int y = 0; y < BRICK_ROWS; y++) { + Brick* brick = &level->bricks[x][y]; + + if (brick->destroyed == true) { + continue; + } + + success |= SDL_SetRenderDrawColor(renderer, brick->colour.r, brick->colour.g, brick->colour.b, brick->colour.a); + success |= SDL_RenderFillRectF(renderer, &(SDL_FRect){brick->pos.x, brick->pos.y, BRICK_WIDTH, BRICK_HEIGHT}); // draw brick + } + } + + if (success < 0) { + error(FAILURE_SDL_RENDERER, "something went wrong whilst rendering: %s\n", SDL_GetError()); + return; + } + + // present the result to the renderer + SDL_RenderPresent(renderer); +} + +void renderer_destroy(SDL_Window* window, SDL_Renderer* renderer) { + SDL_DestroyWindow(window); + SDL_DestroyRenderer(renderer); + SDL_Quit(); +} diff --git a/src/window/renderer.h b/src/window/renderer.h new file mode 100644 index 0000000..8368eed --- /dev/null +++ b/src/window/renderer.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +#include "../game/level.h" + +typedef struct { + SDL_Window* window; + SDL_Renderer* renderer; + Level* level; +} RenderData; + +int renderer_init(SDL_Window** window, SDL_Renderer** renderer); +void renderer_update(RenderData* render_data); +void renderer_destroy(SDL_Window* window, SDL_Renderer* renderer);