fp: Make path finding consider non-traversables

This commit is contained in:
doyle 2023-09-23 13:30:54 +10:00
parent cc202cefac
commit 7d33baab4f
4 changed files with 90 additions and 26 deletions

2
External/tely vendored

@ -1 +1 @@
Subproject commit 82dffbc84d4119c0cc5b8d72972f0f4eaef83fa4 Subproject commit 30b2bb7f10bd8a53785d78a8bdbe919881ffbe2d

View File

@ -478,29 +478,31 @@ void TELY_DLL_Init(void *user_data)
// NOTE: Hero // NOTE: Hero
{ {
FP_GameEntity *hero = FP_Game_MakeEntityPointerF(game, "Hero"); FP_GameEntity *entity = FP_Game_MakeEntityPointerF(game, "Hero");
hero->local_pos = Dqn_V2_InitNx2(100.f, 100.f); entity->local_pos = Dqn_V2_InitNx2(100.f, 100.f);
hero->size_scale = Dqn_V2_InitNx1(4); entity->size_scale = Dqn_V2_InitNx1(4);
hero->sprite_sheet = &game->hero_sprite_sheet; entity->sprite_sheet = &game->hero_sprite_sheet;
hero->sprite_anims = game->hero_sprite_anims; entity->sprite_anims = game->hero_sprite_anims;
hero->local_hit_box_size = Dqn_V2_InitV2I(game->hero_sprite_sheet.sprite_size); entity->local_hit_box_size = Dqn_V2_InitV2I(game->hero_sprite_sheet.sprite_size);
hero->flags |= FP_GameEntityFlag_Clickable; entity->flags |= FP_GameEntityFlag_Clickable;
hero->flags |= FP_GameEntityFlag_MoveByKeyboard; entity->flags |= FP_GameEntityFlag_MoveByKeyboard;
hero->flags |= FP_GameEntityFlag_MoveByMouse; entity->flags |= FP_GameEntityFlag_MoveByMouse;
game->clicked_entity = hero->handle; entity->flags |= FP_GameEntityFlag_NonTraversable;
game->clicked_entity = entity->handle;
} }
// NOTE: Enemy // NOTE: Enemy
{ {
FP_GameEntity *enemy = FP_Game_MakeEntityPointerF(game, "Enemy"); FP_GameEntity *entity = FP_Game_MakeEntityPointerF(game, "Enemy");
enemy->local_pos = Dqn_V2_InitNx2(300.f, 300.f); entity->local_pos = Dqn_V2_InitNx2(300.f, 300.f);
enemy->size_scale = Dqn_V2_InitNx1(4); entity->size_scale = Dqn_V2_InitNx1(4);
enemy->sprite_sheet = &game->hero_sprite_sheet; entity->sprite_sheet = &game->hero_sprite_sheet;
enemy->sprite_anims = game->hero_sprite_anims; entity->sprite_anims = game->hero_sprite_anims;
enemy->local_hit_box_size = Dqn_V2_InitV2I(game->hero_sprite_sheet.sprite_size); entity->local_hit_box_size = Dqn_V2_InitV2I(game->hero_sprite_sheet.sprite_size);
enemy->flags |= FP_GameEntityFlag_Clickable; entity->flags |= FP_GameEntityFlag_Clickable;
enemy->flags |= FP_GameEntityFlag_MoveByKeyboard; entity->flags |= FP_GameEntityFlag_MoveByKeyboard;
enemy->flags |= FP_GameEntityFlag_MoveByMouse; entity->flags |= FP_GameEntityFlag_MoveByMouse;
entity->flags |= FP_GameEntityFlag_NonTraversable;
} }
// NOTE: Wall // NOTE: Wall
@ -511,6 +513,7 @@ void TELY_DLL_Init(void *user_data)
entity->flags |= FP_GameEntityFlag_Clickable; entity->flags |= FP_GameEntityFlag_Clickable;
entity->flags |= FP_GameEntityFlag_MoveByKeyboard; entity->flags |= FP_GameEntityFlag_MoveByKeyboard;
entity->flags |= FP_GameEntityFlag_MoveByMouse; entity->flags |= FP_GameEntityFlag_MoveByMouse;
entity->flags |= FP_GameEntityFlag_NonTraversable;
FP_GameShape *wall = Dqn_FArray_Make(&entity->shapes, Dqn_ZeroMem_Yes); FP_GameShape *wall = Dqn_FArray_Make(&entity->shapes, Dqn_ZeroMem_Yes);
wall->type = FP_GameShapeType_Rect; wall->type = FP_GameShapeType_Rect;
@ -593,9 +596,7 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_Renderer *renderer,
entity->waypoints->next = entity->waypoints; entity->waypoints->next = entity->waypoints;
entity->waypoints->prev = entity->waypoints; entity->waypoints->prev = entity->waypoints;
Dqn_V2I entity_tile = Dqn_V2I_InitNx2(entity_world_pos.x / game->tile_size, entity_world_pos.y / game->tile_size); Dqn_Slice<Dqn_V2I> path_find = FP_Game_AStarPathFind(game, &platform->frame_arena, platform, entity->handle, stalk_tile);
Dqn_Slice<Dqn_V2I> path_find = FP_Game_AStarPathFind(game, &platform->frame_arena, platform, entity_tile, stalk_tile);
for (Dqn_usize index = path_find.size - 1; index < path_find.size; index--) { for (Dqn_usize index = path_find.size - 1; index < path_find.size; index--) {
FP_GameWaypoint *waypoint = TELY_ChunkPool_New(game->chunk_pool, FP_GameWaypoint); FP_GameWaypoint *waypoint = TELY_ChunkPool_New(game->chunk_pool, FP_GameWaypoint);
waypoint->pos = path_find.data[index]; waypoint->pos = path_find.data[index];

View File

@ -450,8 +450,37 @@ static bool FP_Game_EntityActionHasFailed(FP_GameEntityAction const *action)
return result; return result;
} }
static Dqn_Slice<Dqn_V2I> FP_Game_AStarPathFind(FP_Game *game, Dqn_Arena *arena, TELY_Platform *platform, Dqn_V2I src_tile, Dqn_V2I dest_tile) static Dqn_V2I FP_Game_WorldPosToTilePos(FP_Game *game, Dqn_V2 world_pos)
{ {
Dqn_V2I result = Dqn_V2I_InitNx2(world_pos.x / game->tile_size, world_pos.y / game->tile_size);
return result;
}
static Dqn_Slice<Dqn_V2I> FP_Game_AStarPathFind(FP_Game *game,
Dqn_Arena *arena,
TELY_Platform *platform,
FP_GameEntityHandle entity,
Dqn_V2I dest_tile)
{
// NOTE: Enumerate the entities that are collidable ============================================
Dqn_ThreadScratch scratch = Dqn_Thread_GetScratch(arena);
Dqn_List<FP_GameEntity const *> colliders = Dqn_List_Init<FP_GameEntity const *>(scratch.arena, 128);
for (FP_GameEntityIterator it = {}; FP_Game_DFSPreOrderWalkEntityTree(game, &it, game->root_entity); ) {
FP_GameEntity const *walk_entity = it.entity;
if (entity == walk_entity->handle)
continue;
if ((walk_entity->flags & FP_GameEntityFlag_NonTraversable) == 0)
continue;
Dqn_List_Add(&colliders, walk_entity);
}
// NOTE: Setup A* state ========================================================================
Dqn_V2 entity_world_pos = FP_Game_CalcEntityWorldPos(game, entity);
Dqn_V2I src_tile = FP_Game_WorldPosToTilePos(game, entity_world_pos);
Dqn_DSMap<FP_GameAStarNode> astar_info = Dqn_DSMap_Init<FP_GameAStarNode>(128); Dqn_DSMap<FP_GameAStarNode> astar_info = Dqn_DSMap_Init<FP_GameAStarNode>(128);
DQN_DEFER { Dqn_DSMap_Deinit(&astar_info); }; DQN_DEFER { Dqn_DSMap_Deinit(&astar_info); };
@ -465,6 +494,10 @@ static Dqn_Slice<Dqn_V2I> FP_Game_AStarPathFind(FP_Game *game, Dqn_Arena *arena,
uint64_t src_tile_u64 = (DQN_CAST(uint64_t)src_tile.y << 32) | (DQN_CAST(uint64_t)src_tile.x << 0); uint64_t src_tile_u64 = (DQN_CAST(uint64_t)src_tile.y << 32) | (DQN_CAST(uint64_t)src_tile.x << 0);
Dqn_DSMap_MakeKeyU64(&astar_info, src_tile_u64); Dqn_DSMap_MakeKeyU64(&astar_info, src_tile_u64);
// NOTE: Do the A* process =====================================================================
Dqn_usize last_successful_manhattan_dist = UINT64_MAX;
Dqn_V2I last_successful_tile = src_tile;
while (frontier.size) { while (frontier.size) {
Dqn_V2I curr_tile = Dqn_FArray_PopFront(&frontier, 1); Dqn_V2I curr_tile = Dqn_FArray_PopFront(&frontier, 1);
if (curr_tile == dest_tile) if (curr_tile == dest_tile)
@ -491,18 +524,47 @@ static Dqn_Slice<Dqn_V2I> FP_Game_AStarPathFind(FP_Game *game, Dqn_Arena *arena,
Dqn_usize const curr_cost = Dqn_DSMap_FindKeyU64(&astar_info, curr_tile_u64).value->cost; Dqn_usize const curr_cost = Dqn_DSMap_FindKeyU64(&astar_info, curr_tile_u64).value->cost;
for (Dqn_V2I next_tile : neighbours) { for (Dqn_V2I next_tile : neighbours) {
// NOTE: Calculate cost to move to this neighbouring tile.
Dqn_usize new_cost = curr_cost + 1; Dqn_usize new_cost = curr_cost + 1;
uint64_t next_tile_u64 = (DQN_CAST(uint64_t)next_tile.y << 32) | (DQN_CAST(uint64_t)next_tile.x << 0); uint64_t next_tile_u64 = (DQN_CAST(uint64_t)next_tile.y << 32) | (DQN_CAST(uint64_t)next_tile.x << 0);
Dqn_DSMapResult<FP_GameAStarNode> next_cost_result = Dqn_DSMap_MakeKeyU64(&astar_info, next_tile_u64); Dqn_DSMapResult<FP_GameAStarNode> next_cost_result = Dqn_DSMap_MakeKeyU64(&astar_info, next_tile_u64);
// NOTE: If we have already visited this node before, we only keep the cost if it's cheaper
if (next_cost_result.found && new_cost >= next_cost_result.value->cost) if (next_cost_result.found && new_cost >= next_cost_result.value->cost)
continue; continue;
// NOTE: Check if the neighbour is a non-traversable tile, if so we skip it
bool tile_is_non_traversable = false;
for (Dqn_ListIterator<FP_GameEntity const *> it = {}; Dqn_List_Iterate(&colliders, &it, 0); ) {
FP_GameEntity const *collider = *it.data;
Dqn_Rect bounding_box = FP_Game_CalcEntityWorldBoundingBox(game, collider->handle);
Dqn_RectMinMax min_max = Dqn_Rect_MinMax(bounding_box);
Dqn_V2I min_tile = FP_Game_WorldPosToTilePos(game, min_max.min);
Dqn_V2I max_tile = FP_Game_WorldPosToTilePos(game, min_max.max);
if ((next_tile.x >= min_tile.x && next_tile.x <= max_tile.x) &&
(next_tile.y >= min_tile.y && next_tile.y <= max_tile.y)) {
tile_is_non_traversable = true;
}
}
if (tile_is_non_traversable)
continue;
// NOTE: Update the node cost value and the heuristic (estimated cost to the end)
Dqn_usize manhattan_dist = DQN_ABS(dest_tile.x - next_tile.x) + DQN_ABS(dest_tile.y - next_tile.y); Dqn_usize manhattan_dist = DQN_ABS(dest_tile.x - next_tile.x) + DQN_ABS(dest_tile.y - next_tile.y);
next_cost_result.value->cost = new_cost; next_cost_result.value->cost = new_cost;
next_cost_result.value->came_from = curr_tile; next_cost_result.value->came_from = curr_tile;
next_cost_result.value->heuristic = new_cost + manhattan_dist; next_cost_result.value->heuristic = new_cost + manhattan_dist;
// NOTE: Store the last node we visited that had the best cost.
// We may end up with a partial path find that could only get
// part-way to the solution which this variable will track for us.
if (manhattan_dist < last_successful_manhattan_dist) {
last_successful_manhattan_dist = manhattan_dist;
last_successful_tile = next_tile;
}
// TODO(doyle): Find the insert location into the frontier // TODO(doyle): Find the insert location into the frontier
bool inserted = false; bool inserted = false;
DQN_FOR_UINDEX(index, frontier.size) { DQN_FOR_UINDEX(index, frontier.size) {
@ -525,14 +587,14 @@ static Dqn_Slice<Dqn_V2I> FP_Game_AStarPathFind(FP_Game *game, Dqn_Arena *arena,
} }
Dqn_usize slice_size = 0; Dqn_usize slice_size = 0;
for (Dqn_V2I it = dest_tile; it != src_tile; slice_size++) { for (Dqn_V2I it = last_successful_tile; it != src_tile; slice_size++) {
uint64_t key_u64 = (DQN_CAST(uint64_t)it.y << 32) | (DQN_CAST(uint64_t)it.x << 0); uint64_t key_u64 = (DQN_CAST(uint64_t)it.y << 32) | (DQN_CAST(uint64_t)it.x << 0);
it = Dqn_DSMap_FindKeyU64(&astar_info, key_u64).value->came_from; it = Dqn_DSMap_FindKeyU64(&astar_info, key_u64).value->came_from;
} }
Dqn_Slice<Dqn_V2I> result = Dqn_Slice_Alloc<Dqn_V2I>(arena, slice_size, Dqn_ZeroMem_No); Dqn_Slice<Dqn_V2I> result = Dqn_Slice_Alloc<Dqn_V2I>(arena, slice_size, Dqn_ZeroMem_No);
slice_size = 0; slice_size = 0;
for (Dqn_V2I it = dest_tile; it != src_tile; ) { for (Dqn_V2I it = last_successful_tile; it != src_tile; ) {
result.data[slice_size++] = it; result.data[slice_size++] = it;
uint64_t key_u64 = (DQN_CAST(uint64_t)it.y << 32) | (DQN_CAST(uint64_t)it.x << 0); uint64_t key_u64 = (DQN_CAST(uint64_t)it.y << 32) | (DQN_CAST(uint64_t)it.x << 0);
it = Dqn_DSMap_FindKeyU64(&astar_info, key_u64).value->came_from; it = Dqn_DSMap_FindKeyU64(&astar_info, key_u64).value->came_from;

View File

@ -10,6 +10,7 @@ enum FP_GameEntityFlag
FP_GameEntityFlag_MoveByMouse = 1 << 2, FP_GameEntityFlag_MoveByMouse = 1 << 2,
FP_GameEntityFlag_DrawHitBox = 1 << 3, FP_GameEntityFlag_DrawHitBox = 1 << 3,
FP_GameEntityFlag_DeriveHitBoxFromChildrenBoundingBox = 1 << 4, FP_GameEntityFlag_DeriveHitBoxFromChildrenBoundingBox = 1 << 4,
FP_GameEntityFlag_NonTraversable = 1 << 5,
}; };
enum FP_GameShapeType enum FP_GameShapeType