From c3a4a06403821ab6a39188848945805684e9ac48 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sun, 7 Dec 2025 12:32:09 +0200 Subject: [PATCH] add collisions --- src/entity.h | 29 ++++ src/entity_id_list.c | 12 ++ src/entity_id_list.h | 11 ++ src/entity_list.h | 4 +- src/float_list.c | 12 ++ src/float_list.h | 8 + src/game.c | 405 ++++++++++++++++++++++++++++++++++++++++++- src/game.h | 3 +- src/generic_vector.h | 38 ++++ 9 files changed, 515 insertions(+), 7 deletions(-) create mode 100644 src/entity.h create mode 100644 src/entity_id_list.c create mode 100644 src/entity_id_list.h create mode 100644 src/float_list.c create mode 100644 src/float_list.h create mode 100644 src/generic_vector.h diff --git a/src/entity.h b/src/entity.h new file mode 100644 index 0000000..ec75469 --- /dev/null +++ b/src/entity.h @@ -0,0 +1,29 @@ +#pragma once + +#include "main.h" + +enum EntityType { + ENTITY_TYPE_NIL = 0, + ENTITY_TYPE_PLAYER, + ENTITY_TYPE_WALL +}; + +struct EntityFlags { + bool has_friction : 1; + bool has_max_speed : 1; +}; + +struct Entity { + enum EntityType type; + struct EntityFlags flags; + + Vector2 position; + Vector2 velocity; + Vector2 accelaration; + float max_speed; + float friction; + // TODO: Remove this, because this is just a temporary value for resolving collisions + Vector2 step; + + Rectangle collision_rect; +}; diff --git a/src/entity_id_list.c b/src/entity_id_list.c new file mode 100644 index 0000000..9ff43b9 --- /dev/null +++ b/src/entity_id_list.c @@ -0,0 +1,12 @@ +#include "entity_id_list.h" + +DEFINE_VECTOR_ENSURE_METHOD_CAPACITY(entity_id_list_ensure_capacity, struct EntityIdList, EntityId); +DEFINE_VECTOR_METHOD_FREE(entity_id_list_free, struct EntityIdList); +DEFINE_VECTOR_METHOD_FREE(entity_id_list_clear_retaining_capacity, struct EntityIdList); + +void entity_id_list_add(struct EntityIdList *list, EntityId id) +{ + entity_id_list_ensure_capacity(list, list->len + 1); + + list->items[list->len++] = id; +} diff --git a/src/entity_id_list.h b/src/entity_id_list.h new file mode 100644 index 0000000..33f8e44 --- /dev/null +++ b/src/entity_id_list.h @@ -0,0 +1,11 @@ +#pragma once + +#include "main.h" +#include "entity_id.h" +#include "generic_vector.h" + +DEFINE_VECTOR_STRUCT(EntityIdList, EntityId); + +void entity_id_list_add(struct EntityIdList *list, EntityId id); +void entity_id_list_free(struct EntityIdList *list); +void entity_id_list_clear_retaining_capacity(struct EntityIdList *list); diff --git a/src/entity_list.h b/src/entity_list.h index 1f547f4..9594a36 100644 --- a/src/entity_list.h +++ b/src/entity_list.h @@ -2,9 +2,7 @@ #include "main.h" #include "entity_id.h" - -struct Entity { -}; +#include "entity.h" struct EntitySlot { bool used; diff --git a/src/float_list.c b/src/float_list.c new file mode 100644 index 0000000..3490ba5 --- /dev/null +++ b/src/float_list.c @@ -0,0 +1,12 @@ +#include "float_list.h" + +DEFINE_VECTOR_ENSURE_METHOD_CAPACITY(float_list_ensure_capacity, struct FloatList, float); +DEFINE_VECTOR_METHOD_FREE(float_list_free, struct FloatList); +DEFINE_VECTOR_METHOD_FREE(float_list_clear_retaining_capacity, struct FloatList); + +void float_list_add(struct FloatList *list, float value) +{ + float_list_ensure_capacity(list, list->len + 1); + + list->items[list->len++] = value; +} diff --git a/src/float_list.h b/src/float_list.h new file mode 100644 index 0000000..9775581 --- /dev/null +++ b/src/float_list.h @@ -0,0 +1,8 @@ +#pragma once + +#include "generic_vector.h" + +DEFINE_VECTOR_STRUCT(FloatList, float); + +void float_list_add(struct FloatList *list, float value); +void float_list_free(struct FloatList *list); diff --git a/src/game.c b/src/game.c index 5fb3e37..b27dafc 100644 --- a/src/game.c +++ b/src/game.c @@ -1,14 +1,316 @@ #include "game.h" +#include "entity.h" +#include "entity_id_list.h" +#include "float_list.h" +#include "raymath.h" +#include +#include void game_init(struct Game *game) { *game = (struct Game){ .canvas_size = { 800, 600 } }; + + { + struct Entity *player = entity_list_add(&game->entities); + player->type = ENTITY_TYPE_PLAYER; + player->position = Vector2Scale(game->canvas_size, 0.5); + player->collision_rect = (Rectangle){ + .x = -10, + .y = -10, + .width = 20, + .height = 20 + }; + } + + float wall_thickness = 10; + + // Canvas bounds + { + game_add_wall(game, (Rectangle){ + .x = -wall_thickness, + .y = -wall_thickness, + .width = game->canvas_size.x + 2*wall_thickness, + .height = wall_thickness + }); + + game_add_wall(game, (Rectangle){ + .x = -wall_thickness, + .y = game->canvas_size.y, + .width = game->canvas_size.x + 2*wall_thickness, + .height = wall_thickness + }); + + game_add_wall(game, (Rectangle){ + .x = -wall_thickness, + .y = -wall_thickness, + .width = wall_thickness, + .height = game->canvas_size.y + 2*wall_thickness + }); + + game_add_wall(game, (Rectangle){ + .x = game->canvas_size.x, + .y = -wall_thickness, + .width = wall_thickness, + .height = game->canvas_size.y + 2*wall_thickness + }); + } + + for (int i = 0; i < 5; i++) { + game_add_wall(game, (Rectangle){ + .x = 10 + 45 * i, + .y = 10, + .width = 40, + .height = 40 + }); + } + + { + EntityId wall_id = game_add_wall(game, (Rectangle){ + .x = 10, + .y = 10, + .width = 40, + .height = 200 + }); + struct Entity *wall = entity_list_get(&game->entities, wall_id); + wall->velocity.x = 100; + } + + { + EntityId wall_id = game_add_wall(game, (Rectangle){ + .x = 700, + .y = 10, + .width = 40, + .height = 200 + }); + struct Entity *wall = entity_list_get(&game->entities, wall_id); + wall->velocity.x = -100; + } } void game_free(struct Game *game) { + entity_list_free(&game->entities); +} + +EntityId game_add_wall(struct Game *game, Rectangle rect) +{ + struct Entity *wall = entity_list_add(&game->entities); + + wall->type = ENTITY_TYPE_WALL; + wall->position = (Vector2){ + .x = rect.x, + .y = rect.y, + }; + wall->collision_rect = (Rectangle){ + .width = rect.width, + .height = rect.height + }; + + return entity_list_get_id(&game->entities, wall); +} + +static float game_aabb(Rectangle b1, Rectangle b2) +{ + return CheckCollisionRecs(b1, b2); +} + +struct CollisionResult { + float time; + Vector2 normal; +}; + +static bool game_swept_aabb(Rectangle b1, Vector2 v1, Rectangle b2, struct CollisionResult *result) +{ + float entryX, exitX; + if (v1.x > 0) { + entryX = b2.x - (b1.x + b1.width); + exitX = (b2.x + b2.width) - b1.x; + } else { + entryX = (b2.x + b2.width) - b1.x; + exitX = b2.x - (b1.x + b1.width); + } + + float entryY, exitY; + if (v1.y > 0) { + entryY = b2.y - (b1.y + b1.height); + exitY = (b2.y + b2.height) - b1.y; + } else { + entryY = (b2.y + b2.height) - b1.y; + exitY = b2.y - (b1.y + b1.height); + } + + float entryXTime, exitXTime; + if (v1.x == 0) { + entryXTime = -INFINITY; + exitXTime = INFINITY; + } else { + entryXTime = entryX / v1.x; + exitXTime = exitX / v1.x; + } + + float entryYTime, exitYTime; + if (v1.y == 0) { + entryYTime = -INFINITY; + exitYTime = INFINITY; + } else { + entryYTime = entryY / v1.y; + exitYTime = exitY / v1.y; + } + + float entryTime = MAX(entryXTime, entryYTime); + float exitTime = MIN(exitXTime, exitYTime); + + if (entryTime > exitTime || (entryXTime < 0 && entryYTime < 0) || entryXTime > 1.0f || entryYTime > 1.0f) { + return false; + } + + ASSERT(0 <= entryTime && entryTime <= 1); + + *result = (struct CollisionResult){ + .time = entryTime, + }; + + if (entryXTime > entryYTime) { + result->normal.x = entryX < 0.0 ? 1 : -1; + } else { + result->normal.y = entryY < 0.0 ? 1 : -1; + } + + return true; +} + +static Rectangle rect_add_position(Rectangle rect, Vector2 pos) +{ + rect.x += pos.x; + rect.y += pos.y; + return rect; +} + +static Rectangle rect_get_step_bounds(Rectangle rect, Vector2 step) +{ + if (step.x >= 0) { + rect.width += step.x; + } else { + rect.x += step.x; + rect.width -= step.x; + } + + if (step.y >= 0) { + rect.height += step.y; + } else { + rect.y += step.y; + rect.height -= step.y; + } + + return rect; +} + +static Vector2 rect_get_center(Rectangle rect) +{ + return (Vector2){ + .x = rect.x + rect.width / 2, + .y = rect.y + rect.height / 2, + }; +} + +static void game_list_nearest_walls(struct Game *game, struct EntityIdList *walls, Rectangle bounds, struct EntityIdList *result) +{ + struct FloatList distances = { 0 }; + entity_id_list_clear_retaining_capacity(result); + + for (size_t i = 0; i < walls->len; i++) { + EntityId wall_id = walls->items[i]; + struct Entity *wall = entity_list_get(&game->entities, wall_id); + ASSERT(wall != NULL); + + Rectangle wall_collider = rect_add_position(wall->collision_rect, wall->position); + if (game_aabb(wall_collider, bounds)) { + float distance_sqr = Vector2DistanceSqr( + rect_get_center(wall_collider), + rect_get_center(bounds) + ); + entity_id_list_add(result, wall_id); + float_list_add(&distances, distance_sqr); + } + } + + for (int i = 0; i < (int)result->len - 1; i++) { + for (int j = i + 1; j < (int)result->len; j++) { + if (distances.items[i] < distances.items[j]) { + { + float tmp = distances.items[i]; + distances.items[i] = distances.items[j]; + distances.items[j] = tmp; + } + + { + EntityId tmp = result->items[i]; + result->items[i] = result->items[j]; + result->items[j] = tmp; + } + } + } + } + + float_list_free(&distances); +} + +static void game_collision_response_slide(Vector2 *step, struct CollisionResult collision) +{ + Vector2 new_step = Vector2Scale(*step, collision.time); + + float remaining_time = 1 - collision.time; + float dotprod = (step->x * collision.normal.y + step->y * collision.normal.x) * remaining_time; + new_step.x += dotprod * collision.normal.y; + new_step.y += dotprod * collision.normal.x; + + *step = new_step; +} + +static void game_collide_against_walls(struct Game *game, struct EntityIdList *walls, Rectangle collider, Vector2 *step) +{ + TracyCZoneN(game_collide_against_walls, "game_collide_against_walls", true); + + struct EntityIdList nearest_walls = { 0 }; + game_list_nearest_walls( + game, + walls, + rect_get_step_bounds( + collider, + *step + ), + &nearest_walls + ); + + for (size_t i = 0; i < nearest_walls.len; i++) { + struct Entity *wall = entity_list_get(&game->entities, nearest_walls.items[i]); + ASSERT(wall != NULL); + + Rectangle wall_collider = rect_add_position(wall->collision_rect, wall->position); + + struct CollisionResult collision = { 0 }; + if (!game_swept_aabb(collider, *step, wall_collider, &collision)) { + continue; + } + + game_collision_response_slide(step, collision); + } + + entity_id_list_free(&nearest_walls); + + TracyCZoneEnd(game_collide_against_walls) +} + +static int signf(float value) { + if (value > 0) { + return 1; + } else if (value < 0) { + return -1; + } else { + return 0; + } } void game_tick(struct Game *game) @@ -28,10 +330,109 @@ void game_tick(struct Game *game) if (IsKeyDown(KEY_A)) { dir.x -= 1; } + dir = Vector2Normalize(dir); - game->player_position = Vector2Add(game->player_position, Vector2Scale(dir, 50 * game->input.dt)); + float dt = game->input.dt; - DrawRectangleV(game->player_position, (Vector2){ 50, 50 }, RED); + struct Entity *entity = NULL; + entity_list_foreach(&game->entities, entity) { + if (entity->type == ENTITY_TYPE_PLAYER) { + entity->velocity = Vector2Scale(dir, 200); + } + + entity->velocity = Vector2Add(entity->velocity, Vector2Scale(entity->accelaration, dt)); + if (entity->flags.has_max_speed) { + entity->velocity = Vector2ClampValue(entity->velocity, 0, entity->max_speed); + } + if (entity->flags.has_friction) { + float friction_force = pow(1 - entity->friction, dt); + entity->velocity = Vector2Scale(entity->velocity, friction_force); + } + entity->step = Vector2Scale(entity->velocity, dt); + } + + struct EntityIdList walls = { 0 }; + struct EntityIdList players = { 0 }; + + entity_list_foreach(&game->entities, entity) { + EntityId entity_id = entity_list_get_id(&game->entities, entity); + if (entity->type == ENTITY_TYPE_WALL) { + entity_id_list_add(&walls, entity_id); + } else if (entity->type == ENTITY_TYPE_PLAYER) { + entity_id_list_add(&players, entity_id); + } else { + entity->position = Vector2Add(entity->position, entity->step); + } + } + + for (size_t i = 0; i < walls.len; i++) { + struct Entity *wall = entity_list_get(&game->entities, walls.items[i]); + ASSERT(wall != NULL); + + for (size_t i = 0; i < players.len; i++) { + struct Entity *player = entity_list_get(&game->entities, players.items[i]); + ASSERT(player != NULL); + + Rectangle wall_collider = rect_add_position(wall->collision_rect, wall->position); + Rectangle player_collider = rect_add_position(player->collision_rect, player->position); + + if (!game_aabb(player_collider, rect_get_step_bounds(wall_collider, wall->step))) { + continue; + } + + struct CollisionResult collision = { 0 }; + if (game_swept_aabb(wall_collider, wall->step, player_collider, &collision)) { + if (collision.normal.x != 0 && signf(collision.normal.x) == signf(player->step.x)) { + player->step.x = 0; + } + if (collision.normal.y != 0 && signf(collision.normal.y) == signf(player->step.y)) { + player->step.y = 0; + } + + player->step.x += wall->step.x * (1 - collision.time); + player->step.y += wall->step.y * (1 - collision.time); + + player->step.x -= collision.normal.x * 0.001; + player->step.y -= collision.normal.y * 0.001; + } + } + + wall->position = Vector2Add(wall->position, wall->step); + } + + for (size_t i = 0; i < players.len; i++) { + struct Entity *player = entity_list_get(&game->entities, players.items[i]); + ASSERT(player != NULL); + + game_collide_against_walls( + game, + &walls, + rect_add_position(player->collision_rect, player->position), + &player->step + ); + + player->position = Vector2Add(player->position, player->step); + } + + entity_list_foreach(&game->entities, entity) { + if (entity->type == ENTITY_TYPE_PLAYER) { + Vector2 size = (Vector2){ 20, 20 }; + DrawRectangleV(Vector2Subtract(entity->position, Vector2Scale(size, 0.5)), size, RED); + DrawCircleV(entity->position, 4, BLACK); + } + + if (entity->type == ENTITY_TYPE_WALL || entity->type == ENTITY_TYPE_PLAYER) { + Rectangle rect = entity->collision_rect; + rect.x += entity->position.x; + rect.y += entity->position.y; + Color color = BLUE; + DrawRectangleRec(rect, ColorAlpha(color, 0.5)); + DrawRectangleLinesEx(rect, 2, ColorAlpha(color, 0.5)); + } + } + + entity_id_list_free(&walls); + entity_id_list_free(&players); TracyCZoneEnd(game_tick) } diff --git a/src/game.h b/src/game.h index bcfc568..d360336 100644 --- a/src/game.h +++ b/src/game.h @@ -12,11 +12,10 @@ struct Game { Vector2 canvas_size; struct EntityList entities; - Vector2 player_position; - struct Input input; }; void game_init(struct Game *game); void game_free(struct Game *game); void game_tick(struct Game *game); +EntityId game_add_wall(struct Game *game, Rectangle rect); diff --git a/src/generic_vector.h b/src/generic_vector.h new file mode 100644 index 0000000..6deb13b --- /dev/null +++ b/src/generic_vector.h @@ -0,0 +1,38 @@ +#pragma once + +#include "main.h" + +#define DEFINE_VECTOR_STRUCT(vector_name, item_t) \ + struct vector_name { \ + item_t *items; \ + size_t len; \ + size_t capacity; \ + }; + +#define DEFINE_VECTOR_ENSURE_METHOD_CAPACITY(func_name, list_t, item_t) \ + void func_name(list_t *list, size_t expected_capacity) \ + { \ + if (list->capacity < expected_capacity) { \ + size_t larger_capacity = MAX(MAX(list->capacity*2, expected_capacity), 4); \ + item_t *larger_items = realloc(list->items, larger_capacity * sizeof(item_t)); \ + if (larger_items == NULL) { \ + PANIC_OOM(); \ + } \ + list->items = larger_items; \ + list->capacity = larger_capacity; \ + } \ + } + +#define DEFINE_VECTOR_METHOD_FREE(func_name, list_t) \ + void func_name(list_t *list) \ + { \ + free(list->items); \ + list->len = 0; \ + list->capacity = 0; \ + } + +#define DEFINE_VECTOR_METHOD_CLEAR_RETAINING_CAPACITY(func_name, list_t) \ + void func_name(list_t *list) \ + { \ + list->len = 0; \ + }