Oct 23, 2021Kuina-chan


This is the tutorial 7 to learn the basic features of Kuin Programming Language for exe execution environment. This time, let's make a 3D racing game.

1Converting 3D Model

This time I will make a 3D racing game. The completed screen is shown in Figure 1-1.
Completed Screen
Figure 1-1: Completed Screen
First, create a 3D model of the car to be used in the game with your favorite tool and output it in .fbx format. In this tutorial, you can use samples/free_resources/obj_car.fbx as a sample.
The .fbx format is a common format for 3D models, but it is not efficient for loading into programs, so convert it to the more efficient Kuin binary format .knobj.
Start the Kuin editor, select "Tools > 3D Model Converter..." from the menu, select the fbx file from "..." and press the "Convert" button, and the knobj file will be output in the same folder as the fbx file after a while (Figure 1-2).
Converting To Knobj
Figure 1-2: Converting To Knobj
Note that fbx files must have all polygons triangulated. Make sure to triangulate fbx files before saving them.
The 3D Model Converter checkboxes are shown in Table 1-1, although we will not use them in this tutorial.
Table 1-1: Conversion Options
Option Description
Write Tangents And Binormals Calculates tangent and bynormal from UV and writes them to knobj. Normal maps will be available, but the data will be large and the rendering may be corrupted if the UVs are degenerate. If you do not want to use the normal map, turn this off.
Write Joints Writes the bone and bone animation to knobj. The maximum number of bones that can affect a single vertex is two. The maximum total number of bones is 256. If you do not want to move the bones, turn this off.
When the knobj file is output, close the 3D Model Converter dialog, press Ctrl+S to save the main kn file, and then press Ctrl+Shift+R to move the knobj file under the res folder that appears, renaming it obj_car.knobj.
Also, prepare a texture to put on the 3D model under the res folder with the name tex_car_albedo.png. This time, copy and place samples/free_resources/tex_car_albedo.png as a sample.
In addition, let's copy the files we will use from the samples/free_resources folder. Copy and place tex_field_albedo.jpg, tex_sky_albedo.png, tex_wall_albedo.jpg, and map_sample_racing.txt (Figure 1-3).
res Folder
Figure 1-3: res Folder

2Drawing Car

Now let's start drawing the 3D model. Insert the code for "main And A draw Control" from the snippet as shown in the 2nd tutorial, and add the code shown in Figure 2-1.
  1. var wndMain: wnd@Wnd
  2. var drawMain: wnd@Draw
  3. var objCar: draw@Obj
  4. var texCarAlbedo: draw@Tex
  5.  
  6. func main()
  7.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  8.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  9.  
  10.   do @objCar :: draw@makeObj("res/obj_car.knobj")
  11.   do @texCarAlbedo :: draw@makeTex("res/tex_car_albedo.png")
  12.   do draw@depth(true, true)
  13.  
  14.   while(wnd@act())
  15.     do @objCar.draw(0, 0.0, @texCarAlbedo, null, null)
  16.     do draw@render(60)
  17.   end while
  18. end func
Figure 2-1: racing_game1.kn
Lines 3 and 4 declare the variables for the 3D model and texture image, and lines 10 and 11 load them from a file.
Line 12 is the Z-buffer setting for resolving the back and forth relationship of depth. The arguments in parentheses in the draw@depth function are, in order, whether the Z test is enabled and whether Z writing is enabled. Normally, when drawing 3D, enable it and set it to "draw@depth(true,true)", and when drawing 2D at the forefront, set it to "draw@depth(false,false)".
In line 15, a 3D model with a texture on its surface is drawn on the screen. The arguments in parentheses in the draw method are, in order, mesh index, animation frame, diffuse map texture (color reflection), specular map texture (gloss), and normal map texture (unevenness).
The camera and lights are set to be somewhat easy to use without any settings. When you run it, the car will be displayed (Figure 2-2).
Drawing Car
Figure 2-2: Drawing Car

3Drawing Ground And Shadows

