diff --git a/src/dusk/rpg/item/itemgive.c b/src/dusk/rpg/item/itemgive.c index b133ab49..c7416dc2 100644 --- a/src/dusk/rpg/item/itemgive.c +++ b/src/dusk/rpg/item/itemgive.c @@ -7,6 +7,7 @@ #include "itemgive.h" #include "rpg/item/backpack.h" +#include "rpg/item/item.h" #include "ui/rpg/textbox/uitextboxmain.h" #include "util/string.h" #include "error/error.h" diff --git a/src/dusk/scene/overworld/sceneoverworld.c b/src/dusk/scene/overworld/sceneoverworld.c index e20b83da..c10276cb 100644 --- a/src/dusk/scene/overworld/sceneoverworld.c +++ b/src/dusk/scene/overworld/sceneoverworld.c @@ -31,15 +31,15 @@ errorret_t sceneOverworldInit(scenedata_t *sceneData) { errorret_t sceneOverworldUpdate(scenedata_t *sceneData) { assertNotNull(sceneData, "Scene data cannot be null"); - - - errorOk(); } errorret_t sceneOverworldRender(scenedata_t *sceneData) { assertNotNull(sceneData, "Scene data cannot be null"); + sceneoverworld_t *overworld = &sceneData->overworld; + sceneOverworldCullUpdate(overworld); + errorChain(displaySetState((displaystate_t){ .flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL })); @@ -65,7 +65,7 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) { )); // Base terrain meshes, drawn with normal depth testing. - errorChain(sceneOverworldDrawChunksBase()); + errorChain(sceneOverworldDrawChunksBase(overworld)); // Entities are drawn with depth testing fully disabled so sloped tiles // (ramps) never clip them; entity-vs-entity overlap falls back to @@ -75,34 +75,10 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) { })); // Entities - { - for(uint8_t i = 0; i < ENTITY_COUNT; i++) { - entity_t *ent = &ENTITIES[i]; - if(ent->type == ENTITY_TYPE_NULL) continue; - - spritebatchsprite_t sprite; - glm_vec3_copy(ent->renderPosition, sprite.min); - glm_vec3_add(sprite.min, (vec3){ 0, -0.05f, 0.05f }, sprite.min);// Stop Fight - glm_vec3_copy(sprite.min, sprite.max); - glm_vec3_add(sprite.max, (vec3){ 1, 1, 0 }, sprite.max); - glm_vec2_copy((vec2){ 0, 0 }, sprite.uvMin); - glm_vec2_copy((vec2){ 1, 1 }, sprite.uvMax); - - color_t color; - switch(ent->direction) { - case ENTITY_DIR_NORTH: color = COLOR_YELLOW; break; - case ENTITY_DIR_EAST: color = COLOR_RED; break; - case ENTITY_DIR_SOUTH: color = COLOR_GREEN; break; - case ENTITY_DIR_WEST: color = COLOR_BLUE; break; - default: color = COLOR_CYAN; break; - } - - shadermaterial_t material = { - .unlit = { .color = color, .texture = NULL } - }; - spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material); - spriteBatchFlush(); - } + for(uint8_t i = 0; i < ENTITY_COUNT; i++) { + entity_t *ent = &ENTITIES[i]; + if(ent->type == ENTITY_TYPE_NULL) continue; + errorChain(sceneOverworldDrawEntity(overworld, ent)); } // Other chunk meshes (trees, buildings, etc), drawn last with normal @@ -111,15 +87,91 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) { errorChain(displaySetState((displaystate_t){ .flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL })); - errorChain(sceneOverworldDrawChunksProps()); + errorChain(sceneOverworldDrawChunksProps(overworld)); errorOk(); } -errorret_t sceneOverworldDrawChunksBase() { +void sceneOverworldCullUpdate(sceneoverworld_t *overworld) { + rpgCameraGetPosition(overworld->cullTarget); + + const float_t worldH = + (float_t)(SCREEN.height / SCREEN.scale3d) / TILE_SIZE_PIXELS; + const float_t worldW = worldH * SCREEN.aspect; + + overworld->cullHalfRangeX = + (worldW * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_SKIN_X; + overworld->cullHalfRangeY = + (worldH * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_SKIN_Y; +} + +bool_t sceneOverworldChunkShouldRender( + const sceneoverworld_t *overworld, + const chunk_t *chunk +) { + worldpos_t worldPos; + chunkPosToWorldPos(&chunk->position, &worldPos); + + const float_t chunkCenterX = (float_t)worldPos.x + (CHUNK_WIDTH * 0.5f); + const float_t chunkCenterY = (float_t)worldPos.y + (CHUNK_HEIGHT * 0.5f); + + if(fabsf(overworld->cullTarget[0] - chunkCenterX) > + overworld->cullHalfRangeX) return false; + if(fabsf(overworld->cullTarget[1] - chunkCenterY) > + overworld->cullHalfRangeY) return false; + + return true; +} + +bool_t sceneOverworldEntityShouldRender( + const sceneoverworld_t *overworld, + const entity_t *ent +) { + if(fabsf(overworld->cullTarget[0] - ent->renderPosition[0]) > + overworld->cullHalfRangeX) return false; + if(fabsf(overworld->cullTarget[1] - ent->renderPosition[1]) > + overworld->cullHalfRangeY) return false; + + return true; +} + +errorret_t sceneOverworldDrawEntity( + const sceneoverworld_t *overworld, + entity_t *ent +) { + if(!sceneOverworldEntityShouldRender(overworld, ent)) errorOk(); + + spritebatchsprite_t sprite; + glm_vec3_copy(ent->renderPosition, sprite.min); + glm_vec3_add(sprite.min, (vec3){ 0, -0.05f, 0.05f }, sprite.min);// Stop Fight + glm_vec3_copy(sprite.min, sprite.max); + glm_vec3_add(sprite.max, (vec3){ 1, 1, 0 }, sprite.max); + glm_vec2_copy((vec2){ 0, 0 }, sprite.uvMin); + glm_vec2_copy((vec2){ 1, 1 }, sprite.uvMax); + + color_t color; + switch(ent->direction) { + case ENTITY_DIR_NORTH: color = COLOR_YELLOW; break; + case ENTITY_DIR_EAST: color = COLOR_RED; break; + case ENTITY_DIR_SOUTH: color = COLOR_GREEN; break; + case ENTITY_DIR_WEST: color = COLOR_BLUE; break; + default: color = COLOR_CYAN; break; + } + + shadermaterial_t material = { + .unlit = { .color = color, .texture = NULL } + }; + errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material)); + errorChain(spriteBatchFlush()); + + errorOk(); +} + +errorret_t sceneOverworldDrawChunksBase(const sceneoverworld_t *overworld) { for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { chunk_t *chunk = MAP.chunkOrder[i]; if(chunk == NULL) continue; + if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue; if(chunk->meshCount == 0) continue; if(chunk->modelEntries[0] == NULL) continue; if(chunk->modelEntries[0]->state != ASSET_ENTRY_STATE_LOADED) continue; @@ -152,10 +204,11 @@ errorret_t sceneOverworldDrawChunksBase() { errorOk(); } -errorret_t sceneOverworldDrawChunksProps() { +errorret_t sceneOverworldDrawChunksProps(const sceneoverworld_t *overworld) { for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { chunk_t *chunk = MAP.chunkOrder[i]; if(chunk == NULL) continue; + if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue; for(uint8_t m = 1; m < chunk->meshCount; m++) { if(chunk->modelEntries[m] == NULL) continue; diff --git a/src/dusk/scene/overworld/sceneoverworld.h b/src/dusk/scene/overworld/sceneoverworld.h index 03e58dbe..048e20d0 100644 --- a/src/dusk/scene/overworld/sceneoverworld.h +++ b/src/dusk/scene/overworld/sceneoverworld.h @@ -7,11 +7,35 @@ #pragma once #include "scene/scenebase.h" +#include "rpg/overworld/chunk.h" +#include "rpg/entity/entity.h" typedef struct { - + // Cached per-frame chunk culling bounds, refreshed by + // sceneOverworldCullUpdate() so sceneOverworldChunkShouldRender() + // does not need to recompute them for every chunk. + vec3 cullTarget; + float_t cullHalfRangeX; + float_t cullHalfRangeY; } sceneoverworld_t; +// Extra world-space margin, in tiles, added on top of the rough +// screen-size skin below - cheap insurance against the approximation +// culling a chunk that is actually still just in view. Y gets its own +// value since the camera's tilt makes the vertical check less exact. +#define SCENE_OVERWORLD_CHUNK_CULL_PADDING_X 8.0f +#define SCENE_OVERWORLD_CHUNK_CULL_PADDING_Y 4.0f + +// The camera is tilted between the ground-depth (Y) and height (Z) +// axes, so a chunk's on-screen vertical extent also depends on its Z +// layers. Rather than projecting every corner, fold that into a +// generous skin margin on the depth check. +#define SCENE_OVERWORLD_CHUNK_CULL_SKIN_X \ + ((CHUNK_WIDTH * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_PADDING_X) +#define SCENE_OVERWORLD_CHUNK_CULL_SKIN_Y \ + ((CHUNK_HEIGHT * 0.5f) + (CHUNK_DEPTH * WORLD_LAYER_HEIGHT) + \ + SCENE_OVERWORLD_CHUNK_CULL_PADDING_Y) + /** * Initialises the overworld scene. * @@ -28,22 +52,85 @@ errorret_t sceneOverworldInit(scenedata_t *sceneData); */ errorret_t sceneOverworldUpdate(scenedata_t *sceneData); +/** + * Refreshes the chunk culling bounds cached on the overworld scene data, + * from the current camera position and screen dimensions. Must be + * called once per frame before sceneOverworldChunkShouldRender(). + * + * @param overworld The overworld scene data to update. + */ +void sceneOverworldCullUpdate(sceneoverworld_t *overworld); + +/** + * Roughly checks whether a chunk falls within the visible screen area, + * based on chunk size and screen dimensions. Cheap approximation, not + * an exact frustum check - includes a generous skin margin to account + * for the camera's angle tilting height/depth onto the screen's + * vertical axis, so it may pass some chunks that are actually just out + * of view but will never wrongly cull one that is visible. + * + * @param overworld The overworld scene data holding the cached culling + * bounds, as refreshed by sceneOverworldCullUpdate(). + * @param chunk The chunk to check. + * @return true if the chunk should be rendered, false otherwise. + */ +bool_t sceneOverworldChunkShouldRender( + const sceneoverworld_t *overworld, + const chunk_t *chunk +); + /** * Draws the base (tile) mesh of every loaded chunk, with normal depth * testing. Must be called before entities are rendered. * + * @param overworld The overworld scene data holding the cached culling + * bounds, as refreshed by sceneOverworldCullUpdate(). * @return An error if drawing failed, or errorOk() on success. */ -errorret_t sceneOverworldDrawChunksBase(); +errorret_t sceneOverworldDrawChunksBase(const sceneoverworld_t *overworld); /** * Draws every loaded chunk's additional meshes (trees, buildings, etc), * with normal depth testing so they correctly occlude entities standing * beneath them. Must be called after entities are rendered. * + * @param overworld The overworld scene data holding the cached culling + * bounds, as refreshed by sceneOverworldCullUpdate(). * @return An error if drawing failed, or errorOk() on success. */ -errorret_t sceneOverworldDrawChunksProps(); +errorret_t sceneOverworldDrawChunksProps(const sceneoverworld_t *overworld); + +/** + * Roughly checks whether an entity falls within the visible screen + * area. Reuses the same cached chunk culling bounds, since an entity + * is just a point within that same space - deliberately cheap, a + * couple of subtractions and comparisons, since it runs per entity + * every frame and it is not worth spending more math than that to + * avoid drawing one that is slightly off-screen. + * + * @param overworld The overworld scene data holding the cached culling + * bounds, as refreshed by sceneOverworldCullUpdate(). + * @param ent The entity to check. + * @return true if the entity should be rendered, false otherwise. + */ +bool_t sceneOverworldEntityShouldRender( + const sceneoverworld_t *overworld, + const entity_t *ent +); + +/** + * Draws a single entity as a sprite, colored by its facing direction. + * Skips drawing (without error) if the entity should not render. + * + * @param overworld The overworld scene data holding the cached culling + * bounds, as refreshed by sceneOverworldCullUpdate(). + * @param ent The entity to draw. + * @return An error if drawing failed, or errorOk() on success. + */ +errorret_t sceneOverworldDrawEntity( + const sceneoverworld_t *overworld, + entity_t *ent +); /** * Renders the overworld scene.