Added player struct. Multiple chunks, and player interaction updates to support them.

This commit is contained in:
Jake 2025-06-03 21:47:35 -04:00
parent 77cba9260a
commit 06b9a621ef
12 changed files with 303 additions and 148 deletions

View File

@ -1,7 +1,7 @@
# voxelThing # voxelThing
Jake's Raylib Minecraft Clone 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. Code is rough and messy and not much is implemented yet.

View File

@ -1,6 +1,6 @@
// blockTypes.h // blockTypes.h
#ifndef BLOCK_TYPES_H #ifndef BLOCK_TYPES_H
#define BLOCKTYPES_H #define BLOCK_TYPES_H
// Definitions for Block IDs (notes are texture atlas indicies) // Definitions for Block IDs (notes are texture atlas indicies)
#define BLOCK_AIR 0 // No texture. #define BLOCK_AIR 0 // No texture.

11
include/chunkIO.h Normal file
View File

@ -0,0 +1,11 @@
// chunkIO.h
#ifndef CHUNK_IO_H
#define CHUNK_IO_H
#include <stdbool.h>
#include "chunkStructures.h"
bool SaveChunk(const Chunk *chunk);
bool LoadChunk(Chunk *chunk);
#endif

View File

@ -18,6 +18,9 @@ typedef struct {
Block blocks[CHUNK_SIZE_X][CHUNK_SIZE_Y][CHUNK_SIZE_Z]; Block blocks[CHUNK_SIZE_X][CHUNK_SIZE_Y][CHUNK_SIZE_Z];
Mesh mesh; // Owned by the chunk, valid only at runtime Mesh mesh; // Owned by the chunk, valid only at runtime
bool hasMesh; // bool hasMesh; //
bool hasChanged; // Flag that determines if chunk needs to be saved on unload
int x;
int z;
} Chunk; } Chunk;
// 6 directions for checking neighbors: +/-X, +/-Y, +/-Z // 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. // Function that places a tree dumbly.
void PlaceTreeAt(Chunk *chunk, int x, int y, int z) ; void PlaceTreeAt(Chunk *chunk, int x, int y, int z) ;
// Save chunk to disk. void InitChunk(Chunk *chunk, int chunkX, int chunkZ);
bool SaveChunk(const Chunk *chunk, const char *filename);
// Load chunk from disk.
bool LoadChunk(Chunk *chunk, const char *filename);
#endif #endif

View File

