a-game

2D platformer written from scratch.
git clone git://git.amin.space/a-game.git
Log | Files | Refs | README | LICENSE

commit ae068a9ad665bbdb451b6f4665cd232de774908a
parent 71cc0b2476ba24d62a16203dbf13c4192e1541b7
Author: amin <dev@aminmesbah.com>
Date:   Sat,  4 May 2019 05:33:02 +0000

Store rooms in chunks in a hash table

FossilOrigin-Name: a262f048273aa5c531fa28a33216339929f6bf9b779955f9fcd9ef4fd2e96084
Diffstat:
Msrc/am_math.h | 12+++++++-----
Msrc/game.c | 14++++++--------
Msrc/game.h | 1+
Msrc/world.c | 137++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/world.h | 25++++++++++++++++++++++++-
5 files changed, 163 insertions(+), 26 deletions(-)

diff --git a/src/am_math.h b/src/am_math.h @@ -193,6 +193,12 @@ internal inline v2i math_v2i_a(v2i vec1, v2i vec2) return r; } +internal inline bool math_v2i_eq(v2i vec1, v2i vec2) +{ + bool result = (vec1.x == vec2.x && vec1.y == vec2.y); + return result; +} + internal inline v3 math_v3_cross(v3 vec1, v3 vec2) { // cross product, a.k.a. outer product or vector product @@ -570,11 +576,7 @@ internal inline bool math_v2_ge(v2 v, v2 compare) return is_ge; } -internal inline bool math_is_pow_2(u64 n) -{ - bool is_pow_2 = (n & (n - 1)) == 0; - return is_pow_2; -} +#define math_is_pow_2(n) ((n) & ((n) - 1)) == 0 internal inline void math_v2_print(v2 v) { diff --git a/src/game.c b/src/game.c @@ -61,9 +61,8 @@ internal void game_init(struct GameMemory *game_memory, v2u framebuffer) }, }; - game_state->world->rooms[0] = collision_test_zone; - game_state->world->rooms[1] = knytt_hanging_fly_village; - game_state->world->num_rooms = 2; + world_room_set(game_state->world, collision_test_zone, &game_state->world_allocator); + world_room_set(game_state->world, knytt_hanging_fly_village, &game_state->world_allocator); // set up and load tiles { @@ -183,10 +182,9 @@ void game_update_and_render(struct GameMemory *game_memory, struct GameInput *ga m4 projection = math_projection_ortho(0.0f, framebuffer.width, framebuffer.height, 0.0f, -1.0f, 0.0f); v2i current_room_i = game_state->player.pos.room; - struct RoomSearchResult r = get_room_at_index(game_state->world->rooms, game_state->world->num_rooms, current_room_i); - assert(r.room_found); - struct Room current_room = game_state->world->rooms[r.room_array_index]; - u32 *tiles = current_room.tiles; + struct Room *r = world_room_get(game_state->world, current_room_i); + assert(r); + u32 *tiles = r->tiles; // render tiles { @@ -478,7 +476,7 @@ void game_update_and_render(struct GameMemory *game_memory, struct GameInput *ga } #undef RENDER_COLLISION_DEBUG_QUAD - player->pos = pos_recanonicalize(player->pos); + player->pos = world_pos_recanonicalize(player->pos); } // render player diff --git a/src/game.h b/src/game.h @@ -27,6 +27,7 @@ struct RendererState struct Shader shader; }; +// TODO: Move this to world.h struct AbsolutePos { v2i room; diff --git a/src/world.c b/src/world.c @@ -1,4 +1,4 @@ -internal struct AbsolutePos pos_recanonicalize(struct AbsolutePos p) +internal struct AbsolutePos world_pos_recanonicalize(struct AbsolutePos p) { struct AbsolutePos p_final = p; v2i room_index_delta = {0}; @@ -29,24 +29,137 @@ internal struct AbsolutePos pos_recanonicalize(struct AbsolutePos p) return p_final; } -struct RoomSearchResult +internal inline v2i world_room_get_chunk_index(v2i room_i) { - bool room_found; - size_t room_array_index; -}; + // NOTE(amin): integer division here is intentional. We want a square of + // CHUNK_NUM_ROOMS rooms to all have the same chunk index. + v2i chunk_i = { + (room_i.x - (room_i.x % CHUNK_ROOMS_PER_SIDE)) / CHUNK_ROOMS_PER_SIDE, + (room_i.y - (room_i.y % CHUNK_ROOMS_PER_SIDE)) / CHUNK_ROOMS_PER_SIDE, + }; + return chunk_i; +} + +internal u32 world_index_hash(v2i index) +{ + // http://graphics.stanford.edu/~seander/bithacks.html#InterleaveTableObvious + u16 low_x = index.x & 0xFFFF; + u16 low_y = index.y & 0xFFFF; + u32 morton_code = 0; + + // TODO: profile this + for (u32 i = 0; i < sizeof(u32); i++) + { + morton_code |= (low_x & 1u << 0) << 0 | (low_y & 1u << 0) << (0 + 1); + } + // NOTE(amin): We're not really after any of the specific cool things about + // morton codes. I just picked a hash function that seemed sensible given + // the requirements. It makes intuitive sense to mix, rather than + // concatenate the bits of the two numbers, since information from both + // will remain even if the upper bits of the hash value are discarded (for + // example, when the hash value is modulo'd against the number of hash + // table buckets). + return morton_code; +} + +internal struct WorldChunk* world_chunk_get(struct World *world, v2i chunk_i) +{ + struct WorldChunk *result = NULL; + + u32 hash_value = world_index_hash(chunk_i); + size_t hash_table_bucket = hash_value % NUM_CHUNK_BUCKETS; + assert(hash_table_bucket < NUM_CHUNK_BUCKETS); + + struct WorldChunk *chunk = world->chunk_ht[hash_table_bucket]; + while (chunk) + { + if (math_v2i_eq(chunk->index, chunk_i)) + { + result = chunk; + break; + } + chunk = chunk->next; + } + + return result; +} -internal struct RoomSearchResult get_room_at_index(struct Room *rooms, u32 num_rooms, v2i room_i) +internal struct WorldChunk* world_chunk_get_or_create(struct World *world, v2i chunk_i, struct StackAllocator *allocator) { - struct RoomSearchResult result = {0}; - for (size_t i = 0; i < num_rooms; i++) + struct WorldChunk *result = NULL; + + u32 hash_value = world_index_hash(chunk_i); + size_t hash_table_bucket = hash_value % NUM_CHUNK_BUCKETS; + assert(hash_table_bucket < NUM_CHUNK_BUCKETS); + + struct WorldChunk *chunk = world->chunk_ht[hash_table_bucket]; + while (chunk) { - struct Room *r = &rooms[i]; - if (r->index.x == room_i.x && r->index.y == room_i.y) + if (math_v2i_eq(chunk->index, chunk_i)) { - result.room_found = true; - result.room_array_index = i; + result = chunk; + break; } + chunk = chunk->next; + } + + if (!result) + { + struct WorldChunk *prev_head_chunk = world->chunk_ht[hash_table_bucket]; + struct WorldChunk *new_chunk = mem_st_alloc_struct(allocator, struct WorldChunk); + new_chunk->index = chunk_i; + // TODO: Init rooms to 0? + new_chunk->next = prev_head_chunk; + world->chunk_ht[hash_table_bucket] = new_chunk; + result = new_chunk; } + + return result; +} + +internal size_t world_room_get_index_within_chunk(v2i room_i) +{ + v2u room_i_lower_bits = { + room_i.x & CHUNK_MASK_FOR_INTERNAL_ROOM_INDEX, + room_i.y & CHUNK_MASK_FOR_INTERNAL_ROOM_INDEX, + }; + assert(room_i_lower_bits.x < CHUNK_ROOMS_PER_SIDE); + assert(room_i_lower_bits.y < CHUNK_ROOMS_PER_SIDE); + + size_t room_i_within_chunk = room_i_lower_bits.x + room_i_lower_bits.y * CHUNK_ROOMS_PER_SIDE; + assert(room_i_within_chunk < CHUNK_NUM_ROOMS); + return room_i_within_chunk; +} + +internal struct Room* world_room_get(struct World *world, v2i room_i) +{ + struct Room *result = NULL; + v2i chunk_i = world_room_get_chunk_index(room_i); + + struct WorldChunk *chunk = world_chunk_get(world, chunk_i); + if (chunk) + { + size_t room_i_in_chunk = world_room_get_index_within_chunk(room_i); + result = &chunk->rooms[room_i_in_chunk]; + } + + return result; +} + +internal struct Room* world_room_set(struct World *world, struct Room room, struct StackAllocator *allocator) +{ + struct Room *result = NULL; + v2i chunk_i = world_room_get_chunk_index(room.index); + + struct WorldChunk* chunk = world_chunk_get_or_create(world, chunk_i, allocator); + + if (chunk) + { + size_t room_i_in_chunk = world_room_get_index_within_chunk(room.index); + chunk->rooms[room_i_in_chunk] = room; + result = &chunk->rooms[room_i_in_chunk]; + } + return result; } diff --git a/src/world.h b/src/world.h @@ -1,18 +1,41 @@ // TODO: delete #define MAX_ROOMS 120 + #define ROOM_TILE_DIM_X 25 #define ROOM_TILE_DIM_Y 10 #define ROOM_TILE_COUNT ROOM_TILE_DIM_X * ROOM_TILE_DIM_Y #define ROOM_ASPECT_RATIO (f32)ROOM_TILE_DIM_X / (f32)ROOM_TILE_DIM_Y +#define NUM_CHUNK_BUCKETS 2048 +static_assert( + math_is_pow_2(NUM_CHUNK_BUCKETS), + "Must be a power of 2 for the hash table" +); + +// TODO: Tune chunk size +#define CHUNK_ROOMS_PER_SIDE 4 +static_assert( + math_is_pow_2(CHUNK_ROOMS_PER_SIDE), + "Must be a power of 2 for the chunk mask to work" +); +#define CHUNK_NUM_ROOMS (CHUNK_ROOMS_PER_SIDE * CHUNK_ROOMS_PER_SIDE) +#define CHUNK_MASK_FOR_INTERNAL_ROOM_INDEX ((1 << CHUNK_ROOMS_PER_SIDE) - 1) + struct Room { v2i index; u32 tiles[ROOM_TILE_COUNT]; }; +struct WorldChunk +{ + v2i index; + struct Room rooms[CHUNK_NUM_ROOMS]; + struct WorldChunk *next; +}; + struct World { - struct Room rooms[MAX_ROOMS]; + struct WorldChunk *chunk_ht[NUM_CHUNK_BUCKETS]; u32 num_rooms; };