Skip to content

Cocodayow/mini-minecraft

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

79 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Milestone 3

Rivers (Yikai Li)

River system extends the procedural terrain pipeline by carving continuous riverbeds across chunks and filling them with water blocks. When a terrain zone is loaded, the system consults a per-zone river cache; if no path exists, it generates one using an L-System–style curve combined with low-frequency noise to create natural meanders. L-systems are a simple rewriting grammar that repeatedly expands an initial symbol sequence into a longer one, where each symbol maps to a drawing command such as “move forward” or “turn.” I use 5 iteration for each river generation. This makes them ideal for producing organic, non-grid-aligned paths, allowing river curves to feel naturally formed.

During chunk population, any block column intersecting the river path is lowered and converted to a riverbed block, with a water block placed directly above for a continuous surface. Because the river path is defined at the zone level rather than per chunk, the waterline remains seamless across chunk boundaries. All modified chunks are flagged dirty so their VBOs are rebuilt immediately after carving. The river are restrictedly only generating in grass biomes, one way to better control the height of the water and another way to fit in the real world common sense. The bottom of the river is also sampled use noise function to make it more natural.

Post-process Camera Overlay (Yikai Li)

The post-processing overlay shader applies environment-driven visual effects on top of the fully rendered scene using a fullscreen fragment pass as described in milestone2. When the player enters water, the shader introduces a refraction-style distortion by offsetting the framebuffer UVs with sine-based time functions (u_Time). This produces the characteristic underwater wobble, with distortion strength increasing as the player moves deeper. The same pass also applies a lightweight distance blur by sampling neighboring pixels; this softens distant objects and simulates reduced visibility underwater without the cost of a full Gaussian blur.

Lava immersion uses a different treatment. Instead of blur and refraction, the shader overlays a fast, time-varying brightness modulation, creating a heat-flicker effect that makes the screen pulse subtly while the player is surrounded by lava(although now it looks like black stains). Both effects run inside a single GLSL pass, making them inexpensive and easy to extend. By combining distortion, animation, and conditional blending, the shader provides clear environmental feedback while keeping the post-processing pipeline compact and efficient.

Sound (Yikai Li)

The sound system I make centralizes all ambient and interaction audio through a dedicated SoundManager that maintains looping effects for water, lava, wind, and footsteps, along with one-shot sounds for actions such as block placement, removal. Each QSoundEffect instance is initialized with its audio source, loop behavior, and default volume. During gameplay, the sound manager receives updates on whether the player is underwater, in lava, or standing in a grass biome, and enables or disables the corresponding ambient loops, this ensures the audio environment reacts immediately to the player’s state.

For the wind sound, it will appear on a certain height, and with the height become higher, the wind sound will become louder to simulate the truly nature wind. For the bird sound, it will appear only in grass biome, and randomly played with some time interval, also since the river will only appear in grass biome, so these two sounds can be heard, and when you are in water, the water sound will play and the bird sound will exist but with smaller volume. On sand biome, the walk sound will be different with other biome's walk sound. For lava, the sound is like a boiling water, and also the place and remove block has its own sound effect. These small cues make movement and building feel more tactile without adding engine overhead.

Third-Person Mode (Yikai Li)

The third-person system introduces an independent rendering pass for the player model, allowing the camera to orbit behind the player while still using the standard first-person physics and collision logic. When third-person mode is active, the engine positions a separate camera offset behind the player and draws a block-based character mesh built from animated limbs. Walking and action animations are updated each frame through simple phase-based oscillation, and the mesh is rebuilt by the player model class before rendering. Because the model is drawn after the world geometry, its transforms remain independent of chunk rendering or the player’s collision body.

To ensure the model reflects the player's movement direction, the renderer computes a facing angle from the horizontal velocity via atan2(dir.x, dir.z) and applies a Y-axis rotation before drawing the mesh. Translation occurs first, followed by rotation, so the model turns around its own origin rather than orbiting around the world—a common pitfall when composing transforms.When standing still, the model preserves its last orientation; when walking, the limbs animate in sync with movement speed, and right-arm swing animations are triggered during block interactions.

