fp: Add a simplified swarming algorithm

This commit is contained in:
doyle 2023-09-30 16:11:39 +10:00
parent a35cb8d2a6
commit e467abf922
6 changed files with 94 additions and 124 deletions

2
External/tely vendored

@ -1 +1 @@
Subproject commit f28426b027a5d63974ff8901459b8fb8454f39fe
Subproject commit 949f5e16338c7f981b38f03fc406dca3a499ab9a

View File

@ -91,83 +91,6 @@ TELY_AssetSpriteSheet FP_LoadSpriteSheetFromSpec(TELY_Platform *platform, TELY_A
return result;
}
struct FP_GameSwarmSlot
{
Dqn_usize count;
Dqn_V2 world_pos;
};
Dqn_FArray<FP_GameSwarmSlot, 32> FP_Entity_GetSwarmingPositions(FP_Game *game, FP_GameEntityHandle entity_handle)
{
Dqn_FArray<FP_GameSwarmSlot, 32> result = {};
FP_GameEntity *entity = FP_Game_GetEntity(game, entity_handle);
if (FP_Game_IsNilEntity(entity))
return result;
DQN_FOR_UINDEX (dir_index, FP_GameDirection_Count) {
FP_GameSwarm *swarm = entity->swarm + dir_index;
Dqn_f32 const min_dist_between_slots = FP_Game_MetersToPixelsNx1(game, 1.f);
// NOTE: Calculate the number of swarming slots we need for this entity
if (swarm->slots_active == 0) {
switch (DQN_CAST(FP_GameDirection)dir_index) {
case FP_GameDirection_Up: /*FALLTHRU*/
case FP_GameDirection_Down: {
swarm->slots_active = DQN_CAST(Dqn_usize)(entity->local_hit_box_size.w / min_dist_between_slots);
} break;
case FP_GameDirection_Left: /*FALLTHRU*/
case FP_GameDirection_Right: {
swarm->slots_active = DQN_CAST(Dqn_usize)(entity->local_hit_box_size.h / min_dist_between_slots);
} break;
case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break;
}
DQN_ASSERT(swarm->slots_active < Dqn_FArray_Max(&swarm->slots));
}
// NOTE: Calculate potential swarming positions ========================
Dqn_Rect entity_world_hit_box = FP_Game_CalcEntityWorldHitBox(game, entity->handle);
DQN_FOR_UINDEX (slot_index, swarm->slots_active) {
FP_SentinelList<FP_GameEntityHandle> *list = swarm->slots.data + slot_index;
FP_GameSwarmSlot *slot = Dqn_FArray_Make(&result, Dqn_ZeroMem_No);
slot->count = list->size;
switch (DQN_CAST(FP_GameDirection)dir_index) {
case FP_GameDirection_Up: {
Dqn_V2 start = Dqn_V2_InitNx2(entity_world_hit_box.pos.x,
entity_world_hit_box.pos.y - entity_world_hit_box.size.h * .1f);
slot->world_pos = Dqn_V2_InitNx2(start.x + slot_index * min_dist_between_slots, start.y);
} break;
case FP_GameDirection_Down: {
Dqn_V2 start = Dqn_V2_InitNx2(entity_world_hit_box.pos.x,
entity_world_hit_box.pos.y + entity_world_hit_box.size.h + entity_world_hit_box.size.h * .1f);
slot->world_pos = Dqn_V2_InitNx2(start.x + slot_index * min_dist_between_slots, start.y);
} break;
case FP_GameDirection_Left: {
Dqn_V2 start = Dqn_V2_InitNx2(entity_world_hit_box.pos.x - entity_world_hit_box.size.w * .1f,
entity_world_hit_box.pos.y);
slot->world_pos = Dqn_V2_InitNx2(start.x, start.y + slot_index * min_dist_between_slots);
} break;
case FP_GameDirection_Right: {
Dqn_V2 start = Dqn_V2_InitNx2(entity_world_hit_box.pos.x + entity_world_hit_box.size.w + entity_world_hit_box.size.w * .1f,
entity_world_hit_box.pos.y);
slot->world_pos = Dqn_V2_InitNx2(start.x, start.y + slot_index * min_dist_between_slots);
} break;
case FP_GameDirection_Count: DQN_INVALID_CODE_PATH; break;
}
}
}
return result;
}
static void FP_Game_MoveEntity(FP_Game *game, FP_GameEntityHandle entity_handle, Dqn_V2 acceleration_meters_per_s)
{
// f"(t) = a
@ -223,6 +146,7 @@ static void FP_Game_MoveEntity(FP_Game *game, FP_GameEntityHandle entity_handle,
case FP_EntityType_Terry: /*FALLTRHU*/
case FP_EntityType_Merchant: /*FALLTRHU*/
case FP_EntityType_Count: break;
case FP_EntityType_ClubTerry: break;
}
if (!entity_collides_with_collider)
@ -940,6 +864,17 @@ void FP_EntityActionStateMachine(FP_Game *game, TELY_PlatformInput *input, FP_Ga
}
}
bool FindTurnSide(Dqn_f32 current, Dqn_f32 target)
{
Dqn_f32 diff = target - current;
if(diff < 0)
diff += (DQN_PI * 2.f);
if(diff > DQN_PI)
return false; // left turn
else
return true; // right turn
}
void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input)
{
Dqn_Profiler_ZoneScopeWithIndex("FP_Update", FP_ProfileZone_FPUpdate);
@ -1037,8 +972,14 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input
if (entity->flags & FP_GameEntityFlag_AggrosWhenNearTerry) {
FP_GameFindClosestEntityResult closest_result = FP_Game_FindClosestEntityWithType(game, entity->handle, FP_EntityType_Terry);
if (closest_result.dist_squared < DQN_SQUARED(FP_Game_MetersToPixelsNx1(game, 4.f))) {
FP_SentinelListLink<FP_GameWaypoint> *first_waypoint = FP_SentinelList_Front(&entity->waypoints);
if (first_waypoint->data.entity != closest_result.entity) {
bool has_waypoint_to_terry = false;
for (FP_SentinelListLink<FP_GameWaypoint> *link = nullptr;
!has_waypoint_to_terry && FP_SentinelList_Iterate<FP_GameWaypoint>(&entity->waypoints, &link); ) {
has_waypoint_to_terry = link->data.entity == closest_result.entity;
}
if (!has_waypoint_to_terry) {
FP_GameEntity *terry = FP_Game_GetEntity(game, closest_result.entity);
FP_GameDirection aggro_direction = FP_GameDirection_Count;
DQN_FOR_UINDEX(dir_index, FP_GameDirection_Count) {
@ -1050,7 +991,9 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input
}
}
FP_SentinelListLink<FP_GameWaypoint> *link = FP_SentinelList_MakeBefore(&entity->waypoints, first_waypoint, game->chunk_pool);
FP_SentinelListLink<FP_GameWaypoint> *link = FP_SentinelList_MakeBefore(&entity->waypoints,
FP_SentinelList_Front(&entity->waypoints),
game->chunk_pool);
FP_GameWaypoint *waypoint = &link->data;
waypoint->entity = terry->handle;
@ -1063,38 +1006,35 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input
}
}
if (entity->type == FP_EntityType_ClubTerry) {
Dqn_FArray<FP_GameSwarmSlot, 32> candidate_swarm_slots = FP_Entity_GetSwarmingPositions(game, entity->handle);
DQN_ASSERT(candidate_swarm_slots.size);
for (FP_GameSwarmSlot& slot : candidate_swarm_slots)
TELY_Render_CircleColourV4(&platform->renderer, slot.world_pos, 8.f, TELY_RenderShapeMode_Line, TELY_COLOUR_RED_TOMATO_V4);
}
if (entity->flags & FP_GameEntityFlag_RespondsToClubTerry) {
FP_GameFindClosestEntityResult closest_result = FP_Game_FindClosestEntityWithType(game, entity->handle, FP_EntityType_ClubTerry);
if (closest_result.dist_squared < DQN_SQUARED(FP_Game_MetersToPixelsNx1(game, 4.f))) {
FP_SentinelListLink<FP_GameWaypoint> *first_waypoint = FP_SentinelList_Front(&entity->waypoints);
if (first_waypoint->data.entity != closest_result.entity) {
FP_GameEntity *club_terry = FP_Game_GetEntity(game, closest_result.entity);
FP_GameFindClosestEntityResult closest_club = FP_Game_FindClosestEntityWithType(game, entity->handle, FP_EntityType_ClubTerry);
if (closest_club.dist_squared < DQN_SQUARED(FP_Game_MetersToPixelsNx1(game, 4.f))) {
Dqn_FArray<FP_GameSwarmSlot, 32> candidate_swarm_slots = FP_Entity_GetSwarmingPositions(game, closest_result.entity);
DQN_ASSERT(candidate_swarm_slots.size);
Dqn_f32 closest_swarm_dist = DQN_F32_MAX;
Dqn_V2 closest_swarm_pos = {};
for (FP_GameSwarmSlot& slot : candidate_swarm_slots) {
Dqn_f32 dist = Dqn_V2_Length_V2x2(slot.world_pos, entity_pos);
if (dist < closest_swarm_dist) {
closest_swarm_dist = dist;
closest_swarm_pos = slot.world_pos;
}
bool has_waypoint_to_club = false;
for (FP_SentinelListLink<FP_GameWaypoint> *link = nullptr;
!has_waypoint_to_club && FP_SentinelList_Iterate<FP_GameWaypoint>(&entity->waypoints, &link); ) {
has_waypoint_to_club = link->data.entity == closest_club.entity;
}
FP_SentinelListLink<FP_GameWaypoint> *link = FP_SentinelList_MakeBefore(&entity->waypoints, first_waypoint, game->chunk_pool);
if (!has_waypoint_to_club) {
Dqn_Rect club_hit_box = FP_Game_CalcEntityWorldHitBox(game, closest_club.entity);
Dqn_V2 club_top_left = Dqn_Rect_TopLeft(club_hit_box);
Dqn_V2 club_top_right = Dqn_Rect_TopRight(club_hit_box);
FP_SentinelListLink<FP_GameWaypoint> *link = FP_SentinelList_MakeBefore(&entity->waypoints, FP_SentinelList_Front(&entity->waypoints), game->chunk_pool);
FP_GameWaypoint *waypoint = &link->data;
waypoint->entity = club_terry->handle;
waypoint->type = FP_GameWaypointType_Offset;
waypoint->offset = closest_swarm_pos - entity_pos;
waypoint->entity = closest_club.entity;
waypoint->type = FP_GameWaypointType_Side;
if (entity_pos.x <= club_top_left.x) {
waypoint->type_direction = FP_GameDirection_Left;
} else if (entity_pos.x >= club_top_right.x) {
waypoint->type_direction = FP_GameDirection_Right;
} else if (entity_pos.y <= club_top_left.y) {
waypoint->type_direction = FP_GameDirection_Up;
} else {
waypoint->type_direction = FP_GameDirection_Down;
}
}
}
}
@ -1136,7 +1076,7 @@ void FP_Update(TELY_Platform *platform, FP_Game *game, TELY_PlatformInput *input
// NOTE: We have arrived at the waypoint
bool aggro_on_terry = (entity->flags & FP_GameEntityFlag_AggrosWhenNearTerry) && waypoint_entity->type == FP_EntityType_Terry;
bool club_terry_response = (entity->flags & FP_GameEntityFlag_RespondsToClubTerry) && waypoint_entity->type == FP_EntityType_ClubTerry;
if (aggro_on_terry || club_terry_response) {
if (((waypoint->flags & FP_GameWaypointFlag_NonInterruptible) == 0) && (aggro_on_terry || club_terry_response)) {
bool can_attack = !entity->is_dying; // TODO(doyle): State transition needs to check if it's valid to move to making this not necessary
if (can_attack) {
Dqn_V2 attack_dir_vectors[FP_GameDirection_Count] = {};
@ -1497,6 +1437,7 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer)
}
// NOTE: Render attack box =================================================================
Dqn_Rect world_hit_box = FP_Game_CalcEntityWorldHitBox(game, entity->handle);
{
Dqn_Rect attack_box = FP_Game_CalcEntityAttackWorldHitBox(game, entity->handle);
TELY_Render_RectColourV4(renderer, attack_box, TELY_RenderShapeMode_Line, TELY_COLOUR_RED_TOMATO_V4);
@ -1506,7 +1447,6 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer)
TELY_Render_CircleColourV4(renderer, world_pos, 4.f, TELY_RenderShapeMode_Fill, TELY_COLOUR_RED_TOMATO_V4);
// NOTE: Render hot/active entity ==========================================================
Dqn_Rect world_hit_box = FP_Game_CalcEntityWorldHitBox(game, entity->handle);
if (game->clicked_entity == entity->handle) {
TELY_Render_RectColourV4(renderer, world_hit_box, TELY_RenderShapeMode_Line, TELY_COLOUR_WHITE_PALE_GOLDENROD_V4);
@ -1525,13 +1465,15 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer)
if (game->hot_entity == entity->handle) {
if (entity->name.size) {
Dqn_V2I player_tile = Dqn_V2I_InitNx2(world_pos.x / game->tile_size, world_pos.y / game->tile_size);
Dqn_V2 entity_world_pos = FP_Game_CalcEntityWorldPos(game, entity->handle);
Dqn_V2 entity_world_pos = Dqn_Rect_Center(world_hit_box);
Dqn_ThreadScratch scratch = Dqn_Thread_GetScratch(nullptr);
Dqn_String8 label = Dqn_String8_InitF(scratch.allocator,
"%.*s (%.1f, %.1f) (%I32d, %I32d)",
"%.*s|Pos: (%.1f, %.1f)|Size: %.1fx%.1f|Tile: (%I32d, %I32d)",
DQN_STRING_FMT(entity->name),
entity_world_pos.x,
entity_world_pos.y,
world_hit_box.size.w,
world_hit_box.size.h,
player_tile.x,
player_tile.y);
TELY_Render_Text(renderer, world_mouse_p, Dqn_V2_InitNx2(0.f, 1), label);

View File

@ -258,6 +258,16 @@ static bool FP_Game_IsNilEntity(FP_GameEntity *entity)
return result;
}
static bool FP_Game_IsNilEntityHandle(FP_Game *game, FP_GameEntityHandle handle)
{
bool result = true;
if (handle.id != 0) {
FP_GameEntity *entity = FP_Game_GetEntity(game, handle);
result = FP_Game_IsNilEntity(entity);
}
return result;
}
static void FP_Game_DetachEntityIntoFreeList(FP_Game *game, FP_GameEntityHandle handle)
{
FP_GameEntity *entity = FP_Game_GetEntity(game, handle);
@ -651,7 +661,7 @@ static Dqn_V2 FP_Game_CalcWaypointWorldPos(FP_Game *game, FP_GameWaypoint const
Dqn_Rect entity_rect = FP_Game_CalcEntityWorldHitBox(game, waypoint_entity->handle);
switch (waypoint->type_direction) {
case FP_GameDirection_Up: {
result = Dqn_V2_InitNx2(entity_rect.pos.x + entity_rect.size.w * .5f, entity_rect.pos.y + entity_rect.size.h * .1f);
result = Dqn_V2_InitNx2(entity_rect.pos.x + entity_rect.size.w * .5f, entity_rect.pos.y - entity_rect.size.h * .1f);
} break;
case FP_GameDirection_Down: {

View File

@ -80,8 +80,14 @@ enum FP_GameDirection
FP_GameDirection_Count,
};
enum FP_GameWaypointFlag
{
FP_GameWaypointFlag_NonInterruptible = 1 << 0,
};
struct FP_GameWaypoint
{
uint32_t flags;
FP_GameWaypointType type;
FP_GameDirection type_direction; // Used if type is `FP_GameWaypointType_Side`
FP_GameEntityHandle entity; // The entity to move to
@ -122,12 +128,6 @@ struct FP_GameRenderSprite
uint64_t started_at_clock_ms;
};
struct FP_GameSwarm
{
Dqn_FArray<FP_SentinelList<FP_GameEntityHandle>, 8> slots;
Dqn_usize slots_active;
};
struct FP_GameEntity
{
FP_GameEntity *next;
@ -153,7 +153,6 @@ struct FP_GameEntity
FP_SentinelList<FP_GameWaypoint> waypoints;
FP_GameEntityHandle aggro_slot[FP_GameDirection_Count];
FP_GameSwarm swarm[FP_GameDirection_Count];
// NOTE: The entity hit box is positioned at the center of the entity.
Dqn_V2 local_hit_box_size;

View File

@ -61,6 +61,14 @@ FP_SentinelListLink<T> *FP_SentinelList_Make(FP_SentinelList<T> *list, TELY_Chun
return result;
}
template <typename T>
FP_SentinelListLink<T> *FP_SentinelList_Add(FP_SentinelList<T> *list, TELY_ChunkPool *pool, const T& data)
{
FP_SentinelListLink<T> *result = FP_SentinelList_Make(list, pool);
result->data = data;
return result;
}
template <typename T>
FP_SentinelListLink<T> *FP_SentinelList_MakeBefore(FP_SentinelList<T> *list, FP_SentinelListLink<T> *link, TELY_ChunkPool *pool)
{
@ -119,3 +127,14 @@ void FP_SentinelList_Deinit(FP_SentinelList<T> *list, TELY_ChunkPool *pool)
*list = {};
}
template <typename T>
FP_SentinelListLink<T> *FP_SentinelList_Find(FP_SentinelList<T> const *list, T const &find)
{
FP_SentinelListLink<T> *result = nullptr;
for (FP_SentinelListLink<FP_GameEntityHandle> *link = nullptr;
!result && FP_SentinelList_Iterate<FP_GameEntityHandle>(DQN_CAST(FP_SentinelList<T> *)list, &link); ) {
if (link->data == find)
result = link;
}
return result;
}

Binary file not shown.