#include #include #include #include "app.h" #define MIN_TILES_TO_CLEAR 3 //More than this number of tiles per row or column will trigger clear #define MAX_BOARD_WIDTH 10 #define MAX_BOARD_HEIGHT 10 #define MAX_PIECES 300 #define PIECE_SOURCE_YOFFSET 4 #define PIECE_SOURCE_WIDTH 32 #define PIECE_SOURCE_HEIGHT 32 #define PIECE_DEST_WIDTH 50 #define PIECE_DEST_HEIGHT 50 #define PIECE_LINEAR_SPEED 1.5 #define PIECE_SWAP_SPEED 3 #define PIECE_MAX_ROTATION 40 #define PIECE_ROTATION_SPEED 200 #define GRAVITY 2000.0 #define PIECE_MAX_FALL_XSPEED 200 #define PIECE_FALL_YSPEED_INITIAL -15 #define DELETE_PIECE_OFFSET 50 #define BOARD_WIDTH 8 #define BOARD_HEIGHT 6 #define BOARD_XOFFSET 200 #define BOARD_YOFFSET 150 #define BOARD_PADDING 4 #define BACKGROUND_SCROLL_SPEED 40 typedef enum { linear_move_state, fall_state, static_state } piece_state; typedef struct { int piece_id; piece_state state; SDL_FPoint position; float orientation; //Data for linear movement SDL_FPoint start_position; SDL_FPoint dest_position; float travel_speed; float fraction_travelled; //Data for falling animation float xvel, yvel; int piece_color; int piece_face; } piece; typedef enum { tile_filled, tile_currently_empty, tile_always_empty } tile_state; typedef struct { tile_state state; int piece_id; //Only if tile_state == tile_filled } tile; typedef enum { intro_state, neutral_state, piece_held_state } game_scene_state; typedef struct { bool inited; SDL_Texture* spritesheet; game_scene_state state; piece pieces[MAX_PIECES]; int num_pieces; tile board[MAX_BOARD_HEIGHT][MAX_BOARD_WIDTH]; int board_width; int board_height; int selected_piece_id; SDL_Texture* background_texture; float background_offset; float background_scaled_width; float background_scaled_height; } s_game; extern SDL_Window* window; s_game game = {0}; extern SDL_Window* window; static inline float random_float_0to1() { return (float)rand() / RAND_MAX; } static inline bool filled_tile_at(int y, int x) { if (x < 0 || x >= game.board_width || y < 0 || y >= game.board_height) return false; return game.board[y][x].state == tile_filled; } SDL_FPoint board_position(int y, int x) { return (SDL_FPoint) { .x = (PIECE_DEST_WIDTH + BOARD_PADDING) * x + BOARD_XOFFSET, .y = (PIECE_DEST_HEIGHT + BOARD_PADDING) * y + BOARD_YOFFSET }; } void piece_start_moving_to(piece* piece, SDL_FPoint dest_position, float speed) { piece->start_position = piece->position; piece->dest_position = dest_position; piece->travel_speed = speed; piece->state = linear_move_state; piece->fraction_travelled = 0; } int add_piece(piece piece) { static int last_piece_id = 0; if (game.num_pieces + 1 >= MAX_PIECES) { return -1; } piece.piece_id = last_piece_id++; game.pieces[game.num_pieces++] = piece; return piece.piece_id; } piece* retreive_piece(int piece_id) { for (int i = 0; i < game.num_pieces; i++) { if (game.pieces[i].piece_id == piece_id) { return &game.pieces[i]; } } return NULL; } void delete_piece(int piece_id) { piece* to_be_deleted = retreive_piece(piece_id); if (to_be_deleted == NULL) { return; } SDL_memcpy(to_be_deleted, &game.pieces[game.num_pieces - 1], sizeof(piece)); game.num_pieces--; } float rand_float() { return rand() / (float)RAND_MAX; } void add_piece_at(int y, int x) { int new_piece_id = add_piece((piece) { .position = { .x = (800 - PIECE_DEST_WIDTH) * rand_float(), .y = (600 - PIECE_DEST_HEIGHT) * rand_float() }, .orientation = 0, .piece_color = rand() % 9, .piece_face = rand() % 6 }); if (new_piece_id == -1) { return; } float speed_diff = PIECE_LINEAR_SPEED * rand_float() - PIECE_LINEAR_SPEED / 2.0; piece* new_piece = retreive_piece(new_piece_id); piece_start_moving_to(new_piece, board_position(y, x), PIECE_LINEAR_SPEED + speed_diff); game.board[y][x] = (tile) { .state = tile_filled, .piece_id = new_piece_id }; } void fill_board() { for (int i = 0; i < game.board_height; i++) { for (int j = 0; j < game.board_width; j++) { add_piece_at(i, j); } } } void clear_board() { game.num_pieces = 0; } void init_game(SDL_Renderer* renderer) { SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); if (game.spritesheet == NULL) { game.spritesheet = IMG_LoadTexture(renderer, "assets/match3_sheet.png"); SDL_SetTextureScaleMode(game.spritesheet, SDL_SCALEMODE_NEAREST); } if (game.background_texture == NULL) { game.background_texture = IMG_LoadTexture(renderer, "assets/Full-background.png"); /* SDL_SetTextureScaleMode(game.background_texture, SDL_SCALEMODE_NEAREST); */ int window_height; float texture_width, texture_height; SDL_GetWindowSizeInPixels(window, NULL, &window_height); SDL_GetTextureSize(game.background_texture, &texture_width, &texture_height); float scale = window_height / texture_height; game.background_scaled_width = scale * texture_width; game.background_scaled_height = scale * texture_height; } game.board_width = BOARD_WIDTH; game.board_height = BOARD_HEIGHT; game.state = intro_state; game.selected_piece_id = -1; fill_board(); } void free_game() { SDL_DestroyTexture(game.spritesheet); SDL_DestroyTexture(game.background_texture); } SDL_FPoint lerp_points(SDL_FPoint a, SDL_FPoint b, float fraction) { SDL_FPoint target; target.x = fraction * b.x + (1 - fraction) * a.x; target.y = fraction * b.y + (1 - fraction) * a.y; return target; } void piece_start_falling(piece* piece) { float xvel_multiplier = 2*random_float_0to1() - 1; piece->xvel = xvel_multiplier * PIECE_MAX_FALL_XSPEED; piece->yvel = PIECE_FALL_YSPEED_INITIAL; piece->state = fall_state; } void update_piece(piece* piece, float dt) { switch (piece->state) { case linear_move_state: piece->fraction_travelled += dt * piece->travel_speed; if (piece->fraction_travelled >= 1) { piece->fraction_travelled = 1; piece->position = piece->dest_position; piece->state = static_state; } else { piece->position = lerp_points(piece->start_position, piece->dest_position, piece->fraction_travelled); } break; case fall_state: piece->position.x += piece->xvel * dt; piece->yvel += GRAVITY * dt / 2; piece->position.y += piece->yvel * dt; piece->yvel += GRAVITY * dt / 2; break; default: break; } } int mouse_on_piece() { SDL_FPoint mouse_position; SDL_GetMouseState(&mouse_position.x, &mouse_position.y); for (int i = 0; i < game.num_pieces; i++) { const piece* piece = &game.pieces[i]; SDL_FRect piece_box = (SDL_FRect){ .x = piece->position.x, .y = piece->position.y, .w = PIECE_DEST_WIDTH, .h = PIECE_DEST_HEIGHT }; if (SDL_PointInRectFloat(&mouse_position, &piece_box)) { for (int i = 0; i < game.board_height; i++) { for (int j = 0; j < game.board_width; j++) { if (game.board[i][j].state == tile_filled && game.board[i][j].piece_id == piece->piece_id) return piece->piece_id; } } } } return -1; } void update_intro_state() { for (int i = 0; i < game.num_pieces; i++) { if (game.pieces[i].state != static_state && game.pieces[i].state != fall_state) { return; } } game.state = neutral_state; } void handle_piece_rotation(float dt) { int selected_piece_id = mouse_on_piece(); for (int i = 0; i < game.num_pieces; i++) { piece* piece = &game.pieces[i]; if (piece->piece_id == selected_piece_id) { piece->orientation = SDL_min(piece->orientation + PIECE_ROTATION_SPEED * dt, PIECE_MAX_ROTATION); } else { piece->orientation = SDL_max(piece->orientation - PIECE_ROTATION_SPEED * dt, 0); } } } void scroll_background(float dt) { game.background_offset -= BACKGROUND_SCROLL_SPEED * dt; while (game.background_offset < -game.background_scaled_width) { game.background_offset += game.background_scaled_width; } } void update_game(float dt) { int window_width, window_height; SDL_GetWindowSizeInPixels(window, &window_width, &window_height); for (int i = 0; i < game.num_pieces; i++) { update_piece(&game.pieces[i], dt); if (game.pieces[i].position.y > window_height + DELETE_PIECE_OFFSET) { delete_piece(game.pieces[i].piece_id); } } handle_piece_rotation(dt); scroll_background(dt); if (game.state == intro_state) { update_intro_state(); } } void draw_piece(SDL_Renderer* renderer, const piece* piece) { const int piece_source_xoffs[] = {4, 209}; SDL_RenderTextureRotated(renderer, game.spritesheet, &(SDL_FRect){ .x = piece_source_xoffs[piece->piece_color / 9] + piece->piece_face * (PIECE_SOURCE_WIDTH + 1), .y = PIECE_SOURCE_YOFFSET + (piece->piece_color % 9) * (PIECE_SOURCE_HEIGHT + 1), .w = PIECE_SOURCE_WIDTH, .h = PIECE_SOURCE_HEIGHT }, &(SDL_FRect){ .x = piece->position.x, .y = piece->position.y, .w = PIECE_DEST_WIDTH, .h = PIECE_DEST_HEIGHT }, piece->orientation, NULL, SDL_FLIP_NONE ); } void draw_background(SDL_Renderer* renderer) { int x_offset = game.background_offset; while (x_offset <= 800) { SDL_RenderTexture(renderer, game.background_texture, NULL, &(SDL_FRect){ .x = x_offset, .y = 0, .w = game.background_scaled_width, .h = game.background_scaled_height }); x_offset += game.background_scaled_width; } } void draw_game(SDL_Renderer* renderer) { SDL_RenderClear(renderer); draw_background(renderer); piece* selected_piece = NULL; for (int i = 0; i < game.num_pieces; i++) { if (game.pieces[i].piece_id == game.selected_piece_id) { selected_piece = &game.pieces[i]; continue; } draw_piece(renderer, &game.pieces[i]); } //Gives it the apearance that you are sliding your tiles over if (selected_piece) { draw_piece(renderer, selected_piece); } SDL_RenderPresent(renderer); } void neutral_state_event(SDL_Event* e) { if (e->type == SDL_EVENT_MOUSE_BUTTON_DOWN && e->button.button == SDL_BUTTON_LEFT) { int selected_piece_id = mouse_on_piece(); if (selected_piece_id >= 0) { game.selected_piece_id = selected_piece_id; game.state = piece_held_state; } } } void swap_pieces_at(int x1, int y1, int x2 , int y2) { int id1 = game.board[y1][x1].piece_id; int id2 = game.board[y2][x2].piece_id; piece* selected = retreive_piece(id1); piece* swap = retreive_piece(id2); if (selected && swap) { piece_start_moving_to(selected, board_position(y2, x2), PIECE_SWAP_SPEED); piece_start_moving_to(swap, board_position(y1, x1), PIECE_SWAP_SPEED); game.board[y2][x2].piece_id = id1; game.board[y1][x1].piece_id = id2; } } void find_last_color_matching_tile_in_direction(int* sy, int* sx, int dy, int dx) { if (!filled_tile_at(*sy, *sx)) return; piece* start_piece = retreive_piece(game.board[*sy][*sx].piece_id); for(;;) { int ny = *sy + dy, nx = *sx + dx; if (!filled_tile_at(ny, nx)) return; piece* next_piece = retreive_piece(game.board[ny][nx].piece_id); if (!next_piece || next_piece->piece_color != start_piece->piece_color) return; *sy = ny; *sx = nx; } } bool try_clear_at(int y, int x) { const int axes[2][2] = { {1, 0}, //Horizontal {0, 1} //Vertical }; for (int a = 0; a < 2; a++) { int sy = y, sx = x, dy = axes[a][1], dx = axes[a][0]; find_last_color_matching_tile_in_direction(&sy, &sx, dy, dx); int ey = sy, ex = sx; find_last_color_matching_tile_in_direction(&ey, &ex, -dy, -dx); if (SDL_abs(ey-sy) + SDL_abs(ex-sx) + 1 < MIN_TILES_TO_CLEAR) continue; for (;;) { piece_start_falling(retreive_piece(game.board[ey][ex].piece_id)); game.board[ey][ex].state = tile_currently_empty; if (ey == sy && ex == sx) break; ey += dy; ex += dx; } return true; } return false; } void cascade_pieces() { for (int j = 0; j < game.board_width; j++) { for (int i = 0; i < game.board_height; i++) { if (game.board[i][j].state != tile_currently_empty) continue; //Look for the first filled tile above and start moving it down for (int c = i-1; c >= 0; c--) { if (game.board[c][j].state == tile_filled) { piece* piece = retreive_piece(game.board[c][j].piece_id); piece_start_moving_to(piece, board_position(c+1, j), PIECE_LINEAR_SPEED ); game.board[c+1][j] = game.board[c][j]; } } add_piece_at(0, j); } } } void piece_held_event(SDL_Event* e) { if (e->type == SDL_EVENT_MOUSE_BUTTON_UP && e->button.button == SDL_BUTTON_LEFT) { int swap_piece_id = mouse_on_piece(); int selected_piece_id = game.selected_piece_id; if (swap_piece_id >= 0) { int selected_x, selected_y; int swap_x, swap_y; for (int i = 0; i < game.board_height; i++) { for (int j = 0; j < game.board_width; j++) { if (game.board[i][j].state != tile_filled) continue; if (game.board[i][j].piece_id == selected_piece_id) { selected_x = j; selected_y = i; } if (game.board[i][j].piece_id == swap_piece_id) { swap_x = j; swap_y = i; } } } if (SDL_abs(selected_x-swap_x) + SDL_abs(selected_y-swap_y) != 1 || retreive_piece(selected_piece_id)->piece_color == retreive_piece(swap_piece_id)->piece_color) { game.state = neutral_state; return; } swap_pieces_at(selected_x, selected_y, swap_x, swap_y); bool tiles_cleared = try_clear_at(selected_y, selected_x); tiles_cleared = try_clear_at(swap_y, swap_x) || tiles_cleared; //sneaky short circuit glitch if (tiles_cleared) { cascade_pieces(); game.state = intro_state; return; } } game.state = neutral_state; } } void game_event(SDL_Event* e) { if (game.state == neutral_state) { neutral_state_event(e); } else if (game.state == piece_held_state) { piece_held_event(e); } }