Next, let's draw the ground and shadows.
The ground is created by applying a texture to a mere flat surface. If you just want to create a simple 3D model like a plane, you don't need to prepare a .knobj file, you can create it from a function.
To draw a shadow, first create an instance of the draw@Shadow class, register the 3D model that will cast the shadow, and then use the drawWithShadow method instead of the draw method (Figure 3-1).
  1. var wndMain: wnd@Wnd
  2. var drawMain: wnd@Draw
  3. var objCar: draw@Obj
  4. var texCarAlbedo: draw@Tex
  5. var objField: draw@Obj
  6. var texFieldAlbedo: draw@Tex
  7. var shadow: draw@Shadow
  8.  
  9. func main()
  10.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  11.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  12.  
  13.   do @objCar :: draw@makeObj("res/obj_car.knobj")
  14.   do @texCarAlbedo :: draw@makeTex("res/tex_car_albedo.png")
  15.   do draw@depth(true, true)
  16.   do @objField :: draw@makePlane()
  17.   do @objField.pos(80.0, 1.0, 80.0, 0.0, 0.0, 0.0, 38.0, 0.0, 38.0)
  18.   do @texFieldAlbedo :: draw@makeTex("res/tex_field_albedo.jpg")
  19.   do @shadow :: draw@makeShadow(1024, 1024)
  20.  
  21.   while(wnd@act())
  22.     do @shadow.beginRecord(0.0, 0.0, 0.0, 5.0)
  23.     do @shadow.add(@objCar, 0, 0.0)
  24.     do @shadow.endRecord()
  25.  
  26.     do @objField.drawWithShadow(0, 0.0, @texFieldAlbedo, null, null, @shadow)
  27.     do @objCar.drawWithShadow(0, 0.0, @texCarAlbedo, null, null, @shadow)
  28.     do draw@render(60)
  29.   end while
  30. end func
Figure 3-1: racing_game2.kn
Don't forget to change line 27 as well.
A planar model representing the ground is created in line 16, and its position and size are adjusted in line 17. The arguments in parentheses in the pos method are, in order, expansion X, expansion Y, expansion Z, rotation X, rotation Y, rotation Z, position X, position Y, position Z.
In line 19, a shadow buffer is created, and in lines 22 to 24, the model that will cast the shadow is registered. To register a shadow, add models with the add method in the beginRecord and endRecord enclosures as shown here.
In the parentheses of beginRecord in line 22, the range for calculating the shadow is specified as a sphere. The arguments are, in order, sphere position X, position Y, position Z, and sphere radius. If you make the sphere too large, the shadows will be coarse, and if you make it too small, the shadows outside the sphere will not be drawn.
When you run it, it will look like Figure 3-2.
Drawing Ground And Shadows
Figure 3-2: Drawing Ground And Shadows

4Moving Car And Camera

Finally, it's time to move the car.
As in Side View Action Game, the position of the car is managed by a class that inherits from game@Rect (Figure 4-1).
  1. class Car(game@Rect)
  2.   +var angle: float
  3. end class
  4.  
  5. var wndMain: wnd@Wnd
  6. var drawMain: wnd@Draw
  7. var objCar: draw@Obj
  8. var texCarAlbedo: draw@Tex
  9. var objField: draw@Obj
  10. var texFieldAlbedo: draw@Tex
  11. var shadow: draw@Shadow
  12. var car: @Car
  13.  
  14. func main()
  15.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  16.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  17.  
  18.   do @objCar :: draw@makeObj("res/obj_car.knobj")
  19.   do @texCarAlbedo :: draw@makeTex("res/tex_car_albedo.png")
  20.   do draw@depth(true, true)
  21.   do @objField :: draw@makePlane()
  22.   do @objField.pos(80.0, 1.0, 80.0, 0.0, 0.0, 0.0, 38.0, 0.0, 38.0)
  23.   do @texFieldAlbedo :: draw@makeTex("res/tex_field_albedo.jpg")
  24.   do @shadow :: draw@makeShadow(1024, 1024)
  25.  
  26.   do @car :: #@Car
  27.   do @car.x :: 4.0 * 2.0
  28.   do @car.y :: 4.0 * 12.0
  29.   do @car.width :: 1.5
  30.   do @car.height :: 1.5
  31.   do @car.angle :: 0.0
  32.  
  33.   while(wnd@act())
  34.     if(input@pad(0, %a) >= 1)
  35.       do @car.veloX :- lib@sin(@car.angle) * 0.01
  36.       do @car.veloY :- lib@cos(@car.angle) * 0.01
  37.     end if
  38.     if(input@pad(0, %b) >= 1)
  39.       do @car.veloX :+ lib@sin(@car.angle) * 0.01
  40.       do @car.veloY :+ lib@cos(@car.angle) * 0.01
  41.     end if
  42.     if(input@pad(0, %left) >= 1)
  43.       do @car.angle :+ lib@dist(@car.veloX, @car.veloY, 0.0, 0.0) * 0.15
  44.     end if
  45.     if(input@pad(0, %right) >= 1)
  46.       do @car.angle :- lib@dist(@car.veloX, @car.veloY, 0.0, 0.0) * 0.15
  47.     end if
  48.     do @car.move(0.25)
  49.     do @car.backFriction(0.003)
  50.     do @car.update()
  51.     do draw@camera(@car.x + lib@sin(@car.angle) * 20.0, 5.0, @car.y + lib@cos(@car.angle) * 20.0, @car.x, 1.0, @car.y, 0.0, 1.0, 0.0)
  52.     do @objCar.pos(1.0, 1.0, 1.0, 0.0, @car.angle, 0.0, @car.x, 0.0, @car.y)
  53.  
  54.     do @shadow.beginRecord(@car.x, 0.0, @car.y, 5.0)
  55.     do @shadow.add(@objCar, 0, 0.0)
  56.     do @shadow.endRecord()
  57.  
  58.     do @objField.drawWithShadow(0, 0.0, @texFieldAlbedo, null, null, @shadow)
  59.     do @objCar.drawWithShadow(0, 0.0, @texCarAlbedo, null, null, @shadow)
  60.     do draw@render(60)
  61.   end while
  62. end func
