diff --git a/src/display/scene/overworld/sceneoverworld.c b/src/display/scene/overworld/sceneoverworld.c index ad46cf7..ed37077 100644 --- a/src/display/scene/overworld/sceneoverworld.c +++ b/src/display/scene/overworld/sceneoverworld.c @@ -37,6 +37,17 @@ errorret_t sceneOverworldInit(void) { } void sceneOverworldUpdate(void) { + // Move camera to player. + const entity_t *start = &testMap.entities[0]; + const entity_t *end = &testMap.entities[testMap.entityCount]; + while(start < end) { + if(start->type == ENTITY_TYPE_PLAYER) { + SCENE_OVERWORLD.camera.lookat.target[0] = start->position[0]; + SCENE_OVERWORLD.camera.lookat.target[1] = start->position[1]; + break; + } + start++; + } } void sceneOverworldRender(void) { @@ -69,6 +80,7 @@ void sceneOverworldRenderMap(const map_t *map) { assertNotNull(map, "Map pointer cannot be NULL"); // Draw base layer + sceneOverworldRenderMapLayer(map, &map->base); // Draw entities const entity_t *start = &map->entities[0]; @@ -78,8 +90,10 @@ void sceneOverworldRenderMap(const map_t *map) { sceneOverworldRenderEntity(start); start++; } + spriteBatchFlush(); // Draw overlay layer. + sceneOverworldRenderMapLayer(map, &map->overlay); } void sceneOverworldRenderEntity(const entity_t *entity) { @@ -88,7 +102,7 @@ void sceneOverworldRenderEntity(const entity_t *entity) { assertTrue(entity->type != ENTITY_TYPE_NULL, "Cannot have NULL entity type"); vec4 uv; - tilesetTileGetUV(&TILESET_ENTITIES, 0, uv); + tilesetPositionGetUV(&TILESET_ENTITIES, entity->direction, 0, uv); // For now, just draw a placeholder quad. spriteBatchPush( @@ -101,7 +115,30 @@ void sceneOverworldRenderEntity(const entity_t *entity) { ); } +void sceneOverworldRenderMapLayer(const map_t *map, const maplayer_t *layer) { + assertNotNull(layer, "Map layer pointer cannot be NULL"); + + for(uint32_t y = 0; y < map->height; y++) { + for(uint32_t x = 0; x < map->width; x++) { + const tile_t *tile = &layer->tiles[y * map->width + x]; + if(tile->id == 0) continue; + + spriteBatchPush( + NULL, + x * TILESET_ENTITIES.tileWidth, + y * TILESET_ENTITIES.tileHeight, + (x + 1) * TILESET_ENTITIES.tileWidth, + (y + 1) * TILESET_ENTITIES.tileHeight, + COLOR_RED, + 0, 0, 1, 1 + ); + } + } + + spriteBatchFlush(); +} + void sceneOverworldDispose(void) { // Dispose of the overworld scene. if(testAsset) assetUnlock(testAsset, testAssetRef); -} +} \ No newline at end of file diff --git a/src/display/scene/overworld/sceneoverworld.h b/src/display/scene/overworld/sceneoverworld.h index c5aa8b2..a1a948b 100644 --- a/src/display/scene/overworld/sceneoverworld.h +++ b/src/display/scene/overworld/sceneoverworld.h @@ -45,6 +45,14 @@ void sceneOverworldRenderMap(const map_t *map); */ void sceneOverworldRenderEntity(const entity_t *entity); +/** + * Render a map layer in the overworld scene. + * + * @param map Pointer to the map the layer belongs to. + * @param layer Pointer to the map layer to render. + */ +void sceneOverworldRenderMapLayer(const map_t *map, const maplayer_t *layer); + /** * Dispose of the overworld scene. */ diff --git a/src/physics/CMakeLists.txt b/src/physics/CMakeLists.txt index c8326d0..525b700 100644 --- a/src/physics/CMakeLists.txt +++ b/src/physics/CMakeLists.txt @@ -7,4 +7,5 @@ target_sources(${DUSK_TARGET_NAME} PRIVATE physicscircle.c + physicsbox.c ) \ No newline at end of file diff --git a/src/physics/physics.h b/src/physics/physics.h index 601f1fa..c347539 100644 --- a/src/physics/physics.h +++ b/src/physics/physics.h @@ -6,4 +6,5 @@ */ #pragma once -#include "physicscircle.h" \ No newline at end of file +#include "physicscircle.h" +#include "physicsbox.h" \ No newline at end of file diff --git a/src/physics/physicsbox.c b/src/physics/physicsbox.c new file mode 100644 index 0000000..6259a5f --- /dev/null +++ b/src/physics/physicsbox.c @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "physicsbox.h" + +void physicsBoxCheckBox( + const physicsbox_t a, + const physicsbox_t b, + physicsboxboxresult_t *out +) { + float_t dx = (b.min[0] + b.max[0]) / 2.0f - (a.min[0] + a.max[0]) / 2.0f; + float_t dy = (b.min[1] + b.max[1]) / 2.0f - (a.min[1] + a.max[1]) / 2.0f; + float_t combinedHalfWidths = (a.max[0] - a.min[0]) / 2.0f + (b.max[0] - b.min[0]) / 2.0f; + float_t combinedHalfHeights = (a.max[1] - a.min[1]) / 2.0f + (b.max[1] - b.min[1]) / 2.0f; + + if (fabsf(dx) < combinedHalfWidths && fabsf(dy) < combinedHalfHeights) { + out->hit = true; + float_t overlapX = combinedHalfWidths - fabsf(dx); + float_t overlapY = combinedHalfHeights - fabsf(dy); + + if (overlapX < overlapY) { + if (dx > 0) { + out->normal[0] = 1.0f; + out->normal[1] = 0.0f; + } else { + out->normal[0] = -1.0f; + out->normal[1] = 0.0f; + } + out->depth = overlapX; + } else { + if (dy > 0) { + out->normal[0] = 0.0f; + out->normal[1] = 1.0f; + } else { + out->normal[0] = 0.0f; + out->normal[1] = -1.0f; + } + out->depth = overlapY; + } + } else { + out->hit = false; + } +} \ No newline at end of file diff --git a/src/physics/physicsbox.h b/src/physics/physicsbox.h new file mode 100644 index 0000000..0c378a7 --- /dev/null +++ b/src/physics/physicsbox.h @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +typedef struct physicsbox_s { + vec2 min; + vec2 max; +} physicsbox_t; + +typedef struct physicsboxboxresult_s { + bool_t hit; + vec2 normal; + float_t depth; +} physicsboxboxresult_t; + +/** + * Check for collision between two boxes. + * + * @param a The first box. + * @param b The second box. + * @param out Pointer to the result structure to populate. + */ +void physicsBoxCheckBox( + const physicsbox_t a, + const physicsbox_t b, + physicsboxboxresult_t *out +); \ No newline at end of file diff --git a/src/rpg/entity/CMakeLists.txt b/src/rpg/entity/CMakeLists.txt index fd5a1e3..f1edcfd 100644 --- a/src/rpg/entity/CMakeLists.txt +++ b/src/rpg/entity/CMakeLists.txt @@ -9,4 +9,5 @@ target_sources(${DUSK_TARGET_NAME} entity.c npc.c player.c + direction.c ) \ No newline at end of file diff --git a/src/rpg/entity/direction.c b/src/rpg/entity/direction.c new file mode 100644 index 0000000..e4f0e5e --- /dev/null +++ b/src/rpg/entity/direction.c @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "direction.h" +#include "assert/assert.h" + +float_t directionToAngle(const direction_t dir) { + switch(dir) { + case DIRECTION_NORTH: return (M_PI_2); + case DIRECTION_SOUTH: return -(M_PI_2); + case DIRECTION_EAST: return 0; + case DIRECTION_WEST: return (M_PI); + default: return 0; // Should never happen + } +} + +void directionGetCoordinates( + const direction_t dir, + int8_t *x, int8_t *y +) { + assertNotNull(x, "X coordinate pointer cannot be NULL"); + assertNotNull(y, "Y coordinate pointer cannot be NULL"); + + switch(dir) { + case DIRECTION_NORTH: + *x = 0; + *y = -1; + break; + + case DIRECTION_SOUTH: + *x = 0; + *y = 1; + break; + + case DIRECTION_EAST: + *x = 1; + *y = 0; + break; + + case DIRECTION_WEST: + *x = -1; + *y = 0; + break; + + default: + assertUnreachable("Invalid direction"); + break; + } +} \ No newline at end of file diff --git a/src/rpg/entity/direction.h b/src/rpg/entity/direction.h new file mode 100644 index 0000000..65d6f2e --- /dev/null +++ b/src/rpg/entity/direction.h @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +typedef enum { + DIRECTION_SOUTH = 0, + DIRECTION_EAST = 1, + DIRECTION_WEST = 2, + DIRECTION_NORTH = 3, + + DIRECTION_UP = DIRECTION_NORTH, + DIRECTION_DOWN = DIRECTION_SOUTH, + DIRECTION_LEFT = DIRECTION_WEST, + DIRECTION_RIGHT = DIRECTION_EAST, +} direction_t; + +/** + * Converts a direction to an angle in float_t format. + * + * @param dir The direction to convert. + * @return The angle corresponding to the direction. + */ +float_t directionToAngle(const direction_t dir); + +/** + * Gets the relative coordinates for a given direction. + * + * @param dir The direction to get coordinates for. + * @param x Pointer to store the x coordinate. + * @param y Pointer to store the y coordinate. + */ +void directionGetCoordinates( + const direction_t dir, + int8_t *x, int8_t *y +); \ No newline at end of file diff --git a/src/rpg/entity/entity.c b/src/rpg/entity/entity.c index 56c8815..6a65885 100644 --- a/src/rpg/entity/entity.c +++ b/src/rpg/entity/entity.c @@ -6,33 +6,98 @@ */ #include "entity.h" +#include "rpg/world/map.h" #include "assert/assert.h" #include "util/memory.h" #include "display/tileset/tileset_entities.h" +#include "time/time.h" +#include "util/math.h" -entitycallback_t ENTITY_CALLBACKS[ENTITY_TYPE_COUNT] = { - { NULL, NULL }, // ENTITY_TYPE_NULL - { playerInit, playerUpdate }, // ENTITY_TYPE_PLAYER - { npcInit, npcUpdate}, // ENTITY_TYPE_NPC -}; - -void entityInit(entity_t *entity, const entitytype_t type) { +void entityInit(entity_t *entity, const entitytype_t type, map_t *map) { assertNotNull(entity, "Entity pointer cannot be NULL"); + assertNotNull(map, "Map pointer cannot be NULL"); assertTrue(type < ENTITY_TYPE_COUNT, "Invalid entity type"); assertTrue(type != ENTITY_TYPE_NULL, "Cannot have NULL entity type"); - assertNotNull(ENTITY_CALLBACKS[type].init, "Entity type has no init function"); memoryZero(entity, sizeof(entity_t)); entity->type = type; + entity->map = map; - ENTITY_CALLBACKS[type].init(entity); + // Init. I did use a callback struct but it was not flexible enough. + switch(type) { + case ENTITY_TYPE_PLAYER: + playerInit(entity); + break; + + case ENTITY_TYPE_NPC: + npcInit(entity); + break; + + default: + break; + } } void entityUpdate(entity_t *entity) { assertNotNull(entity, "Entity pointer cannot be NULL"); assertTrue(entity->type < ENTITY_TYPE_COUNT, "Invalid entity type"); assertTrue(entity->type != ENTITY_TYPE_NULL, "Cannot have NULL entity type"); - assertNotNull(ENTITY_CALLBACKS[entity->type].update, "enttype lacks update"); - ENTITY_CALLBACKS[entity->type].update(entity); + // Handle movement logic + switch(entity->type) { + case ENTITY_TYPE_PLAYER: + playerMovement(entity); + break; + + case ENTITY_TYPE_NPC: + npcUpdate(entity); + break; + + default: + break; + } + + // Apply velocity + if(entity->velocity[0] != 0.0f || entity->velocity[1] != 0.0f) { + entity->position[0] += entity->velocity[0] * TIME.delta; + entity->position[1] += entity->velocity[1] * TIME.delta; + + // Hit test on other entities. + entity_t *start = entity->map->entities; + entity_t *end = &entity->map->entities[entity->map->entityCount]; + + // Our hitbox + physicscircle_t self; + glm_vec2_copy(entity->position, self.position); + self.radius = TILESET_ENTITIES.tileWidth / 2.0f; + + physicscircle_t other; + other.radius = self.radius; + + // TODO: what if multiple collisions? + do { + if(start == entity) continue; + if(start->type == ENTITY_TYPE_NULL) continue; + glm_vec2_copy(start->position, other.position); + + physicscirclecircleresult_t result; + physicsCircleCheckCircle(self, other, &result); + + if(result.hit) { + entity->position[0] -= result.normal[0] * result.depth; + entity->position[1] -= result.normal[1] * result.depth; + break; + } + } while((start++) != end); + + // Friction (and dampening) + entity->velocity[0] *= ENTITY_FRICTION * TIME.delta; + entity->velocity[1] *= ENTITY_FRICTION * TIME.delta; + if(mathAbs(entity->velocity[0]) < ENTITY_MIN_VELOCITY) { + entity->velocity[0] = 0.0f; + } + if(mathAbs(entity->velocity[1]) < ENTITY_MIN_VELOCITY) { + entity->velocity[1] = 0.0f; + } + } } \ No newline at end of file diff --git a/src/rpg/entity/entity.h b/src/rpg/entity/entity.h index 7b96dea..0015c48 100644 --- a/src/rpg/entity/entity.h +++ b/src/rpg/entity/entity.h @@ -6,15 +6,15 @@ */ #pragma once -// #include "direction.h" +#include "direction.h" #include "rpg/entity/player.h" #include "npc.h" #include "physics/physics.h" -typedef struct { - void (*init)(entity_t *entity); - void (*update)(entity_t *entity); -} entitycallback_t; +#define ENTITY_FRICTION 0.9f +#define ENTITY_MIN_VELOCITY 0.05f + +typedef struct map_s map_t; typedef enum { ENTITY_TYPE_NULL, @@ -25,9 +25,9 @@ typedef enum { } entitytype_t; typedef struct entity_s { - // uint32_t id;// Completely unique ID for this entity. + map_t *map; entitytype_t type; - // direction_t dir; + direction_t direction; vec2 position; vec2 velocity; @@ -38,15 +38,14 @@ typedef struct entity_s { }; } entity_t; -extern entitycallback_t ENTITY_CALLBACKS[ENTITY_TYPE_COUNT]; - /** * Initializes an entity structure. * * @param entity Pointer to the entity structure to initialize. * @param type The type of the entity. + * @param map Pointer to the map the entity belongs to. */ -void entityInit(entity_t *entity, const entitytype_t type); +void entityInit(entity_t *entity, const entitytype_t type, map_t *map); /** * Updates an entity. diff --git a/src/rpg/entity/player.c b/src/rpg/entity/player.c index 90a7f66..63257b0 100644 --- a/src/rpg/entity/player.c +++ b/src/rpg/entity/player.c @@ -7,9 +7,7 @@ #include "entity.h" #include "assert/assert.h" -#include "time/time.h" #include "input/input.h" - #include "display/scene/overworld/sceneoverworld.h" #include "display/tileset/tileset_entities.h" @@ -17,36 +15,48 @@ void playerInit(entity_t *entity) { assertNotNull(entity, "Entity pointer cannot be NULL"); } -void playerUpdate(entity_t *entity) { +void playerMovement(entity_t *entity) { assertNotNull(entity, "Entity pointer cannot be NULL"); - // testing only - float_t move = TIME.delta * 64.0f; // tiles per second + // Update velocity. vec2 dir = { inputAxis(INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT), - inputAxis(INPUT_ACTION_UP, INPUT_ACTION_DOWN) + inputAxis(INPUT_ACTION_DOWN, INPUT_ACTION_UP) }; + if(dir[0] == 0 && dir[1] == 0) return; + glm_vec2_normalize(dir); - entity->position[0] += move * dir[0]; - entity->position[1] -= move * dir[1]; - - SCENE_OVERWORLD.camera.lookat.target[0] = entity->position[0]; - SCENE_OVERWORLD.camera.lookat.target[1] = entity->position[1]; - - // Can we move? - physicscircle_t npc = { - .position = { 32.0f, 32.0f }, - .radius = TILESET_ENTITIES.tileWidth / 2.0f - }; - - physicscircle_t self; - glm_vec2_copy(entity->position, self.position); - self.radius = npc.radius; - - physicscirclecircleresult_t result; - physicsCircleCheckCircle(self, npc, &result); - if(result.hit) { - entity->position[0] -= result.normal[0] * result.depth; - entity->position[1] -= result.normal[1] * result.depth; + entity->velocity[0] += PLAYER_SPEED * dir[0]; + entity->velocity[1] += PLAYER_SPEED * dir[1]; + + // Update direction. + if(dir[0] > 0) { + if(entity->direction == DIRECTION_RIGHT) { + entity->direction = DIRECTION_RIGHT; + } else { + if(dir[1] < 0) { + entity->direction = DIRECTION_UP; + } else if(dir[1] > 0) { + entity->direction = DIRECTION_DOWN; + } else { + entity->direction = DIRECTION_RIGHT; + } + } + } else if(dir[0] < 0) { + if(entity->direction == DIRECTION_LEFT) { + entity->direction = DIRECTION_LEFT; + } else { + if(dir[1] < 0) { + entity->direction = DIRECTION_UP; + } else if(dir[1] > 0) { + entity->direction = DIRECTION_DOWN; + } else { + entity->direction = DIRECTION_LEFT; + } + } + } else if(dir[1] < 0) { + entity->direction = DIRECTION_UP; + } else if(dir[1] > 0) { + entity->direction = DIRECTION_DOWN; } } \ No newline at end of file diff --git a/src/rpg/entity/player.h b/src/rpg/entity/player.h index d0018a5..ea88ef6 100644 --- a/src/rpg/entity/player.h +++ b/src/rpg/entity/player.h @@ -8,6 +8,8 @@ #pragma once #include "dusk.h" +#define PLAYER_SPEED 64.0f + typedef struct entity_s entity_t; typedef struct { @@ -22,8 +24,8 @@ typedef struct { void playerInit(entity_t *entity); /** - * Updates a player entity. + * Handles movement logic for the player entity. * - * @param entity Pointer to the entity structure to update. + * @param entity Pointer to the player entity structure. */ -void playerUpdate(entity_t *entity); \ No newline at end of file +void playerMovement(entity_t *entity); \ No newline at end of file diff --git a/src/rpg/rpg.c b/src/rpg/rpg.c index df75aee..32b1f4e 100644 --- a/src/rpg/rpg.c +++ b/src/rpg/rpg.c @@ -13,12 +13,17 @@ map_t testMap; void rpgInit() { mapInit(&testMap); + testMap.width = 2; + testMap.height = 2; + for(uint32_t i = 0; i < testMap.width * testMap.height; i++) { + testMap.base.tiles[i].id = 1; + } entity_t *ent = mapEntityAdd(&testMap); - entityInit(ent, ENTITY_TYPE_PLAYER); + entityInit(ent, ENTITY_TYPE_PLAYER, &testMap); entity_t *npc = mapEntityAdd(&testMap); - entityInit(npc, ENTITY_TYPE_NPC); + entityInit(npc, ENTITY_TYPE_NPC, &testMap); npc->position[0] = 32.0f; npc->position[1] = 32.0f; } diff --git a/src/rpg/world/map.h b/src/rpg/world/map.h index c01be2b..fce231d 100644 --- a/src/rpg/world/map.h +++ b/src/rpg/world/map.h @@ -22,7 +22,7 @@ typedef struct { tile_t tiles[MAP_TILE_COUNT_MAX]; } maplayer_t; -typedef struct { +typedef struct map_s { entity_t entities[MAP_ENTITY_COUNT_MAX]; uint8_t entityCount; diff --git a/src/util/math.h b/src/util/math.h index 5884113..3e93b98 100644 --- a/src/util/math.h +++ b/src/util/math.h @@ -42,4 +42,12 @@ uint32_t mathNextPowTwo(uint32_t value); * @param upper The upper bound. * @return The clamped value. */ -#define mathClamp(x, lower, upper) (mathMin(upper, mathMax(lower, x))) \ No newline at end of file +#define mathClamp(x, lower, upper) (mathMin(upper, mathMax(lower, x))) + +/** + * Returns the absolute value of a number. + * + * @param amt The number to get the absolute value of. + * @return The absolute value of the number. + */ +#define mathAbs(amt) ((amt) < 0 ? -(amt) : (amt)) \ No newline at end of file