[Game Development] Minicraft - C++ & Procedural Generation

5.9k words

Overview

For this school project, I worked on expanding a minimalist C++ game engine, implementing core features such as 3D rendering, physics, gameplay mechanics, and procedural generation. The goal was to develop a voxel-based world—similar to Minecraft—while addressing challenges related to performance, scene management, and player interaction.

Key Features

Chunk Management & Optimization

  • Implemented chunk-based world rendering to optimize memory usage and performance.
  • Used Vertex Buffer Objects (VBOs) to efficiently render large voxel landscapes.
  • Developed a system to dynamically load and unload chunks based on player position.
chunk-management
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void MWorld::render_world_vbo(bool debug, bool doTransparent) {
glDisable(GL_BLEND);

// Rendu des chunks opaques
for (int x = 0; x < MAT_SIZE; x++)
for (int y = 0; y < MAT_SIZE; y++)
for (int z = MAT_HEIGHT - 1; z >= 0; z--) {
glPushMatrix();
glTranslatef((float)(x * MChunk::CHUNK_SIZE * MCube::CUBE_SIZE),
(float)(y * MChunk::CHUNK_SIZE * MCube::CUBE_SIZE),
(float)(z * MChunk::CHUNK_SIZE * MCube::CUBE_SIZE));
YRenderer::getInstance()->updateMatricesFromOgl();
YRenderer::getInstance()->sendMatricesToShader(YRenderer::CURRENT_SHADER);
Chunks[x][y][z]->render(false);
glPopMatrix();
}

glEnable(GL_BLEND);

// Rendu des chunks transparents
if (doTransparent) {
for (int x = 0; x < MAT_SIZE; x++)
for (int y = 0; y < MAT_SIZE; y++)
for (int z = 0; z < MAT_HEIGHT; z++) {
glPushMatrix();
glTranslatef((float)(x * MChunk::CHUNK_SIZE * MCube::CUBE_SIZE),
(float)(y * MChunk::CHUNK_SIZE * MCube::CUBE_SIZE),
(float)(z * MChunk::CHUNK_SIZE * MCube::CUBE_SIZE));
YRenderer::getInstance()->updateMatricesFromOgl();
YRenderer::getInstance()->sendMatricesToShader(YRenderer::CURRENT_SHADER);
Chunks[x][y][z]->render(true);
glPopMatrix();
}
}

glDepthMask(true);
}

Procedural Terrain Generation

  • Utilized Perlin noise to generate natural-looking landscapes with varying elevations.
  • Implemented biome simulation to create diverse environments (e.g., grass, sand, water).
  • Added height-based penalties to control terrain features like mountains and valleys.
terrain-generation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void MWorld::init_world(int seed) {
srand(seed);
Perlin.updateVecs();
Perlin.setZDecay(MWorld::MAT_HEIGHT_CUBES - 5, 0.5f);

for (int x = 0; x < MAT_SIZE_CUBES; x++)
for (int y = 0; y < MAT_SIZE_CUBES; y++)
for (int z = 0; z < MAT_HEIGHT_CUBES; z++) {
Perlin.DoPenaltyMiddle = true;
Perlin.setFreq(0.04f);
float val = Perlin.sample((float)x, (float)y, (float)z);
Perlin.DoPenaltyMiddle = false;
Perlin.setFreq(0.2f);
val -= (1.0f - max(val, Perlin.sample((float)x, (float)y, (float)z))) / 20.0f;

MCube* cube = getCube(x, y, z);

if (val > 0.5f) cube->setType(MCube::CUBE_HERBE);
if (val > 0.51f) cube->setType(MCube::CUBE_TERRE);
if (val > 0.55) cube->setType(MCube::CUBE_SABLE_01);
if (val < 0.5 && z <= 0.1) cube->setType(MCube::CUBE_EAU);
if (val > 0.48f && val < 0.5f && z <= 0.1) cube->setType(MCube::CUBE_SABLE_01);
if (val > 0.56) cube->setType(MCube::CUBE_EAU);
}
}

