diff --git a/README.md b/README.md index 645e0cd..b8a64c0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # voxelThing Jake's Raylib Minecraft Clone -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. +Renders a voxel world in chunks. Code is rough and messy and not much is implemented yet. diff --git a/include/blockTypes.h b/include/blockTypes.h index 44bf5cf..88f4ed6 100644 --- a/include/blockTypes.h +++ b/include/blockTypes.h @@ -1,6 +1,6 @@ // blockTypes.h #ifndef BLOCK_TYPES_H -#define BLOCKTYPES_H +#define BLOCK_TYPES_H // Definitions for Block IDs (notes are texture atlas indicies) #define BLOCK_AIR 0 // No texture. diff --git a/include/chunkIO.h b/include/chunkIO.h new file mode 100644 index 0000000..db9172d --- /dev/null +++ b/include/chunkIO.h @@ -0,0 +1,11 @@ +// chunkIO.h +#ifndef CHUNK_IO_H +#define CHUNK_IO_H + +#include +#include "chunkStructures.h" + +bool SaveChunk(const Chunk *chunk); +bool LoadChunk(Chunk *chunk); + +#endif diff --git a/include/chunkStructures.h b/include/chunkStructures.h index 199ab0f..e0e8cdf 100644 --- a/include/chunkStructures.h +++ b/include/chunkStructures.h @@ -18,6 +18,9 @@ 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; // + bool hasChanged; // Flag that determines if chunk needs to be saved on unload + int x; + int z; } Chunk; // 6 directions for checking neighbors: +/-X, +/-Y, +/-Z @@ -36,10 +39,6 @@ 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); +void InitChunk(Chunk *chunk, int chunkX, int chunkZ); #endif diff --git a/include/playerController.h b/include/playerController.h index d041078..9506d58 100644 --- a/include/playerController.h +++ b/include/playerController.h @@ -4,6 +4,7 @@ #include "raylib.h" #include "chunkStructures.h" +#include "world.h" typedef struct { Vector3 mapPosition; // Player's world position (camera position) @@ -19,14 +20,20 @@ void UpdatePlayer(Player *player); typedef struct { bool hit; Vector3 position; + int hitBlockX, hitBlockY, hitBlockZ; Vector3 normal; int blockID; float t; + int chunkX; + int chunkZ; } RaycastHit; RaycastHit RaycastChunk(const Chunk *chunk, Vector3 origin, Vector3 direction, float maxDistance); -RaycastHit GetPlayerRaycastHit(Player *player, Chunk *chunk, float maxDistance); +RaycastHit GetPlayerRaycastHit(Player *player, World *world, float maxDistance); + +// Osolete with multichunk worlds. +//RaycastHit GetPlayerRaycastHit(Player *player, Chunk *chunk, float maxDistance); void UpdateFreeCamera(Camera3D *cam, float speed, float *yawOut, float *pitchOut); diff --git a/include/world.h b/include/world.h new file mode 100644 index 0000000..5e77417 --- /dev/null +++ b/include/world.h @@ -0,0 +1,22 @@ +// world.h +#ifndef WORLD_H +#define WORLD_H + +#include "chunkStructures.h" + +#define WORLD_SIZE_X 16 +#define WORLD_SIZE_Z 16 + +// World is currently a flat grid of chunks on the X-Z plane +typedef struct { + Chunk *chunks[WORLD_SIZE_X][WORLD_SIZE_Z]; +} World; + +void InitWorld(World *world); +void FreeWorld(World *world); +void UpdateWorld(World *world); +Chunk *GetChunk(World *world, int chunkX, int chunkZ); + +Chunk *GetChunkContainingBlock(World *world, int wx, int wz); + +#endif diff --git a/source/chunkGenerator.c b/source/chunkGenerator.c index 7afcf3f..3275771 100644 --- a/source/chunkGenerator.c +++ b/source/chunkGenerator.c @@ -5,7 +5,7 @@ #include "chunkGenerator.h" #include "blockTypes.h" -// Fill a chunk with normalish Minecraft style flatworld terrain. A few layers of stone on the bottom, a few layers of dirt, and a layer of grass on top. +// Fill a chunk with normalish Minecraft style flatworld terrain. Some layers of stone on the bottom, a few layers of dirt, and a layer of grass on top. void GenerateFlatChunk(Chunk *chunk) { for (int x = 0; x < CHUNK_SIZE_X; x++) { for (int z = 0; z < CHUNK_SIZE_Z; z++) { diff --git a/source/chunkIO.c b/source/chunkIO.c new file mode 100644 index 0000000..711a8f8 --- /dev/null +++ b/source/chunkIO.c @@ -0,0 +1,30 @@ +// chunkIO.c +#include +#include +#include +#include +#include "chunkIO.h" + +bool SaveChunk(const Chunk *chunk) { + char filename[128]; + snprintf(filename, sizeof(filename), "saves/chunk-%d-%d.dat", chunk->x, chunk->z); + + FILE *file = fopen(filename, "wb"); + if (!file) return false; + + fwrite(chunk->blocks, sizeof(Block), CHUNK_SIZE_X * CHUNK_SIZE_Y * CHUNK_SIZE_Z, file); + fclose(file); + return true; +} + +bool LoadChunk(Chunk *chunk) { + char filename[128]; + snprintf(filename, sizeof(filename), "saves/chunk-%d-%d.dat", chunk->x, chunk->z); + + FILE *file = fopen(filename, "rb"); + if (!file) return false; + + fread(chunk->blocks, sizeof(Block), CHUNK_SIZE_X * CHUNK_SIZE_Y * CHUNK_SIZE_Z, file); + fclose(file); + return true; +} diff --git a/source/chunkStructures.c b/source/chunkStructures.c index 0b11b70..147f35b 100644 --- a/source/chunkStructures.c +++ b/source/chunkStructures.c @@ -10,20 +10,18 @@ #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; +void InitChunk(Chunk *chunk, int chunkX, int chunkZ) { + chunk->mesh = (Mesh){ 0 }; + chunk->x = chunkX; + chunk->z = chunkZ; + // Zero out block data + for (int x = 0; x < CHUNK_SIZE_X; x++) { + for (int y = 0; y < CHUNK_SIZE_Y; y++) { + for (int z = 0; z < CHUNK_SIZE_Z; z++) { + chunk->blocks[x][y][z] = (Block){ .type = 0 }; + } + } + } } // Places a tree at the position specified by x, y, z diff --git a/source/playerController.c b/source/playerController.c index 1d4c5dd..e5e8fe8 100644 --- a/source/playerController.c +++ b/source/playerController.c @@ -3,8 +3,13 @@ #include "raymath.h" #include "playerController.h" #include "blockTypes.h" +#include "world.h" #include +inline int FloorDiv(int a, int b) { + return (a >= 0) ? (a / b) : ((a - b + 1) / b); +} + void UpdatePlayer(Player *player) { Vector2 mouseDelta = GetMouseDelta(); player->playerOrientation.x += mouseDelta.x * -0.002f; @@ -45,108 +50,84 @@ void UpdatePlayer(Player *player) { 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 -RaycastHit RaycastChunk(const Chunk *chunk, Vector3 origin, Vector3 direction, float maxDistance) { - RaycastHit result = {0}; // Initialize result: no hit, zeroed values +RaycastHit GetPlayerRaycastHit(Player *player, World *world, float maxDistance) { + RaycastHit result = {0}; + Vector3 origin = Vector3Add(player->camera.position, Vector3Scale(player->forward, 0.001f)); + Vector3 dir = Vector3Normalize(player->forward); - direction = Vector3Normalize(direction); // Ensure direction is a unit vector - - // Nudge the origin slightly to avoid precision issues at block boundaries - origin = Vector3Add(origin, Vector3Scale(direction, 0.001f)); - - // Determine the next voxel boundary to cross along each axis - float nextX = (direction.x >= 0) ? ceilf(origin.x) : floorf(origin.x); - float nextY = (direction.y >= 0) ? ceilf(origin.y) : floorf(origin.y); - float nextZ = (direction.z >= 0) ? ceilf(origin.z) : floorf(origin.z); - - // Get integer voxel coordinates for current position int x = (int)floorf(origin.x); int y = (int)floorf(origin.y); int z = (int)floorf(origin.z); - // Determine step direction: +1 or -1 along each axis - int stepX = (direction.x >= 0) ? 1 : -1; - int stepY = (direction.y >= 0) ? 1 : -1; - int stepZ = (direction.z >= 0) ? 1 : -1; + int stepX = (dir.x > 0) ? 1 : -1; + int stepY = (dir.y > 0) ? 1 : -1; + int stepZ = (dir.z > 0) ? 1 : -1; - // How far to travel along the ray to cross a voxel in each axis - float tDeltaX = (direction.x == 0) ? INFINITY : fabsf(1.0f / direction.x); - float tDeltaY = (direction.y == 0) ? INFINITY : fabsf(1.0f / direction.y); - float tDeltaZ = (direction.z == 0) ? INFINITY : fabsf(1.0f / direction.z); + float tMaxX = ((stepX > 0 ? (x + 1) : x) - origin.x) / dir.x; + float tMaxY = ((stepY > 0 ? (y + 1) : y) - origin.y) / dir.y; + float tMaxZ = ((stepZ > 0 ? (z + 1) : z) - origin.z) / dir.z; - // Distance from origin to the first voxel boundary (for each axis) - float tMaxX = (direction.x == 0) ? INFINITY : (nextX - origin.x) / direction.x; - float tMaxY = (direction.y == 0) ? INFINITY : (nextY - origin.y) / direction.y; - float tMaxZ = (direction.z == 0) ? INFINITY : (nextZ - origin.z) / direction.z; + float tDeltaX = fabsf(1.0f / dir.x); + float tDeltaY = fabsf(1.0f / dir.y); + float tDeltaZ = fabsf(1.0f / dir.z); - float t = 0.0f; // Total traveled distance along the ray - Vector3 lastNormal = {0}; // Which face was entered (used for highlighting/interactions) + float t = 0.0f; + Vector3 lastNormal = {0}; - // Walk the ray through the voxel grid until we exceed maxDistance - while (t < maxDistance) { - // Check if the current voxel is inside the chunk bounds - if (x >= 0 && y >= 0 && z >= 0 && - x < CHUNK_SIZE_X && y < CHUNK_SIZE_Y && z < CHUNK_SIZE_Z) { + for (int i = 0; i < (int)(maxDistance * 3); i++) { + int chunkX = FloorDiv(x, CHUNK_SIZE_X); + int chunkZ = FloorDiv(z, CHUNK_SIZE_Z); + Chunk *chunk = GetChunk(world, chunkX, chunkZ); - int blockID = chunk->blocks[x][y][z].type; + if (chunk) { + int localX = x - chunkX * CHUNK_SIZE_X; + int localY = y; + int localZ = z - chunkZ * CHUNK_SIZE_Z; - // If it's not air, we hit something! - if (blockID != BLOCK_AIR) { - result.hit = true; - result.blockID = blockID; - result.position = (Vector3){x, y, z}; - result.normal = lastNormal; - result.t = t; - return result; + if (localX >= 0 && localX < CHUNK_SIZE_X && + localY >= 0 && localY < CHUNK_SIZE_Y && + localZ >= 0 && localZ < CHUNK_SIZE_Z) { + + Block block = chunk->blocks[localX][localY][localZ]; + if (block.type != BLOCK_AIR) { + result.hit = true; + result.hitBlockX = x; + result.hitBlockY = y; + result.hitBlockZ = z; + result.normal = lastNormal; + result.chunkX = chunkX; + result.chunkZ = chunkZ; + result.t = t; + + // For visual debugging + result.position = Vector3Add((Vector3){x, y, z}, Vector3Scale(lastNormal, 0.5f)); + + return result; + } + } } - } - // Move to the next voxel along the smallest tMax (i.e., the closest boundary) - if (tMaxX < tMaxY && tMaxX < tMaxZ) { - x += stepX; - t = tMaxX; - tMaxX += tDeltaX; - lastNormal = (Vector3){-stepX, 0, 0}; // Normal points opposite the ray step - } else if (tMaxY < tMaxZ) { - y += stepY; - t = tMaxY; - tMaxY += tDeltaY; - lastNormal = (Vector3){0, -stepY, 0}; - } else { - z += stepZ; - t = tMaxZ; - tMaxZ += tDeltaZ; - lastNormal = (Vector3){0, 0, -stepZ}; - } + // DDA step + if (tMaxX < tMaxY && tMaxX < tMaxZ) { + x += stepX; + t = tMaxX; + tMaxX += tDeltaX; + lastNormal = (Vector3){-stepX, 0, 0}; + } else if (tMaxY < tMaxZ) { + y += stepY; + t = tMaxY; + tMaxY += tDeltaY; + lastNormal = (Vector3){0, -stepY, 0}; + } else { + z += stepZ; + t = tMaxZ; + tMaxZ += tDeltaZ; + lastNormal = (Vector3){0, 0, -stepZ}; + } + + if (t > maxDistance) break; } - // If no block was hit, return default (no hit) return result; } - -RaycastHit GetPlayerRaycastHit(Player *player, Chunk *chunk, float maxDistance) { - Ray ray = { - .position = player->mapPosition, - .direction = Vector3Normalize(player->forward) - }; - 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 566521e..3a463fb 100644 --- a/source/voxelThing.c +++ b/source/voxelThing.c @@ -9,24 +9,19 @@ #include "blockTypes.h" #include "playerController.h" #include "chunkGenerator.h" +#include "chunkIO.h" +#include "world.h" int main(void) { // --- Screen setup --- SetConfigFlags(FLAG_WINDOW_RESIZABLE); - InitWindow(800, 600, "VoxelThing"); + InitWindow(800, 600, "voxelThing"); SetExitKey(-1); 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, 64, 8); - SaveChunk(&chunk, "saves/chunk_0_0_0.dat"); - } - Mesh chunkMesh = GenerateChunkMesh(&chunk); + World world; + InitWorld(&world); // --- Load textures and materials --- Texture2D atlas = LoadTexture("assets/TextureAtlas.png"); @@ -48,7 +43,7 @@ int main(void) { bool paused = false; RaycastHit hit; - int blockSelection = BLOCK_SAND; + int blockSelection = BLOCK_STONE; while (!WindowShouldClose()) { int screenWidth = GetScreenWidth(); @@ -78,35 +73,88 @@ int main(void) { if (!paused) { UpdatePlayer(&player); - hit = GetPlayerRaycastHit(&player, &chunk, 10.0f); - HandleBlockInteraction(&player, &chunk, hit, blockSelection); + hit = GetPlayerRaycastHit(&player, &world, 10.0f); - if (hit.hit && (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) || IsMouseButtonPressed(MOUSE_RIGHT_BUTTON))) { - UnloadMesh(chunkMesh); - chunkMesh = GenerateChunkMesh(&chunk); + + if (hit.hit) { + int bx = hit.hitBlockX; + int by = hit.hitBlockY; + int bz = hit.hitBlockZ; + + int placeX = bx + (int)hit.normal.x; + int placeY = by + (int)hit.normal.y; + int placeZ = bz + (int)hit.normal.z; + + // Handle removal + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + Chunk *targetChunk = GetChunkContainingBlock(&world, bx, bz); + if (targetChunk) { + int lx = bx - targetChunk->x * CHUNK_SIZE_X; + int lz = bz - targetChunk->z * CHUNK_SIZE_Z; + + if (lx >= 0 && lx < CHUNK_SIZE_X && lz >= 0 && lz < CHUNK_SIZE_Z && + by >= 0 && by < CHUNK_SIZE_Y) { + targetChunk->blocks[lx][by][lz].type = BLOCK_AIR; + UnloadMesh(targetChunk->mesh); + targetChunk->mesh = GenerateChunkMesh(targetChunk); + // Mark chunk as changed. + targetChunk->hasChanged = true; + } + } + } + + // Handle placement + if (IsMouseButtonPressed(MOUSE_RIGHT_BUTTON)) { + Chunk *targetChunk = GetChunkContainingBlock(&world, placeX, placeZ); + if (targetChunk) { + int lx = placeX - targetChunk->x * CHUNK_SIZE_X; + int lz = placeZ - targetChunk->z * CHUNK_SIZE_Z; + + if (lx >= 0 && lx < CHUNK_SIZE_X && lz >= 0 && lz < CHUNK_SIZE_Z && + placeY >= 0 && placeY < CHUNK_SIZE_Y) { + targetChunk->blocks[lx][placeY][lz].type = blockSelection; + UnloadMesh(targetChunk->mesh); + targetChunk->mesh = GenerateChunkMesh(targetChunk); + // Mark chunk as changed. + targetChunk->hasChanged = true; + + } + } + } } + + BeginDrawing(); + ClearBackground(RAYWHITE); + + BeginMode3D(player.camera); + //DrawMesh(chunkMesh, mat, MatrixIdentity()); + for (int x = 0; x < WORLD_SIZE_X; x++) { + for (int z = 0; z < WORLD_SIZE_Z; z++) { + Chunk *chunk = world.chunks[x][z]; + if (chunk && chunk->mesh.vertexCount > 0) { + DrawMesh(chunk->mesh, mat, MatrixTranslate(x * CHUNK_SIZE_X, 0, z * CHUNK_SIZE_Z)); + } + } + } + + if (hit.hit) { + Vector3 mid = Vector3Add((Vector3){hit.hitBlockX, hit.hitBlockY, hit.hitBlockZ}, + (Vector3){0.5f, 0.5f, 0.5f}); + DrawCubeWires(mid, 1.02f, 1.02f, 1.02f, BLACK); + //DrawLine3D(mid, Vector3Add(mid, hit.normal), RED); // Normal direction + } + + 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); + } - 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); @@ -117,9 +165,8 @@ int main(void) { } // --- Cleanup --- - SaveChunk(&chunk, "saves/chunk_0_0_0.dat"); UnloadTexture(atlas); - UnloadMesh(chunkMesh); + FreeWorld(&world); UnloadMaterial(mat); CloseWindow(); return 0; diff --git a/source/world.c b/source/world.c new file mode 100644 index 0000000..ebbb81b --- /dev/null +++ b/source/world.c @@ -0,0 +1,60 @@ +// world.c +#include +#include "world.h" +#include "chunkGenerator.h" +#include "chunkRenderer.h" +#include "chunkStructures.h" +#include "chunkIO.h" +#include + +void InitWorld(World *world) { + for (int x = 0; x < WORLD_SIZE_X; x++) { + for (int z = 0; z < WORLD_SIZE_Z; z++) { + world->chunks[x][z] = malloc(sizeof(Chunk)); + Chunk *chunk = world->chunks[x][z]; + InitChunk(chunk, x, z); + + if (!LoadChunk(chunk)) { + // Later + //GenerateChunkTerrain(chunk); + GenerateFlatChunk(chunk); + SaveChunk(chunk); + } + + chunk->mesh = GenerateChunkMesh(chunk); + } + } +} + +void FreeWorld(World *world) { + for (int x = 0; x < WORLD_SIZE_X; x++) { + for (int z = 0; z < WORLD_SIZE_Z; z++) { + Chunk *chunk = world->chunks[x][z]; + if(chunk->hasChanged) SaveChunk(chunk); + if (chunk) { + UnloadMesh(chunk->mesh); + free(chunk); + } + } + } +} + +Chunk *GetChunkContainingBlock(World *world, int wx, int wz) { + int chunkX = wx / CHUNK_SIZE_X; + int chunkZ = wz / CHUNK_SIZE_Z; + if (wx < 0) chunkX--; + if (wz < 0) chunkZ--; + return GetChunk(world, chunkX, chunkZ); +} + + +void UpdateWorld(World *world) { + // For now, stub function. Will handle streaming later. +} + +Chunk *GetChunk(World *world, int chunkX, int chunkZ) { + if (chunkX >= 0 && chunkX < WORLD_SIZE_X && chunkZ >= 0 && chunkZ < WORLD_SIZE_Z) { + return world->chunks[chunkX][chunkZ]; + } + return NULL; +}