From 701c72a2a57c5f60b5b8dc3b63862b5ac4c07135 Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 30 May 2025 10:56:22 -0400 Subject: [PATCH] Better player encapsulation. World persistence. Cleaned up CPU and GPU memory leaks. --- README.md | 7 +- assets/TextureAtlas.png | Bin 6325 -> 6325 bytes include/chunkStructures.h | 17 +++- include/playerController.h | 15 +++ saves/.gitkeep | 0 source/chunkGenerator.c | 6 +- source/chunkRenderer.c | 11 +- source/chunkStructures.c | 17 ++++ source/playerController.c | 124 +++++++++-------------- source/voxelThing.c | 199 ++++++++++++------------------------- 10 files changed, 168 insertions(+), 228 deletions(-) create mode 100644 saves/.gitkeep diff --git a/README.md b/README.md index edb8c1d..645e0cd 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,15 @@ # voxelThing Jake's Raylib Minecraft Clone -Implements a voxel chunk renderer. Chunks are stored as a 3D array of block structs elements which define block types and later other block data. The chunk is converted to a mesh such that only faces exposed to air blocks or the chunk boarders are rendered. +Implements a voxel chunk renderer. Chunks are stored as a 3D array of block struct elements which define block types and later other block data. The chunk is converted to a mesh such that only faces exposed to air blocks or the chunk boarders are rendered. Code is rough and messy and not much is implemented yet. +Once a chunk has been generated, its state is persistent across game restarts. + Controls: - WASD - move + - Left Shift - Down, Space - Up - Mouse - look - LMB - Remove block - RMB - Place Block @@ -20,6 +23,6 @@ Depends on Raylib. 1) Install Raylib. 2) Clone the repo. -3) Build by running make. +3) CD into the repo and build by running make. 4) run bin/voxelThing diff --git a/assets/TextureAtlas.png b/assets/TextureAtlas.png index e90c0acc0654c4666137b2a8d865043f8892ab0e..858e0e4ca1b2e5efece99d2df582c984b66354a2 100644 GIT binary patch delta 32 ocmdmLxYck%G3S4V{|x^Z>|H*2VvPcatN<%}hNY6<#wCIh0O|w`Y5)KL delta 32 ocmdmLxYck%G3PginGE(-lQ$fgSfjup!yvD7!RYCMjY|Y20NFAOblocks[x][y][z].type = BLOCK_STONE; - } else if (y < 7) { + } else if (y < 64) { chunk->blocks[x][y][z].type = BLOCK_DIRT; - } else if (y == 7) { + } else if (y == 64) { chunk->blocks[x][y][z].type = BLOCK_GRASS; } else { chunk->blocks[x][y][z].type = BLOCK_AIR; diff --git a/source/chunkRenderer.c b/source/chunkRenderer.c index 4839635..3585ef9 100644 --- a/source/chunkRenderer.c +++ b/source/chunkRenderer.c @@ -1,8 +1,5 @@ // chunkRenderer.c // Rendering and meshing functions for voxelThing -// TODO: Memory is allocated dynamically in here but never freed. -// TODO: Chunk meshes need to be unloaded from the VRAM with UnloadMesh() when chunks are updated. -// For now new meshes are just added to the VRAM after each update... #include #include "raylib.h" @@ -14,8 +11,8 @@ #define ATLAS_SIZE 256 #define ATLAS_TILES_PER_ROW (ATLAS_SIZE / TILE_SIZE) -/// Returns the UV coordinate for a given tile index and corner index (0–3), -/// assuming tiles are arranged in a grid in the texture atlas. +// Returns the UV coordinate for a given tile index and corner index (0–3), +// assuming tiles are arranged in a grid in the texture atlas. Vector2 GetTileUV(int tile, int corner) { int tileX = tile % ATLAS_TILES_PER_ROW; int tileY = tile / ATLAS_TILES_PER_ROW; @@ -33,7 +30,7 @@ Vector2 GetTileUV(int tile, int corner) { } } -/// Generates a mesh for the given chunk by stitching together visible block faces. +// Generates a mesh for the given chunk by stitching together visible block faces. Mesh GenerateChunkMesh(Chunk *chunk) { const int maxFaces = CHUNK_SIZE_X * CHUNK_SIZE_Y * CHUNK_SIZE_Z * 6; const int maxVerts = maxFaces * 4; // 4 verts per face @@ -141,7 +138,7 @@ Mesh GenerateChunkMesh(Chunk *chunk) { mesh.normals = (float *)normals; mesh.indices = indices; - UploadMesh(&mesh, false); + UploadMesh(&mesh, true); // True here tells the function to free the CPU side allocated memory. return mesh; } diff --git a/source/chunkStructures.c b/source/chunkStructures.c index 3154393..0b11b70 100644 --- a/source/chunkStructures.c +++ b/source/chunkStructures.c @@ -8,6 +8,23 @@ #include "rlgl.h" #include #include +#include + +bool SaveChunk(const Chunk *chunk, const char *filename) { + FILE *fp = fopen(filename, "wb"); + if (!fp) return false; + size_t written = fwrite(chunk->blocks, sizeof(Block), CHUNK_SIZE_X * CHUNK_SIZE_Y * CHUNK_SIZE_Z, fp); + fclose(fp); + return written == CHUNK_SIZE_X * CHUNK_SIZE_Y * CHUNK_SIZE_Z; +} + +bool LoadChunk(Chunk *chunk, const char *filename) { + FILE *fp = fopen(filename, "rb"); + if (!fp) return false; + size_t read = fread(chunk->blocks, sizeof(Block), CHUNK_SIZE_X * CHUNK_SIZE_Y * CHUNK_SIZE_Z, fp); + fclose(fp); + return read == CHUNK_SIZE_X * CHUNK_SIZE_Y * CHUNK_SIZE_Z; +} // Places a tree at the position specified by x, y, z void PlaceTreeAt(Chunk *chunk, int x, int y, int z) { diff --git a/source/playerController.c b/source/playerController.c index 4760ee8..1d4c5dd 100644 --- a/source/playerController.c +++ b/source/playerController.c @@ -1,60 +1,48 @@ // playerController.c -// Player controller for Voxelthing +#include "raylib.h" +#include "raymath.h" +#include "playerController.h" +#include "blockTypes.h" +#include -#include"raylib.h" -#include"raymath.h" -#include "rlgl.h" -#include"playerController.h" -#include"blockTypes.h" - -float yaw = 0; -float pitch = 0; - -// A basic free moving, 'noclip' style camera to get going with the most basic interactions. -// returns yaw because I'm confused and am trying to help -void UpdateFreeCamera(Camera3D *cam, float speed, float *yawOut, float *pitchOut) { +void UpdatePlayer(Player *player) { Vector2 mouseDelta = GetMouseDelta(); + player->playerOrientation.x += mouseDelta.x * -0.002f; + player->playerOrientation.y += mouseDelta.y * -0.002f; - yaw += mouseDelta.x * -0.002f; - pitch += mouseDelta.y * -0.002f; - *yawOut = yaw; - *pitchOut = pitch; + float limit = PI / 1.8f; + if (player->playerOrientation.y > limit) player->playerOrientation.y = limit; + if (player->playerOrientation.y < -limit) player->playerOrientation.y = -limit; - // Clamp pitch - float clampLimit = PI / 1.80f; - if (pitch > clampLimit) pitch = clampLimit; - if (pitch < -clampLimit) pitch = -clampLimit; + float yaw = player->playerOrientation.x; + float pitch = player->playerOrientation.y; - // Compute forward vector from yaw/pitch - Vector3 forward = { + player->forward = (Vector3){ cosf(pitch) * sinf(yaw), sinf(pitch), cosf(pitch) * cosf(yaw) }; - Vector3 right = { - sinf(yaw - PI / 2.0f), + player->right = (Vector3){ + sinf(yaw - PI/2.0f), 0.0f, - cosf(yaw - PI / 2.0f) + cosf(yaw - PI/2.0f) }; - // Movement input Vector3 movement = {0}; - if (IsKeyDown(KEY_W)) movement = Vector3Add(movement, forward); - if (IsKeyDown(KEY_S)) movement = Vector3Subtract(movement, forward); - if (IsKeyDown(KEY_A)) movement = Vector3Subtract(movement, right); - if (IsKeyDown(KEY_D)) movement = Vector3Add(movement, right); + if (IsKeyDown(KEY_W)) movement = Vector3Add(movement, player->forward); + if (IsKeyDown(KEY_S)) movement = Vector3Subtract(movement, player->forward); + if (IsKeyDown(KEY_A)) movement = Vector3Subtract(movement, player->right); + if (IsKeyDown(KEY_D)) movement = Vector3Add(movement, player->right); if (IsKeyDown(KEY_SPACE)) movement.y += 1.0f; if (IsKeyDown(KEY_LEFT_SHIFT)) movement.y -= 1.0f; - // Apply movement if (Vector3Length(movement) > 0.0f) - movement = Vector3Scale(Vector3Normalize(movement), speed * GetFrameTime()); - cam->position = Vector3Add(cam->position, movement); + movement = Vector3Scale(Vector3Normalize(movement), player->moveSpeed * GetFrameTime()); - // Update target so that the camera looks forward - cam->target = Vector3Add(cam->position, forward); - // return the value of cameraYaw... + player->mapPosition = Vector3Add(player->mapPosition, movement); + player->camera.position = player->mapPosition; + player->camera.target = Vector3Add(player->mapPosition, player->forward); } // An implementation of DDA (digital differential analyzer), steps through each voxel boundary along a ray cast from origin along direction to maxDistance @@ -136,47 +124,29 @@ RaycastHit RaycastChunk(const Chunk *chunk, Vector3 origin, Vector3 direction, f return result; } -void DrawFaceHighlight(Vector3 blockPos, Vector3 normal) { - Vector3 center = Vector3Add(blockPos, (Vector3){0.5f, 0.5f, 0.5f}); - Vector3 faceCenter = Vector3Add(center, Vector3Scale(normal, 0.51f)); - Vector3 u = {0}, v = {0}; - - if (normal.x != 0) { - u = (Vector3){0, 0, 0.5f}; - v = (Vector3){0, 0.5f, 0}; - } else if (normal.y != 0) { - u = (Vector3){0.5f, 0, 0}; - v = (Vector3){0, 0, 0.5f}; - } else { - u = (Vector3){0.5f, 0, 0}; - v = (Vector3){0, 0.5f, 0}; - } - - Vector3 corners[4] = { - Vector3Add(Vector3Add(faceCenter, u), v), - Vector3Add(Vector3Subtract(faceCenter, u), v), - Vector3Add(Vector3Subtract(faceCenter, u), Vector3Negate(v)), - Vector3Add(Vector3Add(faceCenter, u), Vector3Negate(v)) +RaycastHit GetPlayerRaycastHit(Player *player, Chunk *chunk, float maxDistance) { + Ray ray = { + .position = player->mapPosition, + .direction = Vector3Normalize(player->forward) }; - - // Flip winding for certain normals so the face always faces outward - bool flip = false; - if (normal.x == 1 || normal.y == 1 || normal.z == -1) flip = true; - - rlBegin(RL_QUADS); - rlColor4ub(0, 255, 0, 100); - - if (flip) { - for (int i = 3; i >= 0; i--) { - rlVertex3f(corners[i].x, corners[i].y, corners[i].z); - } - } else { - for (int i = 0; i < 4; i++) { - rlVertex3f(corners[i].x, corners[i].y, corners[i].z); - } - } - - rlEnd(); + return RaycastChunk(chunk, ray.position, ray.direction, maxDistance); } +void HandleBlockInteraction(Player *player, Chunk *chunk, RaycastHit hit, int blockSelection) { + if (hit.hit && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + chunk->blocks[(int)hit.position.x][(int)hit.position.y][(int)hit.position.z].type = BLOCK_AIR; + } + + if (hit.hit && IsMouseButtonPressed(MOUSE_RIGHT_BUTTON)) { + int px = (int)(hit.position.x + hit.normal.x); + int py = (int)(hit.position.y + hit.normal.y); + int pz = (int)(hit.position.z + hit.normal.z); + + if (px >= 0 && px < CHUNK_SIZE_X && + py >= 0 && py < CHUNK_SIZE_Y && + pz >= 0 && pz < CHUNK_SIZE_Z) { + chunk->blocks[px][py][pz].type = blockSelection; + } + } +} diff --git a/source/voxelThing.c b/source/voxelThing.c index 95f04fb..566521e 100644 --- a/source/voxelThing.c +++ b/source/voxelThing.c @@ -1,57 +1,31 @@ -// voxelThing.c -// A voxel chunk rendering doodad written in C with Raylib - -// External libraries +// voxelThing.c (cleaned-up main loop) #include "raylib.h" #include "raymath.h" #include "rlgl.h" #include "stdio.h" -// Project files #include "chunkStructures.h" #include "chunkRenderer.h" #include "blockTypes.h" #include "playerController.h" #include "chunkGenerator.h" -// Bunch of global variables to clean up later. -float cameraYaw = 0; -float cameraPitch = 0; -bool paused = false; -RaycastHit hit; -int blockSelection = BLOCK_SAND; - -// Random helper function for returning what direction you're facing.' -const char* GetCompassDirection(float yaw) { - yaw = fmodf(yaw * RAD2DEG + 360.0f, 360.0f); - if (yaw < 22.5f || yaw >= 337.5f) return "South (+Z)"; - if (yaw < 67.5f) return "Southwest (+X+Z)"; - if (yaw < 112.5f) return "West (+X)"; - if (yaw < 157.5f) return "Northwest (+X-Z)"; - if (yaw < 202.5f) return "North (-Z)"; - if (yaw < 247.5f) return "Northeast (-X-Z)"; - if (yaw < 292.5f) return "East (-X)"; - return "Southeast (-X+Z)"; -} - -// Another random helper function, this time for checking if a Vector3 is non-zero. -static inline bool Vector3IsNonZero(Vector3 v) { - return v.x != 0.0f || v.y != 0.0f || v.z != 0.0f; -} - int main(void) { // --- Screen setup --- - int screenWidth = 800; - int screenHeight = 600; SetConfigFlags(FLAG_WINDOW_RESIZABLE); - InitWindow(screenWidth, screenHeight, "VoxelThing"); + InitWindow(800, 600, "VoxelThing"); SetExitKey(-1); - DisableCursor(); // Lock mouse to window for FPS-style camera + DisableCursor(); // --- World generation --- Chunk chunk; - GenerateFlatChunk(&chunk); - PlaceTreeAt(&chunk, 8, 7, 8); + if (!LoadChunk(&chunk, "saves/chunk_0_0_0.dat")) { + // There was no save, gotta generate a fresh chunk to play with. + printf("--- WORLDGEN--- No save, creating new chunk.\n"); + GenerateFlatChunk(&chunk); + PlaceTreeAt(&chunk, 8, 64, 8); + SaveChunk(&chunk, "saves/chunk_0_0_0.dat"); + } Mesh chunkMesh = GenerateChunkMesh(&chunk); // --- Load textures and materials --- @@ -59,97 +33,80 @@ int main(void) { Material mat = LoadMaterialDefault(); mat.maps[MATERIAL_MAP_DIFFUSE].texture = atlas; - // --- Initialize camera --- - Camera3D camera = { 0 }; - camera.position = (Vector3){ 10.0f, 10.0f, 10.0f }; // Initial camera position - camera.target = (Vector3){ 0.0f, 0.0f, 0.0f }; // Looking toward origin - camera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; // Y-up world - camera.fovy = 90.0f; // Field of view - camera.projection = CAMERA_PERSPECTIVE; // Use perspective projection + // --- Player setup --- + Player player = { + .mapPosition = (Vector3){ 1.0f, 67.0f, 1.0f }, + .playerOrientation = (Vector2){ 0.0f, 0.0f }, + .moveSpeed = 10.0f + }; + + player.camera.fovy = 90.0f; + player.camera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; + player.camera.projection = CAMERA_PERSPECTIVE; SetTargetFPS(60); - // --- Main game loop --- - while (!WindowShouldClose()) { - screenWidth = GetScreenWidth(); - screenHeight = GetScreenHeight(); - BeginDrawing(); - ClearBackground(RAYWHITE); + bool paused = false; + RaycastHit hit; + int blockSelection = BLOCK_SAND; - // --- Handle jumping to fullscreen mode with F11 --- + while (!WindowShouldClose()) { + int screenWidth = GetScreenWidth(); + int screenHeight = GetScreenHeight(); + + // Toggle fullscreen if (IsKeyPressed(KEY_F11)) { ToggleFullscreen(); - Vector2 center = { GetScreenWidth() / 2.0f, GetScreenHeight() / 2.0f }; - SetMousePosition(center.x, center.y); + SetMousePosition(screenWidth / 2, screenHeight / 2); } - // --- Handle pausing --- + // Pause toggle if (IsKeyPressed(KEY_ESCAPE)) { paused = !paused; - if (paused) EnableCursor(); - else DisableCursor(); + paused ? EnableCursor() : DisableCursor(); } - // --- Handle selecting blocks --- - - if (IsKeyPressed(KEY_ONE)) blockSelection = BLOCK_STONE; - if (IsKeyPressed(KEY_TWO)) blockSelection = BLOCK_DIRT; + // Temporary block selection hotkeys + if (IsKeyPressed(KEY_ONE)) blockSelection = BLOCK_STONE; + if (IsKeyPressed(KEY_TWO)) blockSelection = BLOCK_DIRT; if (IsKeyPressed(KEY_THREE)) blockSelection = BLOCK_GRASS; - if (IsKeyPressed(KEY_FOUR)) blockSelection = BLOCK_SAND; - if (IsKeyPressed(KEY_FIVE)) blockSelection = BLOCK_GRAVEL; - if (IsKeyPressed(KEY_SIX)) blockSelection = BLOCK_LOG; + if (IsKeyPressed(KEY_FOUR)) blockSelection = BLOCK_SAND; + if (IsKeyPressed(KEY_FIVE)) blockSelection = BLOCK_GRAVEL; + if (IsKeyPressed(KEY_SIX)) blockSelection = BLOCK_LOG; if (IsKeyPressed(KEY_SEVEN)) blockSelection = BLOCK_LEAF; if (IsKeyPressed(KEY_EIGHT)) blockSelection = BLOCK_PLANK; if (!paused) { - // --- Update camera and direction --- + UpdatePlayer(&player); + hit = GetPlayerRaycastHit(&player, &chunk, 10.0f); + HandleBlockInteraction(&player, &chunk, hit, blockSelection); - UpdateFreeCamera(&camera, 10.0f, &cameraYaw, &cameraPitch); // Move camera with user input - - - // --- Raycasting from screen center --- - Vector2 screenCenter = { screenWidth / 2.0f, screenHeight / 2.0f }; - // This is where we grab the ray... - // Ray ray = GetMouseRay(screenCenter, camera); - Vector3 camDir = { - cosf(cameraPitch) * sinf(cameraYaw), - sinf(cameraPitch), - cosf(cameraPitch) * cosf(cameraYaw) - }; - - Ray ray = { - .position = camera.position, - .direction = Vector3Normalize(camDir) - }; - hit = RaycastChunk(&chunk, ray.position, ray.direction, 10.0f); - - // --- Begin 3D rendering --- - BeginMode3D(camera); - - DrawMesh(chunkMesh, mat, MatrixIdentity()); - - if (hit.hit) { - // Draw a wireframe cube where the ray hit - DrawCubeWires(Vector3Add(hit.position, (Vector3){0.5f, 0.5f, 0.5f}), 1.02f, 1.02f, 1.02f, BLACK); - //DrawFaceHighlight(hit.position, hit.normal); // Highlight the specific face hit + if (hit.hit && (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) || IsMouseButtonPressed(MOUSE_RIGHT_BUTTON))) { + UnloadMesh(chunkMesh); + chunkMesh = GenerateChunkMesh(&chunk); } - - // Draw a lil debug cube where the player is looking. - //Vector3 hitPoint = Vector3Add(ray.position, Vector3Scale(ray.direction, hit.t)); - //DrawCubeWires(hitPoint, 0.05f, 0.05f, 0.05f, RED); - - EndMode3D(); - - // --- Draw crosshair in screen center --- - DrawLine(screenWidth/2 - 5, screenHeight/2, screenWidth/2 + 5, screenHeight/2, DARKGRAY); - DrawLine(screenWidth/2, screenHeight/2 - 5, screenWidth/2, screenHeight/2 + 5, DARKGRAY); - - // -- Draw debug info --- - DrawText(TextFormat("Facing: %s", GetCompassDirection(cameraYaw)), 10, 10, 20, DARKGRAY); - DrawText(TextFormat("Yaw: %.1f° Pitch: %.1f°", cameraYaw * RAD2DEG, cameraPitch * RAD2DEG), 10, 30, 20, GRAY); } - // --- Draw pause menu if paused --- + BeginDrawing(); + ClearBackground(RAYWHITE); + + BeginMode3D(player.camera); + DrawMesh(chunkMesh, mat, MatrixIdentity()); + + if (hit.hit) { + DrawCubeWires(Vector3Add(hit.position, (Vector3){0.5f, 0.5f, 0.5f}), 1.02f, 1.02f, 1.02f, BLACK); + //DrawFaceHighlight(hit.position, hit.normal); + } + + EndMode3D(); + + // Draw crosshair + DrawLine(screenWidth / 2 - 5, screenHeight / 2, screenWidth / 2 + 5, screenHeight / 2, DARKGRAY); + DrawLine(screenWidth / 2, screenHeight / 2 - 5, screenWidth / 2, screenHeight / 2 + 5, DARKGRAY); + + // Debug info + DrawText(TextFormat("Yaw: %.1f Pitch: %.1f", player.playerOrientation.x * RAD2DEG, player.playerOrientation.y * RAD2DEG), 10, 10, 20, DARKGRAY); + if (paused) { DrawRectangle(0, 0, screenWidth, screenHeight, Fade(DARKGRAY, 0.5f)); DrawText("Paused", screenWidth / 2 - MeasureText("Paused", 40) / 2, screenHeight / 2 - 20, 40, RAYWHITE); @@ -157,38 +114,10 @@ int main(void) { } EndDrawing(); - - // TODO: Shoudn't all this block handling be handled in playerController.c? - - // --- Handle block removal (left click) --- - if (hit.hit && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { - chunk.blocks[(int)hit.position.x][(int)hit.position.y][(int)hit.position.z].type = BLOCK_AIR; - chunkMesh = GenerateChunkMesh(&chunk); // Rebuild mesh after change - } - - // --- Handle block placement (right click) --- - if (hit.hit && IsMouseButtonPressed(MOUSE_RIGHT_BUTTON)) { - int px = (int)(hit.position.x + hit.normal.x); - int py = (int)(hit.position.y + hit.normal.y); - int pz = (int)(hit.position.z + hit.normal.z); - - // Bounds check - if (px >= 0 && px < CHUNK_SIZE_X && - py >= 0 && py < CHUNK_SIZE_Y && - pz >= 0 && pz < CHUNK_SIZE_Z) { - - chunk.blocks[px][py][pz].type = blockSelection; - chunkMesh = GenerateChunkMesh(&chunk); - - printf("Hit at (%f %f %f), normal (%f %f %f), placing at (%d %d %d)\n", - hit.position.x, hit.position.y, hit.position.z, - hit.normal.x, hit.normal.y, hit.normal.z, - px, py, pz); - } - } } // --- Cleanup --- + SaveChunk(&chunk, "saves/chunk_0_0_0.dat"); UnloadTexture(atlas); UnloadMesh(chunkMesh); UnloadMaterial(mat);