Minecraft is one of the most popular game in the world, it is a good game to improve creativity of kids. It has very basic graphics with very useful game mechanism. Developing a 3D gamemight be little bit hard for all developers, requires good math skills, higher programming logics, requires optimization techniques, understanding 3D digital environment, matrix operations, camera movements, texture effects, light effects etc.
C++ Builder is easy to build these kind of simple games. C++ is faster programming language that you can develop fully native and faster games. You can use OpenGL or Direct3D libraries or some other 3rd party 3D Engines. In C++ Builder you can directly create your own 3D objects, you can animate them on runtime. Viewport3D (TViewportd3D) component in C++ Builder FireMonkey projects is good to display many basic 3D Objects like Plane, Cube, Sphere, Cone, Plane, Ellipse3D etc. Please see this post about Working With 3D In Modern Windows C++ Development for creating these 3D objects. You can also easily load your 3D objects into Viewport3D by using Model3D (TModel3D).
To create a 3D object to be used in Viewport3D we need to use TMesh classes. TMesh is a custom 3D shape that can be customized by drawing 3D shapes. It is a class publishes a set of properties from its ancestor, TCustomMesh, in order to let you design new 3D shapes at design time from within the IDE, through the Object Inspector. Use the Data property to specify the points, normals and textures for each point, and the order in which the resulting triangles are drawn. The designed shape is filled with the material specified through MaterialSource property. If no material is specified, then the shape is filled with red color. Please read more about Learn To Quickly Create Specific 3D Objects In Modern C++ Applications For Windows
1. Create a new C++ Builder Console FMX application, save all project and unit files to a folder. And modify code lines as below;
2. Drag a Viewport3D from Tool Palette on to Form. We will use this to display our 3D map space. Add a Label, Cube, a Sphere, Dummy Object and Camera, 2 Color Materials, 2 Lights to our ViewPort3D by dragging from the Tools Palette. Let’s modify these from design and
– Arrange Label1 Position to top left, we will use this to print and see coordinates
– Cube1 will be our ground. Form Object Inspector, Change it’s Name to Ground1 and set its Width=32, Height=0.01; and Depth=32; and set HitTest =false; Set it’s Material1.
– Sphere1 will be a reference to see center of map, it will be at the 0,0,0 position. You can change its position to understand coordinates in 3D space.
– Dummy1 will be our character, we will move this like user moves in the map. So Drag Camera1 into Dummy1 in the Structure Panel above the Object Inspector Panel, Camera will be eye of our character.
– Add a grass Texture image to ColorMaterial1 and brick Texture image to ColorMaterial2.
– Set both Light Types to Point and, put Lights to upper left and right corners, one light could be gray
3. Let’s add some constants before the __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) line shown as below,
1 2 3 4 5 6 7 |
// constants, globals and some functions here __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { } |
4. Now lets start adding width, height and depth constants about our 3D space environment in our game,
1 2 3 4 5 |
# define MAPWIDTH 32 # define MAPHEIGHT 4 # define MAPDEPTH 32 |
5. Let’s define our st_space structure and let’s create our space with given dimensions above.
1 2 3 4 5 6 7 |
struct st_space { TCube *cube; }; struct st_space space[MAPWIDTH][MAPHEIGHT][MAPDEPTH]; |
6. Now we need to define our step size and grid size, assume 0.5 is like 0.5m,, so our bricks will have 0.5×0.5×0.5 m3 size,
1 2 3 |
float step = 0.5, gridwidth = 0.5, griddepth = 0.5, gridheight=0.5; |
7. Now let’s create our function to create cubes in given x, y, z dimension on runtime. At the end we will put this cube pointer address to space[x][y][z].cube If you are new to 3D objects in C++ Builder, please see this post about Working With 3D In Modern Windows C++ Development for creating these 3D objects.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
void create_cube(int x, int y, int z) { TCube *cube =new TCube(Form1->Viewport3D1); if(cube!=NULL) { cube->BeginUpdate(); cube->Parent = Form1->Viewport3D1; cube->Width = gridwidth; cube->Height = gridheight; cube->Depth = griddepth; cube->Opacity = 1.0; cube->Position->X = gridwidth/2+x*gridwidth-MAPWIDTH*gridwidth/2; cube->Position->Y = -1*(gridheight/2+y*gridheight); cube->Position->Z = griddepth/2+z*griddepth-MAPDEPTH*griddepth/2; cube->MaterialSource = Form1->LightMaterialSource2; cube->HitTest = false; cube->EndUpdate(); cube->Repaint(); space[x][y][z].cube = cube; } } |
8. Be sure that your created cube address is safely stored somewhere. Here we store it with space[x][y][z].cube=cube; line. So we can free them from the memory at the end. We must free all those cubes from 3D space at the end. We can write this procedure to free all cubes from their pointer addresses in our space, see below;
1 2 3 4 5 6 7 8 9 10 11 12 |
void free_mapspace() { for(int k=0; k<MAPDEPTH; k++) for(int j=0; j<MAPHEIGHT; j++) for(int i=0; i<MAPWIDTH; i++) { if(space[i][j][k].cube!=NULL) space[i][j][k].cube->Free(); } } |
9. We finished adding constant, variables and some functions which will be used in Form creation. Now we can setup our ground, and some cubes about at the 0,0,0 point. So we can understand that corner is origin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
__fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { Ground1->Width = MAPWIDTH*gridwidth; Ground1->Depth = MAPDEPTH*griddepth; Ground1->Height = 0.01; create_cube(0,0,0); create_cube(1,0,0); create_cube(0,0,1); Sphere1->Position->X=0; Sphere1->Position->Y=0; Sphere1->Position->Z=0; } |
10. We must free all cubes in map when we close the form, when we start a new game, before loading a new map may be too. Select Form1 and double click to OnClose event and add our function as in given example below,
1 2 3 4 5 6 |
void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action) { free_mapspace(); } |
11. In this step you can run and see your environment on run time. But our character (in real it is our camera in our viewport3D) is not moving. To move our character we will move the Dummy1 object which has camera. So when Dummy1 moves camera will also move and we will see that we are getting closer, far or rotating around. To do this, we will check key presses and our character (Dummy1) will move according to these cases. You can use Key or KeyChar, here we will use KeyChar parameter to check input characters. Select Form1 and double click to OnKeyDown event, add switch case format as below,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key, System::WideChar &KeyChar, TShiftState Shift) { switch (KeyChar) { case 'a': // Rotate Left break; case 'd': // Rotate Right break; case 'w': // Move Forward break; case 's': // Move Backward break; case ' ': // Build or Delete Brick in Front break; } } |
Finally we should add actions inside this procedure. As you see we will use lowercase a and d to rotate left and right and w and s to move forward and backwards as in many games. We will use space to build bricks, you can bind b or other keys if you want. To move our character we will use our step variable and according to this our character will move as in this formula,
So, when user presses ‘a’ forward key must update X position along map width and Z position along map depth.
1 2 3 4 |
Dummy1->Position->X += step*Sin(Dummy1->RotationAngle->Y/180*M_PI); Dummy1->Position->Z += step*Cos(Dummy1->RotationAngle->Y/180*M_PI); |
and we can rotate our character, in real this is our alfa angle which is in Y diagonal RotationAngle ıf Dummy1. For example to rotate right we can write this,
1 2 3 |
Dummy1->RotationAngle->Y+=5; |
Note that 2*PI in Radians is equal to 360 Degrees, and we need to convert Degrees to Radians to use in Sin() and Cos() functions, like in give example
1 |
Radian = Degrees/180*M_PI; |
By given information above, now let’s keep editing code above, and let’s add some actions to wasd keys as below,
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 |
void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key, System::WideChar &KeyChar, TShiftState Shift) { switch (KeyChar) { case 'a': // Rotate Left Dummy1->RotationAngle->Y-=5; break; case 'd': // Rotate Right Dummy1->RotationAngle->Y+=5; break; case 'w': // Move Forward Viewport3D1->BeginUpdate(); Dummy1->Position->X += step*Sin(Dummy1->RotationAngle->Y/180*M_PI); Dummy1->Position->Z += step*Cos(Dummy1->RotationAngle->Y/180*M_PI); Viewport3D1->EndUpdate(); break; case 's': // Move Backward Viewport3D1->BeginUpdate(); Dummy1->Position->X -= step*Sin(Dummy1->RotationAngle->Y/180*M_PI); Dummy1->Position->Z -= step*Cos(Dummy1->RotationAngle->Y/180*M_PI); Viewport3D1->EndUpdate(); break; case ' ': // Build or Delete Brick in Front break; } } |
12. At this step, you can run your application. Press F9 to run and try to move and rotate in the map, you must see sphere at the center and try to see 3 cubes around 0,0,0 position that we generate before.
13. Finally let’s add functionality to space key. first we should calculate x and y coords at the one step ahead as in frontx and fronty. And we should find exact integer numbers which shows x y s position in grid form. This will help to check or modify data at space[X][Y][Z]. Finaly if these X Y Z are in limits we will check if there is cube or not. if there is no cube then we will create a new cube at that point else we will delete this brick from the memory also from the map.
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 38 39 40 41 42 43 44 45 46 47 |
void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key, System::WideChar &KeyChar, TShiftState Shift) { switch (KeyChar) { case 'a': // Rotate Left Dummy1->RotationAngle->Y-=5; break; case 'd': // Rotate Right Dummy1->RotationAngle->Y+=5; break; case 'w': // Move Forward Viewport3D1->BeginUpdate(); Dummy1->Position->X += step*Sin(Dummy1->RotationAngle->Y/180*M_PI); Dummy1->Position->Z += step*Cos(Dummy1->RotationAngle->Y/180*M_PI); Viewport3D1->EndUpdate(); break; case 's': // Move Backward Viewport3D1->BeginUpdate(); Dummy1->Position->X -= step*Sin(Dummy1->RotationAngle->Y/180*M_PI); Dummy1->Position->Z -= step*Cos(Dummy1->RotationAngle->Y/180*M_PI); Viewport3D1->EndUpdate(); break; case ' ': // Build or Delete Brick in Front float frontx = Dummy1->Position->X+step*Sin(Dummy1->RotationAngle->Y/180.0*M_PI); float frontz = Dummy1->Position->Z+step*Cos(Dummy1->RotationAngle->Y/180.0*M_PI); int X = floor(MAPWIDTH/2 + frontx/gridwidth); int Y = floor(-1*Dummy1->Position->Y/gridheight-gridheight/2); int Z = floor(MAPDEPTH/2 + frontz/griddepth); Label1->Text=":"+IntToStr(X)+","+IntToStr(Z)+","+IntToStr(Y); //+ " at "+IntToStr(CX)+","+IntToStr(CZ)+","+IntToStr(CY); if (X>=0 && Z>=0 && Y>=0 && X<MAPWIDTH && Z<MAPDEPTH && Y<MAPHEIGHT) { Label1->Text="Build To:"+IntToStr(X)+","+IntToStr(Z)+","+IntToStr(Y);//+ " at "+IntToStr(CX)+","+IntToStr(CZ)+","+IntToStr(CY); if(space[X][Y][Z].cube==NULL) create_cube(X, Y, Z); else { space[X][Y][Z].cube->Free(); space[X][Y][Z].cube=NULL; } } break; } } |
14. This great example has no 3rd party engine, you don’t need to write codes about OpenGL or DirectX etc. It is just native codes and it has some official 3D components. It has less than hundred lines in real, that makes C++ Builder great ! Full code should be like this,
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
//--------------------------------------------------------------------------- #include <fmx.h> #pragma hdrstop #include "MineCraft_Unit1.h" //--------------------------------------------------------------------------- #pragma package(smart_init) #pragma resource "*.fmx" TForm1 *Form1; //--------------------------------------------------------------------------- # define MAPWIDTH 32 # define MAPHEIGHT 4 # define MAPDEPTH 32 struct st_space { TCube *cube; }; struct st_space space[MAPWIDTH][MAPHEIGHT][MAPDEPTH]; float step = 0.5, gridwidth = 0.5, griddepth = 0.5, gridheight=0.5; //--------------------------------------------------------------------------- void create_cube(int x, int y, int z) { TCube *cube =new TCube(Form1->Viewport3D1); if(cube!=NULL) { cube->BeginUpdate(); cube->Parent = Form1->Viewport3D1; cube->Width = gridwidth; cube->Height = gridheight; cube->Depth = griddepth; cube->Opacity = 1.0; cube->Position->X = gridwidth/2+x*gridwidth-MAPWIDTH*gridwidth/2; cube->Position->Y = -1*(gridheight/2+y*gridheight); cube->Position->Z = griddepth/2+z*griddepth-MAPDEPTH*griddepth/2; cube->MaterialSource = Form1->LightMaterialSource2; cube->HitTest = false; cube->EndUpdate(); cube->Repaint(); space[x][y][z].cube = cube; } } //--------------------------------------------------------------------------- void free_mapspace() { for(int k=0; k<MAPDEPTH; k++) for(int j=0; j<MAPHEIGHT; j++) for(int i=0; i<MAPWIDTH; i++) { if(space[i][j][k].cube!=NULL) space[i][j][k].cube->Free(); } } //--------------------------------------------------------------------------- __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { Ground1->Width = MAPWIDTH*gridwidth; Ground1->Depth = MAPDEPTH*griddepth; Ground1->Height = 0.01; create_cube(0,0,0); create_cube(1,0,0); create_cube(0,0,1); Sphere1->Position->X=0; Sphere1->Position->Y=0; Sphere1->Position->Z=0; } //--------------------------------------------------------------------------- void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action) { free_mapspace(); } //--------------------------------------------------------------------------- void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key, System::WideChar &KeyChar, TShiftState Shift) { switch (KeyChar) { case 'a': // Rotate Left Dummy1->RotationAngle->Y-=5; break; case 'd': // Rotate Right Dummy1->RotationAngle->Y+=5; break; case 'w': // Move Forward Viewport3D1->BeginUpdate(); Dummy1->Position->X += step*Sin(Dummy1->RotationAngle->Y/180*M_PI); Dummy1->Position->Z += step*Cos(Dummy1->RotationAngle->Y/180*M_PI); Viewport3D1->EndUpdate(); break; case 's': // Move Backward Viewport3D1->BeginUpdate(); Dummy1->Position->X -= step*Sin(Dummy1->RotationAngle->Y/180*M_PI); Dummy1->Position->Z -= step*Cos(Dummy1->RotationAngle->Y/180*M_PI); Viewport3D1->EndUpdate(); break; case ' ': float frontx = Dummy1->Position->X+step*Sin(Dummy1->RotationAngle->Y/180.0*M_PI); float frontz = Dummy1->Position->Z+step*Cos(Dummy1->RotationAngle->Y/180.0*M_PI); int X = floor(MAPWIDTH/2 + frontx/gridwidth); int Y = floor(-1*Dummy1->Position->Y/gridheight-gridheight/2); int Z = floor(MAPDEPTH/2 + frontz/griddepth); Label1->Text=":"+IntToStr(X)+","+IntToStr(Z)+","+IntToStr(Y); //+ " at "+IntToStr(CX)+","+IntToStr(CZ)+","+IntToStr(CY); if (X>=0 && Z>=0 && Y>=0 && X<MAPWIDTH && Z<MAPDEPTH && Y<MAPHEIGHT) { Label1->Text="Build To:"+IntToStr(X)+","+IntToStr(Z)+","+IntToStr(Y);//+ " at "+IntToStr(CX)+","+IntToStr(CZ)+","+IntToStr(CY); if(space[X][Y][Z].cube==NULL) create_cube(X, Y, Z); else { space[X][Y][Z].cube->Free(); space[X][Y][Z].cube=NULL; } } break; } } |
Here is our test result.
After this step, you can enhance your codes. You can limit your character in your map, you can add jumping, climbing to bricks or other abilities. You can add starting, loading and saving map data by using file operations. You can add some more features into st_space structure, animations, effects, mechanics, sound features etc. Hard part might be Server-Client interactions and multi-player environment.