performance improvements with pathfinding

* Less queuing in Dijkstra by using distance[] to indicate the lowest
  queued distance (dir[] still doubles down as "seen" array). Drops
  iterations from ~350 to ~150, 5.5 ms -> 2.0 ms
* Less raycast attempts in the shortcut method, barely noticeable but
  avoids massive spikes from 7 to 35/75 ms in Lab with 40 entities (!!)
* General optimizations with -O3 instead of -Os (0.5-1.0 ms gained per
  frame for ~6 kB increase in size)
* Compute pathfinding every 4th frame for each entity, instead of every
  frame

Now basically a fairly 30 FPS, and that's 30 ms rendering + 3 ms
simulation.
This commit is contained in:
Lephenixnoir 2022-02-08 22:48:08 +01:00
parent 7bd5163dd6
commit 8d3a0994c5
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
13 changed files with 125 additions and 103 deletions

View File

@ -98,7 +98,7 @@ fxconv_declare_assets(${ASSETS} ${ASSETS} WITH_METADATA)
fxconv_declare_converters(assets-cg/converters.py)
add_executable(addin ${SOURCES} ${ASSETS})
target_compile_options(addin PRIVATE -Wall -Wextra -Os)
target_compile_options(addin PRIVATE -Wall -Wextra -O3)
target_include_directories(addin PRIVATE src)
target_link_libraries(addin LibProf::LibProf Gint::Gint)
target_link_options(addin PRIVATE -Wl,-Map=map)

View File

@ -162,8 +162,8 @@ static bool attack_apply(game_t *game, aoe_t *aoe, entity_t *target)
/* No friendly fire */
bool origin_is_monster = !aoe->origin ? true :
(origin_f && origin_f->enemy_data != NULL);
bool target_is_monster = (target_f->enemy_data != NULL);
(origin_f && origin_f->enemy != NULL);
bool target_is_monster = (target_f->enemy != NULL);
if(origin_is_monster == target_is_monster || target_f->HP == 0)
return false;
@ -174,7 +174,7 @@ static bool attack_apply(game_t *game, aoe_t *aoe, entity_t *target)
/* Knockback */
fixed_t r = (rand() & (fix(0.125)-1)) + fix(0.375);
/* Half knockback against players */
if(target_f->enemy_data == NULL) r /= 2;
if(!target_f->enemy) r /= 2;
vec2 dir = { 0, 0 };
int damage = 0;
@ -220,7 +220,7 @@ static bool attack_apply(game_t *game, aoe_t *aoe, entity_t *target)
/* Spawn damage particle */
entity_t *particle = particle_make_damage(target, damage,
(target_f->enemy_data == NULL) ? C_RED : C_WHITE);
target_f->enemy ? C_WHITE : C_RED);
game_add_entity(game, particle);
/* Quick screenshake for entities hit by a bullet */

View File

@ -68,6 +68,9 @@ void entity_destroy(entity_t *e)
if(e->comps & ENTITY_COMP_aoe) {
aoe_destroy(e);
}
if(e->comps & ENTITY_COMP_fighter) {
fighter_destroy(e);
}
/* Since there's only one allocation for all components, this is easy */
free(e);

View File

