From 7a3705d3420d185be614f5712b96ff8c9eeaf689 Mon Sep 17 00:00:00 2001 From: cowley Date: Sat, 20 Dec 2025 17:12:58 -0500 Subject: [PATCH] Add configuration option for ghost reset distance. Improve documentation. --- AGENTS.md | 19 +++++++++++++++++++ README.md | 19 +++++++++++++++++++ collisions.cpp | 2 ++ collisions.h | 1 + config.cpp | 7 ++++++- config.h | 4 +++- fragmentshader.glsl | 1 + ghost.cpp | 7 +++---- ghost.h | 6 ++++-- ghostfragshader.glsl | 1 + ghostland.cpp | 17 ++++++++++++----- ghostland.json | 3 ++- ghostshader.glsl | 1 + mazeparser.py | 19 ++++++++++++++++++- player.cpp | 8 +++++++- player.h | 13 ++++++++----- shader.cpp | 3 +++ shader.h | 1 + text.cpp | 22 +++++++++++++++------- text.h | 1 + textfragshader.glsl | 1 + textshader.glsl | 1 + trailfragshader.glsl | 1 + trailshader.glsl | 1 + vertexshader.glsl | 1 + 25 files changed, 132 insertions(+), 28 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..21208a2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,19 @@ +# Repository Guidelines + +## Project Structure & Module Organization +Core gameplay lives in root-level C++ translation units: `ghostland.cpp` orchestrates the loop, `player.cpp`, `ghost.cpp`, `collisions.cpp`, `shader.cpp`, `text.cpp`, and `config.cpp` own their respective systems. Runtime assets stay alongside code: GLSL shaders (`*shader.glsl`), UI textures (PNG in the repo root), and bitmap fonts under `fonts/`. Level data is serialized in `maze.txt`, typically regenerated via `mazeparser.py` from `maze.png`. Adjustable settings belong in `ghostland.json`; document experimental JSON entries before relying on them in code to avoid null lookups. + +## Build, Test, and Development Commands +- `make` – builds all objects with C++17 + O3 and links the `ghostland` binary with GLFW, GLAD, stb_image, and Freetype. +- `./ghostland` – runs the game from the repo root; pass config overrides via `ghostland.json`. +- `make clean` – removes all `.o` files and the executable; run before release builds to ensure a full relink. +- `python3 mazeparser.py` – regenerates `maze.txt` from the source maze image; adjust the image path in the script if you keep art assets elsewhere. + +## Coding Style & Naming Conventions +Use 4-space indentation, braces on the same line as control statements, and guard new modules with include guards. Keep standard-library/GLM includes before project headers as in `ghostland.cpp`. Classes use PascalCase (`Player`), local variables use `snake_case`, compile-time constants stay uppercase. Prefer STL containers over manual arrays unless performance-critical, and keep shader uniform names centralized near the top of `ghostland.cpp`. + +## Testing Guidelines +There is no automated framework yet, so rely on focused manual passes. Before opening a PR, build fresh (`make clean && make`), run `./ghostland`, and verify spawn position, collision bounds, ghost ordering, and text rendering. When touching geometry, regenerate `maze.txt` and inspect extremes logged at the bottom of the file. Mention any GPU or driver quirks encountered so others can reproduce. + +## Commit & Pull Request Guidelines +Existing history favors short, descriptive subjects ("Player's position is reset when close to ghosts"). Follow that tone, keep subjects under 72 characters, and add body lines for rationale or performance numbers. Each PR should include: summary of gameplay-visible changes, configs or assets touched, reproduction steps or screenshots when visuals change, and confirmation that `make` succeeds on a clean tree. Link related issues and call out any follow-up tasks explicitly. diff --git a/README.md b/README.md index b1ba1a5..76c8042 100644 --- a/README.md +++ b/README.md @@ -3,4 +3,23 @@ This is a continuation of my [goland-game](https://github.com/five-hundred-eleve While the previous project was implemented in go, for this project I was not able to get the go bindings for OpenGL working, and so I went with C++. +[Repository Guidelines](AGENTS.md) outlines code structure, build steps, and contribution conventions. + +## Configuration (`ghostland.json`) +`ghostland.json` is parsed at startup with a minimal line-by-line reader, so keep it a flat list of key-value pairs (`"key": value`). Omitted keys fall back to in-code defaults. + +| Key | Type | Default | Description | +| --- | --- | --- | --- | +| `camera_speed` | float | `20.0` | Scales how fast the free camera pans when you move the mouse to the screen edge or hold movement keys. Raise for faster fly-through, lower for precise adjustments. | +| `ghost_reset_distance` | float | `10.0` | Minimum distance (units) between the player and the nearest ghost before the player is snapped back to the maze start. Increase for a safer buffer, decrease for higher tension. | + +Example: + +```json +{ + "camera_speed": 30.0, + "ghost_reset_distance": 8.5 +} +``` + ![gameplay capture](https://stromsy.com/content/ghostland_6.gif) diff --git a/collisions.cpp b/collisions.cpp index 819c2e4..dee5bf6 100644 --- a/collisions.cpp +++ b/collisions.cpp @@ -1,5 +1,6 @@ #include +// Minimal triangle intersection helpers; use local versions to avoid glibc dependencies. float abs(float x) { if (x < 0) { return -x; @@ -14,6 +15,7 @@ int signbit(float x) { return 0; } +// Möller–Trumbore style segment-triangle intersection routine. int get_intersection(glm::vec3 vec_start, glm::vec3 vec_stop, glm::vec3 P1, glm::vec3 P2, glm::vec3 P3) { glm::vec3 lba = vec_start - vec_stop; diff --git a/collisions.h b/collisions.h index e78e65e..4cfb4d3 100644 --- a/collisions.h +++ b/collisions.h @@ -3,6 +3,7 @@ #include +// Returns 1 if the segment (vec_start -> vec_stop) intersects triangle (P1,P2,P3). int get_intersection(glm::vec3 vec_start, glm::vec3 vec_stop, glm::vec3 P1, glm::vec3 P2, glm::vec3 P3); #endif diff --git a/config.cpp b/config.cpp index c99588e..adbc576 100644 --- a/config.cpp +++ b/config.cpp @@ -6,6 +6,7 @@ std::map Config::values; bool Config::loadFromFile(const std::string& filename) { + // Extremely small JSON subset reader: expects `"key": value` pairs per line. std::ifstream file(filename); if (!file.is_open()) { std::cout << "Warning: Could not open config file: " << filename << std::endl; @@ -26,6 +27,7 @@ bool Config::loadFromFile(const std::string& filename) { } float Config::getFloat(const std::string& key, float defaultValue) { + // Values are stored as strings; convert on demand and fall back if invalid. auto it = values.find(key); if (it != values.end()) { try { @@ -38,6 +40,7 @@ float Config::getFloat(const std::string& key, float defaultValue) { } int Config::getInt(const std::string& key, int defaultValue) { + // Values are stored as strings; convert on demand and fall back if invalid. auto it = values.find(key); if (it != values.end()) { try { @@ -50,6 +53,7 @@ int Config::getInt(const std::string& key, int defaultValue) { } std::string Config::getString(const std::string& key, const std::string& defaultValue) { + // Direct passthrough for callers that want raw string values. auto it = values.find(key); if (it != values.end()) { return it->second; @@ -58,6 +62,7 @@ std::string Config::getString(const std::string& key, const std::string& default } std::string Config::trim(const std::string& str) { + // Helper for stripping whitespace around keys/values. size_t start = str.find_first_not_of(" \t\r\n"); if (start == std::string::npos) return ""; size_t end = str.find_last_not_of(" \t\r\n"); @@ -100,4 +105,4 @@ bool Config::parseJsonValue(const std::string& line, std::string& key, std::stri } return !key.empty(); -} \ No newline at end of file +} diff --git a/config.h b/config.h index 7569c39..4559c3e 100644 --- a/config.h +++ b/config.h @@ -4,8 +4,10 @@ #include #include +// Config is a minimal line-oriented JSON reader used to expose tuning knobs at runtime. class Config { public: + // Reads the file into a flat string map; best-effort, tolerates missing files. static bool loadFromFile(const std::string& filename); static float getFloat(const std::string& key, float defaultValue); static int getInt(const std::string& key, int defaultValue); @@ -17,4 +19,4 @@ private: static bool parseJsonValue(const std::string& line, std::string& key, std::string& value); }; -#endif \ No newline at end of file +#endif diff --git a/fragmentshader.glsl b/fragmentshader.glsl index f1af52c..14acd00 100644 --- a/fragmentshader.glsl +++ b/fragmentshader.glsl @@ -1,4 +1,5 @@ #version 460 core +// Phong lighting fragment shader driven by a single spotlight described in ghostland.cpp. out vec4 FragColor; struct Material { diff --git a/ghost.cpp b/ghost.cpp index 52fb841..180783c 100644 --- a/ghost.cpp +++ b/ghost.cpp @@ -33,7 +33,7 @@ Ghost::Ghost(float xmin, float xmax, float zmin, float zmax) { void Ghost::apply_movement(float curr_time, float timed) { - // vertical movement + // Bob vertically to make ghosts feel more alive. pos.y = 2.5 + sin(curr_time + y_offset); moved.x = -sin(yawr) + cos(yawr); @@ -77,7 +77,7 @@ glm::mat4 Ghost::get_model(glm::vec3 &camera_pos) { // apply direction // the cross product helps us determine which direction it's going relative to player glm::vec3 ghost_to_player = pos - camera_pos; - glm::vec3 crossed = glm::cross(ghost_to_player, moved); + glm::vec3 crossed = glm::cross(ghost_to_player, moved); // determines whether the sprite should face left/right //float dist2 = ghost_to_player.x * ghost_to_player.x + ghost_to_player.z * ghost_to_player.z; float theta = atan2f(ghost_to_player.x, ghost_to_player.z); if (crossed.y < 0.0) { @@ -113,9 +113,8 @@ glm::vec3 Ghost::get_pos() const { } void Ghost::reverse_direction_from_position(glm::vec3 target_pos) { - // Calculate direction from target_pos to ghost position + // Push the ghost to flee from target_pos (usually the starting area). glm::vec3 direction = pos - target_pos; - // Set yawr to point directly away from target_pos yawr = atan2(direction.x, direction.z); } diff --git a/ghost.h b/ghost.h index 309a344..90ffcf3 100644 --- a/ghost.h +++ b/ghost.h @@ -3,6 +3,8 @@ #include +// Ghost encapsulates AI state for a single floating enemy constrained to an +// axis-aligned bounding box inside the maze. class Ghost { public: Ghost(float xmin, float xmax, float zmin, float zmax); @@ -17,14 +19,14 @@ class Ghost { bool first_frame; glm::vec3 moved; glm::vec3 pos; - float yawr; + float yawr; // facing direction used while wandering float xmin; float xmax; float zmin; float zmax; float y_offset; float prev_move_time; - float direction_persist; + float direction_persist; // blends rotation over time so ghosts don't jitter }; float rand_float(float rmin, float rmax); diff --git a/ghostfragshader.glsl b/ghostfragshader.glsl index e8985a3..5ad67c2 100644 --- a/ghostfragshader.glsl +++ b/ghostfragshader.glsl @@ -1,4 +1,5 @@ #version 460 core +// Ghost sprite fragment shader: samples alpha texture and fades opacity to 30%. out vec4 FragColor; diff --git a/ghostland.cpp b/ghostland.cpp index 27ae78a..55732fe 100644 --- a/ghostland.cpp +++ b/ghostland.cpp @@ -22,6 +22,7 @@ #include "text.h" #include "config.h" +// Forward declarations for GLFW callbacks configured at start-up. void framebuffer_size_callback(GLFWwindow* window, int width, int height); void mouse_callback(GLFWwindow* window, double xpos, double ypos); void scroll_callback(GLFWwindow* window, double xoffset, double yoffset); @@ -40,6 +41,7 @@ float timed = 0.0f; float last_frame = 0.0f; float camera_speed = 20.0f; +float ghost_reset_distance = 10.0f; const char *projectionC = "projection"; const char *viewC = "view"; @@ -69,10 +71,12 @@ int main(int argc, char *argv[]) { glm::vec3 start_position; float start_yaw; - // Load configuration + // Load configuration knobs before spinning up the world. Config::loadFromFile("ghostland.json"); camera_speed = Config::getFloat("camera_speed", camera_speed); + ghost_reset_distance = Config::getFloat("ghost_reset_distance", ghost_reset_distance); + // Maze file contains serialized spawn and wall mesh data generated by mazeparser.py. FILE *fp = fopen("maze.txt", "r"); float yaw; if (fscanf(fp, "%f %f %f %f\n", &position.x, &position.y, &position.z, &yaw) == EOF) { @@ -150,6 +154,7 @@ int main(int argc, char *argv[]) { } fclose(fp); + // Initialize GLFW/GLAD and windowing. glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); @@ -366,6 +371,7 @@ int main(int argc, char *argv[]) { int FPS = -1; + // Main render/game loop. while (!glfwWindowShouldClose(window)) { float current_frame = static_cast(glfwGetTime()); @@ -428,7 +434,7 @@ int main(int argc, char *argv[]) { glm::vec3 light_front = player->get_light_front(); //printf("light_pos: (%f, %f, %f)\n", light_front.x, light_front.y, light_front.z); - // Sort ghosts by distance to player for optimization + // Sort ghosts by distance to player for optimization. glm::vec3 player_pos = player->get_pos(); struct { bool operator()(const Ghost *a, const Ghost *b) const { @@ -441,6 +447,7 @@ int main(int argc, char *argv[]) { return a_dist < b_dist; } } ghost_compare; + // Only the closest 20% need perfect ordering for blending, so partial_sort is cheaper than full sort. std::partial_sort(ghosts.begin(), ghosts.begin() + ghosts.size() / 5, ghosts.end(), ghost_compare); // Calculate nearest ghost distance (first ghost is now closest) @@ -522,15 +529,15 @@ int main(int argc, char *argv[]) { glm::mat4 ghost_model = ghosts[i]->get_model(camera_pos); ghosts[i]->apply_movement(current_frame, timed); - // Check if ghost is within 10.0 units of player + // When ghosts close in, reset player back to the maze entrance. glm::vec3 ghost_pos = ghosts[i]->get_pos(); glm::vec3 player_pos = player->get_pos(); float distance = glm::length(ghost_pos - player_pos); - if (distance <= 10.0f) { + if (distance <= ghost_reset_distance) { player->reset_position(start_position, start_yaw); } - // Check if ghost is within 30.0 units of start_position + // Nudge ghosts away from the spawn area so the player gets a breather. float start_distance = glm::length(ghost_pos - start_position); if (start_distance <= 30.0f) { ghosts[i]->reverse_direction_from_position(start_position); diff --git a/ghostland.json b/ghostland.json index c4918a7..fb2715b 100644 --- a/ghostland.json +++ b/ghostland.json @@ -1,3 +1,4 @@ { - "camera_speed": 30.0 + "camera_speed": 35.0, + "ghost_reset_distance": 5.0 } diff --git a/ghostshader.glsl b/ghostshader.glsl index 4328a86..7bce6d0 100644 --- a/ghostshader.glsl +++ b/ghostshader.glsl @@ -1,4 +1,5 @@ #version 460 core +// Billboarded ghost vertex shader: passes world pos/normal and flips UV.y for texture atlases. layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aNormal; layout (location = 2) in vec2 aTexCoord; diff --git a/mazeparser.py b/mazeparser.py index 4ef6a70..dbaadcb 100644 --- a/mazeparser.py +++ b/mazeparser.py @@ -1,17 +1,24 @@ #!/usr/bin/env python +# Generate maze.txt geometry by tracing the outline of a binary maze image. +# The script expects the maze PNG to be white-on-black and writes walls/floor +# segments in the format ghostland.cpp consumes at runtime. + import matplotlib.image as mpimage +# TODO: parameterize this path via CLI flags or config. img = mpimage.imread("/home/cowley/Pictures/maze.png") rows = len(img) cols = len(img[0]) def isfilled(val): + # Pixels brighter than ~20% are treated as walls. return all(x > 0.2 for x in val) def getval(img, x, y): + # Bounds-check helpers so we can safely probe virtual border pixels. if y < 0 or y >= len(img): return False if x < 0 or x >= len(img[y]): @@ -20,6 +27,8 @@ def getval(img, x, y): segments = [] +# Sweep the image in four directions to emit axis-aligned wall segments. +# Horizontal sweep detecting transitions from empty -> filled above (top edges). for iy in range(-1, rows + 1): seg_start = None for ix in range(-1, cols + 1): @@ -34,6 +43,7 @@ for iy in range(-1, rows + 1): segments.append((seg_start, (cols - 0.5, iy - 0.5), (0.0, 0.0, 1.0))) +# Horizontal sweep detecting transitions from empty -> filled below (bottom edges). for iy in range(-1, rows + 1): seg_start = None for ix in range(-1, cols + 1): @@ -47,6 +57,7 @@ for iy in range(-1, rows + 1): if seg_start is not None: segments.append((seg_start, (cols - 0.5, iy + 0.5), (0.0, 0.0, -1.0))) +# Vertical sweep detecting transitions from empty -> filled on the left. for ix in range(-1, cols + 1): seg_start = None for iy in range(-1, rows + 1): @@ -60,6 +71,7 @@ for ix in range(-1, cols + 1): if seg_start is not None: segments.append((seg_start, (ix - 0.5, rows - 0.5), (-1.0, 0.0, 0.0))) +# Vertical sweep detecting transitions from empty -> filled on the right. for ix in range(-1, cols + 1): seg_start = None for iy in range(-1, rows + 1): @@ -73,10 +85,12 @@ for ix in range(-1, cols + 1): if seg_start is not None: segments.append((seg_start, (ix + 0.5, rows - 0.5), (1.0, 0.0, 0.0))) -print(segments[:10]) +print(segments[:10]) # Quick sanity check; remove or redirect for batch runs. +# Serialize into the legacy maze.txt format ghostland.cpp expects. res = {} res["players"] = [] +# Hard-coded spawn until a better level editor exists. player = { "y": 2.5, "x": 845.0, @@ -86,6 +100,7 @@ player = { res["players"].append(player) res["surfaces"] = [] for segment in segments: + # Build quads with outward normals stored as the fifth element. p1 = {"x": segment[0][0], "z": -segment[0][1], "y": 0.0} p2 = { "x": segment[1][0], @@ -110,10 +125,12 @@ xmax = 0.0 zmin = 0.0 zmax = 0.0 +# Write the flattened surfaces followed by a floor bounding box so the game can detect footsteps. with open("maze.txt", "w") as f: f.write(f"{player['x']} {player['y']} {player['z']} {player['yaw']}\n") f.write(f"{len(res['surfaces'])}\n") for segment in res["surfaces"]: + # Track extrema to write a single floor plane at the end. for point in segment[:4]: if point["x"] < xmin: xmin = point["x"] diff --git a/player.cpp b/player.cpp index 3be7b09..9462395 100644 --- a/player.cpp +++ b/player.cpp @@ -8,6 +8,7 @@ Player::Player(glm::vec3 startpos, float startyaw) { position = startpos; yaw = startyaw; + // Initialize neutral view/camera defaults; yaw follows spawn while pitch starts level. pitch = 0.0f; first_mouse = false; first_frame = false; @@ -66,6 +67,7 @@ float Player::get_fov() { void Player::mouse_callback(GLFWwindow* window, double xposI, double yposI) { + // Track relative mouse deltas to drive FPS-style look controls. float xpos = static_cast(xposI); float ypos = static_cast(yposI); @@ -102,6 +104,7 @@ void Player::mouse_callback(GLFWwindow* window, double xposI, double yposI) { void Player::process_input(GLFWwindow *window) { + // Poll keyboard state and convert to world-space velocity. if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) { glfwSetWindowShouldClose(window, true); } @@ -185,6 +188,7 @@ bool Player::is_in_air() { void Player::set_light_offset(float xoffset, float yoffset) { + // Ease in flashlight offsets so the beam trails behind head motion. xoffset *= light_movement_multiplier; yoffset *= light_movement_multiplier; @@ -205,6 +209,7 @@ void Player::set_light_offset(float xoffset, float yoffset) { void Player::apply_movement(float timed) { + // Apply accumulated mouse/keyboard input, then integrate velocity with collision resolution. // mouse movement glm::vec3 front; front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch)); @@ -274,6 +279,7 @@ glm::vec3 Player::get_vec3_from_indices(int ix, int jx) { glm::vec3 Player::check_intersection(glm::vec3 movement) { + // Raycast against wall quads to zero out components that would clip into geometry. glm::vec3 vec_start, vec_stop, p1, p2, p3, res; bool gotX = false, gotZ = false; vec_start = get_camera_pos(); @@ -286,6 +292,7 @@ glm::vec3 Player::check_intersection(glm::vec3 movement) { if (gotX && gotZ) { break; } + // Only test faces whose normals oppose our intended direction. glm::vec3 norm = get_normal_from_index(ix); if (!gotX) { if ((movement.x > 0 && norm.x < 0) || (movement.x < 0 && norm.x > 0)) { @@ -324,4 +331,3 @@ void Player::scroll_callback(GLFWwindow* window, double xoffset, double yoffset) fov = 90.0f; } } - diff --git a/player.h b/player.h index 5236fba..a1deb98 100644 --- a/player.h +++ b/player.h @@ -8,6 +8,7 @@ #include "collisions.h" #include "ghost.h" +// Flashlight smoothing and locomotion constants tuned for the maze scale. const float lightoffsetmax = 15.0; const float light_persist_factor = 0.99; const float light_offset_factor = 0.01; @@ -16,6 +17,8 @@ const float jumpvelocity = 8.104849; const float vacceleration = -13.4058; const float hvelocity = 10.0; +// Player glues together camera orientation, flashlight pose, and collision-aware +// movement inside the maze geometry. class Player { public: @@ -52,8 +55,8 @@ class Player { float prevy; bool first_mouse; bool first_frame; - float light_xpersist; - float light_ypersist; + float light_xpersist; // smoothed horizontal flashlight offset + float light_ypersist; // smoothed vertical flashlight offset float armlength; float prev_move_time; float mouse_xoffset; @@ -64,14 +67,14 @@ class Player { glm::vec3 camera_up; glm::vec3 light_pos; glm::vec3 light_front; - glm::vec3 velocity; - glm::vec3 camera_offset; + glm::vec3 velocity; // accumulated velocity, including gravity + glm::vec3 camera_offset; // camera lift above feet void set_light_offset(float xoffset, float yoffset); glm::vec3 get_normal_from_index(int ix); glm::vec3 get_vec3_from_indices(int ix, int jx); - glm::vec3 check_intersection(glm::vec3 movement); + glm::vec3 check_intersection(glm::vec3 movement); // clamp movement vector by wall hits }; diff --git a/shader.cpp b/shader.cpp index cd41e51..e3ab986 100644 --- a/shader.cpp +++ b/shader.cpp @@ -5,6 +5,7 @@ #include "shader.h" +// Load a GLSL file from disk and compile it as vertex/fragment/etc. int create_shader(const char *filename, int shadertype) { FILE* fp = fopen(filename, "r"); if (!fp) { @@ -38,6 +39,7 @@ int create_shader(const char *filename, int shadertype) { return shader; } +// Compile and link a shader program from the given vertex/fragment GLSL files. int create_shader_program(const char *vertex_filename, const char *fragment_filename) { int vertex_shader = create_shader(vertex_filename, GL_VERTEX_SHADER); @@ -70,6 +72,7 @@ int create_shader_program(const char *vertex_filename, const char *fragment_file } +// Simple wrappers to make uniform updates less verbose at call sites. void set_uniform(int program, const char *key, float value) { glUniform1f(glGetUniformLocation(program, key), value); } diff --git a/shader.h b/shader.h index bc613fb..23d5130 100644 --- a/shader.h +++ b/shader.h @@ -3,6 +3,7 @@ #include +// Utility helpers for compiling GLSL source files and setting uniforms. int create_shader(const char *filename, int shadertype); int create_shader_program(const char *vertex_filename, const char *fragment_filename); diff --git a/text.cpp b/text.cpp index a472179..a489f1f 100644 --- a/text.cpp +++ b/text.cpp @@ -5,13 +5,16 @@ #include "text.h" #include "shader.h" +// Global freetype context shared across text rendering. FT_Library ft; FT_Face face; +// Uniform key for tinting glyphs. const char *textcolorC = "textColor"; -unsigned int textVAO, textVBO; +unsigned int textVAO, textVBO; // shared quad mesh for all HUD strings +// Glyph metadata + GL texture for a single character. struct character_t { unsigned int texture_id; glm::ivec2 size; @@ -19,8 +22,10 @@ struct character_t { unsigned int advance; }; +// ASCII glyph cache keyed by character codepoint. std::map glyph_to_character; +// Initialize freetype and bake ASCII glyphs into GL textures for reuse. int init_text(int *shader) { if (FT_Init_FreeType(&ft)) { @@ -33,8 +38,9 @@ int init_text(int *shader) { return -1; } + // Bake glyph bitmaps at a fixed pixel height; width is dynamic. FT_Set_Pixel_Sizes(face, 0, 48); - glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // allow byte-aligned glyph rows for (unsigned char c = 0; c < 128; c++) { @@ -44,6 +50,7 @@ int init_text(int *shader) { } unsigned int texture; + // Upload the glyph bitmap into a single-channel texture. glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); glTexImage2D( @@ -78,8 +85,7 @@ int init_text(int *shader) { FT_Done_Face(face); FT_Done_FreeType(ft); - // configure textVAO/textVBO for texture quads - // ----------------------------------- + // Configure buffers for a dynamic quad (two triangles per glyph). glGenVertexArrays(1, &textVAO); glGenBuffers(1, &textVBO); glBindVertexArray(textVAO); @@ -100,9 +106,10 @@ int init_text(int *shader) { } +// Draw a string using pre-baked glyph textures at the supplied screen position. void render_text(int shader, std::string text, float x, float y, float scale, glm::vec3 color) { - set_uniform(shader, textcolorC, color); + set_uniform(shader, textcolorC, color); // apply tint per draw call glActiveTexture(GL_TEXTURE0); glBindVertexArray(textVAO); @@ -116,6 +123,7 @@ void render_text(int shader, std::string text, float x, float y, float scale, gl float w = ch.size.x * scale; float h = ch.size.y * scale; + // Each glyph is rendered as a textured quad. float vertices[24] = { xpos, ypos + h, 0.0f, 0.0f, xpos, ypos, 0.0f, 1.0f, @@ -126,12 +134,12 @@ void render_text(int shader, std::string text, float x, float y, float scale, gl }; glBindTexture(GL_TEXTURE_2D, ch.texture_id); glBindBuffer(GL_ARRAY_BUFFER, textVBO); - glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(float) * (2 + 2) * 6, vertices); + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(float) * (2 + 2) * 6, vertices); // stream quad verts glBindBuffer(GL_ARRAY_BUFFER, 0); glDrawArrays(GL_TRIANGLES, 0, (2 + 2) * 6); - x += (ch.advance >> 6) * scale; + x += (ch.advance >> 6) * scale; // advance is stored in 1/64th pixels } diff --git a/text.h b/text.h index 365cea5..2dc5353 100644 --- a/text.h +++ b/text.h @@ -7,6 +7,7 @@ #include #include FT_FREETYPE_H +// Loads glyphs and configures OpenGL state to render HUD text via Freetype. int init_text(int *shader); void render_text(int shader, std::string text, float x, float y, float scale, glm::vec3 color); diff --git a/textfragshader.glsl b/textfragshader.glsl index b11904a..e04d0bd 100644 --- a/textfragshader.glsl +++ b/textfragshader.glsl @@ -1,4 +1,5 @@ #version 460 core +// Samples single-channel glyph texture and applies RGB tint. in vec2 TexCoords; out vec4 color; diff --git a/textshader.glsl b/textshader.glsl index 177eef4..bfe59cd 100644 --- a/textshader.glsl +++ b/textshader.glsl @@ -1,4 +1,5 @@ #version 460 core +// HUD text vertex shader: projects screen-space quad verts and forwards glyph UVs. layout (location = 0) in vec4 vertex; // out vec2 TexCoords; diff --git a/trailfragshader.glsl b/trailfragshader.glsl index 04feea4..47f68ca 100644 --- a/trailfragshader.glsl +++ b/trailfragshader.glsl @@ -1,4 +1,5 @@ #version 460 core +// Colors the trail geometry with a solid uniform tint. out vec4 FragColor; in vec3 FragPos; diff --git a/trailshader.glsl b/trailshader.glsl index 9d7263f..ad61cf3 100644 --- a/trailshader.glsl +++ b/trailshader.glsl @@ -1,4 +1,5 @@ #version 460 core +// Simple vertex shader for the player's breadcrumb trail triangles. layout (location = 0) in vec3 aPos; out vec3 FragPos; diff --git a/vertexshader.glsl b/vertexshader.glsl index a228f1f..4233943 100644 --- a/vertexshader.glsl +++ b/vertexshader.glsl @@ -1,4 +1,5 @@ #version 460 core +// Basic vertex shader for world geometry (walls/floor). Transforms positions/normals into view space. layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aNormal;