To make it easier, I directly use the default lambert shader used to render the blocks to render the body, which leads to I have to follow the same interleaved vertex layout, which is 14-Float Vertex, which expects position, normal, color, and UV data for every vertex. I initially meant to render the model to be Steve-like colored, but the default lambert shader uses uv texturing. So to save time, I just select a color for the whole model , but I still leave the color implementation inside the model.cpp, so I think future shader upgrade shall be quite easy, and it is already modified using color(didn't expect it that easy to fix).

Inventory and onscreen GUI (Jingjia Peng)

I designed the inventory as an embedded viewport within the existing Player Information window rather than a separate popup, providing always-visible access to block selection. The system dynamically generates block buttons based on a centralized blockTextureInfo map that stores texture atlas coordinates and display names for each placeable block type. Each button displays a 64×64 pixel texture preview extracted directly from the Minecraft texture atlas, ensuring visual consistency between the UI and in-game blocks. Block quantities are rendered as overlays in the bottom-right corner of each icon using QPainter, with white text on a semi-transparent black background and a black outline for readability—the number turns red when quantity reaches zero. A QScrollArea wraps the block grid (4 columns) to enable vertical scrolling when the number of available blocks exceeds the viewport height.

The inventory connects to the Player class through three callback functions: canPlaceBlock checks if the selected block has sufficient quantity before placement, onBlockPlaced decrements inventory after successful placement, and onBlockRemoved increments inventory when a block is mined from the world. Buttons automatically disable when their quantity reaches zero, preventing selection of unavailable blocks, and re-enable when the player mines that block type. The selected block is highlighted with a golden border and cream background, providing clear visual feedback for the current selection. This architecture cleanly separates UI concerns (BlockSelector widget) from game logic (Player class) while maintaining real-time synchronization between displayed quantities and actual inventory state.

Crafting System (Jingjia Peng)

I added a drag-and-drop crafting bar under the inventory: players drag any two inventory blocks into the two input slots, immediately see the crafted output preview, and click the output to finalize crafting. On confirmation, the two inputs are decremented from the inventory and the crafted item is incremented, with all quantities mirrored in the existing block selector UI. Recipes cover both double-ore upgrades and mixed-material blocks (e.g., two gold/copper/crystal/ruby/obsidian ore → their refined form, sand + stone → sandbox, two wood → blackwood, wood + stone → bookshelf, copper ore + stone → toolbox, ruby ore + stone → stove, lava + stone → bomb), with availability gated by actual inventory counts so invalid or insufficient combinations never enable crafting.

A Rules dialog is reachable via the “Rules” button in PlayerInfo and visualizes every recipe with block icons (using the same atlas rendering as the selector) instead of text names. The crafting area uses the shared block atlas for consistent iconography, supports clearing a slot with a click, and keeps crafted outputs disabled when inputs don’t match a recipe or when quantities run out, ensuring a simple, Minecraft-like two-item crafting flow.

Bomb effect (Jingjia Peng)

The bomb system tracks placed BOMB blocks with a 5-second countdown timer. When a bomb is placed, it's registered in Terrain::m_activeBombs and a looping timer sound begins playing. Each game tick, updateBombs() decrements all active timers and checks whether each bomb block still exists—if a player removes the bomb before detonation, it's defused and removed from tracking. When the timer expires, explodeBomb() destroys blocks in a spherical pattern (radius 5) centered on the bomb position, with randomized edge destruction for a natural crater appearance. Bedrock is immune to explosions, and all affected chunks automatically rebuild their VBO data.

The sound system integrates seamlessly: the timer sound loops while any bomb is active and stops when no bombs remain (either exploded or defused), while the explosion sound plays once per detonation. This creates an tense gameplay mechanic where players must decide whether to defuse a placed bomb or flee before the explosion.

Additional Biomes (Jiawen Wang)

I implemented four biomes Snow, Desert, Grass, and Mountain assigned according to temperature and moisture noise fields. Each biome uses its own height generation function.

Grassland

varies more quickly, generating denser and more irregular terrain

High temperature + high moisture

Desert

broad, smooth low basin with minimum elevation changes

High temperature + low moisture

Mountains

steep elevation changes

Low temperature + low moisture

Snow

broad, smooth high plains with gentle elevation changes

Low temperature + high moisture

To avoid sharp biome boundaries and height jumps, the final terrain height is blended by bilinear interpolation. Each biome contributes a height value derived from its own noise function and these values are linearly interpolated based on temperature and moisture values. Temperature blends between the “cold” and “hot” sides of the biome grid, while moisture blends between the “dry” and “wet” sides.

Moisture (M)

Snow | Grass

----------------> Temperature (T)

Mount | Desert

Procedurally Placed Assets (Jiawen Wang)

Biome-specific assets include trees in Grass biomes and cacti in Desert biomes, each spawned probabilistically. Two categories of trees are implemented, “T-shaped” variant and a “crown-shaped”, each with randomized parameters such as trunk height, laef radius, and optional top leaf (like the cherry on cake). Cacti are generated using a simple noise threshold function: only locations where its noise exceeds a preset cutoff are eligible. They are restricted from spawning adjacent to another cactus by checking the four neighboring blocks, ensuring that cactus columns maintain proper desert spacing. Heights vary randomly, and each cactus occupies a single vertical block stack. Because they do not cross chunk boundaries, cacti can be safely generated during the initial terrain creation pass, directly inside the per-chunk block array.

Trees, however, a bit more challenging because they often span multiple blocks horizontally and vertically and may cross chunk boundaries. In earlier versions, terrain for each chunk was generated independently without awareness of its neighbors, causing trees placed near chunk edges to appear cut off or incomplete.

To resolve this, the terrain generation pipeline was redesigned to include a multithreaded population stage. Each chunk enters the population stage only after all chunks in its surrounding 3×3 neighborhood have completed terrain generation. This guarantees that terrain data across chunk boundaries is finalized. A population queue and population workers handle these tasks asynchronously. The same neighborhood requirement also applies before constructing geometry: VBO generation is deferred until all chunks in the 3×3 region have completed both terrain and population phases.

Procedural grass color (Jiawen Wang)

Grass color is procedurally determined by blending between preset color endpoints based on temperature and moisture noise fields. I use linear interpolation to mix between dry yellow-green in hot areas (desert), vibrant green as the uninfluenced original grass color, and darker cool greens in colder climates (snow). On the rendering side, I modified the Lambert fragment shader so that grass blocks sample this procedurally computed color instead of relying entirely on their base texture. When the shader detects a grass top face via its UV coordinates, it blends the original texture with the biome determined color, creating a more natural variation across the landscape. We don't need to check neighboring chunks for biome data - each block computes its own color locally using the global noise fields.

Distance fog (Jiawen Wang)

As the camera moves through the scene, nearby blocks are rendered normally while distant geometry gradually blends into the sky color. Each fragment computes its distance from the camera, maps that distance into a fog factor, and linearly interpolates between its shaded color and the sky.

Milestone 2

Cave Systems (Yikai Li)

Cave System Generation: Implemented fully volumetric caves using 3D Perlin noise sampled across Y ∈ (1,128).Each block is evaluated with an eight-surflet trilinear noise function; negative values produce cave air while positive values remain stone. To meet the milestone requirement for deep lava pools, all cave-air blocks with y < 25 are converted to LAVA, and y == 0 is always set to BEDROCK. I applied non-uniform scaling to the noise—higher frequency in Y than in XZ—to avoid spherical noise artifacts and instead generate more tunnel-like vertical variation. Cave evaluation is done before applying biome or surface rules, allowing caves to carve naturally through dirt layers, grass surfaces, stone strata, and lake basins without disrupting the high-level terrain structure. The main challenges included balancing cave density and vertical shape, as uniform scaling produced round bubble-like voids rather than smooth tunnels. Restricting lava to depths below 25 addressed earlier issues with scattered mid-level lava pockets.

Water and Lava Interaction Added fluid-state detection directly inside the player update loop. By sampling the block at the player’s head position each frame, the system determines whether the player is inside WATER or LAVA. Water reduces horizontal movement and enables upward swimming when Space is pressed, while lava adds even stronger drag but still allows buoyant ascent. All behavior is contained within the player class to avoid interference with collision resolution. A key challenge was preventing liquid effects from conflicting with standard physics; consolidating all movement adjustments into computePhysics() ensured consistent control and eliminated jitter near fluid boundaries.

Post-Processing Rendering Pipeline I implemented a complete post-processing pipeline to tint the screen when the camera is submerged. Instead of rendering the world directly to the default framebuffer as before, the rendering pass was split so the entire scene is first drawn into a custom Framebuffer containing a color texture and depth buffer. This allows the rendered image to be used as a texture input for screen-space operations. After unbinding the FBO, a Fullscreen Quad with its own VAO/VBO is drawn using two custom post shaders (post.vert.glsl and post.frag.glsl). The vertex shader outputs a screen-aligned quad, and the fragment shader samples the FBO’s texture, applying a blue tint when the player is in water, a red tint when in lava, and passing the image through unchanged otherwise. As a result, paintGL() now performs: render to FBO → unbind → draw quad with the post shader, cleanly isolating post-processing from the main scene while satisfying the milestone’s alpha blending requirements. The main technical issue was OpenGL errors caused by drawing the quad with VAO 0; adding explicit VAO creation in the quad’s create() method resolved these attribute binding problems and made the post-processing pass stable.

Texturing and Texture Animation (Jingjia Peng)

Texturing

Implementation: I implemented UV-mapped texture sampling from images, following standard OpenGL texture coordinate mapping. To handle transparency, I introduces separate buffer types (INTERLEAVED_TRANSPARENT and INDEX_TRANSPARENT) that complement the existing opaque buffer types (INTERLEAVED and INDEX). The rendering pipeline employs a two-pass approach: first rendering opaque geometry, then rendering transparent blocks with proper depth testing to ensure correct visual overlap. The system intelligently culls faces between solid blocks while exposing faces at the boundary between opaque and transparent blocks, optimizing both performance and visual correctness.

Challenges: The primary challenge arose with directional texture mapping, particularly for blocks with orientation-dependent textures like grass sides. Initial implementation resulted in upside-down textures on certain faces due to incorrect UV coordinate ordering. This required systematic testing of different UV orderings for each face direction (XPOS, XNEG, YPOS, YNEG, ZPOS, ZNEG) to ensure proper texture orientation across all block faces.

Texture Animation

Implementation: I took a shader-based approach rather than adding per-vertex animatable flags to the VBO data, as suggested in the spec. This design decision avoids memory overhead that would increase VBO size for every vertex. The fragment shader implements block-type-specific animation logic using conditional branching to handle different animation behaviors: water and lava use continuous UV x-coordinate shifting for flowing effects, while future support for other animated blocks (torch, pumpkins) can utilize discrete UV shifts in various directions. A custom time variable m_aniTime tracks animation progress, updating each tick to provide smooth temporal control for UV coordinate transformations.

Challenges: My initial implementation used QDateTime::currentMSecsSinceEpoch() as the time uniform, which produced static textures despite adjustments to the scaling factor in the UV shift calculation uv.x += mod(u_Time * scalingFactor, numFrames) * uvSize. Debugging revealed that the epoch timestamp's magnitude (on the order of 10^9 milliseconds) caused numerical precision issues in the modulo operation, resulting in effectively constant frame offsets. My solution introduced a separate m_aniTime variable that increments at a manageable scale each frame, providing the necessary precision for smooth animation of water and lava textures.

Multithreaded Terrain Generation (Jiawen Wang)

Implemented background worker threads to compute block types and VBO data. All GPU communication occurs only on the main thread. The system continuously generates the 5×5 zone area around the player in the background.

For every tick, after updating player physics, worker threads are spawned in expandTerrain.

There are 2 types worker threads:

  1. BlockTypeWorker populates each chunk with procedural BlockType data. Uses m_chunksMutex to securely instantiate the 16 chunks in a 64×64 zone. Once block types are finished, completed chunks are pushed into m_chunkswType, which is locked by m_chunkswTypeMutex. When a chunk enters m_chunkswType, it signals that its VBO generation can begin.

  2. VBOWorker builds opaque and transparent interleaved vertex buffers and index buffers for each chunk with completed block data but missing VBOs. Uses m_vboResultsMutex to safely push completed VBO data into m_vboResults

Both worker types use std::thread::detach() so the game does not need to block while waiting for them; Each worker runs entirely in the background and places its results queues protected by mutexes. Mutexes ensure thread-safe access so no two threads ever modify the same shared data at the same time. After worker threads finish producing VBO data, the main thread uploads this data to the GPU at uploadVBOResults(), since OpenGL operations must occur on the main thread. Each tick, the main thread collects and clears finished worker results, ensuring that no chunk is processed twice and preventing repeated generation.

Challenges Encountered: One challenge I ran into while implementing multithreading was oversynchronizing access to m_chunks. At first, I added a mutex lock around every place that touched m_chunks, including both reads and writes. This caused unnecessary blocking. Many operations only read from m_chunks, which is safe to do without locking because multiple threads can read the same data at same time. Locking every access forced threads to wait even when they didn’t need to. Also, overusing mutexes especially when several functions indirectly call one another made it easy to create deadlocks.

Milestone 1

Procedural Terrain (Yikai Li)

Two-Biome Terrain System: The terrain generation creates two distinct biomes with realistic characteristics. Grassland biomes feature gentle rolling hills ranging from 128 to 153 blocks, generated using multi-octave Fractional Brownian Motion for organic variation. Mountain biomes showcase dramatic peaks reaching up to 255 blocks, created using ridged Perlin noise for sharp elevation changes and Worley noise for clustered peak distribution. The biomes blend smoothly using a threshold-based system: pure grasslands exist below blend value 0.3, pure mountains above 0.7, with smooth transitions between. Biome distribution uses domain-warped low-frequency Perlin noise to create irregular, natural-looking boundaries resembling real continental shapes.

Implementation Approach: I separated grassland and mountain generation into distinct functions to allow independent tuning and future extensibility. Grasslands use two FBM layers at different frequencies—base layer for large hills and detail layer for fine bumps. Mountains required a more complex approach: ridged noise creates sharp peaks, while multiplicative blending with Worley-based peak detection preserves dramatic elevation contrast where valleys stay low and peaks soar high. The biome blending uses domain warping for organic boundaries and a three-zone system with pure biomes in extreme regions and smooth interpolation in transitions. Mountains are scaled by 20% during blending to compensate for averaging effects and ensure peaks reach maximum height.

Challenges Encountered: The primary challenge was achieving sufficient mountain height, as initial implementations only reached 180 blocks instead of 250+ due to limited noise output range. I solved this by increasing the mountain FBM initial amplitude from 0.5 to 1.2 and enhancing the peak multiplication factor. The second challenge was biome blending weakening mountain peaks even when generated correctly, reducing most to 200 blocks because blend value compression made pure mountain regions rare. I adjusted the pure mountain threshold from 0.5 to 0.7, increasing pure mountain coverage from 10% to 30%. Finally, threshold-based blending created visible seams, which I resolved by implementing localized interpolation within transition zones to create smooth gradients while preserving distinct biome characteristics.

Efficient Terrain Rendering and Chunking (Jiawen Wang)

Data Structure and Rendering Logic

Similar to Minecraft, the terrain is divided into chunks, each sized 16 × 16 × 256 blocks (x, z, y). All voxel data inside a chunk is stored in a single 1-dimensional array for efficient access. A block at local coordinates (x, y, z) is indexed as: index = x + 16 y + 16 256 * z.

Chunks are further grouped into zones, where each zone spans 64 × 64 blocks, or 4 × 4 chunks. Zones are used as the higher-level unit of world streaming.

On every tick, expandTerrain(glm::vec3 pos) takes in player position and ensures that the 3 × 3 zones surrounding the player are fully generated and rendered. As the player approaches the boundary of the current zones, specifically within 16 blocks from the zone’s west/east/north/south edges, adjacent zones and their 4 × 4 chunks are generated.

hasZoneAt(int zoneX, int zoneZ) and addZone(int zoneX, int zoneZ) Checks whether a zone has already been generated by looking up (zoneX, zoneZ) inside m_generatedTerrain and marks a zone as generated. So that each zone is created only once.

Mesh Generation and VBO structure

Each chunk independently builds its mesh using face culling. For every non-empty block, I iterate over its six faces and emit geometry only when the neighboring block in that direction is empty. This prevents drawing internal faces that are never visible to the camera.

To determine visibility at chunk boundaries, adjacent chunks are linked through m_neighbors map. Since each chunk has a fixed coordinate system of 16x16x256, a neighbor block may fall outside this range. It is then we convert its coordinate to neighbor chunk's local coordinate.

Mesh data is stored in 2 buffers:

  1. std::vector<float> interleaved

Stores position, normal, color, UV in sequence for each vertex. Each visible face contributes 4 vertices, and each vertex contributes 13 float attributes.

  1. std::vector<GLuint> indices

Stores triangle indices so each face is rendered as two triangles (0-1-2, 0-2-3).

For every visible face, its VBO data is generated of:

4 vec4 positions

4 vec4 normals

4 vec3 vertex colors

4 vec2 UV coordinates

6 indices (two triangles)

Each face is then rendered as two triangles, using index buffering.

Game Engine Tick Function and Player Physics (Jingjia Peng)

Player motion control

On every tick, the Player processes the InputBundle to update its camera orientation and physical state (acceleration, velocity, and position).

For camera rotation, I found that dragging on the screen offers a more intuitive control over the viewing angle. Therefore, I track the X and Y displacement of the mouse position, but only when the mouse button is pressed. The camera is rotated globally by first rotating around the up axis, followed by a local rotation around the right axis.

For physical movement, I apply a friction coefficient of 0.95 to reduce the player's velocity, then update the velocity based on acceleration. Additionally, a vertical acceleration of -9.8 is applied to simulate gravity in flight mode.

Collision with terrain

Grid March Algorithm: In Player::gridMarch(), we walk through voxels along a ray direction by tracking the distance to the next voxel boundary in each axis (stored in tMax) and the distance needed to cross one voxel (stored in tDelta). At each step, it moves to the next voxel by advancing along the axis with the smallest tMax value. This efficiently finds the first non-empty, non-water block along the ray and also tracks the previous block position, which is crucial for later block placement.

Collision Detection: To detect collision, I first generate 12 test vertices at the player's bounding box corners. For each vertex, I then perform gridMarch separately along X, Y, and Z axes with the displacement vector, to track the minimum collision distance per axis. Finally, I resolve collision separately for vertical and horizontal movements:

  • Y-axis handled separately for floor/ceiling collisions
  • For X and Z axes, only handle the closer collision to enable smooth wall-sliding behavior, which prevents getting stuck in corners

Difficulties Encountered: The main challenge was preventing the player from getting stuck when colliding with corners. Initially, I tried to handle all three axes independently, i.e. clip their magnitude to zero if collisoin is detected in that direction, but this caused the player to "catch" on edges. The solution was to prioritize the closer collision between X and Z axes, this allows player to slide smoothly along walls when moving diagonally into corners.

Block removal and addition

Player::removeBlock() and Player::placeBlock() leverage the gridMarch function to find the target block within a predefined BLOCK_CONTROL_DIST (I set it to be 5 units instead of 3 units). For removal, I simply set the target block to EMPTY. For placement, I place a new STONE block at the previous block position (the empty space adjacent to the hit face), and verify it doesn't intersect with the player's bounding box to prevent placing blocks inside the player.

Block Target Preview: I implemented a visual highlight system using the BlockOutline class. The outline draws the 12 edges of a cube using GL_LINES with a golden color. The edges are slightly offset (0.001 units) outward to prevent z-fighting with the actual block faces. The outline updates each frame in MyGL::paintGL() by querying the player's current look-at block via Player::getCurrentLookAtBlock().

UX Improvement: I changed block modification to require double-clicks instead of single clicks, as single clicks were triggered too frequently during camera movement, leading to unintentional terrain changes.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages