Minecraft, is basically a game about placing blocks with textures and going on adventures. It’s set in infinitely-generated worlds of wide open terrain, icy mountains, swampy bayous, vast pastures and much more. The game was heavily criticised due to its performance as the game was developed originally in Java. For developers, creating a copy of such game could be a really interesting challenge to implement in any other programming language. You can find some ports of this game in multiple languages like Python. Today, we want to share with you a really cool and very acceptable implementation made in the C language using OpenGL. This project is Craft, a Minecraft clone for Windows, Mac OS X and Linux. It contains just a few thousand lines of C using modern OpenGL (shaders). The game features:
- Simple but nice looking terrain generation using simplex noise.
- More than 20 types of blocks and more can be added easily.
- Supports plants (grass, flowers, trees, etc.) and transparency (glass).
- Simple clouds in the sky (they don't move).
- Day / night cycles and a textured sky dome.
- Signs - you can write text on any block.
- Lights - any block can be set as a light source.
- Ambient occlusion for basic shading of blocks.
- World changes persisted in a sqlite3 database.
- Multiplayer support!
In this article, we'll guide you step by step the build process of this project in your Ubuntu machine.
1. Install Ubuntu Dependencies
Before building the project, you will need to install some dependencies in your system so we build the Craft project from scratch. Open a terminal and type the following command to install them:
sudo apt-get install cmake libglew-dev xorg-dev libcurl4-openssl-dev
These packages are:
- glew: The OpenGL Extension Wrangler Library (GLEW) is a cross-platform open-source C/C++ extension loading library. GLEW provides efficient run-time mechanisms for determining which OpenGL extensions are supported on the target platform. OpenGL core and extension functionality is exposed in a single header file. GLEW has been tested on a variety of operating systems, including Windows, Linux, Mac OS X, FreeBSD, Irix, and Solaris.
- xorg: Xorg is basically the X window system used in linux. It's the base of the graphical environment for your computer. This package comes installed by default on Ubuntu 19.04, but, just in case it doesn't.
- libcurl: is a free and easy-to-use client-side URL transfer library, supporting DICT, FILE, FTP, FTPS, Gopher, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, POP3, POP3S, RTMP, RTSP, SCP, SFTP, SMTP, SMTPS, Telnet and TFTP. libcurl supports SSL certificates, HTTP POST, HTTP PUT, FTP uploading, HTTP form based upload, proxies, cookies, user+password authentication (Basic, Digest, NTLM, Negotiate, Kerberos), file transfer resume, http proxy tunneling and more!
For more information about this project, please visit the official repository at Github here. After installing the dependencies, proceed to obtain the source of the project in the next step.
2. Clone Craft Source Code
You need now to clone the source code of the Craft project, you can do this easily with Git (
sudo apt-get install git) running the following command in some directory of your system:
git clone https://github.com/fogleman/Craft.git
This will create the Craft directory, that contains the source code. Switch to the mentioned directory with the following command:
And proceed with the next step.
3. Build & Run
To proceed with the build, you need to generate the standard build files with CMake. CMake is an extensible, open-source system that manages the build process in an operating system and in a compiler-independent manner. Unlike many cross-platform systems, CMake is designed to be used in conjunction with the native build environment. Run the following command on the current directory:
And once the make files are ready, run the build with:
The build process will start and it will take a while, so be patient until it finishes. After the build, you can run the application directly from the command line:
The game can be controlled with the following instructions:
- WASD to move forward, left, backward, right.
- Space to jump.
- Left Click to destroy a block.
- Right Click or Cmd + Left Click to create a block.
- Ctrl + Right Click to toggle a block as a light source.
- 1-9 to select the block type to create.
- E to cycle through the block types.
- Tab to toggle between walking and flying.
- ZXCVBN to move in exact directions along the XYZ axes.
- Left shift to zoom.
- F to show the scene in orthographic mode.
- O to observe players in the main view.
- P to observe players in the picture-in-picture view.
- T to type text into chat.
- Forward slash (/) to enter a command.
- Backquote (`) to write text on any block (signs).
- Arrow keys emulate mouse movement.
- Enter emulates mouse click.
About the implementation logic (deep technical details)
The terrain is generated using Simplex noise - a deterministic noise function seeded based on position. So the world will always be generated the same way in a given location.
The world is split up into 32x32 block chunks in the XZ plane (Y is up). This allows the world to be “infinite” (floating point precision is currently a problem at large X or Z values) and also makes it easier to manage the data. Only visible chunks need to be queried from the database.
Only exposed faces are rendered. This is an important optimization as the vast majority of blocks are either completely hidden or are only exposing one or two faces. Each chunk records a one-block width overlap for each neighboring chunk so it knows which blocks along its perimeter are exposed.
Only visible chunks are rendered. A naive frustum-culling approach is used to test if a chunk is in the camera’s view. If it is not, it is not rendered. This results in a pretty decent performance improvement as well.
Chunk buffers are completely regenerated when a block is changed in that chunk, instead of trying to update the VBO.
Text is rendered using a bitmap atlas. Each character is rendered onto two triangles forming a 2D rectangle.
“Modern” OpenGL is used - no deprecated, fixed-function pipeline functions are used. Vertex buffer objects are used for position, normal and texture coordinates. Vertex and fragment shaders are used for rendering. Matrix manipulation functions are in matrix.c for translation, rotation, perspective, orthographic, etc. matrices. The 3D models are made up of very simple primitives - mostly cubes and rectangles. These models are generated in code in cube.c.
Transparency in glass blocks and plants (plants don’t take up the full rectangular shape of their triangle primitives) is implemented by discarding magenta-colored pixels in the fragment shader.
User changes to the world are stored in a sqlite database. Only the delta is stored, so the default world is generated and then the user changes are applied on top when loading.
The main database table is named “block” and has columns p, q, x, y, z, w. (p, q) identifies the chunk, (x, y, z) identifies the block position and (w) identifies the block type. 0 represents an empty block (air).
In game, the chunks store their blocks in a hash map. An (x, y, z) key maps to a (w) value.
The y-position of blocks are limited to 0 <= y < 256. The upper limit is mainly an artificial limitation to prevent users from building unnecessarily tall structures. Users are not allowed to destroy blocks at y = 0 to avoid falling underneath the world.
Multiplayer mode is implemented using plain-old sockets. A simple, ASCII, line-based protocol is used. Each line is made up of a command code and zero or more comma-separated arguments. The client requests chunks from the server with a simple command: C,p,q,key. “C” means “Chunk” and (p, q) identifies the chunk. The key is used for caching - the server will only send block updates that have been performed since the client last asked for that chunk. Block updates (in realtime or as part of a chunk request) are sent to the client in the format: B,p,q,x,y,z,w. After sending all of the blocks for a requested chunk, the server will send an updated cache key in the format: K,p,q,key. The client will store this key and use it the next time it needs to ask for that chunk. Player positions are sent in the format: P,pid,x,y,z,rx,ry. The pid is the player ID and the rx and ry values indicate the player’s rotation in two different axes. The client interpolates player positions from the past two position updates for smoother animation. The client sends its position to the server at most every 0.1 seconds (less if not moving).
Client-side caching to the sqlite database can be performance intensive when connecting to a server for the first time. For this reason, sqlite writes are performed on a background thread. All writes occur in a transaction for performance. The transaction is committed every 5 seconds as opposed to some logical amount of work completed. A ring / circular buffer is used as a queue for what data is to be written to the database.
In multiplayer mode, players can observe one another in the main view or in a picture-in-picture view. Implementation of the PnP was surprisingly simple - just change the viewport and render the scene again from the other player’s point of view.
Hit testing (what block the user is pointing at) is implemented by scanning a ray from the player’s position outward, following their sight vector. This is not a precise method, so the step rate can be made smaller to be more accurate.
Collision testing simply adjusts the player’s position to remain a certain distance away from any adjacent blocks that are obstacles. (Clouds and plants are not marked as obstacles, so you pass right through them.)
A textured sky dome is used for the sky. The X-coordinate of the texture represents time of day. The Y-values map from the bottom of the sky sphere to the top of the sky sphere. The player is always in the center of the sphere. The fragment shaders for the blocks also sample the sky texture to determine the appropriate fog color to blend with based on the block’s position relative to the backing sky.
Ambient occlusion is implemented as described on this page:
- GLEW is used for managing OpenGL extensions across platforms.
- GLFW is used for cross-platform window management.
- CURL is used for HTTPS / SSL POST for the authentication process.
- lodepng is used for loading PNG textures.
- sqlite3 is used for saving the blocks added / removed by the user.
- tinycthread is used for cross-platform threading.
Happy coding !