Better player encapsulation. World persistence. Cleaned up CPU and GPU memory leaks.

This commit is contained in:
Jake 2025-05-30 10:56:22 -04:00
parent 585f72a4bd
commit 701c72a2a5
10 changed files with 168 additions and 228 deletions

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -14,6 +14,12 @@ typedef struct {
int type; // 0 = air, 1 = dirt, etc.
} Block;
typedef struct {
Block blocks[CHUNK_SIZE_X][CHUNK_SIZE_Y][CHUNK_SIZE_Z];
Mesh mesh; // Owned by the chunk, valid only at runtime
bool hasMesh; //
} Chunk;
// 6 directions for checking neighbors: +/-X, +/-Y, +/-Z
static const int faceOffsets[6][3] = {
{ -1, 0, 0 }, // left
@ -24,13 +30,16 @@ static const int faceOffsets[6][3] = {
{ 0, 0, 1 } // front
};
typedef struct {
Block blocks[CHUNK_SIZE_X][CHUNK_SIZE_Y][CHUNK_SIZE_Z];
} Chunk;
// Function to check if a face of a block is exposed.
int IsBlockFaceExposed(Chunk *chunk, int x, int y, int z, int dir);
// Function that places a tree dumbly.
void PlaceTreeAt(Chunk *chunk, int x, int y, int z) ;
// Save chunk to disk.
bool SaveChunk(const Chunk *chunk, const char *filename);
// Load chunk from disk.
bool LoadChunk(Chunk *chunk, const char *filename);
#endif

View File

@ -5,6 +5,17 @@
#include "raylib.h"
#include "chunkStructures.h"
typedef struct {
Vector3 mapPosition; // Player's world position (camera position)
Vector2 playerOrientation; // Yaw (x) and pitch (y), in radians
Vector3 forward; // Direction vector computed from yaw/pitch
Vector3 right; // Right vector
float moveSpeed; // Movement speed
Camera3D camera;
} Player;
void UpdatePlayer(Player *player);
typedef struct {
bool hit;
Vector3 position;
@ -15,8 +26,12 @@ typedef struct {
RaycastHit RaycastChunk(const Chunk *chunk, Vector3 origin, Vector3 direction, float maxDistance);
RaycastHit GetPlayerRaycastHit(Player *player, Chunk *chunk, float maxDistance);
void UpdateFreeCamera(Camera3D *cam, float speed, float *yawOut, float *pitchOut);
void DrawFaceHighlight(Vector3 blockPos, Vector3 normal);
void HandleBlockInteraction(Player *player, Chunk *chunk, RaycastHit hit, int blockSelection);
#endif

0
saves/.gitkeep Normal file
View File

View File

@ -10,11 +10,11 @@ void GenerateFlatChunk(Chunk *chunk) {
for (int x = 0; x < CHUNK_SIZE_X; x++) {
for (int z = 0; z < CHUNK_SIZE_Z; z++) {
for (int y = 0; y < CHUNK_SIZE_Y; y++) {
if (y < 3) {
if (y < 59) {
chunk->blocks[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;

View File

@ -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 <stdlib.h>
#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 (03),
/// assuming tiles are arranged in a grid in the texture atlas.
// Returns the UV coordinate for a given tile index and corner index (03),
// 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;
}

View File

@ -8,6 +8,23 @@
#include "rlgl.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
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) {

View File

@ -1,60 +1,48 @@
// playerController.c
// Player controller for Voxelthing
#include "raylib.h"
#include "raymath.h"
#include "rlgl.h"
#include "playerController.h"
#include "blockTypes.h"
#include <math.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 = {
player->right = (Vector3){
sinf(yaw - PI/2.0f),
0.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);
}
return RaycastChunk(chunk, ray.position, ray.direction, maxDistance);
}
rlEnd();
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;
}
}
}

View File

@ -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;
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, 7, 8);
PlaceTreeAt(&chunk, 8, 64, 8);
SaveChunk(&chunk, "saves/chunk_0_0_0.dat");
}
Mesh chunkMesh = GenerateChunkMesh(&chunk);
// --- Load textures and materials ---
@ -59,39 +33,40 @@ 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 ---
// 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;
@ -102,54 +77,36 @@ int main(void) {
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
if (hit.hit && (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) || IsMouseButtonPressed(MOUSE_RIGHT_BUTTON))) {
UnloadMesh(chunkMesh);
chunkMesh = GenerateChunkMesh(&chunk);
}
}
BeginDrawing();
ClearBackground(RAYWHITE);
// --- 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);
BeginMode3D(player.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
//DrawFaceHighlight(hit.position, hit.normal);
}
// 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 ---
// 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);
// -- 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);
}
// Debug info
DrawText(TextFormat("Yaw: %.1f Pitch: %.1f", player.playerOrientation.x * RAD2DEG, player.playerOrientation.y * RAD2DEG), 10, 10, 20, DARKGRAY);
// --- Draw pause menu if paused ---
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);