Figure 4-1: racing_game3.kn
Don't forget to change line 54 as well.
The angle in the 2nd line represents the angle that the car is facing. Lines 26 to 31 create an instance of the Car class, and lines 34 to 50 process the movement of the car. This is the same as for the Side View Action Game.
The camera is set up in line 51. The arguments in parentheses for draw@camera are, in order, camera position X, position Y, position Z, camera gazing point X, gazing point Y, gazing point Z, and direction X that the top of the camera will face, direction Y, and direction Z. Here I have set up the camera so that it always shows the car from diagonally behind it.
The position of the car is set in line 52. In line 54, I limit the creation of shadows to the area around the car, which is the area often seen by the camera.
When you run it, it will look like Figure 4-2.
Moving Car
Figure 4-2: Moving Car
Press the A button on the gamepad or the Z key on the keyboard to start the car. Press B or X key to move backward. Press the Left/Right button or the Left/Right key while the car is running to turn the car around.

5Drawing Sky And Walls

Finally, let's draw the sky and walls. Both the sky and the walls will be drawn as cubes.
As in the Side View Action Game, the walls are mapped in a text file and handled by the game@Map class. The collision detection is the same as in the Side View Action Game (Figure 5-1).
  1. class Car(game@Rect)
  2.   +var angle: float
  3. end class
  4.  
  5. var wndMain: wnd@Wnd
  6. var drawMain: wnd@Draw
  7. var objCar: draw@Obj
  8. var texCarAlbedo: draw@Tex
  9. var objField: draw@Obj
  10. var texFieldAlbedo: draw@Tex
  11. var shadow: draw@Shadow
  12. var car: @Car
  13. var objSky: draw@Obj
  14. var texSkyAlbedo: draw@Tex
  15. var objWall: draw@Obj
  16. var texWallAlbedo: draw@Tex
  17. var map: game@Map
  18.  
  19. func main()
  20.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  21.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  22.  
  23.   do @objCar :: draw@makeObj("res/obj_car.knobj")
  24.   do @texCarAlbedo :: draw@makeTex("res/tex_car_albedo.png")
  25.   do draw@depth(true, true)
  26.   do @objField :: draw@makePlane()
  27.   do @objField.pos(80.0, 1.0, 80.0, 0.0, 0.0, 0.0, 38.0, 0.0, 38.0)
  28.   do @texFieldAlbedo :: draw@makeTex("res/tex_field_albedo.jpg")
  29.   do @shadow :: draw@makeShadow(1024, 1024)
  30.   do @objSky :: draw@makeBox()
  31.   do @objSky.pos(320.0, 16.0, -320.0, 0.0, 0.0, 0.0, 40.0, 7.0, 40.0)
  32.   do @texSkyAlbedo :: draw@makeTex("res/tex_sky_albedo.png")
  33.   do @objWall :: draw@makeBox()
  34.   do @texWallAlbedo :: draw@makeTex("res/tex_wall_albedo.jpg")
  35.   do @map :: game@makeMap("res/map_sample_racing.txt", 4.0, 4.0)
  36.  
  37.   do @car :: #@Car
  38.   do @car.x :: 4.0 * 2.0
  39.   do @car.y :: 4.0 * 12.0
  40.   do @car.width :: 1.5
  41.   do @car.height :: 1.5
  42.   do @car.angle :: 0.0
  43.  
  44.   while(wnd@act())
  45.     if(input@pad(0, %a) >= 1)
  46.       do @car.veloX :- lib@sin(@car.angle) * 0.01
  47.       do @car.veloY :- lib@cos(@car.angle) * 0.01
  48.     end if
  49.     if(input@pad(0, %b) >= 1)
  50.       do @car.veloX :+ lib@sin(@car.angle) * 0.01
  51.       do @car.veloY :+ lib@cos(@car.angle) * 0.01
  52.     end if
  53.     if(input@pad(0, %left) >= 1)
  54.       do @car.angle :+ lib@dist(@car.veloX, @car.veloY, 0.0, 0.0) * 0.15
  55.     end if
  56.     if(input@pad(0, %right) >= 1)
  57.       do @car.angle :- lib@dist(@car.veloX, @car.veloY, 0.0, 0.0) * 0.15
  58.     end if
  59.     do @car.move(0.25)
  60.     do @car.backFriction(0.003)
  61.     do game@hitMapRect(@map, @car, getChipInfo, null)
  62.     do @car.update()
  63.     do draw@camera(@car.x + lib@sin(@car.angle) * 20.0, 5.0, @car.y + lib@cos(@car.angle) * 20.0, @car.x, 1.0, @car.y, 0.0, 1.0, 0.0)
  64.     do @objCar.pos(1.0, 1.0, 1.0, 0.0, @car.angle, 0.0, @car.x, 0.0, @car.y)
  65.  
  66.     do @shadow.beginRecord(@car.x, 0.0, @car.y, 5.0)
  67.     do @shadow.add(@objCar, 0, 0.0)
  68.     do @shadow.endRecord()
  69.  
  70.     do @objField.drawWithShadow(0, 0.0, @texFieldAlbedo, null, null, @shadow)
  71.     do @objCar.drawWithShadow(0, 0.0, @texCarAlbedo, null, null, @shadow)
  72.     do @objSky.drawFlat(0, 0.0, @texSkyAlbedo)
  73.  
  74.     for z(0, 19)
  75.       for x(0, 19)
  76.         var chip: int :: @map.get(x, z)
  77.         if(chip >= 0)
  78.           do @objWall.pos(4.0, 2.0, 4.0, 0.0, 0.0, 0.0, x $ float * 4.0, 1.0, z $ float * 4.0)
  79.           do @objWall.drawWithShadow(0, 0.0, @texWallAlbedo, null, null, @shadow)
  80.         end if
  81.       end for
  82.     end for
  83.     do draw@render(60)
  84.   end while
  85.  
  86.   func getChipInfo(chip: int, info: game@ChipInfo)
  87.     if(chip = 0)
  88.       do info.shape :: %rect
  89.       do info.solidFriction :: 0.5
  90.       do info.repulsion :: 0.3
  91.     else
  92.       do info.shape :: %none
  93.     end if
  94.   end func
  95. end func
Figure 5-1: racing_game4.kn
In lines 30 and 33, cube models is created using the draw@makeBox function.
The sky is represented by a giant cube that wraps around the world. For this reason, the surface of the cube polygon needs to face inward, and I have flipped it over in line 31 by setting the Z axis scaling to negative.
When you run it, it will look like Figure 5-2.
Drawing Sky And Walls
Figure 5-2: Drawing Sky And Walls
The car will hit the wall properly. You can play with the car to make it run a lap faster, or modify the stages and programs.
That's it, the 3D racing game is complete. Next time, let's try competitive programming.
1634933827enf