Support large maps up to 8192x8192 tiles
Raise hard map size cap from 4096 to 8192 via MAX_MAP_SIZE constant. Replace fixed 16 KB fgets buffer with dynamic line reader that handles arbitrarily long tile rows. Check calloc returns for tile layer allocation. Add entity distance culling: skip updates for entities beyond 2x screen width from camera, skip rendering beyond 64 px margin. Dead entities bypass culling so cleanup callbacks always run. Player, spacecraft, drone, and moving platforms set ENTITY_ALWAYS_UPDATE to opt out. Replace naive 4-neighbor flood fill with scanline algorithm (O(height) stack instead of O(area)) for safe use on large maps. Raise MAX_ENTITY_SPAWNS from 128 to 512 and MAX_EXIT_ZONES from 8 to 16 to support populated large levels.
This commit is contained in:
@@ -1,7 +1,33 @@
|
||||
#include "engine/entity.h"
|
||||
#include "engine/camera.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* ── Distance culling ──────────────────────────────── */
|
||||
|
||||
/* Margin around the viewport for update culling (in pixels).
|
||||
* Entities within this margin still update even if off-screen,
|
||||
* so they are active before scrolling into view. */
|
||||
#define UPDATE_MARGIN (SCREEN_WIDTH * 2.0f)
|
||||
|
||||
/* Margin for render culling — tighter than update since only
|
||||
* entities actually visible (plus a small buffer) need drawing. */
|
||||
#define RENDER_MARGIN 64.0f
|
||||
|
||||
/* Check whether a body is within a rectangle expanded by margin. */
|
||||
static bool in_camera_range(const Body *body, const Camera *cam, float margin) {
|
||||
float left = cam->pos.x - margin;
|
||||
float top = cam->pos.y - margin;
|
||||
float right = cam->pos.x + cam->viewport.x + margin;
|
||||
float bottom = cam->pos.y + cam->viewport.y + margin;
|
||||
|
||||
float ex = body->pos.x + body->size.x;
|
||||
float ey = body->pos.y + body->size.y;
|
||||
|
||||
return body->pos.x < right && ex > left &&
|
||||
body->pos.y < bottom && ey > top;
|
||||
}
|
||||
|
||||
void entity_manager_init(EntityManager *em) {
|
||||
memset(em, 0, sizeof(EntityManager));
|
||||
}
|
||||
@@ -43,7 +69,8 @@ void entity_destroy(EntityManager *em, Entity *e) {
|
||||
}
|
||||
}
|
||||
|
||||
void entity_update_all(EntityManager *em, float dt, const Tilemap *map) {
|
||||
void entity_update_all(EntityManager *em, float dt, const Tilemap *map,
|
||||
const Camera *cam) {
|
||||
for (int i = 0; i < em->count; i++) {
|
||||
Entity *e = &em->entities[i];
|
||||
if (!e->active) continue;
|
||||
@@ -53,6 +80,14 @@ void entity_update_all(EntityManager *em, float dt, const Tilemap *map) {
|
||||
e->flags |= ENTITY_DEAD;
|
||||
}
|
||||
|
||||
/* Distance culling: skip far-away entities unless they opt out.
|
||||
* Dead entities always get their update callback so type-specific
|
||||
* cleanup (destroy, particles, etc.) can run. */
|
||||
if (cam && !(e->flags & (ENTITY_ALWAYS_UPDATE | ENTITY_DEAD))) {
|
||||
if (!in_camera_range(&e->body, cam, UPDATE_MARGIN))
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Call type-specific update */
|
||||
if (em->update_fn[e->type]) {
|
||||
em->update_fn[e->type](e, dt, map);
|
||||
@@ -65,6 +100,10 @@ void entity_render_all(EntityManager *em, const Camera *cam) {
|
||||
Entity *e = &em->entities[i];
|
||||
if (!e->active) continue;
|
||||
|
||||
/* Render culling: skip entities outside the visible area. */
|
||||
if (cam && !in_camera_range(&e->body, cam, RENDER_MARGIN))
|
||||
continue;
|
||||
|
||||
if (em->render_fn[e->type]) {
|
||||
em->render_fn[e->type](e, cam);
|
||||
}
|
||||
|
||||
@@ -32,9 +32,10 @@ typedef enum EntityType {
|
||||
} EntityType;
|
||||
|
||||
/* Entity flags */
|
||||
#define ENTITY_INVINCIBLE (1 << 0)
|
||||
#define ENTITY_FACING_LEFT (1 << 1)
|
||||
#define ENTITY_DEAD (1 << 2)
|
||||
#define ENTITY_INVINCIBLE (1 << 0)
|
||||
#define ENTITY_FACING_LEFT (1 << 1)
|
||||
#define ENTITY_DEAD (1 << 2)
|
||||
#define ENTITY_ALWAYS_UPDATE (1 << 3) /* never culled by distance */
|
||||
|
||||
typedef struct Entity {
|
||||
EntityType type;
|
||||
@@ -66,7 +67,8 @@ typedef struct EntityManager {
|
||||
void entity_manager_init(EntityManager *em);
|
||||
Entity *entity_spawn(EntityManager *em, EntityType type, Vec2 pos);
|
||||
void entity_destroy(EntityManager *em, Entity *e);
|
||||
void entity_update_all(EntityManager *em, float dt, const Tilemap *map);
|
||||
void entity_update_all(EntityManager *em, float dt, const Tilemap *map,
|
||||
const Camera *cam);
|
||||
void entity_render_all(EntityManager *em, const Camera *cam);
|
||||
void entity_manager_clear(EntityManager *em);
|
||||
|
||||
|
||||
@@ -6,6 +6,33 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* Read a full line from f into a dynamically growing buffer.
|
||||
* *buf and *cap track the heap buffer; the caller must free *buf.
|
||||
* Returns the line length, or -1 on EOF/error. */
|
||||
static int read_line(FILE *f, char **buf, int *cap) {
|
||||
if (!*buf) {
|
||||
*cap = 16384;
|
||||
*buf = malloc(*cap);
|
||||
if (!*buf) return -1;
|
||||
}
|
||||
int len = 0;
|
||||
for (;;) {
|
||||
if (len + 1 >= *cap) {
|
||||
int new_cap = *cap * 2;
|
||||
char *tmp = realloc(*buf, new_cap);
|
||||
if (!tmp) return -1;
|
||||
*buf = tmp;
|
||||
*cap = new_cap;
|
||||
}
|
||||
int ch = fgetc(f);
|
||||
if (ch == EOF) return len > 0 ? len : -1;
|
||||
(*buf)[len++] = (char)ch;
|
||||
if (ch == '\n') break;
|
||||
}
|
||||
(*buf)[len] = '\0';
|
||||
return len;
|
||||
}
|
||||
|
||||
bool tilemap_load(Tilemap *map, const char *path, SDL_Renderer *renderer) {
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) {
|
||||
@@ -15,12 +42,13 @@ bool tilemap_load(Tilemap *map, const char *path, SDL_Renderer *renderer) {
|
||||
|
||||
memset(map, 0, sizeof(Tilemap));
|
||||
|
||||
char line[16384];
|
||||
char tileset_path[256] = {0};
|
||||
int current_layer = -1; /* 0=collision, 1=bg, 2=fg */
|
||||
int row = 0;
|
||||
char *line = NULL;
|
||||
int line_cap = 0;
|
||||
char tileset_path[256] = {0};
|
||||
int current_layer = -1; /* 0=collision, 1=bg, 2=fg */
|
||||
int row = 0;
|
||||
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
while (read_line(f, &line, &line_cap) >= 0) {
|
||||
/* Skip comments and empty lines */
|
||||
if (line[0] == '#' || line[0] == '\n' || line[0] == '\r') continue;
|
||||
|
||||
@@ -30,8 +58,9 @@ bool tilemap_load(Tilemap *map, const char *path, SDL_Renderer *renderer) {
|
||||
} else if (strncmp(line, "SIZE ", 5) == 0) {
|
||||
sscanf(line + 5, "%d %d", &map->width, &map->height);
|
||||
if (map->width <= 0 || map->height <= 0 ||
|
||||
map->width > 4096 || map->height > 4096) {
|
||||
map->width > MAX_MAP_SIZE || map->height > MAX_MAP_SIZE) {
|
||||
fprintf(stderr, "Invalid map size: %dx%d\n", map->width, map->height);
|
||||
free(line);
|
||||
fclose(f);
|
||||
return false;
|
||||
}
|
||||
@@ -39,6 +68,17 @@ bool tilemap_load(Tilemap *map, const char *path, SDL_Renderer *renderer) {
|
||||
map->collision_layer = calloc(total, sizeof(uint16_t));
|
||||
map->bg_layer = calloc(total, sizeof(uint16_t));
|
||||
map->fg_layer = calloc(total, sizeof(uint16_t));
|
||||
if (!map->collision_layer || !map->bg_layer || !map->fg_layer) {
|
||||
fprintf(stderr, "Failed to allocate tile layers (%dx%d)\n",
|
||||
map->width, map->height);
|
||||
free(map->collision_layer);
|
||||
free(map->bg_layer);
|
||||
free(map->fg_layer);
|
||||
memset(map, 0, sizeof(Tilemap));
|
||||
free(line);
|
||||
fclose(f);
|
||||
return false;
|
||||
}
|
||||
} else if (strncmp(line, "SPAWN ", 6) == 0) {
|
||||
float sx, sy;
|
||||
sscanf(line + 6, "%f %f", &sx, &sy);
|
||||
@@ -132,6 +172,7 @@ bool tilemap_load(Tilemap *map, const char *path, SDL_Renderer *renderer) {
|
||||
}
|
||||
}
|
||||
}
|
||||
free(line);
|
||||
fclose(f);
|
||||
|
||||
/* Load tileset texture */
|
||||
|
||||
Reference in New Issue
Block a user