From 1022f4556559e9b39ddb0d8ba5a5244494a2765d Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Thu, 19 Jun 2025 23:49:53 -0500 Subject: [PATCH] Starting on shaped tile hitboxes --- data/map project.tiled-session | 1 + data/map.tmj | 24 +++----- data/overworld.tsx | 25 +++++++++ src/dusk/entity/entity.c | 96 +++++++++++++++++++++++--------- src/dusk/entity/entity.h | 1 + src/dusk/physics/physics.c | 48 ++++++++++++++++ src/dusk/physics/physics.h | 18 ++++++ src/dusk/util/fixed.c | 28 ++++++++-- src/dusk/util/fixed.h | 38 ++++++++++++- src/dusk/world/CMakeLists.txt | 1 + src/dusk/world/chunk.c | 14 +++++ src/dusk/world/chunk.h | 10 ++++ src/dusk/world/tile.c | 8 +++ src/dusk/world/tile.h | 41 +++++++++++++- tools/mapcompile/mapcompile.py | 2 +- tools/tilecompile/CMakeLists.txt | 21 +++++++ tools/tilecompile/tilecompile.py | 32 +++++++++++ 17 files changed, 360 insertions(+), 48 deletions(-) create mode 100644 src/dusk/world/tile.c create mode 100644 tools/tilecompile/CMakeLists.txt create mode 100644 tools/tilecompile/tilecompile.py diff --git a/data/map project.tiled-session b/data/map project.tiled-session index 457c757..fe9676b 100644 --- a/data/map project.tiled-session +++ b/data/map project.tiled-session @@ -33,6 +33,7 @@ "overworld.tsx" ], "project": "map project.tiled-project", + "property.type": "int", "recentFiles": [ "overworld.tsx", "map.tmj", diff --git a/data/map.tmj b/data/map.tmj index 31a262f..fe06d5f 100644 --- a/data/map.tmj +++ b/data/map.tmj @@ -411,17 +411,11 @@ "id":3, "name":"Object Layer 1", "objects":[ - { - "id":3, - "template":"templates\/NPC.tx", - "x":6551.3106060606, - "y":6829.13636363636 - }, { "id":4, "template":"templates\/NPC.tx", - "x":6649.66666666667, - "y":6741.58333333334 + "x":6650.41666666667, + "y":6753.58333333334 }, { "gid":257, @@ -438,14 +432,14 @@ { "id":7, "template":"templates\/NPC.tx", - "x":6497, - "y":6942 + "x":6700.25, + "y":6838.75 }, { "id":8, "template":"templates\/NPC.tx", - "x":6640, - "y":6817 + "x":6559, + "y":6754.75 }, { "id":9, @@ -456,8 +450,8 @@ { "id":10, "template":"templates\/NPC.tx", - "x":6781, - "y":6746.33333333333 + "x":6697, + "y":6769.08333333333 }], "opacity":1, "type":"objectgroup", @@ -466,7 +460,7 @@ "y":0 }], "nextlayerid":6, - "nextobjectid":11, + "nextobjectid":13, "orientation":"orthogonal", "renderorder":"right-down", "tiledversion":"1.11.1", diff --git a/data/overworld.tsx b/data/overworld.tsx index cbfff52..eb6d93e 100644 --- a/data/overworld.tsx +++ b/data/overworld.tsx @@ -1,6 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dusk/entity/entity.c b/src/dusk/entity/entity.c index dcc097e..7d186b4 100644 --- a/src/dusk/entity/entity.c +++ b/src/dusk/entity/entity.c @@ -55,33 +55,77 @@ void entityUpdate(entity_t *entity) { fixed248_t halfTileWH = FIXED248(TILE_WIDTH_HEIGHT / 2, 0); fixed248_t selfCircR = halfTileWH; - // Check for collisions with the world boundaries + // Check for collisions with tiles + uint8_t tileChecks = ENTITY_TILE_COLISSION_RESOLUTION_COUNT; + do { + // loop to resolve tile collisions multiple times to handle sliding + // correctly + bool tileResolved = false; - // Check for collisions with tile - fixed248_t tileStartX = fx248Floor(newX - halfTileWH); - fixed248_t tileStartY = fx248Floor(newY - halfTileWH); - fixed248_t tileEndX = fx248Ceil(newX + halfTileWH); - fixed248_t tileEndY = fx248Ceil(newY + halfTileWH); + // Compute affected tile range + fixed248_t tileStartX = fx248Floor(fx248Divfx248( + (newX - halfTileWH), FIXED248(TILE_WIDTH_HEIGHT, 0) + )); + fixed248_t tileStartY = fx248Floor(fx248Divfx248( + (newY - halfTileWH), FIXED248(TILE_WIDTH_HEIGHT, 0) + )); + fixed248_t tileEndX = fx248Ceil(fx248Divfx248( + (newX + halfTileWH), FIXED248(TILE_WIDTH_HEIGHT, 0) + )); + fixed248_t tileEndY = fx248Ceil(fx248Divfx248( + (newY + halfTileWH), FIXED248(TILE_WIDTH_HEIGHT, 0) + )); - for(fixed248_t y = tileStartY; y <= tileEndY; y+= FIXED248_ONE) { - for(fixed248_t x = tileStartX; x <= tileEndX; x+= FIXED248_ONE) { - // + // For each tile + for(fixed248_t y = tileStartY; y <= tileEndY; y += FIXED248_ONE) { + for(fixed248_t x = tileStartX; x <= tileEndX; x += FIXED248_ONE) { + uint16_t tileX = fx248Tou16(x); + uint16_t tileY = fx248Tou16(y); + uint16_t chunkX = tileX / CHUNK_WIDTH; + uint16_t chunkY = tileY / CHUNK_HEIGHT; + chunk_t *chunk = chunkGetChunkAt(chunkX, chunkY); + if(chunk == NULL) continue; + + uint8_t chunkTileX = tileX % CHUNK_WIDTH; + uint8_t chunkTileY = tileY % CHUNK_HEIGHT; + tile_t tile = chunk->tilesBase[chunkTileY * CHUNK_WIDTH + chunkTileX]; + + tilesolidtype_t solidType = ( + tile < TILE_META_DATA_COUNT ? TILE_META_DATA[tile].solidType : + TILE_SOLID_NONE + ); + + collisionresult_t collision; + switch(solidType) { + case TILE_SOLID_FULL: + collision = physicsCheckCircleAABB( + newX, newY, selfCircR, + fx248Mulfx248(x, FIXED248(TILE_WIDTH_HEIGHT, 0)), + fx248Mulfx248(y, FIXED248(TILE_WIDTH_HEIGHT, 0)), + FIXED248(TILE_WIDTH_HEIGHT, 0), + FIXED248(TILE_WIDTH_HEIGHT, 0) + ); + + default: + continue; + } + + + if(collision.hit && collision.depth > FIXED248(0, 1)) { + fixed248_t slideX = fx248Mulfx248( + collision.normalX, collision.depth + ); + fixed248_t slideY = fx248Mulfx248( + collision.normalY, collision.depth + ); + newX -= slideX; + newY -= slideY; + tileResolved = true; + } + } } - } - - // uint16_t tileStartX = fx248Tou16(fx248Floor(newX - halfTileWH)); - // uint16_t tileStartY = fx248Tou16(fx248Floor(newY - halfTileWH)); - // uint16_t tileEndX = fx248Tou16(fx248Ceil(newX + halfTileWH)) + 1; - // uint16_t tileEndY = fx248Tou16(fx248Ceil(newY + halfTileWH)) + 1; - - // printf("Checking tiles between (%u, %u) and (%u, %u)\n", - // tileStartX, tileStartY, tileEndX, tileEndY - // ); - // for(uint16_t tileY = tileStartY; tileY <= tileEndY; tileY++) { - // for(uint16_t tileX = tileStartX; tileX <= tileEndX; tileX++) { - // } - // } - + if(!tileResolved) break; // no more overlaps + } while(--tileChecks > 0); // Check for collisions with other entities entity_t *otherEntity = ENTITIES; @@ -98,8 +142,8 @@ void entityUpdate(entity_t *entity) { if(!collision.hit) continue; // Collision with entity detected. Slide out of collision. - fixed248_t slideX = fx248Mulf32(collision.normalX, fx248Tof32(collision.depth)); - fixed248_t slideY = fx248Mulf32(collision.normalY, fx248Tof32(collision.depth)); + fixed248_t slideX = fx248Mulfx248(collision.normalX, collision.depth); + fixed248_t slideY = fx248Mulfx248(collision.normalY, collision.depth); newX -= slideX; newY -= slideY; } while(++otherEntity < ENTITIES + ENTITY_COUNT_MAX); diff --git a/src/dusk/entity/entity.h b/src/dusk/entity/entity.h index 2ce559d..545d0ce 100644 --- a/src/dusk/entity/entity.h +++ b/src/dusk/entity/entity.h @@ -11,6 +11,7 @@ #include "util/fixed.h" #define ENTITY_COUNT_MAX 32 +#define ENTITY_TILE_COLISSION_RESOLUTION_COUNT 4 typedef enum { ENTITY_DIR_SOUTH = 0, diff --git a/src/dusk/physics/physics.c b/src/dusk/physics/physics.c index 59a5e57..45c49d9 100644 --- a/src/dusk/physics/physics.c +++ b/src/dusk/physics/physics.c @@ -47,6 +47,54 @@ collisionresult_t physicsCheckCircleCircle( // Penetration depth = sum of radii - distance result.depth = fx248Subfx248(rSum, dist); } + result.hit = true; + return result; +} + +collisionresult_t physicsCheckCircleAABB( + fixed248_t circleX, fixed248_t circleY, fixed248_t circleR, + fixed248_t aabbX, fixed248_t aabbY, + fixed248_t aabbWidth, fixed248_t aabbHeight +) { + collisionresult_t result; + + // Find the closest point on the AABB to the circle center + fixed248_t closestX = fx248Max( + aabbX, fx248Min(circleX, fx248Addfx248(aabbX, aabbWidth)) + ); + fixed248_t closestY = fx248Max( + aabbY, fx248Min(circleY, fx248Addfx248(aabbY, aabbHeight)) + ); + + // Vector from circle center to closest point + fixed248_t dx = fx248Subfx248(closestX, circleX); + fixed248_t dy = fx248Subfx248(closestY, circleY); + + // Distance squared from circle center to closest point + fixed248_t distSq = fx248Addfx248(fx248Mulfx248(dx, dx), fx248Mulfx248(dy, dy)); + + // Check if distance is less than radius squared + if(distSq > fx248Mulfx248(circleR, circleR)) { + result.hit = false; + return result; + } + + // Collision: calculate normal and penetration depth + fixed248_t dist = fx248Sqrt(distSq); + + if(dist <= FIXED248(0, 1)) { + // Circle center is at the AABB corner + result.normalX = FIXED248(1,0); + result.normalY = FIXED248(0,1); + result.depth = circleR; + } else { + // Normalized direction from circle center to closest point + result.normalX = fx248Divfx248(dx, dist); + result.normalY = fx248Divfx248(dy, dist); + // Penetration depth = radius - distance + result.depth = fx248Subfx248(circleR, dist); + } + result.hit = true; return result; } \ No newline at end of file diff --git a/src/dusk/physics/physics.h b/src/dusk/physics/physics.h index 3432819..8bc32f6 100644 --- a/src/dusk/physics/physics.h +++ b/src/dusk/physics/physics.h @@ -28,4 +28,22 @@ typedef struct { collisionresult_t physicsCheckCircleCircle( fixed248_t circle0x, fixed248_t circle0y, fixed248_t circle0r, fixed248_t circle1x, fixed248_t circle1y, fixed248_t circle1r +); + +/** + * Check for collision between a circle and an axis-aligned bounding box (AABB). + * + * @param circleX X coordinate of the circle's center. + * @param circleY Y coordinate of the circle's center. + * @param circleR Radius of the circle. + * @param aabb X coordinate of the AABB's top-left corner. + * @param aabbY Y coordinate of the AABB's top-left corner. + * @param aabbWidth Width of the AABB. + * @param aabbHeight Height of the AABB. + * @return A collisionresult_t structure containing collision information. + */ +collisionresult_t physicsCheckCircleAABB( + fixed248_t circleX, fixed248_t circleY, fixed248_t circleR, + fixed248_t aabb, fixed248_t aabbY, + fixed248_t aabbWidth, fixed248_t aabbHeight ); \ No newline at end of file diff --git a/src/dusk/util/fixed.c b/src/dusk/util/fixed.c index 7cdc02b..e54b454 100644 --- a/src/dusk/util/fixed.c +++ b/src/dusk/util/fixed.c @@ -24,24 +24,34 @@ fixed248_t fx248Fromu16(const uint16_t b) { return (fixed248_t)((int32_t)b << FIXED248_FRACTION_BITS); } +fixed248_t fx248Fromu8(const uint8_t b) { + return (fixed248_t)((int32_t)b << FIXED248_FRACTION_BITS); +} + int32_t fx248Toi32(const fixed248_t a) { - return (int32_t)(a >> FIXED248_FRACTION_BITS); + return a >> FIXED248_FRACTION_BITS; } uint32_t fx248Tou32(const fixed248_t a) { - return (uint32_t)((a >> FIXED248_FRACTION_BITS) & 0xFFFFFFFF); + return (uint32_t)(a >> FIXED248_FRACTION_BITS); } float_t fx248Tof32(const fixed248_t a) { - return (float_t)(a / (float_t)(1 << FIXED248_FRACTION_BITS)); + return (float_t)a / (1 << FIXED248_FRACTION_BITS); } uint16_t fx248Tou16(const fixed248_t a) { - return (uint16_t)((a >> FIXED248_FRACTION_BITS) & 0xFFFF); + return (uint16_t)(a >> FIXED248_FRACTION_BITS); } +uint8_t fx248Tou8(const fixed248_t a) { + return (uint8_t)(a >> FIXED248_FRACTION_BITS); +} + + + fixed248_t fx248Addfx248(const fixed248_t a, const fixed248_t b) { return a + b; } @@ -170,4 +180,14 @@ fixed248_t fx248Sqrt(const fixed248_t a) { y = (y + div) >> 1; } return y; +} + + + +fixed248_t fx248Max(const fixed248_t a, const fixed248_t b) { + return (a > b) ? a : b; +} + +fixed248_t fx248Min(const fixed248_t a, const fixed248_t b) { + return (a < b) ? a : b; } \ No newline at end of file diff --git a/src/dusk/util/fixed.h b/src/dusk/util/fixed.h index 3ccfa1d..63bbc54 100644 --- a/src/dusk/util/fixed.h +++ b/src/dusk/util/fixed.h @@ -52,6 +52,14 @@ fixed248_t fx248Fromf32(const float_t b); */ fixed248_t fx248Fromu16(const uint16_t b); +/** + * Convert a uint8_t value to a fixed248_t value. + * + * @param b The uint8_t value to convert. + * @return The converted fixed248_t value. + */ +fixed248_t fx248Fromu8(const uint8_t b); + /** @@ -86,6 +94,14 @@ float_t fx248Tof32(const fixed248_t a); */ uint16_t fx248Tou16(const fixed248_t a); +/** + * Convert a fixed248_t value to an uint8_t value. + * + * @param a The fixed248_t value to convert. + * @return The converted uint8_t value. + */ +uint8_t fx248Tou8(const fixed248_t a); + /** @@ -299,4 +315,24 @@ uint32_t fx248Roundu32(const fixed248_t a); * * @param a The fixed248_t value to calculate the square root of. */ -fixed248_t fx248Sqrt(const fixed248_t a); \ No newline at end of file +fixed248_t fx248Sqrt(const fixed248_t a); + + + +/** + * Returns the maximum of two fixed248_t values. + * + * @param a First fixed248_t value. + * @param b Second fixed248_t value. + * @return The maximum of the two values. + */ +fixed248_t fx248Max(const fixed248_t a, const fixed248_t b); + +/** + * Returns the minimum of two fixed248_t values. + * + * @param a First fixed248_t value. + * @param b Second fixed248_t value. + * @return The minimum of the two values. + */ +fixed248_t fx248Min(const fixed248_t a, const fixed248_t b); \ No newline at end of file diff --git a/src/dusk/world/CMakeLists.txt b/src/dusk/world/CMakeLists.txt index 446cdbf..6fb8d74 100644 --- a/src/dusk/world/CMakeLists.txt +++ b/src/dusk/world/CMakeLists.txt @@ -8,4 +8,5 @@ target_sources(${DUSK_TARGET_NAME} PRIVATE chunk.c overworld.c + tile.c ) \ No newline at end of file diff --git a/src/dusk/world/chunk.c b/src/dusk/world/chunk.c index a7cc100..4bf7e6b 100644 --- a/src/dusk/world/chunk.c +++ b/src/dusk/world/chunk.c @@ -169,6 +169,20 @@ void chunkMapSetPosition(const uint16_t x, const uint16_t y) { chunkMapShift(shiftX, shiftY); } +chunk_t * chunkGetChunkAt(const uint16_t chunkX, const uint16_t chunkY) { + assertTrue( + chunkX < WORLD_WIDTH && chunkY < WORLD_HEIGHT, + "Chunk coordinates out of bounds" + ); + + chunk_t *chunk = CHUNK_MAP.chunks; + do { + if(chunk->x == chunkX && chunk->y == chunkY) return chunk; + chunk++; + } while(chunk < CHUNK_MAP.chunks + CHUNK_MAP_COUNT); + return NULL; +} + void chunkLoad(chunk_t *chunk, const uint16_t x, const uint16_t y) { assertNotNull(chunk, "Chunk pointer is null"); diff --git a/src/dusk/world/chunk.h b/src/dusk/world/chunk.h index 4194931..b389fc2 100644 --- a/src/dusk/world/chunk.h +++ b/src/dusk/world/chunk.h @@ -56,6 +56,16 @@ void chunkMapShift(const int16_t x, const int16_t y); */ void chunkMapSetPosition(const uint16_t x, const uint16_t y); +/** + * Gets the chunk at the specified chunk coordinates. + * + * @param chunkX The x coordinate of the chunk. + * @param chunkY The y coordinate of the chunk. + * @return A pointer to the chunk at the specified chunk coordinates, or NULL if + * no chunk exists at those coordinates. + */ +chunk_t * chunkGetChunkAt(const uint16_t chunkX, const uint16_t chunkY); + /** * Loads a chunk at the specified coordinates. * diff --git a/src/dusk/world/tile.c b/src/dusk/world/tile.c new file mode 100644 index 0000000..23aa912 --- /dev/null +++ b/src/dusk/world/tile.c @@ -0,0 +1,8 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "tile.h" \ No newline at end of file diff --git a/src/dusk/world/tile.h b/src/dusk/world/tile.h index 5663921..755221f 100644 --- a/src/dusk/world/tile.h +++ b/src/dusk/world/tile.h @@ -10,4 +10,43 @@ #define TILE_WIDTH_HEIGHT 16 -typedef uint8_t tile_t; \ No newline at end of file +typedef uint8_t tile_t; + +typedef enum { + TILE_SOLID_NONE = 0, + TILE_SOLID_FULL = 1, + TILE_SOLID_TRIANGLE_TOP_RIGHT = 2, + TILE_SOLID_TRIANGLE_TOP_LEFT = 3, + TILE_SOLID_TRIANGLE_BOTTOM_RIGHT = 4, + TILE_SOLID_TRIANGLE_BOTTOM_LEFT = 5, +} tilesolidtype_t; + +typedef struct { + tilesolidtype_t solidType; +} tilemeta_t; + +static const tilemeta_t TILE_META_DATA[] = { + {TILE_SOLID_NONE}, // 0 + {TILE_SOLID_NONE}, // 1 + {TILE_SOLID_NONE}, // 2 + {TILE_SOLID_NONE}, // 3 + {TILE_SOLID_NONE}, // 4 + {TILE_SOLID_NONE}, // 5 + {TILE_SOLID_NONE}, // 6 + {TILE_SOLID_NONE}, // 7 + {TILE_SOLID_NONE}, // 8 + {TILE_SOLID_NONE}, // 9 + {TILE_SOLID_NONE}, // 10 + {TILE_SOLID_NONE}, // 11 + {TILE_SOLID_NONE}, // 12 + {TILE_SOLID_NONE}, // 13 + {TILE_SOLID_NONE}, // 14 + {TILE_SOLID_NONE}, // 15 + {TILE_SOLID_NONE}, // 16 + {TILE_SOLID_NONE}, // 17 + {TILE_SOLID_FULL}, // 18 + {TILE_SOLID_FULL}, // 19 + {TILE_SOLID_FULL}, // 19 +}; + +#define TILE_META_DATA_COUNT (sizeof(TILE_META_DATA) / sizeof(tilemeta_t)) \ No newline at end of file diff --git a/tools/mapcompile/mapcompile.py b/tools/mapcompile/mapcompile.py index 37102b1..2c65ef3 100644 --- a/tools/mapcompile/mapcompile.py +++ b/tools/mapcompile/mapcompile.py @@ -422,7 +422,7 @@ with open(headerPath, 'w') as f: for i in range(worldHeight): f.write(" ") for j in range(worldWidth): - if (j, i) in chunksDone: + if(j, i) in chunksDone: f.write(f"&CHUNK_{j}_{i}, ") else: f.write("NULL, ") diff --git a/tools/tilecompile/CMakeLists.txt b/tools/tilecompile/CMakeLists.txt new file mode 100644 index 0000000..7effe6c --- /dev/null +++ b/tools/tilecompile/CMakeLists.txt @@ -0,0 +1,21 @@ +# Copyright (c) 2025 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +find_package(Python3 COMPONENTS Interpreter REQUIRED) + +# Custom command to generate all header files +add_custom_target(DUSK_CHUNKS + # OUTPUT ${DUSK_GENERATED_HEADERS_DIR}/world/world.h + COMMAND + ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/mapcompile.py + --output ${DUSK_GENERATED_HEADERS_DIR} + --input ${DUSK_DATA_DIR}/map.tmj + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/mapcompile.py + COMMENT "Generating chunk header files" + VERBATIM +) + +# Ensure headers are generated before compiling main +add_dependencies(${DUSK_TARGET_NAME} DUSK_CHUNKS) \ No newline at end of file diff --git a/tools/tilecompile/tilecompile.py b/tools/tilecompile/tilecompile.py new file mode 100644 index 0000000..bc80fee --- /dev/null +++ b/tools/tilecompile/tilecompile.py @@ -0,0 +1,32 @@ +import sys, os +import argparse +from datetime import datetime +import json +import math + +# Check if the script is run with the correct arguments +parser = argparse.ArgumentParser(description="Generate chunk header files") +parser.add_argument('--output', required=True, help='Dir to output headers') +parser.add_argument('--input', required=True, help='Input JSON file from tiled') +args = parser.parse_args() + +# Ensure outdir exists +outputDir = args.output +os.makedirs(outputDir, exist_ok=True) + +# Create world directory if it does not exist +worldDir = os.path.join(outputDir, "world") +os.makedirs(worldDir, exist_ok=True) + +# Some vars used during printing +now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + +# Read the input JSON file +inputFile = args.input +if not os.path.isfile(inputFile): + print(f"Error: Input file '{inputFile}' does not exist.") + sys.exit(1) + +with open(inputFile, 'r') as f: + data = json.load(f) + \ No newline at end of file