Contents

Minecraft remake

Introduction

For my second block in my first year at BUAS we were tasked to create a Minecraft clone with OpenGL that had to run on the Raspberry Pi 4 & PC in 8 Weeks.

Project features

  • Threaded world generation
  • Biomes
  • Villages
  • 3D noise and spaghetti caves
  • Day/Night cycle
  • Clouds
  • Block placement/mining
  • Block states
  • Advanced crafting (supports shapeless)
  • Minecraft model loading
  • Trees & other world generation structures
  • Player Health & Food
  • Player movement
  • Dynamically scaling UI
  • Inventory system
  • Interactable blocks e.g. doors/fence-gates
  • Block icons generated on startup
  • Creeper :)

Procedural Generation

This project contains a lot of procedural generation.

Height-map

The world’s height map was generated with a bunch of 2D simplex noise layers. I used FastNoiseLite for this, and I tried to replicate how Minecraft generated their worlds. I got a lot of information from Henrik Kniberg’s Minecraft terrain generation in a nutshell.

Biomes

Biomes where generated using temperature and humidity noise maps. Which would then decide what kind of blocks would be placed such as snow, dirt and sand. The generated structure would also be different depending on the current biome. The biomes where generated with Simplex noise but I also tried cellular noise. However the cellular noise always had a similar sharp shape which made it look unnatural.

The final biomes look like this:

/images/minecraft/week8_3biomes.webp

Perlin noise carver caves

At the end of week-8 I really wanted to add spaghetti caves (noise carver caves). It took me quite a some time to understand how to make such caves I learned a lot watching this GDC talk from Squirrel Eiserloh. He has many good talks about RNG & procedural generation here is a playlist of some talks. I create noise cave by spawning cave-worms inside of Chunk regions I then keep stepping through the world with a yaw and pitch and steer the noise carver through the world with a downward bias.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
std::uniform_real_distribution<float> angleDist(0.f, 2.f * glm::pi<float>());
std::uniform_real_distribution<float> pitchDist(-0.5f, 0.1f); 
float yaw = angleDist(rng);      
float pitch = pitchDist(rng);

for (int step{ 0 }; step < maxSteps; step++) {
    const float stepF = static_cast<float>(step);
    float yawNoise = carverXDirNoise.GetNoise(stepF, wormSeed, 0.f);
    float pitchNoise = carverYDirNoise.GetNoise(stepF, wormSeed + 1000.f, 0.f);

    constexpr float yawChangeRate = 0.15f;   // How much to turn horizontally
    constexpr float pitchChangeRate = 0.1f;  // How much to turn vertically
    
    yaw += yawNoise * yawChangeRate;
    pitch += pitchNoise * pitchChangeRate;
    // clamp so we don't go straight down
    pitch = glm::clamp(pitch, -0.8f, 0.8f);
	// calculate if noisecarver overlaps with current chunk
    if(intersects)
	    CarveSphereInChunk(data, localCenterPos, radius);
}
Results

/images/minecraft/week8_wormcaves.webp

Icons

Most of the icon’s used for the UI are procedurally generated on startup. However Items can still choose Icon’s to override the generated ones. The block Icon’s are generated by first rendering the block to a texture at an angle using a different framebuffer. They are put into an 2D texture array and are then rendered as UI.

Early screenshot:

/images/minecraft/icons_early_screenshot.webp

Bunch of items (Door icon is overridden)

/images/minecraft/bunch_of_items.webp

Villages

They were quite a challenge, at first I was really pondering how I should implement this. As they have to be generated across chunk-borders and have to generated the same way in any order e.g. Northern chunk first then southern or the inverse. What I ended up going for was for every 20x20 chunks region It would decide if there was going to be a village or not. You could then check if your chunk falls within a village region and if It does we start generating the village roads. first the RNG is seeded by the region X&Z-coordinates scrambled. Then It generates some roads going in both directions, they can have variable lengths. We store their AABB’s and then start looping over all the roads and at pseudo-random (the RNG is still based on the seed) locations next to the road It tries to place houses by checking if their AABB overlaps with any roads or generated structure. Then after we generated the roads and structures we check if ANY of them START in the current or neighbouring chunk. If they do we start generating the structure from their origin and if any of the blocks we’re trying to place fall into the currently generating Chunk we update the blocks.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct VillageStructure {
    INTAABB aabb{};
    StructureTypes type{};
    int rotation{};          // 0, 90, 180, 270
};
struct VillagePlan {
    glm::ivec2 center{};
    std::vector<VillageStructure> structures;
    std::vector<INTAABB> roads;  // x1, z1, x2, z2
};
I think the end result looks quite believable (Raspberry Pi 4)

/images/minecraft/villages.webp

Modular data

Block state support

I made a custom file format for the items in my game however they use Minecraft model logic so I could use there models without making a bunch on my own. However this turned out to be quite a pain as Minecraft’s texture packs have a lot of different versions and a bunch of legacy/hardcoded values. I made sure that my blocks can easily add behaviors and you can change the blockstates to use a different block.
Here is an example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "name": "Oak Door",
  "type": "block",
  "blockstates": "oak_door",

  "icon": "item/oak_door",
  "behaviors": [ "facing", "open", "tallblock" ],
  "transparent": false,
  "gravity": false,
  "stack_size": 16
}
Minecraft model

It’s also possible to use a singular model instead of a block states file which points to model files. Almost all (if not all) Minecraft model files are supported. Here is an example of a crafting table which has the crafting functionality added in behaviours, I could also add this behaviour to another block and it would work the same.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "name": "Crafting Table",
  "type": "block",
  "model": "block/crafting_table",

  "behaviors": [ "crafting" ],
  "transparent": true,
  "gravity": false,
  "stack_size": 64
}
Items

Items have different files(durability not used yet).

1
2
3
4
5
6
7
8
9
{
  "name": "Diamond Pickaxe",
  "icon": "item/diamond_pickaxe",
  
  "durability": 59,
  "mining_level": 1,
  "attack_damage": 5,
  "attack_speed": 1.2
}
Block state logic

I made Block Behavior to work with the minecraft blockstates these had functions they could override in the placement/destruction phase or when the block is being “used” by a player.

1
2
3
4
5
6
7
8
class BlockBehavior
{
public:
    virtual bool TryPlace(const glm::ivec3& position, ChunkManager& manager)const { return true; }
    virtual void OnPlace(const BlockData* blockData, BlockChangePlan& plan,const BlockPlaceData& hit)const {}
    virtual void OnDestruction(const BlockData* blockData, BlockChangePlan& plan)const {}
    virtual bool OnUse(Player* player, const BlockData* blockData, BlockChangePlan& plan)const { return false; }
};

These are probably more like interfaces and the implementation is not as great as I think it could be but it does what it needs to. The state of each block is encoded as a single Byte, as long as we don’t attach too many behaviours with many states to a block.

8-Week progress video