Camera & Player Controls

  • Developed a first-person camera system with smooth movement and rotation.
  • Integrated player controls for seamless navigation through the voxel environment.
  • Implemented mouse look for intuitive camera control and keyboard input for movement.
player-controls
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void MAvatar::update(float elapsed) {
YVec3f Force = YVec3f();

if (!CreativeMode)
Force += Gravity; // Apply gravity

YVec3f camDirection = Cam->Direction;
camDirection.Z = 0;

if (avance) Force += camDirection * PlayerSpeed; // Move forward
if (recule) Force -= camDirection * PlayerSpeed; // Move backward
if (gauche) Force -= Cam->RightVec * PlayerSpeed; // Strafe left
if (droite) Force += Cam->RightVec * PlayerSpeed; // Strafe right

if (Jump && Standing) {
Jump = false;
Force += YVec3f(0, 0, 5000); // Jump
}

Speed += Force * elapsed;
Speed.X *= 0.95; // Friction
Speed.Y *= 0.95;

YVec3f tempPosition = Position + Speed * elapsed;

// Handle collisions
float colMin;
MWorld::MAxis axis = World->getMinCol(tempPosition, Width / 2, Height / 2, colMin);
switch (axis) {
case MWorld::AXIS_X: Speed.X = 0; break;
case MWorld::AXIS_Y: Speed.Y = 0; break;
case MWorld::AXIS_Z: Speed.Z = 0; Standing = true; break;
}

Position = tempPosition;
Cam->moveTo(Position + YVec3f(0, 0, CurrentHeight / 2));
}

Physics & Collision Detection

  • Developed a voxel-based collision system to handle interactions between the player and the environment.
  • Implemented axis-aligned bounding boxes (AABB) for efficient collision detection.
  • [WIP] Added face-based collision handling to improve accuracy and prevent clipping through blocks.
collision-detection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
MAxis MWorld::getMinCol(YVec3f pos, float width, float height, float& valueColMin, bool oneShot = true) {
int x = (int)(pos.X / MCube::CUBE_SIZE);
int y = (int)(pos.Y / MCube::CUBE_SIZE);
int z = (int)(pos.Z / MCube::CUBE_SIZE);

// Vérification des collisions sur chaque axe
MAxis axis = 0x00;
valueColMin = oneShot ? 0.5f : 10000.0f;

// Collisions sur l'axe X
if (getCube(xPrev, yPrev, zPrev)->isSolid() ||
getCube(xPrev, yPrev, zNext)->isSolid() ||
getCube(xPrev, yNext, zPrev)->isSolid() ||
getCube(xPrev, yNext, zNext)->isSolid()) {
float depassement = ((xPrev + 1) * MCube::CUBE_SIZE) - (pos.X - width / 2.0f);
if (abs(depassement) < abs(valueColMin)) {
valueColMin = depassement;
axis = AXIS_X;
}
}

return axis;
}

Dynamic Environment & Gameplay

  • Created a day-night cycle, with sun and moon movement changing dynamically over time.
night-day-cycle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void updateLights(float boostTime = 0) {
// Calcul de la direction du soleil en fonction de l'heure
bool nuit = getSunDirFromDayTime(SunDirection, 6.0f * 60.0f, 19.0f * 60.0f, boostTime);
SunPosition = Renderer->Camera->Position + SunDirection * 500.0f;

// Définition des couleurs en fonction du jour ou de la nuit
if (!nuit) {
SunColor = YColor(1.0f, 1.0f, 0.8f, 1.0f); // Couleur du jour
SkyColor = YColor(0.0f, 181.f / 255.f, 221.f / 255.f, 1.0f); // Ciel bleu
} else {
SunColor = YColor(1, 1, 1, 1); // Lune blanche
SkyColor = YColor(0, 0, 0, 1); // Ciel noir
}

// Application de la couleur de fond
Renderer->setBackgroundColor(SkyColor);
}