@ -38,11 +38,11 @@ int fighter_damage(entity_t *e, int base_damage)
if(f->HP < damage) f->HP = 0;
else f->HP -= damage;
if(f->enemy_data != NULL) {
if(f->enemy) {
if(f->HP == 0)
visible_set_anim(e, f->enemy_data->anim_death, 4);
visible_set_anim(e, f->enemy->id->anim_death, 4);
else
visible_set_anim(e, f->enemy_data->anim_hit, 3);
visible_set_anim(e, f->enemy->id->anim_hit, 3);
}
else {
visible_set_anim(e, &anims_player_Hit, 3);
@ -62,3 +62,9 @@ void fighter_invulnerability(entity_t *e, fixed_t duration)
fighter_t *f = getcomp(e, fighter);
f->invulnerability_delay = max(f->invulnerability_delay, duration);
}
void fighter_destroy(entity_t *e)
{
fighter_t *f = getcomp(e, fighter);
free(f->enemy);
}

View File

@ -29,7 +29,9 @@ typedef struct
/* Whether attack follows movement */
uint8_t attack_follows_movement;
/* Pointer to enemy data (NULL if player) */
struct enemy const *enemy_data;
struct enemy_data *enemy;
/* Pointer to player data (NULL if enemy) */
struct player_data *player;
/* Combat statistics */
uint16_t HP, ATK, MAG, DEF, HP_max;
@ -65,3 +67,6 @@ void fighter_stun(entity_t *e, fixed_t duration);
/* Make the fighter invulnerable for a specified duration. */
void fighter_invulnerability(entity_t *e, fixed_t duration);
/* Free dynamic data (enemy info). */
void fighter_destroy(entity_t *e);

View File

@ -144,10 +144,13 @@ entity_t *enemy_make(int enemy_id)
fighter_t *f = getcomp(e, fighter);
memset(f, 0, sizeof *f);
f->enemy_data = data;
fighter_set_stats(f, &data->stats, data->level, fix(1.0));
f->HP = f->HP_max;
f->combo_length = 1;
f->enemy = malloc(sizeof *f->enemy);
f->enemy->id = data;
f->enemy->pathfind_dir = (vec2){ 0, 0 };
f->enemy->pathfind_cycles = 0;
/* Specify skills */
if(enemy_id == ENEMY_GUNSLINGER_8) {
@ -162,22 +165,34 @@ entity_t *enemy_make(int enemy_id)
static void move_towards_player(game_t *g, entity_t *e, fixed_t dt)
{
physical_t *p = getcomp(e, physical);
fighter_t *f = getcomp(e, fighter);
vec2 pos = physical_pos(e);
vec2 direction;
vec2 direction = { 0, 0 };
if(dist2(pos, physical_pos(g->player)) <= fix(0.25)) /* 0.5^2 */
return;
pfg_path_t path = pfg_inwards(&g->paths_to_player, vec_f2i(pos));
if(path.points) {
direction = pfc_shortcut_one(&path, pos, physical_pos(g->player),
p->hitbox);
pfg_path_free(&path);
if(f->enemy && f->enemy->pathfind_cycles > 0) {
direction = f->enemy->pathfind_dir;
f->enemy->pathfind_cycles--;
}
else {
pfg_path_t path = pfg_inwards(&g->paths_to_player, vec_f2i(pos));
if(path.points) {
direction = pfc_shortcut_one(&path, pos, physical_pos(g->player),
p->hitbox);
pfg_path_free(&path);
}
if(direction.x && direction.y) {
direction.x -= pos.x;
direction.y -= pos.y;
if(direction.x && direction.y) {
direction.x -= pos.x;
direction.y -= pos.y;
}
if(f->enemy) {
f->enemy->pathfind_dir = direction;
f->enemy->pathfind_cycles = 4;
}
}
mechanical_move(e, direction, dt, g->map);
@ -214,7 +229,7 @@ static bool contact_attack(game_t *g, entity_t *e)
f->current_attack = aoe;
f->attack_follows_movement = true;
visible_set_anim(e, f->enemy_data->anim_attack, 2);
visible_set_anim(e, f->enemy->id->anim_attack, 2);
return true;
}
@ -235,16 +250,16 @@ void enemy_ai(game_t *g, entity_t *e, fixed_t dt)
{
fighter_t *f = getcomp(e, fighter);
if(f->enemy_data == &slime_1 || f->enemy_data == &bat_2) {
if(f->enemy->id == &slime_1 || f->enemy->id == &bat_2) {
if(move_within_range_of_player(g, e, fix(1.0), dt)) {
contact_attack(g, e);
}
}
else if(f->enemy_data == &fire_slime_4) {
else if(f->enemy->id == &fire_slime_4) {
}
else if(f->enemy_data == &gunslinger_8) {
else if(f->enemy->id == &gunslinger_8) {
rect hitbox = { fix(-1/16), fix(1/16), fix(-1/16), fix(1/16) };
bool clear = raycast_clear_hitbox(g->map, physical_pos(e),
physical_pos(g->player), hitbox);

View File

@ -12,7 +12,7 @@
#include "anim.h"
/* enemy_t: Static enemy information */
typedef struct enemy
typedef struct
{
/* Enemy name (shown in wave information) */
char const *name;
@ -44,6 +44,16 @@ enum {
ENEMY_GUNSLINGER_8 = 4,
};
/* Dynamic enemy information. */
typedef struct enemy_data {
enemy_t const *id;
/* Current pathfinding direction */
vec2 pathfind_dir;
/* Number of frames left until next pathfinding cycle */
uint8_t pathfind_cycles;
} enemy_data_t;
/* Get enemy data by ID. */
enemy_t const *enemy_data(int enemy_id);

View File

@ -180,9 +180,9 @@ void game_remove_dead_entities(game_t *g)
visible_t *v = getcomp(e, visible);
bool anim_finished = !v || (v->anim.frame == NULL);
if(f && f->HP == 0 && f->enemy_data != NULL && anim_finished) {
if(f && f->HP == 0 && f->enemy != NULL && anim_finished) {
/* Give XP points to player */
bool lvup = player_add_xp(g->player_data, f->enemy_data->xp);
bool lvup = player_add_xp(g->player_data, f->enemy->id->xp);
if(lvup) {
g->hud_xp_anim.frame = anims_hud_xp_Explode.start[0];

View File

@ -132,7 +132,9 @@ int main(void)
player_f->combo_length = 2;
player_f->combo_next = 0;
player_f->combo_delay = fix(0);
player_f->enemy_data = NULL;
player_f->enemy = NULL;
// TODO: Set link to player data in entity
// player_f->player = &player_data;
player_f->skills[1] = SKILL_DASH;
player_f->skills[2] = AOE_SHOCK;
player_f->skills[3] = AOE_JUDGEMENT;
@ -299,22 +301,6 @@ int main(void)
ivec2 j = camera_map2screen(c, p);
ivec2 k = camera_map2screen(c, q);
dline(j.x, j.y, k.x, k.y, clear ? C_GREEN : C_RED);
extern int rch_c1, rch_c2;
extern vec2 rch_p1[64], rch_p2[64];
for(int k = 0; k < rch_c1 && k < 64; k++) {
ivec2 i = camera_map2screen(c, rch_p1[k]);
dline(i.x-2, i.y, i.x+2, i.y, C_RGB(0, 31, 31));
dline(i.x, i.y-2, i.x, i.y+2, C_RGB(0, 31, 31));
}
for(int k = 0; k < rch_c2 && k < 64; k++) {
ivec2 i = camera_map2screen(c, rch_p2[k]);
dline(i.x-2, i.y, i.x+2, i.y, C_RGB(0, 31, 31));
dline(i.x, i.y-2, i.x, i.y+2, C_RGB(0, 31, 31));
}
}
if(debug.show_bfs_field && game.paths_to_player.direction) {
@ -322,7 +308,9 @@ int main(void)
}
if(debug.show_perf) {
dprint(1, 15, C_WHITE, "Render: %.3D ms", time_render);
extern uint32_t time_render_map, time_render_hud;
dprint(1, 15, C_WHITE, "Render: map %.3D, hud %.3D, total %.3D ms",
time_render_map, time_render_hud, time_render);
dprint(1, 29, C_WHITE, "Simul: %.3D ms", time_simul);
}
@ -484,15 +472,15 @@ int main(void)
entity_t *e = game.entities[i];
fighter_t *f = getcomp(e, fighter);
mechanical_t *m = getcomp(e, mechanical);
if(!f || !m || f->enemy_data == NULL || f->HP == 0)
if(!f || !m || !f->enemy || f->HP == 0)
continue;
enemy_ai(&game, e, dt);
if(mechanical_moving(e))
visible_set_anim(e, f->enemy_data->anim_walking, 1);
visible_set_anim(e, f->enemy->id->anim_walking, 1);
else
visible_set_anim(e, f->enemy_data->anim_idle, 1);
visible_set_anim(e, f->enemy->id->anim_idle, 1);
}
/* Player attack */
@ -557,8 +545,8 @@ int main(void)
entity_t *e = game.entities[i];
fighter_t *f = getcomp(e, fighter);
visible_t *v = getcomp(e, visible);
if(f && f->enemy_data != NULL && v && !v->anim.frame) {
visible_set_anim(e, f->enemy_data->anim_idle, 1);
if(f && f->enemy && v && !v->anim.frame) {
visible_set_anim(e, f->enemy->id->anim_idle, 1);
}
}

View File

@ -68,10 +68,9 @@ pfg_all2one_t pfg_dijkstra(map_t const *map, ivec2 center, uint8_t *occupation)
/* Pop a node from the queue; ignore it if we've visited it before */
pqueue_pop(&queue, &point);
int point_i = idx(point.x, point.y);
if(paths.distance[point_i] != -1)
if(paths.direction[point_i] != -1)
continue;
paths.distance[point_i] = point.cost;
paths.direction[point_i] = point.dir;
for(int dir = 0; dir < 4; dir++) {
@ -88,6 +87,11 @@ pfg_all2one_t pfg_dijkstra(map_t const *map, ivec2 center, uint8_t *occupation)
is [dir] due to how [dx_array] and [dy_array] are laid out */
struct point next = { .x=point.x+dx, .y=point.y+dy };
next.cost = paths.distance[point_i] + 1 + 2*occ;
if(paths.distance[next_i]>=0 && paths.distance[next_i]<=next.cost)
continue;
paths.distance[next_i] = next.cost;
next.dir = dir;
pqueue_add(&queue, &next);
}
@ -145,9 +149,6 @@ pfg_path_t pfg_outwards(pfg_all2one_t const *field, ivec2 p)
// Raycasting tools
//---
int raycast_clear_points = 0;
vec2 *raycast_clear_p;
bool raycast_clear(map_t const *map, vec2 start, vec2 end)
{
vec2 u = { end.x - start.x, end.y - start.y };
@ -156,30 +157,22 @@ bool raycast_clear(map_t const *map, vec2 start, vec2 end)
fixed_t inv_ux = u.x ? fdiv(fix(1), u.x) : 0;
fixed_t inv_uy = u.y ? fdiv(fix(1), u.y) : 0;
/* Current point is [start + t*u]; when t = 1, we're reached [end] */
/* Current point is [start + t*u]; when t = 1, we've reached [end] */
fixed_t t = fix(0);
raycast_clear_points = 0;
while(t < fix(1)) {
fixed_t x = start.x + fmul(t, u.x);
fixed_t y = start.y + fmul(t, u.y);
/* Re-check current cell to avoid diagonal clips, where we change tiles
diagonally in a single stpe (which happens quite often when snapping
to points with integer or half-integer coordinates) as things align
perfectly */
diagonally in a single step (which happens quite often when snapping
to points with integer or half-integer coordinates as things align
perfectly) */
int current_x = ffloor(x-(u.x < 0));
int current_y = ffloor(y-(u.y < 0));
tile_t *tile = map_tile(map, current_x, current_y);
if(tile && tile->solid) return false;
if(raycast_clear_points < 64) {
raycast_clear_p[raycast_clear_points].x = x;
raycast_clear_p[raycast_clear_points].y = y;
}
raycast_clear_points++;
/* Distance to the next horizontal, and vertical line */
fixed_t dist_y = (u.y >= 0) ? fix(1) - fdec(y) : -(fdec(y-1) + 1);
fixed_t dist_x = (u.x >= 0) ? fix(1) - fdec(x) : -(fdec(x-1) + 1);
@ -188,36 +181,28 @@ bool raycast_clear(map_t const *map, vec2 start, vec2 end)
fixed_t dty = fmul(dist_y, inv_uy);
fixed_t dtx = fmul(dist_x, inv_ux);
int next_x = current_x;
int next_y = current_y;
/* Move to the next point */
if(!u.x || (u.y && dty <= dtx)) {
/* Make sure we don't get stuck, at all costs */
t += dty + (dty == 0);
if(t > fix(1)) break;
int next_x = ffloor(x-(u.x < 0));
int next_y = ffloor(y-(u.y < 0)) + (u.y >= 0 ? 1 : -1);
tile_t *tile = map_tile(map, next_x, next_y);
if(tile && tile->solid) return false;
next_y += (u.y >= 0 ? 1 : -1);
}
else {
t += dtx + (dtx == 0);
if(t > fix(1)) break;
int next_x = ffloor(x-(u.x < 0)) + (u.x >= 0 ? 1 : -1);
int next_y = ffloor(y-(u.y < 0));
tile_t *tile = map_tile(map, next_x, next_y);
if(tile && tile->solid) return false;
next_x += (u.x >= 0 ? 1 : -1);
}
if(t > fix(1)) break;
tile = map_tile(map, next_x, next_y);
if(tile && tile->solid) return false;
}
return true;
}
int rch_c1=0, rch_c2=0;
vec2 rch_p1[64], rch_p2[64];
bool raycast_clear_hitbox(map_t const *map, vec2 start, vec2 end,
rect hitbox)
{
@ -241,15 +226,7 @@ bool raycast_clear_hitbox(map_t const *map, vec2 start, vec2 end,
vec2 e1 = { end.x + p1.x, end.y + p1.y };
vec2 e2 = { end.x + p2.x, end.y + p2.y };
raycast_clear_p = rch_p1;
bool b1 = raycast_clear(map, s1, e1);
rch_c1 = raycast_clear_points;
raycast_clear_p = rch_p2;
bool b2 = raycast_clear(map, s2, e2);
rch_c2 = raycast_clear_points;
return b1 && b2;
return raycast_clear(map, s1, e1) && raycast_clear(map, s2, e2);
}
//---
@ -308,8 +285,8 @@ vec2 pfc_shortcut_one(pfg_path_t const *grid, vec2 start, vec2 end,
vec2 target = start;
if(!grid->points) return target;
/* Find the furthest point that can be reached */
for(int next = grid->length + 1; next >= 0; next--) {
/* Find a point that can be reached directly by bisecting the path */
for(int next = grid->length + 1; next >= 0; next /= 2) {
vec2 p2 = (next == grid->length + 1) ? end :
vec_i2f_center(grid->points[next]);
@ -317,6 +294,8 @@ vec2 pfc_shortcut_one(pfg_path_t const *grid, vec2 start, vec2 end,
target = p2;
break;
}
if(next == 0)
break;
}
return target;

View File

@ -13,6 +13,7 @@
#include <gint/display.h>
#include <gint/defs/util.h>
#include <stdio.h>
#include <libprof.h>
//---
// Camera management
@ -348,6 +349,9 @@ void render_window(int x, int y, int w, int h)
dsubimage(x+w-20, y+h-11, &img_hud_window, 40, 11, 20, 11, DIMAGE_NONE);
}
uint32_t time_render_map = 0;
uint32_t time_render_hud = 0;
void render_game(game_t const *g, bool show_hitboxes)
{
camera_t const *camera = &g->camera;
@ -360,6 +364,9 @@ void render_game(game_t const *g, bool show_hitboxes)
ss_y = rand() % amp - (amp/2);
}
prof_t ctx = prof_make();
prof_enter(ctx);
/* Render map floor and floor entities */
render_map_layer(g->map, camera, ss_x, ss_y, HORIZONTAL);
render_entities(g, camera, floor_depth_measure, ss_x, ss_y, show_hitboxes);
@ -373,9 +380,15 @@ void render_game(game_t const *g, bool show_hitboxes)
render_map_layer(g->map, camera, ss_x, ss_y, CEILING);
render_entities(g, camera, ceiling_depth_measure, ss_x,ss_y,show_hitboxes);
prof_leave(ctx);
time_render_map = prof_time(ctx);
extern font_t font_rogue;
font_t const *old_font = dfont(&font_rogue);
ctx = prof_make();
prof_enter(ctx);
/* Render wave information */
render_wave_info(g);
@ -446,6 +459,9 @@ void render_game(game_t const *g, bool show_hitboxes)
}
}
prof_leave(ctx);
time_render_hud = prof_time(ctx);
dfont(old_font);
}

View File

@ -35,19 +35,19 @@ void skill_use(game_t *game, entity_t *e, int slot, vec2 dir)
entity_t *aoe = aoe_make_attack(AOE_PROJECTILE, e, dir);
game_add_entity(game, aoe);
if(!f->enemy_data)
if(!f->enemy)
visible_set_anim(e, &anims_player_Attack, 2);
else
visible_set_anim(e, f->enemy_data->anim_attack, 2);
visible_set_anim(e, f->enemy->id->anim_attack, 2);
}
else if(skill == AOE_SHOCK) {
entity_t *aoe = aoe_make_attack(AOE_SHOCK, e, dir);
game_add_entity(game, aoe);
if(!f->enemy_data)
if(!f->enemy)
visible_set_anim(e, &anims_player_Attack, 2);
else
visible_set_anim(e, f->enemy_data->anim_attack, 2);
visible_set_anim(e, f->enemy->id->anim_attack, 2);
f->current_attack = aoe;
f->attack_follows_movement = true;
@ -58,10 +58,10 @@ void skill_use(game_t *game, entity_t *e, int slot, vec2 dir)
entity_t *aoe = aoe_make_attack(AOE_JUDGEMENT, e, dir);
game_add_entity(game, aoe);
if(!f->enemy_data)
if(!f->enemy)
visible_set_anim(e, &anims_player_Attack, 2);
else
visible_set_anim(e, f->enemy_data->anim_attack, 2);
visible_set_anim(e, f->enemy->id->anim_attack, 2);
game_shake(game, 3, fix(1.3));
}
@ -69,9 +69,9 @@ void skill_use(game_t *game, entity_t *e, int slot, vec2 dir)
entity_t *aoe = aoe_make_attack(AOE_BULLET, e, dir);
game_add_entity(game, aoe);
if(!f->enemy_data)
if(!f->enemy)
visible_set_anim(e, &anims_player_Attack, 2);
else
visible_set_anim(e, f->enemy_data->anim_attack, 2);
visible_set_anim(e, f->enemy->id->anim_attack, 2);
}
}

View File

@ -27,7 +27,7 @@ pqueue_t pqueue_alloc(size_t size, size_t elsize,
.alloc_size = size,
.size = 0,
.elsize = elsize,
.array = malloc(size * elsize),
.array = malloc((size+1) * elsize),
.compare = compare,
};
}