@ -4,6 +4,7 @@
#include "raylib.h" #include "raylib.h"
#include "chunkStructures.h" #include "chunkStructures.h"
#include "world.h"
typedef struct { typedef struct {
Vector3 mapPosition; // Player's world position (camera position) Vector3 mapPosition; // Player's world position (camera position)
@ -19,14 +20,20 @@ void UpdatePlayer(Player *player);
typedef struct { typedef struct {
bool hit; bool hit;
Vector3 position; Vector3 position;
int hitBlockX, hitBlockY, hitBlockZ;
Vector3 normal; Vector3 normal;
int blockID; int blockID;
float t; float t;
int chunkX;
int chunkZ;
} RaycastHit; } RaycastHit;
RaycastHit RaycastChunk(const Chunk *chunk, Vector3 origin, Vector3 direction, float maxDistance); 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); void UpdateFreeCamera(Camera3D *cam, float speed, float *yawOut, float *pitchOut);

22
include/world.h Normal file
View File

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

View File

@ -5,7 +5,7 @@
#include "chunkGenerator.h" #include "chunkGenerator.h"
#include "blockTypes.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) { void GenerateFlatChunk(Chunk *chunk) {
for (int x = 0; x < CHUNK_SIZE_X; x++) { for (int x = 0; x < CHUNK_SIZE_X; x++) {
for (int z = 0; z < CHUNK_SIZE_Z; z++) { for (int z = 0; z < CHUNK_SIZE_Z; z++) {

30
source/chunkIO.c Normal file
View File

@ -0,0 +1,30 @@
// chunkIO.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#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;
}

View File

@ -10,20 +10,18 @@
#include <string.h> #include <string.h>
#include <stdio.h> #include <stdio.h>
bool SaveChunk(const Chunk *chunk, const char *filename) { void InitChunk(Chunk *chunk, int chunkX, int chunkZ) {
FILE *fp = fopen(filename, "wb"); chunk->mesh = (Mesh){ 0 };
if (!fp) return false; chunk->x = chunkX;
size_t written = fwrite(chunk->blocks, sizeof(Block), CHUNK_SIZE_X * CHUNK_SIZE_Y * CHUNK_SIZE_Z, fp); chunk->z = chunkZ;
fclose(fp); // Zero out block data
return written == CHUNK_SIZE_X * CHUNK_SIZE_Y * CHUNK_SIZE_Z; 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 };
}
}
} }
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 // Places a tree at the position specified by x, y, z

View File

@ -3,8 +3,13 @@
#include "raymath.h" #include "raymath.h"
#include "playerController.h" #include "playerController.h"
#include "blockTypes.h" #include "blockTypes.h"
#include "world.h"
#include <math.h> #include <math.h>
inline int FloorDiv(int a, int b) {
return (a >= 0) ? (a / b) : ((a - b + 1) / b);
}
void UpdatePlayer(Player *player) { void UpdatePlayer(Player *player) {
Vector2 mouseDelta = GetMouseDelta(); Vector2 mouseDelta = GetMouseDelta();
player->playerOrientation.x += mouseDelta.x * -0.002f; player->playerOrientation.x += mouseDelta.x * -0.002f;
@ -45,68 +50,69 @@ void UpdatePlayer(Player *player) {
player->camera.target = Vector3Add(player->mapPosition, player->forward); 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 GetPlayerRaycastHit(Player *player, World *world, float maxDistance) {
RaycastHit RaycastChunk(const Chunk *chunk, Vector3 origin, Vector3 direction, float maxDistance) { RaycastHit result = {0};
RaycastHit result = {0}; // Initialize result: no hit, zeroed values 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 x = (int)floorf(origin.x);
int y = (int)floorf(origin.y); int y = (int)floorf(origin.y);
int z = (int)floorf(origin.z); int z = (int)floorf(origin.z);
// Determine step direction: +1 or -1 along each axis int stepX = (dir.x > 0) ? 1 : -1;
int stepX = (direction.x >= 0) ? 1 : -1; int stepY = (dir.y > 0) ? 1 : -1;
int stepY = (direction.y >= 0) ? 1 : -1; int stepZ = (dir.z > 0) ? 1 : -1;
int stepZ = (direction.z >= 0) ? 1 : -1;
// How far to travel along the ray to cross a voxel in each axis float tMaxX = ((stepX > 0 ? (x + 1) : x) - origin.x) / dir.x;
float tDeltaX = (direction.x == 0) ? INFINITY : fabsf(1.0f / direction.x); float tMaxY = ((stepY > 0 ? (y + 1) : y) - origin.y) / dir.y;
float tDeltaY = (direction.y == 0) ? INFINITY : fabsf(1.0f / direction.y); float tMaxZ = ((stepZ > 0 ? (z + 1) : z) - origin.z) / dir.z;
float tDeltaZ = (direction.z == 0) ? INFINITY : fabsf(1.0f / direction.z);
// Distance from origin to the first voxel boundary (for each axis) float tDeltaX = fabsf(1.0f / dir.x);
float tMaxX = (direction.x == 0) ? INFINITY : (nextX - origin.x) / direction.x; float tDeltaY = fabsf(1.0f / dir.y);
float tMaxY = (direction.y == 0) ? INFINITY : (nextY - origin.y) / direction.y; float tDeltaZ = fabsf(1.0f / dir.z);
float tMaxZ = (direction.z == 0) ? INFINITY : (nextZ - origin.z) / direction.z;
float t = 0.0f; // Total traveled distance along the ray float t = 0.0f;
Vector3 lastNormal = {0}; // Which face was entered (used for highlighting/interactions) Vector3 lastNormal = {0};
// Walk the ray through the voxel grid until we exceed maxDistance for (int i = 0; i < (int)(maxDistance * 3); i++) {
while (t < maxDistance) { int chunkX = FloorDiv(x, CHUNK_SIZE_X);
// Check if the current voxel is inside the chunk bounds int chunkZ = FloorDiv(z, CHUNK_SIZE_Z);
if (x >= 0 && y >= 0 && z >= 0 && Chunk *chunk = GetChunk(world, chunkX, chunkZ);
x < CHUNK_SIZE_X && y < CHUNK_SIZE_Y && z < CHUNK_SIZE_Z) {
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 (localX >= 0 && localX < CHUNK_SIZE_X &&
if (blockID != BLOCK_AIR) { 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.hit = true;
result.blockID = blockID; result.hitBlockX = x;
result.position = (Vector3){x, y, z}; result.hitBlockY = y;
result.hitBlockZ = z;
result.normal = lastNormal; result.normal = lastNormal;
result.chunkX = chunkX;
result.chunkZ = chunkZ;
result.t = t; result.t = t;
// For visual debugging
result.position = Vector3Add((Vector3){x, y, z}, Vector3Scale(lastNormal, 0.5f));
return result; return result;
} }
} }
}
// Move to the next voxel along the smallest tMax (i.e., the closest boundary) // DDA step
if (tMaxX < tMaxY && tMaxX < tMaxZ) { if (tMaxX < tMaxY && tMaxX < tMaxZ) {
x += stepX; x += stepX;
t = tMaxX; t = tMaxX;
tMaxX += tDeltaX; tMaxX += tDeltaX;
lastNormal = (Vector3){-stepX, 0, 0}; // Normal points opposite the ray step lastNormal = (Vector3){-stepX, 0, 0};
} else if (tMaxY < tMaxZ) { } else if (tMaxY < tMaxZ) {
y += stepY; y += stepY;
t = tMaxY; t = tMaxY;
@ -118,35 +124,10 @@ RaycastHit RaycastChunk(const Chunk *chunk, Vector3 origin, Vector3 direction, f
tMaxZ += tDeltaZ; tMaxZ += tDeltaZ;
lastNormal = (Vector3){0, 0, -stepZ}; lastNormal = (Vector3){0, 0, -stepZ};
} }
if (t > maxDistance) break;
} }
// If no block was hit, return default (no hit)
return result; 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;
}
}
}

View File

@ -9,24 +9,19 @@
#include "blockTypes.h" #include "blockTypes.h"
#include "playerController.h" #include "playerController.h"
#include "chunkGenerator.h" #include "chunkGenerator.h"
#include "chunkIO.h"
#include "world.h"
int main(void) { int main(void) {
// --- Screen setup --- // --- Screen setup ---
SetConfigFlags(FLAG_WINDOW_RESIZABLE); SetConfigFlags(FLAG_WINDOW_RESIZABLE);
InitWindow(800, 600, "VoxelThing"); InitWindow(800, 600, "voxelThing");
SetExitKey(-1); SetExitKey(-1);
DisableCursor(); DisableCursor();
// --- World generation --- // --- World generation ---
Chunk chunk; World world;
if (!LoadChunk(&chunk, "saves/chunk_0_0_0.dat")) { InitWorld(&world);
// 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 --- // --- Load textures and materials ---
Texture2D atlas = LoadTexture("assets/TextureAtlas.png"); Texture2D atlas = LoadTexture("assets/TextureAtlas.png");
@ -48,7 +43,7 @@ int main(void) {
bool paused = false; bool paused = false;
RaycastHit hit; RaycastHit hit;
int blockSelection = BLOCK_SAND; int blockSelection = BLOCK_STONE;
while (!WindowShouldClose()) { while (!WindowShouldClose()) {
int screenWidth = GetScreenWidth(); int screenWidth = GetScreenWidth();
@ -78,12 +73,53 @@ int main(void) {
if (!paused) { if (!paused) {
UpdatePlayer(&player); UpdatePlayer(&player);
hit = GetPlayerRaycastHit(&player, &chunk, 10.0f); hit = GetPlayerRaycastHit(&player, &world, 10.0f);
HandleBlockInteraction(&player, &chunk, hit, blockSelection);
if (hit.hit && (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) || IsMouseButtonPressed(MOUSE_RIGHT_BUTTON))) {
UnloadMesh(chunkMesh); if (hit.hit) {
chunkMesh = GenerateChunkMesh(&chunk); 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;
}
}
} }
} }
@ -91,11 +127,21 @@ int main(void) {
ClearBackground(RAYWHITE); ClearBackground(RAYWHITE);
BeginMode3D(player.camera); BeginMode3D(player.camera);
DrawMesh(chunkMesh, mat, MatrixIdentity()); //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) { if (hit.hit) {
DrawCubeWires(Vector3Add(hit.position, (Vector3){0.5f, 0.5f, 0.5f}), 1.02f, 1.02f, 1.02f, BLACK); Vector3 mid = Vector3Add((Vector3){hit.hitBlockX, hit.hitBlockY, hit.hitBlockZ},
//DrawFaceHighlight(hit.position, hit.normal); (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(); EndMode3D();
@ -107,6 +153,8 @@ int main(void) {
// Debug info // Debug info
DrawText(TextFormat("Yaw: %.1f Pitch: %.1f", player.playerOrientation.x * RAD2DEG, player.playerOrientation.y * RAD2DEG), 10, 10, 20, DARKGRAY); DrawText(TextFormat("Yaw: %.1f Pitch: %.1f", player.playerOrientation.x * RAD2DEG, player.playerOrientation.y * RAD2DEG), 10, 10, 20, DARKGRAY);
}
if (paused) { if (paused) {
DrawRectangle(0, 0, screenWidth, screenHeight, Fade(DARKGRAY, 0.5f)); DrawRectangle(0, 0, screenWidth, screenHeight, Fade(DARKGRAY, 0.5f));
DrawText("Paused", screenWidth / 2 - MeasureText("Paused", 40) / 2, screenHeight / 2 - 20, 40, RAYWHITE); DrawText("Paused", screenWidth / 2 - MeasureText("Paused", 40) / 2, screenHeight / 2 - 20, 40, RAYWHITE);
@ -117,9 +165,8 @@ int main(void) {
} }
// --- Cleanup --- // --- Cleanup ---
SaveChunk(&chunk, "saves/chunk_0_0_0.dat");
UnloadTexture(atlas); UnloadTexture(atlas);
UnloadMesh(chunkMesh); FreeWorld(&world);
UnloadMaterial(mat); UnloadMaterial(mat);
CloseWindow(); CloseWindow();
return 0; return 0;

60
source/world.c Normal file
View File

@ -0,0 +1,60 @@
// world.c
#include <stdlib.h>
#include "world.h"
#include "chunkGenerator.h"
#include "chunkRenderer.h"
#include "chunkStructures.h"
#include "chunkIO.h"
#include <stdio.h>
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;
}